Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(biome_css_analyzer): noShorthandPropertyOverrides #2958

Merged
merged 14 commits into from
Jun 10, 2024
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