feat(css/lints): Add CSS linter (#3765)

This commit is contained in:
Pig Fang 2022-02-27 23:31:19 +08:00 committed by GitHub
parent e475a83ebc
commit 66c6cae8dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 1712 additions and 2 deletions

View File

@ -137,6 +137,8 @@ jobs:
os: windows-latest
- crate: swc_css_codegen_macros
os: ubuntu-latest
- crate: swc_css_lints
os: ubuntu-latest
- crate: swc_css_minifier
os: ubuntu-latest
- crate: swc_css_parser

17
Cargo.lock generated
View File

@ -2866,6 +2866,23 @@ dependencies = [
"syn",
]
[[package]]
name = "swc_css_lints"
version = "0.1.0"
dependencies = [
"auto_impl",
"parking_lot 0.12.0",
"rayon",
"serde",
"serde_json",
"swc_atoms",
"swc_common",
"swc_css_ast",
"swc_css_parser",
"swc_css_visit",
"testing",
]
[[package]]
name = "swc_css_minifier"
version = "0.56.0"

View File

@ -4,6 +4,7 @@ members = [
"crates/node",
"crates/swc_cli",
"crates/swc_css",
"crates/swc_css_lints",
"crates/swc_ecmascript",
"crates/swc_ecma_lints",
"crates/swc_ecma_quote",
@ -40,5 +41,5 @@ opt-level = 3
opt-level = 3
[patch.crates-io]
cranelift-codegen = {git = "https://github.com/kdy1/wasmtime", branch = "tls"}
cranelift-entity = {git = "https://github.com/kdy1/wasmtime", branch = "tls"}
cranelift-codegen = { git = "https://github.com/kdy1/wasmtime", branch = "tls" }
cranelift-entity = { git = "https://github.com/kdy1/wasmtime", branch = "tls" }

View File

@ -0,0 +1,26 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "CSS linter"
documentation = "https://rustdoc.swc.rs/swc_css_lints/"
edition = "2021"
license = "Apache-2.0"
name = "swc_css_lints"
repository = "https://github.com/swc-project/swc.git"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
auto_impl = "0.5.0"
parking_lot = "0.12.0"
rayon = "1.5.1"
serde = {version = "1.0.133", features = ["derive"]}
swc_atoms = {version = "0.2.9", path = "../swc_atoms"}
swc_common = {version = "0.17.0", path = "../swc_common"}
swc_css_ast = {version = "0.86.0", path = "../swc_css_ast"}
swc_css_visit = {version = "0.85.0", path = "../swc_css_visit"}
[dev-dependencies]
serde_json = "1.0.79"
swc_css_parser = {version = "0.92.0", path = "../swc_css_parser"}
testing = {version = "0.18.0", path = "../testing"}

View File

@ -0,0 +1,112 @@
use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use crate::rules::{
at_rule_no_unknown::AtRuleNoUnknownConfig, color_hex_length::ColorHexLengthConfig,
no_invalid_position_at_import_rule::NoInvalidPositionAtImportRuleConfig,
unit_no_unknown::UnitNoUnknownConfig,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LintRuleReaction {
Off,
Warning,
Error,
}
impl Default for LintRuleReaction {
fn default() -> Self {
Self::Off
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(untagged)]
enum LintRuleLevel {
Str(LintRuleReaction),
Number(u8),
}
impl Default for LintRuleLevel {
fn default() -> Self {
Self::Str(LintRuleReaction::Off)
}
}
impl From<LintRuleLevel> for LintRuleReaction {
fn from(level: LintRuleLevel) -> Self {
match level {
LintRuleLevel::Str(level) => level,
LintRuleLevel::Number(level) => match level {
1 => LintRuleReaction::Warning,
2 => LintRuleReaction::Error,
_ => LintRuleReaction::Off,
},
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RuleConfig<T: Debug + Clone + Serialize + Default>(
#[serde(default)] LintRuleLevel,
#[serde(default)] T,
);
impl<T: Debug + Clone + Serialize + Default> RuleConfig<T> {
#[inline]
pub(crate) fn get_rule_reaction(&self) -> LintRuleReaction {
self.0.into()
}
#[inline]
pub(crate) fn get_rule_config(&self) -> &T {
&self.1
}
pub(crate) fn is_enabled(&self) -> bool {
!matches!(self.get_rule_reaction(), LintRuleReaction::Off)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "kebab-case")]
pub struct RulesConfig {
#[serde(default, alias = "blockNoEmpty")]
pub block_no_empty: RuleConfig<()>,
#[serde(default, alias = "atRuleNoUnknown")]
pub at_rule_no_unknown: RuleConfig<AtRuleNoUnknownConfig>,
#[serde(default, alias = "noEmptySource")]
pub no_empty_source: RuleConfig<()>,
#[serde(default, alias = "declarationNoImportant")]
pub declaration_no_important: RuleConfig<()>,
#[serde(default, alias = "keyframeDeclarationNoImportant")]
pub keyframe_declaration_no_important: RuleConfig<()>,
#[serde(default, alias = "noInvalidPositionAtImportRule")]
pub no_invalid_position_at_import_rule: RuleConfig<NoInvalidPositionAtImportRuleConfig>,
#[serde(default, alias = "selectorMaxClass")]
pub selector_max_class: RuleConfig<Option<usize>>,
#[serde(default, alias = "colorHexLength")]
pub color_hex_length: RuleConfig<ColorHexLengthConfig>,
#[serde(default, alias = "colorNoInvalidHex")]
pub color_no_invalid_hex: RuleConfig<()>,
#[serde(default, alias = "unitNoUnknown")]
pub unit_no_unknown: RuleConfig<UnitNoUnknownConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LintConfig {
#[serde(default)]
pub rules: RulesConfig,
}

View File

@ -0,0 +1,7 @@
mod config;
mod rule;
mod rules;
pub use config::LintConfig;
pub use rule::LintRule;
pub use rules::{get_rules, LintParams};

View File

@ -0,0 +1,85 @@
use std::{fmt::Debug, sync::Arc};
use auto_impl::auto_impl;
use parking_lot::Mutex;
use rayon::prelude::*;
use swc_common::errors::{Diagnostic, DiagnosticBuilder, Emitter, Handler, HANDLER};
use swc_css_ast::Stylesheet;
use swc_css_visit::{Visit, VisitWith};
/// A lint rule.
///
/// # Implementation notes
///
/// Must report error to [swc_common::HANDLER]
#[auto_impl(Box, &mut)]
pub trait LintRule: Debug + Send + Sync {
fn lint_stylesheet(&mut self, stylesheet: &Stylesheet);
}
/// This preserves the order of errors.
impl<R> LintRule for Vec<R>
where
R: LintRule,
{
fn lint_stylesheet(&mut self, stylesheet: &Stylesheet) {
if cfg!(target_arch = "wasm32") {
for rule in self {
rule.lint_stylesheet(stylesheet);
}
} else {
let errors = self
.par_iter_mut()
.flat_map(|rule| {
let emitter = Capturing::default();
{
let handler = Handler::with_emitter(true, false, Box::new(emitter.clone()));
HANDLER.set(&handler, || {
rule.lint_stylesheet(stylesheet);
});
}
Arc::try_unwrap(emitter.errors).unwrap().into_inner()
})
.collect::<Vec<_>>();
HANDLER.with(|handler| {
for error in errors {
DiagnosticBuilder::new_diagnostic(&handler, error).emit();
}
});
}
}
}
#[derive(Default, Clone)]
struct Capturing {
errors: Arc<Mutex<Vec<Diagnostic>>>,
}
impl Emitter for Capturing {
fn emit(&mut self, db: &DiagnosticBuilder<'_>) {
self.errors.lock().push((**db).clone());
}
}
pub(crate) fn visitor_rule<V>(v: V) -> Box<dyn LintRule>
where
V: 'static + Send + Sync + Visit + Default + Debug,
{
Box::new(VisitorRule(v))
}
#[derive(Debug)]
struct VisitorRule<V>(V)
where
V: Send + Sync + Visit;
impl<V> LintRule for VisitorRule<V>
where
V: Send + Sync + Visit + Debug,
{
fn lint_stylesheet(&mut self, stylesheet: &Stylesheet) {
stylesheet.visit_with(&mut self.0);
}
}

View File

@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
use swc_common::{errors::HANDLER, Spanned};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AtRuleNoUnknownConfig {
ignore_at_rules: Option<Vec<String>>,
}
pub fn at_rule_no_unknown(config: &RuleConfig<AtRuleNoUnknownConfig>) -> Box<dyn LintRule> {
visitor_rule(AtRuleNoUnknown {
reaction: config.get_rule_reaction(),
ignored: config
.get_rule_config()
.ignore_at_rules
.clone()
.unwrap_or_default(),
})
}
#[derive(Debug, Default)]
struct AtRuleNoUnknown {
reaction: LintRuleReaction,
ignored: Vec<String>,
}
impl Visit for AtRuleNoUnknown {
fn visit_unknown_at_rule(&mut self, unknown_at_rule: &UnknownAtRule) {
let name = match &unknown_at_rule.name {
AtRuleName::DashedIdent(dashed_ident) => &dashed_ident.value,
AtRuleName::Ident(ident) => &ident.value,
};
if self.ignored.iter().all(|item| name != item) {
let message = format!("Unexpected unknown at-rule \"@{}\".", name);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler
.struct_span_err(unknown_at_rule.name.span(), &message)
.emit(),
LintRuleReaction::Warning => handler
.struct_span_warn(unknown_at_rule.name.span(), &message)
.emit(),
_ => {}
});
}
unknown_at_rule.visit_children_with(self);
}
}

View File

@ -0,0 +1,39 @@
use swc_common::errors::HANDLER;
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn block_no_empty(config: &RuleConfig<()>) -> Box<dyn LintRule> {
visitor_rule(BlockNoEmpty {
reaction: config.get_rule_reaction(),
})
}
const MESSAGE: &str = "Unexpected empty block.";
#[derive(Debug, Default)]
struct BlockNoEmpty {
reaction: LintRuleReaction,
}
impl Visit for BlockNoEmpty {
fn visit_simple_block(&mut self, simple_block: &SimpleBlock) {
if simple_block.value.is_empty() {
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => {
handler.struct_span_err(simple_block.span, MESSAGE).emit()
}
LintRuleReaction::Warning => {
handler.struct_span_warn(simple_block.span, MESSAGE).emit()
}
_ => {}
});
}
simple_block.visit_children_with(self);
}
}

View File

@ -0,0 +1,111 @@
use serde::{Deserialize, Serialize};
use swc_common::errors::HANDLER;
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub type ColorHexLengthConfig = Option<HexForm>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum HexForm {
Long,
Short,
}
impl Default for HexForm {
fn default() -> Self {
Self::Long
}
}
pub fn color_hex_length(config: &RuleConfig<ColorHexLengthConfig>) -> Box<dyn LintRule> {
visitor_rule(ColorHexLength {
reaction: config.get_rule_reaction(),
form: config.get_rule_config().clone().unwrap_or_default(),
})
}
#[derive(Debug, Default)]
struct ColorHexLength {
reaction: LintRuleReaction,
form: HexForm,
}
impl ColorHexLength {
fn build_message(&self, actual: &str, expected: &str) -> String {
format!(
"Hex color value '#{}' should be written into: '#{}'.",
actual, expected
)
}
}
impl Visit for ColorHexLength {
fn visit_hex_color(&mut self, hex_color: &HexColor) {
match self.form {
HexForm::Long => {
if let Some(lengthened) = lengthen(&hex_color.value) {
let message = self.build_message(&hex_color.value, &lengthened);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => {
handler.struct_span_err(hex_color.span, &message).emit()
}
LintRuleReaction::Warning => {
handler.struct_span_warn(hex_color.span, &message).emit()
}
_ => {}
});
}
}
HexForm::Short => {
if let Some(shortened) = shorten(&hex_color.value) {
let message = self.build_message(&hex_color.value, &shortened);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => {
handler.struct_span_err(hex_color.span, &message).emit()
}
LintRuleReaction::Warning => {
handler.struct_span_warn(hex_color.span, &message).emit()
}
_ => {}
});
}
}
}
hex_color.visit_children_with(self);
}
}
fn shorten(hex: &str) -> Option<String> {
let chars = hex.chars().collect::<Vec<_>>();
match &*chars {
[c1, c2, c3, c4, c5, c6] if c1 == c2 && c3 == c4 && c5 == c6 => {
Some(format!("{c1}{c3}{c5}"))
}
[c1, c2, c3, c4, c5, c6, c7, c8] if c1 == c2 && c3 == c4 && c5 == c6 && c7 == c8 => {
Some(format!("{c1}{c3}{c5}{c7}"))
}
_ => None,
}
}
fn lengthen(hex: &str) -> Option<String> {
let chars = hex.chars().collect::<Vec<_>>();
match &*chars {
[c1, c2, c3] => Some(format!("{r}{r}{g}{g}{b}{b}", r = c1, g = c2, b = c3)),
[c1, c2, c3, c4] => Some(format!(
"{r}{r}{g}{g}{b}{b}{a}{a}",
r = c1,
g = c2,
b = c3,
a = c4
)),
_ => None,
}
}

View File

@ -0,0 +1,41 @@
use swc_common::errors::HANDLER;
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn color_no_invalid_hex(config: &RuleConfig<()>) -> Box<dyn LintRule> {
visitor_rule(ColorNoInvalidHex {
reaction: config.get_rule_reaction(),
})
}
#[derive(Debug, Default)]
struct ColorNoInvalidHex {
reaction: LintRuleReaction,
}
impl Visit for ColorNoInvalidHex {
fn visit_hex_color(&mut self, hex_color: &HexColor) {
let HexColor { span, value, .. } = hex_color;
let length = value.len();
if (length == 3 || length == 4 || length == 6 || length == 8)
&& value.chars().all(|c| c.is_ascii_hexdigit())
{
hex_color.visit_children_with(self);
return;
}
let message = format!("Unexpected invalid hex color '#{}'.", value);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler.struct_span_err(*span, &message).emit(),
LintRuleReaction::Warning => handler.struct_span_warn(*span, &message).emit(),
_ => {}
});
hex_color.visit_children_with(self);
}
}

View File

@ -0,0 +1,56 @@
use swc_common::{errors::HANDLER, Span};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn declaration_no_important(config: &RuleConfig<()>) -> Box<dyn LintRule> {
visitor_rule(DeclarationNoImportant {
reaction: config.get_rule_reaction(),
keyframe_rules: vec![],
})
}
const MESSAGE: &str = "Unexpected '!important'.";
#[derive(Debug, Default)]
struct DeclarationNoImportant {
reaction: LintRuleReaction,
// rule interal
keyframe_rules: Vec<Span>,
}
impl Visit for DeclarationNoImportant {
fn visit_keyframes_rule(&mut self, keyframes_rule: &KeyframesRule) {
self.keyframe_rules.push(keyframes_rule.span);
keyframes_rule.visit_children_with(self);
self.keyframe_rules.pop();
}
fn visit_important_flag(&mut self, important_flag: &ImportantFlag) {
match self.keyframe_rules.last() {
Some(span) if span.contains(important_flag.span) => {
// This rule doesn't check `!important` flag inside `@keyframe`.
}
_ => {
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => {
handler.struct_span_err(important_flag.span, MESSAGE).emit()
}
LintRuleReaction::Warning => handler
.struct_span_warn(important_flag.span, MESSAGE)
.emit(),
_ => {}
});
}
}
important_flag.visit_children_with(self);
}
}

View File

@ -0,0 +1,54 @@
use swc_common::{errors::HANDLER, Span};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn keyframe_declaration_no_important(config: &RuleConfig<()>) -> Box<dyn LintRule> {
visitor_rule(KeyframeDeclarationNoImportant {
reaction: config.get_rule_reaction(),
keyframe_rules: vec![],
})
}
const MESSAGE: &str = "Unexpected '!important'.";
#[derive(Debug, Default)]
struct KeyframeDeclarationNoImportant {
reaction: LintRuleReaction,
// rule interal
keyframe_rules: Vec<Span>,
}
impl Visit for KeyframeDeclarationNoImportant {
fn visit_keyframes_rule(&mut self, keyframes_rule: &KeyframesRule) {
self.keyframe_rules.push(keyframes_rule.span);
keyframes_rule.visit_children_with(self);
self.keyframe_rules.pop();
}
fn visit_important_flag(&mut self, important_flag: &ImportantFlag) {
match self.keyframe_rules.last() {
Some(span) if span.contains(important_flag.span) => {
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => {
handler.struct_span_err(important_flag.span, MESSAGE).emit()
}
LintRuleReaction::Warning => handler
.struct_span_warn(important_flag.span, MESSAGE)
.emit(),
_ => {}
});
}
_ => {}
}
important_flag.visit_children_with(self);
}
}

View File

@ -0,0 +1,72 @@
use crate::{
config::LintConfig,
rule::LintRule,
rules::{
at_rule_no_unknown::at_rule_no_unknown, block_no_empty::block_no_empty,
color_hex_length::color_hex_length, color_no_invalid_hex::color_no_invalid_hex,
declaration_no_important::declaration_no_important,
keyframe_declaration_no_important::keyframe_declaration_no_important,
no_empty_source::no_empty_source,
no_invalid_position_at_import_rule::no_invalid_position_at_import_rule,
selector_max_class::selector_max_class, unit_no_unknown::unit_no_unknown,
},
};
pub mod at_rule_no_unknown;
pub mod block_no_empty;
pub mod color_hex_length;
pub mod color_no_invalid_hex;
pub mod declaration_no_important;
pub mod keyframe_declaration_no_important;
pub mod no_empty_source;
pub mod no_invalid_position_at_import_rule;
pub mod selector_max_class;
pub mod unit_no_unknown;
pub struct LintParams<'a> {
pub lint_config: &'a LintConfig,
}
pub fn get_rules(LintParams { lint_config }: &LintParams) -> Vec<Box<dyn LintRule>> {
let mut rules = vec![];
let rules_config = &lint_config.rules;
if rules_config.block_no_empty.is_enabled() {
rules.push(block_no_empty(&rules_config.block_no_empty));
}
if rules_config.at_rule_no_unknown.is_enabled() {
rules.push(at_rule_no_unknown(&rules_config.at_rule_no_unknown));
}
if rules_config.no_empty_source.is_enabled() {
rules.push(no_empty_source(&rules_config.no_empty_source));
}
if rules_config.declaration_no_important.is_enabled() {
rules.push(declaration_no_important(
&rules_config.declaration_no_important,
));
}
if rules_config.keyframe_declaration_no_important.is_enabled() {
rules.push(keyframe_declaration_no_important(
&rules_config.keyframe_declaration_no_important,
));
}
if rules_config.no_invalid_position_at_import_rule.is_enabled() {
rules.push(no_invalid_position_at_import_rule(
&rules_config.no_invalid_position_at_import_rule,
));
}
if rules_config.selector_max_class.is_enabled() {
rules.push(selector_max_class(&rules_config.selector_max_class));
}
if rules_config.color_hex_length.is_enabled() {
rules.push(color_hex_length(&rules_config.color_hex_length));
}
if rules_config.color_no_invalid_hex.is_enabled() {
rules.push(color_no_invalid_hex(&rules_config.color_no_invalid_hex));
}
if rules_config.unit_no_unknown.is_enabled() {
rules.push(unit_no_unknown(&rules_config.unit_no_unknown));
}
rules
}

View File

@ -0,0 +1,39 @@
use swc_common::errors::HANDLER;
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn no_empty_source(config: &RuleConfig<()>) -> Box<dyn LintRule> {
visitor_rule(NoEmptySource {
reaction: config.get_rule_reaction(),
})
}
const MESSAGE: &str = "Unexpected empty source.";
#[derive(Debug, Default)]
struct NoEmptySource {
reaction: LintRuleReaction,
}
impl Visit for NoEmptySource {
fn visit_stylesheet(&mut self, stylesheet: &Stylesheet) {
// TODO: we should allow comments here,
// but parser doesn't handle comments currently.
if stylesheet.rules.is_empty() {
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler.struct_span_err(stylesheet.span, MESSAGE).emit(),
LintRuleReaction::Warning => {
handler.struct_span_warn(stylesheet.span, MESSAGE).emit()
}
_ => {}
});
}
stylesheet.visit_children_with(self);
}
}

View File

@ -0,0 +1,70 @@
use serde::{Deserialize, Serialize};
use swc_common::{errors::HANDLER, Spanned};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
const MESSAGE: &str = "Unexpected invalid position '@import' rule.";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoInvalidPositionAtImportRuleConfig {
ignore_at_rules: Option<Vec<String>>,
}
pub fn no_invalid_position_at_import_rule(
config: &RuleConfig<NoInvalidPositionAtImportRuleConfig>,
) -> Box<dyn LintRule> {
visitor_rule(NoInvalidPositionAtImportRule {
reaction: config.get_rule_reaction(),
ignored: config
.get_rule_config()
.ignore_at_rules
.clone()
.unwrap_or_default(),
})
}
#[derive(Debug, Default)]
struct NoInvalidPositionAtImportRule {
reaction: LintRuleReaction,
ignored: Vec<String>,
}
impl Visit for NoInvalidPositionAtImportRule {
fn visit_stylesheet(&mut self, stylesheet: &Stylesheet) {
stylesheet.rules.iter().fold(false, |seen, rule| {
if seen && matches!(rule, Rule::AtRule(AtRule::Import(..))) {
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler.struct_span_err(rule.span(), MESSAGE).emit(),
LintRuleReaction::Warning => {
handler.struct_span_warn(rule.span(), MESSAGE).emit()
}
_ => {}
});
}
match rule {
Rule::AtRule(AtRule::Charset(..) | AtRule::Import(..)) => seen,
Rule::AtRule(AtRule::Unknown(UnknownAtRule { name, .. })) => {
let name = match name {
AtRuleName::DashedIdent(dashed_ident) => &dashed_ident.value,
AtRuleName::Ident(ident) => &ident.value,
};
if self.ignored.iter().any(|item| name == item) {
seen
} else {
true
}
}
_ => true,
}
});
stylesheet.visit_children_with(self);
}
}

View File

@ -0,0 +1,67 @@
use swc_common::errors::HANDLER;
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
pub fn selector_max_class(config: &RuleConfig<Option<usize>>) -> Box<dyn LintRule> {
visitor_rule(SelectorMaxClass {
reaction: config.get_rule_reaction(),
max: config.get_rule_config().unwrap_or(3),
})
}
#[derive(Debug, Default)]
struct SelectorMaxClass {
reaction: LintRuleReaction,
max: usize,
}
impl SelectorMaxClass {
fn build_message(&self, count: usize) -> String {
let class = if self.max == 1 { "class" } else { "classes" };
format!(
"Expected selector to have no more than {} {}, but {} actually.",
self.max, class, count
)
}
}
impl Visit for SelectorMaxClass {
fn visit_complex_selector(&mut self, complex_selector: &ComplexSelector) {
let count = complex_selector
.children
.iter()
.filter_map(|selector| match selector {
ComplexSelectorChildren::CompoundSelector(compound_selector) => {
Some(compound_selector)
}
_ => None,
})
.flat_map(|selector| {
selector
.subclass_selectors
.iter()
.filter(|selector| matches!(selector, SubclassSelector::Class(..)))
})
.count();
if count > self.max {
let message = self.build_message(count);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler
.struct_span_err(complex_selector.span, &message)
.emit(),
LintRuleReaction::Warning => handler
.struct_span_warn(complex_selector.span, &message)
.emit(),
_ => {}
});
}
complex_selector.visit_children_with(self);
}
}

View File

@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
use swc_common::{errors::HANDLER, Spanned};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, LintRule},
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnitNoUnknownConfig {
ignore_units: Option<Vec<String>>,
}
pub fn unit_no_unknown(config: &RuleConfig<UnitNoUnknownConfig>) -> Box<dyn LintRule> {
visitor_rule(UnitNoUnknown {
reaction: config.get_rule_reaction(),
ignored_units: config
.get_rule_config()
.ignore_units
.clone()
.unwrap_or_default(),
})
}
#[derive(Debug, Default)]
struct UnitNoUnknown {
reaction: LintRuleReaction,
ignored_units: Vec<String>,
}
impl Visit for UnitNoUnknown {
fn visit_unknown_dimension(&mut self, unknown_dimension: &UnknownDimension) {
let unit = &unknown_dimension.unit.value;
if self.ignored_units.iter().all(|item| unit != item) {
let message = format!("Unexpected unknown unit \"{}\".", unit);
HANDLER.with(|handler| match self.reaction {
LintRuleReaction::Error => handler
.struct_span_err(unknown_dimension.unit.span(), &message)
.emit(),
LintRuleReaction::Warning => handler
.struct_span_warn(unknown_dimension.unit.span(), &message)
.emit(),
_ => {}
});
}
unknown_dimension.visit_children_with(self);
}
}

View File

@ -0,0 +1,93 @@
#![allow(clippy::needless_update)]
use std::{fs, path::PathBuf};
use swc_common::{errors::HANDLER, input::SourceFileInput};
use swc_css_lints::{get_rules, LintConfig, LintParams, LintRule};
use swc_css_parser::{
lexer::Lexer,
parser::{Parser, ParserConfig},
};
#[testing::fixture("tests/rules/pass/**/input.css")]
fn pass(input: PathBuf) {
let config_path = input.parent().unwrap().join("config.json");
let lint_config =
serde_json::from_str::<LintConfig>(&fs::read_to_string(config_path).unwrap()).unwrap();
testing::run_test2(false, |cm, handler| -> Result<(), _> {
let config = ParserConfig {
..Default::default()
};
let fm = cm.load_file(&input).unwrap();
let lexer = Lexer::new(SourceFileInput::from(&*fm), config);
let mut parser = Parser::new(lexer, config);
let stylesheet = match parser.parse_all() {
Ok(stylesheet) => stylesheet,
Err(err) => {
err.to_diagnostics(&handler).emit();
panic!();
}
};
let mut rules = get_rules(&LintParams {
lint_config: &lint_config,
});
HANDLER.set(&handler, || {
rules.lint_stylesheet(&stylesheet);
});
if handler.has_errors() {
return Err(());
}
Ok(())
})
.unwrap();
}
#[testing::fixture("tests/rules/fail/**/input.css")]
fn fail(input: PathBuf) {
let stderr_path = input.parent().unwrap().join("output.stderr");
let config_path = input.parent().unwrap().join("config.json");
let lint_config =
serde_json::from_str::<LintConfig>(&fs::read_to_string(config_path).unwrap()).unwrap();
let stderr = testing::run_test2(false, |cm, handler| -> Result<(), _> {
let config = ParserConfig {
..Default::default()
};
let fm = cm.load_file(&input).unwrap();
let lexer = Lexer::new(SourceFileInput::from(&*fm), config);
let mut parser = Parser::new(lexer, config);
let stylesheet = match parser.parse_all() {
Ok(stylesheet) => stylesheet,
Err(err) => {
err.to_diagnostics(&handler).emit();
panic!();
}
};
let mut rules = get_rules(&LintParams {
lint_config: &lint_config,
});
HANDLER.set(&handler, || {
rules.lint_stylesheet(&stylesheet);
});
if !handler.has_errors() {
panic!("should error");
}
Err(())
})
.unwrap_err();
stderr.compare_to_file(&stderr_path).unwrap();
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"at-rule-no-unknown": ["error"]
}
}

View File

@ -0,0 +1,2 @@
@test {
}

View File

@ -0,0 +1,6 @@
error: Unexpected unknown at-rule "@test".
--> $DIR/tests/rules/fail/at-rule-no-unknown/default/input.css:1:2
|
1 | @test {
| ^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"at-rule-no-unknown": ["error", { "ignoreAtRules": ["custom"] }]
}
}

View File

@ -0,0 +1,2 @@
@test {
}

View File

@ -0,0 +1,6 @@
error: Unexpected unknown at-rule "@test".
--> $DIR/tests/rules/fail/at-rule-no-unknown/ignored/input.css:1:2
|
1 | @test {
| ^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"block-no-empty": ["error"]
}
}

View File

@ -0,0 +1,6 @@
error: Unexpected empty block.
--> $DIR/tests/rules/fail/block-no-empty/default/input.css:1:3
|
1 | a {}
| ^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-hex-length": ["error", "long"]
}
}

View File

@ -0,0 +1,5 @@
a {
color: #FFF;
color: #abcd;
color: #123;
}

View File

@ -0,0 +1,18 @@
error: Hex color value '#FFF' should be written into: '#FFFFFF'.
--> $DIR/tests/rules/fail/color-hex-length/long/input.css:2:12
|
2 | color: #FFF;
| ^^^^
error: Hex color value '#abcd' should be written into: '#aabbccdd'.
--> $DIR/tests/rules/fail/color-hex-length/long/input.css:3:12
|
3 | color: #abcd;
| ^^^^^
error: Hex color value '#123' should be written into: '#112233'.
--> $DIR/tests/rules/fail/color-hex-length/long/input.css:4:12
|
4 | color: #123;
| ^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-hex-length": ["error", "short"]
}
}

View File

@ -0,0 +1,5 @@
a {
color: #FFFFFF;
color: #aabbccdd;
color: #112233;
}

View File

@ -0,0 +1,18 @@
error: Hex color value '#FFFFFF' should be written into: '#FFF'.
--> $DIR/tests/rules/fail/color-hex-length/short/input.css:2:12
|
2 | color: #FFFFFF;
| ^^^^^^^
error: Hex color value '#aabbccdd' should be written into: '#abcd'.
--> $DIR/tests/rules/fail/color-hex-length/short/input.css:3:12
|
3 | color: #aabbccdd;
| ^^^^^^^^^
error: Hex color value '#112233' should be written into: '#123'.
--> $DIR/tests/rules/fail/color-hex-length/short/input.css:4:12
|
4 | color: #112233;
| ^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-no-invalid-hex": ["error"]
}
}

View File

@ -0,0 +1,15 @@
a {
color: #ababa;
}
a {
unknown: #00, #fff, #ababab;
}
a {
color: #12345abcdefg;
}
a {
color: #xyz;
}

View File

@ -0,0 +1,24 @@
error: Unexpected invalid hex color '#ababa'.
--> $DIR/tests/rules/fail/color-no-invalid-hex/input.css:2:12
|
2 | color: #ababa;
| ^^^^^^
error: Unexpected invalid hex color '#00'.
--> $DIR/tests/rules/fail/color-no-invalid-hex/input.css:6:14
|
6 | unknown: #00, #fff, #ababab;
| ^^^
error: Unexpected invalid hex color '#12345abcdefg'.
--> $DIR/tests/rules/fail/color-no-invalid-hex/input.css:10:12
|
10 | color: #12345abcdefg;
| ^^^^^^^^^^^^^
error: Unexpected invalid hex color '#xyz'.
--> $DIR/tests/rules/fail/color-no-invalid-hex/input.css:14:12
|
14 | color: #xyz;
| ^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,3 @@
a {
color: #000 !important;
}

View File

@ -0,0 +1,6 @@
error: Unexpected '!important'.
--> $DIR/tests/rules/fail/declaration-no-important/default/input.css:2:17
|
2 | color: #000 !important;
| ^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,13 @@
@keyframes foo {
from {
opacity: 0;
}
to {
/* should be ignored */
opacity: 1 !important;
}
}
a {
color: #111 !important;
}

View File

@ -0,0 +1,6 @@
error: Unexpected '!important'.
--> $DIR/tests/rules/fail/declaration-no-important/keyframe/input.css:12:17
|
12 | color: #111 !important;
| ^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"keyframe-declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,8 @@
@keyframes foo {
from {
opacity: 0;
}
to {
opacity: 1 !important;
}
}

View File

@ -0,0 +1,6 @@
error: Unexpected '!important'.
--> $DIR/tests/rules/fail/keyframe-declaration-no-important/input.css:6:20
|
6 | opacity: 1 !important;
| ^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-empty-source": ["error"]
}
}

View File

@ -0,0 +1 @@
/* comments */

View File

@ -0,0 +1,6 @@
error: Unexpected empty source.
--> $DIR/tests/rules/fail/no-empty-source/comments/input.css:1:15
|
1 | /* comments */
| ^

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-empty-source": ["error"]
}
}

View File

@ -0,0 +1,2 @@
error: Unexpected empty source.

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-position-at-import-rule": ["error"]
}
}

View File

@ -0,0 +1,5 @@
@media print {
}
@import "kumiko.css";
@import "reina.css";

View File

@ -0,0 +1,12 @@
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/default/after-other-at-rule/input.css:4:1
|
4 | @import "kumiko.css";
| ^^^^^^^^^^^^^^^^^^^^^
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/default/after-other-at-rule/input.css:5:1
|
5 | @import "reina.css";
| ^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-position-at-import-rule": ["error"]
}
}

View File

@ -0,0 +1,6 @@
@import "kumiko.css";
a {
}
@import "reina.css";

View File

@ -0,0 +1,6 @@
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/default/after-other-rule/input.css:6:1
|
6 | @import "reina.css";
| ^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-position-at-import-rule": ["error"]
}
}

View File

@ -0,0 +1,5 @@
a {
}
@import "kumiko.css";
@import "reina.css";

View File

@ -0,0 +1,12 @@
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/default/fail-all/input.css:4:1
|
4 | @import "kumiko.css";
| ^^^^^^^^^^^^^^^^^^^^^
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/default/fail-all/input.css:5:1
|
5 | @import "reina.css";
| ^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,8 @@
{
"rules": {
"no-invalid-position-at-import-rule": [
"error",
{ "ignoreAtRules": ["hibike"] }
]
}
}

View File

@ -0,0 +1,8 @@
@euphonium {
}
@import "kumiko.css";
@import "reina.css";
a {
}

View File

@ -0,0 +1,12 @@
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/ignored/input.css:4:1
|
4 | @import "kumiko.css";
| ^^^^^^^^^^^^^^^^^^^^^
error: Unexpected invalid position '@import' rule.
--> $DIR/tests/rules/fail/no-invalid-position-at-import-rule/ignored/input.css:5:1
|
5 | @import "reina.css";
| ^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"selector-max-class": ["error", 1]
}
}

View File

@ -0,0 +1,2 @@
.a.b {
}

View File

@ -0,0 +1,6 @@
error: Expected selector to have no more than 1 class, but 2 actually.
--> $DIR/tests/rules/fail/selector-max-class/custom/input.css:1:1
|
1 | .a.b {
| ^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"selector-max-class": ["error"]
}
}

View File

@ -0,0 +1,13 @@
.a.b.c.d {
}
.a .b .c .d {
}
@media print {
.a.b.c.d {
}
}
:not(.a.b.c.d) {
}

View File

@ -0,0 +1,24 @@
error: Expected selector to have no more than 3 classes, but 4 actually.
--> $DIR/tests/rules/fail/selector-max-class/default/input.css:1:1
|
1 | .a.b.c.d {
| ^^^^^^^^
error: Expected selector to have no more than 3 classes, but 4 actually.
--> $DIR/tests/rules/fail/selector-max-class/default/input.css:4:1
|
4 | .a .b .c .d {
| ^^^^^^^^^^^
error: Expected selector to have no more than 3 classes, but 4 actually.
--> $DIR/tests/rules/fail/selector-max-class/default/input.css:8:5
|
8 | .a.b.c.d {
| ^^^^^^^^
error: Expected selector to have no more than 3 classes, but 4 actually.
--> $DIR/tests/rules/fail/selector-max-class/default/input.css:12:6
|
12 | :not(.a.b.c.d) {
| ^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"unit-no-unknown": ["error"]
}
}

View File

@ -0,0 +1,16 @@
a { font-size: 13pp; }
a { margin: 13xpx; }
a { font-size: .5remm; }
a { font-size: 0.5remm; }
a { color: rgb(255pix, 0, 51); }
a { margin: calc(13pix + 10px); }
a { margin: calc(10pix*2); }
a { margin: calc(2*10pix); }
a { -webkit-transition-delay: 10pix; }
@media (min-width: 13pix) {}
a { width: 1e4pz; }
a { flex: 0 9r9 auto; }
a { width: 400x; }
@media (resolution: 2x) and (min-width: 200x) {}
a { background: image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x / 15% 60% repeat-x; }
a { background-image: image-set('img1x.png' 1pix, 'img2x.png' 2x); }

View File

@ -0,0 +1,78 @@
error: Unexpected unknown unit "pp".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:1:18
|
1 | a { font-size: 13pp; }
| ^^
error: Unexpected unknown unit "xpx".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:2:15
|
2 | a { margin: 13xpx; }
| ^^^
error: Unexpected unknown unit "remm".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:3:18
|
3 | a { font-size: .5remm; }
| ^^^^
error: Unexpected unknown unit "remm".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:4:19
|
4 | a { font-size: 0.5remm; }
| ^^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:5:19
|
5 | a { color: rgb(255pix, 0, 51); }
| ^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:6:20
|
6 | a { margin: calc(13pix + 10px); }
| ^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:7:20
|
7 | a { margin: calc(10pix*2); }
| ^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:8:22
|
8 | a { margin: calc(2*10pix); }
| ^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:9:33
|
9 | a { -webkit-transition-delay: 10pix; }
| ^^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:10:22
|
10 | @media (min-width: 13pix) {}
| ^^^
error: Unexpected unknown unit "pz".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:11:15
|
11 | a { width: 1e4pz; }
| ^^
error: Unexpected unknown unit "r9".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:12:14
|
12 | a { flex: 0 9r9 auto; }
| ^^
error: Unexpected unknown unit "pix".
--> $DIR/tests/rules/fail/unit-no-unknown/default/input.css:16:46
|
16 | a { background-image: image-set('img1x.png' 1pix, 'img2x.png' 2x); }
| ^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"unit-no-unknown": ["error", { "ignoreUnits": ["nanometer"] }]
}
}

View File

@ -0,0 +1,3 @@
a {
width: 2lightyear;
}

View File

@ -0,0 +1,6 @@
error: Unexpected unknown unit "lightyear".
--> $DIR/tests/rules/fail/unit-no-unknown/ignored-units/input.css:2:13
|
2 | width: 2lightyear;
| ^^^^^^^^^

View File

@ -0,0 +1,5 @@
{
"rules": {
"at-rule-no-unknown": ["error"]
}
}

View File

@ -0,0 +1,2 @@
@media (max-width: 960px) {
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"at-rule-no-unknown": ["error", { "ignoreAtRules": ["custom"] }]
}
}

View File

@ -0,0 +1,2 @@
@custom {
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"block-no-empty": ["error"]
}
}

View File

@ -0,0 +1,3 @@
a {
color: #fff;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-hex-length": ["error", "long"]
}
}

View File

@ -0,0 +1,6 @@
a {
color: #123456;
color: #112233;
color: #FFFFFF;
color: #aabbccdd;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-hex-length": ["error", "short"]
}
}

View File

@ -0,0 +1,7 @@
a {
color: #123456;
color: #556789;
color: #123;
color: #FFF;
color: #abcd;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"color-no-invalid-hex": ["error"]
}
}

View File

@ -0,0 +1,19 @@
a {
color: greenyellow;
}
a {
color: #000;
}
a {
unknown: #000, #fff, #ababab;
}
a {
color: #0000ffcc;
}
a {
color: #00fc;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,3 @@
a {
color: #000;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,8 @@
@keyframes foo {
from {
opacity: 0;
}
to {
opacity: 1 !important;
}
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"keyframe-declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,8 @@
@keyframes foo {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"keyframe-declaration-no-important": ["error"]
}
}

View File

@ -0,0 +1,3 @@
a {
color: pink !important;
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-empty-source": ["error"]
}
}

View File

@ -0,0 +1,2 @@
a {
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-position-at-import-rule": ["error"]
}
}

View File

@ -0,0 +1,7 @@
@charset "utf-8";
@import "kumiko.css";
@import "reina.css";
a {
}

Some files were not shown because too many files have changed in this diff Show More