biomejs / biome

A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
https://biomejs.dev
Apache License 2.0
13.54k stars 414 forks source link

☂️ CSS Semantic Model #3411

Open togami2864 opened 1 month ago

togami2864 commented 1 month ago

Description

This document outlines the concept of a Semantic Model for CSS.

What is a Semantic Model?

In Biome, a semantic model is a data structure that stores information such as scope, variable references, and global references. This model processes SyntaxNode data generated from code parsing.

We have already implemented this model for JavaScript.

The Model for CSS

[!WARNING] This is still an idea and requires a feasibility check.

The primary use case will likely be as a linter. ~The model tracks invalid CSS and is utilized by some lint rules via Semantic or SemanticService.~ The model stores semantic information about selectors, properties.. and more, then those are utilized by some lint rules via Semantic or SemanticService.

~Here are examples of what it can track:~ Here are some examples of what rules can do using a model:

We need to consider the appropriate data structures for storing this information and how we will access them.

Expected Usage Example in Linter

// For https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-custom-properties/
impl Rule for SampleRule {
    type Query = CssSemanticServices;
    type State = RuleState;
    type Signals = Vec<()>;
    type Options = ();

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
         let duplicated_custom_properties = ctx.query().all_duplicated_custom_properties();          
        // ....
    }
}
// For https://stylelint.io/user-guide/rules/custom-property-no-missing-var-function/
impl Rule for SampleRule {
    type Query = Semantic<CssDashedIdentifier>;
    type State = RuleState;
    type Signals = Vec<()>;
    type Options = ();

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
         let node = ctx.query();
         let is_missed_var_func = true; 
         let at_properties = ctx.model().at_properties();

        if at_properties.empty() && is_missed_var_func {
            return node;
        }
        // ....
    }
}

Limitation

Unresolved variable(CSS variables)

In many cases, CSS variables are defined in a :root selector for global styles and referenced from other files, as shown below:

// global.css
:root {
  --custom-color: #FFFFF
}
/* main.css */
a {
  color: var(--costom-color)
}

The model can't analyze across multiple files because it only focuses on a single file. Therefore, we can't implement the noUnresolvedReference rule for CSS variables at this stage. ~To achieve this, we need to implement multi-file support.~

DOM

The model does not have access to HTML or the DOM, so it can't interpret the use of CSS in these contexts. For example, it can't handle cases like the one below:

// index.html
<body>
    <div>Div 1</div>
    <div>Div 2</div>
</body>
/* main.css */
body {
    --bgColor: limegreen;
}

div {
  background: var(--bgColor); // This works even though the variable is not declared in this selector's scope because div is a child of body.
}

Task

Feel free to share your thought and idea about the model:)

arendjr commented 1 month ago

Nice work! Some thoughts regarding the limitations:

ematipico commented 1 month ago

Here are examples of what it can track:

* Duplicate selectors

* Duplicate CSS properties

* Duplicate custom CSS properties

* Properties declared with @\property

* Declared CSS variables (see also Limitations)

The semantic model's responsibility isn't to track duplicate instances but all instances. Then, rules can read those instances, decide if there are duplicates, and emit diagnostics.

Other than that, another thing that we could provide is the specificity of selectors or a way to compare the specificity between two selectors.

togami2864 commented 1 month ago

Maybe multifile analysis wouldn’t even be enough to resolve variables either, because they could be set from JS as well. I guess at some point maybe Biome would even like to cross-reference, but it will take more effort than only multifile to support that. Maybe a lint rule noVariableDeclarationsOutsideRoot might actually be helpful? Then again, anyone defining themes and such from JS will already want exceptions or escape hatches from that…

I completely forgot about css in js :( And Yes, I also agree that adding a rule like noVariableDeclarationsOutsideRoot can lead to other false positives being reported and may not be very useful for users.