Skip to content

Commit

Permalink
feat(biome_css_analyze): add noShorthandPropertyOverrides rule
Browse files Browse the repository at this point in the history
  • Loading branch information
neoki07 committed May 29, 2024
1 parent 32d73c4 commit 8df660d
Show file tree
Hide file tree
Showing 13 changed files with 1,080 additions and 61 deletions.
137 changes: 79 additions & 58 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

434 changes: 433 additions & 1 deletion crates/biome_css_analyze/src/keywords.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::collections::HashSet;

use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use biome_console::markup;
use biome_css_syntax::{CssDeclarationOrRuleList, CssGenericProperty, CssSyntaxKind};
use biome_rowan::{AstNode, SyntaxNodeCast, TextRange};

use crate::utils::{get_longhand_sub_properties, get_reset_to_initial_properties};

declare_rule! {
/// Disallow shorthand properties that override related longhand properties.
///
/// For details on shorthand properties, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties).
///
///
/// This rule ignores:
///
/// - vendor-prefixed properties (e.g., `-webkit-transition`)
///
///
/// ## Examples
///
/// ### Invalid
///
/// ```css,expect_diagnostic
/// a { padding-left: 10px; padding: 20px; }
/// ```
///
/// ### Valid
///
/// ```css
/// a { padding: 10px; padding-left: 20px; }
/// ```
///
/// ```css
/// a { transition-property: opacity; } a { transition: opacity 1s linear; }
/// ```
///
pub NoShorthandPropertyOverrides {
version: "next",
name: "noShorthandPropertyOverrides",
language: "css",
recommended: true,
}
}

fn get_css_declaration_list(property: &CssGenericProperty) -> Option<CssDeclarationOrRuleList> {
for ancestor in property.syntax().ancestors() {
if matches!(ancestor.kind(), CssSyntaxKind::CSS_DECLARATION_OR_RULE_LIST) {
return Some(ancestor.cast::<CssDeclarationOrRuleList>()?);
}
}

return None;
}

fn get_prior_property_names_in_block(
target_property: &CssGenericProperty,
) -> Option<HashSet<String>> {
let declaration_list = get_css_declaration_list(target_property)?;

let mut prior_declarations = HashSet::new();
for declaration in declaration_list {
if let Some(declaration) = declaration.as_css_declaration_with_semicolon() {
let current_property = declaration.declaration().ok()?.property().ok()?;

let current_property =
if let Some(current_property) = current_property.as_css_generic_property() {
current_property
} else {
continue;
};

if current_property == target_property {
break;
}

let current_property_name = current_property.name().ok()?.text().to_lowercase();
prior_declarations.insert(current_property_name);
}
}

Some(prior_declarations)
}

pub struct NoDeclarationBlockShorthandPropertyOverridesState {
target_property_name: String,
override_property_name: String,
span: TextRange,
}

impl Rule for NoShorthandPropertyOverrides {
type Query = Ast<CssGenericProperty>;
type State = NoDeclarationBlockShorthandPropertyOverridesState;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let node = ctx.query();
dbg!(node);

let target_property_name_node = node.name().ok()?;
let target_property_name = target_property_name_node.text().to_lowercase();

let prior_property_names = get_prior_property_names_in_block(node)?;
let longhand_sub_properties = get_longhand_sub_properties(&target_property_name);
let reset_to_initial_properties = get_reset_to_initial_properties(&target_property_name);

for prior_property_name in prior_property_names {
if longhand_sub_properties.contains(&prior_property_name.as_str())
|| reset_to_initial_properties.contains(&prior_property_name.as_str())
{
return Some(NoDeclarationBlockShorthandPropertyOverridesState {
target_property_name,
override_property_name: prior_property_name,
span: target_property_name_node.range(),
});
}
}

None
}

fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
state.span,
markup! {
"Unexpected shorthand property "<Emphasis>{state.target_property_name}</Emphasis>" after "<Emphasis>{state.override_property_name}</Emphasis>
},
))
}
}
1 change: 1 addition & 0 deletions crates/biome_css_analyze/src/options.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions crates/biome_css_analyze/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use crate::keywords::{
FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, KNOWN_CHROME_PROPERTIES,
KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES,
KNOWN_SAFARI_PROPERTIES, KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES,
LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, MEDIA_FEATURE_NAMES,
OTHER_PSEUDO_ELEMENTS, SHADOW_TREE_PSEUDO_ELEMENTS, SYSTEM_FAMILY_NAME_KEYWORDS,
LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS,
LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, MEDIA_FEATURE_NAMES, OTHER_PSEUDO_ELEMENTS,
RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT,
SHADOW_TREE_PSEUDO_ELEMENTS, SHORTHAND_PROPERTIES, SYSTEM_FAMILY_NAME_KEYWORDS,
VENDOR_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS,
};
use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList};
Expand Down Expand Up @@ -174,3 +176,19 @@ pub fn is_media_feature_name(prop: &str) -> bool {
}
false
}

pub fn get_longhand_sub_properties(shorthand_property: &str) -> &'static [&'static str] {
if let Ok(index) = SHORTHAND_PROPERTIES.binary_search(&shorthand_property) {
return LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES[index];
}

&[]
}

pub fn get_reset_to_initial_properties(shorthand_property: &str) -> &'static [&'static str] {
match shorthand_property {
"border" => &RESET_TO_INITIAL_PROPERTIES_BY_BORDER,
"font" => &RESET_TO_INITIAL_PROPERTIES_BY_FONT,
_ => &[],
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
a { padding-left: 10px; padding: 20px; }

a { border-width: 20px; border: 1px solid black; }

a { border-color: red; border: 1px solid black; }

a { border-style: dotted; border: 1px solid black; }

a { border-image: url("foo.png"); border: 1px solid black; }

a { border-image-source: url("foo.png"); border: 1px solid black; }

a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; }

a { PADDING-LEFT: 10PX; PADDING: 20PX; }

/* Overriding within nested rule */
a { padding-left: 10px; { b { padding-top: 10px; padding: 20px; }}}

a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; }

a { transition-property: opacity; transition: opacity 1s linear; }

a { background-repeat: no-repeat; background: url(lion.png); }

/* @media (color) { background-repeat: no-repeat; background: url(lion.png); } */

/* a { @media (color) { background-repeat: no-repeat; background: url(lion.png); }} */

/* a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } */

/* a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } */

a { font-variant: small-caps; font: sans-serif; }

a { font-variant: all-small-caps; font: sans-serif; }

a { font-size-adjust: 0.545; font: Verdana; }

/* a { font-variant-caps: small-caps; font-variant: normal; } */

/* Multiple shorthand property overrides */
a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; }
Loading

0 comments on commit 8df660d

Please sign in to comment.