feat(css/lints): Allow using regex in ignore list (#3855)

This commit is contained in:
Pig Fang 2022-03-06 02:31:52 +08:00 committed by GitHub
parent 3034e355ed
commit dc0de58a46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 165 additions and 38 deletions

2
Cargo.lock generated
View File

@ -2924,11 +2924,13 @@ dependencies = [
"serde",
"serde_json",
"swc_atoms",
"swc_cached",
"swc_common",
"swc_css_ast",
"swc_css_parser",
"swc_css_visit",
"testing",
"thiserror",
]
[[package]]

View File

@ -1,10 +1,11 @@
use std::{ops::Deref, sync::Arc};
pub use anyhow::Error;
use anyhow::{Context, Result};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{de::Error, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
/// A regex which can be used as a configuration.
///
@ -47,6 +48,8 @@ impl<'de> Deserialize<'de> for CachedRegex {
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
Self::new(&s).map_err(|err| D::Error::custom(err.to_string()))

View File

@ -17,9 +17,11 @@ 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_cached = {version = "0.1.0", path = "../swc_cached"}
swc_common = {version = "0.17.0", path = "../swc_common"}
swc_css_ast = {version = "0.89.0", path = "../swc_css_ast"}
swc_css_visit = {version = "0.88.0", path = "../swc_css_visit"}
thiserror = "1.0.30"
[dev-dependencies]
serde_json = "1.0.79"

View File

@ -0,0 +1,7 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("invalid regex")]
Regex(#[from] swc_cached::regex::Error),
}

View File

@ -2,9 +2,12 @@
mod config;
mod dataset;
mod error;
mod pattern;
mod rule;
mod rules;
pub use config::LintConfig;
pub(crate) use error::ConfigError;
pub use rule::LintRule;
pub use rules::{get_rules, LintParams};

View File

@ -0,0 +1,41 @@
use swc_cached::regex::CachedRegex;
#[derive(Debug)]
pub(crate) enum NamePattern {
Str(String),
Regex(CachedRegex),
}
impl NamePattern {
pub(crate) fn is_match<S>(&self, name: S) -> bool
where
S: AsRef<str>,
{
let name = name.as_ref();
match self {
Self::Str(s) => s == name,
Self::Regex(regex) => regex.is_match(name),
}
}
}
impl TryFrom<String> for NamePattern {
type Error = swc_cached::regex::Error;
fn try_from(pattern: String) -> Result<Self, Self::Error> {
if let Some(pattern) = pattern
.strip_prefix('/')
.and_then(|pattern| pattern.strip_suffix('/'))
{
CachedRegex::new(pattern).map(Self::Regex)
} else {
Ok(Self::Str(pattern))
}
}
}
impl Default for NamePattern {
fn default() -> Self {
Self::Str(String::new())
}
}

View File

@ -2,7 +2,11 @@ use serde::{Deserialize, Serialize};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::rule::{visitor_rule, LintRule, LintRuleContext};
use crate::{
pattern::NamePattern,
rule::{visitor_rule, LintRule, LintRuleContext},
ConfigError,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -10,15 +14,27 @@ pub struct AtRuleNoUnknownConfig {
ignore_at_rules: Option<Vec<String>>,
}
pub fn at_rule_no_unknown(ctx: LintRuleContext<AtRuleNoUnknownConfig>) -> Box<dyn LintRule> {
let ignored = ctx.config().ignore_at_rules.clone().unwrap_or_default();
visitor_rule(ctx.reaction(), AtRuleNoUnknown { ctx, ignored })
pub fn at_rule_no_unknown(
ctx: LintRuleContext<AtRuleNoUnknownConfig>,
) -> Result<Box<dyn LintRule>, ConfigError> {
let ignored = ctx
.config()
.ignore_at_rules
.clone()
.unwrap_or_default()
.into_iter()
.map(NamePattern::try_from)
.collect::<Result<_, _>>()?;
Ok(visitor_rule(
ctx.reaction(),
AtRuleNoUnknown { ctx, ignored },
))
}
#[derive(Debug, Default)]
struct AtRuleNoUnknown {
ctx: LintRuleContext<AtRuleNoUnknownConfig>,
ignored: Vec<String>,
ignored: Vec<NamePattern>,
}
impl Visit for AtRuleNoUnknown {
@ -28,7 +44,7 @@ impl Visit for AtRuleNoUnknown {
AtRuleName::Ident(ident) => &ident.value,
};
if self.ignored.iter().all(|item| name != item) {
if self.ignored.iter().all(|item| !item.is_match(name)) {
let message = format!("Unexpected unknown at-rule \"@{}\".", name);
self.ctx.report(&unknown_at_rule.name, message);
}

View File

@ -6,7 +6,9 @@ use swc_css_visit::{Visit, VisitWith};
use crate::{
dataset::is_generic_font_keyword,
pattern::NamePattern,
rule::{visitor_rule, LintRule, LintRuleContext},
ConfigError,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -17,19 +19,25 @@ pub struct FontFamilyNoDuplicateNamesConfig {
pub fn font_family_no_duplicate_names(
ctx: LintRuleContext<FontFamilyNoDuplicateNamesConfig>,
) -> Box<dyn LintRule> {
) -> Result<Box<dyn LintRule>, ConfigError> {
let ignored = ctx
.config()
.ignore_font_family_names
.clone()
.unwrap_or_default();
visitor_rule(ctx.reaction(), FontFamilyNoDuplicateNames { ctx, ignored })
.unwrap_or_default()
.into_iter()
.map(NamePattern::try_from)
.collect::<Result<_, _>>()?;
Ok(visitor_rule(
ctx.reaction(),
FontFamilyNoDuplicateNames { ctx, ignored },
))
}
#[derive(Debug, Default)]
struct FontFamilyNoDuplicateNames {
ctx: LintRuleContext<FontFamilyNoDuplicateNamesConfig>,
ignored: Vec<String>,
ignored: Vec<NamePattern>,
}
impl FontFamilyNoDuplicateNames {
@ -73,7 +81,7 @@ impl FontFamilyNoDuplicateNames {
AHashSet::with_capacity(values.len()),
|mut seen, (font, span)| {
let name = font.name();
if seen.contains(&font) && self.ignored.iter().all(|item| name != item) {
if seen.contains(&font) && self.ignored.iter().all(|item| !item.is_match(name)) {
self.ctx
.report(span, format!("Unexpected duplicate name '{}'.", name));
}

View File

@ -1,5 +1,6 @@
use crate::{
config::LintConfig,
error::ConfigError,
rule::LintRule,
rules::{
at_rule_no_unknown::at_rule_no_unknown, block_no_empty::block_no_empty,
@ -33,24 +34,28 @@ pub struct LintParams<'a> {
pub lint_config: &'a LintConfig,
}
pub fn get_rules(LintParams { lint_config }: &LintParams) -> Vec<Box<dyn LintRule>> {
pub fn get_rules(
LintParams { lint_config }: &LintParams,
) -> Result<Vec<Box<dyn LintRule>>, ConfigError> {
let rules_config = &lint_config.rules;
vec![
let rules = vec![
block_no_empty((&rules_config.block_no_empty).into()),
at_rule_no_unknown((&rules_config.at_rule_no_unknown).into()),
at_rule_no_unknown((&rules_config.at_rule_no_unknown).into())?,
no_empty_source((&rules_config.no_empty_source).into()),
declaration_no_important((&rules_config.declaration_no_important).into()),
keyframe_declaration_no_important((&rules_config.keyframe_declaration_no_important).into()),
no_invalid_position_at_import_rule(
(&rules_config.no_invalid_position_at_import_rule).into(),
),
)?,
selector_max_class((&rules_config.selector_max_class).into()),
color_hex_length((&rules_config.color_hex_length).into()),
color_no_invalid_hex((&rules_config.color_no_invalid_hex).into()),
unit_no_unknown((&rules_config.unit_no_unknown).into()),
unit_no_unknown((&rules_config.unit_no_unknown).into())?,
selector_max_combinators((&rules_config.selector_max_combinators).into()),
font_family_no_duplicate_names((&rules_config.font_family_no_duplicate_names).into()),
font_family_no_duplicate_names((&rules_config.font_family_no_duplicate_names).into())?,
color_hex_alpha((&rules_config.color_hex_alpha).into()),
]
];
Ok(rules)
}

View File

@ -2,7 +2,11 @@ use serde::{Deserialize, Serialize};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::rule::{visitor_rule, LintRule, LintRuleContext};
use crate::{
pattern::NamePattern,
rule::{visitor_rule, LintRule, LintRuleContext},
ConfigError,
};
const MESSAGE: &str = "Unexpected invalid position '@import' rule.";
@ -14,18 +18,25 @@ pub struct NoInvalidPositionAtImportRuleConfig {
pub fn no_invalid_position_at_import_rule(
ctx: LintRuleContext<NoInvalidPositionAtImportRuleConfig>,
) -> Box<dyn LintRule> {
let ignored = ctx.config().ignore_at_rules.clone().unwrap_or_default();
visitor_rule(
) -> Result<Box<dyn LintRule>, ConfigError> {
let ignored = ctx
.config()
.ignore_at_rules
.clone()
.unwrap_or_default()
.into_iter()
.map(NamePattern::try_from)
.collect::<Result<_, _>>()?;
Ok(visitor_rule(
ctx.reaction(),
NoInvalidPositionAtImportRule { ctx, ignored },
)
))
}
#[derive(Debug, Default)]
struct NoInvalidPositionAtImportRule {
ctx: LintRuleContext<NoInvalidPositionAtImportRuleConfig>,
ignored: Vec<String>,
ignored: Vec<NamePattern>,
}
impl Visit for NoInvalidPositionAtImportRule {
@ -47,7 +58,7 @@ impl Visit for NoInvalidPositionAtImportRule {
AtRuleName::DashedIdent(dashed_ident) => &dashed_ident.value,
AtRuleName::Ident(ident) => &ident.value,
};
if self.ignored.iter().any(|item| name == item) {
if self.ignored.iter().any(|item| item.is_match(name)) {
seen
} else {
true

View File

@ -2,7 +2,11 @@ use serde::{Deserialize, Serialize};
use swc_css_ast::*;
use swc_css_visit::{Visit, VisitWith};
use crate::rule::{visitor_rule, LintRule, LintRuleContext};
use crate::{
pattern::NamePattern,
rule::{visitor_rule, LintRule, LintRuleContext},
ConfigError,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -10,22 +14,34 @@ pub struct UnitNoUnknownConfig {
ignore_units: Option<Vec<String>>,
}
pub fn unit_no_unknown(ctx: LintRuleContext<UnitNoUnknownConfig>) -> Box<dyn LintRule> {
let ignored_units = ctx.config().ignore_units.clone().unwrap_or_default();
visitor_rule(ctx.reaction(), UnitNoUnknown { ctx, ignored_units })
pub fn unit_no_unknown(
ctx: LintRuleContext<UnitNoUnknownConfig>,
) -> Result<Box<dyn LintRule>, ConfigError> {
let ignored_units = ctx
.config()
.ignore_units
.clone()
.unwrap_or_default()
.into_iter()
.map(NamePattern::try_from)
.collect::<Result<_, _>>()?;
Ok(visitor_rule(
ctx.reaction(),
UnitNoUnknown { ctx, ignored_units },
))
}
#[derive(Debug, Default)]
struct UnitNoUnknown {
ctx: LintRuleContext<UnitNoUnknownConfig>,
ignored_units: Vec<String>,
ignored_units: Vec<NamePattern>,
}
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) {
if self.ignored_units.iter().all(|item| !item.is_match(unit)) {
let message = format!("Unexpected unknown unit \"{}\".", unit);
self.ctx.report(&unknown_dimension.unit, message);
}

View File

@ -34,7 +34,8 @@ fn pass(input: PathBuf) {
let mut rules = get_rules(&LintParams {
lint_config: &lint_config,
});
})
.unwrap();
HANDLER.set(&handler, || {
rules.lint_stylesheet(&stylesheet);
@ -75,7 +76,8 @@ fn fail(input: PathBuf) {
let mut rules = get_rules(&LintParams {
lint_config: &lint_config,
});
})
.unwrap();
HANDLER.set(&handler, || {
rules.lint_stylesheet(&stylesheet);

View File

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

View File

@ -1,2 +1,5 @@
@custom {
}
@my-at-rule {
}

View File

@ -2,7 +2,7 @@
"rules": {
"font-family-no-duplicate-names": [
"error",
{ "ignoreFontFamilyNames": ["monospace"] }
{ "ignoreFontFamilyNames": ["monospace", "/^my-/"] }
]
}
}

View File

@ -1,3 +1,4 @@
pre { font-family: monospace, monospace; }
pre { font: 1em monospace, monospace; }
pre { font-family: monospace, "Roberto Mono", monospace; }
pre { font-family: my-font, my-font; }

View File

@ -2,7 +2,7 @@
"rules": {
"no-invalid-position-at-import-rule": [
"error",
{ "ignoreAtRules": ["hibike"] }
{ "ignoreAtRules": ["hibike", "/^x-/"] }
]
}
}

View File

@ -3,6 +3,9 @@
@hibike {
}
@x-rule {
}
@import "kumiko.css";
@import "reina.css";

View File

@ -1,5 +1,5 @@
{
"rules": {
"unit-no-unknown": ["error", { "ignoreUnits": ["lightyear"] }]
"unit-no-unknown": ["error", { "ignoreUnits": ["lightyear", "/x$/"] }]
}
}

View File

@ -1,3 +1,4 @@
a {
width: 2lightyear;
width: 6ax;
}