first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
+868
View File
@@ -0,0 +1,868 @@
/**
* @fileoverview Rule to enforce the use of baseline features.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------
import { BASELINE_HIGH, BASELINE_LOW, properties, propertyValues, atRules, mediaConditions, functions, units, selectors, } from "../data/baseline-data.js";
import { namedColors } from "../data/colors.js";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { CSSRuleDefinition } from "../types.js"
* @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[],
* allowUnits?: string[]
* }]} UseBaselineOptions
* @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Represents a property that is supported via `@supports`.
*/
class SupportedProperty {
/**
* The name of the property.
* @type {string}
*/
name;
/**
* Supported identifier values.
* @type {Set<string>}
*/
#identifiers = new Set();
/**
* Supported units.
* @type {Set<string>}
*/
#units = new Set();
/**
* Supported function types.
* @type {Set<string>}
*/
#functions = new Set();
/**
* Creates a new instance.
* @param {string} name The name of the property.
*/
constructor(name) {
this.name = name;
}
/**
* Adds an identifier to the list of supported identifiers.
* @param {string} identifier The identifier to add.
* @returns {void}
*/
addIdentifier(identifier) {
this.#identifiers.add(identifier);
}
/**
* Determines if an identifier is supported.
* @param {string} identifier The identifier to check.
* @returns {boolean} `true` if the identifier is supported, `false` if not.
*/
hasIdentifier(identifier) {
return this.#identifiers.has(identifier);
}
/**
* Determines if any identifiers are supported.
* @returns {boolean} `true` if any identifiers are supported, `false` if not.
*/
hasIdentifiers() {
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.
* @returns {void}
*/
addFunction(func) {
this.#functions.add(func);
}
/**
* Determines if a function is supported.
* @param {string} func The function to check.
* @returns {boolean} `true` if the function is supported, `false` if not.
*/
hasFunction(func) {
return this.#functions.has(func);
}
/**
* Determines if any functions are supported.
* @returns {boolean} `true` if any functions are supported, `false` if not.
*/
hasFunctions() {
return this.#functions.size > 0;
}
}
/**
* Represents an `@supports` rule and everything it enables.
*/
class SupportsRule {
/**
* The properties supported by this rule.
* @type {Map<string, SupportedProperty>}
*/
#properties = new Map();
/**
* The selectors supported by this rule.
* @type {Set<string>}
*/
#selectors = new Set();
/**
* Adds a property to the rule.
* @param {string} property The name of the property.
* @returns {SupportedProperty} The supported property object.
*/
addProperty(property) {
if (this.#properties.has(property)) {
return this.#properties.get(property);
}
const supportedProperty = new SupportedProperty(property);
this.#properties.set(property, supportedProperty);
return supportedProperty;
}
/**
* Determines if the rule supports a property.
* @param {string} property The name of the property.
* @returns {boolean} `true` if the property is supported, `false` if not.
*/
hasProperty(property) {
return this.#properties.has(property);
}
/**
* Gets the supported property.
* @param {string} property The name of the property.
* @returns {SupportedProperty} The supported property.
*/
getProperty(property) {
return this.#properties.get(property);
}
/**
* Determines if the rule supports a property value.
* @param {string} property The name of the property.
* @param {string} identifier The identifier to check.
* @returns {boolean} `true` if the property value is supported, `false` if not.
*/
hasPropertyIdentifier(property, identifier) {
const supportedProperty = this.#properties.get(property);
if (!supportedProperty) {
return false;
}
return supportedProperty.hasIdentifier(identifier);
}
/**
* Determines if the rule supports any property values.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any property values are supported, `false` if not.
*/
hasPropertyIdentifiers(property) {
const supportedProperty = this.#properties.get(property);
if (!supportedProperty) {
return false;
}
return supportedProperty.hasIdentifiers();
}
/**
* Determines if the rule supports a function.
* @param {string} property The name of the property.
* @param {string} func The function to check.
* @returns {boolean} `true` if the function is supported, `false` if not.
*/
hasFunction(property, func) {
const supportedProperty = this.#properties.get(property);
if (!supportedProperty) {
return false;
}
return supportedProperty.hasFunction(func);
}
/**
* Determines if the rule supports any functions.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any functions are supported, `false` if not.
*/
hasFunctions(property) {
const supportedProperty = this.#properties.get(property);
if (!supportedProperty) {
return false;
}
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.
* @returns {void}
*/
addSelector(selector) {
this.#selectors.add(selector);
}
/**
* Determines if the rule supports a selector.
* @param {string} selector The name of the selector.
* @returns {boolean} `true` if the selector is supported, `false` if not.
*/
hasSelector(selector) {
return this.#selectors.has(selector);
}
}
/**
* Represents a collection of supports rules.
*/
class SupportsRules {
/**
* A collection of supports rules.
* @type {Array<SupportsRule>}
*/
#rules = [];
/**
* Adds a rule to the collection.
* @param {SupportsRule} rule The rule to add.
* @returns {void}
*/
push(rule) {
this.#rules.push(rule);
}
/**
* Removes the last rule from the collection.
* @returns {SupportsRule} The last rule in the collection.
*/
pop() {
return this.#rules.pop();
}
/**
* Retrieves the last rule in the collection.
* @returns {SupportsRule} The last rule in the collection.
*/
last() {
return this.#rules.at(-1);
}
/**
* Determines if any rule supports a property.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any rule supports the property, `false` if not.
*/
hasProperty(property) {
return this.#rules.some(rule => rule.hasProperty(property));
}
/**
* Determines if any rule supports a property identifier.
* @param {string} property The name of the property.
* @param {string} identifier The identifier to check.
* @returns {boolean} `true` if any rule supports the property value, `false` if not.
*/
hasPropertyIdentifier(property, identifier) {
return this.#rules.some(rule => rule.hasPropertyIdentifier(property, identifier));
}
/**
* Determines if any rule supports any property identifiers.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any rule supports the property values, `false` if not.
*/
hasPropertyIdentifiers(property) {
return this.#rules.some(rule => rule.hasPropertyIdentifiers(property));
}
/**
* Determines if any rule supports a function.
* @param {string} property The name of the property.
* @param {string} func The function to check.
* @returns {boolean} `true` if any rule supports the function, `false` if not.
*/
hasPropertyFunction(property, func) {
return this.#rules.some(rule => rule.hasFunction(property, func));
}
/**
* Determines if any rule supports any functions.
* @param {string} property The name of the property.
* @returns {boolean} `true` if any rule supports the functions, `false` if not.
*/
hasPropertyFunctions(property) {
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.
* @returns {boolean} `true` if any rule supports the selector, `false` if not.
*/
hasSelector(selector) {
return this.#rules.some(rule => rule.hasSelector(selector));
}
}
/**
* Represents the required availability of a feature.
*/
class BaselineAvailability {
/**
* The preferred Baseline year.
* @type {number}
*/
#baselineYear = undefined;
/**
* The preferred Baseline status.
* @type {number}
*/
#baselineStatus = undefined;
/**
* @param {string | number} availability The required level of feature availability.
*/
constructor(availability) {
this.availability = availability;
if (typeof availability === "number") {
this.#baselineYear = availability;
}
else {
this.#baselineStatus =
availability === "widely" ? BASELINE_HIGH : BASELINE_LOW;
}
}
/**
* Determines whether a feature meets the required availability.
* @param {Object} encodedStatus A feature's encoded baseline status and year.
* @returns {boolean} `true` if the feature is supported, `false` if not.
*/
isSupported(encodedStatus) {
if (!encodedStatus) {
// if we don't know the status, assume it's supported
return true;
}
const parts = encodedStatus.split(":");
const status = Number(parts[0]);
const year = Number(parts[1] || NaN);
if (this.#baselineYear) {
return year <= this.#baselineYear;
}
return status >= this.#baselineStatus;
}
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {UseBaselineRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
description: "Enforce the use of baseline features",
recommended: true,
url: "https://github.com/eslint/css/blob/main/docs/rules/use-baseline.md",
},
schema: [
{
type: "object",
properties: {
available: {
anyOf: [
{
enum: ["widely", "newly"],
},
{
// baseline year
type: "integer",
minimum: 2000,
maximum: new Date().getFullYear(),
},
],
},
allowAtRules: {
type: "array",
items: {
enum: Array.from(atRules.keys()),
},
uniqueItems: true,
},
allowFunctions: {
type: "array",
items: {
enum: Array.from(functions.keys()),
},
uniqueItems: true,
},
allowMediaConditions: {
type: "array",
items: {
enum: Array.from(mediaConditions.keys()),
},
uniqueItems: true,
},
allowProperties: {
type: "array",
items: {
enum: Array.from(properties.keys()),
},
uniqueItems: true,
},
allowPropertyValues: {
type: "object",
properties: Object.fromEntries(Array.from(propertyValues.entries()).map(([prop, valuesMap]) => [
prop,
{
type: "array",
items: {
enum: Array.from(valuesMap.keys()),
},
uniqueItems: true,
},
])),
additionalProperties: false,
},
allowSelectors: {
type: "array",
items: {
enum: Array.from(selectors.keys()),
},
uniqueItems: true,
},
allowUnits: {
type: "array",
items: {
enum: Array.from(units.keys()),
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
available: "widely",
allowAtRules: [],
allowFunctions: [],
allowMediaConditions: [],
allowProperties: [],
allowPropertyValues: {},
allowSelectors: [],
allowUnits: [],
},
],
messages: {
notBaselineProperty: "Property '{{property}}' is not a {{availability}} available baseline feature.",
notBaselinePropertyValue: "Value '{{value}}' of property '{{property}}' is not a {{availability}} available baseline feature.",
notBaselineAtRule: "At-rule '@{{atRule}}' is not a {{availability}} available baseline feature.",
notBaselineFunction: "Function '{{function}}' is not a {{availability}} available baseline feature.",
notBaselineMediaCondition: "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.",
},
},
create(context) {
const baselineAvailability = new BaselineAvailability(context.options[0].available);
const supportsRules = new SupportsRules();
const allowAtRules = new Set(context.options[0].allowAtRules);
const allowProperties = new Set(context.options[0].allowProperties);
const allowSelectors = new Set(context.options[0].allowSelectors);
const allowFunctions = new Set(context.options[0].allowFunctions);
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)) {
allowPropertyValuesMap.set(prop, new Set(values));
}
/**
* Checks a property value identifier to see if it's a baseline feature.
* @param {string} property The name of the property.
* @param {Identifier} child The node to check.
* @returns {void}
*/
function checkPropertyValueIdentifier(property, child) {
// named colors are always valid
if (namedColors.has(child.name)) {
return;
}
const allowedValues = allowPropertyValuesMap.get(property);
if (allowedValues?.has(child.name)) {
return;
}
const possiblePropertyValues = propertyValues.get(property);
// if we don't know of any possible property values, just skip it
if (!possiblePropertyValues) {
return;
}
const featureStatus = possiblePropertyValues.get(child.name);
// if we don't know of any possible property values, just skip it
if (featureStatus === undefined) {
return;
}
if (!baselineAvailability.isSupported(featureStatus)) {
context.report({
loc: child.loc,
messageId: "notBaselinePropertyValue",
data: {
property,
value: child.name,
availability: baselineAvailability.availability,
},
});
}
}
/**
* Checks a property value function to see if it's a baseline feature.
* @param {FunctionNodePlain} child The node to check.
* @returns {void}
*/
function checkPropertyValueFunction(child) {
if (allowFunctions.has(child.name)) {
return;
}
const featureStatus = functions.get(child.name);
// if we don't know of any possible property values, just skip it
if (featureStatus === undefined) {
return;
}
if (!baselineAvailability.isSupported(featureStatus)) {
context.report({
loc: child.loc,
messageId: "notBaselineFunction",
data: {
function: child.name,
availability: baselineAvailability.availability,
},
});
}
}
/**
* 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());
},
"Atrule[name=/^supports$/i] > AtrulePrelude > Condition"(node) {
const supportsRule = supportsRules.last();
for (let i = 0; i < node.children.length; i++) {
const conditionChild = node.children[i];
// if a SupportsDeclaration is preceded by "not" then we don't consider it
if (conditionChild.type === "Identifier" &&
conditionChild.name === "not") {
i++;
continue;
}
// save the supported properties and values for this at-rule
if (conditionChild.type === "SupportsDeclaration") {
const { declaration } = conditionChild;
const property = declaration.property;
const supportedProperty = supportsRule.addProperty(property);
declaration.value.children.forEach(child => {
if (child.type === "Identifier") {
supportedProperty.addIdentifier(child.name);
return;
}
if (child.type === "Dimension") {
supportedProperty.addUnit(child.unit);
return;
}
if (child.type === "Function") {
supportedProperty.addFunction(child.name);
}
});
continue;
}
if (conditionChild.type === "FeatureFunction" &&
conditionChild.feature === "selector") {
for (const selectorChild of conditionChild.value
.children) {
supportsRule.addSelector(selectorChild.name);
}
}
}
},
"Rule > Block > Declaration"(node) {
const property = node.property;
// ignore unknown properties - no-invalid-properties already catches this
if (!properties.has(property)) {
return;
}
if (allowProperties.has(property)) {
return;
}
/*
* Step 1: Check that the property is in the baseline.
*
* If the property has been tested in a @supports rule, we don't need to
* check it because it won't be applied if the browser doesn't support it.
*/
if (!supportsRules.hasProperty(property)) {
const featureStatus = properties.get(property);
if (!baselineAvailability.isSupported(featureStatus)) {
context.report({
loc: {
start: node.loc.start,
end: {
line: node.loc.start.line,
column: node.loc.start.column +
node.property.length,
},
},
messageId: "notBaselineProperty",
data: {
property,
availability: baselineAvailability.availability,
},
});
/*
* If the property isn't in baseline, then we don't go
* on to check the values. If the property itself isn't
* in baseline then chances are the values aren't too,
* and there's no need to report multiple errors for the
* same property.
*/
return;
}
}
/*
* With tolerant parsing, it's possible that the value is `Raw`
* and therefore doesn't have children. If that's the case then
* we just exit.
*/
if (!node.value?.children) {
return;
}
/*
* Step 2: Check that the property values are in the baseline.
*/
for (const child of node.value.children) {
if (child.type === "Identifier") {
// if the property value has been tested in a @supports rule, don't check it
if (!supportsRules.hasPropertyIdentifier(property, child.name)) {
checkPropertyValueIdentifier(property, child);
}
continue;
}
if (child.type === "Dimension") {
if (!supportsRules.hasPropertyUnit(property, child.unit)) {
checkPropertyValueUnit(property, child);
}
continue;
}
if (child.type === "Function") {
if (!supportsRules.hasPropertyFunction(property, child.name)) {
checkPropertyValueFunction(child);
}
}
}
},
"Atrule[name=/^supports$/i]:exit"() {
supportsRules.pop();
},
"Atrule[name=/^media$/i] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(node) {
for (const child of node.children) {
// ignore unknown media conditions - no-invalid-at-rules already catches this
if (!mediaConditions.has(child.name)) {
continue;
}
if (child.type !== "Feature") {
continue;
}
if (allowMediaConditions.has(child.name)) {
continue;
}
const featureStatus = mediaConditions.get(child.name);
if (!baselineAvailability.isSupported(featureStatus)) {
const loc = child.loc;
context.report({
loc: {
start: {
line: loc.start.line,
// add 1 to account for the @ symbol
column: loc.start.column + 1,
},
end: {
line: loc.start.line,
column:
// add 1 to account for the @ symbol
loc.start.column +
child.name.length +
1,
},
},
messageId: "notBaselineMediaCondition",
data: {
condition: child.name,
availability: baselineAvailability.availability,
},
});
}
}
},
Atrule(node) {
// ignore unknown at-rules - no-invalid-at-rules already catches this
const atRuleName = node.name.toLowerCase();
if (!atRules.has(atRuleName)) {
return;
}
if (allowAtRules.has(atRuleName)) {
return;
}
const featureStatus = atRules.get(atRuleName);
if (!baselineAvailability.isSupported(featureStatus)) {
const loc = node.loc;
context.report({
loc: {
start: loc.start,
end: {
line: loc.start.line,
// add 1 to account for the @ symbol
column: loc.start.column + node.name.length + 1,
},
},
messageId: "notBaselineAtRule",
data: {
atRule: node.name,
availability: baselineAvailability.availability,
},
});
}
},
"PseudoClassSelector,PseudoElementSelector"(node) {
const selector = node.name;
if (!selectors.has(selector)) {
return;
}
if (allowSelectors.has(selector)) {
return;
}
// if the selector has been tested in a @supports rule, don't check it
if (supportsRules.hasSelector(selector)) {
return;
}
const featureStatus = selectors.get(selector);
if (!baselineAvailability.isSupported(featureStatus)) {
const loc = node.loc;
// some selectors are prefixed with the : or :: symbols
let prefixSymbolLength = 0;
if (node.type.startsWith("PseudoClass")) {
prefixSymbolLength = 1;
}
else if (node.type.startsWith("PseudoElement")) {
prefixSymbolLength = 2;
}
context.report({
loc: {
start: loc.start,
end: {
line: loc.start.line,
column: loc.start.column +
selector.length +
prefixSymbolLength,
},
},
messageId: "notBaselineSelector",
data: {
selector,
availability: baselineAvailability.availability,
},
});
}
},
NestingSelector(node) {
// NestingSelector implies CSS nesting
const selector = "nesting";
if (allowSelectors.has(selector)) {
return;
}
const featureStatus = selectors.get(selector);
if (baselineAvailability.isSupported(featureStatus)) {
return;
}
context.report({
loc: node.loc,
messageId: "notBaselineSelector",
data: {
selector,
availability: baselineAvailability.availability,
},
});
},
};
},
});