Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/rules/use-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This rule warns when it finds any of the following:
- A media condition inside `@media` that isn't widely available.
- A CSS property value that isn't widely available or otherwise isn't enclosed in a `@supports` block (currently limited to identifiers only).
- A CSS property function that isn't widely available.
- A CSS unit that isn't widely available or otherwise isn't enclosed in a `@supports` block.
- A CSS pseudo-element or pseudo-class selector that isn't widely available.

The data is provided via the [web-features](https://npmjs.com/package/web-features) package.
Expand All @@ -48,6 +49,12 @@ a {
width: abs(20% - 100px);
}

/* invalid - svh is not available before 2022 */
/* eslint css/use-baseline: ["error", { available: 2021 }] */
a {
height: 100svh;
}

/* invalid - :has() is not widely available */
h1:has(+ h2) {
margin: 0 0 0.25rem 0;
Expand Down Expand Up @@ -110,6 +117,7 @@ This rule accepts an options object with the following properties:
- `allowProperties` (default: `[]`) - Specify an array of properties that are allowed to be used.
- `allowPropertyValues` (default: `{}`) - Specify an object mapping properties to an array of allowed identifier values for that property.
- `allowSelectors` (default: `[]`) - Specify an array of selectors that are allowed to be used.
- `allowUnits` (default: `[]`) - Specify an array of CSS units that are allowed to be used.

#### `allowAtRules`

Expand Down Expand Up @@ -201,6 +209,18 @@ h1:has(+ h2) {
}
```

#### `allowUnits`

Examples of **correct** code with `{ allowUnits: ["svh"] }`:

```css
/* eslint css/use-baseline: ["error", { available: 2021, allowUnits: ["svh"] }] */

a {
height: 100svh;
}
```

## When Not to Use It

If your web application doesn't target all Baseline browsers then you can safely disable this rule.
Expand Down
57 changes: 47 additions & 10 deletions src/data/baseline-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,6 @@ export const properties = new Map([
["text-decoration", "10:2015"],
["text-decoration-color", "10:2020"],
["text-decoration-line", "10:2020"],
["text-decoration-skip", "0:"],
["text-decoration-style", "10:2020"],
["text-decoration-thickness", "10:2021"],
["text-decoration-skip-ink", "10:2022"],
Expand Down Expand Up @@ -639,9 +638,9 @@ export const functions = new Map([
["paint", "0:"],
["path", "10:2020"],
["xywh", "5:2024"],
["rem", "5:2024"],
["rgb", "10:2015"],
["mod", "5:2024"],
["rem", "5:2024"],
["env", "10:2020"],
["circle", "10:2020"],
["ellipse", "10:2020"],
Expand Down Expand Up @@ -677,6 +676,51 @@ export const functions = new Map([
["sin", "10:2023"],
["tan", "10:2023"],
]);
export const units = new Map([
["cap", "5:2023"],
["ch", "10:2015"],
["cqw", "10:2023"],
["cqh", "10:2023"],
["cqi", "10:2023"],
["cqb", "10:2023"],
["cqmin", "10:2023"],
["cqmax", "10:2023"],
["em", "10:2015"],
["ex", "10:2015"],
["ic", "10:2022"],
["lh", "5:2023"],
["Q", "10:2020"],
["rcap", "5:2026"],
["rch", "5:2026"],
["rem", "10:2015"],
["rex", "5:2026"],
["ric", "5:2026"],
["rlh", "5:2023"],
["vb", "10:2022"],
["vi", "10:2022"],
["dvb", "10:2022"],
["dvh", "10:2022"],
["dvi", "10:2022"],
["dvmax", "10:2022"],
["dvmin", "10:2022"],
["dvw", "10:2022"],
["lvb", "10:2022"],
["lvh", "10:2022"],
["lvi", "10:2022"],
["lvmax", "10:2022"],
["lvmin", "10:2022"],
["lvw", "10:2022"],
["svb", "10:2022"],
["svh", "10:2022"],
["svi", "10:2022"],
["svmax", "10:2022"],
["svmin", "10:2022"],
["svw", "10:2022"],
["vh", "10:2015"],
["vmax", "10:2017"],
["vmin", "10:2015"],
["vw", "10:2015"],
]);
export const selectors = new Map([
["active-view-transition", "5:2025"],
["active-view-transition-type", "5:2026"],
Expand Down Expand Up @@ -1990,7 +2034,7 @@ export const propertyValues = new Map([
["nastaliq", "10:2015"],
["sans-serif", "10:2015"],
["serif", "10:2015"],
["math", "5:2025"],
["math", "0:"],
["system-ui", "10:2021"],
["ui-monospace", "0:"],
["ui-rounded", "0:"],
Expand Down Expand Up @@ -3132,13 +3176,6 @@ export const propertyValues = new Map([
["spelling-error", "5:2025"],
]),
],
[
"text-decoration-skip",
new Map([
["auto", "0:"],
["none", "0:"],
]),
],
["text-decoration-style", new Map([["wavy", "10:2020"]])],
[
"text-decoration-thickness",
Expand Down
137 changes: 134 additions & 3 deletions src/rules/use-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
atRules,
mediaConditions,
functions,
units,
selectors,
} from "../data/baseline-data.js";
import { namedColors } from "../data/colors.js";
Expand All @@ -25,16 +26,17 @@ import { namedColors } from "../data/colors.js";

/**
* @import { CSSRuleDefinition } from "../types.js"
* @import { Identifier, FunctionNodePlain } from "@eslint/css-tree"
* @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector"} UseBaselineMessageIds
* @import { Identifier, FunctionNodePlain, Dimension } from "@eslint/css-tree"
* @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector" | "notBaselineUnit"} UseBaselineMessageIds
* @typedef {[{
* available?: "widely" | "newly" | number,
* allowAtRules?: string[],
* allowFunctions?: string[],
* allowMediaConditions?: string[],
* allowProperties?: string[],
* allowPropertyValues?: { [property: string]: string[] },
* allowSelectors?: string[]
* allowSelectors?: string[],
* allowUnits?: string[]
* }]} UseBaselineOptions
* @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition
*/
Expand All @@ -59,6 +61,12 @@ class SupportedProperty {
*/
#identifiers = new Set();

/**
* Supported units.
* @type {Set<string>}
*/
#units = new Set();

/**
* Supported function types.
* @type {Set<string>}
Expand Down Expand Up @@ -99,6 +107,32 @@ class SupportedProperty {
return this.#identifiers.size > 0;
}

/**
* Adds a unit to the list of supported units.
* @param {string} unit The unit to add.
* @returns {void}
*/
addUnit(unit) {
this.#units.add(unit);
}

/**
* Determines if a unit is supported.
* @param {string} unit The unit to check.
* @returns {boolean} `true` if the unit is supported, `false` if not.
*/
hasUnit(unit) {
return this.#units.has(unit);
}

/**
* Determines if any units are supported.
* @returns {boolean} `true` if any units are supported, `false` if not.
*/
hasUnits() {
return this.#units.size > 0;
}

/**
* Adds a function to the list of supported functions.
* @param {string} func The function to add.
Expand Down Expand Up @@ -238,6 +272,37 @@ class SupportsRule {
return supportedProperty.hasFunctions();
}

/**
* Determines if the rule supports a unit.
* @param {string} property The name of the property.
* @param {string} unit The unit to check.
* @returns {boolean} `true` if the unit is supported, `false` if not.
*/
hasPropertyUnit(property, unit) {
const supportedProperty = this.#properties.get(property);

if (!supportedProperty) {
return false;
}

return supportedProperty.hasUnit(unit);
}

/**
* Determines if the rule supports any units.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any units are supported, `false` if not.
*/
hasPropertyUnits(property) {
const supportedProperty = this.#properties.get(property);

if (!supportedProperty) {
return false;
}

return supportedProperty.hasUnits();
}

/**
* Adds a selector to the rule.
* @param {string} selector The name of the selector.
Expand Down Expand Up @@ -341,6 +406,16 @@ class SupportsRules {
return this.#rules.some(rule => rule.hasFunctions(property));
}

/**
* Determines if any rule supports a unit.
* @param {string} property The name of the property.
* @param {string} unit The unit to check.
* @returns {boolean} `true` if any rule supports the unit, `false` if not.
*/
hasPropertyUnit(property, unit) {
return this.#rules.some(rule => rule.hasPropertyUnit(property, unit));
}

/**
* Determines if any rule supports a selector.
* @param {string} selector The name of the selector.
Expand Down Expand Up @@ -489,6 +564,13 @@ export default {
},
uniqueItems: true,
},
allowUnits: {
type: "array",
items: {
enum: Array.from(units.keys()),
},
uniqueItems: true,
},
},
additionalProperties: false,
},
Expand All @@ -503,6 +585,7 @@ export default {
allowProperties: [],
allowPropertyValues: {},
allowSelectors: [],
allowUnits: [],
},
],

Expand All @@ -519,6 +602,8 @@ export default {
"Media condition '{{condition}}' is not a {{availability}} available baseline feature.",
notBaselineSelector:
"Selector '{{selector}}' is not a {{availability}} available baseline feature.",
notBaselineUnit:
"Unit '{{unit}}' is not a {{availability}} available baseline feature.",
},
},

Expand All @@ -534,6 +619,7 @@ export default {
const allowMediaConditions = new Set(
context.options[0].allowMediaConditions,
);
const allowUnits = new Set(context.options[0].allowUnits);
const allowPropertyValuesMap = new Map();
for (const [prop, values] of Object.entries(
context.options[0].allowPropertyValues,
Expand Down Expand Up @@ -614,6 +700,36 @@ export default {
}
}

/**
* Checks a property value unit to see if it's a baseline feature.
* @param {string} property The name of the property.
* @param {Dimension} child The node to check.
* @returns {void}
*/
function checkPropertyValueUnit(property, child) {
if (allowUnits.has(child.unit)) {
return;
}

const featureStatus = units.get(child.unit);

// if we don't know of this unit, just skip it
if (featureStatus === undefined) {
return;
}

if (!baselineAvailability.isSupported(featureStatus)) {
context.report({
loc: child.loc,
messageId: "notBaselineUnit",
data: {
unit: child.unit,
availability: baselineAvailability.availability,
},
});
}
}

return {
"Atrule[name=/^supports$/i]"() {
supportsRules.push(new SupportsRule());
Expand Down Expand Up @@ -647,6 +763,11 @@ export default {
return;
}

if (child.type === "Dimension") {
supportedProperty.addUnit(child.unit);
return;
}

if (child.type === "Function") {
supportedProperty.addFunction(child.name);
}
Expand Down Expand Up @@ -744,6 +865,16 @@ export default {
continue;
}

if (child.type === "Dimension") {
if (
!supportsRules.hasPropertyUnit(property, child.unit)
) {
checkPropertyValueUnit(property, child);
}

continue;
}

if (child.type === "Function") {
if (
!supportsRules.hasPropertyFunction(
Expand Down
Loading