Skip to content

Commit

Permalink
feat(biome_css_analyzer): noShorthandPropertyOverrides (#2958)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <[email protected]>
  • Loading branch information
neoki07 and ematipico authored Jun 10, 2024
1 parent da6f180 commit 746db0a
Show file tree
Hide file tree
Showing 13 changed files with 1,199 additions and 63 deletions.
141 changes: 81 additions & 60 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,209 @@
use crate::utils::{get_longhand_sub_properties, get_reset_to_initial_properties, vender_prefix};
use biome_analyze::{
context::RuleContext, declare_rule, AddVisitor, Phases, QueryMatch, Queryable, Rule,
RuleDiagnostic, RuleSource, ServiceBag, Visitor, VisitorContext,
};
use biome_console::markup;
use biome_css_syntax::{AnyCssDeclarationName, CssGenericProperty, CssLanguage, CssSyntaxKind};
use biome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent};

fn remove_vendor_prefix(prop: &str, prefix: &str) -> String {
if let Some(prop) = prop.strip_prefix(prefix) {
return prop.to_string();
}

prop.to_string()
}

fn get_override_props(property: &str) -> Vec<&str> {
let longhand_sub_props = get_longhand_sub_properties(property);
let reset_to_initial_props = get_reset_to_initial_properties(property);

let mut merged = Vec::with_capacity(longhand_sub_props.len() + reset_to_initial_props.len());

let (mut i, mut j) = (0, 0);

while i < longhand_sub_props.len() && j < reset_to_initial_props.len() {
if longhand_sub_props[i] < reset_to_initial_props[j] {
merged.push(longhand_sub_props[i]);
i += 1;
} else {
merged.push(reset_to_initial_props[j]);
j += 1;
}
}

if i < longhand_sub_props.len() {
merged.extend_from_slice(&longhand_sub_props[i..]);
}

if j < reset_to_initial_props.len() {
merged.extend_from_slice(&reset_to_initial_props[j..]);
}

merged
}

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).
///
/// ## 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,
sources: &[RuleSource::Stylelint("declaration-block-no-shorthand-property-overrides")],
}
}

#[derive(Default)]
struct PriorProperty {
original: String,
lowercase: String,
}

#[derive(Default)]
struct NoDeclarationBlockShorthandPropertyOverridesVisitor {
prior_props_in_block: Vec<PriorProperty>,
}

impl Visitor for NoDeclarationBlockShorthandPropertyOverridesVisitor {
type Language = CssLanguage;

fn visit(
&mut self,
event: &WalkEvent<SyntaxNode<Self::Language>>,
mut ctx: VisitorContext<Self::Language>,
) {
if let WalkEvent::Enter(node) = event {
match node.kind() {
CssSyntaxKind::CSS_DECLARATION_OR_RULE_BLOCK => {
self.prior_props_in_block.clear();
}
CssSyntaxKind::CSS_GENERIC_PROPERTY => {
if let Some(prop_node) = CssGenericProperty::cast_ref(node)
.and_then(|property_node| property_node.name().ok())
{
let prop = prop_node.text();
let prop_lowercase = prop.to_lowercase();

let prop_prefix = vender_prefix(&prop_lowercase);
let unprefixed_prop = remove_vendor_prefix(&prop_lowercase, &prop_prefix);
let override_props = get_override_props(&unprefixed_prop);

self.prior_props_in_block.iter().for_each(|prior_prop| {
let prior_prop_prefix = vender_prefix(&prior_prop.lowercase);
let unprefixed_prior_prop =
remove_vendor_prefix(&prior_prop.lowercase, &prior_prop_prefix);

if prop_prefix == prior_prop_prefix
&& override_props
.binary_search(&unprefixed_prior_prop.as_str())
.is_ok()
{
ctx.match_query(
NoDeclarationBlockShorthandPropertyOverridesQuery {
property_node: prop_node.clone(),
override_property: prior_prop.original.clone(),
},
);
}
});

self.prior_props_in_block.push(PriorProperty {
original: prop,
lowercase: prop_lowercase,
});
}
}
_ => {}
}
}
}
}

#[derive(Clone)]
pub struct NoDeclarationBlockShorthandPropertyOverridesQuery {
property_node: AnyCssDeclarationName,
override_property: String,
}

impl QueryMatch for NoDeclarationBlockShorthandPropertyOverridesQuery {
fn text_range(&self) -> TextRange {
self.property_node.range()
}
}

impl Queryable for NoDeclarationBlockShorthandPropertyOverridesQuery {
type Input = Self;
type Language = CssLanguage;
type Output = NoDeclarationBlockShorthandPropertyOverridesQuery;
type Services = ();

fn build_visitor(
analyzer: &mut impl AddVisitor<Self::Language>,
_: &<Self::Language as Language>::Root,
) {
analyzer.add_visitor(
Phases::Syntax,
NoDeclarationBlockShorthandPropertyOverridesVisitor::default,
);
}

fn unwrap_match(_: &ServiceBag, query: &Self::Input) -> Self::Output {
query.clone()
}
}

pub struct NoDeclarationBlockShorthandPropertyOverridesState {
target_property: String,
override_property: String,
span: TextRange,
}

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

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

Some(NoDeclarationBlockShorthandPropertyOverridesState {
target_property: query.property_node.text(),
override_property: query.override_property.clone(),
span: query.text_range(),
})
}

fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
state.span,
markup! {
"Unexpected shorthand property "<Emphasis>{state.target_property}</Emphasis>" after "<Emphasis>{state.override_property}</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 @@ -7,8 +7,10 @@ use crate::keywords::{
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, LINGUISTIC_PSEUDO_CLASSES,
LOGICAL_COMBINATIONS_PSEUDO_CLASSES, MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES,
OTHER_PSEUDO_ELEMENTS, RESOURCE_STATE_PSEUDO_CLASSES, SHADOW_TREE_PSEUDO_ELEMENTS,
LOGICAL_COMBINATIONS_PSEUDO_CLASSES, LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES,
MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES, OTHER_PSEUDO_ELEMENTS,
RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT,
RESOURCE_STATE_PSEUDO_CLASSES, 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 @@ -197,3 +199,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,37 @@
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; }

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) { a { 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 { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; }

a { padding-left: 10px; padding-right: 10px; padding: 20px; }
Loading

0 comments on commit 746db0a

Please sign in to comment.