mirror of
https://github.com/swc-project/swc.git
synced 2024-11-23 00:32:15 +03:00
feat(css/lints): Add CSS linter (#3765)
This commit is contained in:
parent
e475a83ebc
commit
66c6cae8dc
2
.github/workflows/cargo.yml
vendored
2
.github/workflows/cargo.yml
vendored
@ -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
17
Cargo.lock
generated
@ -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"
|
||||
|
@ -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" }
|
||||
|
26
crates/swc_css_lints/Cargo.toml
Normal file
26
crates/swc_css_lints/Cargo.toml
Normal 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"}
|
112
crates/swc_css_lints/src/config.rs
Normal file
112
crates/swc_css_lints/src/config.rs
Normal 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,
|
||||
}
|
7
crates/swc_css_lints/src/lib.rs
Normal file
7
crates/swc_css_lints/src/lib.rs
Normal 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};
|
85
crates/swc_css_lints/src/rule.rs
Normal file
85
crates/swc_css_lints/src/rule.rs
Normal 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);
|
||||
}
|
||||
}
|
57
crates/swc_css_lints/src/rules/at_rule_no_unknown.rs
Normal file
57
crates/swc_css_lints/src/rules/at_rule_no_unknown.rs
Normal 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);
|
||||
}
|
||||
}
|
39
crates/swc_css_lints/src/rules/block_no_empty.rs
Normal file
39
crates/swc_css_lints/src/rules/block_no_empty.rs
Normal 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);
|
||||
}
|
||||
}
|
111
crates/swc_css_lints/src/rules/color_hex_length.rs
Normal file
111
crates/swc_css_lints/src/rules/color_hex_length.rs
Normal 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,
|
||||
}
|
||||
}
|
41
crates/swc_css_lints/src/rules/color_no_invalid_hex.rs
Normal file
41
crates/swc_css_lints/src/rules/color_no_invalid_hex.rs
Normal 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);
|
||||
}
|
||||
}
|
56
crates/swc_css_lints/src/rules/declaration_no_important.rs
Normal file
56
crates/swc_css_lints/src/rules/declaration_no_important.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
72
crates/swc_css_lints/src/rules/mod.rs
Normal file
72
crates/swc_css_lints/src/rules/mod.rs
Normal 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
|
||||
}
|
39
crates/swc_css_lints/src/rules/no_empty_source.rs
Normal file
39
crates/swc_css_lints/src/rules/no_empty_source.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
67
crates/swc_css_lints/src/rules/selector_max_class.rs
Normal file
67
crates/swc_css_lints/src/rules/selector_max_class.rs
Normal 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);
|
||||
}
|
||||
}
|
54
crates/swc_css_lints/src/rules/unit_no_unknown.rs
Normal file
54
crates/swc_css_lints/src/rules/unit_no_unknown.rs
Normal 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);
|
||||
}
|
||||
}
|
93
crates/swc_css_lints/tests/lints.rs
Normal file
93
crates/swc_css_lints/tests/lints.rs
Normal 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();
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"at-rule-no-unknown": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
@test {
|
||||
}
|
@ -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 {
|
||||
| ^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"at-rule-no-unknown": ["error", { "ignoreAtRules": ["custom"] }]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
@test {
|
||||
}
|
@ -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 {
|
||||
| ^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"block-no-empty": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
a {}
|
@ -0,0 +1,6 @@
|
||||
error: Unexpected empty block.
|
||||
--> $DIR/tests/rules/fail/block-no-empty/default/input.css:1:3
|
||||
|
|
||||
1 | a {}
|
||||
| ^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-hex-length": ["error", "long"]
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
a {
|
||||
color: #FFF;
|
||||
color: #abcd;
|
||||
color: #123;
|
||||
}
|
@ -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;
|
||||
| ^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-hex-length": ["error", "short"]
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
a {
|
||||
color: #FFFFFF;
|
||||
color: #aabbccdd;
|
||||
color: #112233;
|
||||
}
|
@ -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;
|
||||
| ^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-no-invalid-hex": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
a {
|
||||
color: #ababa;
|
||||
}
|
||||
|
||||
a {
|
||||
unknown: #00, #fff, #ababab;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #12345abcdefg;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #xyz;
|
||||
}
|
@ -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;
|
||||
| ^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
a {
|
||||
color: #000 !important;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
error: Unexpected '!important'.
|
||||
--> $DIR/tests/rules/fail/declaration-no-important/default/input.css:2:17
|
||||
|
|
||||
2 | color: #000 !important;
|
||||
| ^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
@keyframes foo {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
/* should be ignored */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #111 !important;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
error: Unexpected '!important'.
|
||||
--> $DIR/tests/rules/fail/declaration-no-important/keyframe/input.css:12:17
|
||||
|
|
||||
12 | color: #111 !important;
|
||||
| ^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"keyframe-declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@keyframes foo {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
error: Unexpected '!important'.
|
||||
--> $DIR/tests/rules/fail/keyframe-declaration-no-important/input.css:6:20
|
||||
|
|
||||
6 | opacity: 1 !important;
|
||||
| ^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-empty-source": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
/* comments */
|
@ -0,0 +1,6 @@
|
||||
error: Unexpected empty source.
|
||||
--> $DIR/tests/rules/fail/no-empty-source/comments/input.css:1:15
|
||||
|
|
||||
1 | /* comments */
|
||||
| ^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-empty-source": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
error: Unexpected empty source.
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-invalid-position-at-import-rule": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
@media print {
|
||||
}
|
||||
|
||||
@import "kumiko.css";
|
||||
@import "reina.css";
|
@ -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";
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-invalid-position-at-import-rule": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
@import "kumiko.css";
|
||||
|
||||
a {
|
||||
}
|
||||
|
||||
@import "reina.css";
|
@ -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";
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-invalid-position-at-import-rule": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
a {
|
||||
}
|
||||
|
||||
@import "kumiko.css";
|
||||
@import "reina.css";
|
@ -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";
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-invalid-position-at-import-rule": [
|
||||
"error",
|
||||
{ "ignoreAtRules": ["hibike"] }
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@euphonium {
|
||||
}
|
||||
|
||||
@import "kumiko.css";
|
||||
@import "reina.css";
|
||||
|
||||
a {
|
||||
}
|
@ -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";
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"selector-max-class": ["error", 1]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
.a.b {
|
||||
}
|
@ -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 {
|
||||
| ^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"selector-max-class": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
.a.b.c.d {
|
||||
}
|
||||
|
||||
.a .b .c .d {
|
||||
}
|
||||
|
||||
@media print {
|
||||
.a.b.c.d {
|
||||
}
|
||||
}
|
||||
|
||||
:not(.a.b.c.d) {
|
||||
}
|
@ -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) {
|
||||
| ^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"unit-no-unknown": ["error"]
|
||||
}
|
||||
}
|
@ -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); }
|
@ -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); }
|
||||
| ^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"unit-no-unknown": ["error", { "ignoreUnits": ["nanometer"] }]
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
a {
|
||||
width: 2lightyear;
|
||||
}
|
@ -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;
|
||||
| ^^^^^^^^^
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"at-rule-no-unknown": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
@media (max-width: 960px) {
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"at-rule-no-unknown": ["error", { "ignoreAtRules": ["custom"] }]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
@custom {
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"block-no-empty": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-hex-length": ["error", "long"]
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
a {
|
||||
color: #123456;
|
||||
color: #112233;
|
||||
color: #FFFFFF;
|
||||
color: #aabbccdd;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-hex-length": ["error", "short"]
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
a {
|
||||
color: #123456;
|
||||
color: #556789;
|
||||
color: #123;
|
||||
color: #FFF;
|
||||
color: #abcd;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"color-no-invalid-hex": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
a {
|
||||
color: greenyellow;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a {
|
||||
unknown: #000, #fff, #ababab;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0000ffcc;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00fc;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
a {
|
||||
color: #000;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@keyframes foo {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"keyframe-declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@keyframes foo {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"keyframe-declaration-no-important": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
a {
|
||||
color: pink !important;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-empty-source": ["error"]
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
a {
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-invalid-position-at-import-rule": ["error"]
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user