diff --git a/api/GeolocationCoordinates.json b/api/GeolocationCoordinates.json index 7b79abd5f36bab..17f50df34e1c93 100644 --- a/api/GeolocationCoordinates.json +++ b/api/GeolocationCoordinates.json @@ -47,7 +47,6 @@ "oculus": "mirror", "opera": [ { - "alternative_name": "Coordinates", "version_added": "16" }, { diff --git a/api/Navigator.json b/api/Navigator.json index 489b75c0e16d87..280f1c2c3d4643 100644 --- a/api/Navigator.json +++ b/api/Navigator.json @@ -135,9 +135,22 @@ "version_removed": "80" }, "edge": "mirror", - "firefox": { - "version_added": "73" - }, + "firefox": [ + { + "version_added": "98", + "flags": [ + { + "type": "preference", + "name": "dom.vr.enabled", + "value_to_set": "true" + } + ] + }, + { + "version_added": "73", + "version_removed": "98" + } + ], "firefox_android": { "version_added": false }, diff --git a/api/WebTransport.json b/api/WebTransport.json index 65ac29bc017767..d5bc5161915ee0 100644 --- a/api/WebTransport.json +++ b/api/WebTransport.json @@ -100,7 +100,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -138,7 +144,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -176,7 +188,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -214,7 +232,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -534,7 +558,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -650,7 +680,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { @@ -688,7 +724,13 @@ }, "chrome_android": "mirror", "deno": { - "version_added": "2.2" + "version_added": "2.2", + "flags": [ + { + "type": "runtime_flag", + "name": "--unstable-net" + } + ] }, "edge": "mirror", "firefox": { diff --git a/css/properties/resize.json b/css/properties/resize.json index 50d476f3188df9..bf9d095f8c36db 100644 --- a/css/properties/resize.json +++ b/css/properties/resize.json @@ -138,7 +138,11 @@ "firefox": { "version_added": "4" }, - "firefox_android": "mirror", + "firefox_android": { + "version_added": "4", + "version_removed": "79", + "notes": "The property is recognized, but has no effect. See [bug 1776834](https://bugzil.la/1776834)." + }, "oculus": "mirror", "opera": "mirror", "opera_android": "mirror", @@ -171,7 +175,11 @@ "firefox": { "version_added": "4" }, - "firefox_android": "mirror", + "firefox_android": { + "version_added": "4", + "version_removed": "79", + "notes": "The property is recognized, but has no effect. See [bug 1776834](https://bugzil.la/1776834)." + }, "oculus": "mirror", "opera": "mirror", "opera_android": "mirror", @@ -243,7 +251,11 @@ "firefox": { "version_added": "4" }, - "firefox_android": "mirror", + "firefox_android": { + "version_added": "4", + "version_removed": "79", + "notes": "The property is recognized, but has no effect. See [bug 1776834](https://bugzil.la/1776834)." + }, "oculus": "mirror", "opera": "mirror", "opera_android": "mirror", @@ -276,7 +288,11 @@ "firefox": { "version_added": "4" }, - "firefox_android": "mirror", + "firefox_android": { + "version_added": "4", + "version_removed": "79", + "notes": "The property is recognized, but has no effect. See [bug 1776834](https://bugzil.la/1776834)." + }, "oculus": "mirror", "opera": "mirror", "opera_android": "mirror", diff --git a/javascript/builtins/Temporal.json b/javascript/builtins/Temporal.json index 37150fc5f7e66e..d412f7df987b31 100644 --- a/javascript/builtins/Temporal.json +++ b/javascript/builtins/Temporal.json @@ -30,13 +30,6 @@ "opera_android": "mirror", "safari": { "version_added": "preview", - "flags": [ - { - "name": "useTemporal", - "value_to_set": "1", - "type": "runtime_flag" - } - ], "impl_url": "https://webkit.org/b/223166" }, "safari_ios": { diff --git a/lint/linter/test-consistency.js b/lint/linter/test-consistency.js index f3a76a6fd970e9..5aa9a95cfd47f0 100644 --- a/lint/linter/test-consistency.js +++ b/lint/linter/test-consistency.js @@ -14,7 +14,7 @@ import bcd from '../../index.js'; /** @import {BrowserName, InternalCompatData, InternalCompatStatement, InternalIdentifier, InternalSimpleSupportStatement, InternalSupportBlock, InternalSupportStatement, VersionValue} from '../../types/index.js' */ /** - * @typedef {'unsupported' | 'subfeature_earlier_implementation'} ErrorType + * @typedef {'unsupported' | 'subfeature_earlier_implementation' | 'subfeature_outside_parent_range'} ErrorType */ /** @@ -135,6 +135,120 @@ export class ConsistencyChecker { return subfeatures; } + /** + * Whether a support statement represents unprefixed support under the + * feature's canonical name (no `prefix`, no `alternative_name`). + * @param {InternalSimpleSupportStatement} statement The support statement + * @returns {boolean} Whether the statement is for the canonical name + */ + #isCanonicalName(statement) { + return !statement.prefix && !statement.alternative_name; + } + + /** + * Whether a support statement represents ongoing (not removed), unflagged + * support under the canonical name — i.e. the feature is currently usable, + * including `"preview"` builds. + * @param {InternalSimpleSupportStatement} statement The support statement + * @returns {boolean} Whether the statement is currently supported + */ + #isCurrentlySupported(statement) { + return ( + this.#isCanonicalName(statement) && + !statement.flags && + Boolean(statement.version_added) && + statement.version_removed === undefined + ); + } + + /** + * Resolve a browser's support statement to an array of simple statements, + * resolving `"mirror"` if necessary. + * @param {InternalSupportBlock | undefined} supportBlock The support block + * @param {BrowserName} browser The browser to resolve + * @returns {InternalSimpleSupportStatement[]} The simple support statements + */ + #resolveStatements(supportBlock, browser) { + if (!supportBlock) { + return []; + } + let support = supportBlock[browser]; + if (support === undefined) { + return []; + } + if (support === 'mirror') { + support = this.#resolveMirror(browser, supportBlock); + } + return Array.isArray(support) + ? support + : [/** @type {InternalSimpleSupportStatement} */ (support)]; + } + + /** + * Format a support statement as a human-readable version range. + * @param {InternalSimpleSupportStatement} statement The statement to format + * @returns {string} The formatted range + */ + #formatSupport(statement) { + /** @type {string[]} */ + const parts = [`added: ${statement.version_added}`]; + if (statement.version_removed) { + parts.push(`removed: ${statement.version_removed}`); + } + if (statement.flags) { + parts.push('behind a flag'); + } + return `{ ${parts.join(', ')} }`; + } + + /** + * Check whether a child's supported version range extends beyond its parent's + * for a given browser. This complements the scalar `version_added` checks by + * flagging a child that is currently (or in `"preview"`) supported under the + * canonical name while the parent's canonical-name support exists but is no + * longer current (entirely `version_removed` or only available behind a flag). + * Prefixed and alternative-name statements are intentionally ignored, since + * BCD does not consistently propagate those to sub-features. + * @param {InternalCompatStatement} parentCompat The parent compat data + * @param {InternalCompatStatement} childCompat The child compat data + * @param {BrowserName} browser The browser to check + * @returns {InternalSimpleSupportStatement | null} The violating child + * statement, or `null` if the child range is contained + */ + checkParentRangeContainment(parentCompat, childCompat, browser) { + const childStatements = this.#resolveStatements( + childCompat?.support, + browser, + ); + const violatingChild = childStatements.find((statement) => + this.#isCurrentlySupported(statement), + ); + if (!violatingChild) { + return null; + } + + const parentStatements = this.#resolveStatements( + parentCompat?.support, + browser, + ); + const parentHasCanonicalSupport = parentStatements.some((statement) => + this.#isCanonicalName(statement), + ); + const parentCurrentlySupported = parentStatements.some((statement) => + this.#isCurrentlySupported(statement), + ); + + // Only flag when the parent actually records canonical-name support that is + // no longer current. If the parent lacks canonical support altogether + // (unsupported, or only prefixed/alternative-name), that is either handled + // by the `unsupported` check or is an intentional modelling choice. + if (parentHasCanonicalSupport && !parentCurrentlySupported) { + return violatingChild; + } + + return null; + } + /** * Checks a specific feature for errors * @param {InternalIdentifier} data The data to test @@ -252,6 +366,57 @@ export class ConsistencyChecker { } }); + // Test whether sub-features are supported in a version range that is not + // contained within the parent's supported version range. + inconsistentSubfeaturesByBrowser = {}; + + for (const subfeature of subfeatures) { + const subfeatureData = /** @type {InternalIdentifier} */ ( + query(subfeature, data) + ); + for (const browser of /** @type {BrowserName[]} */ ( + Object.keys(bcd.browsers) + )) { + if (!data.__compat || !subfeatureData.__compat) { + continue; + } + const violation = this.checkParentRangeContainment( + data.__compat, + subfeatureData.__compat, + browser, + ); + if (violation) { + inconsistentSubfeaturesByBrowser[browser] = + inconsistentSubfeaturesByBrowser[browser] || []; + inconsistentSubfeaturesByBrowser[browser]?.push([ + subfeature, + this.#formatSupport(violation), + ]); + } + } + } + + // Add errors + Object.keys(inconsistentSubfeaturesByBrowser).forEach((browser) => { + const subfeatures = + inconsistentSubfeaturesByBrowser[/** @type {BrowserName} */ (browser)]; + if (subfeatures) { + const parentCanonical = this.#resolveStatements( + data.__compat?.support, + /** @type {BrowserName} */ (browser), + ) + .filter((statement) => this.#isCanonicalName(statement)) + .map((statement) => this.#formatSupport(statement)) + .join(', '); + errors.push({ + type: 'subfeature_outside_parent_range', + browser: /** @type {BrowserName} */ (browser), + parentValue: parentCanonical, + subfeatures, + }); + } + }); + return errors; } @@ -455,6 +620,8 @@ export default { errorMessage += `No support in ${styleText('bold', browser)}, but support is declared in the following sub-feature(s):`; } else if (type == 'subfeature_earlier_implementation') { errorMessage += `Basic support in ${styleText('bold', browser)} was declared implemented in a later version (${styleText('bold', String(parentValue))}) than the following sub-feature(s):`; + } else if (type == 'subfeature_outside_parent_range') { + errorMessage += `Support range in ${styleText('bold', browser)} is not contained within the parent's support range (${styleText('bold', String(parentValue))}) for the following sub-feature(s):`; } for (const subfeature of subfeatures) { diff --git a/lint/linter/test-consistency.test.js b/lint/linter/test-consistency.test.js index 763a28c86e9b67..0f2dff9829ef07 100644 --- a/lint/linter/test-consistency.test.js +++ b/lint/linter/test-consistency.test.js @@ -81,3 +81,154 @@ describe('ConsistencyChecker.getVersionAdded()', () => { ); }); }); + +describe('ConsistencyChecker.checkParentRangeContainment()', () => { + /** + * Wrap support statements in a compat statement. + * @param {object} support The support block + * @returns {object} The compat statement + */ + const compat = (support) => ({ support }); + + it('flags a child in "preview" under a parent whose canonical support was removed', () => { + // The issue's own example (css.properties.line-clamp). + const parent = compat({ + safari: [ + { prefix: '-webkit-', version_added: '5' }, + { version_added: '18.2', version_removed: '18.4' }, + ], + }); + const child = compat({ safari: { version_added: 'preview' } }); + assert.deepEqual( + check.checkParentRangeContainment(parent, child, 'safari'), + { + version_added: 'preview', + }, + ); + }); + + it('flags an unflagged child in "preview" under a flag-gated parent', () => { + const parent = compat({ + safari: { + version_added: 'preview', + flags: [{ type: 'runtime_flag', name: 'useTemporal' }], + }, + }); + const child = compat({ safari: { version_added: 'preview' } }); + assert.ok(check.checkParentRangeContainment(parent, child, 'safari')); + }); + + it('flags an ongoing child under a parent whose canonical support was removed', () => { + const parent = compat({ + opera: { version_added: '10.6', version_removed: '15' }, + }); + const child = compat({ opera: { version_added: '16' } }); + assert.ok(check.checkParentRangeContainment(parent, child, 'opera')); + }); + + it('flags an unflagged child under a flag-gated parent', () => { + const parent = compat({ + deno: { + version_added: '2.2', + flags: [{ type: 'runtime_flag', name: '--unstable-net' }], + }, + }); + const child = compat({ deno: { version_added: '2.2' } }); + assert.ok(check.checkParentRangeContainment(parent, child, 'deno')); + }); + + it('resolves "mirror" before comparing', () => { + const parent = compat({ + chrome: { version_added: '10', version_removed: '20' }, + edge: 'mirror', + }); + const child = compat({ + chrome: { version_added: '25' }, + edge: 'mirror', + }); + assert.ok(check.checkParentRangeContainment(parent, child, 'edge')); + }); + + it('does not flag when the parent is currently supported', () => { + const parent = compat({ chrome: { version_added: '10' } }); + const child = compat({ chrome: { version_added: '12' } }); + assert.equal( + check.checkParentRangeContainment(parent, child, 'chrome'), + null, + ); + }); + + it('does not flag when the parent has no canonical support (only prefixed)', () => { + const parent = compat({ + safari: { prefix: 'webkit', version_added: '6' }, + }); + const child = compat({ safari: { version_added: '14' } }); + assert.equal( + check.checkParentRangeContainment(parent, child, 'safari'), + null, + ); + }); + + it('does not flag when the parent lacks the browser entirely', () => { + const parent = compat({ chrome: { version_added: '10' } }); + const child = compat({ firefox: { version_added: '12' } }); + assert.equal( + check.checkParentRangeContainment(parent, child, 'firefox'), + null, + ); + }); + + it('does not flag a prefixed or alternative-name child', () => { + const parent = compat({ + chrome: { version_added: '10', version_removed: '20' }, + }); + assert.equal( + check.checkParentRangeContainment( + parent, + compat({ chrome: { prefix: 'webkit', version_added: '25' } }), + 'chrome', + ), + null, + ); + assert.equal( + check.checkParentRangeContainment( + parent, + compat({ chrome: { alternative_name: 'legacy', version_added: '25' } }), + 'chrome', + ), + null, + ); + }); + + it('does not flag a flagged child under a flag-gated parent', () => { + const parent = compat({ + chrome: { + version_added: '10', + flags: [{ type: 'preference', name: 'test' }], + }, + }); + const child = compat({ + chrome: { + version_added: '12', + flags: [{ type: 'preference', name: 'test' }], + }, + }); + assert.equal( + check.checkParentRangeContainment(parent, child, 'chrome'), + null, + ); + }); + + it('does not flag a child that is itself removed', () => { + const parent = compat({ + chrome: { version_added: '10', version_removed: '20' }, + }); + const child = compat({ + chrome: { version_added: '12', version_removed: '18' }, + }); + assert.equal( + check.checkParentRangeContainment(parent, child, 'chrome'), + null, + ); + }); +});