Skip to content

Conversation

@ddbeck
Copy link
Collaborator

@ddbeck ddbeck commented Jul 3, 2024

This is another incremental step toward handling version ranges more completely, by taking a support statement and mapping it to every release that a browser has.

More specifically, this PR:

  • Adds toReleaseSupportMap() method to SupportStatements. It's very much like supportedBy(), except it has something to say about every version, not just the supporting versions. I've made some of supportedBy() reusable. Eventually, but not today, we'll rip out supportedBy() entirely.

  • Adds an inRange(start, end?) method to Releasess. This is a not-infrequent thing to check and remembering what >= 0 means is hard.

  • Remove some never-used code relating to ranges.

  • Handle bugged version_removed values in BCD. Basically, some values should never happen according to the schema, but are happening anyway. This won't be completely fixed until it gets fixed upstream.

The next step after this will be to reconcile multiple support statements at the feature level (i.e., filtering out "unsupported" entries when there's a competing "supported" entry) and do it across browsers (e.g., to create something like Map<Browser, Map<Release, Supported | Unsupported | UnknownSupport>>).

*/
toReleaseSupportMap(): ReleaseSupportMap {
if (this.browser === undefined) {
throw Error("This support statement's browser is unknown.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw Error("This support statement's browser is unknown.");
throw new Error("This support statement's browser is unknown.");

// returning `[this.browser.current()]` at least.
supportedBy(): { release: Release; qualifications?: Qualifications }[] {
if (this.browser === undefined) {
throw Error("This support statement's browser is unknown.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw Error("This support statement's browser is unknown.");
throw new Error("This support statement's browser is unknown.");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a problem in a bunch of places. I'll open a follow-up PR to fix this across the board.

throw Error("This support statement's browser is unknown.");
}

if (this.version_added === false || this.version_removed === true) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If version_removed is true, support is unknown for everything except the version_added version.

let startRanged = false;
if (this.version_added === true) {
startRanged = true;
start = this.browser.current();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, but if this code is actually used, our dist files will keep changing. Should we instead refuse to work with true values and wait for them to be fixed in BCD?

* Expand this support statement into a `Map` from `Release` objects to objects
* describing whether the release is supporting, unsupporting, or unknown.
*/
toReleaseSupportMap(): ReleaseSupportMap {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this expands a single support statement (not an array) into a map. That means the resulting map will always be simple, typically with an initial run of false values followed by a run or true values. Sometimes unknown support before a point, but never flip-flopping. Is a map really necessary to represent this, or should we just parse version_added and version_removed into a form that makes it easy to get support for a specific release?


let end: Release | undefined;
if (this.version_removed) {
end = this.browser.version(this.version_removed);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ranges here?

Comment on lines 162 to 174
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 });
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about putting this into a method to get support for a specific release? That's cheaper when the caller doesn't want to know about all releases.

* 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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@foolip I've refactored this as a method that just checks whether one release is supported. If one still wants to know the situation for every release, then that's a one-liner away:

new Map(browser.releases.map(r => [r, statement.supportedIn(r)]))

Also, doing this for any arbitrary support statement, while possible, was getting very tricky and hard to follow. Since we don't need this I decided it wasn't worth the effort, so it's only available on RealSupportStatements.

(I've kept some test cases and work from the all-values version, but it's not urgent, so I'll re-introduce that when there's an actual need or a spare couple of hours for it.)

@ddbeck ddbeck requested a review from foolip July 8, 2024 11:59
assert.throws(() => cr.version("50").inRange(fx.version("50")), Error);
});

it("handles closed ranges", function () {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test for when the version equals the end version, to show the range is exclusive of the end point?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with 7c2bbad.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not the 125 assert here, so it's the same asserts for unranged and ranged? (You might remove 101 if you want to reduce the number of rows.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with 4e0110b.

);

assert.equal(rangedEnd.supportedIn(cr.version("100")).supported, true);
assert.equal(rangedEnd.supportedIn(cr.version("124")).supported, null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also test 101 here to make it clear that there's just a single release we're sure about?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with 4e0110b.

return qualifications;
}

function isRangedVersion(s: any): s is string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only seen this syntax once before, but does s is string really do the right thing if this method returns false? Won't TypeScript infer that the argument wasn't a string even though it may have been?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this really does the right thing. Type predicates are (by themselves) one-way: if the function returns true, then the type is satisfied. The inverse (false → s is not string) is not inferred. Demo.

AFAICT, even in general TypeScript doesn't actually track the list of types something isn't, just the set of remaining possible types.

@foolip foolip merged commit f6171e4 into web-platform-dx:main Jul 9, 2024
@ddbeck ddbeck deleted the cb-exploder branch July 9, 2024 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants