From 3b28a74b3f8a13ba6972affaf3d70872beaee0e4 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 14:23:33 +0200 Subject: [PATCH 01/11] Make turning a statement into a list of qualifications reusable --- .../browser-compat-data/supportStatements.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index 4168096e868..7954154bea5 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -177,19 +177,7 @@ export class RealSupportStatement extends SupportStatement { ); // Release is on or after start and before the end } - let qualifications: Qualifications = {}; - if (this.data.prefix) { - qualifications.prefix = this.data.prefix; - } - if (this.data.alternative_name) { - qualifications.alternative_name = this.data.alternative_name; - } - if (this.flags.length) { - qualifications.flags = this.flags; - } - if (this.partial_implementation) { - qualifications.partial_implementation = this.partial_implementation; - } + let qualifications: Qualifications = statementToQualifications(this); if (Object.keys(qualifications).length) { return releases.map((release) => ({ release, qualifications })); } @@ -198,3 +186,22 @@ export class RealSupportStatement extends SupportStatement { })); } } + +function statementToQualifications( + statement: SupportStatement, +): Qualifications { + let qualifications: Qualifications = {}; + if (statement.data.prefix) { + qualifications.prefix = statement.data.prefix; + } + if (statement.data.alternative_name) { + qualifications.alternative_name = statement.data.alternative_name; + } + if (statement.flags.length) { + qualifications.flags = statement.flags; + } + if (statement.partial_implementation) { + qualifications.partial_implementation = statement.partial_implementation; + } + return qualifications; +} From 9f271ecf8afc87038323a5e1bc96069d454000a6 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 14:52:09 +0200 Subject: [PATCH 02/11] Add convenience method for checking if a release is in a range --- .../src/browser-compat-data/release.test.ts | 26 +++++++++++++++++++ .../src/browser-compat-data/release.ts | 13 ++++++++++ .../browser-compat-data/supportStatements.ts | 8 +++--- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/release.test.ts b/packages/compute-baseline/src/browser-compat-data/release.test.ts index 73b7b5146e8..968391337c4 100644 --- a/packages/compute-baseline/src/browser-compat-data/release.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/release.test.ts @@ -87,4 +87,30 @@ describe("Release", function () { assert.equal(safariPreview.isPrerelease(), true); }); }); + + describe("inRange()", function () { + it("handles closed ranges", function () { + const cr = browser("chrome"); + assert.equal( + cr.version("1").inRange(cr.version("1"), cr.version("125")), + true, + ); + assert.equal( + cr.version("1").inRange(cr.version("10"), cr.version("15")), + false, + ); + assert.equal( + cr.version("100").inRange(cr.version("10"), cr.version("15")), + false, + ); + }); + + it("handles open ranges", function () { + const cr = browser("chrome"); + assert.equal(cr.version("1").inRange(cr.version("1")), true); + assert.equal(cr.version("1").inRange(cr.version("10")), false); + assert.equal(cr.version("100").inRange(cr.version("10")), true); + assert.equal(cr.version("preview").inRange(cr.version("10")), true); + }); + }); }); diff --git a/packages/compute-baseline/src/browser-compat-data/release.ts b/packages/compute-baseline/src/browser-compat-data/release.ts index 3db2140e008..46636597476 100644 --- a/packages/compute-baseline/src/browser-compat-data/release.ts +++ b/packages/compute-baseline/src/browser-compat-data/release.ts @@ -44,6 +44,19 @@ export class Release { return this.releaseIndex - otherRelease.releaseIndex; } + /** + * Check if this release is the same as or after a starting release and, + * optionally, before an ending release. + */ + inRange(start: Release, end?: Release): boolean { + const onOrAfterStart = this.compare(start) >= 0; + if (end) { + const beforeEnd = this.compare(end) < 0; + return onOrAfterStart && beforeEnd; + } + return onOrAfterStart; + } + isPrerelease(): boolean { if (["beta", "nightly", "planned"].includes(this.data.status)) { return true; diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index 7954154bea5..68e352f2a6f 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -168,13 +168,11 @@ export class RealSupportStatement extends SupportStatement { } let releases; - if (this.version_removed === false) { - releases = this.browser.releases.filter((rel) => rel.compare(start) >= 0); // Release is on or after start + if (this.version_removed === undefined) { + releases = this.browser.releases.filter((rel) => rel.inRange(start)); } else { const end: Release = this.browser.version(this.version_removed); - releases = this.browser.releases.filter( - (rel) => rel.compare(start) >= 0 && rel.compare(end) < 0, - ); // Release is on or after start and before the end + releases = this.browser.releases.filter((rel) => rel.inRange(start, end)); } let qualifications: Qualifications = statementToQualifications(this); From 222fd8211b29e4dcec087baf3f8c4bd59e45b74a Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 16:37:36 +0200 Subject: [PATCH 03/11] `compute-baseline`: add method to expand a support statement into per-release support information --- .../supportStatements.test.ts | 99 +++++++++++++++++++ .../browser-compat-data/supportStatements.ts | 87 ++++++++++++++-- 2 files changed, 178 insertions(+), 8 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts index e315e873489..a5ed4a76025 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts @@ -101,6 +101,105 @@ describe("statements", function () { assert.equal(s.version_removed, false); }); }); + + describe("toReleaseSupportMap()", function () { + it("expands false values to unsupported", function () { + const cr = browser("chrome"); + const statement = new SupportStatement({ version_added: false }, cr); + const supportMap = statement.toReleaseSupportMap(); + + assert.equal(supportMap.size, cr.releases.length); + for (const { supported } of supportMap.values()) { + assert.equal(supported, false); + } + }); + + it("expands null values to unknown support", function () { + const cr = browser("chrome"); + const statement = new SupportStatement({ version_added: null }, cr); + const supportMap = statement.toReleaseSupportMap(); + + assert.equal(supportMap.size, cr.releases.length); + for (const { supported } of supportMap.values()) { + assert.equal(supported, null); + } + }); + + it("expands true values to supported in current and future releases", function () { + const cr = browser("chrome"); + const statement = new SupportStatement({ version_added: true }, cr); + const supportMap = statement.toReleaseSupportMap(); + + assert.equal(supportMap.size, cr.releases.length); + assert.equal(supportMap.get(cr.current())?.supported, true); + assert.equal( + supportMap.get(cr.releases.at(-1) as any)?.supported, + true, + ); + }); + + it("expands open-ended statements to (unsupported, …, supported, …)", function () { + const cr = browser("chrome"); + const statement = new SupportStatement({ version_added: "100" }, cr); + const supportMap = statement.toReleaseSupportMap(); + const entries = [...supportMap.entries()]; + + assert.equal(supportMap.size, entries.length); + assert.equal(supportMap.get(cr.version("1"))?.supported, false); + assert.equal(supportMap.get(cr.version("99"))?.supported, false); + assert.equal(supportMap.get(cr.version("100"))?.supported, true); + assert.equal(supportMap.get(cr.version("101"))?.supported, true); + assert.equal(supportMap.get(cr.current())?.supported, true); + assert.equal( + supportMap.get(cr.releases.at(-1) as any)?.supported, + true, + ); + }); + + it("expands ranged open-ended statements to (unknown, …, supported, …)", function () { + const cr = browser("chrome"); + const statement = new SupportStatement({ version_added: "≤100" }, cr); + const supportMap = statement.toReleaseSupportMap(); + const entries = [...supportMap.entries()]; + + assert.equal(supportMap.size, entries.length); + assert.equal(supportMap.get(cr.version("1"))?.supported, null); + assert.equal(supportMap.get(cr.version("99"))?.supported, null); + assert.equal(supportMap.get(cr.version("100"))?.supported, true); + assert.equal(supportMap.get(cr.version("101"))?.supported, true); + assert.equal(supportMap.get(cr.current())?.supported, true); + console.log(cr.releases.at(-1) as any); + console.log(supportMap.get(cr.releases.at(-1) as any)); + assert.equal( + supportMap.get(cr.releases.at(-1) as any)?.supported, + true, + ); + }); + + it("expands ranged closed statements to (unknown, …, supported, …, unsupported, …)", function () { + const cr = browser("chrome"); + const statement = new SupportStatement( + { version_added: "≤100", version_removed: "125" }, + cr, + ); + const supportMap = statement.toReleaseSupportMap(); + const entries = [...supportMap.entries()]; + + assert.equal(supportMap.size, entries.length); + assert.equal(supportMap.get(cr.version("1"))?.supported, null); + assert.equal(supportMap.get(cr.version("99"))?.supported, null); + assert.equal(supportMap.get(cr.version("100"))?.supported, true); + assert.equal(supportMap.get(cr.version("101"))?.supported, true); + assert.equal(supportMap.get(cr.version("124"))?.supported, true); + assert.equal(supportMap.get(cr.version("125"))?.supported, false); + assert.equal(supportMap.get(cr.version("126"))?.supported, false); + assert.equal(supportMap.get(cr.current())?.supported, false); + assert.equal( + supportMap.get(cr.releases.at(-1) as any)?.supported, + false, + ); + }); + }); }); describe("RealSupportStatement", function () { diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index 68e352f2a6f..5ad30a2971c 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -15,11 +15,14 @@ export interface Qualifications { partial_implementation?: true; } -// TODO: This stuff is slow, clunky, and weirdly indirect. It was helpful to get -// started (especially before I knew all of the variations that a given -// statement could express), but we might be better served by extracting -// `supportedBy()` into something like an `expandToReleases(browser: Browser, -// statement: SimpleSupportStatement)` function or similar. +export type Supported = { supported: true; qualifications?: Qualifications }; +export type Unsupported = { supported: false }; +export type UnknownSupport = { supported: null }; + +export type ReleaseSupportMap = Map< + Release, + Supported | Unsupported | UnknownSupport +>; export function statement( incoming: @@ -111,6 +114,77 @@ export class SupportStatement { // Strictness guarantee: unset version_removed returns false return this.data?.version_removed || false; } + + /** + * Expand this support statement into a `Map` from `Release` objects to objects + * describing whether the release is supporting, unsupporting, or unknown. + */ + toReleaseSupportMap(): ReleaseSupportMap { + if (this.browser === undefined) { + throw Error("This support statement's browser is unknown."); + } + + if (this.version_added === false || this.version_removed === true) { + return new Map( + this.browser.releases.map((r) => [r, { supported: false }]), + ); + } + + if (this.version_added === null) { + return new Map( + this.browser.releases.map((r) => [r, { supported: null }]), + ); + } + + if (this.version_added === true) { + const result = new Map(); + for (const r of this.browser.releases) { + if (r.inRange(this.browser.current())) { + result.set(r, { supported: true }); + } else { + result.set(r, { supported: false }); + } + } + return result; + } + + const result = new Map(); + + let start: Release; + let startRanged = false; + if (this.version_added.startsWith("≤")) { + startRanged = true; + start = this.browser.version(this.version_added.slice(1)); + } else { + start = this.browser.version(this.version_added); + } + + let end: Release | undefined; + if (this.version_removed) { + end = this.browser.version(this.version_removed); + } + + const qualifications = statementToQualifications(this); + const isQualified = Boolean(Object.keys(qualifications).length); + + for (const r of this.browser.releases) { + if (isQualified && r.inRange(start, end)) { + // Supported with qualifications + result.set(r, { supported: true, qualifications }); + } else if (r.inRange(start, end)) { + // Supported without qualification + result.set(r, { supported: true }); + } else if (startRanged && !r.inRange(start)) { + // Support unknown (before a ≤ version) + result.set(r, { supported: null }); + } else { + // Unsupported (outside a hard range) + result.set(r, { supported: false }); + } + } + + return result; + } } export class RealSupportStatement extends SupportStatement { @@ -148,9 +222,6 @@ export class RealSupportStatement extends SupportStatement { return super.version_removed as string | false; } - // TODO: `supportedBy()` ought to be (partially) implemented on non-real value - // support statements. For example, `"version_added": true` should allow for - // returning `[this.browser.current()]` at least. supportedBy(): { release: Release; qualifications?: Qualifications }[] { if (this.browser === undefined) { throw Error("This support statement's browser is unknown."); From 5129251bde0d4cfed337a49b2d60820276313430 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 16:55:05 +0200 Subject: [PATCH 04/11] Remove dead code relating to version ranges --- .../browser-compat-data/supportStatements.ts | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index 5ad30a2971c..c7ee0642395 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -71,26 +71,6 @@ export class SupportStatement { this.feature = feature; } - _isRanged(key: "version_added" | "version_removed" | undefined): boolean { - if (key === undefined) { - return ( - this._isRanged("version_added") || this._isRanged("version_removed") - ); - } - - const version = this.data?.[key]; - - if ( - typeof version === "boolean" || - version === undefined || - version === null - ) { - return false; - } - - return version.includes("≤"); - } - get flags(): FlagStatement[] { return this.data?.flags ?? []; } From fd5319051bbe8150d60d958bfbe2bddf46bdbb80 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 17:38:55 +0200 Subject: [PATCH 05/11] Handle `version_removed` values not matching schema --- .../supportStatements.test.ts | 12 +++++-- .../browser-compat-data/supportStatements.ts | 34 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts index a5ed4a76025..424f1f5c76a 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts @@ -96,10 +96,18 @@ describe("statements", function () { assert.equal(s.version_removed, "2"); }); - it("returns false for undefined", function () { - const s = new SupportStatement({ version_added: "1" }); + it("returns false", function () { + const s = new SupportStatement({ + version_added: "1", + version_removed: false, + }); assert.equal(s.version_removed, false); }); + + it("returns undefined", function () { + const s = new SupportStatement({ version_added: "1" }); + assert.equal(s.version_removed, undefined); + }); }); describe("toReleaseSupportMap()", function () { diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index c7ee0642395..a64411b5b2f 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -90,9 +90,29 @@ export class SupportStatement { return this.data?.version_added; } - get version_removed(): VersionValue { - // Strictness guarantee: unset version_removed returns false - return this.data?.version_removed || false; + get version_removed(): string | boolean | undefined { + const value = this.data?.version_removed; + + // TODO: Report and fix upstream bug in BCD, then uncomment or drop the code + // below + + // According to @mdn/browser-compat-data's schema, `version_removed` values + // should only be `true` or strings. In practice (and according to the + // exported types), this is not the case, because mirroring inserts `false` + // values. + + // if (value === null || value === false) { + // throw new Error( + // "`version_added` should never be `null` or `false`. This is a bug, so please file an issue!", + // ); + // } + if (value === null) { + throw new Error( + "`version_added` should never be `null`. This is a bug, so please file an issue!", + ); + } + + return value; } /** @@ -194,12 +214,12 @@ export class RealSupportStatement extends SupportStatement { } } - get version_added() { + get version_added(): string | false { return super.version_added as string | false; } - get version_removed() { - return super.version_removed as string | false; + get version_removed(): string | false | undefined { + return super.version_removed as string | false | undefined; } supportedBy(): { release: Release; qualifications?: Qualifications }[] { @@ -219,7 +239,7 @@ export class RealSupportStatement extends SupportStatement { } let releases; - if (this.version_removed === undefined) { + if (this.version_removed === undefined || this.version_removed === false) { releases = this.browser.releases.filter((rel) => rel.inRange(start)); } else { const end: Release = this.browser.version(this.version_removed); From c1124f90689c12f0bc2d7486a242a06c1929c627 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 3 Jul 2024 22:11:43 +0200 Subject: [PATCH 06/11] =?UTF-8?q?Treat=20`version=5Fadded:=20true`=20like?= =?UTF-8?q?=20`version=5Fadded:=20"=E2=89=A4current"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browser-compat-data/supportStatements.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index a64411b5b2f..9d5c23d221f 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -136,23 +136,14 @@ export class SupportStatement { ); } - if (this.version_added === true) { - const result = new Map(); - for (const r of this.browser.releases) { - if (r.inRange(this.browser.current())) { - result.set(r, { supported: true }); - } else { - result.set(r, { supported: false }); - } - } - return result; - } - const result = new Map(); let start: Release; let startRanged = false; - if (this.version_added.startsWith("≤")) { + if (this.version_added === true) { + startRanged = true; + start = this.browser.current(); + } else if (this.version_added.startsWith("≤")) { startRanged = true; start = this.browser.version(this.version_added.slice(1)); } else { From 72dc14ba87d006a914c270542e0ca0b7acdfe5db Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Fri, 5 Jul 2024 10:50:18 +0200 Subject: [PATCH 07/11] Test comparing releases across browers (throws) --- .../src/browser-compat-data/release.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/compute-baseline/src/browser-compat-data/release.test.ts b/packages/compute-baseline/src/browser-compat-data/release.test.ts index 968391337c4..795c9007d3a 100644 --- a/packages/compute-baseline/src/browser-compat-data/release.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/release.test.ts @@ -40,6 +40,16 @@ describe("Release", function () { }); describe("compare()", function () { + it("throws when comparing between two browsers", function () { + const cr = browser("chrome"); + const ed = browser("edge"); + + assert.throws( + () => cr.version("100").inRange(ed.version("79"), cr.version("125")), + Error, + ); + }); + it("returns 0 for equivalent releases", function () { const chrome100 = browser("chrome").version("100"); assert.equal(chrome100.compare(chrome100), 0); @@ -89,6 +99,13 @@ describe("Release", function () { }); describe("inRange()", function () { + it("throws when comparing between two browsers", function () { + const cr = browser("chrome"); + const fx = browser("firefox"); + + assert.throws(() => cr.version("50").inRange(fx.version("50")), Error); + }); + it("handles closed ranges", function () { const cr = browser("chrome"); assert.equal( From 1b41409fa4ae9c944b76272a0869b3cb49da521c Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Mon, 8 Jul 2024 13:50:59 +0200 Subject: [PATCH 08/11] =?UTF-8?q?Refactor=20`toSupportReleaseMap()`=20to?= =?UTF-8?q?=20`supportedIn(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../supportStatements.test.ts | 242 +++++++++++------- .../browser-compat-data/supportStatements.ts | 174 ++++++++----- 2 files changed, 250 insertions(+), 166 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts index 424f1f5c76a..72d7c0a177d 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts @@ -109,105 +109,6 @@ describe("statements", function () { assert.equal(s.version_removed, undefined); }); }); - - describe("toReleaseSupportMap()", function () { - it("expands false values to unsupported", function () { - const cr = browser("chrome"); - const statement = new SupportStatement({ version_added: false }, cr); - const supportMap = statement.toReleaseSupportMap(); - - assert.equal(supportMap.size, cr.releases.length); - for (const { supported } of supportMap.values()) { - assert.equal(supported, false); - } - }); - - it("expands null values to unknown support", function () { - const cr = browser("chrome"); - const statement = new SupportStatement({ version_added: null }, cr); - const supportMap = statement.toReleaseSupportMap(); - - assert.equal(supportMap.size, cr.releases.length); - for (const { supported } of supportMap.values()) { - assert.equal(supported, null); - } - }); - - it("expands true values to supported in current and future releases", function () { - const cr = browser("chrome"); - const statement = new SupportStatement({ version_added: true }, cr); - const supportMap = statement.toReleaseSupportMap(); - - assert.equal(supportMap.size, cr.releases.length); - assert.equal(supportMap.get(cr.current())?.supported, true); - assert.equal( - supportMap.get(cr.releases.at(-1) as any)?.supported, - true, - ); - }); - - it("expands open-ended statements to (unsupported, …, supported, …)", function () { - const cr = browser("chrome"); - const statement = new SupportStatement({ version_added: "100" }, cr); - const supportMap = statement.toReleaseSupportMap(); - const entries = [...supportMap.entries()]; - - assert.equal(supportMap.size, entries.length); - assert.equal(supportMap.get(cr.version("1"))?.supported, false); - assert.equal(supportMap.get(cr.version("99"))?.supported, false); - assert.equal(supportMap.get(cr.version("100"))?.supported, true); - assert.equal(supportMap.get(cr.version("101"))?.supported, true); - assert.equal(supportMap.get(cr.current())?.supported, true); - assert.equal( - supportMap.get(cr.releases.at(-1) as any)?.supported, - true, - ); - }); - - it("expands ranged open-ended statements to (unknown, …, supported, …)", function () { - const cr = browser("chrome"); - const statement = new SupportStatement({ version_added: "≤100" }, cr); - const supportMap = statement.toReleaseSupportMap(); - const entries = [...supportMap.entries()]; - - assert.equal(supportMap.size, entries.length); - assert.equal(supportMap.get(cr.version("1"))?.supported, null); - assert.equal(supportMap.get(cr.version("99"))?.supported, null); - assert.equal(supportMap.get(cr.version("100"))?.supported, true); - assert.equal(supportMap.get(cr.version("101"))?.supported, true); - assert.equal(supportMap.get(cr.current())?.supported, true); - console.log(cr.releases.at(-1) as any); - console.log(supportMap.get(cr.releases.at(-1) as any)); - assert.equal( - supportMap.get(cr.releases.at(-1) as any)?.supported, - true, - ); - }); - - it("expands ranged closed statements to (unknown, …, supported, …, unsupported, …)", function () { - const cr = browser("chrome"); - const statement = new SupportStatement( - { version_added: "≤100", version_removed: "125" }, - cr, - ); - const supportMap = statement.toReleaseSupportMap(); - const entries = [...supportMap.entries()]; - - assert.equal(supportMap.size, entries.length); - assert.equal(supportMap.get(cr.version("1"))?.supported, null); - assert.equal(supportMap.get(cr.version("99"))?.supported, null); - assert.equal(supportMap.get(cr.version("100"))?.supported, true); - assert.equal(supportMap.get(cr.version("101"))?.supported, true); - assert.equal(supportMap.get(cr.version("124"))?.supported, true); - assert.equal(supportMap.get(cr.version("125"))?.supported, false); - assert.equal(supportMap.get(cr.version("126"))?.supported, false); - assert.equal(supportMap.get(cr.current())?.supported, false); - assert.equal( - supportMap.get(cr.releases.at(-1) as any)?.supported, - false, - ); - }); - }); }); describe("RealSupportStatement", function () { @@ -270,6 +171,7 @@ describe("statements", function () { ); }); }); + describe("#supportedBy", function () { it("returns an array of releases represented by the statement", function () { const st = new RealSupportStatement( @@ -293,5 +195,147 @@ describe("statements", function () { assert.equal(rels.length, browser("chrome").releases.length - 10); }); }); + + describe("supportedIn()", function () { + it("throws when browser is undefined", function () { + const cr = browser("chrome"); + const statement = new RealSupportStatement({ version_added: "1" }); + assert.throws(() => statement.supportedIn(cr.current()), Error); + }); + + it("throws when release does not correspond to the statement's browser", function () { + const statement = new RealSupportStatement( + { version_added: "1" }, + browser("chrome"), + ); + assert.throws( + () => statement.supportedIn(browser("firefox").current()), + Error, + ); + }); + + it("returns supported when release is on after version_added", function () { + const cr = browser("chrome"); + const unranged = new RealSupportStatement({ version_added: "100" }, cr); + const ranged = new RealSupportStatement({ version_added: "≤100" }, cr); + + assert.equal(unranged.supportedIn(cr.version("100")).supported, true); + assert.equal(unranged.supportedIn(cr.version("101")).supported, true); + assert.equal(unranged.supportedIn(cr.current()).supported, true); + assert.equal( + unranged.supportedIn(cr.releases.at(-1) as any).supported, + true, + ); + + assert.equal(ranged.supportedIn(cr.version("99")).supported, null); + assert.equal(ranged.supportedIn(cr.version("100")).supported, true); + assert.equal(ranged.supportedIn(cr.version("101")).supported, true); + assert.equal(ranged.supportedIn(cr.current()).supported, true); + assert.equal( + ranged.supportedIn(cr.releases.at(-1) as any).supported, + true, + ); + }); + + it("returns supported when release is on after version_added and before version_removed", function () { + const cr = browser("chrome"); + const unranged = new RealSupportStatement( + { version_added: "100", version_removed: "125" }, + cr, + ); + const ranged = new RealSupportStatement( + { version_added: "≤100", version_removed: "125" }, + cr, + ); + + assert.equal(unranged.supportedIn(cr.version("99")).supported, false); + assert.equal(unranged.supportedIn(cr.version("100")).supported, true); + assert.equal(unranged.supportedIn(cr.version("101")).supported, true); + assert.equal(unranged.supportedIn(cr.version("124")).supported, true); + assert.equal(unranged.supportedIn(cr.version("125")).supported, false); + + assert.equal(ranged.supportedIn(cr.version("99")).supported, null); + assert.equal(ranged.supportedIn(cr.version("100")).supported, true); + assert.equal(ranged.supportedIn(cr.version("101")).supported, true); + assert.equal(ranged.supportedIn(cr.version("124")).supported, true); + }); + + it("returns unknown support when release is before ranged version_added", function () { + const cr = browser("chrome"); + const rangedOpen = new RealSupportStatement( + { version_added: "≤100" }, + cr, + ); + const rangedClosed = new RealSupportStatement( + { version_added: "≤100", version_removed: "125" }, + cr, + ); + + assert.equal(rangedOpen.supportedIn(cr.version("99")).supported, null); + assert.equal( + rangedClosed.supportedIn(cr.version("99")).supported, + null, + ); + }); + + it("returns unknown support when release is after version_added and before ranged version_removed", function () { + const cr = browser("chrome"); + const rangedEnd = new RealSupportStatement( + { version_added: "100", version_removed: "≤125" }, + cr, + ); + + assert.equal(rangedEnd.supportedIn(cr.version("100")).supported, true); + assert.equal(rangedEnd.supportedIn(cr.version("124")).supported, null); + assert.equal(rangedEnd.supportedIn(cr.version("125")).supported, false); + }); + + it("returns unsupported when statement is version_added false", function () { + const cr = browser("chrome"); + const statement = new RealSupportStatement( + { version_added: false }, + cr, + ); + + const allReleases = cr.releases.map((r) => statement.supportedIn(r)); + for (const { supported } of allReleases) { + assert.equal(supported, false); + } + }); + + it("returns unsupported when release is before fixed version_added", function () { + const cr = browser("chrome"); + const unranged = new RealSupportStatement({ version_added: "100" }, cr); + assert.equal(unranged.supportedIn(cr.version("99")).supported, false); + }); + + it("returns unsupported when release is on or after version_removed", function () { + const cr = browser("chrome"); + + const unranged = new RealSupportStatement( + { version_added: "1", version_removed: "10" }, + cr, + ); + assert.equal(unranged.supportedIn(cr.version("10")).supported, false); + assert.equal(unranged.supportedIn(cr.version("11")).supported, false); + assert.equal(unranged.supportedIn(cr.current()).supported, false); + assert.equal( + unranged.supportedIn(cr.releases.at(-1) as any).supported, + false, + ); + + const ranged = new RealSupportStatement( + { version_added: "≤5", version_removed: "10" }, + cr, + ); + assert.equal(ranged.supportedIn(cr.version("10")).supported, false); + assert.equal(ranged.supportedIn(cr.version("11")).supported, false); + assert.equal(ranged.supportedIn(cr.current()).supported, false); + assert.equal( + ranged.supportedIn(cr.releases.at(-1) as any).supported, + false, + ); + }); + }); }); }); diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts index 9d5c23d221f..244255e1eea 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.ts @@ -19,11 +19,6 @@ export type Supported = { supported: true; qualifications?: Qualifications }; export type Unsupported = { supported: false }; export type UnknownSupport = { supported: null }; -export type ReleaseSupportMap = Map< - Release, - Supported | Unsupported | UnknownSupport ->; - export function statement( incoming: | Partial @@ -114,68 +109,6 @@ export class SupportStatement { return value; } - - /** - * Expand this support statement into a `Map` from `Release` objects to objects - * describing whether the release is supporting, unsupporting, or unknown. - */ - toReleaseSupportMap(): ReleaseSupportMap { - if (this.browser === undefined) { - throw Error("This support statement's browser is unknown."); - } - - if (this.version_added === false || this.version_removed === true) { - return new Map( - this.browser.releases.map((r) => [r, { supported: false }]), - ); - } - - if (this.version_added === null) { - return new Map( - this.browser.releases.map((r) => [r, { supported: null }]), - ); - } - - const result = new Map(); - - let start: Release; - let startRanged = false; - if (this.version_added === true) { - startRanged = true; - start = this.browser.current(); - } else if (this.version_added.startsWith("≤")) { - startRanged = true; - start = this.browser.version(this.version_added.slice(1)); - } else { - start = this.browser.version(this.version_added); - } - - let end: Release | undefined; - if (this.version_removed) { - end = this.browser.version(this.version_removed); - } - - const qualifications = statementToQualifications(this); - const isQualified = Boolean(Object.keys(qualifications).length); - - for (const r of this.browser.releases) { - if (isQualified && r.inRange(start, end)) { - // Supported with qualifications - result.set(r, { supported: true, qualifications }); - } else if (r.inRange(start, end)) { - // Supported without qualification - result.set(r, { supported: true }); - } else if (startRanged && !r.inRange(start)) { - // Support unknown (before a ≤ version) - result.set(r, { supported: null }); - } else { - // Unsupported (outside a hard range) - result.set(r, { supported: false }); - } - } - - return result; - } } export class RealSupportStatement extends SupportStatement { @@ -213,6 +146,105 @@ export class RealSupportStatement extends SupportStatement { return super.version_removed as string | false | undefined; } + /** + * Find out whether this support statement says a given browser release is + * supported (with or without qualifications), unsupported, or unknown. + */ + supportedIn(release: Release): Supported | Unsupported | UnknownSupport { + if (this.browser === undefined) { + throw new Error("This support statement's browser is unknown."); + } + + if (release.browser !== this.browser) { + throw new Error( + "Browser-release mismatch. The release is not part of the statement's browser's set of releases.", + ); + } + + if (this.version_added === false) { + return { supported: false }; + } + + // From here, some releases might be supporting + const qualifications = statementToQualifications(this); + const asSupported = Boolean(Object.keys(qualifications).length) + ? { supported: true, qualifications } + : { supported: true }; + + // Let's deal with the most fiendish case first: + // { version_added: "≤", version_removed: "≤…" } + // That is, a case where unknown values are in two version ranges: + // - Supported in version_added + // - Unsupported from version_removed + // - Unknown before version_added + // - Unknown from version_added + 1 to removed (exclusive) + if ( + isRangedVersion(this.version_added) && + isRangedVersion(this.version_removed) + ) { + const supportedIn = this.browser.version( + this.version_added.replaceAll("≤", ""), + ) as Release; + const unsupportedFrom = this.browser.version( + this.version_removed.replaceAll("≤", ""), + ) as Release; + + if (release === supportedIn) { + return asSupported; + } + if (release.inRange(unsupportedFrom)) { + return { supported: false }; + } + return { supported: null }; + } + + const initial = this.browser.releases[0] as Release; + + // The other fiendish case is: + // { version_added: "…", version_removed: "≤…" } + // That is, cases such that: + // - Supported in version_added + // - Unsupported before version_added + // - Unsupported from version_removed + // - Unknown from version_added + 1 to removed (exclusive) + if ( + isFixedVersion(this.version_added) && + isRangedVersion(this.version_removed) + ) { + const supportedIn = this.browser.version(this.version_added); + const unsupportedFrom = this.browser.version( + this.version_removed.replaceAll("≤", ""), + ) as Release; + + if (release === supportedIn) { + return asSupported; + } + if ( + release.inRange(unsupportedFrom) || + release.inRange(initial, supportedIn) + ) { + return { supported: false }; + } + return { supported: null }; + } + + const start = this.browser.version(this.version_added.replaceAll("≤", "")); + const startRanged = isRangedVersion(this.version_added); + + const end: Release | undefined = + typeof this.version_removed === "string" + ? this.browser.version(this.version_removed) + : undefined; + + if (release.inRange(start, end)) { + return asSupported; + } + if (startRanged && release.inRange(initial, start)) { + return { supported: null }; + } + return { supported: false }; + } + supportedBy(): { release: Release; qualifications?: Qualifications }[] { if (this.browser === undefined) { throw Error("This support statement's browser is unknown."); @@ -265,3 +297,11 @@ function statementToQualifications( } return qualifications; } + +function isRangedVersion(s: any): s is string { + return typeof s === "string" && s.startsWith("≤"); +} + +function isFixedVersion(s: any): s is string { + return typeof s === "string" && !s.startsWith("≤"); +} From 3cfd35d8fd4d94c9ce7e63c1a84994ec59bc7067 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 9 Jul 2024 14:25:17 +0200 Subject: [PATCH 09/11] Simplify test case for unsupported on `version_added: false` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philip Jägenstedt --- .../src/browser-compat-data/supportStatements.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts index 72d7c0a177d..e7e1be18076 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts @@ -297,9 +297,8 @@ describe("statements", function () { cr, ); - const allReleases = cr.releases.map((r) => statement.supportedIn(r)); - for (const { supported } of allReleases) { - assert.equal(supported, false); + for (const release of cr.releases) { + assert.equal(statement.supportedIn(release).supported, false); } }); From 7c2bbadf46f97d8ab98e20bc2b23202461cde0cf Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 9 Jul 2024 14:21:18 +0200 Subject: [PATCH 10/11] Add test (and comments) for `inRange` end exclusivity --- .../src/browser-compat-data/release.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/compute-baseline/src/browser-compat-data/release.test.ts b/packages/compute-baseline/src/browser-compat-data/release.test.ts index 795c9007d3a..bd6860c647c 100644 --- a/packages/compute-baseline/src/browser-compat-data/release.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/release.test.ts @@ -108,10 +108,19 @@ describe("Release", function () { it("handles closed ranges", function () { const cr = browser("chrome"); + + // Start of range is inclusive assert.equal( cr.version("1").inRange(cr.version("1"), cr.version("125")), true, ); + + // End of range is exclusive + assert.equal( + cr.version("20").inRange(cr.version("1"), cr.version("20")), + false, + ); + assert.equal( cr.version("1").inRange(cr.version("10"), cr.version("15")), false, From 4e0110b52d7ae60a9ced2e0c19f54ea58b8fa0cb Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 9 Jul 2024 14:23:44 +0200 Subject: [PATCH 11/11] Improve consistency/symmetry of tests --- .../src/browser-compat-data/supportStatements.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts index e7e1be18076..fb7d0e21bf6 100644 --- a/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts +++ b/packages/compute-baseline/src/browser-compat-data/supportStatements.test.ts @@ -258,6 +258,7 @@ describe("statements", function () { assert.equal(ranged.supportedIn(cr.version("100")).supported, true); assert.equal(ranged.supportedIn(cr.version("101")).supported, true); assert.equal(ranged.supportedIn(cr.version("124")).supported, true); + assert.equal(unranged.supportedIn(cr.version("125")).supported, false); }); it("returns unknown support when release is before ranged version_added", function () { @@ -286,6 +287,7 @@ describe("statements", function () { ); assert.equal(rangedEnd.supportedIn(cr.version("100")).supported, true); + assert.equal(rangedEnd.supportedIn(cr.version("101")).supported, null); assert.equal(rangedEnd.supportedIn(cr.version("124")).supported, null); assert.equal(rangedEnd.supportedIn(cr.version("125")).supported, false); });