From 5b604828deedd3fc7a2be48140cb0757d41cc38c Mon Sep 17 00:00:00 2001 From: xbinaryx Date: Thu, 19 Mar 2026 15:15:17 +0300 Subject: [PATCH] fix: correctly extract baseline support for CSS functions --- src/data/baseline-data.js | 5 +- tests/rules/use-baseline.test.js | 16 +++ tools/generate-baseline.js | 218 ++++++++++++++++--------------- 3 files changed, 132 insertions(+), 107 deletions(-) diff --git a/src/data/baseline-data.js b/src/data/baseline-data.js index 5e7319a6..810e5585 100644 --- a/src/data/baseline-data.js +++ b/src/data/baseline-data.js @@ -589,12 +589,11 @@ export const functions = new Map([ ["sign", "5:2025"], ["anchor", "5:2026"], ["anchor-size", "5:2026"], - ["color", "10:2023"], - ["image", "10:2015"], ["attr", "10:2015"], ["calc", "10:2015"], ["calc-size", "0:"], ["rect", "5:2024"], + ["color", "10:2023"], ["color-mix", "10:2023"], ["conic-gradient", "10:2020"], ["repeating-conic-gradient", "10:2020"], @@ -620,12 +619,14 @@ export const functions = new Map([ ["opacity", "10:2016"], ["saturate", "10:2016"], ["sepia", "10:2016"], + ["fit-content", "0:"], ["linear-gradient", "10:2015"], ["radial-gradient", "10:2015"], ["repeating-linear-gradient", "10:2015"], ["repeating-radial-gradient", "10:2015"], ["hsl", "10:2015"], ["hwb", "10:2022"], + ["image", "0:"], ["image-set", "5:2023"], ["lab", "10:2023"], ["lch", "10:2023"], diff --git a/tests/rules/use-baseline.test.js b/tests/rules/use-baseline.test.js index 82734d18..d94d240b 100644 --- a/tests/rules/use-baseline.test.js +++ b/tests/rules/use-baseline.test.js @@ -395,6 +395,22 @@ ruleTester.run("use-baseline", rule, { }, ], }, + { + code: 'a { background-image: image("foo.png", red); }', + errors: [ + { + messageId: "notBaselineFunction", + data: { + function: "image", + availability: "widely", + }, + line: 1, + column: 23, + endLine: 1, + endColumn: 44, + }, + ], + }, { code: "@media (inverted-colors: inverted) { a { color: red; } }", errors: [ diff --git a/tools/generate-baseline.js b/tools/generate-baseline.js index 22c9edd1..16b542f1 100644 --- a/tools/generate-baseline.js +++ b/tools/generate-baseline.js @@ -15,7 +15,7 @@ import prettier from "prettier"; import fs from "node:fs"; //------------------------------------------------------------------------------ -// Helpers +// Constants //------------------------------------------------------------------------------ /* @@ -25,6 +25,13 @@ import fs from "node:fs"; const WIDE_SUPPORT_PROPERTIES = new Set(["cursor"]); +/* + * Some bare `css.types.` compat keys refer to CSS data types + * instead of functions. For example, `css.types.image` refers to ``, + * not `image()`. + */ +const UNGROUPED_NON_FUNCTION_TYPES = new Set(["color", "image"]); + const BASELINE_HIGH = 10; const BASELINE_LOW = 5; const BASELINE_FALSE = 0; @@ -34,6 +41,34 @@ const baselineIds = new Map([ [false, BASELINE_FALSE], ]); +/* + * The following regular expressions are used to match the keys in the + * features object. The regular expressions are used to extract the + * property name, value, at-rule, type, or selector from the key. + * + * For example, the key "css.properties.color" would match the + * property regular expression and the "color" property would be + * extracted. + * + * Note that these values cannot contain underscores as underscores are + * only used in feature names to provide descriptions rather than syntax. + * Example: css.properties.align-self.position_absolute_context + */ +const PATTERNS = { + property: /^css\.properties\.(?[a-zA-Z$\d-]+)$/u, + propertyValue: + /^css\.properties\.(?[a-zA-Z$\d-]+)\.(?[a-zA-Z$\d-]+)$/u, + atRule: /^css\.at-rules\.(?[a-zA-Z$\d-]+)$/u, + mediaCondition: /^css\.at-rules\.media\.(?[a-zA-Z$\d-]+)$/u, + selector: /^css\.selectors\.(?[a-zA-Z$\d-]+)$/u, + type: /^css\.types\.(?:(?[a-zA-Z\d-]+)\.)?(?[a-zA-Z\d-]+)$/u, + functionName: /^(?[a-zA-Z\d-]+)\(\)$/u, +}; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + /** * Encodes the baseline status and year fields into a single string. * @param {string} status The feature's baseline status. @@ -61,19 +96,26 @@ function mapFeatureStatus(status) { } /** - * Flattens the compat features into an object where the key is the feature - * name and the value is the baseline. - * @param {[string, {compat_features?: string[]}]} featureEntry The entry to flatten. - * @returns {Object} The flattened entry. + * Determines if a name matches a known CSS function in MDN data. + * @param {string} functionName The function name without trailing parentheses. + * @returns {boolean} True if the function exists in MDN data. + */ +function isKnownCSSFunction(functionName) { + return `${functionName}()` in mdnData.css.functions; +} + +/** + * Determines if a compat-derived CSS type key actually represents a function. + * @param {string|undefined} group The optional CSS type group. + * @param {string} typeName The parsed type name to check (e.g., "color" or "calc"). + * @returns {boolean} True if the compat key represents a CSS function. */ -function flattenCompatFeatures([featureId, entry]) { - if (!entry.compat_features) { - return {}; +function isCompatTypeAFunction(group, typeName) { + if (!group && UNGROUPED_NON_FUNCTION_TYPES.has(typeName)) { + return false; } - return Object.fromEntries( - entry.compat_features.map(feature => [feature, featureId]), - ); + return isKnownCSSFunction(typeName); } /** @@ -82,120 +124,86 @@ function flattenCompatFeatures([featureId, entry]) { * @returns {Object} The extracted CSS features. */ function extractCSSFeatures(features) { - /* - * The following regular expressions are used to match the keys in the - * features object. The regular expressions are used to extract the - * property name, value, at-rule, type, or selector from the key. - * - * For example, the key "css.properties.color" would match the - * cssPropertyPattern regular expression and the "color" property would be - * extracted. - * - * Note that these values cannot contain underscores as underscores are - * only used in feature names to provide descriptions rather than syntax. - * Example: css.properties.align-self.position_absolute_context - */ - const cssPropertyPattern = /^css\.properties\.(?[a-zA-Z$\d-]+)$/u; - const cssPropertyValuePattern = - /^css\.properties\.(?[a-zA-Z$\d-]+)\.(?[a-zA-Z$\d-]+)$/u; - const cssAtRulePattern = /^css\.at-rules\.(?[a-zA-Z$\d-]+)$/u; - const cssMediaConditionPattern = - /^css\.at-rules\.media\.(?[a-zA-Z$\d-]+)$/u; - const cssTypePattern = /^css\.types\.(?:.*?\.)?(?[a-zA-Z\d-]+)$/u; - const cssSelectorPattern = /^css\.selectors\.(?[a-zA-Z$\d-]+)$/u; - - const properties = {}; - const propertyValues = {}; - const atRules = {}; - const mediaConditions = {}; - const functions = {}; - const selectors = {}; - - for (const [key, featureId] of Object.entries(features)) { - const feature = webFeatures[featureId]; - const status = feature.status.by_compat_key[key]; - let match; - - // property names - if ( - (match = cssPropertyPattern.exec(key)) !== null && - !WIDE_SUPPORT_PROPERTIES.has(match.groups.property) - ) { - properties[match.groups.property] = mapFeatureStatus(status); - continue; - } - - // property values - if ((match = cssPropertyValuePattern.exec(key)) !== null) { - // don't include values for these properties - if (WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) { - continue; - } - - if (!propertyValues[match.groups.property]) { - propertyValues[match.groups.property] = {}; - } - propertyValues[match.groups.property][match.groups.value] = - mapFeatureStatus(status); - continue; - } + const output = { + properties: {}, + propertyValues: {}, + atRules: {}, + mediaConditions: {}, + selectors: {}, + functions: {}, + }; - // at-rules - if ((match = cssAtRulePattern.exec(key)) !== null) { - atRules[match.groups.atRule] = mapFeatureStatus(status); - continue; + for (const feature of Object.values(features)) { + // Check if the feature name itself represents a function + const nameMatch = PATTERNS.functionName.exec(feature.name); + if (nameMatch && isKnownCSSFunction(nameMatch.groups.name)) { + output.functions[nameMatch.groups.name] = mapFeatureStatus( + feature.status, + ); } - // Media conditions (@media features) - if ((match = cssMediaConditionPattern.exec(key)) !== null) { - mediaConditions[match.groups.condition] = mapFeatureStatus(status); + if (!feature.compat_features) { continue; } - // functions - if ((match = cssTypePattern.exec(key)) !== null) { - const type = match.groups.type; - if (!(`${type}()` in mdnData.css.functions)) { + for (const key of feature.compat_features) { + if (!key.startsWith("css.")) { continue; } - functions[type] = mapFeatureStatus(status); - continue; - } - // selectors - if ((match = cssSelectorPattern.exec(key)) !== null) { - selectors[match.groups.selector] = mapFeatureStatus(status); - continue; + const status = feature.status.by_compat_key[key]; + let match; + + // properties + if ((match = PATTERNS.property.exec(key))) { + if (!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) { + output.properties[match.groups.property] = + mapFeatureStatus(status); + } + } + // property values + else if ((match = PATTERNS.propertyValue.exec(key))) { + if (!WIDE_SUPPORT_PROPERTIES.has(match.groups.property)) { + output.propertyValues[match.groups.property] ??= {}; + output.propertyValues[match.groups.property][ + match.groups.value + ] = mapFeatureStatus(status); + } + } + // at-rules + else if ((match = PATTERNS.atRule.exec(key))) { + output.atRules[match.groups.atRule] = mapFeatureStatus(status); + } + // media conditions (@media features) + else if ((match = PATTERNS.mediaCondition.exec(key))) { + output.mediaConditions[match.groups.condition] = + mapFeatureStatus(status); + } + // functions + else if ((match = PATTERNS.type.exec(key))) { + const { group, type } = match.groups; + if (isCompatTypeAFunction(group, type)) { + output.functions[type] = mapFeatureStatus(status); + } + } + // selectors + else if ((match = PATTERNS.selector.exec(key))) { + output.selectors[match.groups.selector] = + mapFeatureStatus(status); + } } } - return { - properties, - propertyValues, - atRules, - mediaConditions, - functions, - selectors, - }; + return output; } //------------------------------------------------------------------------------ // Main //------------------------------------------------------------------------------ -// create one object with all features then filter just on the css ones -const allFeatures = Object.entries(webFeatures).reduce( - (acc, entry) => Object.assign(acc, flattenCompatFeatures(entry)), - {}, -); -const cssFeatures = extractCSSFeatures( - Object.fromEntries( - Object.entries(allFeatures).filter(([key]) => key.startsWith("css.")), - ), -); +const cssFeatures = extractCSSFeatures(webFeatures); const featuresPath = "./src/data/baseline-data.js"; -// export each group separately as a Set, such as highProperties, lowProperties, etc. const code = `/** * @fileoverview CSS features extracted from the web-features package. * @author tools/generate-baseline.js