From e662b2f393c8368169443ac4ea4e99c5ea2a9bf0 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Tue, 4 Mar 2025 15:55:20 -0700 Subject: [PATCH] Version range compression utils (#2840) * DNF normalization wip * a bunch of wip stuff * it is alive! * tests * deduplicate strings in tests * fix != flavor behavior & parse flavor constraints & equals shorthand for normalize * use normalization * more comments & fix tests not running because of bad rebase * fix comments+tests * slightly better comment * fix dependency & typos --------- Co-authored-by: Aiden McClelland --- sdk/base/lib/exver/exver.pegjs | 12 +- sdk/base/lib/exver/exver.ts | 308 ++++++----- sdk/base/lib/exver/index.ts | 506 +++++++++++++++++- .../test/{exverList.test.ts => exver.test.ts} | 47 ++ sdk/base/package-lock.json | 19 + sdk/base/package.json | 3 +- sdk/package/lib/version/VersionGraph.ts | 40 +- 7 files changed, 779 insertions(+), 156 deletions(-) rename sdk/base/lib/test/{exverList.test.ts => exver.test.ts} (83%) diff --git a/sdk/base/lib/exver/exver.pegjs b/sdk/base/lib/exver/exver.pegjs index fc11bb9b5..e9dbd9523 100644 --- a/sdk/base/lib/exver/exver.pegjs +++ b/sdk/base/lib/exver/exver.pegjs @@ -14,6 +14,7 @@ VersionRangeAtom / Not / Any / None + / FlavorAtom Parens = "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } } @@ -24,13 +25,16 @@ Anchor VersionSpec = flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } } +FlavorAtom + = "#" flavor:Lowercase { return { type: "Flavor", flavor: flavor } } + Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }} Any = "*" { return { type: "Any" } } None = "!" { return { type: "None" } } -CmpOp +CmpOp = ">=" { return ">="; } / "<=" { return "<="; } / ">" { return ">"; } @@ -89,7 +93,7 @@ String Version = number:VersionNumber prerelease: PreRelease? { - return { + return { number, prerelease: prerelease || [] }; @@ -106,7 +110,7 @@ PreReleaseSegment } VersionNumber - = first:Digit rest:("." Digit)* { + = first:Digit rest:("." Digit)* { return [first].concat(rest.map(r => r[1])); } @@ -114,4 +118,4 @@ Digit = [0-9]+ { return parseInt(text(), 10); } _ "whitespace" - = [ \t\n\r]* \ No newline at end of file + = [ \t\n\r]* diff --git a/sdk/base/lib/exver/exver.ts b/sdk/base/lib/exver/exver.ts index 60f766d69..9bb2a776a 100644 --- a/sdk/base/lib/exver/exver.ts +++ b/sdk/base/lib/exver/exver.ts @@ -296,7 +296,7 @@ function peg$parse(input, options) { var peg$source = options.grammarSource; // @ts-ignore - var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmverVersionRange: peg$parseEmverVersionRange, EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, EmverParens: peg$parseEmverParens, EmverAnchor: peg$parseEmverAnchor, EmverNot: peg$parseEmverNot, Emver: peg$parseEmver, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, FlavorAtom: peg$parseFlavorAtom, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmverVersionRange: peg$parseEmverVersionRange, EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, EmverParens: peg$parseEmverParens, EmverAnchor: peg$parseEmverAnchor, EmverNot: peg$parseEmverNot, Emver: peg$parseEmver, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; // @ts-ignore var peg$startRuleFunction = peg$parseVersionRange; @@ -306,18 +306,18 @@ function peg$parse(input, options) { var peg$c2 = "("; var peg$c3 = ")"; var peg$c4 = ":"; - var peg$c5 = "!"; - var peg$c6 = "*"; - var peg$c7 = ">="; - var peg$c8 = "<="; - var peg$c9 = ">"; - var peg$c10 = "<"; - var peg$c11 = "="; - var peg$c12 = "!="; - var peg$c13 = "^"; - var peg$c14 = "~"; - var peg$c15 = "."; - var peg$c16 = "#"; + var peg$c5 = "#"; + var peg$c6 = "!"; + var peg$c7 = "*"; + var peg$c8 = ">="; + var peg$c9 = "<="; + var peg$c10 = ">"; + var peg$c11 = "<"; + var peg$c12 = "="; + var peg$c13 = "!="; + var peg$c14 = "^"; + var peg$c15 = "~"; + var peg$c16 = "."; var peg$c17 = "-"; var peg$r0 = /^[a-z]/; @@ -330,18 +330,18 @@ function peg$parse(input, options) { var peg$e2 = peg$literalExpectation("(", false); var peg$e3 = peg$literalExpectation(")", false); var peg$e4 = peg$literalExpectation(":", false); - var peg$e5 = peg$literalExpectation("!", false); - var peg$e6 = peg$literalExpectation("*", false); - var peg$e7 = peg$literalExpectation(">=", false); - var peg$e8 = peg$literalExpectation("<=", false); - var peg$e9 = peg$literalExpectation(">", false); - var peg$e10 = peg$literalExpectation("<", false); - var peg$e11 = peg$literalExpectation("=", false); - var peg$e12 = peg$literalExpectation("!=", false); - var peg$e13 = peg$literalExpectation("^", false); - var peg$e14 = peg$literalExpectation("~", false); - var peg$e15 = peg$literalExpectation(".", false); - var peg$e16 = peg$literalExpectation("#", false); + var peg$e5 = peg$literalExpectation("#", false); + var peg$e6 = peg$literalExpectation("!", false); + var peg$e7 = peg$literalExpectation("*", false); + var peg$e8 = peg$literalExpectation(">=", false); + var peg$e9 = peg$literalExpectation("<=", false); + var peg$e10 = peg$literalExpectation(">", false); + var peg$e11 = peg$literalExpectation("<", false); + var peg$e12 = peg$literalExpectation("=", false); + var peg$e13 = peg$literalExpectation("!=", false); + var peg$e14 = peg$literalExpectation("^", false); + var peg$e15 = peg$literalExpectation("~", false); + var peg$e16 = peg$literalExpectation(".", false); var peg$e17 = peg$classExpectation([["a", "z"]], false, false); var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); var peg$e19 = peg$literalExpectation("-", false); @@ -359,57 +359,60 @@ function peg$parse(input, options) { var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore - var peg$f3 = function(value) {// @ts-ignore + var peg$f3 = function(flavor) {// @ts-ignore + return { type: "Flavor", flavor: flavor } };// @ts-ignore + + var peg$f4 = function(value) {// @ts-ignore return { type: "Not", value: value }};// @ts-ignore - var peg$f4 = function() {// @ts-ignore + var peg$f5 = function() {// @ts-ignore return { type: "Any" } };// @ts-ignore - var peg$f5 = function() {// @ts-ignore + var peg$f6 = function() {// @ts-ignore return { type: "None" } };// @ts-ignore - var peg$f6 = function() {// @ts-ignore + var peg$f7 = function() {// @ts-ignore return ">="; };// @ts-ignore - var peg$f7 = function() {// @ts-ignore + var peg$f8 = function() {// @ts-ignore return "<="; };// @ts-ignore - var peg$f8 = function() {// @ts-ignore + var peg$f9 = function() {// @ts-ignore return ">"; };// @ts-ignore - var peg$f9 = function() {// @ts-ignore + var peg$f10 = function() {// @ts-ignore return "<"; };// @ts-ignore - var peg$f10 = function() {// @ts-ignore + var peg$f11 = function() {// @ts-ignore return "="; };// @ts-ignore - var peg$f11 = function() {// @ts-ignore + var peg$f12 = function() {// @ts-ignore return "!="; };// @ts-ignore - var peg$f12 = function() {// @ts-ignore + var peg$f13 = function() {// @ts-ignore return "^"; };// @ts-ignore - var peg$f13 = function() {// @ts-ignore + var peg$f14 = function() {// @ts-ignore return "~"; };// @ts-ignore - var peg$f14 = function(flavor, upstream, downstream) { + var peg$f15 = function(flavor, upstream, downstream) { // @ts-ignore return { flavor: flavor || null, upstream, downstream } };// @ts-ignore - var peg$f15 = function(expr) {// @ts-ignore + var peg$f16 = function(expr) {// @ts-ignore return { type: "Parens", expr } };// @ts-ignore - var peg$f16 = function(operator, version) {// @ts-ignore + var peg$f17 = function(operator, version) {// @ts-ignore return { type: "Anchor", operator, version } };// @ts-ignore - var peg$f17 = function(value) {// @ts-ignore + var peg$f18 = function(value) {// @ts-ignore return { type: "Not", value: value }};// @ts-ignore - var peg$f18 = function(major, minor, patch, revision) {// @ts-ignore + var peg$f19 = function(major, minor, patch, revision) {// @ts-ignore return revision };// @ts-ignore - var peg$f19 = function(major, minor, patch, revision) { + var peg$f20 = function(major, minor, patch, revision) { // @ts-ignore return { // @ts-ignore @@ -431,18 +434,18 @@ function peg$parse(input, options) { } };// @ts-ignore - var peg$f20 = function(flavor) {// @ts-ignore + var peg$f21 = function(flavor) {// @ts-ignore return flavor };// @ts-ignore - var peg$f21 = function() {// @ts-ignore + var peg$f22 = function() {// @ts-ignore return text() };// @ts-ignore - var peg$f22 = function() {// @ts-ignore + var peg$f23 = function() {// @ts-ignore return text(); };// @ts-ignore - var peg$f23 = function(number, prerelease) { + var peg$f24 = function(number, prerelease) { // @ts-ignore - return { + return { // @ts-ignore number, // @ts-ignore @@ -450,22 +453,22 @@ function peg$parse(input, options) { }; };// @ts-ignore - var peg$f24 = function(first, rest) { + var peg$f25 = function(first, rest) { // @ts-ignore return [first].concat(rest.map(r => r[1])); };// @ts-ignore - var peg$f25 = function(segment) { + var peg$f26 = function(segment) { // @ts-ignore return segment; };// @ts-ignore - var peg$f26 = function(first, rest) { + var peg$f27 = function(first, rest) { // @ts-ignore return [first].concat(rest.map(r => r[1])); };// @ts-ignore - var peg$f27 = function() {// @ts-ignore + var peg$f28 = function() {// @ts-ignore return parseInt(text(), 10); }; // @ts-ignore var peg$currPos = 0; @@ -928,6 +931,11 @@ peg$parseVersionRangeAtom() { if (s0 === peg$FAILED) { // @ts-ignore s0 = peg$parseNone(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseFlavorAtom(); + } } } } @@ -1131,14 +1139,14 @@ peg$parseVersionSpec() { // @ts-ignore function // @ts-ignore -peg$parseNot() { +peg$parseFlavorAtom() { // @ts-ignore - var s0, s1, s2, s3; + var s0, s1, s2; // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.charCodeAt(peg$currPos) === 33) { + if (input.charCodeAt(peg$currPos) === 35) { // @ts-ignore s1 = peg$c5; // @ts-ignore @@ -1152,6 +1160,56 @@ peg$parseNot() { } // @ts-ignore if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseLowercase(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { // @ts-ignore s2 = peg$parse_(); // @ts-ignore @@ -1161,7 +1219,7 @@ peg$parseNot() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f3(s3); + s0 = peg$f4(s3); // @ts-ignore } else { // @ts-ignore @@ -1192,7 +1250,7 @@ peg$parseAny() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 42) { // @ts-ignore - s1 = peg$c6; + s1 = peg$c7; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1200,14 +1258,14 @@ peg$parseAny() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e6); } + if (peg$silentFails === 0) { peg$fail(peg$e7); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f4(); + s1 = peg$f5(); } // @ts-ignore s0 = s1; @@ -1227,7 +1285,7 @@ peg$parseNone() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 33) { // @ts-ignore - s1 = peg$c5; + s1 = peg$c6; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1235,14 +1293,14 @@ peg$parseNone() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f5(); + s1 = peg$f6(); } // @ts-ignore s0 = s1; @@ -1260,9 +1318,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c7) { + if (input.substr(peg$currPos, 2) === peg$c8) { // @ts-ignore - s1 = peg$c7; + s1 = peg$c8; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1270,14 +1328,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e7); } + if (peg$silentFails === 0) { peg$fail(peg$e8); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f6(); + s1 = peg$f7(); } // @ts-ignore s0 = s1; @@ -1286,9 +1344,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c8) { + if (input.substr(peg$currPos, 2) === peg$c9) { // @ts-ignore - s1 = peg$c8; + s1 = peg$c9; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1296,14 +1354,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e8); } + if (peg$silentFails === 0) { peg$fail(peg$e9); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f7(); + s1 = peg$f8(); } // @ts-ignore s0 = s1; @@ -1314,7 +1372,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 62) { // @ts-ignore - s1 = peg$c9; + s1 = peg$c10; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1322,14 +1380,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e9); } + if (peg$silentFails === 0) { peg$fail(peg$e10); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f8(); + s1 = peg$f9(); } // @ts-ignore s0 = s1; @@ -1340,7 +1398,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 60) { // @ts-ignore - s1 = peg$c10; + s1 = peg$c11; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1348,14 +1406,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e10); } + if (peg$silentFails === 0) { peg$fail(peg$e11); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f9(); + s1 = peg$f10(); } // @ts-ignore s0 = s1; @@ -1366,7 +1424,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 61) { // @ts-ignore - s1 = peg$c11; + s1 = peg$c12; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1374,14 +1432,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f10(); + s1 = peg$f11(); } // @ts-ignore s0 = s1; @@ -1390,9 +1448,9 @@ peg$parseCmpOp() { // @ts-ignore s0 = peg$currPos; // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c12) { + if (input.substr(peg$currPos, 2) === peg$c13) { // @ts-ignore - s1 = peg$c12; + s1 = peg$c13; // @ts-ignore peg$currPos += 2; // @ts-ignore @@ -1400,14 +1458,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e12); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f11(); + s1 = peg$f12(); } // @ts-ignore s0 = s1; @@ -1418,7 +1476,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 94) { // @ts-ignore - s1 = peg$c13; + s1 = peg$c14; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1426,14 +1484,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f12(); + s1 = peg$f13(); } // @ts-ignore s0 = s1; @@ -1444,7 +1502,7 @@ peg$parseCmpOp() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 126) { // @ts-ignore - s1 = peg$c14; + s1 = peg$c15; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1452,14 +1510,14 @@ peg$parseCmpOp() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e14); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } // @ts-ignore if (s1 !== peg$FAILED) { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f13(); + s1 = peg$f14(); } // @ts-ignore s0 = s1; @@ -1516,7 +1574,7 @@ peg$parseExtendedVersion() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f14(s1, s2, s4); + s0 = peg$f15(s1, s2, s4); // @ts-ignore } else { // @ts-ignore @@ -1756,7 +1814,7 @@ peg$parseEmverParens() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f15(s3); + s0 = peg$f16(s3); // @ts-ignore } else { // @ts-ignore @@ -1807,7 +1865,7 @@ peg$parseEmverAnchor() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f16(s1, s3); + s0 = peg$f17(s1, s3); // @ts-ignore } else { // @ts-ignore @@ -1831,7 +1889,7 @@ peg$parseEmverNot() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 33) { // @ts-ignore - s1 = peg$c5; + s1 = peg$c6; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1839,7 +1897,7 @@ peg$parseEmverNot() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } // @ts-ignore if (s1 !== peg$FAILED) { @@ -1852,7 +1910,7 @@ peg$parseEmverNot() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f17(s3); + s0 = peg$f18(s3); // @ts-ignore } else { // @ts-ignore @@ -1887,7 +1945,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s2 = peg$c15; + s2 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1895,7 +1953,7 @@ peg$parseEmver() { // @ts-ignore s2 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s2 !== peg$FAILED) { @@ -1906,7 +1964,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1914,7 +1972,7 @@ peg$parseEmver() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -1927,7 +1985,7 @@ peg$parseEmver() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s7 = peg$c15; + s7 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -1935,7 +1993,7 @@ peg$parseEmver() { // @ts-ignore s7 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s7 !== peg$FAILED) { @@ -1946,7 +2004,7 @@ peg$parseEmver() { // @ts-ignore peg$savedPos = s6; // @ts-ignore - s6 = peg$f18(s1, s3, s5, s8); + s6 = peg$f19(s1, s3, s5, s8); // @ts-ignore } else { // @ts-ignore @@ -1969,7 +2027,7 @@ peg$parseEmver() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f19(s1, s3, s5, s6); + s0 = peg$f20(s1, s3, s5, s6); // @ts-ignore } else { // @ts-ignore @@ -2021,7 +2079,7 @@ peg$parseFlavor() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 35) { // @ts-ignore - s1 = peg$c16; + s1 = peg$c5; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2029,7 +2087,7 @@ peg$parseFlavor() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e5); } } // @ts-ignore if (s1 !== peg$FAILED) { @@ -2055,7 +2113,7 @@ peg$parseFlavor() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f20(s2); + s0 = peg$f21(s2); // @ts-ignore } else { // @ts-ignore @@ -2135,7 +2193,7 @@ peg$parseLowercase() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f21(); + s1 = peg$f22(); } // @ts-ignore s0 = s1; @@ -2197,7 +2255,7 @@ peg$parseString() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f22(); + s1 = peg$f23(); } // @ts-ignore s0 = s1; @@ -2228,7 +2286,7 @@ peg$parseVersion() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f23(s1, s2); + s0 = peg$f24(s1, s2); // @ts-ignore } else { // @ts-ignore @@ -2275,7 +2333,7 @@ peg$parsePreRelease() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s5 = peg$c15; + s5 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2283,7 +2341,7 @@ peg$parsePreRelease() { // @ts-ignore s5 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s5 !== peg$FAILED) { @@ -2318,7 +2376,7 @@ peg$parsePreRelease() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s5 = peg$c15; + s5 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2326,7 +2384,7 @@ peg$parsePreRelease() { // @ts-ignore s5 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s5 !== peg$FAILED) { @@ -2356,7 +2414,7 @@ peg$parsePreRelease() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f24(s2, s3); + s0 = peg$f25(s2, s3); // @ts-ignore } else { // @ts-ignore @@ -2387,7 +2445,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s1 = peg$c15; + s1 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2395,7 +2453,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore s1 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s1 === peg$FAILED) { @@ -2414,7 +2472,7 @@ peg$parsePreReleaseSegment() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f25(s2); + s0 = peg$f26(s2); // @ts-ignore } else { // @ts-ignore @@ -2446,7 +2504,7 @@ peg$parseVersionNumber() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2454,7 +2512,7 @@ peg$parseVersionNumber() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -2489,7 +2547,7 @@ peg$parseVersionNumber() { // @ts-ignore if (input.charCodeAt(peg$currPos) === 46) { // @ts-ignore - s4 = peg$c15; + s4 = peg$c16; // @ts-ignore peg$currPos++; // @ts-ignore @@ -2497,7 +2555,7 @@ peg$parseVersionNumber() { // @ts-ignore s4 = peg$FAILED; // @ts-ignore - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } // @ts-ignore if (s4 !== peg$FAILED) { @@ -2527,7 +2585,7 @@ peg$parseVersionNumber() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s0 = peg$f26(s1, s2); + s0 = peg$f27(s1, s2); // @ts-ignore } else { // @ts-ignore @@ -2593,7 +2651,7 @@ peg$parseDigit() { // @ts-ignore peg$savedPos = s0; // @ts-ignore - s1 = peg$f27(); + s1 = peg$f28(); } // @ts-ignore s0 = s1; @@ -2764,7 +2822,7 @@ peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; export interface ParseOptions { filename?: string; - startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmverVersionRange" | "EmverVersionRangeAtom" | "EmverParens" | "EmverAnchor" | "EmverNot" | "Emver" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "FlavorAtom" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmverVersionRange" | "EmverVersionRangeAtom" | "EmverParens" | "EmverAnchor" | "EmverNot" | "Emver" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; tracer?: any; [key: string]: any; } @@ -2779,6 +2837,7 @@ export type ParseFunction = ( StartRule extends "Parens" ? Parens : StartRule extends "Anchor" ? Anchor : StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "FlavorAtom" ? FlavorAtom : StartRule extends "Not" ? Not : StartRule extends "Any" ? Any : StartRule extends "None" ? None : @@ -2813,7 +2872,7 @@ export type VersionRange = [ ]; export type Or = "||"; export type And = "&&"; -export type VersionRangeAtom = Parens | Anchor | Not | Any | None; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None | FlavorAtom; export type Parens = { type: "Parens"; expr: VersionRange }; export type Anchor = { type: "Anchor"; @@ -2825,6 +2884,7 @@ export type VersionSpec = { upstream: Version; downstream: any; }; +export type FlavorAtom = { type: "Flavor"; flavor: Lowercase_1 }; export type Not = { type: "Not"; value: VersionRangeAtom }; export type Any = { type: "Any" }; export type None = { type: "None" }; diff --git a/sdk/base/lib/exver/index.ts b/sdk/base/lib/exver/index.ts index 9965d7287..5ec5bccfd 100644 --- a/sdk/base/lib/exver/index.ts +++ b/sdk/base/lib/exver/index.ts @@ -1,7 +1,8 @@ +import { DeepMap } from "deep-equality-data-structures"; import * as P from "./exver" // prettier-ignore -export type ValidateVersion = +export type ValidateVersion = T extends `-${infer A}` ? never : T extends `${infer A}-${string}` ? ValidateVersion : T extends `${bigint}` ? unknown : @@ -9,9 +10,9 @@ T extends `${infer A}-${string}` ? ValidateVersion : never // prettier-ignore -export type ValidateExVer = +export type ValidateExVer = T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : - T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : never // prettier-ignore @@ -43,19 +44,385 @@ type Not = { value: VersionRange } +type Flavor = { + type: "Flavor", + flavor: string | null, +} + +type FlavorNot = { + type: "FlavorNot", + flavors: Set, +} + +type FlavorAtom = Flavor | FlavorNot; + +/** + * Splits a number line of versions in half, so that every possible semver is either to the left or right. + * The `side` field handles inclusively. + * + * # Example + * Consider the version `1.2.3`. For side=-1 the version point is like `1.2.2.999*.999*.**` (that is, 1.2.3.0.0.** is greater) and + * for side=+1 the point is like `1.2.3.0.0.**.1` (that is, 1.2.3.0.0.** is less). + */ +type VersionRangePoint = { + upstream: Version, + downstream: Version, + side: -1 | 1; +} + +function compareVersionRangePoints(a: VersionRangePoint, b: VersionRangePoint): -1 | 0 | 1 { + let up = a.upstream.compareForSort(b.upstream); + if (up != 0) { + return up; + } + let down = a.upstream.compareForSort(b.upstream); + if (down != 0) { + return down; + } + if (a.side < b.side) { + return -1; + } else if (a.side > b.side) { + return 1; + } else { + return 0; + } +} + +function adjacentVersionRangePoints(a: VersionRangePoint, b: VersionRangePoint): boolean { + let up = a.upstream.compareForSort(b.upstream); + if (up != 0) { + return false; + } + let down = a.upstream.compareForSort(b.upstream); + if (down != 0) { + return false; + } + return a.side == -1 && b.side == 1; +} + +function flavorAnd(a: FlavorAtom, b: FlavorAtom): FlavorAtom | null { + if (a.type == 'Flavor') { + if (b.type == 'Flavor') { + if (a.flavor == b.flavor) { + return a; + } else { + return null; + } + } else { + if (b.flavors.has(a.flavor)) { + return null; + } else { + return a; + } + } + } else { + if (b.type == 'Flavor') { + if (a.flavors.has(b.flavor)) { + return null; + } else { + return b; + } + } else { + // TODO: use Set.union if targeting esnext or later + return { type: 'FlavorNot', flavors: new Set([...a.flavors, ...b.flavors]) }; + } + } +} + +/** + * Truth tables for version numbers and flavors. For each flavor we need a separate table, which + * is quite straightforward. But in order to exhaustively enumerate the boolean values of every + * combination of flavors and versions we also need tables for flavor negations. + */ +type VersionRangeTables = DeepMap | boolean; + +/** + * A truth table for version numbers. This is easiest to picture as a number line, cut up into + * ranges of versions between version points. + */ +class VersionRangeTable { + private constructor(protected points: Array, protected values: boolean[]) {} + + static zip(a: VersionRangeTable, b: VersionRangeTable, func: (a: boolean, b: boolean) => boolean): VersionRangeTable { + let c = new VersionRangeTable([], []); + let i = 0; + let j = 0; + while (true) { + let next = func(a.values[i], b.values[j]); + if (c.values.length > 0 && c.values[c.values.length - 1] == next) { + // collapse automatically + c.points.pop(); + } else { + c.values.push(next); + } + + // which point do we step over? + if (i == a.points.length) { + if (j == b.points.length) { + // just added the last segment, no point to jump over + return c; + } else { + // i has reach the end, step over j + c.points.push(b.points[j]); + j += 1; + } + } else { + if (j == b.points.length) { + // j has reached the end, step over i + c.points.push(a.points[i]); + i += 1; + } else { + // depends on which of the next two points is lower + switch (compareVersionRangePoints(a.points[i], b.points[j])) { + case -1: + // i is the lower point + c.points.push(a.points[i]); + i += 1; + break; + case 1: + // j is the lower point + c.points.push(b.points[j]); + j += 1; + break; + default: + // step over both + c.points.push(a.points[i]); + i += 1; + j += 1; + break; + } + } + } + } + } + + /** + * Creates a version table which is `true` for the given flavor, and `false` for any other flavor. + */ + static eqFlavor(flavor: string | null): VersionRangeTables { + return new DeepMap([ + [{ type: 'Flavor', flavor } as FlavorAtom, new VersionRangeTable([], [true])], + // make sure the truth table is exhaustive, or `not` will not work properly. + [{ type: 'FlavorNot', flavors: new Set([flavor]) } as FlavorAtom, new VersionRangeTable([], [false])], + ]); + } + + /** + * Creates a version table with exactly two ranges (to the left and right of the given point) and with `false` for any other flavor. + * This is easiest to understand by looking at `VersionRange.tables`. + */ + static cmpPoint(flavor: string | null, point: VersionRangePoint, left: boolean, right: boolean): VersionRangeTables { + return new DeepMap([ + [{ type: 'Flavor', flavor } as FlavorAtom, new VersionRangeTable([point], [left, right])], + // make sure the truth table is exhaustive, or `not` will not work properly. + [{ type: 'FlavorNot', flavors: new Set([flavor]) } as FlavorAtom, new VersionRangeTable([], [false])], + ]); + } + + /** + * Helper for `cmpPoint`. + */ + static cmp(version: ExtendedVersion, side: -1 | 1, left: boolean, right: boolean): VersionRangeTables { + return VersionRangeTable.cmpPoint(version.flavor, { upstream: version.upstream, downstream: version.downstream, side }, left, right) + } + + static not(tables: VersionRangeTables) { + if (tables === true || tables === false) { + return !tables; + } + // because tables are always exhaustive, we can simply invert each range + for (let [f, t] of tables) { + for (let i = 0; i < t.values.length; i++) { + t.values[i] = !t.values[i]; + } + } + return tables; + } + + static and(a_tables: VersionRangeTables, b_tables: VersionRangeTables): VersionRangeTables { + if (a_tables === true) { + return b_tables; + } + if (b_tables === true) { + return a_tables; + } + if (a_tables === false || b_tables == false) { + return false; + } + let c_tables: VersionRangeTables = true; + for (let [f_a, a] of a_tables) { + for (let [f_b, b] of b_tables) { + let flavor = flavorAnd(f_a, f_b); + if (flavor == null) { + continue; + } + let c = VersionRangeTable.zip(a, b, (a, b) => a && b); + if (c_tables === true) { + c_tables = new DeepMap(); + } + let prev_c = c_tables.get(flavor); + if (prev_c == null) { + c_tables.set(flavor, c); + } else { + c_tables.set(flavor, VersionRangeTable.zip(c, prev_c, (a, b) => a || b)); + } + } + } + return c_tables; + } + + static or(...in_tables: VersionRangeTables[]): VersionRangeTables { + let out_tables: VersionRangeTables = false; + for (let tables of in_tables) { + if (tables === false) { + continue; + } + if (tables === true) { + return true; + } + if (out_tables === false) { + out_tables = new DeepMap(); + } + for (let [flavor, table] of tables) { + let prev = out_tables.get(flavor); + if (prev == null) { + out_tables.set(flavor, table); + } else { + out_tables.set(flavor, VersionRangeTable.zip(table, prev, (a, b) => a || b)); + } + } + } + return out_tables; + } + + /** + * If this is true for all versions or false for all versions, returen that value. Otherwise return null. + */ + static collapse(tables: VersionRangeTables): boolean | null { + if (tables === true || tables === false) { + return tables; + } else { + let found = null; + for (let table of tables.values()) { + for (let x of table.values) { + if (found == null) { + found = x; + } else if (found != x) { + return null; + } + } + } + return found; + } + } + + /** + * Expresses this truth table as a series of version range operators. + * https://en.wikipedia.org/wiki/Canonical_normal_form#Minterms + */ + static minterms(tables: VersionRangeTables): VersionRange { + let collapse = VersionRangeTable.collapse(tables); + if (tables === true || collapse === true) { + return VersionRange.any() + } + if (tables == false || collapse === false) { + return VersionRange.none() + } + let sum_terms: VersionRange[] = []; + for (let [flavor, table] of tables) { + let cmp_flavor = null; + if (flavor.type == 'Flavor') { + cmp_flavor = flavor.flavor; + } + for (let i = 0; i < table.values.length; i++) { + let term: VersionRange[] = []; + if (!table.values[i]) { + continue + } + + if (flavor.type == 'FlavorNot') { + for (let not_flavor of flavor.flavors) { + term.push(VersionRange.flavor(not_flavor).not()); + } + } + + let p = null; + let q = null; + if (i > 0) { + p = table.points[i - 1]; + } + if (i < table.points.length) { + q = table.points[i]; + } + + if (p != null && q != null && adjacentVersionRangePoints(p, q)) { + term.push(VersionRange.anchor('=', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream))); + } else { + if (p != null && p.side < 0) { + term.push(VersionRange.anchor('>=', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream))); + } + if (p != null && p.side >= 0) { + term.push(VersionRange.anchor('>', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream))); + } + if (q != null && q.side < 0) { + term.push(VersionRange.anchor('<', new ExtendedVersion(cmp_flavor, q.upstream, q.downstream))); + } + if (q != null && q.side >= 0) { + term.push(VersionRange.anchor('<=', new ExtendedVersion(cmp_flavor, q.upstream, q.downstream))); + } + } + + if (term.length == 0) { + term.push(VersionRange.flavor(cmp_flavor)); + } + + sum_terms.push(VersionRange.and(...term)); + } + } + return VersionRange.or(...sum_terms); + } +} + export class VersionRange { - private constructor(public atom: Anchor | And | Or | Not | P.Any | P.None) {} + constructor(public atom: Anchor | And | Or | Not | P.Any | P.None | Flavor) {} + + toStringParens(parent: "And" | "Or" | "Not") { + let needs = true; + switch (this.atom.type) { + case "And": + case "Or": + needs = parent != this.atom.type; + break + case "Anchor": + case "Any": + case "None": + needs = parent == "Not" + break + case "Not": + case "Flavor": + needs = false; + break + } + + if (needs) { + return "(" + this.toString() + ")"; + } else { + return this.toString(); + } + } toString(): string { switch (this.atom.type) { case "Anchor": return `${this.atom.operator}${this.atom.version}` case "And": - return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})` + return `${this.atom.left.toStringParens(this.atom.type)} && ${this.atom.right.toStringParens(this.atom.type)}` case "Or": - return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})` + return `${this.atom.left.toStringParens(this.atom.type)} || ${this.atom.right.toStringParens(this.atom.type)}` case "Not": - return `!(${this.atom.value.toString()})` + return `!${this.atom.value.toStringParens(this.atom.type)}` + case "Flavor": + return this.atom.flavor == null ? `#` : `#${this.atom.flavor}` case "Any": return "*" case "None": @@ -88,6 +455,8 @@ export class VersionRange { ), ), }) + case "Flavor": + return VersionRange.flavor(atom.flavor) default: return new VersionRange(atom) } @@ -123,6 +492,14 @@ export class VersionRange { ) } + static anchor(operator: P.CmpOp, version: ExtendedVersion) { + return new VersionRange({ type: "Anchor", operator, version }) + } + + static flavor(flavor: string | null) { + return new VersionRange({ type: "Flavor", flavor }) + } + static parseEmver(range: string): VersionRange { return VersionRange.parseRange( P.parse(range, { startRule: "EmverVersionRange" }), @@ -141,8 +518,40 @@ export class VersionRange { return new VersionRange({ type: "Not", value: this }) } - static anchor(operator: P.CmpOp, version: ExtendedVersion) { - return new VersionRange({ type: "Anchor", operator, version }) + static and(...xs: Array) { + let y = VersionRange.any(); + for (let x of xs) { + if (x.atom.type == 'Any') { + continue; + } + if (x.atom.type == 'None') { + return x; + } + if (y.atom.type == 'Any') { + y = x; + } else { + y = new VersionRange({ type: 'And', left: y, right: x}); + } + } + return y; + } + + static or(...xs: Array) { + let y = VersionRange.none(); + for (let x of xs) { + if (x.atom.type == 'None') { + continue; + } + if (x.atom.type == 'Any') { + return x; + } + if (y.atom.type == 'None') { + y = x; + } else { + y = new VersionRange({ type: 'Or', left: y, right: x}); + } + } + return y; } static any() { @@ -156,6 +565,71 @@ export class VersionRange { satisfiedBy(version: Version | ExtendedVersion) { return version.satisfies(this) } + + tables(): VersionRangeTables { + switch(this.atom.type) { + case "Anchor": + switch (this.atom.operator) { + case "=": + // `=1.2.3` is equivalent to `>=1.2.3 && <=1.2.4 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version, 1, true, false), + ) + case ">": + return VersionRangeTable.cmp(this.atom.version, 1, false, true) + case "<": + return VersionRangeTable.cmp(this.atom.version, -1, true, false) + case ">=": + return VersionRangeTable.cmp(this.atom.version, -1, false, true) + case "<=": + return VersionRangeTable.cmp(this.atom.version, 1, true, false) + case "!=": + // `!=1.2.3` is equivalent to `!(>=1.2.3 && <=1.2.3 && #flavor)` + // **not** equivalent to `(<1.2.3 || >1.2.3) && #flavor` + return VersionRangeTable.not(VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version, 1, true, false), + )) + case "^": + // `^1.2.3` is equivalent to `>=1.2.3 && <2.0.0 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version.incrementMajor(), -1, true, false), + ) + case "~": + // `~1.2.3` is equivalent to `>=1.2.3 && <1.3.0 && #flavor` + return VersionRangeTable.and( + VersionRangeTable.cmp(this.atom.version, -1, false, true), + VersionRangeTable.cmp(this.atom.version.incrementMinor(), -1, true, false), + ) + } + case "Flavor": + return VersionRangeTable.eqFlavor(this.atom.flavor) + case "Not": + return VersionRangeTable.not(this.atom.value.tables()) + case "And": + return VersionRangeTable.and(this.atom.left.tables(), this.atom.right.tables()) + case "Or": + return VersionRangeTable.or(this.atom.left.tables(), this.atom.right.tables()) + case "Any": + return true + case "None": + return false + } + } + + satisfiable(): boolean { + return VersionRangeTable.collapse(this.tables()) !== false; + } + + intersects(other: VersionRange): boolean { + return VersionRange.and(this, other).satisfiable(); + } + + normalize(): VersionRange { + return VersionRangeTable.minterms(this.tables()); + } } export class Version { @@ -211,6 +685,17 @@ export class Version { return "equal" } + compareForSort(other: Version): -1 | 0 | 1 { + switch (this.compare(other)) { + case "greater": + return 1 + case "equal": + return 0 + case "less": + return -1 + } + } + static parse(version: string): Version { const parsed = P.parse(version, { startRule: "Version" }) return new Version(parsed.number, parsed.prerelease) @@ -409,6 +894,8 @@ export class ExtendedVersion { return false } } + case "Flavor": + return versionRange.atom.flavor == this.flavor case "And": return ( this.satisfies(versionRange.atom.left) && @@ -433,6 +920,7 @@ export const testTypeExVer = (t: T & ValidateExVer) => t export const testTypeVersion = (t: T & ValidateVersion) => t + function tests() { testTypeVersion("1.2.3") testTypeVersion("1") diff --git a/sdk/base/lib/test/exverList.test.ts b/sdk/base/lib/test/exver.test.ts similarity index 83% rename from sdk/base/lib/test/exverList.test.ts rename to sdk/base/lib/test/exver.test.ts index e29a9f0d1..6bddf3723 100644 --- a/sdk/base/lib/test/exverList.test.ts +++ b/sdk/base/lib/test/exver.test.ts @@ -71,6 +71,19 @@ describe("ExVer", () => { }) } { + // TODO: this this correct? if not, also fix normalize + const checker = VersionRange.parse("=1") + test(`VersionRange.parse("=1") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.0:0"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse("=1") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.0.0:1"))).toEqual(false) + }) + } { const checker = VersionRange.parse(">=1.2.3:4") test(`VersionRange.parse(">=1.2.3:4") valid`, () => { expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) @@ -290,6 +303,38 @@ describe("ExVer", () => { }) } + { + function testNormalization(input: string, expected: string) { + test(`"${input}" normalizes to "${expected}"`, () => { + const checker = VersionRange.parse(input).normalize(); + expect(checker.toString()).toEqual(expected); + }); + } + + testNormalization("=2.0", "=2.0:0"); + testNormalization("=1 && =2", "!"); + testNormalization("!(=1 && =2)", "*"); + testNormalization("!=1 || !=2", "*"); + testNormalization("(!=#foo:1 || !=#foo:2) && #foo", "#foo"); + testNormalization("!=#foo:1 || !=#bar:2", "<#foo:1:0 || >#foo:1:0 || !#foo || <#bar:2:0 || >#bar:2:0 || !#bar"); + testNormalization("!(=1 || =2)", "<1:0 || (>1:0 && <2:0) || >2:0 || !#"); + testNormalization("=1 && (=2 || =3)", "!"); + testNormalization("=1 && (=1 || =2)", "=1:0"); + testNormalization("=#foo:1 && =#bar:1", "!"); + testNormalization("!(=#foo:1) && !(=#bar:1)", "<#foo:1:0 || >#foo:1:0 || <#bar:1:0 || >#bar:1:0 || (!#foo && !#bar)"); + testNormalization("!(=#foo:1) && !(=#bar:1) && >2", ">2:0"); + testNormalization("~1.2.3", ">=1.2.3:0 && <1.3.0:0"); + testNormalization("^1.2.3", ">=1.2.3:0 && <2.0.0:0"); + testNormalization("^1.2.3 && >=1 && >=1.2 && >=1.3", ">=1.3:0 && <2.0.0:0"); + testNormalization("(>=1.0 && <1.1) || (>=1.1 && <1.2) || (>=1.2 && <1.3)", ">=1.0:0 && <1.3:0"); + testNormalization(">1 || <2", "#"); + + testNormalization("=1 && =1.2 && =1.2.3", "!"); + // testNormalization("=1 && =1.2 && =1.2.3", "=1.2.3:0"); TODO: should it be this instead? + testNormalization("=1 || =1.2 || =1.2.3", "=1:0 || =1.2:0 || =1.2.3:0"); + // testNormalization("=1 || =1.2 || =1.2.3", "=1:0"); TODO: should it be this instead? + } + { test(">1 && =1.2", () => { const checker = VersionRange.parse(">1 && =1.2") @@ -305,6 +350,8 @@ describe("ExVer", () => { const checker = VersionRange.parse("=1 || =2") expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual(false) // really? + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual(false) // really? expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) }) diff --git a/sdk/base/package-lock.json b/sdk/base/package-lock.json index eadbd049b..9e31a4869 100644 --- a/sdk/base/package-lock.json +++ b/sdk/base/package-lock.json @@ -11,6 +11,7 @@ "@iarna/toml": "^2.2.5", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", + "deep-equality-data-structures": "^1.5.0", "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", @@ -1780,6 +1781,15 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-equality-data-structures": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-1.5.1.tgz", + "integrity": "sha512-P7zsL2/AbZIGHDxbo/LLEhCp11AttRp8GvzXOXudqMT/qiGCLo/pyI4lAZvjUZyQnlIbPna3fv8DMsuRvLt4ww==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3216,6 +3226,15 @@ "node": ">=8" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/sdk/base/package.json b/sdk/base/package.json index 7a162c218..6a050b61b 100644 --- a/sdk/base/package.json +++ b/sdk/base/package.json @@ -28,7 +28,8 @@ "lodash.merge": "^4.6.2", "mime-types": "^2.1.35", "ts-matches": "^6.2.1", - "yaml": "^2.2.2" + "yaml": "^2.2.2", + "deep-equality-data-structures": "^1.5.0" }, "prettier": { "trailingComma": "all", diff --git a/sdk/package/lib/version/VersionGraph.ts b/sdk/package/lib/version/VersionGraph.ts index 91c7a0cc0..129153e52 100644 --- a/sdk/package/lib/version/VersionGraph.ts +++ b/sdk/package/lib/version/VersionGraph.ts @@ -163,15 +163,17 @@ export class VersionGraph { (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), - ).reduce( - (acc, x) => - acc.or( - x.metadata instanceof VersionRange - ? x.metadata - : VersionRange.anchor("=", x.metadata), - ), - VersionRange.none(), - ), + ) + .reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ) + .normalize(), ) canMigrateTo = once(() => Array.from( @@ -182,15 +184,17 @@ export class VersionGraph { (v.metadata instanceof ExtendedVersion && v.metadata.equals(this.currentVersion())), ), - ).reduce( - (acc, x) => - acc.or( - x.metadata instanceof VersionRange - ? x.metadata - : VersionRange.anchor("=", x.metadata), - ), - VersionRange.none(), - ), + ) + .reduce( + (acc, x) => + acc.or( + x.metadata instanceof VersionRange + ? x.metadata + : VersionRange.anchor("=", x.metadata), + ), + VersionRange.none(), + ) + .normalize(), ) }