diff --git a/.gitignore b/.gitignore index 4922ef6df..7b7bc57f2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ output/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ !tests/fixtures/provenance/node_modules/*/dist/ !tests/fixtures/npm-static/node_modules/*/dist/ +!tests/corpus/*/node_modules/*/dist/ .scriptc/ *.log .DS_Store diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 805d7f612..b27327b91 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -76,7 +76,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. - `SC6005`: a `Date` string literal uses a form the native parser does not accept. diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 76ff0ae74..a1faa1c34 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -2,7 +2,7 @@ A record converted to a record type with a different set of properties, such as Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order. `SC6002` reports object literals whose order differs where the program observes it. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. The order matches Node.js when every value of the type is created in the same order. When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. `Object.values` and `Object.entries` check those values against their static result type and throw a `TypeError` if one does not fit. Copies of such a record made by object spread, `Object.assign`, rest destructuring, or conversion to another record type contain only the declared fields. Index-signature records list declared fields before the other keys. diff --git a/docs/content/limitations/strings-and-sorting.mdx b/docs/content/limitations/strings-and-sorting.mdx index eacca198e..b1616f9f4 100644 --- a/docs/content/limitations/strings-and-sorting.mdx +++ b/docs/content/limitations/strings-and-sorting.mdx @@ -1,4 +1,4 @@ -Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length and string methods use UTF-16 semantics, including for lone surrogates, but relational comparisons such as `<` and `>` use code-point order. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted. +Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted. One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, combining diacritical marks, and U+2044 FRACTION SLASH): letters order before case and accents, so `"a".localeCompare("B")` is `-1`. Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals. The locales and options arguments are unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 402e161fb..be1a931cc 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -328,10 +328,10 @@ "entries": 92, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -854,9 +854,9 @@ "apiEntries": 156, "static": { "supported": 2, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 144, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -35693,9 +35693,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36360,9 +36360,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -37165,9 +37165,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -37303,9 +37303,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80229,9 +80229,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80252,9 +80252,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80275,9 +80275,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80298,9 +80298,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80321,9 +80321,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80344,9 +80344,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80436,9 +80436,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80574,9 +80574,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80735,9 +80735,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80758,9 +80758,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80804,9 +80804,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 75e789c61..a02446908 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -3,12 +3,12 @@ "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", "summary": { - "taskCount": 3454, - "tierItemCount": 3454, + "taskCount": 3453, + "tierItemCount": 3453, "tiers": { "static": { - "audit-partial": 1165, - "verify-gap": 2161, + "audit-partial": 1179, + "verify-gap": 2146, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -17,7 +17,7 @@ "priorities": { "high": 2287, "normal": 694, - "low": 473 + "low": 472 } }, "tasks": [ @@ -33201,12 +33201,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -33932,12 +33938,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -34853,12 +34865,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] } } }, @@ -34987,31 +35003,6 @@ } } }, - { - "id": "globals:fc0a32e8c26f", - "chapter": "globals", - "label": "userAgent", - "signature": "`userAgent` Type: {string}", - "apiSymbol": "userAgent", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator", - "nodeStability": { - "index": "1.1", - "level": 1, - "text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.", - "inherited": true - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] - } - } - }, { "id": "globals:b412bf9a456d", "chapter": "globals", @@ -81024,12 +81015,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81049,12 +81046,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81074,12 +81075,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81099,12 +81104,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81124,12 +81133,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81149,12 +81162,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81249,12 +81266,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81399,12 +81422,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81574,12 +81604,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81599,12 +81636,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81649,12 +81693,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c93030602..3690ce077 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -340,10 +340,10 @@ "entries": 94, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -911,9 +911,9 @@ "apiEntries": 156, "static": { "supported": 2, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 144, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52242,8 +52242,16 @@ "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js", "tests/corpus/5830-timers-promises-signal.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/6331-jose-root-entry/main.ts", "tests/corpus/text-codec-values/main.ts", - "tests/harness/fetch-conformance.test.ts" + "tests/fixtures/node-types/navigator.ts", + "tests/harness/fetch-conformance.test.ts", + "tests/harness/project-config.test.ts" ] }, "anchorSource": "chapter" @@ -52664,8 +52672,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -53338,8 +53353,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -54244,8 +54266,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] }, "anchorSource": "exact" }, @@ -54382,8 +54409,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.userAgent", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts" + ] }, "anchorSource": "ancestor" }, @@ -106872,10 +106903,16 @@ "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", + "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110411,8 +110448,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110434,8 +110478,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110457,8 +110506,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110480,8 +110534,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110503,8 +110562,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110526,8 +110590,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110618,8 +110687,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110756,8 +110832,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110917,8 +111001,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110940,8 +111032,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110986,8 +111086,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 71605612e..855d065f7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -179,11 +179,19 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], + "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, + { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, {"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]}, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 3b522dd3e..a8f1f8a8b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -511,14 +511,119 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* Capability-gated globals harness code probes behind feature guards - * (globalThis.crypto under a hasCrypto check, the SQLite-backed storage - * pair): typed `unknown` — no lowering exists, the guard is expected to - * be false in a compiled binary, and any reached use fences per site. */ -declare var crypto: unknown; +/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for + * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as + * exactly that surface (the declarations mirror @types/node's webcrypto + * shapes). Every other SubtleCrypto method and crypto member is absent, + * so a use is an honest type error. CryptoKey values are native handles + * whose four members read through the handle. */ +declare var crypto: Crypto; +/* The Crypto and SubtleCrypto constructors (feature probes only) stay + * `unknown`; the values keep this declaration order. */ declare var Crypto: unknown; -declare var CryptoKey: unknown; +declare var CryptoKey: { + prototype: CryptoKey; + new (): CryptoKey; +}; declare var SubtleCrypto: unknown; +type KeyUsage = + | "encrypt" + | "decrypt" + | "sign" + | "verify" + | "deriveKey" + | "deriveBits" + | "encapsulateBits" + | "decapsulateBits" + | "encapsulateKey" + | "decapsulateKey" + | "wrapKey" + | "unwrapKey"; +type KeyType = "private" | "public" | "secret"; +interface Algorithm { + name: string; +} +type AlgorithmIdentifier = Algorithm | string; +type HashAlgorithmIdentifier = AlgorithmIdentifier; +interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface KeyAlgorithm { + name: string; +} +interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; +} +interface CryptoKey { + readonly algorithm: KeyAlgorithm; + readonly extractable: boolean; + readonly type: KeyType; + readonly usages: KeyUsage[]; +} +type KeyFormat = + | "jwk" + | "pkcs8" + | "raw" + | "raw-private" + | "raw-public" + | "raw-secret" + | "raw-seed" + | "spki"; +interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} +/* The 'jwk' form type-checks like @types/node's; the compiler refuses it + * (the JsonWebKey import is not implemented). */ +interface SubtleCrypto { + digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + importKey( + format: Exclude, + keyData: BufferSource, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + verify( + algorithm: AlgorithmIdentifier, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise; +} +interface Crypto { + readonly subtle: SubtleCrypto; +} + +/* Capability-gated globals harness code probes behind feature guards + * (the SQLite-backed storage pair): typed `unknown` — no lowering exists, + * the guard is expected to be false in a compiled binary, and any reached + * use fences per site. */ declare var localStorage: unknown; declare var sessionStorage: unknown; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index d2c22eda4..eeb6c6900 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -16,6 +16,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { fileHandle: features.fileHandle, fetch: features.fetch, ...(features.eventTarget ? { eventTarget: true } : {}), + ...(features.warningReplace ? { warningReplace: true } : {}), + ...(features.stdioExtended ? { stdioExtended: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, @@ -41,5 +43,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, }; } diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 554c40524..5070a47a8 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -53,6 +53,7 @@ export async function compilePackedLibrary( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }; const selection = await loadRuntimePack({ diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 765fb2597..e86b447d9 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3672,13 +3672,30 @@ export class LlEmitter { } defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``); } - // Ordering also decides inline when the first bytes differ: byte order - // is the plain comparison's order, and the UTF-16 order whenever both - // bytes are ASCII. Callers only test the sign of the result. - for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) { - if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`)) continue; + // Ordering decides inline from the first differing bytes. The bytes + // before them are equal, so both strings are at the same position of a + // character: either both bytes continue characters with the same lead + // byte, or both start a character. Byte order is the UTF-16 order for + // two continuation bytes, and for two lead bytes when one starts a + // character below U+D000, whose UTF-16 unit is below every unit of a + // character with a lead byte of at least 0xED. Between lead bytes of at + // least 0xED, a supplementary character (0xF0..0xF4, a high surrogate + // unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of + // 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime. + // Differing words without a byte of at least 0xED decide in byte + // order; other differing words decide by their first differing byte, + // and strings shorter than a word by their first byte. Otherwise a + // proven-ASCII string on either side orders by bytes (scr_str_cmp), and + // other pairs call scr_str_cmp_u16_bytes, which only reads the two + // strings, so LLVM can merge repeated comparisons of one pair. Callers + // only test the sign of the result. + if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { + const fn = "scr_str_cmp_u16"; this.declare("declare i64 @llvm.bswap.i64(i64)"); - const utf16 = fn === "scr_str_cmp_u16"; + this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)"); + this.declare("declare i8 @llvm.umin.i8(i8, i8)"); + this.declare("declare i32 @scr_str_cmp(ptr, ptr)"); + this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)"); defs.push( `define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, @@ -3693,8 +3710,7 @@ export class LlEmitter { ` %bempty = icmp eq ${this.sizeType} %blen, 0`, ` %empty = or i1 %aempty, %bempty`, ` br i1 %empty, label %order, label %words`, - // Both at least 8 bytes: the first differing byte of one word pair. - // Byte-swapped little-endian words compare in byte order. + // Both at least 8 bytes: the first word of each. `words:`, ` %along = icmp uge ${this.sizeType} %alen, 8`, ` %blong = icmp uge ${this.sizeType} %blen, 8`, @@ -3706,22 +3722,7 @@ export class LlEmitter { ` %aw = load i64, ptr %awp, align 1`, ` %bw = load i64, ptr %bwp, align 1`, ` %wdiffer = icmp ne i64 %aw, %bw`, - ` br i1 %wdiffer, label %wdiff, label %wsame`, - `wdiff:`, - ...(utf16 - ? [ - ` %wbits = or i64 %aw, %bw`, - ` %whigh = and i64 %wbits, -9187201950435737472`, - ` %wascii = icmp eq i64 %whigh, 0`, - ` br i1 %wascii, label %wordorder, label %order`, - ] - : [` br label %wordorder`]), - `wordorder:`, - ` %asw = call i64 @llvm.bswap.i64(i64 %aw)`, - ` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`, - ` %wbelow = icmp ult i64 %asw, %bsw`, - ` %wsign = select i1 %wbelow, i32 -1, i32 1`, - ` ret i32 %wsign`, + ` br i1 %wdiffer, label %differ, label %wsame`, // Equal first words and one length of at most 16 bytes: the word // ending at that length covers the rest, and any difference lies // past the equal first word. @@ -3737,42 +3738,89 @@ export class LlEmitter { ` %at = load i64, ptr %atp, align 1`, ` %bt = load i64, ptr %btp, align 1`, ` %tdiffer = icmp ne i64 %at, %bt`, - ` br i1 %tdiffer, label %tdiff, label %equal`, - `tdiff:`, - ...(utf16 - ? [ - ` %tbits = or i64 %at, %bt`, - ` %thigh = and i64 %tbits, -9187201950435737472`, - ` %tascii = icmp eq i64 %thigh, 0`, - ` br i1 %tascii, label %tailorder, label %order`, - ] - : [` br label %tailorder`]), - `tailorder:`, - ` %ats = call i64 @llvm.bswap.i64(i64 %at)`, - ` %bts = call i64 @llvm.bswap.i64(i64 %bt)`, - ` %tbelow = icmp ult i64 %ats, %bts`, - ` %tsign = select i1 %tbelow, i32 -1, i32 1`, - ` ret i32 %tsign`, + ` br i1 %tdiffer, label %differ, label %equal`, `first:`, ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, ` %x = load i8, ptr %adata`, ` %y = load i8, ptr %bdata`, - ` %differ = icmp ne i8 %x, %y`, - ...(utf16 - ? [ - ` %bits = or i8 %x, %y`, - ` %ascii = icmp sge i8 %bits, 0`, - ` %decide = and i1 %differ, %ascii`, - ] - : [` %decide = and i1 %differ, true`]), - ` br i1 %decide, label %byte, label %order`, + ` %fdiffer = icmp ne i8 %x, %y`, + ` br i1 %fdiffer, label %bytes, label %order`, + // Two differing words. When neither has a byte of at least 0xED + // (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are + // set), byte-swapped little-endian words compare in byte order. + // Otherwise the lowest set byte of their xor is the first differing + // byte. + `differ:`, + ` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`, + ` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`, + ` %xl = and i64 %dx, 9187201950435737471`, + ` %yl = and i64 %dy, 9187201950435737471`, + ` %xa = add i64 %xl, 1374463283923456787`, + ` %ya = add i64 %yl, 1374463283923456787`, + ` %xh = and i64 %dx, %xa`, + ` %yh = and i64 %dy, %ya`, + ` %hh = or i64 %xh, %yh`, + ` %whigh = and i64 %hh, -9187201950435737472`, + ` %dlow = icmp eq i64 %whigh, 0`, + ` br i1 %dlow, label %wordorder, label %locate`, + `wordorder:`, + ` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`, + ` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`, + ` %wbelow = icmp ult i64 %xsw, %ysw`, + ` %wsign = select i1 %wbelow, i32 -1, i32 1`, + ` ret i32 %wsign`, + `locate:`, + ` %dxor = xor i64 %dx, %dy`, + ` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`, + ` %dshift = and i64 %dtz, 56`, + ` %dxs = lshr i64 %dx, %dshift`, + ` %dys = lshr i64 %dy, %dshift`, + ` %dxb = trunc i64 %dxs to i8`, + ` %dyb = trunc i64 %dys to i8`, + ` br label %bytes`, + // The first differing bytes. + `bytes:`, + ` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`, + ` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`, + ` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`, + ` %blow = icmp ult i8 %bmin, -19`, + ` br i1 %blow, label %byte, label %high`, + // Both at least 0xED. A lone surrogate (0xED) takes the runtime; + // supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF + // once 0x10 is subtracted from them. + `high:`, + ` %xed = icmp eq i8 %bx, -19`, + ` %yed = icmp eq i8 %by, -19`, + ` %ed = or i1 %xed, %yed`, + ` br i1 %ed, label %order, label %planes`, + `planes:`, + ` %xsupp = icmp uge i8 %bx, -16`, + ` %ysupp = icmp uge i8 %by, -16`, + ` %xlow = add i8 %bx, -16`, + ` %ylow = add i8 %by, -16`, + ` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`, + ` %yk = select i1 %ysupp, i8 %ylow, i8 %by`, + ` %kbelow = icmp ult i8 %xk, %yk`, + ` %ksign = select i1 %kbelow, i32 -1, i32 1`, + ` ret i32 %ksign`, `byte:`, - ` %below = icmp ult i8 %x, %y`, + ` %below = icmp ult i8 %bx, %by`, ` %sign = select i1 %below, i32 -1, i32 1`, ` ret i32 %sign`, `order:`, - ` %c = call i32 @${fn}(ptr %a, ptr %b)`, + ` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`, + ` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`, + ` %acw = load i32, ptr %acw.p`, + ` %bcw = load i32, ptr %bcw.p`, + ` %cws = or i32 %acw, %bcw`, + ` %anyascii = icmp slt i32 %cws, 0`, + ` br i1 %anyascii, label %memorder, label %units`, + `memorder:`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` ret i32 %m`, + `units:`, + ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, ` ret i32 %c`, `equal:`, ` ret i32 0`, @@ -5882,6 +5930,8 @@ export class LlEmitter { // Process streams use a numeric descriptor internally, including zero // for stdin. They remain JavaScript objects in boolean contexts. case "procStream": + // A module namespace object is always present. + case "moduleNs": return "true"; case "array": case "record": diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 4e7d418f8..35e759110 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -379,7 +379,29 @@ export function emitOperatorExpr( const fn = libm[e.op]; if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc); host.declare(`declare double @${fn}(double, double)`); - B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`); + // libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where + // `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline + // guard selects NaN for a base of ±1 with a non-finite exponent and + // keeps the direct pow call. A finite literal exponent, or a literal + // base other than ±1, cannot meet those cases and needs no guard. + if (fn === "pow" && !libmPowAgrees(e.left, e.right)) { + host.declare(`declare double @llvm.fabs.f64(double)`); + const power = B.tmp(); + B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`); + const base = B.tmp(); + const unit = B.tmp(); + const magnitude = B.tmp(); + const finite = B.tmp(); + const reject = B.tmp(); + B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`); + B.line(`${unit} = fcmp oeq double ${base}, 1.0`); + B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`); + B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`); + B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`); + B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`); + } else { + B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`); + } } return { name: t, type: e.type }; } @@ -602,7 +624,7 @@ export function emitStringExpr( } case "strCmp": { const [l, r] = emitStringInputs(host, [e.left, e.right]); - const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp"; + const fn = "scr_str_cmp_u16"; host.declare(`declare i32 @${fn}(ptr, ptr)`); const c = B.tmp(); const t = B.tmp(); @@ -1106,3 +1128,19 @@ export function emitRecordLitFields( host.initField(ptr, type, inline, v); } } + +/** Whether libm pow matches `**` for these operands: every case except a + * base of ±1 with a NaN or infinite exponent agrees. */ +function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean { + const power = numericLiteral(exponent); + if (power !== null && Number.isFinite(power)) return true; + const value = numericLiteral(base); + return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1; +} + +function numericLiteral(expr: IrExpr): number | null { + if (expr.kind === "numLit") return expr.value; + if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit") + return -expr.operand.value; + return null; +} diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index d21d22169..31d7ca5c2 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -835,6 +835,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "util": case "bigint": case "crypto": + case "webcrypto": case "buffer": case "bytes": case "tlsca": diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index e6d8acdbe..eaf7f31cd 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -546,15 +546,24 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`); return { name: "", type: e.type }; } + // A string listener takes the setEncoding-decoded chunk. + const stringData = e.fn === "stdin.onData" && cbT.params[0]?.kind === "string"; const adapter = e.fn === "stdin.onData" ? cbT.params.length === 0 ? "scr_stdin_data_thunk0" - : "scr_stdin_data_thunk_bytes" + : stringData + ? "scr_stdin_data_thunk_str" + : "scr_stdin_data_thunk_bytes" : cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; - const sym = e.fn === "stdin.onData" ? "scr_stdin_on_data" : "scr_stdin_on_error"; + const sym = + e.fn === "stdin.onData" + ? stringData + ? "scr_stdin_on_data_str" + : "scr_stdin_on_data" + : "scr_stdin_on_error"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`); B.line(`call void @${sym}(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 73b6d72e7..2d91386b3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -588,6 +588,11 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeySync": "scr_crypto_generate_key_sync", "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", + "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.sign": "scr_webcrypto_sign", + "webcrypto.verify": "scr_webcrypto_verify", + "webcrypto.digest": "scr_webcrypto_digest", + "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1336,6 +1341,7 @@ export const LIB_FN_SYMS: Record = { "process.emitWarningText": "scr_process_emit_warning_text", "process.deprecationFlag": "scr_process_deprecation_flag", "process.setDeprecationFlag": "scr_process_set_deprecation_flag", + "process.setEmitWarning": "scr_process_set_emit_warning", "process.onUncaughtException": "scr_process_on_uncaught_exception", "process.offUncaughtException": "scr_process_off_uncaught_exception", "process.onUnhandledRejection": "scr_process_on_unhandled_rejection", @@ -1471,6 +1477,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", // Decipheriv.setAuthTag reports DEP0182 on the next tick. "crypto.cipherNew", + // WebCrypto results settle through the deferred crypto queue. + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", "process.emitWarningText", // The legacy url entries queue DEP0169/DEP0170 on the next tick. "url.legacyParse", diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index f6a75bb62..8622085b4 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -432,7 +432,6 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", op: "<", left: str("a"), right: str("b"), - utf16: true, type: BOOL, loc, }); @@ -443,16 +442,14 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", }); test("string comparisons decide identity, lengths and leading bytes before the runtime", () => { - const order = (utf16: boolean) => - fn(`order${utf16 ? 16 : 8}`, [], { - kind: "strCmp", - op: "<", - left: str("apple"), - right: str("apricot"), - utf16, - type: BOOL, - loc, - }); + const order = fn("order", [], { + kind: "strCmp", + op: "<", + left: str("apple"), + right: str("apricot"), + type: BOOL, + loc, + }); const same = fn("same", ["a", "b"], { kind: "strEq", left: { kind: "varRef", localId: "a", type: STRING, loc }, @@ -462,23 +459,28 @@ test("string comparisons decide identity, lengths and leading bytes before the r loc, }); for (const bits of [32, 64] as const) { - const ir = emitLlvmModule(mod(order(false), order(true), same), { pointerBits: bits }); + const ir = emitLlvmModule(mod(order, same), { pointerBits: bits }); const helper = (name: string) => new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir)![0]; const eq = helper("sc_str_eq"); expect(eq).toContain("icmp eq ptr %a, %b"); expect(eq).toMatch(/icmp eq i(32|64) %alen, %blen/); expect(eq).toContain("call zeroext i1 @scr_str_eq(ptr %a, ptr %b)"); - const bytes = helper("sc_str_cmp"); - expect(bytes).toContain("@llvm.bswap.i64"); - expect(bytes).not.toContain("%ascii"); - expect(bytes).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); const units = helper("sc_str_cmp_u16"); - expect(units).toContain("%wascii = icmp eq i64 %whigh, 0"); - expect(units).toContain("%tascii = icmp eq i64 %thigh, 0"); - expect(bytes).toContain("%tailable = and i1 %samelen, %short"); - expect(units).toContain("%ascii = icmp sge i8 %bits, 0"); - expect(units).toContain("call i32 @scr_str_cmp_u16(ptr %a, ptr %b)"); + expect(units).toContain("%dx = phi i64 [ %aw, %word ], [ %at, %tail ]"); + expect(units).toContain("%dlow = icmp eq i64 %whigh, 0"); + expect(units).toContain("@llvm.bswap.i64"); + expect(units).toContain("%dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)"); + expect(units).toContain("%bx = phi i8 [ %x, %first ], [ %dxb, %locate ]"); + expect(units).toContain("%blow = icmp ult i8 %bmin, -19"); + expect(units).toContain("%kbelow = icmp ult i8 %xk, %yk"); + expect(units).toContain("%below = icmp ult i8 %bx, %by"); + expect(units).toContain("%tailable = and i1 %samelen, %short"); + expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); + expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); } }); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 09371e747..86146259d 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -25,6 +25,12 @@ export interface NativeLinkFeatures { fileHandle: boolean; fetch: boolean; eventTarget?: boolean; + /** The program assigns process.emitWarning: links the + * SCR_WARNING_REPLACE variant of the dyn-async unit. */ + warningReplace?: boolean; + /** The program uses process.stdin.setEncoding, string 'data' listeners + * or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */ + stdioExtended?: boolean; zlib: boolean; assert: boolean; inspect: boolean; @@ -54,6 +60,9 @@ export interface NativeLinkFeatures { * archive. */ cipher: boolean; tlsCa: boolean; + /** crypto.subtle importKey/sign/verify/digest and CryptoKey values: + * scr_webcrypto.c. */ + webcrypto: boolean; } export interface NativeLinkInfo { diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 62e76137b..33b13960e 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -118,6 +118,10 @@ export interface CcOptions { * native fetch AbortSignal built on the same engine: compiles * scr_event_target.c. */ eventTarget?: boolean; + /** SCR_WARNING_REPLACE: process.emitWarning replacement support. */ + warningReplace?: boolean; + /** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */ + stdioExtended?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep @@ -203,6 +207,8 @@ export interface CcOptions { /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links * the mbedTLS archive (shared with tls). */ cipher?: boolean; + /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ + webcrypto?: boolean; /** Internal compiler hook: called only after a strict native artifact hit * or a successful stable build has installed `outPath`. The executable * frontend cache uses it to publish its stamp after native dependencies diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index e4d36852a..e632795c9 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -445,6 +445,8 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), + ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", // The runtime object model uses type-punned C: a hierarchy @@ -579,6 +581,7 @@ export async function compileCInternal( ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []), ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []), ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []), + ...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []), @@ -645,6 +648,8 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []), + ...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []), ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 5faad73b3..6a660e00c 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -83,6 +83,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { youngNursery: boolean; eventTarget: boolean; dynTier: boolean; + warningReplace: boolean; + stdioExtended: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -271,6 +273,8 @@ export function effectiveRuntimeFeatures( youngNursery: features.youngNursery === true, eventTarget: features.eventTarget === true, dynTier: features.dynTier === true, + warningReplace: features.warningReplace === true, + stdioExtended: features.stdioExtended === true, nativeFetch, netEffective, httpEffective, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index faa335e33..fd57cb49d 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -119,6 +119,7 @@ const features = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test.each([ diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ffe581366..61d1dba5e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 67e627c91..2cd111060 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, }); diff --git a/packages/compiler/src/coverage/verdict-cache.test.ts b/packages/compiler/src/coverage/verdict-cache.test.ts index 1c3e9bc3d..0380e8b8d 100644 --- a/packages/compiler/src/coverage/verdict-cache.test.ts +++ b/packages/compiler/src/coverage/verdict-cache.test.ts @@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true })); test("unchanged and comment-only reruns replay the verdict; code edits relower", () => { const entry = join(dir, "main.ts"); - const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`; + // The blocker: an object pattern over a union source (arrays alone compile). + const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`; writeFileSync(entry, program); const entries = new Map(); const store: VerdictStore = { diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index e5887e855..1b42c3eca 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(): Promise<{ diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index b00372e22..8c67c1006 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -56,6 +56,8 @@ export interface EarlyExecutableNativeFeatures { fileHandle: boolean; fetch: boolean; eventTarget?: boolean; + warningReplace?: boolean; + stdioExtended?: boolean; zlib: boolean; assert: boolean; inspect: boolean; @@ -81,6 +83,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; tlsCa: boolean; + webcrypto: boolean; } interface EarlyExecutableCacheStamp { @@ -194,6 +197,7 @@ const BOOLEAN_NATIVE_KEYS = [ "tls", "tlsCa", "cipher", + "webcrypto", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 9ba041d2c..c30788fa1 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -109,7 +109,7 @@ function markerNamed( if (!MARKERS.has(written)) return null; let member = written; const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; let importDecl: ts.Node | undefined; if (ts.isImportSpecifier(decl) && !qualified) { diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 86aa10933..8fc2238bf 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -18,7 +18,7 @@ export function experimentalImportOf( ): ExperimentalMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( @@ -67,7 +67,7 @@ export function lifetimeRegionMemberOf( ): "run" | "release" | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( @@ -91,7 +91,7 @@ export function internTableMemberOf( ): InternTableMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if ( decl === undefined || !( @@ -129,7 +129,7 @@ export function threadSlotsMemberOf( ): ThreadSlotsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index a7248c674..592bbb2d6 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -8,7 +8,7 @@ import { moduleSourceCandidates } from "./module-source-candidates.js"; export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(expression); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; return ( declarations.length > 0 && declarations.every((declaration) => { @@ -45,7 +45,7 @@ export function isBuiltinMemberImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl !== undefined && ts.isImportSpecifier(decl)) { const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -60,7 +60,7 @@ export function isBuiltinMemberImport( } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let parent: ts.Node | undefined = targetDecl.parent; @@ -88,7 +88,7 @@ export function isBuiltinNamespaceImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; let importDecl: ts.ImportDeclaration | undefined; if (decl !== undefined && ts.isNamespaceImport(decl)) { const candidate = decl.parent?.parent; @@ -108,12 +108,9 @@ function constInitializer(program: ts.Program, expr: ts.Expression): ts.Expressi if (!ts.isIdentifier(current)) return null; const symbol = program.getTypeChecker().getSymbolAtLocation(current); const decl = symbol - ? program - .getTypeChecker() - .declarationsOf(symbol) - .find((candidate): candidate is ts.VariableDeclaration => - ts.isVariableDeclaration(candidate), - ) + ? symbol.declarations.find((candidate): candidate is ts.VariableDeclaration => + ts.isVariableDeclaration(candidate), + ) : undefined; if ( decl === undefined || diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index a4ad48892..7095cea5d 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -25,15 +25,13 @@ export function inferredJsDiagnosticSuppressed( const symbol = checker.getSymbolAtLocation(node.name); if ( symbol && - checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.type && - !decl.initializer && - !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.type && + !decl.initializer && + !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), + ) ) { // Bare JS fields exist with value undefined before the first write; // native storage includes that arm even when inference omits it. @@ -43,7 +41,7 @@ export function inferredJsDiagnosticSuppressed( } if (diagnostic.code === 2554 && ts.isCallExpression(node)) { const signature = checker.getResolvedSignature(node); - const declaration = signature && checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( declaration && isJsSourceFileName(declaration.getSourceFile().fileName) && @@ -83,9 +81,7 @@ export function inferredJsDiagnosticSuppressed( checker.getTypeOfSymbolAtLocation(throwMethod, node.expression), ); const declaration = - throwSignatures.length === 1 - ? checker.signatureDeclaration(throwSignatures[0]!) - : undefined; + throwSignatures.length === 1 ? throwSignatures[0]!.declaration : undefined; // A sole throw cannot return an invalid IteratorResult. Avoid inferring // termination through arbitrary control flow or explicit return types. if ( diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index feeb23019..ee8a34ea2 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -176,7 +176,7 @@ export function assertInstanceOf( ): AssertInstanceOptions | null { if (!ts.isIdentifier(expr)) return null; const symbol = lowerer.checker.getSymbolAtLocation(expr); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; return staticAssertInstanceDecl(lowerer, decl.name, decl.initializer); } diff --git a/packages/compiler/src/frontend/lowering/assertion-identity.ts b/packages/compiler/src/frontend/lowering/assertion-identity.ts new file mode 100644 index 000000000..6a00b7afe --- /dev/null +++ b/packages/compiler/src/frontend/lowering/assertion-identity.ts @@ -0,0 +1,78 @@ +/* Type assertions of the global object. + * + * `globalThis as { [KEY]?: State }` is the process-global state idiom: a + * module asserts the global object to a record type and reads and writes + * its members, and every other module that asserts it sees the same + * members. Node evaluates the assertion to the global object itself. + * + * The global object is a checked-dynamic value, and a checked-dynamic value + * asserted to a record or array type takes the checked extraction + * (dynCheck), which builds a new native value. For the global object that + * copy is always wrong: a write through it never reached the global object, + * and a view bound before another module's write never saw it. So an + * assertion of the global object keeps the checked-dynamic value itself: a + * view whose member reads and writes go to the global object, and bindings + * of it hold that object. + * + * Other checked-dynamic operands keep the checked extraction: a native value + * held as `unknown` unwraps by identity there, and a checked-dynamic tree is + * copied at the documented checked-value boundary. + */ +import * as ts from "../ts7/adapter.js"; +import type { IrType } from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { stdlibGlobalNameOf } from "./surfaces.js"; + +/** A target whose checked extraction builds a new mutable value. */ +export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean { + const seen = new Set(); + const copies = (t: IrType): boolean => { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false; + }; + return copies(type); +} + +/** The asserted expression without parentheses, `!` and nested + * assertions (`globalThis as unknown as T`). */ +function assertedSource(expr: ts.Expression): ts.Expression { + let e = expr; + while ( + ts.isParenthesizedExpression(e) || + ts.isNonNullExpression(e) || + ts.isAsExpression(e) || + ts.isTypeAssertion(e) || + ts.isSatisfiesExpression(e) + ) + e = e.expression; + return e; +} + +/** `expr` asserts the global object (`globalThis`, `global`). */ +export function assertsGlobalObject( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, +): boolean { + const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression)); + return name === "globalThis" || name === "global"; +} + +/** The assertion a binding initializer is, when it keeps the global object: + * bindings of it hold that object rather than a native value of the + * asserted type. A target with no native layout keeps the object as well + * (the assertion lowers to the checked-dynamic value either way). */ +export function keptIdentityAssertion( + lowerer: Lowerer, + initializer: ts.Expression, +): ts.AsExpression | ts.TypeAssertion | undefined { + let e = initializer; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined; + if (!assertsGlobalObject(lowerer, e)) return undefined; + const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type)); + return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target) + ? e + : undefined; +} diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 816294e08..88b0f91dc 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -114,7 +114,7 @@ export function nullishValueUnitOf( const cached = lowerer.nullishBindings.get(sym); if (cached !== undefined) return cached; lowerer.nullishBindings.set(sym, null); // cycle guard: self-referential probes answer non-qualifying - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; // Statement-position declarators only: a for-loop head (`for (let x = // null as any; ...)`) declares a LOCAL with per-iteration semantics — // lowerVarDeclList's contract requires a lowered statement for it, so diff --git a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts new file mode 100644 index 000000000..e6221384b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts @@ -0,0 +1,52 @@ +/** A builtin module function used as a JavaScript value when it has no + * value adapter (lowerNativeCallableValue). Node hands the program a real + * function; the compiled value is one function per builtin that throws the + * named fence when called. Identity flows (`fn === fs.readFileSync`, Set + * membership) still hold, `typeof` answers "function" and inspection + * prints `[Function: name]`, where a string token would answer "string" + * and turn a call into an ordinary "is not a function" TypeError. */ +import * as ts from "../ts7/adapter.js"; +import { DYN, type IrExpr, type SrcLoc, funcOf } from "../../ir/ir.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import type { Lowerer } from "./lowerer.js"; + +export function builtinFunctionFenceValue( + lowerer: Lowerer, + bi: { module: string; member: string }, + node: ts.Identifier, + loc: SrcLoc, +): IrExpr { + const display = `${bi.module}.${bi.member}`; + const name = `%builtin.fence.${display}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag( + "SC1090", + loc, + `library functions as values ('${display}' has no function-value lowering; call it directly)`, + ); + lowerer.runtimeFences.push(diag); + const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1; + lowerer.liftedFns.push({ + name, + params: [], + locals: [], + returnType: DYN, + body: [ + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + loc, + }); + } + return { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc }, + fnName: bi.member, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 708284be1..e6de99b09 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -34,7 +34,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression // has no other first-class story. if (ts.isIdentifier(node) && node.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(node); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index 5e4db921c..416217716 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -93,7 +93,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol) if (builtinClassAliasOf(lowerer, symbol) === null) return false; const binding = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker.valueDeclarationOf(binding); + const decl = binding.valueDeclaration; if (decl === undefined || !ts.isVariableDeclaration(decl)) return false; if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false; if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false; @@ -189,7 +189,7 @@ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolea * can name a class (an identifier or a property access), or null. */ function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null { if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl === undefined || !ts.isVariableDeclaration(decl) || diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 0cd25cde6..e69011522 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -138,9 +138,7 @@ export function zlibStreamModeOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const mode = ZLIB_STREAM_MODES.get(target.name); if (mode === undefined) return null; - return lowerer.checker - .declarationsOf(target) - .some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) + return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) ? mode : null; } @@ -161,15 +159,13 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - d.parent.name?.text === "ZlibBase" && - isZlibFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + d.parent.name?.text === "ZlibBase" && + isZlibFallbackDecl(lowerer, d.parent), + ); } /** `stream.bytesWritten` on a zlib stream. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index abfc20cdc..611805d94 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -106,7 +106,7 @@ export function builtinConstantsDestructureDecl( * its baked literal value. Null for every other binding. */ export function builtinConstantBindingOf(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null; if (!ts.isObjectBindingPattern(decl.parent)) return null; const varDecl = decl.parent.parent; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 425b15f22..cd490bc87 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -879,7 +879,7 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean { return ( !!symbol && CIPHER_TYPE_NAMES.has(symbol.name) && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 1572aa6ab..0313cdd10 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -19,9 +19,9 @@ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean { const sym = t.getSymbol(); return ( sym?.name === "ExecException" && - lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + sym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 58ad82358..22e524690 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -34,15 +34,13 @@ function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isFsStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && - isFsFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && + isFsFallbackDecl(lowerer, d.parent), + ); } /** bytesRead / bytesWritten / pending / path reads. Null for other @@ -156,9 +154,9 @@ export function fsStreamClassOf( symbol: ts.Symbol | null | undefined, ): "ReadStream" | "WriteStream" | null { if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null; - const declared = lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d)); + const declared = symbol.declarations.some( + (d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d), + ); return declared ? symbol.name : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index b89e25f39..b48030ba5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -504,9 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(node); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 3a178b126..86d93de2c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -28,7 +28,7 @@ export function builtinImportOf( ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident ? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent) : lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin of the named import: a destructured require // binding (`const { readFileSync } = require("fs")`, renames via @@ -123,7 +123,7 @@ export function builtinImportOf( // keys the same tables under the builtin's own member name. if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = lowerer.checker.getAliasedSymbol(symbol); - const tdecl = lowerer.checker.declarationsOf(target)[0]; + const tdecl = target.declarations[0]; if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = tdecl.parent; @@ -153,7 +153,7 @@ export function esmImportBindingKind( ident: ts.Identifier, ): "namespace" | "named" | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; if (ts.isNamespaceImport(decl)) return "namespace"; return ts.isImportSpecifier(decl) ? "named" : null; @@ -290,7 +290,7 @@ export function createRequireCalleeFileOf( } if (!ts.isIdentifier(e)) return null; const symbol = lowerer.checker.getSymbolAtLocation(e); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; return createRequireBindingDecl(lowerer, decl.name, decl.initializer) ? decl.getSourceFile() @@ -391,7 +391,7 @@ export function createRequireProgramModuleDecl( * binding poison with the same module-naming diagnostic. */ export function fencedBuiltinImportOf(lowerer: Lowerer, ident: ts.Identifier): string | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twins: `const x = require("net")` and // `const { createServer } = require("net")` — the require statement diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts new file mode 100644 index 000000000..637edaf3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -0,0 +1,60 @@ +/* Node 24's `navigator` global (lib/internal/navigator.js), the part + * libraries read to tell runtimes apart: `typeof navigator` is "object" + * and `navigator.userAgent` is `Node.js/`. jose reads exactly + * that at module load (jwks/remote.js). Both fold to string literals, so + * nothing links. The other members (hardwareConcurrency, language, + * languages, platform, locks) and the object as a value keep a named + * refusal. */ +import * as ts from "../../ts7/adapter.js"; +import { strLit } from "../../../ir/build.js"; +import type { IrExpr } from "../../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import type { Lowerer } from "../lowerer.js"; + +/** True when `node` names Node's global `navigator`: the @types/node + * declaration, or, in a JavaScript file, the free name no declaration + * binds (Node defines it on the global object). */ +export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false; + if (lowerer.isStdlibGlobal(expr, "navigator")) return true; + return ( + isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined + ); +} + +/** `navigator.userAgent` (`Node.js/` of `nodeVersion`, the pinned + * release builtins/process.ts names), or the named refusal for another + * member. Null when the receiver is not the global. */ +export function lowerNavigatorMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + nodeVersion: string, +): IrExpr | null { + if (!isNavigatorGlobal(lowerer, access.expression)) return null; + if (access.name.text === "userAgent") { + return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access)); + } + return refuseNavigator( + lowerer, + access, + `navigator.${access.name.text}`, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** The refusal for the `navigator` object itself or an unlowered member. */ +export function refuseNavigator( + lowerer: Lowerer, + node: ts.Node, + what: string, + symbol?: ts.Symbol, +): never { + return lowerer.noLowering( + what, + node, + "of Node's navigator global only typeof navigator and navigator.userAgent compile", + symbol ?? null, + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index e4e5004c1..555eaf84f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -26,6 +26,7 @@ import { funcOf, } from "../../../ir/ir.js"; import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js"; +import { canonicalBuiltinModule } from "../../builtin-modules.js"; import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js"; import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js"; @@ -148,6 +149,18 @@ export function lowerProcessStreamProperty( loc: locOf(expr), }; } + // `if (process.stdin.setRawMode)`: Node's stdin has the method only + // when it is a TTY. The runtime's stream object answers the same + // presence (the method or undefined); calls keep their direct lowering. + if (direct === "stdin" && member === "setRawMode") { + return { + kind: "dynKeyGet", + value: lowerer.lowerExpr(expr.expression), + key: strLit(member, locOf(expr.name)), + type: DYN, + loc: locOf(expr), + }; + } if (member !== "isTTY" && member !== "columns" && member !== "rows") return null; let recv: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) @@ -364,6 +377,11 @@ export function lowerProcessProperty( if (member === "versions") { return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc }; } + // process.features as a value (`"sea" in process.features`): an object + // holding none of the flags, consistent with the member reads above. + if (member === "features") { + return { kind: "dynObjLit", type: DYN, loc }; + } // process.noDeprecation / process.throwDeprecation: the values the // program assigned (undefined before that — no Node flags reach a // compiled binary). @@ -627,6 +645,73 @@ export function lowerProcessMethodCall( } return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc }; } + // process.stdout.on("resize", cb) (and once/off/removeListener/ + // addListener, stderr too): the terminal resize event, registered on the + // runtime's process stream object (a SIGWINCH listener that emits when a + // TTY's size changed; it never keeps the loop alive). Statement position: + // Node returns the stream. + if ( + ["on", "once", "off", "removeListener", "addListener"].includes(access.name.text) && + ts.isPropertyAccessExpression(access.expression) && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) && + ts.isExpressionStatement(call.parent) + ) { + const stream = lowerer.stdlibGlobalMember(access.expression, "process"); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); + if ( + (stream === "stdout" || stream === "stderr") && + evT.isStringLiteralType() && + evT.value === "resize" + ) { + const loc = locOf(call); + const receiverLocal = lowerer.declareHiddenLocal("%stdioReceiver", DYN); + const receiver = varRef(receiverLocal.id, DYN, loc); + const method = + access.name.text === "off" + ? "removeListener" + : access.name.text === "addListener" + ? "on" + : access.name.text; + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: receiverLocal.id, + init: { + kind: "libCall", + fn: "process.stdio", + args: [numLit(stream === "stdout" ? 1 : 2, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + result: { + kind: "dynCall", + callee: { + kind: "dynKeyGet", + value: receiver, + key: strLit(method, locOf(access.name)), + type: DYN, + loc, + }, + receiver, + calleeName: access.getText(), + args: [ + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + ], + type: DYN, + loc, + }, + type: DYN, + loc, + }; + } + } // process.stdin.on/once("data" | "end" | "error", cb): the piped-stdin // event slice. A 'data' listener keeps the event loop alive until EOF // (Node's flowing stdin); 'end'/'error' listeners alone do not. `once` @@ -678,11 +763,18 @@ export function lowerProcessMethodCall( const param = cb.type.params[0]; const onceArg: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc }; if (event === "data") { - if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) { + // A string parameter receives the chunks process.stdin.setEncoding + // decodes (pi-tui's terminal input); the runtime refuses a delivery + // while no encoding is set, where Node would pass a Buffer. + if ( + param !== undefined && + !(param.kind === "bytes" && param.elem === "u8") && + param.kind !== "string" + ) { lowerer.unsupported( "SC1090", call.arguments.nodes[1]!, - `data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`, + `data listeners whose parameter is not 'Uint8Array' or 'string' (got '${lowerer.fmt(param)}')`, ); } return { kind: "libCall", fn: "stdin.onData", args: [cb, onceArg], type: VOID, loc }; @@ -1537,3 +1629,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr { loc, }; } + +/** `process.getBuiltinModule("node:os")`: a string-literal id naming a + * supported builtin whose call the checker types as that module's + * namespace (@types/node's BuiltInModule map) answers the namespace value + * `import * as ns` binds, so its members key the builtin tables at their + * use sites. The receiver may be cast (`(process as P)`) and the call + * optional: Node 24's process always has the member, and the cast changes + * no runtime value. Other ids keep the dynamic loader (undefined for + * unknown ids, Node's argument errors). */ +export function lowerProcessBuiltinModuleNamespace( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule") + return null; + const id = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null; + let receiver: ts.Expression = callee.expression; + while ( + ts.isParenthesizedExpression(receiver) || + ts.isAsExpression(receiver) || + ts.isSatisfiesExpression(receiver) || + ts.isNonNullExpression(receiver) + ) + receiver = receiver.expression; + if (!lowerer.isStdlibGlobal(receiver, "process")) return null; + const module = canonicalBuiltinModule(id.text); + if (module === null) return null; + const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call))); + if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null; + return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 3a0e7ccf0..6baeb65d1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -46,7 +46,7 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Interface") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; return isReadlineModule(ambientModuleOf(d)); @@ -107,7 +107,7 @@ function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Readline") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; const spec = ambientModuleOf(d); diff --git a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts index b90eb4844..fc507e2bd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts @@ -11,11 +11,9 @@ export function isBuiltinClassInstance( const type = lowerer.checker.getTypeAtLocation(node); const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (symbol?.name !== name) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && - lowerer.isStdlibFile(declaration.getSourceFile()), - ); + return symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && + lowerer.isStdlibFile(declaration.getSourceFile()), + ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index 013cd3fda..54ab22c05 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; -import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js"; +import { + BOOL, + DYN, + F64, + type IrExpr, + type IrStmt, + NULL_T, + STRING, + arrayOf, + bytesOf, + isUnitType, + typeEquals, +} from "../../../ir/ir.js"; /** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument * lowers as a number and packs into ONE f64[] array-literal argument @@ -17,6 +29,8 @@ export function lowerStringStaticCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken) return null; + const applied = lowerFromCharCodeApply(lowerer, call, access); + if (applied) return applied; const member = lowerer.stdlibGlobalMember(access, "String"); if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null; const loc = locOf(call); @@ -108,6 +122,99 @@ export function lowerStringStaticCall( return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc }; } +/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom + * (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver + * is ignored, and a typed array or number array forwards to the packed + * fromCharCode like a spread. An untyped list reads a Uint8Array the same + * way at run time; any other value keeps Function.prototype.apply's + * generic array-like call. Null for every other call. */ +function lowerFromCharCodeApply( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "apply" || access.questionDotToken) return null; + const target = access.expression; + if ( + !ts.isPropertyAccessExpression(target) || + lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode" + ) { + return null; + } + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; + const listNode = call.arguments.nodes[1]!; + const checkerT = lowerer.typeOf(listNode); + const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT); + if ( + listT === null || + (listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64))) + ) { + return null; + } + const loc = locOf(call); + const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); + const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }]; + if (listT.kind !== "dyn") { + const list = lowerer.lowerExprExpecting(listNode, listT); + const result: IrExpr = { + kind: "libCall", + fn: "string.fromCharCode", + args: [list], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; + } + const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN); + stmts.push({ + kind: "varDecl", + localId: listLocal.id, + init: lowerer.lowerExprExpecting(listNode, DYN), + loc, + }); + const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc }); + const bytes = bytesOf("u8"); + const generic: IrExpr = { + kind: "dynCheck", + value: { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(target, DYN), + method: "apply", + calleeName: "String.fromCharCode.apply", + args: [ + { + kind: "dynFrom", + value: { kind: "unitLit", unit: "null", type: NULL_T, loc }, + type: DYN, + loc, + }, + list(), + ], + type: DYN, + loc, + }, + type: STRING, + loc, + }; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc }, + then: { + kind: "libCall", + fn: "string.fromCharCode", + args: [{ kind: "dynCheck", value: list(), type: bytes, loc }], + type: STRING, + loc, + }, + else_: generic, + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + /** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16 * indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end; * MAX_SAFE_INTEGER has the same effect for every representable string. diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index a5f34f44e..e89b1f07e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -361,13 +361,11 @@ export function storedTextCodecClassOf( ): TextCodecCtor["cls"] | null { const sym = lowerer.typeOf(expr).getSymbol(); if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + return sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ? sym.name : null; } diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index dabfb1d2a..9f950cddc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -155,9 +155,9 @@ function isTimerHandleTyped( const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())); + return sym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ); } function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean { return isTimerHandleTyped(lowerer, node, "Timeout"); diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts new file mode 100644 index 000000000..2a9ff9eef --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -0,0 +1,130 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; +import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; + +/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, + * `sign`, `verify` and `digest` called on the crypto global (also through + * globalThis) lower to runtime entry points that take the argument vector + * as one dyn array and answer a promise, so Web IDL conversion, algorithm + * normalization and every error stay Node-shaped at run time. CryptoKey + * values are native handles; their type maps to dyn and their members + * are handle reads. Other SubtleCrypto methods are refused by name. */ + +const SUBTLE_LOWERED: ReadonlyMap = new Map([ + ["importKey", "webcrypto.importKey"], + ["sign", "webcrypto.sign"], + ["verify", "webcrypto.verify"], + ["digest", "webcrypto.digest"], +]); + +const SUBTLE_HINT = + "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + +/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isPropertyAccessExpression(node) && + !node.questionDotToken && + node.name.text === "subtle" && + stdlibGlobalNameOf(lowerer, node.expression) === "crypto" + ); +} + +/** `crypto.subtle.(...args)`: the lowered methods become their + * libCall over the argument vector; every other method is refused. */ +export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) { + return null; + } + const member = callee.name.text; + const fn = SUBTLE_LOWERED.get(member); + if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT); + if (callee.questionDotToken || call.questionDotToken) { + lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT); + } + if (call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); + } + // JsonWebKey imports are not implemented: a literal 'jwk' format is + // refused here (the runtime refuses a computed one). + const format = call.arguments.nodes[0]; + if ( + member === "importKey" && + format !== undefined && + ts.isStringLiteralLike(format) && + format.text === "jwk" + ) { + lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); + } + const loc = locOf(call); + const args = call.arguments.nodes.map((arg) => + ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + return { + kind: "libCall", + fn, + args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }], + type: { kind: "promise", inner: member === "verify" ? BOOL : DYN }, + loc, + }; +} + +/** `v instanceof CryptoKey` against the global. */ +export function lowerCryptoKeyInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null; + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "webcrypto.isCryptoKey", + args: [value], + type: BOOL, + loc: locOf(expr), + }; +} + +/** TypeScript reads of `crypto.subtle` and its members outside a direct + * call: the global's dynamic object does not carry the lowered methods, + * so a detached read is refused rather than answering a value without + * them. JavaScript keeps the dynamic object's reads. */ +export function fenceWebCryptoMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression, + use: "read" | "call", +): IrExpr | null { + if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null; + if (isJsSourceFile(access.getSourceFile())) return null; + if (isSubtleReceiver(lowerer, access)) { + lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT); + } + if (isSubtleReceiver(lowerer, access.expression)) { + lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT); + } + return null; +} + +/** Whether a checker type is one of the WebCrypto interfaces reached + * through the crypto global's members (SubtleCrypto, CryptoKey and the + * algorithm dictionaries). While `crypto` was typed `unknown` those member + * reads were untyped, so JavaScript destructurings of them keep the + * module-global path untyped bindings take. `Crypto` itself is excluded: + * the global was `unknown`, which already mapped to dyn. */ +export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean { + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + return ( + symbol !== undefined && + symbol.name !== "Crypto" && + WEBCRYPTO_DYN_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index aa826cc53..335fdb969 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -175,7 +175,7 @@ function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: strin const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let up: ts.Node | undefined = d.parent; diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9d9c9668c..6db1735db 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -408,7 +408,15 @@ export function paramShapes( return { type: mapped, mode: "rest" }; } if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped); - if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) { + // A checked-value slot holds undefined directly, so an omitted + // argument needs no union arm (paramShape's declared-signature rule; + // an erased type parameter and a class-implemented interface view + // both map here). + if ( + declParam.questionToken && + !lowerer.bareUndefinedArmedUnion(mapped) && + mapped.kind !== "dyn" + ) { lowerer.unsupported( "SC1090", declParam, @@ -878,8 +886,7 @@ export function lambdaSignature( const ctxSigs = ctxType ? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType)) : []; - const ctxDecl = - ctxSigs.length === 1 ? lowerer.checker.signatureDeclaration(ctxSigs[0]!) : undefined; + const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined; const ctxRetRaw = ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!)) diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index 0fb5aadd3..5ea4001fc 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -37,7 +37,7 @@ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): b if (!ts.isIdentifier(node) || node.text !== "exports") return false; if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; return !decls.some( (d) => !d.getSourceFile().isDeclarationFile && @@ -349,7 +349,7 @@ function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { /** A function declaration, or a variable initialized with a function or * arrow expression, that never reads `this`. */ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const fn = decls.find(ts.isFunctionDeclaration); if (fn !== undefined) return !functionUsesThis(fn); const v = decls.find(ts.isVariableDeclaration); diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 9b41f0012..174f9dc7c 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -353,7 +353,7 @@ export function cjsExportValueByName( if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); const direct = lowerer.globalsBySymbol.get(sym); if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { const byDecl = lowerer.globalsByDeclNode.get(d); if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; // Alias plumbing: a shorthand/identifier table entry or a member diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index 93a1e3260..cd83c8709 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -132,7 +132,7 @@ export function tscInteropBindingOf( ): TscInteropBinding | null { if (!ts.isIdentifier(expr)) return null; const sym = lowerer.checker.getSymbolAtLocation(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + const decl = sym ? sym.declarations.find(ts.isVariableDeclaration) : undefined; return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; } diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 4dae7ecd0..7f4a003c9 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; +import { constructedValue } from "./constructor-returns.js"; import { BOOL, DYN, @@ -389,13 +390,13 @@ export class ClassConstructionDispatch { if (!args) continue; const type: IrType = { kind: "classval", className: name }; const callee = varRef("p.0", DYN, loc); - const value: IrExpr = { + const value = constructedValue(lowerer, info, { kind: "newValue", callee: { kind: "dynCheck", value: callee, type, loc }, args, type: { kind: "object", className: name }, loc, - }; + }); const body: IrStmt[] = [ { kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }, ]; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a0ed496e1..6b515c1ff 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; import { BOOL, @@ -2883,7 +2884,7 @@ export class ClassDynamicDispatch { if (info.symbolMethods) for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]); for (const [symbol, name] of symbolEntries) { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -3164,13 +3165,13 @@ export class ClassDynamicDispatch { } if (!supported || !info.decl) continue; const cls = classValueRef(lowerer, info, info.decl); - const result: IrExpr = { + const result = constructedValue(lowerer, info, { kind: "new", className: name, args, type: { kind: "object", className: name }, loc, - }; + }); this.constructDispatch.body.unshift({ kind: "if", cond: { diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 2a0e3a7eb..c07ec9136 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -93,7 +93,7 @@ export function collectClassMethodMutations( value = unwrap(value); if (ts.isIdentifier(value)) { const symbol = lowerer.checker.getSymbolAtLocation(value); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -162,7 +162,7 @@ export function collectClassMethodMutations( const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { if (!ts.isIdentifier(key)) return null; const symbol = lowerer.checker.getSymbolAtLocation(key); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null; @@ -413,7 +413,7 @@ export function collectClassMethodMutations( const symbol = ts.isPropertyAccessExpression(access) ? lowerer.checker.getSymbolAtLocation(access.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); - if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) + if (symbol && symbol.declarations.some(ts.isMethodDeclaration)) lowerer.prototypeMethodAccesses.set(name, access); } lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); @@ -480,8 +480,7 @@ export function collectClassMethodMutations( } const type = lowerer.checker.getDeclaredTypeOfSymbol(symbol); const property = lowerer.checker.getPropertyOfType(type, name); - if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration)) - continue; + if (!property || !property.declarations.some(ts.isMethodDeclaration)) continue; if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) { owners = null; break; diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index c8e56f3cc..0c9a24f24 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -99,7 +99,7 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean { if (!symbol) return false; const cached = lowerer.protocolOwnedBindings.get(symbol); if (cached !== undefined) return cached; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; let owned = decl !== undefined && decl.getSourceFile() === id.getSourceFile() && @@ -191,7 +191,7 @@ function protocolSignature( const recvTs = lowerer.typeOf(access.expression); const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if (!property) return null; - const declarations = lowerer.checker.declarationsOf(property); + const declarations = property.declarations; if ( declarations.length === 0 || !declarations.every( @@ -200,7 +200,7 @@ function protocolSignature( ) return null; const signature = lowerer.checker.getResolvedSignature(call); - const declaration = signature && lowerer.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( !signature || !declaration || @@ -490,7 +490,7 @@ export function lowerProtocolPropertyRead( if (!lowerer.classImplementedProtocol(receiverType)) return null; const name = access.name.text; const property = lowerer.checker.getPropertyOfType(receiverType, name); - const declarations = property ? lowerer.checker.declarationsOf(property) : []; + const declarations = property ? property.declarations : []; if (declarations.length === 0 || !declarations.every((decl) => ts.isPropertySignature(decl))) return null; const result = lowerer.mapTypeOf(lowerer.typeOf(access)); diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 9e69d08b7..5a27f0815 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -37,15 +37,13 @@ export class ClassProtocols { const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined; for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) { if ( - !this.checker - .declarationsOf(property) - .some( - (member) => - ts.isMethodDeclaration(member) && - !member.modifiers?.nodes.some( - (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, - ), - ) + !property.declarations.some( + (member) => + ts.isMethodDeclaration(member) && + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), + ) ) continue; const entries = this.methods.get(property.name) ?? []; @@ -289,7 +287,7 @@ export class ClassProtocols { for (const part of parts) { if ((part.flags & ts.TypeFlags.TypeParameter) === 0) continue; const symbol = part.getSymbol(); - for (const declaration of symbol ? this.checker.declarationsOf(symbol) : []) + for (const declaration of symbol ? symbol.declarations : []) if (ts.isTypeParameterDeclaration(declaration) && declaration.parent !== undefined) facts.genericObservers.add(declaration.parent); } @@ -501,7 +499,7 @@ export class ClassProtocols { if (context !== undefined && this.acceptsAnyValue(context)) return true; if (facts.genericObservers.size === 0) return false; const signature = this.checker.getResolvedSignature(arg.call); - const declaration = signature && this.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; return ( declaration !== undefined && (facts.genericObservers.has(declaration) || @@ -520,13 +518,10 @@ export class ClassProtocols { .some( (property) => names.has(property.name) && - this.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration), - ), + property.declarations.some( + (declaration) => + ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration), + ), ); } @@ -580,9 +575,7 @@ export class ClassProtocols { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - this.checker - .declarationsOf(symbol) - .some((declaration) => !declaration.getSourceFile().isDeclarationFile) && + symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) && // `typeof C` (the class value) shares the class symbol. this.checker.getConstructSignatures(type).length === 0 ); @@ -603,13 +596,10 @@ export class ClassProtocols { // A program interface a class matches by its methods (protocol below). return ( this.methods.size > 0 && - this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) && + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) && this.protocol(type) ); } @@ -624,7 +614,7 @@ export class ClassProtocols { if (this.checker.getConstructSignatures(type).length > 0) return false; const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; return ( declarations.length > 0 && declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile) @@ -712,13 +702,10 @@ export class ClassProtocols { if (symbol && protocols.has(symbol)) return true; if ( !symbol || - !this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) + !symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) ) return false; for (const property of properties) { diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 403a4a3b4..2787c9021 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -555,7 +555,7 @@ function prototypeProtectedKeys( } else if (name.startsWith("%symbol:")) { const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : []; const entry = entries.find(([, field]) => field === name); - const declaration = entry ? lowerer.checker.valueDeclarationOf(entry[0]) : undefined; + const declaration = entry ? entry[0].valueDeclaration : undefined; if ( !declaration || !ts.isVariableDeclaration(declaration) || diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index c172b40cf..f52026ba4 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,8 +1,115 @@ -import { BOOL, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { checkedClassAssertion } from "./class-assertions.js"; import type { Lowerer } from "./lowerer.js"; +/** The classes a narrowed read of a `staticClass` value can hold when the + * checker typed it as the subclass `narrowed`, as a class union, or null + * when `narrowed` is the only one. + * + * TypeScript reduces a union of type-parameter intersections by structural + * subtyping. With ``, `node instanceof Identifier || node + * instanceof PrivateIdentifier` narrows `node` to `T & PrivateIdentifier` + * when Identifier has every member PrivateIdentifier has, so the narrowed + * read can hold an Identifier. Class identity is nominal in compiled code: + * such a read holds the narrowed class or any class under `staticClass` + * that is assignable to it without extending it. A narrowing outside a + * type-parameter intersection keeps every class it names. */ +export function structuralNarrowing( + lowerer: Lowerer, + node: ts.Node, + staticClass: string, + narrowed: IrType & { kind: "object" }, +): (IrType & { kind: "union" }) | null { + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const key = `${staticClass}:${narrowed.className}`; + let peers = lowerer.structuralClassPeers.get(key); + if (peers === undefined) { + peers = assignableClassPeers(lowerer, staticClass, narrowed.className); + lowerer.structuralClassPeers.set(key, peers); + } + if (peers.length === 0) return null; + const arms: IrType[] = [ + narrowed, + ...peers.map((className) => ({ kind: "object" as const, className })), + ]; + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: lowerer.unions.intern(arms) }; +} + +/** The type a member access dispatches on when its receiver `node` is a + * narrowed read of a type-parameter intersection, or null to use the + * checker's type: the class union structuralNarrowing reads, or the + * receiver's own class when this instantiation's class is unrelated to the + * narrowed one but assignable to it (with `T = Identifier`, the reduced + * `T & PrivateIdentifier` still holds an Identifier). */ +export function structuralReadType(lowerer: Lowerer, node: ts.Expression): IrType | null { + if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (narrowed?.kind !== "object") return null; + const stored = ts.isIdentifier(node) ? lowerer.peekLocal(node)?.type : undefined; + const symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(node) ? node : node.name); + const declared = + stored ?? (symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null); + if (declared?.kind !== "object" || declared.className === narrowed.className) return null; + if (lowerer.isSubclassOf(narrowed.className, declared.className)) + return structuralNarrowing(lowerer, node, declared.className, narrowed); + if (lowerer.isSubclassOf(declared.className, narrowed.className)) return null; + const declaredType = classInstanceType(lowerer, declared.className); + const narrowedType = classInstanceType(lowerer, narrowed.className); + return declaredType !== null && + narrowedType !== null && + lowerer.checker.isTypeAssignableTo(declaredType, narrowedType) + ? declared + : null; +} + +function classInstanceType(lowerer: Lowerer, className: string): ts.Type | null { + const decl = lowerer.classes.get(className)?.decl; + if (!decl?.name || decl.typeParameters?.nodes.length) return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; +} + +function intersectsTypeParameter(type: ts.Type): boolean { + try { + if (type.isUnionType()) return ts.constituentTypes(type).some(intersectsTypeParameter); + return ( + (type.flags & ts.TypeFlags.Intersection) !== 0 && + ts.constituentTypes(type).some((part) => (part.flags & ts.TypeFlags.TypeParameter) !== 0) + ); + } catch (e) { + if (ts.isUnreadableConstituentsPanic(e)) return false; + throw e; + } +} + +/** The outermost classes under `staticClass`, other than `target` and its + * subclasses, whose instance type is assignable to `target`'s. */ +function assignableClassPeers(lowerer: Lowerer, staticClass: string, target: string): string[] { + const targetType = classInstanceType(lowerer, target); + if (targetType === null) return []; + const assignable: string[] = []; + for (const className of lowerer.classes.keys()) { + if ( + className === target || + !lowerer.isSubclassOf(className, staticClass) || + lowerer.isSubclassOf(className, target) + ) + continue; + const type = classInstanceType(lowerer, className); + if (type !== null && lowerer.checker.isTypeAssignableTo(type, targetType)) + assignable.push(className); + } + // A subclass of an assignable class is one of its instances already. + return assignable.filter( + (className) => + !assignable.some((other) => other !== className && lowerer.isSubclassOf(className, other)), + ); +} + /** A base-class slot stays one pointer after a guard selects several * subclasses. Recover the tagged view from runtime membership without * copying the object or guessing which of the remaining layouts it has. */ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 78c7cb73d..2bc991c14 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,4 +1,4 @@ -import { planRecordUnionWrap } from "../union-retag.js"; +import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js"; import type { WidthLift } from "../width-lift.js"; import type { IrType } from "../../../ir/ir.js"; import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; @@ -78,6 +78,24 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width }); if (absentOnly.length === 1) selected = absentOnly; } + // A record that fits several arms of a discriminated union selects one + // by its discriminant at run time. Without this, an arm that a nested + // field could reach only ambiguously dropped out of the candidates, and + // an arm reached by dropping fields could win alone. + if (selected.length !== 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + if ( + source && + planRecordUnionLift( + source, + def, + (id) => lowerer.shapes.get(id), + (from, to) => lowerer.widthLiftPlan(from, to), + ) + ) { + return { how: "discriminantRoute" }; + } + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 925f59d9b..c85b9868d 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; import { buildArrayConversion } from "./builders.js"; @@ -106,6 +107,16 @@ export function applyWidthLift( ); return { kind: "call", callee: helper, args: [value], type: dst, loc }; } + case "discriminantRoute": { + if (dst.kind !== "union" || value.type.kind !== "record") + throw new InternalCompilerError("lowerer bug: record discriminant route shape"); + const helper = lowerer.recordUnionRouteHelper(value.type, dst.unionId, loc); + if (!helper) + throw new InternalCompilerError( + "lowerer bug: planned record discriminant route failed to intern", + ); + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "retag": { if (dst.kind !== "union" || value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: retag lift shape"); @@ -524,7 +535,13 @@ export function recordClassWidthHelper( params: [{ localId: "r.0", name: "r", type: fromT }], returnType: toT, locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }], - body: [{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc }], + body: [ + { + kind: "return", + value: constructedValue(lowerer, info, { kind: "new", className, args, type: toT, loc }), + loc, + }, + ], loc, }); return name; @@ -603,6 +620,10 @@ function buildRecordProjection( const localId = parameter + ".0"; const receiver = varRef(localId, source, loc); const result: IrType = { kind: "record", shapeId: target.id }; + // A record source's copy keeps the source's key order; a class + // instance's fields keep today's table. + const copyOf = (node: IrExpr): IrExpr => + source.kind === "record" ? lowerer.keyOrder.copy(node, source.shapeId) : node; return { name, params: [{ localId, name: parameter, type: source }], @@ -611,7 +632,7 @@ function buildRecordProjection( body: [ { kind: "return", - value: { + value: copyOf({ kind: "recordLit", fields: target.fields.map((field) => { const conversion = plan.get(field.name)!; @@ -661,7 +682,7 @@ function buildRecordProjection( }), type: result, loc, - }, + }), loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 0356b714e..06c0af9c6 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -4,6 +4,7 @@ import { nodeThrowExpr } from "../../../ir/build.js"; import { planUnionRetag, buildUnionRetag, + planRecordUnionLift, planRecordUnionWrap, buildRecordUnionWrap, } from "../union-retag.js"; @@ -284,6 +285,38 @@ export function recordUnionWrapHelper( return name; } +/** Interned `%record.route.(r)`: a record that fits several arms of a + * discriminated union, converted into the arm its discriminant names + * (planRecordUnionLift). The width plan's `discriminantRoute` lift. */ +export function recordUnionRouteHelper( + lowerer: Lowerer, + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, +): string | null { + const shape = lowerer.shapes.get(source.shapeId); + const to = lowerer.unions.get(toId); + if (!shape || !to) return null; + const key = `recordRoute:${source.shapeId}:${toId}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const plan = planRecordUnionLift( + shape, + to, + (id) => lowerer.shapes.get(id), + (from, dst) => lowerer.widthLiftPlan(from, dst), + ); + if (!plan) return null; + const name = `%record.route.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.liftedFns.push( + buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => + lowerer.applyWidthLift(lift, value, dst, loc), + ), + ); + return name; +} + export function unionRetagHelper( lowerer: Lowerer, fromId: string, diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts index 57b75daf5..29b358e99 100644 --- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -13,12 +13,7 @@ export function concreteCollectionNarrow( const symbol = narrowed.getSymbol(); const name = symbol?.name; if (name !== "Map" && name !== "Set") return null; - if ( - !symbol || - !lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) - ) + if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))) return null; const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; diff --git a/packages/compiler/src/frontend/lowering/constructor-returns.ts b/packages/compiler/src/frontend/lowering/constructor-returns.ts new file mode 100644 index 000000000..bee2e7567 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/constructor-returns.ts @@ -0,0 +1,522 @@ +/** Constructors that `return` a value. + * + * JavaScript's `new C()` answers the constructor's returned value when it + * is an object (functions included) and the constructed `this` otherwise. + * A primitive or bare return therefore keeps the ordinary lowering, which + * evaluates the operand and drops it. + * + * A return whose value is an instance of the constructor's own class keeps + * the class representation (`if (Single.instance) return + * Single.instance;`). The return stores that instance in a per-class + * result slot and every direct `new C()` takes it from there. Any other + * object has no representation as a `C` instance and refuses at the return + * statement, deferred to run time in JavaScript like other statement + * fences. A JavaScript value that is an object only at run time (`any`) + * is tested when it returns: a primitive answers the constructed + * instance, an instance of the class takes the result slot, and any other + * object refuses. + * + * `super()` from a subclass refuses: Node would rebind the subclass's + * `this` to the returned instance. In JavaScript the refusal is a run-time + * test after the call: an instance other than the subclass's own `this` + * refuses, and the constructed instance itself (`return this.set(r, g, + * b)`, where `set` answers `this`) continues. */ +import * as ts from "../ts7/adapter.js"; +import { varRef } from "../../ir/build.js"; +import { + BOOL, + DYN, + type IrExpr, + type IrGlobal, + type IrStmt, + type IrType, + STRING, + typeKey, + VOID, +} from "../../ir/ir.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import type { ClassInfo } from "./lower-classes.js"; +import type { Lowerer } from "./lowerer.js"; + +type ReturnKind = "instance" | "object" | "mixed"; + +export interface ConstructorReturnPlan { + /** Returns whose value may be an object, by kind. Primitive and `this` + * returns are absent: they keep the ordinary lowering. */ + returns: Map; + /** The result slot (`C | undefined`), present when some return yields an + * instance of the class itself. */ + slot: IrGlobal | null; +} + +const PRIMITIVE_FLAGS = + ts.TypeFlags.Primitive | ts.TypeFlags.EnumLike | ts.TypeFlags.Void | ts.TypeFlags.Never; + +/** Types whose values may be primitives or objects: open types, type + * variables and intersections (a branded `string & { tag }` is a string). */ +const OPEN_FLAGS = + ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Intersection; + +function unparenthesized(node: ts.Expression): ts.Expression { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + +/** Whether a value of this checker type can be an object: "never", + * "always", or "maybe" (open types and primitive/object unions). */ +function objectness(type: ts.Type): "never" | "always" | "maybe" { + const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; + let primitive = false; + let object = false; + for (const part of parts) { + if ((part.flags & OPEN_FLAGS) !== 0) return "maybe"; + if ((part.flags & PRIMITIVE_FLAGS) !== 0) primitive = true; + else object = true; + } + if (!object) return "never"; + return primitive ? "maybe" : "always"; +} + +/** The constructor's own `return ` statements: nested functions, + * arrows and classes have their own returns. */ +function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] { + const out: ts.ReturnStatement[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node); + node.forEachChild(visit); + }; + ctor.body?.forEachChild(visit); + return out; +} + +/** A return inside a `try` with a `finally` block: the finally block runs + * after the result is stored and may throw past it. */ +function insideFinallyTry(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean { + for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) { + if (ts.isTryStatement(p) && p.finallyBlock !== undefined && !nodeWithin(node, p.finallyBlock)) + return true; + } + return false; +} + +function nodeWithin(node: ts.Node, ancestor: ts.Node): boolean { + return node.pos >= ancestor.pos && node.end <= ancestor.end; +} + +function instanceType(info: ClassInfo): IrType { + return { kind: "object", className: info.def.name }; +} + +function isInstanceOf(lowerer: Lowerer, info: ClassInfo, type: IrType | null): boolean { + if (type === null) return false; + if (type.kind === "object") return type.className === info.def.name; + if (type.kind !== "union") return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return ( + arms.length === 2 && + arms.some((arm) => arm.kind === "object" && arm.className === info.def.name) && + arms.some((arm) => arm.kind === "undefinedT") + ); +} + +/** Whether an expression always yields the receiver itself: `this`, or a + * call `this.m(...)` / `super.m(...)` whose every implementation the + * receiver can reach (the statically resolved method and, for `this` + * calls, each subclass override) returns only `this`, a primitive, or + * nothing (`return this.set(r, g, b)` with a chainable setter). Such a + * return answers the constructed instance. Methods already being checked + * count as returning `this` (their other returns decide). */ +function returnsThis( + lowerer: Lowerer, + info: ClassInfo, + node: ts.Expression, + visiting: Set = new Set(), +): boolean { + let e = node; + while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e)) + e = e.expression; + if (e.kind === ts.SyntaxKind.ThisKeyword) return true; + if (!ts.isCallExpression(e)) return false; + const callee = unparenthesized(e.expression); + if ( + !ts.isPropertyAccessExpression(callee) || + callee.questionDotToken !== undefined || + !ts.isIdentifier(callee.name) + ) + return false; + const receiver = unparenthesized(callee.expression).kind; + if (receiver !== ts.SyntaxKind.ThisKeyword && receiver !== ts.SyntaxKind.SuperKeyword) + return false; + const name = callee.name.text; + const symbol = lowerer.checker.getSymbolAtLocation(callee.name); + if (symbol === undefined) return false; + const methods: ts.Node[] = [...symbol.declarations]; + if (receiver === ts.SyntaxKind.ThisKeyword) { + const pending = [...info.subclasses]; + while (pending.length > 0) { + const sub = pending.pop()!; + pending.push(...sub.subclasses); + for (const member of sub.decl?.members.nodes ?? []) { + if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== name) + continue; + if (!ts.isMethodDeclaration(member)) return false; + methods.push(member); + } + } + } + if (methods.length === 0) return false; + for (const method of methods) { + if (!ts.isMethodDeclaration(method) || method.body === undefined) return false; + if (visiting.has(method)) continue; + visiting.add(method); + for (const ret of ownReturns(method.body)) { + if (ret.expression === undefined) continue; + if (objectness(lowerer.typeOf(ret.expression)) === "never") continue; + if (!returnsThis(lowerer, info, ret.expression, visiting)) return false; + } + } + return true; +} + +/** A function body's own return statements (nested functions, arrows and + * classes have their own). */ +function ownReturns(body: ts.Node): ts.ReturnStatement[] { + const out: ts.ReturnStatement[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node)) out.push(node); + node.forEachChild(visit); + }; + body.forEachChild(visit); + return out; +} + +export function constructorReturnPlan( + lowerer: Lowerer, + info: ClassInfo, +): ConstructorReturnPlan | null { + const byClass = lowerer.constructorReturnPlans; + const known = byClass.get(info.def.name); + if (known !== undefined) return known; + const ctor = info.ctor; + let plan: ConstructorReturnPlan | null = null; + if (ctor?.body !== undefined && info.decl !== null) { + const returns = new Map(); + for (const stmt of ownValueReturns(ctor)) { + const value = unparenthesized(stmt.expression!); + if (returnsThis(lowerer, info, value)) continue; + const type = lowerer.typeOf(value); + const shape = objectness(type); + if (shape === "never") continue; + // Local classes construct through their own environment, which has + // no result slot. + const instance = + info.localClass === undefined && + !insideFinallyTry(stmt, ctor) && + isInstanceOf(lowerer, info, lowerer.mapTypeOf(type)); + returns.set(stmt, instance ? "instance" : shape === "always" ? "object" : "mixed"); + } + if (returns.size > 0) { + let slot: IrGlobal | null = null; + // A JavaScript value typed only at run time may hold an instance of + // the class too (`if (C.instance) return C.instance;`). + const runtimeInstance = + info.localClass === undefined && + isJsSourceFile(ctor.getSourceFile()) && + [...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor)); + if ([...returns.values()].includes("instance") || runtimeInstance) { + slot = { + id: `%g.${info.def.name}.%constructorResult`, + name: `${info.def.name}.%constructorResult`, + type: lowerer.withUndefinedArm(instanceType(info)), + mutable: true, + }; + lowerer.globalsList.push(slot); + } + plan = { returns, slot }; + } + } + byClass.set(info.def.name, plan); + return plan; +} + +function foreignObjectFeature(info: ClassInfo): string { + const display = info.def.name.replace(/^%|^%m\d+\./, ""); + return `returning an object other than an instance of the class from a constructor (Node's 'new ${display}()' answers the returned object, which has no representation as a '${display}' instance)`; +} + +/** `return ` inside a class constructor whose value may be an + * object. Null when the statement keeps the ordinary lowering. */ +export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null { + const info = lowerer.currentClass; + if (info === null || info.ctor === null || stmt.expression === undefined) return null; + let fn: ts.Node | undefined = stmt.parent; + while (fn !== undefined && !ts.isFunctionLike(fn) && !ts.isClassLikeDeclaration(fn)) + fn = fn.parent; + if (fn !== info.ctor) return null; + const plan = constructorReturnPlan(lowerer, info); + const kind = plan?.returns.get(stmt); + if (plan === null || kind === undefined) return null; + const loc = locOf(stmt); + if (kind === "instance") { + const value = lowerer.coerceInto( + stmt.expression, + lowerer.lowerExpr(stmt.expression), + plan.slot!.type, + ); + return { + kind: "block", + body: [ + { kind: "assign", localId: plan.slot!.id, value, loc }, + { kind: "return", value: null, loc }, + ], + loc, + }; + } + const feature = foreignObjectFeature(info); + if (kind === "object" || !isJsSourceFile(stmt.getSourceFile())) + lowerer.unsupported("SC1090", stmt, feature); + // A JavaScript value that is an object only sometimes: a primitive + // still answers the constructed instance, an object refuses. + const value = lowerer.coerceToExpected(lowerer.lowerExpr(stmt.expression), DYN); + if (value.type.kind !== "dyn") lowerer.unsupported("SC1090", stmt, feature); + const held = lowerer.declareHiddenLocal("%returned", DYN); + const read = varRef(held.id, DYN, loc); + const typeOf: IrExpr = { kind: "libCall", fn: "dyn.typeof", args: [read], type: STRING, loc }; + const typeIs = (name: string): IrExpr => ({ + kind: "strEq", + negated: false, + left: typeOf, + right: { kind: "strLit", value: name, type: STRING, loc }, + type: BOOL, + loc, + }); + const diag = unsupportedDiag("SC1090", loc, feature); + lowerer.runtimeFences.push(diag); + const sf = stmt.getSourceFile(); + const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: held.id, init: value, loc }, + { + kind: "if", + // null answers "object" and is falsy; every object is truthy. + cond: { + kind: "logical", + op: "||", + left: typeIs("function"), + right: { + kind: "logical", + op: "&&", + left: typeIs("object"), + right: lowerer.ensureBool(read, stmt.expression), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: [ + ...(plan.slot !== null && !insideFinallyTry(stmt, info.ctor) + ? [ + { + kind: "if" as const, + cond: { + kind: "libCall" as const, + fn: "dyn.typedRefIs" as const, + args: [ + read, + { + kind: "strLit" as const, + value: typeKey(instanceType(info)), + type: STRING, + loc, + }, + ], + type: BOOL, + loc, + }, + then: [ + { + kind: "assign" as const, + localId: plan.slot.id, + value: lowerer.coerceToExpected( + lowerer.coerceToExpected(read, instanceType(info)), + plan.slot.type, + ), + loc, + }, + { kind: "return" as const, value: null, loc }, + ], + else_: null, + loc, + }, + ] + : []), + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: null, loc }, + ], + loc, + }; +} + +/** The value of a construction of exactly class `C`: the constructed + * instance, or the instance the constructor returned. Unchanged for + * classes without a result slot. The slot is read and cleared by a + * per-class helper, so every construction site (direct `new`, class + * values, the checked dynamic dispatchers) shares one protocol. */ +export function constructedValue(lowerer: Lowerer, info: ClassInfo, constructed: IrExpr): IrExpr { + const slot = constructorReturnPlan(lowerer, info)?.slot; + if (slot === null || slot === undefined) return constructed; + const loc = constructed.loc; + const type = instanceType(info); + const name = `%${info.def.name}.%constructed`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const instance = { id: "instance.0", name: "instance", type, mutable: false }; + const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; + lowerer.liftedFns.push({ + name, + params: [{ localId: instance.id, name: instance.name, type }], + returnType: type, + locals: [instance, returned], + body: [ + { kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc }, + { kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc }, + { + kind: "return", + value: { + kind: "orDefault", + left: varRef(returned.id, slot.type, loc), + right: varRef(instance.id, type, loc), + type, + loc, + }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [constructed], type, loc }; +} + +/** A construction through a class value whose static class is `info`: the + * value holds `info` itself or a descendant. Descendants of a class with a + * result slot refuse at their `super()`, so only `info`'s own slot can + * apply; a descendant with its own slot has no static home here. */ +export function constructedFromValue( + lowerer: Lowerer, + info: ClassInfo, + constructed: IrExpr, + blame: ts.Node, +): IrExpr { + const pending = [...info.subclasses]; + while (pending.length > 0) { + const sub = pending.pop()!; + pending.push(...sub.subclasses); + const plan = constructorReturnPlan(lowerer, sub); + if (plan === null || plan.slot === null) continue; + const display = sub.def.name.replace(/^%|^%m\d+\./, ""); + lowerer.unsupported( + "SC1090", + blame, + `constructing through a class value that may hold '${display}', whose constructor can return an existing instance`, + ); + } + return constructedValue(lowerer, info, constructed); +} + +/** `super()` into a class with a result slot, from the constructor of + * class `sub`: `call` runs the base constructor over `self` (the + * subclass's `this`, upcast to the base). Node rebinds the subclass's + * `this` to an instance the base constructor returns, which has no + * representation. A TypeScript subclass refuses. In JavaScript the base + * stores the instance it returns in its slot, and a per-subclass helper + * reads and clears the slot after the call: an instance other than `self` + * refuses at run time. Unchanged for bases without a result slot. */ +export function superIntoResultSlot( + lowerer: Lowerer, + base: ClassInfo, + sub: ClassInfo, + call: IrStmt, + self: IrExpr, + blame: ts.Node, +): IrStmt { + const plan = constructorReturnPlan(lowerer, base); + if (plan === null || plan.slot === null) return call; + const slot = plan.slot; + const display = base.def.name.replace(/^%|^%m\d+\./, ""); + const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`; + if (!isJsSourceFile(blame.getSourceFile())) { + lowerer.unsupported("SC1090", blame, feature); + return call; + } + const loc = locOf(blame); + const type = instanceType(base); + const name = `%${sub.def.name}.%superResult`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const diag = unsupportedDiag("SC1090", loc, feature); + lowerer.runtimeFences.push(diag); + const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1; + const instance = { id: "instance.0", name: "instance", type, mutable: false }; + const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; + lowerer.liftedFns.push({ + name, + params: [{ localId: instance.id, name: instance.name, type }], + returnType: VOID, + locals: [instance, returned], + body: [ + { kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc }, + { kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc }, + { + kind: "if", + cond: { + kind: "bin", + op: "!==", + left: { + kind: "orDefault", + left: varRef(returned.id, slot.type, loc), + right: varRef(instance.id, type, loc), + type, + loc, + }, + right: varRef(instance.id, type, loc), + type: BOOL, + loc, + }, + then: [ + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: null, loc }, + ], + loc, + }); + } + const check: IrStmt = { + kind: "exprStmt", + expr: { kind: "call", callee: name, args: [self], type: VOID, loc }, + loc, + }; + return { kind: "block", body: [call, check], loc }; +} diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 0e14a2e42..c5a41cf32 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -555,9 +555,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice"; @@ -581,13 +581,11 @@ export function lowerBytesMethodCall( const symbol = lowerer.checker.getSymbolAtLocation(access.name); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration.parent) && - declaration.parent.name.text === "Buffer", - ) + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration.parent) && + declaration.parent.name.text === "Buffer", + ) ); })() ) { @@ -647,9 +645,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (receiverIr.elem !== "u8" || !declaredOnBuffer) { @@ -725,9 +723,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (declOnBuffer && receiverIr.elem === "u8") { @@ -1274,16 +1272,14 @@ function knownBufferProducer( const symbol = lowerer.checker.getSymbolAtLocation(node); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - ts.isVariableDeclarationList(decl.parent) && - (decl.parent.flags & ts.NodeFlags.Const) !== 0 && - decl.initializer !== undefined && - knownBufferProducer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + decl.initializer !== undefined && + knownBufferProducer(lowerer, decl.initializer, seen), + ); } if (!ts.isCallExpression(node)) return false; const callee = node.expression; diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 79f970eb1..2331ee25a 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -153,7 +153,7 @@ export function lowerForOfSearchParams( } : singleRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -357,7 +357,7 @@ export function lowerForOfArrayIter( } : keyRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, declT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -650,7 +650,7 @@ function lowerForOfMapOrSet( } : singleRead(); if (ts.isIdentifier(decl!.name)) { - const varTarget = forOfVarTarget(lowerer, decl!); + const varTarget = forOfVarTarget(lowerer, decl!, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index 38df368ac..450e64142 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -810,6 +810,7 @@ export function lowerRecordOvfCaptureHelper( // Every source declared field must flow into the keyed-write slot — // in DECLARATION order (insertion order below; declaredOrder omits // internal '%'-fields, which never enter the copy). + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); @@ -1097,6 +1098,7 @@ export function lowerObjectAssignIndexShape( const from = lowerer.shapes.get(srcIr.shapeId); if (!from || from.tuple) return null; if (from.fields.some((f) => f.name.startsWith("%"))) return null; + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index aeefffe48..46074573d 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -298,7 +298,7 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { if (isStageCall(lowerer, expr)) return true; if (!ts.isIdentifier(expr)) return false; const symbol = lowerer.resolveValueSymbol(expr); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -1800,9 +1800,15 @@ function foldStages( ...inner(value), ]; } else { + // A take before this stage sets `done` as it delivers its last + // value, which still flattens in full: the inner walk runs with the + // flag clear, and keeps it set afterwards. + const afterTake = slots.slice(0, si).some((slot) => slot.kind === "take"); emit = (value) => { const innerT = s.fnT!.ret; const innerId = frame.local("fm", innerT, false); + const heldId = afterTake ? frame.local("held", BOOL, false) : undefined; + const walk = walkInner(lowerer, frame, innerId, innerT, done(), inner, loc); return [ { kind: "varDecl", @@ -1811,7 +1817,26 @@ function foldStages( loc, }, ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), - ...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc), + ...(heldId === undefined + ? walk + : [ + { kind: "varDecl", localId: heldId, init: done(), loc } as IrStmt, + { kind: "assign", localId: doneId, value: boolLit(false, loc), loc } as IrStmt, + ...walk, + { + kind: "assign", + localId: doneId, + value: { + kind: "logical", + op: "||", + left: varRef(heldId, BOOL, loc), + right: done(), + type: BOOL, + loc, + }, + loc, + } as IrStmt, + ]), ]; }; } diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 1e8b9e5a5..fb62fb998 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -41,6 +41,10 @@ export interface CycleInitFlag { * own module's early calls is checked opportunistically: whatever cannot * be checked keeps the unchecked behavior instead of refusing. */ cycle: boolean; + /** A pointer-guarded binding declared without an initializer: its + * declaration initializes it (to undefined, which the pointer cannot + * hold), so the flag alone decides its writes (markUninitializedWrites). */ + uninitialized?: true; /** A class binding: its references are the class's own IR forms and its * static field storage (`global` is the flag itself). */ className?: string; @@ -107,6 +111,30 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void { registerInitFlag(lowerer, binding, global, false); } } + // `let f: T;` with a pointer-guarded slot: once the declaration ran the + // binding is initialized while the slot is still empty, so a write + // checks the flag (where code can run before the declaration) instead of + // the empty pointer. `f = () => f(...)` is the shape. + for (const [id, entry] of lowerer.uninitializedLexicalGlobals) { + const global = lowerer.globalsList.find((g) => g.id === id); + if (global === undefined) continue; + if (!lowerer.cycleInitFlags.has(id)) { + registerInitFlag( + lowerer, + { + symbol: entry.symbol, + name: entry.name, + kind: "lexical", + module: entry.module, + reference: entry.name, + settled: new Set(), + }, + global, + false, + ); + } + lowerer.cycleInitFlags.get(id)!.uninitialized = true; + } } /** A class that its module's early-running code can reach before the class @@ -304,6 +332,22 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void const fileOfInit = new Map(); for (const [sf, name] of lowerer.initNameOf) fileOfInit.set(name, sf); for (const fn of mod.functions) rewriteFunction(lowerer, fn, fileOfInit.get(fn.name)); + markUninitializedWrites(lowerer, mod); +} + +/** Writes to a binding declared without an initializer that run after its + * declaration store into an initialized binding whose slot may still be + * empty: the pointer guard must not refuse them. Writes that can run + * earlier carry the flag check rewriteFunction inserted. */ +function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void { + const ids = new Set(); + for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id); + if (ids.size === 0) return; + const mark = (s: IrStmt): IrStmt => + s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s; + const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt); + const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); + for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); } function rewriteFunction( @@ -444,6 +488,7 @@ function rewriteFunction( kind: "assign", localId: s.localId, value: { kind: "varRef", localId: t, type: s.value.type, loc: s.loc }, + ...(flag.uninitialized ? { initializes: true as const } : {}), loc: s.loc, }; return { diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index 6b0b54b55..e3ca2270d 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -286,7 +286,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(callee); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; if (!symbol || !declaration || !bindingNeverReassigned(lowerer, symbol, declaration)) return false; if (ts.isFunctionDeclaration(declaration)) return true; @@ -346,7 +346,7 @@ function registryInitializer( } if (packageSource && ts.isPropertyAccessExpression(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node.name); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; const owner = ts.isIdentifier(node.expression) ? lowerer.resolveValueSymbol(node.expression) : undefined; @@ -394,7 +394,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(node); - const functionDeclaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const functionDeclaration = symbol && symbol.valueDeclaration; if ( functionDeclaration && ts.isFunctionDeclaration(functionDeclaration) && @@ -449,7 +449,7 @@ function registryOwnerIsData( symbol: ts.Symbol, statement: ts.Statement, ): boolean { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index ceea9a3e3..36fbf48b9 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -23,6 +23,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { isCollationCovered } from "./collation-repertoire.js"; import type { Lowerer } from "./lowerer.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** How an identity-sensitive binding is used after a copy could replace it. */ interface IdentityUse { @@ -179,7 +180,7 @@ export function scanDivergences( if (!candidates.some((fn) => identityUsesOf(fn).some((u) => u !== null))) continue; const signature = safely(() => checker.getResolvedSignature(call)); if (signature === undefined) continue; - const decl = safely(() => checker.signatureDeclaration(signature)); + const decl = safely(() => signature.declaration); if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue; const fn = decl as ts.FunctionLikeDeclaration; const uses = identityUsesOf(fn); @@ -217,6 +218,8 @@ export function scanDivergences( } for (const { decl, source, type } of castDecls) { + // An assertion of the global object keeps that object: no copy. + if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue; const name = (decl.name as ts.Identifier).text; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -232,11 +235,9 @@ export function scanDivergences( const left = assignment.left as ts.Identifier; const symbol = safely(() => checker.getSymbolAtLocation(left)); if (symbol === undefined) continue; - const decl = checker - .declarationsOf(symbol) - .find( - (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, - ); + const decl = symbol.declarations.find( + (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, + ); if (decl === undefined) continue; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -287,7 +288,7 @@ export function scanDivergences( if (ts.isObjectLiteralExpression(inner)) lit = inner; else if (ts.isIdentifier(inner)) { const symbol = safely(() => checker.getSymbolAtLocation(inner)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -373,8 +374,7 @@ export function scanDivergences( const name = calleeName(call); if (name === null || !fnsByName.has(name)) continue; const signature = safely(() => checker.getResolvedSignature(call)); - const decl = - signature === undefined ? undefined : safely(() => checker.signatureDeclaration(signature)); + const decl = signature === undefined ? undefined : safely(() => signature.declaration); const sites = decl === undefined ? undefined : downcasts.get(decl); if (sites === undefined) continue; for (const site of sites) { @@ -423,7 +423,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp if (stringLiteralText(arg) !== null) return [arg]; if (!ts.isIdentifier(arg)) return []; const symbol = safely(() => checker.getSymbolAtLocation(arg)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; const list = decl?.parent; const loop = list?.parent; if ( @@ -438,7 +438,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp let iterated: ts.Expression = loop.expression; if (ts.isIdentifier(iterated)) { const source = safely(() => checker.getSymbolAtLocation(iterated)); - const init = source === undefined ? undefined : checker.valueDeclarationOf(source); + const init = source === undefined ? undefined : source.valueDeclaration; if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined) return []; if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return []; @@ -743,7 +743,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - checker.declarationsOf(symbol).some((d) => !d.getSourceFile().isDeclarationFile) + symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile) ); } @@ -756,7 +756,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean { const symbol = type.getSymbol(); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; // Library-declared object types (Map, Date, Promise, ...) are not records. - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false; return true; } @@ -769,7 +769,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node const symbol = alias ?? type.getSymbol(); if (symbol === undefined) return null; if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null; - const decl = checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl; } @@ -781,7 +781,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { - const present = declared.filter((name) => written.includes(name)); + // A shape whose table the witness pass re-chose (key-order-witness.ts) + // enumerates that table, not the checker's declaration order. + const table = witnessTable(lowerer, contextual); + const native = table ?? declared; + const present = native.filter((name) => written.includes(name) && declared.includes(name)); const writtenKnown = written.filter((name) => declared.includes(name)); if (present.join("\0") !== writtenKnown.join("\0") && writtenKnown.length >= 2) { return { native: present.join(", "), node: writtenKnown.join(", "), detail: "reordered" }; @@ -797,12 +801,21 @@ function keyOrderFinding( const prop = checker.getPropertyOfType(contextual, name); if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) === 0) continue; const nodeOrder = [...writtenKnown, name]; - const nativeOrder = declared.filter((d) => nodeOrder.includes(d)); + const nativeOrder = native.filter((d) => nodeOrder.includes(d)); + if (table !== null && nativeOrder.join("\0") === nodeOrder.join("\0")) continue; return { native: nativeOrder.join(", "), node: nodeOrder.join(", "), detail: "added-later" }; } return null; } +/** The re-chosen enumeration table of the record shape a contextual type + * lowered to, or null when the witness pass kept today's table. */ +function witnessTable(lowerer: Lowerer, contextual: ts.Type): readonly string[] | null { + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind !== "record" || !lowerer.keyOrder.changed.has(mapped.shapeId)) return null; + return lowerer.shapes.get(mapped.shapeId)?.declaredOrder ?? null; +} + function classDeclarationOfTypeNode( lowerer: Lowerer, node: ts.TypeNode, @@ -810,9 +823,7 @@ function classDeclarationOfTypeNode( if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return null; const symbol = lowerer.checker.getSymbolAtLocation(node.typeName); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = lowerer.checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -823,9 +834,7 @@ function classDeclarationOfType( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -850,9 +859,9 @@ function classExtends( (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker - .declarationsOf(resolved) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = resolved.declarations.find( + (d) => ts.isClassDeclaration(d) || ts.isClassExpression(d), + ); cur = decl === undefined ? null : (decl as ts.ClassLikeDeclaration); } return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 216069362..6238479e0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -24,6 +24,7 @@ import { PoisonError, neverTaintedJsType } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js"; import { lowerIndexMergeHelper } from "../containers/indexed-objects.js"; import type { IndexMergeContributor } from "../containers/indexed-objects.js"; +import type { KeyOrderSegment } from "../key-order-witness.js"; import { PERF_ENTRY_MARKER, isGenericCallableMemberType, @@ -1596,6 +1597,8 @@ export function lowerObjectLiteral( // Member names DROPPED from the shape (JS deferral — unlowerable pure // member reads narrow away; see the catch in the property loop). const droppedNames = new Set(); + // The order this literal creates its keys in (key-order-witness.ts). + const orderSegments: KeyOrderSegment[] = []; for (const prop of expr.properties.nodes) { // A later contributor can mutate an earlier source or overwrite a // value with effects. Snapshot pending fields at their source position, @@ -1651,6 +1654,7 @@ export function lowerObjectLiteral( v = lowerer.coerceInto(valueNode, v, fieldType); if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode)); conditionalNames.add(name); + orderSegments.push({ kind: "key", name, definite: false }); fields.push({ name, value: { @@ -1758,6 +1762,7 @@ export function lowerObjectLiteral( } } const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode); + orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true }); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; const targetType = fieldTypes.get(f.name); @@ -1993,6 +1998,12 @@ export function lowerObjectLiteral( laterNames.add(propNameText(lowerer, later.name)); } } + // A checked-dynamic source's keys are a run-time fact. + orderSegments.push( + srcLowered.type.kind === "dyn" + ? { kind: "opaque" } + : { kind: "spread", shapeId: srcType.shapeId, maybeAbsent: false }, + ); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; let targetType = fieldTypes.get(f.name); @@ -2307,6 +2318,7 @@ export function lowerObjectLiteral( const at = fields.findIndex((x) => x.name === name); if (at >= 0) fields.splice(at, 1); fields.push({ name, value }); + orderSegments.push({ kind: "key", name, definite: true }); } // Optional fields may be omitted: the field holds its absent state. // tsc rejects omission of required fields before lowering. @@ -2335,7 +2347,10 @@ export function lowerObjectLiteral( fields.push({ name: f.name, value: absent }); } } - const result: IrExpr = { kind: "recordLit", fields, type, loc }; + const result: IrExpr = lowerer.keyOrder.literal( + { kind: "recordLit", fields, type, loc }, + orderSegments, + ); return spreadStmts.length === 0 ? result : { kind: "seqExpr", stmts: spreadStmts, result, type, loc }; @@ -3047,7 +3062,7 @@ export function lowerShorthandValue( return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName); } const sig = lowerer.fnSigsBySymbol.get(resolved); - const decl = lowerer.checker.declarationsOf(resolved)[0]; + const decl = resolved.declarations[0]; if ( sig && decl && diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index f413f4680..c0e70013c 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -1,3 +1,4 @@ +import { mapAsyncGeneratorAnnotation } from "../type-mapper.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -146,6 +147,12 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + if ( + mappedReturn === null && + decl.asteriskToken && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ) + mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx); if ( isJsSourceFile(decl.getSourceFile()) && decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bf5f2563d..f588faf6d 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -187,9 +187,9 @@ function requireStableGenericBinding( name: string, ): void { const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(symbol) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = symbol.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (!isConst && (redeclared || !bindingNeverReassigned(lowerer, symbol, decl))) { lowerer.unsupported( "SC1090", @@ -266,7 +266,7 @@ export function genericCallInstance( // honest story (monomorphize per implementation signature with the // reconcile bridge, like the non-generic path). { - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; if ( rdecl && (ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) && @@ -451,7 +451,7 @@ function localClassOfResult( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) if (parent === info.decl) return decl; @@ -1549,9 +1549,9 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | const symbol = lowerer.resolveValueSymbol(node.expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) openReturn = true; return undefined; @@ -2041,9 +2041,9 @@ export function implicitLocalFnNodeOf( const sym = lowerer.checker.getSymbolAtLocation(decl.name); if (!sym) return null; const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(sym) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = sym.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (redeclared) return null; if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null; // The capture scan: instances are module functions with no frame. @@ -2059,7 +2059,7 @@ export function implicitLocalFnNodeOf( } if (ts.isIdentifier(n)) { const s = lowerer.checker.getSymbolAtLocation(n); - const d = s ? lowerer.checker.valueDeclarationOf(s) : undefined; + const d = s ? s.valueDeclaration : undefined; if ( d && d.getSourceFile() === decl.getSourceFile() && @@ -2146,7 +2146,7 @@ export function objLitGenericFnNodeOf( fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction; literal: ts.ObjectLiteralExpression; } | null { - const decl = lowerer.checker.valueDeclarationOf(propSym); + const decl = propSym.valueDeclaration; if (!decl) return null; if (ts.isMethodDeclaration(decl) && ts.isObjectLiteralExpression(decl.parent)) { return decl.typeParameters !== undefined && decl.body !== undefined @@ -2420,7 +2420,7 @@ export function requireObjLitGenericReceiver( ); if (!ts.isIdentifier(recv)) fenceReceiver(); const recvSym = lowerer.resolveValueSymbol(recv); - const recvDecl = recvSym ? lowerer.checker.valueDeclarationOf(recvSym) : undefined; + const recvDecl = recvSym ? recvSym.valueDeclaration : undefined; if ( !recvDecl || !ts.isVariableDeclaration(recvDecl) || diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index 8f17a2448..d39e5cceb 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -190,9 +190,9 @@ export function lowerValueReturningConstructorNew( if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = lowerer.checker.getAliasedSymbol(symbol); const decl = symbol - ? lowerer.checker - .declarationsOf(symbol) - .find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node)) + ? symbol.declarations.find( + (node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node), + ) : undefined; if (decl === undefined) return null; const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts new file mode 100644 index 000000000..01f632cf3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -0,0 +1,329 @@ +import { expect, test } from "vitest"; +import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { ShapeRegistry } from "../type-mapper.js"; +import { + applyKeyOrderWitnesses, + KeyOrderNotes, + type KeyOrderSegment, +} from "./key-order-witness.js"; +import type { Lowerer } from "./lowerer.js"; + +const loc = { file: "order.ts", start: 0, end: 1 }; +/** `number | undefined`: the optional-field slot type. */ +const OPT: IrType = { kind: "union", unionId: "u0" }; +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const absent: IrExpr = { kind: "fieldAbsent", unionId: "u0", type: OPT, loc }; + +interface Fixture { + lowerer: Lowerer; + shapes: ShapeRegistry; + notes: KeyOrderNotes; + record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; + literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; + run(body: IrStmt[]): void; + order(type: IrType & { kind: "record" }): string[] | undefined; +} + +function fixture(): Fixture { + const shapes = new ShapeRegistry(); + const notes = new KeyOrderNotes(); + const lowerer = { + shapes, + keyOrder: notes, + unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), + classes: new Map(), + armTag: (unionId: string, arm: IrType): number => + unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + } as unknown as Lowerer; + const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ + kind: "record", + shapeId: shapes.intern( + [...fields].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([name, type]) => ({ name, type })), + false, + undefined, + order, + ), + }); + const literal = (type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr => { + const shape = shapes.get((type as IrType & { kind: "record" }).shapeId)!; + const fields = keys.map((name) => ({ name, value: num(1) })); + for (const f of shape.fields) + if (!keys.includes(f.name)) fields.push({ name: f.name, value: absent }); + return notes.literal( + { kind: "recordLit", fields, type, loc }, + segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), + ); + }; + const run = (body: IrStmt[]): void => { + const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; + applyKeyOrderWitnesses(lowerer, [fn]); + }; + const order = (type: IrType & { kind: "record" }): string[] | undefined => + shapes.get(type.shapeId)?.declaredOrder; + return { lowerer, shapes, notes, record, literal, run, order }; +} + +const observe = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, + loc, +}); +const decl = (localId: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId, init, loc }); +const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); +const set = (obj: IrExpr, field: string): IrStmt => ({ + kind: "recordSet", + obj, + shapeId: (obj.type as IrType & { kind: "record" }).shapeId, + field, + value: num(2), + loc, +}); + +test("a shape takes the one order its literals write", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["b", "a"]); + expect(f.notes.changed.has(ab.shapeId)).toBe(true); +}); + +test("literals that agree with the table leave the same array in place", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const before = f.order(ab); + f.run([observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toBe(before); + expect(f.notes.changed.size).toBe(0); +}); + +test("values built in two orders keep today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an unobserved shape keeps today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("x", f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a synthesized construction pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const synthesized: IrExpr = { + kind: "recordLit", + fields: [ + { name: "a", value: num(1) }, + { name: "b", value: num(2) }, + ], + type: ab, + loc, + }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(synthesized)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a checked conversion pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(parsed)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a spread contributes its source's order, and an override keeps its position", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", f.literal(src, ["b", "a"])), + // { ...s, a: 1, c: 2 } — `a` stays where the spread put it. + observe( + f.literal( + out, + ["a", "b", "c"], + [ + spread, + { kind: "key", name: "a", definite: true }, + { kind: "key", name: "c", definite: true }, + ], + ), + ), + ]); + expect(f.order(out)).toEqual(["b", "a", "c"]); +}); + +test("a spread of a checked-conversion source cannot be ordered statically", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["c", "a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: src, loc }; + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", parsed), + observe(f.literal(out, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }])), + ]); + expect(f.order(out)).toEqual(["c", "a", "b"]); +}); + +test("a late-only optional key follows the keys values are built with", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("o", f.literal(o, ["b"])), set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["b", "a"]); +}); + +test("straight-line writes of late-only keys keep program order", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("o", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "y", "x"]); +}); + +test("late-only keys written through different bindings keep today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("p", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("a late-only key written in a loop over an outer binding keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + { + kind: "while", + cond: { kind: "boolLit", value: false, type: { kind: "bool" }, loc }, + body: [set(ref("o", o), "x")], + loc, + }, + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("re-adding a key a literal creates earlier keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + const del: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "a", + value: absent, + loc, + }; + f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["a", "b"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts new file mode 100644 index 000000000..ba8001792 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -0,0 +1,675 @@ +/* Record enumeration order chosen from the orders values are built in. + * + * Node enumerates an object's string keys as canonical array indices + * ascending, then every other key in creation order. A record stores its + * fields name-sorted and enumerates them through one per-shape table, + * `IrRecordShape.declaredOrder`, which the type mapper fills with the + * checker's property order of the first type interned to the shape. That + * table is exact only while every value of the shape is created in that + * order. This pass replaces it, after lowering, with the order the + * program's construction sites actually create, when that order is one + * order. It changes the table's contents only: storage layout, shape + * identity and every helper's structure stay as they are, so the generated + * code differs only in the constant order the enumeration surfaces walk. + * + * Witnesses, per shape: + * - an object literal contributes the order it writes its keys in. Spread + * segments contribute their source shape's order (an inheritance edge, + * never the desugar's field list), and a key keeps the position of its + * first occurrence, so an override inside a spread stays where the + * spread put it. A key whose earlier occurrence may be absent at run + * time contributes every position it can take; + * - a width copy or a rest binding contributes its source shape's order; + * - any other construction (synthesized builtin results, opaque runtime + * producers) pins today's table; + * - checked conversions from dynamic values (typed JSON.parse, values + * from JavaScript) pin today's table: the record-source table keeps + * their own key order relative to it, and another table would make + * values parsed in today's order keep a source view. Their order is + * unknown, so a copy out of such a shape cannot be ordered statically; + * - a write to an optional key that may be absent appends it in Node. A + * key no construction creates ("late-only") goes after every key a + * construction can create; any other moving write goes after every key. + * Two late-only keys are ordered only by a straight-line sequence of + * writes through one binding. + * + * The orders merge into a precedence graph per shape, topologically sorted + * with ties broken by today's table, so a shape whose witnesses agree with + * the table keeps it untouched. A cycle means the order depends on the + * value: that shape, and every shape copied from it, keeps today's table + * (and today's SC6002 warnings). Shapes that no order surface observes + * keep today's table too. */ +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; +import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; + +/** One property of an object literal, in source order. */ +export type KeyOrderSegment = + /** An explicit key (`definite`), or a conditional spread's key, which + * exists only when its condition holds. */ + | { kind: "key"; name: string; definite: boolean } + /** `...source` of a record shape; `maybeAbsent` when the whole source + * can be undefined (`...options` of `T | undefined`). */ + | { kind: "spread"; shapeId: string; maybeAbsent: boolean } + /** A spread whose keys are only known at run time. */ + | { kind: "opaque" }; + +/** Witness notes the lowering attaches to the IR nodes it builds. Keyed by + * node identity, so nodes a speculative lowering discards are never walked, + * and a node some later pass rebuilds falls back to pinning. */ +export class KeyOrderNotes { + private readonly literals = new Map(); + private readonly copies = new Map(); + /** Shapes a frontend order surface (keys, values/entries, inspect, + * index-signature merges) walks. */ + readonly observed = new Set(); + /** Shapes whose table this pass replaced. */ + readonly changed = new Set(); + + literal(node: IrExpr, segments: KeyOrderSegment[]): IrExpr { + this.literals.set(node, segments); + return node; + } + + copy(node: IrExpr, sourceShapeId: string): IrExpr { + this.copies.set(node, sourceShapeId); + return node; + } + + observe(type: IrType): void { + if (type.kind === "record") this.observed.add(type.shapeId); + } + + literalOf(node: IrExpr): KeyOrderSegment[] | undefined { + return this.literals.get(node); + } + + copyOf(node: IrExpr): string | undefined { + return this.copies.get(node); + } +} + +const isArrayIndex = (name: string): boolean => { + const n = Number(name); + return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === name; +}; + +interface WriteSite { + field: string; + kind: "write" | "delete" | "clone"; + fn: string; + binding: string | null; + loop: string; + pos: number; +} + +interface BindingInit { + count: number; + fn: string; + loop: string; + pos: number; + fresh: boolean; +} + +interface ShapeFacts { + literals: KeyOrderSegment[][]; + copies: { source: string; keys: Set }[]; + /** A construction in today's order (synthesized literal). */ + pinned: boolean; + /** An opaque runtime producer: today's order, any key may be absent. */ + opaque: boolean; + /** A checked conversion from a dynamic value. */ + runtime: boolean; + clones: boolean; + present: Set; + absent: Set; + deleted: Set; + writes: WriteSite[]; +} + +type Resolved = { order: string[] } | "unknown"; + +/** `structuredClone(v)` checked back into v's own type: the copy is built + * from v's dyn view, so it creates exactly v's keys in v's order. */ +function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { + const value = node.value; + if (value.kind !== "libCall" || value.fn !== "dyn.structuredClone") return false; + const source = value.args[0]; + return ( + source !== undefined && source.kind === "dynFrom" && typeEquals(source.value.type, node.type) + ); +} + +/** Chooses each in-scope record shape's enumeration table from its + * witnesses. Runs once retained reachability has settled the historical + * tables and derived metadata, before the order-snapshotting helper bodies + * rebuild. */ +export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunction[]): void { + const notes = lowerer.keyOrder; + const shapes = lowerer.shapes; + const facts = new Map(); + const factsOf = (shapeId: string): ShapeFacts => { + let f = facts.get(shapeId); + if (f === undefined) { + f = { + literals: [], + copies: [], + pinned: false, + opaque: false, + runtime: false, + clones: false, + present: new Set(), + absent: new Set(), + deleted: new Set(), + writes: [], + }; + facts.set(shapeId, f); + } + return f; + }; + const observedRoots: IrType[] = []; + const inits = new Map(); + const undefinedArmed = (t: IrType): boolean => + t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0; + const containsAbsent = (value: IrExpr): boolean => + value.kind === "fieldAbsent" || + (value.kind === "ternary" && (containsAbsent(value.then) || containsAbsent(value.else_))); + + /** Every record shape a type can hold, through fields, arms, elements, + * callables, promises and class fields. */ + /** Each walk shares one `seen` set per result set, so every type graph + * is walked once however many roots reach it. */ + const shapesIn = (type: IrType, out: Set, seen: Set): void => { + const visit = (t: IrType): boolean => { + if (t.kind === "record") { + if (seen.has("r:" + t.shapeId)) return true; + seen.add("r:" + t.shapeId); + out.add(t.shapeId); + const shape = shapes.get(t.shapeId); + if (shape) { + for (const f of shape.fields) visit(f.type); + if (shape.indexValue) visit(shape.indexValue); + } + return true; + } + if (t.kind === "union") { + if (seen.has("u:" + t.unionId)) return true; + seen.add("u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + return true; + } + if (t.kind === "object") { + if (seen.has("c:" + t.className)) return true; + seen.add("c:" + t.className); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + return true; + } + return everyTypeChild(t, visit); + }; + visit(type); + }; + const opaqueShapes = new Set(); + const opaqueSeen = new Set(); + const runtimeShapes = new Set(); + const runtimeSeen = new Set(); + const producedBy = (type: IrType, opaque: boolean): void => { + if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); + else shapesIn(type, runtimeShapes, runtimeSeen); + }; + + // ── collection ────────────────────────────────────────────────────── + let pos = 0; + let fnName = ""; + const loops: number[] = []; + let loopIds = 0; + const loopKey = (): string => loops.join("/"); + const noteInit = (id: string, init: IrExpr): void => { + const previous = inits.get(id); + if (previous !== undefined) { + previous.count++; + return; + } + const fresh = + init.kind === "recordLit" || + init.kind === "recordClone" || + (init.kind === "seqExpr" && init.result.kind === "recordLit"); + inits.set(id, { count: 1, fn: fnName, loop: loopKey(), pos, fresh }); + }; + const visitExpr = (e: IrExpr): void => { + switch (e.kind) { + case "recordLit": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + const presentKeys = new Set(); + for (const field of e.fields) { + if (field.overflow) continue; + if (field.value.kind === "fieldAbsent") { + f.absent.add(field.name); + continue; + } + presentKeys.add(field.name); + f.present.add(field.name); + if (containsAbsent(field.value)) f.absent.add(field.name); + } + const segments = notes.literalOf(e); + const copy = notes.copyOf(e); + if (segments !== undefined) f.literals.push(segments); + else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); + else f.pinned = true; + return; + } + case "recordClone": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + f.clones = true; + for (const o of e.overrides) + f.writes.push({ field: o.name, kind: "clone", fn: fnName, binding: null, loop: "", pos }); + return; + } + case "assignExpr": + noteInit(e.localId, e.value); + return; + case "jsonStringify": + case "dynFrom": + observedRoots.push(e.value.type); + return; + case "recordKeyGet": + observedRoots.push(e.obj.type); + return; + case "libCall": + // Runtime surfaces (console, util.format, workers) that take a + // record may walk its keys; runtime builders make new records. + for (const arg of e.args) observedRoots.push(arg.type); + producedBy(e.type, true); + return; + case "intrinsic": + // Intrinsics hand back values built elsewhere, except allSettled's + // result records; console output renders through the inspect + // helpers, which note their shapes. + if (e.name === "promise.allSettled") producedBy(e.type, true); + return; + case "ffiCall": + case "strIntrinsic": + case "regexIntrinsic": + case "bytesIntrinsic": + producedBy(e.type, true); + return; + case "dynCheck": + if (!orderPreservingClone(e)) producedBy(e.type, false); + return; + case "dynDestrCheck": + case "caughtCheck": + case "caughtNarrow": + producedBy(e.type, false); + return; + default: + return; + } + }; + const visitStmt = (st: IrStmt): void => { + switch (st.kind) { + case "recordSet": + if (st.value.kind === "fieldAbsent") factsOf(st.shapeId).deleted.add(st.field); + factsOf(st.shapeId).writes.push({ + field: st.field, + kind: st.value.kind === "fieldAbsent" ? "delete" : "write", + fn: fnName, + binding: st.obj.kind === "varRef" ? st.obj.localId : null, + loop: loopKey(), + pos, + }); + return; + case "varDecl": + if (st.init !== null) noteInit(st.localId, st.init); + return; + case "assign": + noteInit(st.localId, st.value); + return; + default: + return; + } + }; + const expr = (e: IrExpr): boolean => { + pos++; + visitExpr(e); + return everyExprChild(e, expr, stmt); + }; + const stmt = (st: IrStmt): boolean => { + pos++; + visitStmt(st); + if (st.kind === "while" || st.kind === "doWhile" || st.kind === "for" || st.kind === "forOf") { + loops.push(++loopIds); + everyStmtChild(st, expr, stmt); + loops.pop(); + return true; + } + return everyStmtChild(st, expr, stmt); + }; + for (const fn of fns) { + fnName = fn.name; + for (const st of fn.body) stmt(st); + } + for (const id of opaqueShapes) { + const f = factsOf(id); + f.pinned = true; + f.opaque = true; + } + for (const id of runtimeShapes) factsOf(id).runtime = true; + + // ── scope ─────────────────────────────────────────────────────────── + // Only plain declared-field shapes with a table take part. + const inScope = (shape: IrRecordShape | undefined): shape is IrRecordShape => + shape !== undefined && + !shape.tuple && + shape.indexValue === undefined && + shape.declaredOrder !== undefined && + !shapeHasAccessorSlots(shape); + const tableOf = (shape: IrRecordShape): string[] => + shape.declaredOrder!.filter((k) => !k.startsWith("%") && !isArrayIndex(k)); + // A checked conversion builds values in their source's own order, which + // the record-source table reproduces relative to the shape's table. A new + // table would make every value parsed in today's order keep a source + // view, so such shapes keep today's table. + for (const f of facts.values()) if (f.runtime) f.pinned = true; + + // Nothing to choose: every witness agrees with today's table and no + // optional key is written late. + const interesting = [...facts].some(([id, f]) => { + const shape = shapes.get(id); + if (!inScope(shape)) return false; + if (f.writes.some((w) => w.kind !== "delete") || f.copies.length > 0) return true; + const table = tableOf(shape).join("\0"); + return f.literals.some((segments) => { + const keys: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key") return true; + if (!isArrayIndex(segment.name)) keys.push(segment.name); + } + return keys.join("\0") !== table; + }); + }); + if (!interesting) return; + + // ── presence classes and late-write constraints ───────────────────── + const maybeAbsent = (id: string, key: string): boolean => { + const shape = shapes.get(id); + const field = shape?.fields.find((x) => x.name === key); + if (!field || !undefinedArmed(field.type)) return false; + const f = facts.get(id); + if (f === undefined) return true; + return f.absent.has(key) || f.runtime || f.opaque || f.deleted.has(key); + }; + /** Edges every value of the shape satisfies whatever its sources' + * orders are, or "unknown" when late writes make the order depend on + * the value. */ + const lateEdges = (id: string, keys: readonly string[]): [string, string][] | "unknown" => { + const f = facts.get(id); + if (f === undefined) return []; + const edges: [string, string][] = []; + const keySet = new Set(keys); + const createdKeys = (k: string): boolean => f.present.has(k) || f.runtime || f.opaque; + const required = keys.filter((k) => !maybeAbsent(id, k)); + const created = keys.filter((k) => maybeAbsent(id, k) && createdKeys(k)); + const lateOnly = new Set(keys.filter((k) => maybeAbsent(id, k) && !createdKeys(k))); + // The constraint depends on the written key only, not on the site. + const moving = new Set(); + for (const w of f.writes) + if (w.kind !== "delete" && keySet.has(w.field) && maybeAbsent(id, w.field)) + moving.add(w.field); + for (const field of moving) { + const before = lateOnly.has(field) ? [...required, ...created] : keys; + for (const k of before) if (k !== field) edges.push([k, field]); + } + const lateSites = f.writes.filter((w) => lateOnly.has(w.field)); + const lateWritten = new Set(lateSites.filter((w) => w.kind !== "delete").map((w) => w.field)); + if (lateWritten.size < 2) return edges; + // Two or more late-only keys: each value gains them in the order its + // writes run, so every write must belong to one straight-line + // sequence through one binding initialized once, in the same function + // and loop body as its writes. + const groups = new Map(); + for (const w of lateSites) { + if (w.kind === "clone" || w.binding === null) return "unknown"; + const init = inits.get(w.binding); + if ( + init === undefined || + init.count !== 1 || + init.fn !== w.fn || + init.loop !== w.loop || + init.pos > w.pos + ) + return "unknown"; + const group = groups.get(w.binding) ?? []; + group.push(w); + groups.set(w.binding, group); + } + if (groups.size > 1 && [...groups.keys()].some((b) => !inits.get(b)!.fresh)) return "unknown"; + for (const group of groups.values()) { + const seen = new Set(); + for (const w of group) { + if (seen.has(w.field)) return "unknown"; + seen.add(w.field); + } + const writes = group.filter((w) => w.kind === "write").sort((a, b) => a.pos - b.pos); + for (let i = 0; i + 1 < writes.length; i++) + edges.push([writes[i]!.field, writes[i + 1]!.field]); + } + return edges; + }; + + // ── witness sequences ─────────────────────────────────────────────── + /** The key sequences one literal can create, or null when a spread + * source's order is unknown. A key sits at its first occurrence that + * certainly defines it; each earlier occurrence that may define it is + * another possible position. */ + const literalSequences = ( + segments: readonly KeyOrderSegment[], + keySet: ReadonlySet, + resolved: (id: string) => Resolved, + ): string[][] | null => { + const occurrences: { key: string; definite: boolean }[] = []; + for (const s of segments) { + if (s.kind === "opaque") return null; + if (s.kind === "key") { + if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite }); + continue; + } + const source = resolved(s.shapeId); + if (source === "unknown") return null; + for (const k of source.order) { + if (keySet.has(k)) + occurrences.push({ key: k, definite: !s.maybeAbsent && !maybeAbsent(s.shapeId, k) }); + } + } + // Per key, the occurrences that can create it: every one up to and + // including the first definite occurrence. The first such position is + // the base; each other one is an alternative. + const positions = new Map(); + const settled = new Set(); + occurrences.forEach((o, i) => { + if (settled.has(o.key)) return; + const list = positions.get(o.key) ?? []; + list.push(i); + positions.set(o.key, list); + if (o.definite) settled.add(o.key); + }); + const base = new Map(); + for (const [key, list] of positions) base.set(key, list[0]!); + const sequenceWith = (at: Map): string[] => + [...at].sort((a, b) => a[1] - b[1]).map(([k]) => k); + const out = [sequenceWith(base)]; + for (const [key, list] of positions) { + for (const alternative of list.slice(1)) { + const moved = new Map(base); + moved.set(key, alternative); + out.push(sequenceWith(moved)); + } + } + return out; + }; + + /** Today's table with every witness edge applied, or "unknown". */ + const merge = (table: readonly string[], edges: readonly [string, string][]): Resolved => { + const rank = new Map(table.map((k, i) => [k, i])); + const succ = new Map>(table.map((k) => [k, new Set()])); + const indeg = new Map(table.map((k) => [k, 0])); + for (const [a, b] of edges) { + if (a === b || !rank.has(a) || !rank.has(b)) continue; + const s = succ.get(a)!; + if (s.has(b)) continue; + s.add(b); + indeg.set(b, indeg.get(b)! + 1); + } + const order: string[] = []; + const ready = table.filter((k) => indeg.get(k) === 0); + while (ready.length > 0) { + ready.sort((a, b) => rank.get(a)! - rank.get(b)!); + const k = ready.shift()!; + order.push(k); + for (const next of succ.get(k)!) { + indeg.set(next, indeg.get(next)! - 1); + if (indeg.get(next) === 0) ready.push(next); + } + } + return order.length === table.length ? { order } : "unknown"; + }; + + const chain = (seq: readonly string[], out: [string, string][]): void => { + for (let i = 0; i + 1 < seq.length; i++) out.push([seq[i]!, seq[i + 1]!]); + }; + + // ── resolution over inheritance edges ─────────────────────────────── + const result = new Map(); + const late = new Map(); + const dependencies = (id: string): string[] => { + const f = facts.get(id); + if (f === undefined) return []; + const deps = new Set(); + for (const segments of f.literals) + for (const s of segments) if (s.kind === "spread") deps.add(s.shapeId); + for (const c of f.copies) deps.add(c.source); + return [...deps]; + }; + /** One shape's order given its sources' current orders. */ + const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { + const shape = shapes.get(id); + if (!inScope(shape)) return "unknown"; + const table = tableOf(shape); + const f = facts.get(id); + // A shape the program never constructs natively gets its values from + // somewhere this pass cannot see. + if (f === undefined || !(f.literals.length > 0 || f.copies.length > 0 || f.pinned || f.runtime)) + return "unknown"; + // Clones copy their source's own key order, which a checked + // conversion leaves unknown. + if (f.runtime && f.clones) return "unknown"; + const keySet = new Set(table); + const edges: [string, string][] = []; + if (f.pinned) chain(table, edges); + for (const segments of f.literals) { + const sequences = literalSequences(segments, keySet, current); + if (sequences === null) return "unknown"; + for (const seq of sequences) chain(seq, edges); + } + for (const c of f.copies) { + const source = current(c.source); + if (source === "unknown") return "unknown"; + chain( + source.order.filter((k) => keySet.has(k) && c.keys.has(k)), + edges, + ); + } + const cached = late.get(id); + const lateFor = cached !== undefined ? cached : lateEdges(id, table); + if (cached === undefined) late.set(id, lateFor); + if (lateFor === "unknown") return "unknown"; + for (const edge of lateFor) edges.push(edge); + return merge(table, edges); + }; + /** A source's order as its copies see it: values of a shape built by a + * checked conversion keep their source's own order. */ + const asSource = (id: string, resolved: Resolved): Resolved => + facts.get(id)?.runtime === true ? "unknown" : resolved; + + // Tarjan's SCCs over "depends on" edges: sources finish first. + const index = new Map(); + const low = new Map(); + const stack: string[] = []; + const onStack = new Set(); + let counter = 0; + const sccs: string[][] = []; + const strong = (v: string): void => { + index.set(v, counter); + low.set(v, counter); + counter++; + stack.push(v); + onStack.add(v); + for (const w of dependencies(v)) { + if (!index.has(w)) { + strong(w); + low.set(v, Math.min(low.get(v)!, low.get(w)!)); + } else if (onStack.has(w)) low.set(v, Math.min(low.get(v)!, index.get(w)!)); + } + if (low.get(v) === index.get(v)) { + const scc: string[] = []; + let w: string; + do { + w = stack.pop()!; + onStack.delete(w); + scc.push(w); + } while (w !== v); + sccs.push(scc); + } + }; + for (const id of facts.keys()) if (!index.has(id)) strong(id); + const external = (id: string): Resolved => { + const known = result.get(id); + return known === undefined ? "unknown" : asSource(id, known); + }; + for (const scc of sccs) { + const members = new Set(scc); + const guess = new Map(); + for (const id of scc) { + const shape = shapes.get(id); + guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); + } + const lookup = (id: string): Resolved => + members.has(id) ? asSource(id, guess.get(id)!) : external(id); + let stable = false; + for (let round = 0; round < scc.length + 3 && !stable; round++) { + stable = true; + for (const id of scc) { + const next = resolveOne(id, lookup); + const prev = guess.get(id)!; + const same = + next === "unknown" + ? prev === "unknown" + : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); + if (!same) { + stable = false; + guess.set(id, next); + } + } + } + for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); + } + + // ── observation ───────────────────────────────────────────────────── + const observed = new Set(); + const observedSeen = new Set(); + for (const id of notes.observed) + shapesIn({ kind: "record", shapeId: id }, observed, observedSeen); + for (const root of observedRoots) shapesIn(root, observed, observedSeen); + + // ── apply ─────────────────────────────────────────────────────────── + for (const [id, resolved] of result) { + if (resolved === "unknown" || !observed.has(id)) continue; + const shape = shapes.get(id); + if (!inScope(shape)) continue; + const table = tableOf(shape); + if (resolved.order.join("\0") === table.join("\0")) continue; + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; + notes.changed.add(id); + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 8c49db594..cb31ecc79 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -870,7 +870,7 @@ export function lowerAssertDirectCall( const module = lowerer.builtinNamespaceModuleOf(callee); if (module !== "assert" && module !== "assert/strict") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index c20f32480..f095c5b9c 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -20,7 +20,7 @@ import { VOID, } from "../../ir/ir.js"; import { locOf } from "../program.js"; -import { extractIteratorValue } from "./lower-generators.js"; +import { extractIteratorValue, type GenType } from "./lower-generators.js"; import { streamSidesOf } from "./lower-stream.js"; import { staticReadableStreamElementType } from "./lower-stdlib.js"; import { genResultRecord } from "../type-mapper.js"; @@ -282,6 +282,43 @@ function classAsyncIteratorPlan( }; } +/** `for await (const x of obj)` where obj's class declares (or inherits) + * `async *[Symbol.asyncIterator]()`: the loop opens the iterator with that + * method call, evaluated once, and then drives the async generator it + * returns exactly like `for await (const x of obj[Symbol.asyncIterator]())` + * (pi's EventStream). Null for other sources. */ +export function classAsyncGeneratorIterator( + lowerer: Lowerer, + expression: ts.Expression, +): (IrExpr & { type: GenType }) | null { + const sourceType = lowerer.mapTypeOf(lowerer.typeOf(expression)); + if (sourceType?.kind !== "object") return null; + const info = lowerer.classes.get(sourceType.className); + const open = info ? lowerer.findMethodOn(info, "sym:asyncIterator") : null; + if ( + !open || + open.sig.abstract === true || + open.sig.params.length !== 0 || + open.sig.ret.kind !== "generator" || + !open.sig.ret.async + ) + return null; + const retT = open.sig.ret; + const source = lowerer.lowerExpr(expression); + if (source.type.kind !== "object") return null; + return { + ...lowerer.accessorCall( + sourceType.className, + "sym:asyncIterator", + source, + [], + retT, + locOf(expression), + ), + type: retT, + }; +} + export function checkForAwaitBinding( lowerer: Lowerer, stmt: ts.ForOfStatement, diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index c05430ad3..5e04ee83c 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -132,13 +132,11 @@ export function isNativeBuiltinValueInitializer( const symbol = lowerer.resolveValueSymbol(expr); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), + ); } /** The stored Array.isArray function shares the direct call's native test. */ diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index e0d4493c1..5a4838782 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,5 @@ import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; +import { refuseModuleMemberThis } from "./module-member-this.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { lowerHashStreamCall } from "./builtins/hash-stream.js"; import { lowerResponseStaticJson } from "./lower-stdlib.js"; @@ -150,7 +151,10 @@ import { lowerPerfHooksCall, perfEntryTag, } from "./builtins/performance.js"; -import { lowerProcStreamMethodCall } from "./builtins/process.js"; +import { + lowerProcStreamMethodCall, + lowerProcessBuiltinModuleNamespace, +} from "./builtins/process.js"; import { recordMayKeepSource, throughRecordSource } from "./record-sources.js"; import { lowerReflectCall } from "./builtins/reflection.js"; import { @@ -168,6 +172,7 @@ import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; +import { structuralReadType } from "./class-unions.js"; import { httpClientFnBindingOf, lowerCompatReqStreamOptionalCall, @@ -425,6 +430,15 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { return { kind: "libCall", fn: "http2.streamNoop", args: [], type: VOID, loc }; } + // `process.getBuiltinModule("node:os")` typed as the module namespace, + // including pi's cast-and-probe spelling + // `(process as P).getBuiltinModule?.("node:os")` (the member always + // exists on Node 24's process object). + { + const namespace = lowerProcessBuiltinModuleNamespace(lowerer, expr); + if (namespace) return namespace; + } + // Optional-chain call forms: `f?.()` (the token on the call) and // `a?.m()` (the token on the member access). The handled markers keep // the chain lowering's re-entrant dispatch from looping. @@ -685,12 +699,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { }; // The declared result is the value's own type (the generic's T): // validate the dyn copy back into it when the type can be checked; - // dyn-typed and unmappable results stay dyn values (JS files). + // dyn-typed and unmappable results stay dyn values (JS files). An + // empty record (`{}` inferred from a JavaScript `payload = {}` + // default) is the checker's supertype of whatever arrives, not a + // layout: checking into it would drop every member of the copy. const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const resultShape = + resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined; + const emptyRecord = + resultShape !== undefined && + !resultShape.tuple && + resultShape.indexValue === undefined && + resultShape.fields.length === 0; if ( resultT !== null && resultT.kind !== "dyn" && resultT.kind !== "void" && + !emptyRecord && canDynCheckTo( resultT, (id) => lowerer.shapes.get(id), @@ -1159,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.noteEdge(sig.name); const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; // A bare call supplies undefined as this, even inside a callback // currently carrying a receiver. The value-call ABI saves and // restores that ambient receiver around the function invocation. @@ -1254,6 +1279,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (facadeServed) return facadeServed; const memberSym = lowerer.checker.getSymbolAtLocation(nsMember); if (memberSym) fenceEarlyNsMemberRef(lowerer, expr.expression, memberSym); + refuseModuleMemberThis(lowerer, expr, nsMember); const generic = lowerer.genericFnOf(nsMember); if (generic) return lowerer.lowerGenericCall(expr, generic); const sig = lowerer.fnSigOf(nsMember); @@ -1317,6 +1343,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (!ts.isIdentifier(nameId)) { lowerer.unsupported("SC1090", nameId, "private-named module members"); } + refuseModuleMemberThis(lowerer, expr, nameId); const generic = lowerer.genericFnOf(nameId); if (generic) return lowerer.lowerGenericCall(expr, generic); const sig = lowerer.fnSigOf(nameId); @@ -3537,9 +3564,9 @@ function lowerDynReceiverMethodCall( // Those are not stored members of a checked-dynamic object, so // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only // an authored declaration can justify the own-member path. - const authored = lowerer.checker - .declarationsOf(prop) - .some((decl) => !lowerer.isStdlibFile(decl.getSourceFile())); + const authored = prop.declarations.some( + (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + ); return ( authored && lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(prop)).length > 0 @@ -3640,9 +3667,9 @@ function lowerDynReceiverMethodCall( return true; return ( prop !== undefined && - lowerer.checker - .declarationsOf(prop) - .some((decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) + prop.declarations.some( + (decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl), + ) ); })(); // Protocol methods keep their static signature: instances of the @@ -3677,8 +3704,14 @@ function lowerDynReceiverMethodCall( // own or inherited member exactly like a non-prototype name (chalk's // `chalk.red.bold(...)` reads a prototype getter of a function), so // only a string receiver keeps the refusal, deferred to run time. + // The same holds when only the MEMBER is checker-untyped: TypeScript + // importing chalk's JavaScript source types the default export as a + // plain function, so `chalk.bold` reads as `any` on a typed receiver. const stringOnlyName = - checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); + (checkerUntyped || + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) && + !ownCallable && + DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text); if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null; // Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain // machinery's short-circuit semantics — not modeled here yet. @@ -4297,9 +4330,7 @@ function lowerCaughtPredicateCall( const callee = call.expression; if (!ts.isIdentifier(callee)) return null; const symbol = lowerer.resolveValueSymbol(callee); - const decl = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isFunctionDeclaration) - : undefined; + const decl = symbol ? symbol.declarations.find(ts.isFunctionDeclaration) : undefined; if (!symbol || !decl || !decl.body) return null; if (!decl.type || !ts.isTypePredicateNode(decl.type)) return null; if (decl.parameters.nodes.length !== 1) return null; @@ -4868,7 +4899,7 @@ function implicitReceiverOverloadReturn( if (!member) return null; const signatures = lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(member)); const matches = signatures.filter((signature) => { - const decl = lowerer.checker.signatureDeclaration(signature); + const decl = signature.declaration; if (!decl || !(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) || decl.body) { return false; } @@ -4920,7 +4951,7 @@ function reconcileOverloadReturn( call: IrExpr, ): IrExpr { const rsig = lowerer.checker.getResolvedSignature(expr); - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; // Nominal covariant overrides share the base's return ABI, but retain // their declared subclass result at the call site. Unannotated fluent // JS overrides use the same bridge for their proven receiver result. @@ -6619,6 +6650,7 @@ export function recordKeysArrayCall( loc: SrcLoc, ): IrExpr { const resultT = arrayOf(STRING); + lowerer.keyOrder.observe(argIr); const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -7861,7 +7893,7 @@ function lowerObjectStaticCall( ? lowerer.checker.getSymbolAtLocation(argNode) : undefined; const namespace = symbol - ? lowerer.checker.declarationsOf(symbol).some((decl) => ts.isNamespaceImport(decl)) + ? symbol.declarations.some((decl) => ts.isNamespaceImport(decl)) : false; const names = namespace ? [...UTIL_TYPES_KEYS, "default"].sort() : UTIL_TYPES_KEYS; const loc = locOf(call); @@ -8099,6 +8131,7 @@ function lowerObjectStaticCall( valueT = tupleShape.fields.find((f) => f.name === "1")!.type; } + lowerer.keyOrder.observe(argIr); const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -8504,7 +8537,7 @@ function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExp function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType { const { checker } = lowerer; const signature = checker.getResolvedSignature(call); - const decl = signature ? checker.signatureDeclaration(signature) : undefined; + const decl = signature ? signature.declaration : undefined; const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined; const source = declared && declared.getTypeParameters().length === 0 ? declared : signature; const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null; @@ -8696,7 +8729,7 @@ const SP_BRAND_METHODS = new Set([ function arrowSignatureOf(lowerer: Lowerer, node: ts.Expression): ts.Signature | null { const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); if (signatures.length !== 1) return null; - const declaration = lowerer.checker.signatureDeclaration(signatures[0]!); + const declaration = signatures[0]!.declaration; return declaration !== undefined && ts.isArrowFunction(declaration) ? signatures[0]! : null; } @@ -8949,7 +8982,7 @@ function lowerObjLitGenericMethodCall( const recvT = lowerer.typeOf(access.expression); const propSym = lowerer.checker.getPropertyOfType(recvT, name); if (!propSym) return null; - const propertyDecl = lowerer.checker.valueDeclarationOf(propSym); + const propertyDecl = propSym.valueDeclaration; if ( propertyDecl && ts.isShorthandPropertyAssignment(propertyDecl) && @@ -8973,13 +9006,11 @@ function lowerObjLitGenericMethodCall( // claimed compilable ones; a class that failed collection keeps its // own diagnostics and the generic method-call fence downstream). if ( - lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { return null; } @@ -9046,9 +9077,9 @@ function lowerObjLitGenericMethodCall( // receiver's runtime class must be provable — name that discipline // instead of the object-literal wording when the method lives on an // interface. - const onInterface = lowerer.checker - .declarationsOf(propSym) - .some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent)); + const onInterface = propSym.declarations.some( + (d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent), + ); // A receiver held as a checked-dynamic value (a class-backed interface) // dispatches by name to the runtime object's member, which a generic // method answers with its erased instance; the result converts to this @@ -9102,7 +9133,9 @@ export function lowerObjectMethodCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; - let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let mappedReceiver = + structuralReadType(lowerer, access.expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(access.expression)); // EventEmitter's static side (the class, a value of it, or the events // module, which is the class) keeps its static surface on the emitter // dispatch (`EventEmitter.setMaxListeners(n)`), which tells the statics @@ -9716,6 +9749,44 @@ function unionObjectMethodHelper( if (existing) return existing; const name = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, name); + // A probe that catches a refusal while the body lowers must not leave + // the name registered for a helper that was never emitted. + try { + lowerer.liftedFns.push( + unionObjectMethodFunction( + lowerer, + name, + sourceCall, + receiverT, + method, + plans, + shapes, + resultT, + loc, + ), + ); + } catch (e) { + lowerer.unionCallHelpers.delete(key); + throw e; + } + return name; +} + +function unionObjectMethodFunction( + lowerer: Lowerer, + name: string, + sourceCall: ts.CallExpression, + receiverT: IrType & { kind: "union" }, + method: string, + plans: { + arm: IrType & { kind: "object" }; + info: ClassInfo; + found: NonNullable>; + }[], + shapes: readonly ParamShape[], + resultT: IrType, + loc: SrcLoc, +): IrFunction { const receiver = varRef("this.0", receiverT, loc); const argRefs = shapes.map((shape, i) => varRef(`a.${i}`, shape.type, loc)); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -9790,8 +9861,7 @@ function unionObjectMethodHelper( type: param.type, mutable: false, })); - lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc }); - return name; + return { name, params, returnType: resultT, locals, body, loc }; } /** The argument's lowered type for a message (what the static form saw), diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 770c74840..8a5170a7a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,5 +1,10 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { + constructedFromValue, + constructedValue, + superIntoResultSlot, +} from "./constructor-returns.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { lowerReadlineUiNew } from "./builtins/readline.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; @@ -86,7 +91,7 @@ import { isNodeTypesPath, locOf, } from "../program.js"; -import { PoisonError, dynFallbackType, newFnCtx, own } from "./lowerer.js"; +import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js"; import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; @@ -178,6 +183,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { return valid; } +/** returnsOnlyThis, also accepting a comma expression whose last operand + * is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent + * claim setters). Used only to narrow a call's result back to the + * receiver's class. */ +export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { + const body = member.body; + const last = body?.statements?.nodes.at(-1); + if (!body || !last || !ts.isReturnStatement(last)) return false; + let valid = true; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node)) { + let value = node.expression; + for (;;) { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if ( + value && + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + value = value.right; + continue; + } + break; + } + if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; + } + node.forEachChild(visit); + }; + body.forEachChild(visit); + return valid; +} + function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; @@ -221,14 +259,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { const value = storedClassValueType(lowerer, declaration.initializer, visiting); if (value?.kind === "classval") return value; @@ -243,7 +279,7 @@ export function storedClassValueType( const mapped = lowerer.mapTypeOf(type); if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) { for (const signature of lowerer.checker.getConstructSignatures(type)) { - const constructor = lowerer.checker.signatureDeclaration(signature); + const constructor = signature.declaration; const declaration = constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined; if ( @@ -284,14 +320,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(callee); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { callee = declaration.initializer; continue; @@ -353,7 +387,7 @@ export function storedClassValueType( // Cache the module-level closure so its captures and lifted bodies are // exactly the ones used when the declaration executes. for (const signature of lowerer.checker.getConstructSignatures(type)) { - const declaration = lowerer.checker.signatureDeclaration(signature); + const declaration = signature.declaration; const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null; if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue; let factory: ts.ArrowFunction | ts.FunctionExpression | undefined; @@ -725,7 +759,7 @@ export interface GenericClassInfo { * or the ElementAccessExpression itself). Null when no declaration has * that shape (well-known-symbol members like `[Symbol.iterator]`). */ function lateBoundKeySymOf(lowerer: Lowerer, p: ts.Symbol): ts.Symbol | null { - for (const d of lowerer.checker.declarationsOf(p)) { + for (const d of p.declarations) { const access = ts.isBinaryExpression(d) && ts.isElementAccessExpression(d.left) ? d.left @@ -990,7 +1024,7 @@ export function builtinStreamInfoOf( // member to the stdlib class symbol. The declaration itself is // alias plumbing (streamClassAliasDecl — both declaration walks // skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -1012,7 +1046,7 @@ export function builtinStreamInfoOf( if (rec.lib === symbol.name) irName = name; } if (!irName) return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d)) return false; if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; @@ -1034,15 +1068,13 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ((ts.isConstructorDeclaration(decl.parent) && - !checkedJsDocumentedParameter(lowerer, decl)) || - (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ((ts.isConstructorDeclaration(decl.parent) && + !checkedJsDocumentedParameter(lowerer, decl)) || + (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), + ) ) return DYN; } @@ -1102,7 +1134,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(part.name); if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { const member = ts.isBinaryExpression(decl) ? decl.left : decl; return ( ts.isPropertyAccessExpression(member) && @@ -1244,13 +1276,10 @@ function returnsWidenedJsField( : undefined; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isMethodDeclaration(declaration) && - lowerer.checkedVirtualJsMethods.has(declaration), - ) + symbol.declarations.some( + (declaration) => + ts.isMethodDeclaration(declaration) && lowerer.checkedVirtualJsMethods.has(declaration), + ) ) return true; } @@ -1263,14 +1292,12 @@ function returnsWidenedJsField( const symbol = lowerer.checker.getSymbolAtLocation(expression.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration)) && - lowerer.checkedVirtualJsAccessors.has(declaration), - ) + symbol.declarations.some( + (declaration) => + (ts.isGetAccessorDeclaration(declaration) || + ts.isSetAccessorDeclaration(declaration)) && + lowerer.checkedVirtualJsAccessors.has(declaration), + ) ) return true; } @@ -1303,11 +1330,11 @@ function returnsWidenedJsField( * (genResultRecord's list, including scalar-backed Date values). */ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const t = lowerer.checker.getTypeOfSymbol(p); - const site = lowerer.checker.declarationsOf(p)[0]; + const site = p.declarations[0]; let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null); if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions); if (!mapped || mapped.kind === "void") return null; - for (const declaration of lowerer.checker.declarationsOf(p)) { + for (const declaration of p.declarations) { const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent; if ( assignment && @@ -1354,7 +1381,7 @@ export function builtinEmitterInfoOf( // follow the member off the module namespace. The declaration // itself is alias plumbing (builtinMemberRequireDecl — both // declaration walks skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1370,7 +1397,7 @@ export function builtinEmitterInfoOf( return null; } if (symbol.name !== "EventEmitter") return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; while (node) { @@ -1539,7 +1566,7 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) const baseSymbol = reference && ts.isIdentifier(reference) ? lowerer.resolveValueSymbol(reference) : undefined; const baseDeclaration = baseSymbol - ? lowerer.checker.declarationsOf(baseSymbol).find(ts.isClassDeclaration) + ? baseSymbol.declarations.find(ts.isClassDeclaration) : undefined; if ( baseDeclaration && @@ -1886,8 +1913,7 @@ export function collectClassShapeInner( return true; } const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression)); - const source = - signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined; + const source = signatures.length === 1 ? signatures[0]!.declaration : undefined; if ( !source || !isJsSourceFile(source.getSourceFile()) || @@ -2023,12 +2049,9 @@ export function collectClassShapeInner( // what actually blocks it). const rebinds = memberSym !== undefined && - lowerer.checker - .declarationsOf(memberSym) - .filter( - (d) => - ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, - ).length > 1; + memberSym.declarations.filter( + (d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, + ).length > 1; if (rebinds) { lowerer.unsupported( "SC1090", @@ -4015,14 +4038,12 @@ export function collectClassShapeInner( if (fields.has(p.name) || methods.has(p.name)) continue; if (hasAccessor(p.name)) continue; if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; - const site = lowerer.checker - .declarationsOf(p) - .find( - (d) => - ts.isPropertyAccessExpression(d) || - ts.isBinaryExpression(d) || - ts.isElementAccessExpression(d), - ); + const site = p.declarations.find( + (d) => + ts.isPropertyAccessExpression(d) || + ts.isBinaryExpression(d) || + ts.isElementAccessExpression(d), + ); if (!site) continue; // Late-bound properties: the ones the scan above collected are // real fields under their printable names — skip. The rest keep @@ -4726,8 +4747,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void : null; if (ambientCallee !== null) { const sym = lowerer.resolveValueSymbol(ambientCallee); - const vdecl = - sym && !lowerer.isStdlibSymbol(sym) ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const vdecl = sym && !lowerer.isStdlibSymbol(sym) ? sym.declarations[0] : undefined; const ambientVar = vdecl !== undefined && ts.isVariableDeclaration(vdecl) && @@ -5747,7 +5767,7 @@ export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): Cla // static write would create an own property in JS. The general // class-value write fence answers instead. if (direct) return direct.classDecorators?.valueGlobalId !== undefined ? null : direct; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -5795,7 +5815,7 @@ export function propertyAssignedClassInfoOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const registered = lowerer.classBySymbol.get(resolved); if (registered) return registered; - const decls = lowerer.checker.declarationsOf(resolved); + const decls = resolved.declarations; // The class expression's OWN symbol (tsgo's answer through CJS export // aliases): the declaration is the expression itself. Its top-level // assignment statement must be the binding's ONLY producer. The @@ -6259,7 +6279,7 @@ function capturesEnclosingScope( if (captured) return; if (ts.isIdentifier(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; // The enclosing function's own name is the same binding for every // evaluation. if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true; @@ -6665,7 +6685,7 @@ export function collectInheritedJsFieldWrites( for (const base of lowerer.checker.getBaseTypes(target)) { for (const [name, expressions] of writes) { const property = lowerer.checker.getPropertyOfType(base, name); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if (ts.isMethodDeclaration(declaration) || ts.isAccessor(declaration)) continue; let owner: ts.Node | undefined = declaration.parent; while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) @@ -6724,7 +6744,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of accessors) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( (ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration)) && @@ -6767,7 +6787,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of members) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( ts.isMethodDeclaration(declaration) && isJsSourceFile(declaration.getSourceFile()) && @@ -6877,7 +6897,7 @@ export function exactInstanceClassOf(lowerer: Lowerer, expr: ts.Expression): Cla if (direct) return direct; if (!ts.isIdentifier(e)) return null; const symbol = lowerer.resolveValueSymbol(e); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -6988,19 +7008,43 @@ export function lowerClassGenericMethodCall( } : upcastTo(lowerer, receiver, declarer.def.name); const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); - return { + const result: IrExpr = { kind: "call", callee: instance.name, args: [thisArg, ...args], type: instance.returnType, loc, }; + // A fluent JavaScript method (every return ends in `this`, jose's claim + // setters) answers the receiver: the instance returns the declarer's + // class, a used result's checker type is the receiver's subclass. + if ( + result.type.kind === "object" && + !ts.isExpressionStatement(call.parent) && + ts.isMethodDeclaration(info.decl) && + isJsSourceFile(info.decl.getSourceFile()) && + returnsOnlyThisValue(info.decl) + ) { + return lowerer.maybeNarrow(result, call); + } + return result; } /** Wraps a derived-class expression in an upcast when the target base * class differs (a no-op reinterpret at runtime; keeps IR types exact). */ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr { if (expr.type.kind === "object" && expr.type.className !== className) { + // A value lowered at an ANCESTOR of the class the site needs (a + // fluent `this` result whose lowering kept the method's declaring + // class, the receiver of the next call in a chain): the checker + // vouches for the subclass, so the bridge is the verified downcast, + // never an upcast the validator rejects (an internal error before). + if ( + !lowerer.isSubclassOf(expr.type.className, className) && + lowerer.isSubclassOf(className, expr.type.className) + ) { + return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; + } return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; } return expr; @@ -8303,7 +8347,7 @@ export function superCallStmt( )[0]!; } lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`); - return { + const call: IrStmt = { kind: "exprStmt", expr: { kind: "call", @@ -8314,6 +8358,14 @@ export function superCallStmt( }, loc, }; + return superIntoResultSlot( + lowerer, + base, + info, + call, + lowerer.upcastTo({ ...thisRef }, base.def.name), + info.ctor ?? info.decl!, + ); } /** `super.method(args)`: the base chain's implementation, called @@ -8711,21 +8763,21 @@ function lowerProgramClassNew( : lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr); for (let current: ClassInfo | null = info; current; current = current.base) { if (current.def.baseValueGlobal) - return { + return constructedValue(lowerer, info, { kind: "newValue", callee: classValueRef(lowerer, info, expr), args, type: { kind: "object", className: info.def.name }, loc, - }; + }); } - return { + return constructedValue(lowerer, info, { kind: "new", className: info.def.name, args, type: { kind: "object", className: info.def.name }, loc, - }; + }); } function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { @@ -8767,7 +8819,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { let intlConstructor = expr.expression; if (ts.isIdentifier(intlConstructor)) { const symbol = lowerer.resolveValueSymbol(intlConstructor); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) intlConstructor = declaration.initializer; } @@ -8791,9 +8843,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const symbol = lowerer.resolveValueSymbol(expr.expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); + symbol.declarations.find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) { const last = declaration.body.statements.nodes.at(-1); if ( @@ -9026,13 +9076,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, expr, ); - return { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }; + return constructedFromValue( + lowerer, + info, + { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, + expr, + ); } } } @@ -9659,7 +9714,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const a0 = args[0]!; if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the @@ -9955,13 +10010,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, expr, ); - const built: IrExpr = { - kind: "newValue", - callee, - args, - type: { kind: "object", className: info.def.name }, - loc, - }; + const built = constructedFromValue( + lowerer, + info, + { + kind: "newValue", + callee, + args, + type: { kind: "object", className: info.def.name }, + loc, + }, + expr, + ); // A runtime class slot widened from a union (`[Readable, Writable]`'s // elements hold EventEmitter values): the checker types the result as // the union of the arms' instances, which the checked class view @@ -10028,7 +10088,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ((callee.type.kind === "func" || callee.type.kind === "classval") && lowerer.dynConvertible(callee.type))) ) { - return lowerCheckedConstructor(lowerer, expr, lowerer.coerceToExpected(callee, DYN)); + let boxed = lowerer.coerceToExpected(callee, DYN); + // A plain function's box carries its name: the construction's + // prototype records it as `constructor`, which util.inspect prints + // (`F { a: 2 }`). + if (boxed.kind === "dynFrom" && boxed.fnName === undefined && callee.type.kind === "func") { + const name = jsFuncNameOf(expr.expression, lowerer); + if (name !== null) boxed = { ...boxed, fnName: name }; + } + return lowerCheckedConstructor(lowerer, expr, boxed); } lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index f811d13b6..41c6704c5 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -162,7 +162,7 @@ export function rejectComptimeCaptures( const symbol = ts.isShorthandPropertyAssignment(parent) ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) : lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : undefined; + const decls = symbol ? symbol.declarations : undefined; if (symbol && decls && decls.length > 0) { const ownLocal = decls.every( (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end, diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index 03e22e684..159d72f32 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -73,7 +73,7 @@ export function lowerEnumAccess( if (ts.isPropertyAccessExpression(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl) && effectFreeReceiver(expr.expression)) { return lowerEnumMemberRead(lowerer, expr, decl); } @@ -86,9 +86,9 @@ export function lowerEnumAccess( if (!ts.isIdentifier(recv)) return null; const recvSym = lowerer.resolveValueSymbol(recv); if (!recvSym || !(recvSym.flags & ts.SymbolFlags.Enum)) return null; - const decls = lowerer.checker - .declarationsOf(recvSym) - .filter((d): d is ts.EnumDeclaration => ts.isEnumDeclaration(d)); + const decls = recvSym.declarations.filter((d): d is ts.EnumDeclaration => + ts.isEnumDeclaration(d), + ); if (decls.length === 0) return null; let key = expr.argumentExpression; while (ts.isParenthesizedExpression(key)) key = key.expression; @@ -222,7 +222,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { if (ts.isPropertyAccessExpression(x)) { const sym = lowerer.checker.getSymbolAtLocation(x.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl)) { const v = lowerer.checker.getConstantValue(decl); if (typeof v === "number") return v; diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index b8297fe91..06e9c4ab1 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -420,7 +420,7 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { // binding's type in JavaScript; its source property still names it. if (!ts.isIdentifier(arg)) return false; const symbol = lowerer.resolveValueSymbol(arg); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (!declaration || !ts.isBindingElement(declaration)) return false; const pattern = declaration.parent; const variable = pattern?.parent; @@ -475,7 +475,7 @@ function staticStringPrefixOf( const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -509,7 +509,7 @@ function staticStringSuffixOf( const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 5e4c43021..5ed97d4d7 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -97,7 +97,7 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n // existing member paths stay put. const sym = lowerer.resolveValueSymbol(recv); if (!sym) return null; - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (!decl || decl.getSourceFile().isDeclarationFile) return null; if (isJsSourceFile(decl.getSourceFile())) return null; if (ts.isFunctionDeclaration(decl)) { @@ -202,7 +202,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void const memberName = typeof w.key === "string" ? w.key - : `sym%${w.key.name}%${lowerer.checker.declarationsOf(w.key)[0]?.getStart() ?? 0}`; + : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`; const g: IrGlobal = { id: `%g.${tag}${fnName}%.${memberName}`, name: `${fnName}.${memberName}`, @@ -239,7 +239,7 @@ function nsOwnedMember( const nameNode = ts.isPropertyAccessExpression(access) ? access.name : access.argumentExpression; const sym = lowerer.checker.getSymbolAtLocation(nameNode); if (!sym) return false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { for ( let p: ts.Node | undefined = d.parent; p !== undefined && !ts.isSourceFile(p); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 18f17f390..2a939d748 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6,13 +6,14 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js"; import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; -import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; +import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; @@ -33,6 +34,7 @@ import { literalUnionArm } from "../union-discriminants.js"; * (FieldTarget). */ import { isFsStreamMember, lowerFsStreamProperty } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; +import { builtinFunctionFenceValue } from "./builtin-function-fences.js"; import { isZlibStreamMember, lowerZlibInstanceOf, @@ -68,7 +70,12 @@ import { } from "./maybe-promise.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; -import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; +import { + narrowClassUnion, + narrowStoredClassValue, + structuralNarrowing, + structuralReadType, +} from "./class-unions.js"; import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js"; import { captureContextArguments, declareContextLocal } from "./function-context.js"; import { OBJECT_CALLABLE_VALUES } from "./surfaces.js"; @@ -148,6 +155,7 @@ import { unsupportedDiag, } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; +import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { @@ -215,6 +223,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js"; import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { @@ -345,23 +354,41 @@ export function abstractPropertyDeclOf( ): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - ts.isPropertyDeclaration(d) && - ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, - ); + return sym.declarations.some( + (d) => + ts.isPropertyDeclaration(d) && + ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, + ); } /** A field declared by node:util's parseArgs checked-dynamic type family. * Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias * identity no longer reaches mapType; declaration ancestry is the stable * provenance check. */ +/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */ +const UINT8ARRAY_BASE64_METHODS: ReadonlySet = new Set([ + "toBase64", + "toHex", + "setFromBase64", + "setFromHex", +]); + +/** `Uint8Array.prototype.` read off the global. */ +function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false; + const proto = expr.expression; + return ( + ts.isPropertyAccessExpression(proto) && + !proto.questionDotToken && + proto.name.text === "prototype" && + stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array" + ); +} + function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { @@ -389,7 +416,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression const symbol = lowerer.checker.getSymbolAtLocation(expr.name); return ( !!symbol && - lowerer.checker.declarationsOf(symbol).some((declaration) => { + symbol.declarations.some((declaration) => { if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false; const parent = declaration.parent; if (!ts.isInterfaceDeclaration(parent)) return false; @@ -646,6 +673,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return { kind: "strLit", value: "object", type: STRING, loc }; if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + if (isNavigatorGlobal(lowerer, expr.expression)) + return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); if (performance) return performance; if (isNodeModuleValue(lowerer, expr.expression)) { @@ -708,7 +737,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibMember(tested) ) { const sym = lowerer.checker.getSymbolAtLocation(tested.name); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if ( decls.some(isBuiltinModuleValueDecl) && decls.every((decl) => builtinModuleExportParent(decl) !== undefined) @@ -997,7 +1026,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { recordTextCodecClass(lowerer.shapes.get(local.type.shapeId)!) !== null ) { const symbol = lowerer.resolveValueSymbol(expr); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( decl && ts.isVariableDeclaration(decl) && @@ -1368,6 +1397,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). if (isJsSourceFile(expr.getSourceFile())) { + // A function member is a function value that fences when called. + if (builtinModuleFnOf(lowerer, bi.module, bi.member)) + return builtinFunctionFenceValue(lowerer, bi, expr, loc); return { kind: "strLit", value: `[builtin ${bi.module}.${bi.member}]`, @@ -1581,7 +1613,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // is above); only user-file declares do. { const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1693,6 +1725,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } + if (isNavigatorGlobal(lowerer, expr)) { + refuseNavigator( + lowerer, + expr, + "the navigator object as a value", + lowerer.checker.getSymbolAtLocation(expr), + ); + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. @@ -1956,6 +1996,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + if (!expr.questionDotToken) { + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; + } // `require("./x").member` over a program module's export table: the // module's %init at this position, then the member read exactly like // a `const m = require("./x")` binding's (cjs-inline-require.ts). @@ -2154,6 +2198,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ) { return dynUndefinedExpr(loc); } + // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex + // feature probes libraries such as jose make: the methods are newer + // than Node 24, whose Uint8Array prototype answers undefined for them. + if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc); if (expr.questionDotToken && expr.name.text === "electron") { const processProperty = lowerer.lowerProcessProperty(expr); if (processProperty) return processProperty; @@ -2644,13 +2692,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { (recvSym?.name === "IteratorResult" || recvSym?.name === "IteratorYieldResult" || recvSym?.name === "IteratorReturnResult") && - lowerer.checker - .declarationsOf(recvSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + recvSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) { const recv = lowerer.lowerExpr(expr.expression); if (recv.type.kind === "dyn") @@ -3344,15 +3390,13 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.checker.getSymbolAtLocation(node.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.initializer && - !decl.type && - isJsSourceFile(decl.getSourceFile()), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.initializer && + !decl.type && + isJsSourceFile(decl.getSourceFile()), + ) ) { // Inference describes later writes, not the initial undefined. // A use that requires the inferred type must validate the value. @@ -3420,48 +3464,44 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.resolveValueSymbol(node); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ts.isMethodDeclaration(decl.parent) && - lowerer.checkedVirtualJsMethods.has(decl.parent), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ts.isMethodDeclaration(decl.parent) && + lowerer.checkedVirtualJsMethods.has(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent)) + symbol.declarations.some( + (decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - decl.initializer && - !decl.type && - !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), - ), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + decl.initializer && + !decl.type && + !/@(?:param|type)\b/.test( + decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ), + ) ) return expr; if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { if (!ts.isParameter(decl) || decl.type) return false; const fn = decl.parent; if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return false; @@ -3534,6 +3574,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx narrowed.className !== expr.type.className && lowerer.isSubclassOf(narrowed.className, expr.type.className) ) { + const reduced = structuralNarrowing(lowerer, node, expr.type.className, narrowed); + const union = reduced ? narrowClassUnion(lowerer, expr, reduced) : null; + if (union) return union; if (lowerer.narrowing.untrusted(node, narrowed)) { const checked = checkedClassAssertion(lowerer, expr, narrowed, expr.loc); if (checked) return checked; @@ -7087,6 +7130,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } + if (key.type.kind === "f64") { + const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); + if (safe) return safe; + } if (key.type.kind === "string" || key.type.kind === "f64") { const lit = ts.isStringLiteral(expr.argumentExpression) ? expr.argumentExpression.text @@ -8048,6 +8095,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr } return null; } + // `m[2]` on a JavaScript `RegExpExecArray | null` that a previous + // statement ruled non-null without narrowing it (jose's secs()). + if ( + receiverT?.kind === "union" && + isJsSourceFile(expr.getSourceFile()) && + !expr.questionDotToken && + !hasOptionalChainGuard(expr.expression) + ) { + const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "array") return null; + const value = lowerer.lowerExpr(expr.expression); + const index = lowerer.lowerExpr(expr.argumentExpression); + if (index.type.kind !== "f64") return null; + return lowerOptionalArrayIndexRead(lowerer, expr, value, index); + } if (receiverT?.kind !== "array") return null; const arr = lowerer.lowerExpr(expr.expression); if (arr.type.kind !== "array") return null; @@ -9624,6 +9687,11 @@ export function lowerAsExpression( return inner; } } + // The global object: the checked extraction would build a separate + // value, so the assertion keeps the object and its member operations + // reach it (assertion-identity.ts). + if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) + return inner; if ( target.kind === "void" || !canDynCheckTo( @@ -11826,6 +11894,25 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE } } } + // `&&` over an always-truthy plain left operand (an array, object or + // function value) whose result union has no arm for it: every JS + // object is truthy, so `list && list[0]` evaluates the left for its + // effects and answers the right. Coercing the left into that union + // would trap at run time where Node answers the right operand. + if ( + op === ts.SyntaxKind.AmpersandAmpersandToken && + target?.kind === "union" && + REF_TRUTHY_KINDS.has(left.type.kind) && + lowerer.armTag(target.unionId, left.type) < 0 + ) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: left, loc }], + result: lowerer.coerceInto(expr.right, right, target), + type: target, + loc, + }; + } if (target?.kind === "union") { // `&&` whose left does NOT fit the result union: the checker // built that result by DROPPING left arms which are always @@ -12336,7 +12423,7 @@ function aliasTypeofArmOf( const a = strip(a0); if (!ts.isIdentifier(a)) continue; const aliasSym = lowerer.resolveValueSymbol(a); - const aliasDecl = aliasSym ? lowerer.checker.valueDeclarationOf(aliasSym) : undefined; + const aliasDecl = aliasSym ? aliasSym.valueDeclaration : undefined; if ( !aliasSym || !aliasDecl || @@ -12350,7 +12437,7 @@ function aliasTypeofArmOf( if (!ts.isIdentifier(opnd)) continue; if (!bindingNeverReassigned(lowerer, aliasSym, aliasDecl)) continue; const valSym = lowerer.resolveValueSymbol(opnd); - const valDecl = valSym ? lowerer.checker.valueDeclarationOf(valSym) : undefined; + const valDecl = valSym ? valSym.valueDeclaration : undefined; if (!valSym || !valDecl) continue; // v1: identifier PARAMETERS only — initialized at function entry, // before any alias can capture their typeof. @@ -12632,9 +12719,7 @@ function lowerReplacedExportsRead( * rare enough to keep fenced). */ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.resolveValueSymbol(ident); - const getter = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isGetAccessorDeclaration) - : undefined; + const getter = symbol ? symbol.declarations.find(ts.isGetAccessorDeclaration) : undefined; if (!getter) return null; if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) { return null; @@ -12912,6 +12997,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (blob) return blob; const keyObject = lowerKeyObjectInstanceof(lowerer, expr); if (keyObject) return keyObject; + const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr); + if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; } @@ -14528,7 +14615,7 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null * destructured bindings, and unresolvable names. */ function constInitializerOf(lowerer: Lowerer, ident: ts.Identifier): ts.Expression | null { const sym = lowerer.resolveValueSymbol(ident); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0) @@ -14644,7 +14731,7 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression // The for-of binding over a matchAll drain: `for (const m of // s.matchAll(re))` (or over a stored const rows). const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -15091,13 +15178,11 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). - !lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + !propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { const found = objLitGenericFnNodeOf(lowerer, propSym); if (!found) { @@ -15187,7 +15272,9 @@ export function lowerUnionProperty( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(expr)) return null; - const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); + const receiverIr = + structuralReadType(lowerer, expr.expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (receiverIr?.kind !== "union") return null; // Lower the receiver first: its runtime value can still carry the full // binding union even when the checker has removed several variants. @@ -15394,6 +15481,62 @@ export function lowerUnionProperty( ); } +/** A number-keyed read in JavaScript on an array that may be undefined or + * null outside an optional chain (`m[1]` on a `RegExpExecArray | null` + * that an earlier statement ruled non-null without narrowing it): the + * array arm is an ordinary JavaScript element read, so a hole, a present + * undefined (a regex group that did not participate) or an invalid index + * answers undefined, as lowerSafeIndexRead does for a plain array. The + * unit arms keep the keyed read's answer. The receiver and key evaluate + * once, in source order. Null when the union is not exactly one array arm + * beside unit arms; optional-chain tails and TypeScript keep the keyed + * read. */ +function lowerOptionalArrayIndexRead( + lowerer: Lowerer, + expr: ts.ElementAccessExpression, + value: IrExpr, + key: IrExpr, +): IrExpr | null { + if ( + !isJsSourceFile(expr.getSourceFile()) || + expr.questionDotToken || + hasOptionalChainGuard(expr.expression) + ) + return null; + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms) return null; + const present = arms.filter((arm) => !isUnitType(arm)); + const arm = present[0]; + if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null; + const tag = arms.indexOf(arm); + const loc = locOf(expr); + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string): IrExpr => { + if (e.kind === "varRef" || e.kind === "numLit") return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null; + const recv = stable(value, "%indexReceiver"); + const index = stable(key, "%indexKey"); + const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc }; + const read = lowerSafeIndexRead(lowerer, narrowed, index, loc); + if (read === null) return null; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc }, + then: lowerer.coerceToExpected(read, keyed.type), + else_: keyed, + type: keyed.type, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc }; +} + /** The unionDisc generalization: a keyed read on a union receiver whose * arms answer DIFFERENT (but joinable) types. Each arm contributes its * declared answer — an array element's type (number keys), a declared field's type (literal keys), an @@ -15619,14 +15762,12 @@ function classFieldTarget( ); if ( property && - lowerer.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isPropertyAccessExpression(declaration) || - ts.isBinaryExpression(declaration) || - ts.isElementAccessExpression(declaration), - ) + property.declarations.some( + (declaration) => + ts.isPropertyAccessExpression(declaration) || + ts.isBinaryExpression(declaration) || + ts.isElementAccessExpression(declaration), + ) ) { return { container: "dynamic", @@ -15830,7 +15971,7 @@ export function fieldTarget( // fences: JS finds the prototype function, never an overflow miss. const canonicalized = (): boolean => { if (!nameSym) return false; - const nameDecls = lowerer.checker.declarationsOf(nameSym); + const nameDecls = nameSym.declarations; if (!nameDecls.length) return false; if (!nameDecls.every((d) => ts.isPropertySignature(d))) return false; const declared = lowerer.mapTypeOf(lowerer.typeOf(access)); @@ -15903,7 +16044,7 @@ export function uniqueSymbolKeyOf( // which a runtime-identity symbol value can never satisfy. if (!(t.flags & (ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.ESSymbol))) return null; const sym = lowerer.resolveValueSymbol(key); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl)) return null; if (!(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const)) return null; // Module level only: a unique-symbol const inside a FUNCTION is a @@ -16980,7 +17121,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr const recvType = lowerer.typeOf(access.expression); if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); - const decls = member ? lowerer.checker.declarationsOf(member) : []; + const decls = member ? member.declarations : []; return ( decls.length > 0 && decls.every((d) => { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 7cc448994..0ef601672 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -59,7 +59,7 @@ function declareCallReceiver( export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; return ( !!decl && ts.isFunctionDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 6bf414f29..b1d0a1aa5 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -203,14 +203,12 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression const member = lowerer.checker.getSymbolAtLocation(access.name); return ( member !== undefined && - lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + member.declarations.some( + (d) => + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ); } @@ -268,28 +266,24 @@ export function isSettledIteratorValueRead( const member = lowerer.checker.getSymbolAtLocation(node.name); if ( !member || - !lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isPropertySignature(d) && - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "IteratorYieldResult" || - d.parent.name.text === "IteratorReturnResult") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + !member.declarations.some( + (d) => + ts.isPropertySignature(d) && + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "IteratorYieldResult" || + d.parent.name.text === "IteratorReturnResult") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) return false; const receiver = lowerer.typeOf(node.expression); const stdlib = (sym: ts.Symbol | undefined): boolean => sym !== undefined && - lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ); + sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ); const alias = receiver.getAliasSymbol(); const symbol = receiver.getSymbol(); if ( diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index be7f21c1c..245d1b676 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -10,6 +10,7 @@ import { DYN, F64, type IrExpr, + type IrFunction, type IrStmt, type IrType, RUNTIME_ERROR_CLASSES, @@ -992,6 +993,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // Object.keys' declared order (SEMANTICS.md 36's stance). Retained // reachability can settle this shape after the helper is first // interned, so rebuild the field walk once metadata is final. + lowerer.keyOrder.observe(t); rebuildShapeOrderBody = (): void => { const current = lowerer.shapes.get(t.shapeId) ?? shape; const order = current.declaredOrder ?? current.fields.map((f) => f.name); @@ -1342,7 +1344,10 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { prependCycleGuard(); } - const fn = { + // Typed as the IR function itself: the finalizer below replaces the + // body of the very object liftedFns holds (natively, a differently typed + // literal would be copied into the list). + const fn: IrFunction = { name, params: [ { localId: "v.0", name: "v", type: t }, @@ -1768,7 +1773,7 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo const base = cls.base ? ` extends ${cls.base.def.name.replace(/^%/, "")}` : ""; return strLit(`[class ${cls.def.name}${base}]`, loc); } - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return null; if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 5677b2812..90789b4a0 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -223,7 +223,7 @@ export function mixinFnOfCallee(lowerer: Lowerer, callee: ts.Expression): MixinF let sym = lowerer.checker.getSymbolAtLocation(callee) ?? null; if (sym && sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined) return null; // overloads / merged declarations const d = decls[0]; if (ts.isFunctionDeclaration(d)) return mixinFnShapeOf(lowerer, d); @@ -348,7 +348,7 @@ export function mixinResultBindingClassOf( sym: ts.Symbol | null | undefined, ): ClassInfo | null { if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined || !ts.isVariableDeclaration(decls[0])) return null; const decl = decls[0]; @@ -541,7 +541,7 @@ export function mixinIntersectionInstanceType(lowerer: Lowerer, widened: ts.Type const plainParts: string[] = []; for (const part of parts) { const sym = part.getSymbol(); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; const node = decl && (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) && diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 6edb4c6ff..fa4c98372 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -93,6 +93,7 @@ import { import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; +import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, @@ -145,6 +146,7 @@ import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; import { jsHoldsDynObject } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** One file's declarations, split for collection and init-body lowering. */ export interface FileParts { @@ -652,9 +654,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]); const ownModule = modSym !== undefined && - lowerer.checker - .declarationsOf(modSym) - .some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); + modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); const builtin = canonicalBuiltinModule(spec); const res = ownModule ? ({ kind: "program-module" } as const) @@ -692,7 +692,7 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { const nameSym = lowerer.checker.getSymbolAtLocation(clause.name); if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue; const target = lowerer.checker.getAliasedSymbol(nameSym); - const jsonSf = lowerer.checker.declarationsOf(target)[0]?.getSourceFile(); + const jsonSf = target.declarations[0]?.getSourceFile(); if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue; try { const tsType = lowerer.typeOf(clause.name); @@ -1054,7 +1054,7 @@ function checkedGlobalReference(lowerer: Lowerer, expression: ts.Expression): bo ) return false; const root = lowerer.checker.getSymbolAtLocation(expression.expression); - const namespace = root && lowerer.checker.declarationsOf(root).some(ts.isNamespaceImport); + const namespace = root && root.declarations.some(ts.isNamespaceImport); return ( (namespace || lowerer.cjsLocalModuleBindingOf(expression.expression)) && lowerer.globalOf(expression.name)?.type.kind === "dyn" @@ -1262,7 +1262,7 @@ export function collectGlobals( if (!symbol) continue; if (symbol.flags & ts.SymbolFlags.Alias) { const target = lowerer.checker.getAliasedSymbol(symbol); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -1371,8 +1371,7 @@ export function collectGlobals( if (vG) { if (!vG.mutable) { lowerer.globalsBySymbol.set(symbol, vG); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, vG); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, vG); return; } const g: IrGlobal = { @@ -1382,8 +1381,7 @@ export function collectGlobals( mutable: false, }; lowerer.globalsBySymbol.set(symbol, g); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); return; } @@ -1393,8 +1391,7 @@ export function collectGlobals( lowerer.globalsBySymbol.set(symbol, g); // Importer aliases resolve to a DISTINCT late-bound symbol with // the same declaration — key the node too (globalOf's fallback). - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -1537,7 +1534,7 @@ export function collectGlobals( ) continue; const target = lowerer.checker.getAliasedSymbol(aliasSym); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -1921,7 +1918,8 @@ export function collectGlobals( decl.initializer && !decl.type && !hasJsTypeAnnotation(decl) && - !lowerer.mapTypeOf(lowerer.typeOf(nameNode)) + (!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) || + isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode))) ) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol && !lowerer.globalsBySymbol.has(symbol)) { @@ -1972,6 +1970,30 @@ export function collectGlobals( } continue; } + // A file-scope binding of an assertion of the global object + // (`const state = globalThis as { [KEY]?: State }`): the binding + // holds that object, so writes through it reach the object other + // modules read (assertion-identity.ts). + const asserted = decl.initializer + ? keptIdentityAssertion(lowerer, decl.initializer) + : undefined; + if (nameNode === decl.name && !decl.type && asserted) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${nsPrefix}${nameNode.text}`, + name: nameNode.text, + type: DYN, + mutable: isLet, + source: bindingSource(nameNode), + ...(!isVarDeclared(decl) ? { tdz: true as const } : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + continue; + } // A JS file-scope evolving ARRAY (`const mustCallChecks = [];` // — test/common's exit-accounting ledger): the strict type // (any[]) has no mapping, but the VALUE is the dyn array the @@ -2250,8 +2272,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) ? lowerer.checker.getResolvedSignature(decl.initializer) : undefined; - const callDeclaration = - callSignature && lowerer.checker.signatureDeclaration(callSignature); + const callDeclaration = callSignature && callSignature.declaration; const callType = callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(nameNode)) @@ -2518,6 +2539,11 @@ export function collectGlobals( }; lowerer.globalsBySymbol.set(symbol, g); lowerer.globalsList.push(g); + // `let f: T;` initializes the binding (to undefined) where the + // declaration runs: later writes are ordinary stores, which the + // pointer guard alone would refuse (markCycleEarlyBindings). + if (g.tdz && !decl.initializer) + lowerer.uninitializedLexicalGlobals.set(g.id, { name: nameNode, module: sf, symbol }); // Mutable checked-dynamic LET globals ride the same entry // init: a closure called above the declaration statement // reads the dyn undefined instead of faulting on NULL — the @@ -2879,11 +2905,16 @@ export function lowerFileInit( // a function called above the declaration reads `undefined`, exactly // Node); checked-dynamic var globals hold the dyn undefined the same // way. After the guard: a cache-hit revisit must not reset them. + // A union slot the body retypes checked-dynamic (lowerVarDecl: a + // JavaScript var initialized with an existing checked-dynamic object) + // gets the dyn undefined once the body has lowered. + const unionEntryInits: { at: number; g: IrGlobal }[] = []; for (const g of lowerer.varGlobalEntryInits.get(sf) ?? []) { if (g.type.kind !== "union" && g.type.kind !== "dyn") continue; const wrapped = g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0); if (wrapped) { + if (g.type.kind === "union") unionEntryInits.push({ at: header.length, g }); // A let global whose early reads are checked (initFlag) only // gets its storage filled here: this is not the source write, so // it must neither run the TDZ check nor set the flag. @@ -2978,6 +3009,16 @@ export function lowerFileInit( ); at++; } + for (const { at, g } of unionEntryInits) { + if (g.type.kind !== "dyn") continue; + body[at] = { + kind: "assign", + localId: g.id, + value: dynUndefinedExpr(loc0), + ...(g.initFlag !== undefined ? { initializes: true as const } : {}), + loc: loc0, + }; + } const moduleRef = cjsModuleRef(lowerer, sf); if (moduleRef !== null) { const guardedBody = body.splice(guardId !== undefined ? 2 : 0); diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 7c1d70ed0..23b7d0f57 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -96,7 +96,7 @@ function nsSymbolRuntimeKind( if (!(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; let sawTypeOnly = false; let sawNamespace = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (!ts.isModuleDeclaration(d)) continue; sawNamespace = true; let body = d.body; @@ -189,7 +189,7 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { * null. The walk stops at class/function boundaries: a static member of a * class INSIDE a namespace is the class's business, not the namespace's. */ function nsBlockKindOfSymbol(lowerer: Lowerer, sym: ts.Symbol): "flattened" | "typeOnly" | null { - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { // TYPE declarations don't make a member a namespace VALUE: a class // static `C.B` whose name also carries `namespace C { export // interface B }` is the static's business — value references consult @@ -253,7 +253,7 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile } if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if ( sym.flags & ts.SymbolFlags.ValueModule && ts.isSourceFile(d) && @@ -322,7 +322,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi if (!sym || !(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; if (lowerer.isStdlibSymbol(sym)) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { // A namespace declaration, or the fundule/clazzdule merge partners @@ -427,7 +427,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id if (lowerer.trapBindings.has(sym)) return root; if (lowerer.isStdlibSymbol(sym)) return null; if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null; @@ -608,7 +608,7 @@ export function ambientUndefinedFnSymbolOf( if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; let sawFn = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners if (ts.isFunctionDeclaration(d)) { if (d.body) return null; // an implementation exists — not ambient-undefined @@ -656,7 +656,7 @@ function initExecutingPosition(node: ts.Node): boolean { function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number { let earliest = Infinity; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart()); } return earliest; @@ -669,7 +669,7 @@ function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.Sour export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNode: ts.Node): void { const sym = lowerer.checker.getSymbolAtLocation(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return; - const aliasDecl = lowerer.checker.declarationsOf(sym).find(ts.isImportEqualsDeclaration); + const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration); if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return; if (!initExecutingPosition(refNode)) return; const sf = refNode.getSourceFile(); @@ -766,7 +766,7 @@ export function fenceCrossBlockNsRef( ) { if (lowerer.nsBlocks.has(q)) refBlocks.push(q); } - for (const d of lowerer.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { for ( let q: ts.Node | undefined = d.parent; q !== undefined && !ts.isSourceFile(q); diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 68f11b948..fee25b10f 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -42,7 +42,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ if (!ts.isIdentifier(node) || node.text !== name) return false; if (lowerer.isStdlibGlobal(node, name)) return true; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; const sourceShadow = decls.some( (decl) => !decl.getSourceFile().isDeclarationFile && @@ -204,16 +204,14 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { if ( symbol && (symbol.name === "Module" || symbol.name === "ScriptcModule") && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ) { return true; } const property = lowerer.checker.getPropertyOfType(type, "filename"); const declaredByModule = property && - lowerer.checker.declarationsOf(property).some((decl) => { + property.declarations.some((decl) => { for (let parent: ts.Node | undefined = decl.parent; parent; parent = parent.parent) { if ( (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) && diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index aedd123c6..0e1438fc9 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -5051,7 +5051,7 @@ function clientFnRefOf( * `const` declaration, or a parameter/let whose enclosing scope contains * no assignment or ++/-- targeting it. */ function neverReassigned(lowerer: Lowerer, sym: ts.Symbol): boolean { - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return false; if ( ts.isVariableDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 92d9f0690..2f0e1c66f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -177,15 +177,13 @@ function requestInitConstBacked( if (!symbol || seen.has(symbol)) return false; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return false; seen.add(symbol); - const declarations = lowerer.checker - .declarationsOf(symbol) - .filter( - (declaration): declaration is ts.VariableDeclaration => - ts.isVariableDeclaration(declaration) && - declaration.initializer !== undefined && - ts.isVariableDeclarationList(declaration.parent) && - (declaration.parent.flags & ts.NodeFlags.Const) !== 0, - ); + const declarations = symbol.declarations.filter( + (declaration): declaration is ts.VariableDeclaration => + ts.isVariableDeclaration(declaration) && + declaration.initializer !== undefined && + ts.isVariableDeclarationList(declaration.parent) && + (declaration.parent.flags & ts.NodeFlags.Const) !== 0, + ); return ( declarations.length > 0 && declarations.every((declaration) => @@ -300,7 +298,7 @@ function visitRequestInitConstSelectedValues( seen.add(symbol); let sawDeclaration = false; let defines = true; - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if ( ts.isVariableDeclaration(declaration) && declaration.initializer !== undefined && @@ -413,7 +411,7 @@ function fenceRequestInitProperty( : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), key); if (!symbol || seen.has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isPropertyAssignment(declaration)) { fenceRequestInitValueInner(lowerer, declaration.initializer, seen); } else if (ts.isShorthandPropertyAssignment(declaration) && ts.isIdentifier(declaration.name)) { @@ -533,7 +531,7 @@ function fenceRequestInitValueInner( if (!symbol || seen.has(symbol)) return; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isBindingElement(declaration)) { const source = requestInitConstBindingSource(lowerer, declaration); if (source !== null) { @@ -1618,9 +1616,9 @@ export function lowerStaticAbortControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "AbortController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1672,9 +1670,9 @@ export function lowerStaticReadableStreamControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1780,9 +1778,9 @@ export function lowerStaticAbortSignalListenerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -2127,9 +2125,9 @@ export function lowerStaticReadableStreamReaderCall( const symbol = receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultReader" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -2183,7 +2181,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression /** The checker-resolved program source behind one literal import(). */ function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null { const modSym = lowerer.checker.getSymbolAtLocation(arg); - for (const d of modSym ? lowerer.checker.declarationsOf(modSym) : []) { + for (const d of modSym ? modSym.declarations : []) { if (ts.isSourceFile(d) && !d.isDeclarationFile) return d; } return null; @@ -2543,7 +2541,7 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres * npmPackageOf, also asked directly for import-alias bindings (whose * aliased symbol is the package's export). */ export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return null; if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null; return npmPackageNameOf(decls[0]!.getSourceFile().fileName); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d4bcb8574..2c36e4973 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -19,6 +19,7 @@ import { processDataMemberValue } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; +import { lowerConstructorReturn } from "./constructor-returns.js"; import type { Lowerer } from "./lowerer.js"; import { cjsExportValueByName, @@ -46,7 +47,7 @@ import { lowerYieldStarStatement, type GenType, } from "./lower-generators.js"; -import { lowerForAwaitBuiltin } from "./lower-async-iteration.js"; +import { classAsyncGeneratorIterator, lowerForAwaitBuiltin } from "./lower-async-iteration.js"; import { BOOL, BYTES_U8, @@ -193,10 +194,12 @@ import { import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { + jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, lowerStrictEquality, } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerUnionFieldWrite } from "./expressions/union-fields.js"; @@ -348,8 +351,7 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit); const sym = t.getAliasSymbol() ?? t.getSymbol(); return ( - sym?.name === name && - lowerer.checker.declarationsOf(sym).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); }; return visit(lowerer.typeOf(node)); @@ -912,7 +914,7 @@ const TDZ_KINDS = new Set([ * is empty. Mutable bindings also guard writes; only the declaration's * initializing assignment may leave the temporal dead zone. */ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): boolean { - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -992,7 +994,7 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b * lower the declaration under the owning statement list's context, then * resume the caller and thread its captures normally. */ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): boolean { - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters?.nodes) return false; for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { @@ -1069,13 +1071,124 @@ function hostVariableDeclarationOf(name: ts.Identifier): ts.VariableDeclaration export function forOfVarTarget( lowerer: Lowerer, decl: ts.VariableDeclaration, + valueType?: IrType, ): { id: string; type: IrType } | null { if (!ts.isIdentifier(decl.name) || !isVarDeclared(decl)) return null; const symbol = lowerer.checker.getSymbolAtLocation(decl.name); if (!symbol) return null; const g = lowerer.globalsBySymbol.get(symbol); - if (g) return g; - return hoistVarBinding(lowerer, symbol, decl.name); + if (g) { + if (valueType?.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, decl.name, g.type)) + g.type = DYN; + return g; + } + return hoistVarBinding(lowerer, symbol, decl.name, valueType); +} + +/** JavaScript: whether an unannotated variable binding whose static type + * is `type` keeps a checked-dynamic value unconverted. The checker types + * such a binding by the object it receives (an element of an evolving + * `[]`, a `find` result, a member of a checked-dynamic object), but the + * value is an existing checked-dynamic object or array: a record- or + * array-typed slot would hold a converted copy, so writes through the + * binding would miss the object other references see, and `===` against + * them would fail. Block-scoped bindings with an identifier name keep such + * values by the same rule in lowerVarDecl (preservesObjectIdentity). */ +function jsBindingKeepsCheckedObject( + lowerer: Lowerer, + nameNode: ts.Identifier, + type: IrType, +): boolean { + if (!isJsSourceFile(nameNode.getSourceFile())) return false; + const host = hostVariableDeclarationOf(nameNode); + if (!host || host.type || hasJsTypeAnnotation(host)) return false; + const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type]; + return ( + arms.some((arm) => arm.kind === "record" || arm.kind === "array") && + arms.every((arm) => arm.kind === "record" || arm.kind === "array" || isUnitType(arm)) + ); +} + +/** Whether `node` mentions the binding `symbol` anywhere, nested functions + * included. */ +function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { + if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol; + return ( + node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) === + true + ); +} + +/** Whether `node` evaluates to an existing value rather than constructing + * one whose layout a destination directs: not a literal, `new`, function + * or class expression, nor a conditional with such an arm (an arm that + * is `null` or `undefined` constructs nothing). */ +function existingValueForm(node: ts.Expression, armOfConditional = false): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isConditionalExpression(node)) + return existingValueForm(node.whenTrue, true) && existingValueForm(node.whenFalse, true); + if ( + armOfConditional && + (node.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(node) && node.text === "undefined")) + ) + return true; + return !( + ts.isObjectLiteralExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isNewExpression(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isClassExpression(node) || + ts.isLiteralExpression(node) || + node.kind === ts.SyntaxKind.NullKeyword + ); +} + +/** JavaScript `var x = e` whose binding could hold an existing + * checked-dynamic object (jsBindingKeepsCheckedObject): lowers `e` BEFORE + * minting the hoisted slot, as a block-scoped declaration does, so a + * checked-dynamic value gets a checked-dynamic slot instead of a converted + * copy. Other values coerce into the planned slot exactly as + * lowerExprExpecting would. Null when the declaration does not qualify: + * the slot already exists, the initializer is fresh construction whose + * layout the slot type directs (existingValueForm; a literal holding an + * existing checked-dynamic object qualifies), or it refers to the binding + * itself (hoist-before-init resolves that reference). */ +function lowerJsVarFromExisting( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + symbol: ts.Symbol, +): IrStmt | null { + if (!ts.isIdentifier(decl.name) || !decl.initializer) return null; + if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null; + if (lowerer.bindingIn(lowerer.ctx, symbol)) return null; + let node: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) + node = node.expression; + // A literal holding an existing checked-dynamic object builds as a + // checked-dynamic literal without a destination (lowerArrayLiteral, + // lowerObjectLiteral): a record- or array-typed slot would copy it. + const holdsChecked = + (ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) && + jsHoldsDynObject(lowerer, node); + if (!holdsChecked && !existingValueForm(node)) return null; + const planned = varBindingType(lowerer, decl.name); + if (!planned || !jsBindingKeepsCheckedObject(lowerer, decl.name, planned)) return null; + if (mentionsSymbol(lowerer, node, symbol)) return null; + const value = lowerer.lowerExpr(node); + const local = hoistVarBinding( + lowerer, + symbol, + decl.name, + value.type.kind === "dyn" ? DYN : undefined, + ); + return { + kind: "assign", + localId: local.id, + value: lowerer.coerceExistingInto(node, value, local.type), + loc: locOf(decl), + }; } /** The `var` symbol's function-scoped binding TYPE — the declared type at @@ -1131,7 +1244,12 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul * a loop re-ASSIGNS the one binding per pass and never resets it — the * classic capture semantics fall out: closures made in a loop share the * single boxed binding, where `let` gets a fresh box per iteration. */ -function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Identifier): IrLocal { +function hoistVarBinding( + lowerer: Lowerer, + symbol: ts.Symbol, + nameNode: ts.Identifier, + valueType?: IrType, +): IrLocal { const existing = lowerer.ctx.hoistedVars.get(symbol); if (existing) return existing; // The parameter merge: the symbol already binds a function-root local. @@ -1140,7 +1258,15 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident lowerer.ctx.hoistedVars.set(symbol, bound); return bound; } - const type = varBindingType(lowerer, nameNode); + let type = varBindingType(lowerer, nameNode); + // The first value stored is an existing checked-dynamic object: keep it + // (jsBindingKeepsCheckedObject). A checked-dynamic slot holds undefined + // natively, so no runtime-optional widening applies. + const keepsChecked = + valueType?.kind === "dyn" && + type !== null && + jsBindingKeepsCheckedObject(lowerer, nameNode, type); + if (keepsChecked) type = DYN; if (!type) lowerer.badType(nameNode, lowerer.typeOf(nameNode)); const root = lowerer.activeStmtLists.find((e) => e.ctx === lowerer.ctx); if (!root) @@ -1157,7 +1283,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident }; lowerer.ctx.locals.push(local); lowerer.ctx.scopes[0]!.set(symbol, local); - noteEarlyReadVar(lowerer, symbol, local); + if (!keepsChecked) noteEarlyReadVar(lowerer, symbol, local); // A checked-dynamic slot holds the dyn undefined (a NULL dyn is a // trap); everything else rides unassignedSlotInit (interned union arm, @@ -1186,7 +1312,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident export function predeclareForwardVar(lowerer: Lowerer, symbol: ts.Symbol): boolean { // Module-scope vars already have their global storage. if (lowerer.globalsBySymbol.has(symbol)) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isVariableDeclaration(decl) || !isVarDeclared(decl)) return false; const nameNode = ts.isIdentifier(decl.name) ? decl.name : null; if (!nameNode) { @@ -1452,6 +1578,11 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt // incompatible boundary keeps the ordinary per-statement refusal. let value: IrExpr | null = null; if (stmt.expression) { + // A constructor's returned object replaces the constructed `this`. + if (lowerer.currentClass?.ctor) { + const constructorReturn = lowerConstructorReturn(lowerer, stmt); + if (constructorReturn !== null) return constructorReturn; + } return lowerer.lowerReturnStmt(stmt.expression, locOf(stmt)); } else if (lowerer.ctx.returnType.kind === "union") { value = lowerer.wrappedUndefined(lowerer.ctx.returnType, locOf(stmt)); @@ -1927,11 +2058,7 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool return false; }; const sym = widened.getAliasSymbol() ?? widened.getSymbol(); - if ( - sym && - isParseArgsDynTypeName(sym.name) && - lowerer.checker.declarationsOf(sym).some(declarationBelongs) - ) { + if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) { return true; } // A discriminant check erases the token alias and leaves one anonymous @@ -1939,7 +2066,44 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool // is the same stable signal the dotted-member bridge uses. return lowerer.checker .getPropertiesOfType(widened) - .some((p) => lowerer.checker.declarationsOf(p).some(declarationBelongs)); + .some((p) => p.declarations.some(declarationBelongs)); +} + +/** True when an `any` value comes from a JavaScript function's result: + * a call (possibly awaited) whose callee is declared in a JavaScript file, + * or a const bound to one. A TypeScript program importing a JavaScript + * package sees the package's inferred types (`jwtVerify` from jose + * answers `any`), and its object destructuring reads the members of that + * checked-dynamic value as JavaScript does — member reads on the same + * value already compile. Other `any` sources keep the fence. */ +function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; + if (ts.isCallExpression(expr)) { + const callee = ts.isPropertyAccessExpression(expr.expression) + ? expr.expression.name + : expr.expression; + if (!ts.isIdentifier(callee)) return false; + const symbol = lowerer.resolveValueSymbol(callee); + const declarations = symbol ? symbol.declarations : []; + return ( + declarations.length > 0 && + declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) + ); + } + if (ts.isIdentifier(expr) && depth === 0) { + const symbol = lowerer.resolveValueSymbol(expr); + const declaration = symbol ? symbol.valueDeclaration : undefined; + return ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.initializer !== undefined && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && + javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) + ); + } + return false; } /** Destructuring declarations, desugared as sugar over indexed/field @@ -1986,7 +2150,12 @@ export function lowerDestructuringDecl( lowerer.anyOrigin(decl.initializer) && !isJsSourceFile(decl.getSourceFile()) && init.type.kind !== "record" && - init.type.kind !== "array" + init.type.kind !== "array" && + !( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + javaScriptAnyOrigin(lowerer, decl.initializer, 0) + ) ) { lowerer.anyOpFence("destructuring", decl); } @@ -2908,6 +3077,10 @@ export function lowerBindingPattern( lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + if (srcType.kind === "array" && isArrayPositionPattern(lowerer, pattern)) { + lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3238,19 +3411,8 @@ function lowerMatchResultObjectPattern( ); } let value: IrExpr; - if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) { - const index = Number(propName); - const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; - value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { - kind: "arrayGet", - arr: srcRef(), - index: position, - type: srcType.elem, - loc, - }; - if (el.initializer) { - value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out); - } + if (ARRAY_POSITION_KEY.test(propName)) { + value = arrayPositionBinding(lowerer, el, srcRef, srcType, Number(propName), out); } else if (propName === "index" || propName === "input") { if (el.initializer) { lowerer.unsupported( @@ -3296,6 +3458,83 @@ function lowerMatchResultObjectPattern( } } +/** A canonical array index key (the numeric positions an object pattern + * reads off an array). */ +const ARRAY_POSITION_KEY = /^(?:0|[1-9]\d{0,8})$/; + +/** The positional read `{ : x }` takes off an array: the element, + * undefined past the end, with the binding default applied. */ +function arrayPositionBinding( + lowerer: Lowerer, + el: ts.BindingElement, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + index: number, + out: IrStmt[], +): IrExpr { + const loc = locOf(el); + const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; + const value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { + kind: "arrayGet", + arr: srcRef(), + index: position, + type: srcType.elem, + loc, + }; + return el.initializer + ? arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out) + : value; +} + +/** Whether every element of an object pattern over an array names a + * numeric position or `length` (`const { 0: a, 1: b, length } = + * s.split(".")`), with no rest element. */ +function isArrayPositionPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { + return pattern.elements.nodes.every((el) => { + if (el.name === undefined) return true; + if (el.dotDotDotToken) return false; + const prop = el.propertyName ?? el.name; + if ( + !ts.isIdentifier(prop) && + !ts.isComputedPropertyName(prop) && + !ts.isStringLiteralLike(prop) && + !ts.isNumericLiteral(prop) + ) + return false; + const name = patternKeyNameOf(lowerer, prop as ts.PropertyName); + return name !== null && (name === "length" || ARRAY_POSITION_KEY.test(name)); + }); +} + +/** An object pattern of positions and `length` over an array: each + * position is the read the equivalent array pattern makes, and `length` + * the array's length (always present, so its default never runs). */ +function lowerArrayPositionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + isLet: boolean, + out: IrStmt[], +): void { + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + const name = patternKeyNameOf(lowerer, (el.propertyName ?? el.name) as ts.PropertyName)!; + const value: IrExpr = + name === "length" + ? { + kind: "arrIntrinsic", + method: "length", + receiver: srcRef(), + args: [], + type: F64, + loc: locOf(el), + } + : arrayPositionBinding(lowerer, el, srcRef, srcType, Number(name), out); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Decode one non-rest object-assignment property into its static source * field, destination expression, and optional default initializer. */ function destructuringPropertyOf( @@ -4016,7 +4255,7 @@ function objectRestValue( ), }; }); - return { kind: "recordLit", fields, type: restT, loc }; + return lowerer.keyOrder.copy({ kind: "recordLit", fields, type: restT, loc }, srcType.shapeId); } /** The fresh record for `{ a, ...rest }` over a CLASS-INSTANCE source: @@ -4199,6 +4438,8 @@ export function bindPatternTarget( const symbol = lowerer.checker.getSymbolAtLocation(name); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (g) { + if (value.type.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, name, g.type)) + g.type = DYN; out.push({ kind: "assign", localId: g.id, @@ -4215,7 +4456,7 @@ export function bindPatternTarget( // here by declareParams) carry no block-scope flag either, but their // bindings are the parameters' own. if (symbol && hostVariableDeclarationOf(name) !== null && isVarDeclared(name)) { - const hoisted = hoistVarBinding(lowerer, symbol, name); + const hoisted = hoistVarBinding(lowerer, symbol, name, value.type); out.push({ kind: "assign", localId: hoisted.id, @@ -4230,7 +4471,7 @@ export function bindPatternTarget( value.type.kind === "dyn" && (() => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(name)); - return mapped === null; + return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped); })(); let type = dynStays ? DYN @@ -4714,7 +4955,11 @@ export function lowerVarDecl( !decl.type && !hasJsTypeAnnotation(decl) && raw.type.kind === "dyn" && - (g.type.kind === "record" || g.type.kind === "object") + (g.type.kind === "record" || + g.type.kind === "object" || + (ts.isIdentifier(decl.name) && + jsBindingKeepsCheckedObject(lowerer, decl.name, g.type) && + (existingValueForm(decl.initializer) || jsHoldsDynObject(lowerer, decl.initializer)))) ) g.type = DYN; g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type); @@ -4761,6 +5006,8 @@ export function lowerVarDecl( // types already hold the interned undefined arm), and a declaration // re-run in a loop must not reset the persisting value. if (isVarDeclared(decl) && declSymbol) { + const fromExisting = lowerJsVarFromExisting(lowerer, decl, declSymbol); + if (fromExisting) return fromExisting; const local = hoistVarBinding(lowerer, declSymbol, decl.name); if (!decl.initializer) return null; const init = lowerer.lowerExprExpecting(decl.initializer, local.type); @@ -4921,9 +5168,16 @@ export function lowerVarDecl( lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType))))) && !decl.type && !hasJsTypeAnnotation(decl); + // The binding of an assertion of the global object holds that object, + // whatever its asserted type (assertion-identity.ts). + const keepsAssertedObject = + init.type.kind === "dyn" && + !decl.type && + keptIdentityAssertion(lowerer, decl.initializer) !== undefined; if ( expandsObject || preservesObjectIdentity || + keepsAssertedObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) @@ -5136,7 +5390,7 @@ export function lowerVarDecl( ? npmStaticOptionalSelfWriteType(lowerer, decl, settledType) : null; settledType = - expandsObject || preservesObjectIdentity + expandsObject || preservesObjectIdentity || keepsAssertedObject ? DYN : lowerer.runtimeOptionalBindingType(decl.name, settledType); if (optionalSelfWriteType !== null) settledType = optionalSelfWriteType; @@ -5980,6 +6234,28 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt }; } let obj = lowerer.lowerExpr(target.expression); + // `delete this.stack` in an Error subclass that declares a `stack` + // accessor (quickjs-wasi's JSException): V8 gives each error an own + // `stack` that shadows the accessor, and the delete removes it so reads + // reach the accessor. Compiled errors keep the trace internally and + // class dispatch already reads the accessor, so only the receiver's + // evaluation remains. + if ( + obj.type.kind === "object" && + ts.isPropertyAccessExpression(target) && + target.name.text === "stack" + ) { + let accessor = false; + let errorBase = false; + for (let c = lowerer.classes.get(obj.type.className); c; c = c.base ?? undefined) { + if (c.builtinError) { + errorBase = true; + break; + } + if (c.methods.has("get:stack")) accessor = true; + } + if (accessor && errorBase) return { kind: "exprStmt", expr: obj, loc }; + } if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") { const info = lowerer.classes.get(obj.type.className); const keyType = ts.isElementAccessExpression(target) @@ -6404,10 +6680,7 @@ function lowerCjsSingleValueExport( // own statement already evaluated it), and a builtin global // (`module.exports = TypeError`) is the global itself: requirer // bindings alias straight through to it, so nothing is stored. - if ( - lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || - lowerer.isStdlibSymbol(sym) - ) { + if (sym.declarations.some((d) => ts.isSourceFile(d)) || lowerer.isStdlibSymbol(sym)) { return { kind: "block", body: [], loc }; } const g = lowerer.globalsBySymbol.get(sym); @@ -6526,10 +6799,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // positions keep the first-class-namespace fences. if (ts.isIdentifier(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr); - if ( - sym !== undefined && - lowerer.checker.declarationsOf(sym).some((d) => ts.isNamespaceImport(d)) - ) { + if (sym !== undefined && sym.declarations.some((d) => ts.isNamespaceImport(d))) { return { kind: "block", body: [], loc: locOf(expr) }; } } @@ -7019,6 +7289,32 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm loc, }; } + // process.emitWarning = fn: an ordinary writable property. Later + // process.emitWarning(...) calls, and the warnings Node's own + // modules emit through process.emitWarning, call the replacement + // instead (silencing them when it does nothing). + if (flag === "emitWarning") { + const loc = locOf(expr); + let rhs: ts.Expression = expr.right; + while ( + ts.isParenthesizedExpression(rhs) || + ts.isAsExpression(rhs) || + ts.isSatisfiesExpression(rhs) + ) + rhs = rhs.expression; + const value = lowerer.lowerExprExpecting(rhs, DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "process.setEmitWarning", + args: [value], + type: VOID, + loc, + }, + loc, + }; + } // process.title = name: libuv's uv_set_process_title (the // runtime truncates to the original argument area on POSIX). if (flag === "title") { @@ -7305,6 +7601,15 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (droppedPureStdlibStatement(lowerer, expr)) { return { kind: "block", body: [], loc: locOf(expr) }; } + if ( + stmtNode !== undefined && + ts.isExpressionStatement(stmtNode) && + ts.isBinaryExpression(expr) && + (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) { + return lowerDiscardedLogical(lowerer, expr); + } const value = lowerer.lowerExpr(expr); // A bare unit-literal statement (`undefined;`): a pure no-op in JS, // and the IR has no bare-unit VALUE outside a union wrap — drop it @@ -7313,6 +7618,59 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm return { kind: "exprStmt", expr: value, loc: locOf(expr) }; } +/** A discarded `a && b` / `a || b` statement (the minifier's `if`). The + * value form lowers first, exactly as before. Only when it fails on the + * logical's own result type — the operands have no common representation + * (jose's `(!m || m[4] && m[1]) && invalid()` joins a boolean, a string + * and undefined into the checker's `string`) — does the statement lower + * as the control flow it is: `if (a) b;` / `if (!a) b;`, a condition test + * that needs no result value. Diagnostics outside the statement, and + * internal errors, keep the original failure. */ +function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt { + const diagsBefore = lowerer.diags.length; + try { + const value = lowerer.lowerExpr(expr); + if (value.kind === "unitLit") return { kind: "block", body: [], loc: locOf(expr) }; + return { kind: "exprStmt", expr: value, loc: locOf(expr) }; + } catch (e) { + // A probe (a capture buffer is active) keeps its failure: its owner + // picks another lowering, exactly as before. + if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; + const added = lowerer.diags.slice(diagsBefore); + const file = expr.getSourceFile().fileName; + const start = expr.getStart(); + const local = + added.length > 0 && + added.every( + (d) => + d.code !== "SC9001" && + d.loc.file === file && + d.loc.start >= start && + d.loc.end <= expr.end, + ); + if (!local) throw e; + lowerer.diags.length = diagsBefore; + } + const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; + const cond = lowerer.lowerCondition(expr.left); + if (cond.kind === "boolLit") { + return cond.value === and + ? lowerExprStatement(lowerer, expr.right) + : { kind: "block", body: [], loc: locOf(expr) }; + } + const right = (): IrStmt[] => + lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, expr.left, and), () => + and + ? withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, expr.left), () => [ + lowerExprStatement(lowerer, expr.right), + ]) + : [lowerExprStatement(lowerer, expr.right)], + ); + return and + ? { kind: "if", cond, then: right(), else_: [], loc: locOf(expr) } + : { kind: "if", cond, then: [], else_: right(), loc: locOf(expr) }; +} + /** True for a statement-position expression whose evaluation cannot be * observed once the value is discarded: * - a property/element READ (`?.` included) whose receiver IS a @@ -7910,22 +8268,25 @@ function destructuringAssignInto( ), ); } - const packed: IrExpr = { - kind: "recordLit", - fields: remaining.map((f): Extract["fields"][number] => ({ - name: f.name, - value: { - kind: "recordGet", - obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, - shapeId: srcType.shapeId, - field: f.name, - type: f.type, - loc: locOf(prop), - }, - })), - type: { kind: "record", shapeId: restShapeId }, - loc: locOf(prop), - }; + const packed: IrExpr = lowerer.keyOrder.copy( + { + kind: "recordLit", + fields: remaining.map((f): Extract["fields"][number] => ({ + name: f.name, + value: { + kind: "recordGet", + obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, + shapeId: srcType.shapeId, + field: f.name, + type: f.type, + loc: locOf(prop), + }, + })), + type: { kind: "record", shapeId: restShapeId }, + loc: locOf(prop), + }, + srcType.shapeId, + ); if (!ts.isIdentifier(restTo)) { // `({ a, ...box.rest } = src)` — the rest packs identically and // lands through the property/element write machinery. @@ -8529,6 +8890,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } } } + { + const opened = classAsyncGeneratorIterator(lowerer, stmt.expression); + if (opened) return lowerForAwaitGenerator(lowerer, stmt, opened, labels); + } if ( ts.isPropertyAccessExpression(stmt.expression) && lowerer.stdlibGlobalMember(stmt.expression, "process") === "stdin" @@ -9223,7 +9588,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // local and the body opens by ASSIGNING it into the one hoisted // binding — closures capture the shared slot, and the value persists // after the loop (both Node-exact for var). - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemValueT); if (varTarget) { const elemType = elemValueT; const loc = locOf(decl.name); @@ -9959,7 +10324,7 @@ function lowerForOfDyn( binds, ); } else if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, DYN); if (varTarget) { // `for (var x of d)`: the element mechanics stay on a hidden // per-iteration local; the body opens by assigning the ONE diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 7639e0974..5769e1e0a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -492,7 +492,7 @@ function followOptionsArg( } catch { return node; } - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return node; if ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0) return node; diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 5c6011425..32870c2cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -334,7 +334,7 @@ function staticFunctionName(lowerer: Lowerer, node: ts.Expression): string | nul if (!ts.isIdentifier(node)) return null; const symbol = lowerer.resolveValueSymbol(node); if (!symbol) return null; - for (const decl of lowerer.checker.declarationsOf(symbol)) { + for (const decl of symbol.declarations) { if (ts.isFunctionDeclaration(decl)) return decl.name?.text ?? "default"; if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) { let init = decl.initializer; @@ -728,7 +728,7 @@ export function lowerTestDirectCall( if (!ts.isIdentifier(callee)) return null; if (lowerer.builtinNamespaceModuleOf(callee) !== "test") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 740ff66f1..22983fbb4 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -330,7 +330,7 @@ function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean { if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers"; if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) { const sym = lowerer.checker.getSymbolAtLocation(callee.expression); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; return decl !== undefined && ts.isNamespaceImport(decl); } return false; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 093c78632..bacdc7941 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -241,25 +241,26 @@ test("optional union widening keeps refused and missing contracts current", () = }); test("stdlib provenance is cached by symbol identity, including merged and shadowed declarations", () => { - const builtin = {} as Symbol, - shadow = {} as Symbol; const userFile = { fileName: "user.ts" } as SourceFile; const library = { fileName: "library.d.ts" } as SourceFile; const userDeclaration = { getSourceFile: () => userFile } as Node; const libraryDeclaration = { getSourceFile: () => library } as Node; - const declarationsOf = vi.fn((symbol: Symbol) => - symbol === builtin ? [userDeclaration, libraryDeclaration] : [userDeclaration], - ); - const lowerer = new Lowerer( - { getTypeChecker: () => ({ declarationsOf }) } as unknown as Program, - userFile, - [], - ); + let declarationReads = 0; + const symbolWith = (declarations: readonly Node[]): Symbol => + ({ + get declarations() { + declarationReads++; + return declarations; + }, + }) as unknown as Symbol; + const builtin = symbolWith([userDeclaration, libraryDeclaration]), + shadow = symbolWith([userDeclaration]); + const lowerer = new Lowerer({ getTypeChecker: () => ({}) } as unknown as Program, userFile, []); vi.spyOn(lowerer, "isStdlibFile").mockImplementation((file) => file === library); for (let i = 0; i < 20; i++) { expect(lowerer.isStdlibSymbol(builtin)).toBe(true); expect(lowerer.isStdlibSymbol(shadow)).toBe(false); expect(lowerer.isStdlibSymbol(undefined)).toBe(false); } - expect(declarationsOf).toHaveBeenCalledTimes(2); + expect(declarationReads).toBe(2); }); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 639356abb..3045fd3e6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4,6 +4,7 @@ import { lowerLifetimeRegionCall, lowerLifetimeRegionNew, } from "./lifetime-regions.js"; +import type { ConstructorReturnPlan } from "./constructor-returns.js"; import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js"; import { lowerThreadSlotsCall, lowerThreadSlotsNew, threadSlotsType } from "./thread-slots.js"; import { isExperimentalDeclarationFile } from "../experimental-import.js"; @@ -20,6 +21,7 @@ import { } from "./require-initialization.js"; import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; +import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js"; import { checkedClassAssertion } from "./class-assertions.js"; import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js"; import { isOptionalProcessStreamProperty } from "./builtins/process.js"; @@ -31,6 +33,7 @@ import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; @@ -72,6 +75,7 @@ import { uninhabitedArmRetagHelper, strandedUnitTrap, strandedCoercionTrap, + recordUnionRouteHelper, recordUnionWrapHelper, unionRetagHelper, narrowedArmHelper, @@ -1196,9 +1200,9 @@ export function staticImportNamespaceType( const symbol = lowerer.checker.getSymbolAtLocation(arg); const source = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile); + symbol.declarations.find( + (decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile, + ); if ( source && !source.fileName.endsWith(".cts") && @@ -1453,7 +1457,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir // slot. JS inference reports the body type (`boolean` for // `enabled = true`) rather than that call-site type, so recover the // syntactic optionality from the parameter declaration. - const decl = lowerer.checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; mapped = jsOpenObjectType(decl, mapped, lowerer.shapes, lowerer.unions); const optional = decl !== undefined && @@ -1465,7 +1469,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir return mapped; }); const retT = lowerer.checker.getReturnTypeOfSignature(sig); - const sigDecl = lowerer.checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -1494,7 +1498,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { lowerer.checker.getPropertiesOfType(t).length !== 0 ) return null; - const decl = lowerer.checker.signatureDeclaration(sigs[0]!); + const decl = sigs[0]!.declaration; if ( decl === undefined || !( @@ -1540,14 +1544,14 @@ function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | lowerer.checker.getConstructSignatures(t).length === 0 && sigs[0]!.getTypeParameters().length === 0 && sigs[0]!.getParameters().every((p) => { - const pDecl = lowerer.checker.valueDeclarationOf(p); + const pDecl = p.valueDeclaration; return !pDecl || !ts.isParameter(pDecl) || pDecl.dotDotDotToken === undefined; }) && // A SYNTHESIZED rest param (tsc's `arguments` inference — no // valueDeclaration to carry the dotDotDot): param-count mismatch // against the signature's declaration; the whole value stays dyn. (() => { - const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!); + const sigDecl = sigs[0]!.declaration; const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if (declParams !== undefined && declParams.length !== sigs[0]!.getParameters().length) @@ -1603,7 +1607,7 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { const symbol = lowerer.resolveValueSymbol(n); if (!symbol || seen.has(symbol)) break; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isFunctionDeclaration(declaration)) { n = declaration; break; @@ -1721,7 +1725,7 @@ export class Lowerer { get stringIndexBounds(): StringIndexBounds { this.stringIndexBoundsCache ??= new StringIndexBounds( (node) => this.checker.getSymbolAtLocation(node) ?? null, - (symbol) => this.checker.valueDeclarationOf(symbol), + (symbol) => symbol.valueDeclaration, (node) => (this.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0, ); return this.stringIndexBoundsCache; @@ -2083,7 +2087,7 @@ export class Lowerer { // directly from the aliased declaration as a final read-side hook, so // the initializer cannot fall back to a narrowing adapter. if (ts.isIdentifier(node) && fallback?.kind === "func") { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -2442,6 +2446,9 @@ export class Lowerer { * Reachability lowers inits before the declarations they discover, so * these rebuild after the worklist restores historical shape metadata. */ readonly shapeOrderHelperFinalizers: (() => void)[] = []; + /** Construction witnesses that choose each shape's enumeration order + * once lowering settles (key-order-witness.ts). */ + readonly keyOrder = new KeyOrderNotes(); /** Emit-override specializations (`%C.emit:` — lower-event-emitter.ts's * emit-overrides block): interned names, the drive-loop queue, and the * currently-lowering specialization's context (the super-forward @@ -2453,6 +2460,9 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** structuralNarrowing's assignable classes, keyed by static and + * narrowed class name. */ + readonly structuralClassPeers = new Map(); /** Interned node:assert helpers (deep-equality comparisons keyed by * typeKey, throws wrappers keyed by callback type + expected class) — @@ -2534,6 +2544,8 @@ export class Lowerer { * so named imports, namespace reads, CommonJS bindings, and alias chains * all reuse one immortal zero-capture closure. */ readonly builtinCallableValueFns = new Map(); + /** Each builtin function-value adapter's Node name, for boxed values. */ + readonly builtinCallableValueNames = new Map(); /** Optional-chain lowering state. While a chain body lowers, the guarded * receiver NODE reads as a chainRecv (typed by the narrowed arm) instead * of re-lowering, its checker type reads non-nullish (typeOf), and the @@ -2819,6 +2831,16 @@ export class Lowerer { * by class name (cycle-initialization.ts). */ readonly classInitFlags = new Map(); readonly cycleInitDeclarations = new Map(); + /** Each class's constructor return plan by class name + * (constructor-returns.ts); null when no return can yield an object. */ + readonly constructorReturnPlans = new Map(); + /** Pointer-guarded (`tdz`) module bindings declared without an + * initializer (`let f: T;`): an initialization flag tracks their + * declaration (markCycleEarlyBindings). */ + readonly uninitializedLexicalGlobals = new Map< + string, + { name: ts.Identifier; module: ts.SourceFile; symbol: ts.Symbol } + >(); /** CommonJS require declarations whose bindings code running before the * require can read, and those references (require-initialization.ts). */ readonly requireInitFlags = new Map(); @@ -3153,15 +3175,38 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * is `unknown` or `object` (also with `null` or `undefined`, or an array - * of those), names checked protocol identity, or is an owned runtime - * handle (a Worker or MessagePort, or a fetch Headers/Request/Response); - * `any` fields stay refused. */ + * of those), names checked protocol identity, is an owned runtime + * handle (a Worker or MessagePort, or a fetch Headers/Request/Response), + * or is a WeakMap/WeakSet (native weak collections live in + * checked-dynamic storage, as locals of those types do); `any` fields + * stay refused. */ classFieldStoresDyn(type: ts.Type): boolean { return ( this.opaqueValueType(type) || this.classImplementedProtocol(type) || isWorkerHandleType(type, this.typeCtx) || - isFetchHandleType(type, this.typeCtx) + isFetchHandleType(type, this.typeCtx) || + this.weakCollectionType(type) + ); + } + + /** A stdlib WeakMap or WeakSet, alone or with `null`/`undefined`. */ + private weakCollectionType(type: ts.Type): boolean { + const weak = (part: ts.Type): boolean => { + const symbol = part.getSymbol(); + return ( + (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + symbol.declarations + .some((d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(d.getSourceFile())) + ); + }; + if (!type.isUnionType()) return weak(type); + const parts = ts.constituentTypes(type); + return ( + parts.some(weak) && + parts.every( + (part) => (part.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 || weak(part), + ) ); } @@ -3190,7 +3235,7 @@ export class Lowerer { let sym = type.getSymbol(); if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = this.checker.getAliasedSymbol(sym); - for (const decl of this.checker.declarationsOf(sym)) { + for (const decl of sym.declarations) { if ( ts.isSourceFile(decl) && !decl.isDeclarationFile && @@ -3454,7 +3499,7 @@ export class Lowerer { * every project-module spelling. */ private moduleSourceFileOf(from: ts.SourceFile, spec: ts.StringLiteral): ts.SourceFile | null { const moduleSymbol = this.checker.getSymbolAtLocation(spec); - for (const decl of moduleSymbol ? this.checker.declarationsOf(moduleSymbol) : []) { + for (const decl of moduleSymbol ? moduleSymbol.declarations : []) { if (ts.isSourceFile(decl)) return decl; } return resolveImport(this.program, from, spec.text); @@ -3499,9 +3544,7 @@ export class Lowerer { } if (!ts.isIdentifier(value)) return null; const symbol = this.checker.getSymbolAtLocation(value); - const namespaceDecl = symbol - ? this.checker.declarationsOf(symbol).find(ts.isNamespaceImport) - : undefined; + const namespaceDecl = symbol ? symbol.declarations.find(ts.isNamespaceImport) : undefined; if (namespaceDecl === undefined) return null; const importDecl = namespaceDecl.parent?.parent; if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) @@ -3521,7 +3564,7 @@ export class Lowerer { ): string | null { if (symbol === undefined || seenSymbols.has(symbol)) return null; seenSymbols.add(symbol); - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; // Route-aware alias hops run before declaration-file ownership. An // exact import must keep the specifier it actually named, rather than @@ -3590,9 +3633,9 @@ export class Lowerer { let sym: ts.Symbol | undefined = alias; for (let hop = 0; sym !== undefined && hop < 32; hop++) { if (hop > 0 && sym.flags & ts.SymbolFlags.Alias && this.globalsBySymbol.has(sym)) return sym; - const d = this.checker - .declarationsOf(sym) - .find((x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x)); + const d = sym.declarations.find( + (x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x), + ); let spec: ts.Expression | undefined; let name: string | undefined; if (d && ts.isImportClause(d) && ts.isImportDeclaration(d.parent)) { @@ -3639,7 +3682,7 @@ export class Lowerer { * see resolveValueSymbol. Null when `symbol` is not such a property (or * the property's value is not a plain identifier reference). */ private cjsExportValueSymbol(symbol: ts.Symbol): ts.Symbol | null { - const d = this.checker.declarationsOf(symbol)[0]; + const d = symbol.declarations[0]; if (!d) return null; // MEMBER-form class exports (`exports.C = C` — commander's error.js): // alias plumbing exactly like a table entry, so importers land on the @@ -3685,7 +3728,7 @@ export class Lowerer { let value = this.checker.getSymbolAtLocation(assign.right); if (!value) return null; if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); - const isClass = this.checker.declarationsOf(value).some((decl) => ts.isClassDeclaration(decl)); + const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl)); return isClass ? value : null; } @@ -3731,7 +3774,7 @@ export class Lowerer { * member-symbol fallback and namespace classification in lockstep. */ private localModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { const sym = this.checker.getSymbolAtLocation(expr); - const decls = sym ? this.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const decl = decls.find(ts.isImportClause) ?? decls[0]; if (!decl) return null; if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); @@ -3759,31 +3802,27 @@ export class Lowerer { * static storage or callable/class metadata. */ private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const decl = binding - ? this.checker.declarationsOf(binding).find(ts.isVariableDeclaration) - : undefined; + const decl = binding ? binding.declarations.find(ts.isVariableDeclaration) : undefined; if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null; const target = createRequireProgramModuleOf(this, decl.initializer); if (target === null) return null; const moduleSym = this.checker.getSymbolAtLocation(target.dep); let symbol = moduleSym?.getExports().get("export=" as ts.__String); if (!symbol) return null; - const wholeAssignment = this.checker - .declarationsOf(symbol) - .find((d): d is ts.BinaryExpression => { - if ( - !ts.isBinaryExpression(d) || - !ts.isExpressionStatement(d.parent) || - !ts.isSourceFile(d.parent.parent) - ) - return false; - const exported = cjsExportAssignmentOf(d.parent); - return ( - exported?.kind === "table" && - exported.obj === null && - cjsExportDiscardReason(d.parent) === null - ); - }); + const wholeAssignment = symbol.declarations.find((d): d is ts.BinaryExpression => { + if ( + !ts.isBinaryExpression(d) || + !ts.isExpressionStatement(d.parent) || + !ts.isSourceFile(d.parent.parent) + ) + return false; + const exported = cjsExportAssignmentOf(d.parent); + return ( + exported?.kind === "table" && + exported.obj === null && + cjsExportDiscardReason(d.parent) === null + ); + }); if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; @@ -3810,9 +3849,7 @@ export class Lowerer { * target module's real export symbol. */ private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const element = binding - ? this.checker.declarationsOf(binding).find(ts.isBindingElement) - : undefined; + const element = binding ? binding.declarations.find(ts.isBindingElement) : undefined; if ( !element || element.name === undefined || @@ -3860,7 +3897,7 @@ export class Lowerer { if (sym && sym.flags & ts.SymbolFlags.Alias) { // `module.exports = TypeError`: the binding IS the builtin global. if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; - const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; + const d = this.checker.getAliasedSymbol(sym).declarations[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { let init = d.initializer; @@ -3925,7 +3962,7 @@ export class Lowerer { if (g.type.kind === "func") g.type = this.runtimeOptionalBindingType(ident, g.type); return g; } - for (const d of this.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const byDecl = this.globalsByDeclNode.get(d); if (byDecl) return byDecl; } @@ -4441,7 +4478,7 @@ export class Lowerer { }; const recordFlows = new RecordFlowClasses(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, mayBeRecord: (node) => { if (node.parent && ts.isParameter(node.parent)) return plainRecordShape(parameterType(node)) !== null; @@ -4472,7 +4509,7 @@ export class Lowerer { const nanCoded = new NanCodedReads( { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, bytesElemOf: (receiver) => { const t = mappedOrNull(receiver); return t?.kind === "bytes" ? t.elem : null; @@ -4524,7 +4561,7 @@ export class Lowerer { const seen = new Set(); while (symbol && !signatureBySymbol.has(symbol) && !seen.has(symbol)) { seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isMethodDeclaration(declaration)) { symbol = symbolOf(declaration.name); } else if ( @@ -4669,7 +4706,7 @@ export class Lowerer { if (ts.isIdentifier(e)) { // A binding initialized from such a read and never reassigned. const symbol = symbolOf(e); - const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; return ( symbol !== null && !reassigned.has(symbol) && @@ -4710,7 +4747,7 @@ export class Lowerer { const read = peel(node); if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false; const source = symbolOf(read.expression); - const declaration = source ? this.checker.valueDeclarationOf(source) : undefined; + const declaration = source ? source.valueDeclaration : undefined; if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return false; const init = peel(declaration.initializer); @@ -4939,7 +4976,7 @@ export class Lowerer { * exactly `T | undefined`: an assignment from a missing read stores the * undefined arm while the checker narrows later reads to `T`. */ const optionalInstanceField = (symbol: ts.Symbol): boolean => { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false; if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false; const sf = decl.getSourceFile(); @@ -5023,13 +5060,42 @@ export class Lowerer { return out; }; const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => { - if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init); + if (ts.isObjectBindingPattern(name)) { + // `const { 0: a, 1: b, length } = parts` reads positions like the + // equivalent array pattern: each may sit past the end. + // Regex match results keep their own pattern lowering. + const arrayType = this.mapTypeOf(this.typeOf(init)); + const initType = this.checker.getNonNullableType(this.typeOf(init)); + const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); + const matchResult = + initSymbol !== undefined && + (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); + const positions = name.elements.nodes.filter((el) => { + const key = el.propertyName; + return ( + key !== undefined && ts.isNumericLiteral(key) && /^(?:0|[1-9]\d{0,8})$/.test(key.text) + ); + }); + return scanPositions(positions, init, arrayType); + } if (!ts.isArrayBindingPattern(name)) return false; const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; + return scanPositions( + name.elements.nodes.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), + init, + sourceType, + ); + }; + const scanPositions = ( + elements: readonly ts.BindingElement[], + init: ts.Expression, + sourceType: IrType & { kind: "array" }, + ): boolean => { let changed = false; - name.elements.nodes.forEach((el) => { - if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; + elements.forEach((el) => { + if (el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); if (symbol && !el.initializer && !optionalSymbols.has(symbol)) { @@ -5278,7 +5344,7 @@ export class Lowerer { // monomorphic ABI or poisons valid generic value programs. if (this.genericFnsBySymbol.has(symbol)) return false; seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { if (promoteHofCallback(peel(declaration.initializer), parameterIndices, seen)) changed = true; @@ -5412,7 +5478,7 @@ export class Lowerer { seen.add(symbol); const fn = functionDeclBySymbol.get(symbol); if (fn) return bodyReturnsOptional(fn); - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; return ( !!decl && ts.isVariableDeclaration(decl) && @@ -5493,7 +5559,7 @@ export class Lowerer { }; const ownership = new ArrayOwnership(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, typeOf: typeOrNull, instanceAssignableTo: (owner, target) => panicSafe( @@ -5518,7 +5584,7 @@ export class Lowerer { // flow type such as `Array.isArray(x)`'s `any[]` only narrows it. const e = peel(node); const symbol = ts.isIdentifier(e) ? symbolOf(e) : null; - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const declared = declaration && (ts.isVariableDeclaration(declaration) || ts.isParameter(declaration)) && @@ -5840,7 +5906,7 @@ export class Lowerer { const assertion = forwardedAssertion(node.right); const symbol = assertion ? symbolOf(node.left) : null; // Only a variable's storage is promoted (see the final pass). - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if ( assertion && symbol && @@ -5926,9 +5992,7 @@ export class Lowerer { let nativeArrayReceiver = receiver?.kind === "array" || tuple; if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) { const receiverSymbol = symbolOf(receiverNode); - const declaration = receiverSymbol - ? this.checker.valueDeclarationOf(receiverSymbol) - : undefined; + const declaration = receiverSymbol ? receiverSymbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration)) { const declared = this.mapTypeOf(this.typeOf(declaration.name)); if ( @@ -5988,7 +6052,7 @@ export class Lowerer { // number) => ...`): a possibly missing argument widens that // parameter, as for a callback the same value is passed as. if (!ts.isIdentifier(node.expression) || reassigned.has(symbol)) return; - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const value = declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? peel(declaration.initializer) @@ -6215,7 +6279,7 @@ export class Lowerer { // there, whatever its declared type. for (const symbol of earlyReadVarSymbols(this, sourceFiles)) { // Text codec bindings keep their own presence-checked storage. - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const mapped = declaration && ts.isVariableDeclaration(declaration) ? mappedOrNull(declaration.name) @@ -7436,6 +7500,13 @@ export class Lowerer { this.settleGenericDemandPriorities(); this.shapes.settleDeclaredOrderPriorities(); for (const finalize of this.shapeOrderMetadataFinalizers) finalize(); + applyKeyOrderWitnesses(this, [ + ...orderedUnits, + ...initFunctions, + ...instanceFunctions, + ...this.liftedFns, + ...this.implicitFns, + ]); for (const finalize of this.shapeOrderHelperFinalizers) finalize(); const functions = [ ...orderedUnits, @@ -7758,13 +7829,11 @@ export class Lowerer { const ownSym = widened.getSymbol(); if ( ownSym && - this.checker - .declarationsOf(ownSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ) + ownSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ) ) { this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); throw new PoisonError(); @@ -7806,13 +7875,11 @@ export class Lowerer { const stdlibOwnSym = widened.getSymbol(); const stdlibNominal = stdlibOwnSym !== undefined && - this.checker - .declarationsOf(stdlibOwnSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ); + stdlibOwnSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ); // OVERLOADED call signatures (SC2007) — after the generic branch, so a // generic overload set keeps the monomorphization story: a value of a // multi-signature type has no single compiled signature to hold. @@ -7908,7 +7975,11 @@ export class Lowerer { access: ts.PropertyAccessExpression | ts.ElementAccessExpression, use: "read" | "call", ): IrExpr | null { - return fenceStaticResponseMember(this, access, use) ?? fenceStaticBlobMember(this, access, use); + return ( + fenceStaticResponseMember(this, access, use) ?? + fenceStaticBlobMember(this, access, use) ?? + fenceWebCryptoMember(this, access, use) + ); } fenceUnsupportedFetchConstructorMember( @@ -8025,7 +8096,7 @@ export class Lowerer { // JSDoc and initializer inference are declaration types, not flow // proofs. A checked specialization must not reinstate those // stale layouts (mutable/null slots and heterogeneous JS arrays). - const declaration = sym && this.checker.valueDeclarationOf(sym); + const declaration = sym && sym.valueDeclaration; const declared = declaration && ts.isParameter(declaration) ? this.mapTypeOf(this.checker.getTypeAtLocation(declaration.name)) @@ -8604,12 +8675,17 @@ export class Lowerer { liveReference(expr.type) || (expr.type.kind === "union" && (this.unions.get(expr.type.unionId)?.arms.some(liveReference) ?? false)); + // A builtin function's adapter boxes under the builtin's name, the + // name Node's function carries (`[Function: randomUUID]`). + const builtinName = + expr.kind === "closure" ? this.builtinCallableValueNames.get(expr.fnName) : undefined; return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc, ...(liveRef ? { liveRef: true as const } : {}), + ...(builtinName !== undefined ? { fnName: builtinName } : {}), }; } return expr; @@ -9152,6 +9228,14 @@ export class Lowerer { return recordUnionWrapHelper(this, source, toId, loc); } + recordUnionRouteHelper( + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, + ): string | null { + return recordUnionRouteHelper(this, source, toId, loc); + } + unionRetagHelper( fromId: string, toId: string, @@ -9348,7 +9432,7 @@ export class Lowerer { if (!ts.isIdentifier(n)) return null; const sym = this.resolveValueSymbol(n); if (!sym) return null; - const decls = this.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; const units = new Set(); let allConst = true; @@ -9652,16 +9736,23 @@ export class Lowerer { } } } - let e = this.lowerExpr(node); + const e = this.lowerExpr(node); + return expected ? this.coerceExistingInto(node, e, expected) : e; + } + + /** The conversion lowerExprExpecting applies to `e`, the lowered value + * of `node` when it is not fresh construction the destination directs + * (a literal, conditional, or empty collection). */ + coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr { if ( - (expected?.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || - (expected?.kind === "dyn" && isJsSourceFile(node.getSourceFile())) + (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || + (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile())) ) { // A destination that accepts undefined must retain an unchecked // read's storage value even when the checker still calls it present. e = this.runtimeOptionalSourceValue(node, e) ?? e; } - return expected ? this.coerceInto(node, e, expected) : e; + return this.coerceInto(node, e, expected); } /** An unannotated empty collection has no existing elements or aliases @@ -10121,7 +10212,7 @@ export class Lowerer { // declaration poisons under a source-anchored diagnostic, deferred // like any collection fence — never a crashed CLI. if (isCheckerPanic(e)) { - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; sink.push( checkerPanicDiag( e.message.split("\n", 1)[0]!, @@ -11046,7 +11137,7 @@ export class Lowerer { // symbol — which is what declareParams registered the local under. // On a miss, normalize to the declaration's key and retry (ordinary // bindings never reach this — their two sides intern to one symbol). - const vd = this.checker.valueDeclarationOf(symbol); + const vd = symbol.valueDeclaration; if ( vd && ts.isParameter(vd) && @@ -11097,9 +11188,7 @@ export class Lowerer { }); if (resolved.error !== null) { const location = - blame ?? - (symbol === undefined ? undefined : this.checker.declarationsOf(symbol)[0]) ?? - this.entry; + blame ?? (symbol === undefined ? undefined : symbol.declarations[0]) ?? this.entry; const message = `the binding '${resolved.origin!.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`; this.unsupported("SC1090", location, message); } @@ -11181,7 +11270,7 @@ export class Lowerer { // concrete signature — name the shape instead of the generic // binding-form text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if (d && ts.isVariableDeclaration(d) && d.initializer === undefined) { const t = this.checker.getTypeOfSymbol(symbol); const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; @@ -11203,7 +11292,7 @@ export class Lowerer { // assignment are `undefined`, and this binding's type has no slot for // that value — name the shape instead of the generic no-lowering text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if ( d && ts.isVariableDeclaration(d) && @@ -11727,6 +11816,7 @@ export class Lowerer { lowerStaticReadableStreamReaderCall(this, expr) ?? lowerStaticResponseCall(this, expr) ?? lowerStaticBlobCall(this, expr) ?? + lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? @@ -12062,9 +12152,21 @@ export class Lowerer { if (mapped?.kind === "moduleNs" && mapped.moduleId.startsWith("builtin:")) { return mapped.moduleId.slice("builtin:".length); } + // A nullable namespace binding (`const os: typeof NodeOs | null`, a + // probed process.getBuiltinModule result) read where the checker + // narrowed it present: its members key the same tables. + if (stored?.kind === "union") { + const present = this.unions.get(stored.unionId)?.arms.filter((arm) => !isUnitType(arm)); + const only = present?.length === 1 ? present[0] : undefined; + if (only?.kind === "moduleNs" && only.moduleId.startsWith("builtin:")) { + const narrowed = this.mapTypeOf(this.typeOf(expr)); + if (narrowed?.kind === "moduleNs" && narrowed.moduleId === only.moduleId) + return only.moduleId.slice("builtin:".length); + } + } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin: `const fs = require("fs")` binds the same // namespace surface as `import * as fs from "node:fs"` — and the @@ -12129,7 +12231,7 @@ export class Lowerer { } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The DEFAULT-import twin: Node's default export of a CJS builtin // IS the module object, so `import path from "node:path"` exposes @@ -12393,11 +12495,12 @@ export class Lowerer { param.mode === "optional" ? `${param.mode}:${param.defaultValue}` : param.mode, ) .join(","); - const key = `${fn.fn}:${typeKey(funcType)}:${adapterShape}`; + const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; this.builtinCallableValueFns.set(key, fnName); + this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -12462,7 +12565,7 @@ export class Lowerer { loc, }; }); - const call: IrExpr = { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; + const call: IrExpr = { kind: "libCall", fn: fn.valueFn ?? fn.fn, args, type: fn.result, loc }; const body: IrStmt[] = fn.result.kind === "void" ? [ diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts new file mode 100644 index 000000000..205cc63e9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -0,0 +1,64 @@ +/* A module export called through its module binding (`m.f()` where `m` is + * `const m = require("./x")` or `import * as m from "./x"`): Node passes + * the module object (the exports object, or the namespace) as `this`. The + * member call lowers to a direct call of the exported function, which has + * no module object to pass, so an export that reads `this` refuses instead + * of seeing another receiver. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** The function an export binding holds, when its declaration shows it. */ +function exportedFunction(decl: ts.Node): ts.Node | null { + let node: ts.Node | undefined = decl; + if (ts.isPropertyAccessExpression(node) && node.parent && ts.isBinaryExpression(node.parent)) + node = node.parent; + if (ts.isBinaryExpression(node)) node = node.right; + else if (ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node)) node = node.initializer; + while (node !== undefined && ts.isParenthesizedExpression(node)) node = node.expression; + if (node === undefined) return null; + if ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isMethodDeclaration(node) + ) + return node; + return null; +} + +/** Whether a function's own body reads `this` (arrows share it; other + * functions and classes have their own). */ +function readsOwnThis(fn: ts.Node): boolean { + let found = false; + const visit = (node: ts.Node): void => { + if (found) return; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return; + } + if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node)) + return; + node.forEachChild(visit); + }; + fn.forEachChild(visit); + return found; +} + +export function refuseModuleMemberThis( + lowerer: Lowerer, + call: ts.CallExpression, + member: ts.Identifier, +): void { + let symbol = lowerer.checker.getSymbolAtLocation(member); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined) return; + for (const decl of symbol.declarations) { + const fn = exportedFunction(decl); + if (fn === null || !readsOwnThis(fn)) continue; + lowerer.unsupported( + "SC1090", + call, + `calling a module export that reads 'this' through its module binding ('${member.text}': Node passes the module object as 'this', which this call has no value for)`, + ); + } +} diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index 368ce3c5f..d2425750b 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -31,7 +31,7 @@ export function lowerModuleNamespaceElement( ? lowerer.checker.getAliasedSymbol(exported) : exported; if (!(target.flags & ts.SymbolFlags.Value)) return; - const declaration = lowerer.checker.valueDeclarationOf(target); + const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { lowerer.unsupported("SC1090", expr, "computed namespace reads of unnamed exports"); diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 500267263..01801470d 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -190,7 +190,7 @@ export class NarrowingStability { const { call, argument } = slot; const checker = this.lowerer.checker; const signature = checker.getResolvedSignature(call); - const declaration = signature ? checker.signatureDeclaration(signature) : undefined; + const declaration = signature ? signature.declaration : undefined; if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true; const callee = skipOuter(call.expression); if ( @@ -372,7 +372,7 @@ export class NarrowingStability { this.staleBindings.set(symbol, false); let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (decl !== undefined && ts.isVariableDeclaration(decl)) { if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true; else { @@ -421,7 +421,7 @@ export class NarrowingStability { if (cached !== undefined) return cached; let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; const mutableBinding = decl !== undefined && ((ts.isVariableDeclaration(decl) && diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 3ccd54b64..c9ff3300d 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -163,7 +163,7 @@ function ffiDeclarationDiagnostic( loc: SrcLoc, ): ScrDiagnostic | null { const { binding: bindingDiag, signature: signatureDiag } = ffiFlavor(lowerer); - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const functionDecls = declarations.filter(ts.isFunctionDeclaration); if ( functionDecls.length === 0 || @@ -386,7 +386,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr // are untouched. if (lowerer.libraryCallbacks) { const symbol = lowerer.resolveValueSymbol(expr.expression); - const decls = symbol === null ? [] : lowerer.checker.declarationsOf(symbol); + const decls = symbol === null ? [] : symbol.declarations; const callbackShaped = decls.length > 0 && decls.every( @@ -427,7 +427,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (validSymbols === undefined) throw new PoisonError(); if (!validSymbols.has(symbol)) { if (lowerer.libraryCallbacks) { - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const programDeclarations = declarations.filter((decl) => libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), ); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index ac83dccd8..ddd8d6d56 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -21,15 +21,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { lowerer.collecting = wasCollecting; } if (!symbol) return false; - const owner = lowerer.checker - .declarationsOf(symbol) - .find( - (node) => - (ts.isFunctionDeclaration(node) && node.body !== undefined) || - (ts.isVariableDeclaration(node) && - node.initializer && - ts.isFunctionExpression(node.initializer)), - ); + const owner = symbol.declarations.find( + (node) => + (ts.isFunctionDeclaration(node) && node.body !== undefined) || + (ts.isVariableDeclaration(node) && + node.initializer && + ts.isFunctionExpression(node.initializer)), + ); const declaration = owner && ts.isVariableDeclaration(owner) && diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index 11443051c..9624ac0aa 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -21,7 +21,7 @@ export function isPrivateSplitConsumer( const local = lowerer.bindingIn(lowerer.ctx, symbol); if (!local || local.boxed || local.tdz || local.mutable || local.id !== iterable.localId) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index be202dfe3..7ae1371c2 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -794,6 +794,10 @@ export interface BuiltinModuleFn { * omission or explicit undefined; rest parameters pack into one typed * array slot. Entries with call-site-specific validation remain absent. */ valueParams?: BuiltinValueParam[]; + /** The libCall a function value's adapter makes when it differs from the + * direct-call row's `fn` (a direct call picks an arity-specific entry + * point; the value accepts every arity through one). */ + valueFn?: IrLibFn; } export type BuiltinValueParam = @@ -1503,7 +1507,15 @@ export const BUILTIN_MODULE_FNS: Record< userInfo: { fn: "os.userName", params: [], result: VOID }, }, crypto: { - randomUUID: { fn: "crypto.randomUUID", params: [], result: STRING }, + randomUUID: { + fn: "crypto.randomUUID", + params: [], + result: STRING, + // As a value (uuid's `export default { randomUUID }`), the optional + // options argument goes through the validating entry point. + valueFn: "crypto.randomUUIDOpts", + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, // A real Buffer result. The composed randomBytes(n).toString(enc) // form KEEPS its one-libCall lowering (lowerCryptoComposedCall runs // first and the Buffer never materializes there); this entry covers @@ -1525,7 +1537,12 @@ export const BUILTIN_MODULE_FNS: Record< getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, - randomUUIDv7: { fn: "crypto.randomUUIDv7", params: [DYN], result: STRING }, + randomUUIDv7: { + fn: "crypto.randomUUIDv7", + params: [DYN], + result: STRING, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN }, generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN }, generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID }, @@ -2162,6 +2179,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["blob.fileNew"], note: "a Blob with name and lastModified (Date.now() when absent)", }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], + note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + }, + { + id: "stdlib.webcrypto.CryptoKey", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, { id: "stdlib.atomics", kind: "stdlib", @@ -3232,7 +3263,7 @@ export function isChildSurfaceMember( ): boolean { if (isStdlibMember(lowerer, access)) return true; const sym = lowerer.checker.getSymbolAtLocation(access.name); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; const iface = decl?.parent; if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false; if (lowerer.isStdlibFile(decl.getSourceFile())) return false; @@ -3245,10 +3276,7 @@ export function isChildSurfaceMember( * with lib interfaces, so a member can carry declarations from both. A * user's own declaration is in neither, so shadowing never matches. */ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean { - return ( - !!symbol && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) - ); + return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())); } /** The canonical stdlib-global name `expr` denotes, or null. Three @@ -3274,9 +3302,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin // symbol has no ordinary declarations for the provenance check. if ( expr.text === "globalThis" && - (!symbol || - lowerer.checker.declarationsOf(symbol).length === 0 || - lowerer.isStdlibSymbol(symbol)) + (!symbol || symbol.declarations.length === 0 || lowerer.isStdlibSymbol(symbol)) ) return "globalThis"; if (!symbol) return null; @@ -3284,7 +3310,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if (alias !== undefined) return alias; const classAlias = lowerer.builtinClassAliasOf(symbol); if (classAlias?.kind === "collection") return classAlias.which; - const decl = lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) { const importDecl = decl.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -3447,7 +3473,7 @@ export function stdlibGlobalAliasDecl( * the SC2020 fence's wording: "typed by @types/node", not "standard * library". */ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return false; let viaNode = false; for (const d of decls) { diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index f4f536af9..5d7a20c06 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -26,7 +26,7 @@ function constantSymbolDescription( const symbol = lowerer.resolveValueSymbol(name); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || @@ -94,8 +94,8 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const cached = cache.get(sym); if (cached !== undefined) return cached; cache.set(sym, null); - const decls = lowerer.checker.declarationsOf(sym); - const decl = lowerer.checker.valueDeclarationOf(sym); + const decls = sym.declarations; + const decl = sym.valueDeclaration; if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 6ec9af036..50b5ecf09 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -168,7 +168,7 @@ function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpressi let symbol = checker.getSymbolAtLocation(ref); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; const list = decl.parent; if ( diff --git a/packages/compiler/src/frontend/lowering/union-retag.test.ts b/packages/compiler/src/frontend/lowering/union-retag.test.ts index 0cb7b1b9f..8116d6378 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.test.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "vitest"; import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals, typeKey } from "../../ir/ir.js"; import type { IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; -import { planUnionRetag, buildUnionRetag, planRecordUnionWrap } from "./union-retag.js"; +import { + planUnionRetag, + buildUnionRetag, + planRecordUnionLift, + planRecordUnionWrap, +} from "./union-retag.js"; import type { UnionRetagArm } from "./union-retag.js"; import type { WidthLift } from "./width-lift.js"; @@ -126,6 +131,30 @@ describe("record conversion with shared layouts", () => { f.shapes[2]!.fields.reverse(); expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); }); + + test("routes a record that fits several arms by its discriminant", () => { + const f = fixture(); + expect(planRecordUnionLift(f.shapes[0]!, f.to, f.shapeOf, f.lift)).toEqual({ + field: "kind", + fieldType: STRING, + routes: [ + { tag: 0, lift: { how: "width" }, values: ["left"] }, + { tag: 1, lift: { how: "width" }, values: ["right"] }, + ], + }); + // Without the discriminant field, or without an arm it converts into, + // there is no route. + const unnamed: IrRecordShape = { id: "r3", fields: f.shapes[0]!.fields.slice(1) }; + expect(planRecordUnionLift(unnamed, f.to, f.shapeOf, f.lift)).toBeNull(); + const unrelated: IrRecordShape = { + id: "r4", + fields: [ + { name: "kind", type: STRING }, + { name: "other", type: F64 }, + ], + }; + expect(planRecordUnionLift(unrelated, f.to, f.shapeOf, f.lift)).toBeNull(); + }); }); describe("union conversion planning", () => { diff --git a/packages/compiler/src/frontend/lowering/union-retag.ts b/packages/compiler/src/frontend/lowering/union-retag.ts index 8d1c61d09..1c435ab36 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.ts @@ -86,6 +86,36 @@ export function planRecordUnionWrap( return { field: discriminant.field, fieldType, routes }; } +/** A record whose layout converts into more than one arm of a + * discriminated union. The IR keeps the discriminant field as a plain + * string, so its layout alone cannot say which variant it is; the value of + * the discriminant does, as in JavaScript. Every record arm the record + * converts into (by copy or width) becomes a route selected by that arm's + * discriminant values. A value no route names throws the catchable + * TypeError. */ +export function planRecordUnionLift( + source: IrRecordShape, + to: IrUnionDef, + shapeOf: (id: string) => IrRecordShape | undefined, + widthLift: (src: IrType, dst: IrType) => WidthLift | null, +): RecordUnionWrapPlan | null { + const discriminant = to.discriminant; + if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source)) + return null; + const fieldType = discriminantField(source, discriminant.field); + if (!fieldType || discriminantOwners(to, shapeOf) === null) return null; + const sourceType: IrType = { kind: "record", shapeId: source.id }; + const routes: UnionRetagRoute[] = []; + for (const entry of discriminant.cases) { + const arm = to.arms[entry.tag]; + if (!arm || arm.kind !== "record") continue; + const lift = arm.shapeId === source.id ? { how: "copy" as const } : widthLift(sourceType, arm); + if (lift === null) continue; + routes.push({ tag: entry.tag, lift, values: entry.values }); + } + return routes.length > 0 ? { field: discriminant.field, fieldType, routes } : null; +} + /** Pure conversion planning. A source storage tag need not denote just * one semantic variant: refinements can coalesce recursive record layouts. * When both unions name the same discriminant, its values select target diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index 40c533d72..d90f9ed35 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -7,6 +7,7 @@ export type WidthLift = | { how: "copy" } | { how: "wrap"; tag: number } | { how: "discriminantWrap" } + | { how: "discriminantRoute" } | { how: "retag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index a81cbf263..04c66a559 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -16,14 +16,12 @@ export function isNpmStaticSubclassArgument( const symbol = type.getSymbol(); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && - npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, - ) + symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && + /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && + npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, + ) ); }; const subtype = (actual: ts.Type, expected: ts.Type): boolean => { diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bde168c38..d7c2ba66a 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1225,9 +1225,9 @@ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const symbol = checker.getSymbolAtLocation(value); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent) + symbol.declarations.some( + (decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent, + ) ); } @@ -1236,9 +1236,9 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { const symbol = checker.getSymbolAtLocation(ident); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl)) + symbol.declarations.some( + (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl), + ) ); } @@ -1953,7 +1953,7 @@ function stripRequireCasts7(expr: ts.Expression): ts.Expression { function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return false; const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -1963,7 +1963,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = targetDecl.parent; @@ -1988,9 +1988,7 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(callee); const decl = symbol - ? checker - .declarationsOf(symbol) - .find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) + ? symbol.declarations.find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) : undefined; if ( decl === undefined || @@ -2054,7 +2052,7 @@ function stableCreateRequireBindingReason7( } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(decl.name); - if (!symbol || checker.declarationsOf(symbol).length !== 1) + if (!symbol || symbol.declarations.length !== 1) return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; @@ -2204,7 +2202,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } @@ -2359,7 +2357,7 @@ function selfImportTdzFences7( // The aliased declaration's TDZ horizon: the position after which reads // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { - const d = checker.valueDeclarationOf(aliased) ?? checker.declarationsOf(aliased)[0]; + const d = aliased.valueDeclaration ?? aliased.declarations[0]; if (d === undefined || d.getSourceFile() !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; return d.getEnd(); @@ -2616,7 +2614,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod let sym = checker.getSymbolAtLocation(root); if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); - const decls = checker.declarationsOf(sym); + const decls = sym.declarations; return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => @@ -3840,7 +3838,7 @@ function returnsOnlyClosuresOver7( if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; if (decl.getSourceFile().isDeclarationFile) return false; @@ -3963,7 +3961,7 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea symbol = checker.getAliasedSymbol(symbol); } if (symbol === undefined) return false; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; @@ -4081,6 +4079,12 @@ function resolveNpmImport7( if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) { return null; } + // Node resolves a bare core-module name (`string_decoder`, `events`, + // `fs/promises`) to the builtin before it looks in node_modules, so an + // installed npm package of the same name never answers it. Prefix-only + // builtins (`node:test`) are listed with their prefix, so a bare `test` + // still names the package. + if (nodeBuiltinNames.has(specifier)) return null; // --provenance-sources: a registered specifier is NOT an npm import — // its attested source compiles as program modules (resolveProjectModule @@ -5582,18 +5586,16 @@ function analyzeEsmNamedImportLinks( if ( name === "default" && hasNodeTsDefaultInterfacePlaceholder7(dep) && - checker - .declarationsOf(resolved) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, - ) + resolved.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, + ) ) { return resolved; } diff --git a/packages/compiler/src/frontend/threads-import.ts b/packages/compiler/src/frontend/threads-import.ts index 12425c360..1944b2b81 100644 --- a/packages/compiler/src/frontend/threads-import.ts +++ b/packages/compiler/src/frontend/threads-import.ts @@ -11,7 +11,7 @@ export type ThreadsMember = "publish" | "sharesPublishedGraphs"; export function threadsImportOf(program: ts.Program, ident: ts.Identifier): ThreadsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index 248b3f922..2ed5c8c9b 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -474,7 +474,7 @@ class UnitResolver { let symbol = this.checker.getSymbolAtLocation(root); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = this.checker.getAliasedSymbol(symbol); - const decls = symbol === undefined ? [] : this.checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile); if (decl === undefined) { // CommonJS `exports`/`module` as values: the module object. diff --git a/packages/compiler/src/frontend/ts7/checker-cache.ts b/packages/compiler/src/frontend/ts7/checker-cache.ts index b9a455a0d..1613272e1 100644 --- a/packages/compiler/src/frontend/ts7/checker-cache.ts +++ b/packages/compiler/src/frontend/ts7/checker-cache.ts @@ -44,9 +44,6 @@ export class CheckerCache { readonly managedSymbols = new Set(); readonly intrinsics = new Map(); readonly tupleTypeAnswer = new Map(); - readonly declsOf = new Map(); - readonly valueDeclOf = new Map(); - readonly sigDeclOf = new Map(); readonly callSigsOf = new Map(); readonly ctorSigsOf = new Map(); readonly constantValueOf = new Map(); @@ -90,9 +87,6 @@ export class CheckerCache { this.managedSymbols.clear(); this.intrinsics.clear(); this.tupleTypeAnswer.clear(); - this.declsOf.clear(); - this.valueDeclOf.clear(); - this.sigDeclOf.clear(); this.callSigsOf.clear(); this.ctorSigsOf.clear(); this.constantValueOf.clear(); diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index d9b6e44a1..7b3647b2d 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -80,7 +80,7 @@ test("unchanged AST identities receive independent semantic answers after a depe const oldType = old.getTypeAtLocation(name); const oldSymbol = old.getSymbolAtLocation(name)!; expect(old.typeToString(oldType)).toBe("number"); - expect(old.valueDeclarationOf(oldSymbol)).toBe(declaration(root, "result")); + expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); const second = h.session.updateSnapshot({ @@ -165,9 +165,6 @@ test("all public query and prefetch entry points reject a disposed facade before facade.typeToString(type); facade.prefetchSourceFile(root); const calls: Record unknown> = { - declarationsOf: () => facade.declarationsOf(symbol), - valueDeclarationOf: () => facade.valueDeclarationOf(symbol), - signatureDeclaration: () => facade.signatureDeclaration(signature), getCallSignatures: () => facade.getCallSignatures(type), getConstructSignatures: () => facade.getConstructSignatures(type), getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index f3ef51a3d..41eb0d0b9 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -1,5 +1,4 @@ import { CheckerCache } from "./checker-cache.js"; -import { InternalCompilerError } from "../../errors.js"; /* The checker facade: 5.9.3-shaped TypeChecker methods over 7.0.2's sync * client, built around the survey's feasibility verdict. Naive per-call use * of the 7.0.2 client costs 0.1-0.3 ms of IPC per query; the census counted @@ -238,60 +237,6 @@ export class CheckerFacade { this.cache.dispose(); } - /* ── the symbol-declaration surface (phase 3) ───────────────────────── - * 7's Symbol carries declarations as NodeHandles (server references), - * where 5.9.3 handed out the nodes themselves. The lowering reads - * symbol.declarations/valueDeclaration pervasively, so the facade owns - * the resolve step (NodeHandle.resolve into the client AST — identity- - * stable, probe-verified) and memoizes per symbol. Requires the project - * the symbols came from (options.project — Ts7Program supplies it). */ - - private requireProject(): Project { - const project = this.options.project; - if (!project) - throw new InternalCompilerError( - "CheckerFacade built without a project cannot resolve declarations", - ); - return project; - } - - /** 5.9.3's symbol.declarations (never undefined here: 7 answers an empty - * array where 5.9.3 answered undefined — callers treat them alike). */ - declarationsOf(symbol: Ts7Symbol): readonly Node[] { - this.ensureActive(); - let decls = this.cache.declsOf.get(symbol); - if (decls === undefined) { - const project = this.requireProject(); - const resolved: Node[] = []; - for (const handle of symbol.declarations) { - const node = handle.resolve(project); - if (node !== undefined) resolved.push(node); - } - decls = resolved; - this.cache.declsOf.set(symbol, decls); - } - return decls; - } - - /** 5.9.3's symbol.valueDeclaration. */ - valueDeclarationOf(symbol: Ts7Symbol): Node | undefined { - this.ensureActive(); - if (this.cache.valueDeclOf.has(symbol)) return this.cache.valueDeclOf.get(symbol); - const decl = symbol.valueDeclaration?.resolve(this.requireProject()); - this.cache.valueDeclOf.set(symbol, decl); - return decl; - } - - /** 5.9.3's signature.getDeclaration() (undefined for synthesized - * signatures — same contract as sig.declaration there). */ - signatureDeclaration(signature: Signature): Node | undefined { - this.ensureActive(); - if (this.cache.sigDeclOf.has(signature)) return this.cache.sigDeclOf.get(signature); - const decl = signature.declaration?.resolve(this.requireProject()); - this.cache.sigDeclOf.set(signature, decl); - return decl; - } - /** 5.9.3's type.getCallSignatures(). */ getCallSignatures(type: Type): readonly Signature[] { this.ensureActive(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 095021922..4920c96ae 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -294,8 +294,12 @@ export class SemanticSymbol { readonly name: string; readonly flags: number; readonly checkFlags: number; - readonly declarations: SemanticNodeHandle[]; - readonly valueDeclaration: SemanticNodeHandle | undefined; + /** The declarations' node handles, as the server sends them. */ + readonly declarationHandles: SemanticNodeHandle[]; + readonly valueDeclarationHandle: SemanticNodeHandle | undefined; + private declarationNodes: readonly AstNode[] | undefined; + private valueDeclarationNode: AstNode | undefined; + private valueDeclarationResolved = false; private readonly parent: number | undefined; private readonly exportSymbol: number | undefined; private membersCache: Map | undefined; @@ -310,16 +314,40 @@ export class SemanticSymbol { this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name; this.flags = data.flags; this.checkFlags = data.checkFlags; - this.declarations = (data.declarations ?? []).map( + this.declarationHandles = (data.declarations ?? []).map( (handle) => new SemanticNodeHandle(handle, canonicalProject), ); - this.valueDeclaration = data.valueDeclaration + this.valueDeclarationHandle = data.valueDeclaration ? new SemanticNodeHandle(data.valueDeclaration, canonicalProject) : undefined; this.parent = data.parent; this.exportSymbol = data.exportSymbol; } + /** The declaration nodes, resolved in the symbol's project. A handle whose + * file the project doesn't have resolves to nothing and is left out. */ + get declarations(): readonly AstNode[] { + this.canonicalProject.ensureActive(); + if (this.declarationNodes === undefined) { + const nodes: AstNode[] = []; + for (const handle of this.declarationHandles) { + const node = handle.resolve(); + if (node !== undefined) nodes.push(node); + } + this.declarationNodes = nodes; + } + return this.declarationNodes; + } + + get valueDeclaration(): AstNode | undefined { + this.canonicalProject.ensureActive(); + if (!this.valueDeclarationResolved) { + this.valueDeclarationNode = this.valueDeclarationHandle?.resolve(); + this.valueDeclarationResolved = true; + } + return this.valueDeclarationNode; + } + getParent(): SemanticSymbol | undefined { return this.canonicalProject.fetchSymbol(this.id, "getParentOfSymbol", this.parent); } @@ -603,7 +631,10 @@ export class SemanticType { export class SemanticSignature { readonly id: number; readonly flags: number; - readonly declaration: SemanticNodeHandle | undefined; + /** The declaration's node handle, as the server sends it. */ + readonly declarationHandle: SemanticNodeHandle | undefined; + private declarationNode: AstNode | undefined; + private declarationResolved = false; constructor( private readonly data: SignatureResponse, @@ -611,11 +642,22 @@ export class SemanticSignature { ) { this.id = data.id; this.flags = data.flags; - this.declaration = data.declaration + this.declarationHandle = data.declaration ? new SemanticNodeHandle(data.declaration, project) : undefined; } + /** The declaration node, resolved in the signature's project; undefined + * for a signature the checker synthesized. */ + get declaration(): AstNode | undefined { + this.project.ensureActive(); + if (!this.declarationResolved) { + this.declarationNode = this.declarationHandle?.resolve(); + this.declarationResolved = true; + } + return this.declarationNode; + } + getTypeParameters(): SemanticType[] { return this.project.fetchTypes( this.id, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 32e8c91c8..09ec50fb2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -125,6 +125,19 @@ export function jsOpenObjectType( : type; } +/** The WebCrypto interfaces whose values are checked-dynamic: the crypto + * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries + * (scr_webcrypto.c). */ +export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ + "Crypto", + "SubtleCrypto", + "CryptoKey", + "Algorithm", + "KeyAlgorithm", + "HmacImportParams", + "HmacKeyAlgorithm", +]); + export const FETCH_AMBIENT_TYPES = [ "Response", "Request", @@ -931,13 +944,11 @@ export function isWebAssemblyType( ): boolean { const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (!symbol) return false; - return ctx.checker - .declarationsOf(symbol) - .some( - (d) => - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), - ); + return symbol.declarations.some( + (d) => + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), + ); } /** True when a type contains a record anywhere (the trigger for the @@ -1209,7 +1220,7 @@ export function isLegacyUrlSymbol( ctx: Pick, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { if (!ctx.isStdlibFile(d.getSourceFile())) continue; let family = false; for (let n: ts.Node | undefined = d; n; n = n.parent) { @@ -1246,7 +1257,7 @@ export function isOsConstantsSymbol( types = false, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const alias = types && ts.isTypeAliasDeclaration(d) && d.name.text === "SignalConstants"; const signals = !types && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "signals"; @@ -1277,14 +1288,12 @@ function isWorkerHandleSymbol( symbol?.name === "MessagePort" || symbol?.name === "MessageChannel" || symbol?.name === "BroadcastChannel") && - ctx.checker - .declarationsOf(symbol) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "worker_threads"), - ) + symbol.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "worker_threads"), + ) ); } @@ -1298,9 +1307,7 @@ function isReceiveMessageResult( let found = false; for (const member of members) { if (member.flags & ts.TypeFlags.Undefined) continue; - const decl = member.getSymbol() - ? ctx.checker.declarationsOf(member.getSymbol()!)[0] - : undefined; + const decl = member.getSymbol() ? member.getSymbol()!.declarations[0] : undefined; let fn: ts.Node | undefined = decl?.parent; while (fn && ts.isUnionTypeNode(fn)) fn = fn.parent; if ( @@ -1340,6 +1347,31 @@ export function isWorkerHandleType( return handle; } +/** The return annotation of an `async *` declaration spelled with the + * protocol interfaces the generator object implements — + * `AsyncIterator`, `AsyncIterableIterator` or `AsyncIterable` (pi's + * `async *[Symbol.asyncIterator](): AsyncIterator`): the body always + * produces an async generator, so the declaration maps exactly like an + * `AsyncGenerator` annotation. Values of these types + * elsewhere stay unmapped (they can hold any object implementing the + * protocol). Null when the annotation is not one of them. */ +export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): IrType | null { + const { checker } = ctx; + const symbol = type.getSymbol(); + if ( + !symbol || + !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(symbol.name) || + !symbol.declarations + .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + ) + return null; + const args = checker.getTypeArguments(type as ts.TypeReference); + const channels = genChannels(args[0], args[1], args[2], ctx); + if (!channels) return null; + if (!genResultRecord(channels.yieldT, channels.retT, ctx.shapes, ctx.unions)) return null; + return { kind: "generator", async: true, ...channels }; +} + /** The native fetch handles (Headers, Request, Response), optionally in * arrays or with null/undefined: checked-dynamic handles wherever they are * stored, like the worker handles above. */ @@ -1359,13 +1391,11 @@ export function isFetchHandleType( if ( !symbol || (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || - !ctx.checker - .declarationsOf(symbol) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + !symbol.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return false; } @@ -1440,15 +1470,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); - if (intrinsic?.name === "globalThis" && checker.declarationsOf(intrinsic).length === 0) - return DYN; + if (intrinsic?.name === "globalThis" && intrinsic.declarations.length === 0) return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0; const classInstance = (intrinsic.flags & ts.SymbolFlags.Class) !== 0 && type.isClassOrInterface(); if ( (objectLiteral || classInstance) && - checker.declarationsOf(intrinsic).some((decl) => ctx.jsDynamicDeclaration!(decl)) + intrinsic.declarations.some((decl) => ctx.jsDynamicDeclaration!(decl)) ) return DYN; } @@ -1466,7 +1495,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const directAlias = type.getAliasSymbol(); if ( directAlias?.name === "ExecFileException" && - checker.declarationsOf(directAlias).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + directAlias.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ) { return { kind: "object", className: "%Error" }; } @@ -1476,14 +1505,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const execException = type.getSymbol(); if ( execException?.name === "ExecException" && - checker - .declarationsOf(execException) - .some( - (decl) => - ts.isInterfaceDeclaration(decl) && - ctx.isStdlibFile(decl.getSourceFile()) && - isDeclaredInAmbientModule(decl, "child_process"), - ) + execException.declarations.some( + (decl) => + ts.isInterfaceDeclaration(decl) && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "child_process"), + ) ) { return { kind: "object", className: "%Error" }; } @@ -1579,7 +1606,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const errorOptionsSymbol = widened.getSymbol(); if ( errorOptionsSymbol?.name === "ErrorOptions" && - checker.declarationsOf(errorOptionsSymbol).some((d) => ctx.isStdlibFile(d.getSourceFile())) + errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -1611,17 +1638,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( parseArgsSym && PARSE_ARGS_DYN_TYPES.has(parseArgsSym.name) && - checker.declarationsOf(parseArgsSym).some(belongs) + parseArgsSym.declarations.some(belongs) ) return DYN; // ReturnType and instantiated conditional types can erase the alias. // Require every member to retain the util declaration provenance so // unrelated records with similar property names keep their own layout. const members = checker.getPropertiesOfType(widened); - if ( - members.length > 0 && - members.every((member) => checker.declarationsOf(member).some(belongs)) - ) + if (members.length > 0 && members.every((member) => member.declarations.some(belongs))) return DYN; } // The lib's BOXED wrapper interfaces used as TYPES (`const n: Number = @@ -1634,7 +1658,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( boxSym && (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") && - checker.declarationsOf(boxSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + boxSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL; } @@ -1657,7 +1681,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctorSym.name === "StringConstructor" || ctorSym.name === "NumberConstructor" || ctorSym.name === "BooleanConstructor") && - checker.declarationsOf(ctorSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + ctorSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return funcOf( [STRING], @@ -1769,7 +1793,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const classSym = widened.getSymbol(); const name = classSym?.name; if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { - for (const d of checker.declarationsOf(classSym!)) { + for (const d of classSym!.declarations) { if (!ts.isClassDeclaration(d)) continue; const synthetic = name === "LifetimeRegion" @@ -1791,7 +1815,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // types-only package), so it maps structurally like a local declaration. // Values the package itself would produce stay refused at their import. const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => { - const decls = sym ? checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; return decls && decls.length > 0 && decls.every((d) => { @@ -2005,7 +2029,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Class instances name a program class. The same symbol with construct // signatures names its static side, mapped to classval below. const widenedSym = widened.getSymbol(); - const classDecl = widenedSym ? checker.valueDeclarationOf(widenedSym) : undefined; + const classDecl = widenedSym ? widenedSym.valueDeclaration : undefined; if ( classDecl && (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) && @@ -2133,9 +2157,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const psym = widened.getSymbol(); const isStdlibInterface = (name: string): boolean => psym?.name === name && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); // The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class // hierarchy (provenance, not the name — a user's own `class Error` // resolved through the class-instance branch above, and a user @@ -2149,9 +2173,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and in @types/node), not as a lib interface. if ( psym?.name === "AssertionError" && - checker - .declarationsOf(psym) - .some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) ) return { kind: "object", className: "%AssertionError" }; // The lib's `Object` interface is a TOP type: tsc admits every @@ -2169,16 +2191,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // included, while unrelated user interfaces named Module stay structural. if ( (psym?.name === "ScriptcModule" || psym?.name === "Module") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (psym.name === "ScriptcModule" || - isDeclaredInAmbientNamespace(d, "NodeJS") || - isDeclaredInAmbientModule(d, "module")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (psym.name === "ScriptcModule" || + isDeclaredInAmbientNamespace(d, "NodeJS") || + isDeclaredInAmbientModule(d, "module")), + ) ) { return F64; } @@ -2191,14 +2211,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // et al. extend) is the same runtime value. Provenance-checked. if ( psym?.name === "EventEmitter" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), + ) ) { return { kind: "object", className: RUNTIME_EMITTER_CLASS }; } @@ -2208,15 +2226,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // enclosing ambient module, like net.Server. if ( psym?.name === "Interface" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return F64; } @@ -2225,29 +2241,25 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // code member). if ( psym?.name === "Key" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return DYN; } // readline/promises.Readline: an f64 handle the same way. if ( psym?.name === "Readline" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "readline/promises"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "readline/promises"), + ) ) { return F64; } @@ -2259,14 +2271,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and @types/node's class Channel. if ( psym?.name === "Channel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } @@ -2275,14 +2285,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // process-lived; contexts ride the fiber machinery). if ( psym?.name === "AsyncLocalStorage" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "async_hooks"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "async_hooks"), + ) ) { return F64; } @@ -2290,21 +2298,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // runtime's tracing registry (five event channels per entry). if ( psym?.name === "TracingChannel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } // Node names this result; the ambient WHATWG declaration uses a type literal. const encodeResult = psym && - checker.declarationsOf(psym).some((decl) => { + psym.declarations.some((decl) => { if (!ctx.isStdlibFile(decl.getSourceFile())) return false; if (ts.isInterfaceDeclaration(decl) && decl.name.text === "EncodeIntoResult") return true; const parent = decl.parent; @@ -2331,13 +2337,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Their private encoding slot is omitted from enumeration and JSON. if ( (psym?.name === "TextEncoder" || psym?.name === "TextDecoder") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2367,13 +2371,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record shape never surfaces. if ( psym?.name === "StringDecoder" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2393,7 +2395,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record-holding binding forms compile. User-declared literals with // these member names keep the ordinary record path (their declarations // are not in declaration files). - const promiseDeclarations = psym ? checker.declarationsOf(psym) : []; + const promiseDeclarations = psym ? psym.declarations : []; if ( flags & ts.TypeFlags.Object && promiseDeclarations.length > 0 && @@ -2459,9 +2461,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "ReadableStreamReadResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = widened.getAliasTypeArguments()?.[0]; const elem = @@ -2474,9 +2476,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym && (psym.name === "ReadableStreamReadValueResult" || psym.name === "ReadableStreamReadDoneResult") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const elem = @@ -2488,8 +2490,22 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // whose getters and methods dispatch through the handle. if ( (psym?.name === "SocketAddress" || psym?.name === "BlockList") && - checker - .declarationsOf(psym) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) { + return DYN; + } + // WebCrypto: the crypto global and its subtle object are dynamic + // objects, CryptoKey values are native handles (scr_webcrypto.c), and + // the algorithm dictionaries the methods take and keys report are plain + // checked-dynamic objects. + if ( + psym && + WEBCRYPTO_DYN_TYPES.has(psym.name) && + psym.declarations .some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && @@ -2503,13 +2519,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // that export() returns is a plain checked-dynamic object. if ( (psym?.name === "KeyObject" || psym?.name === "JsonWebKey") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2518,17 +2532,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (FETCH_AMBIENT_TYPES as readonly string[]).includes(psym.name) && !( psym.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS")) - ) && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) + ) && + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2544,9 +2556,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "Disposable" || psym?.name === "AsyncDisposable" || psym?.name === "DisposableStack") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2556,7 +2568,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && CIPHER_TYPE_NAMES.has(psym.name) && - checker.declarationsOf(psym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -2684,13 +2696,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the same symbol); provenance-checked like URL. if ( psym?.name === "Buffer" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return bytesOf("u8"); } @@ -2701,13 +2711,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record/class like any other. if ( psym?.name === "URL" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "url" }; } @@ -2716,13 +2724,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // URLSearchParams` in the "url" module). Provenance-checked like URL. if ( psym?.name === "URLSearchParams" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "searchParams" }; } @@ -2730,13 +2736,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declarations' interface. Provenance-checked like URL. if ( psym?.name === "Stats" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "stats" }; } @@ -2746,14 +2750,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "StatsFs" || psym?.name === "BigIntStatsFs" || psym?.name === "StatFsOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) return DYN; if (isWorkerHandleSymbol(psym, ctx)) return DYN; @@ -2762,14 +2764,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node declare it in "fs/promises". if ( psym?.name === "FileHandle" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs/promises"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs/promises"), + ) ) { return { kind: "fileHandle" }; } @@ -2777,14 +2777,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node. if ( psym?.name === "Dir" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "dir" }; } @@ -2794,13 +2792,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like Stats. if ( psym?.name === "SpawnSyncReturns" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "spawnRes" }; } @@ -2812,14 +2808,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym.name === "ChildProcess" || psym.name === "ChildProcessWithoutNullStreams" || psym.name === "ChildProcessByStdio") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + ) ) { return { kind: "child" }; } @@ -2832,7 +2826,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // site. if ( (psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") && - checker.declarationsOf(psym).some( + psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && ctx.isStdlibFile(d.getSourceFile()) && @@ -2858,14 +2852,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // "tls"`, and the shipped fallback mirrors it. if ( psym?.name === "SecureContext" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "tls"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "tls"), + ) ) { return { kind: "secureCtx" }; } @@ -2873,14 +2865,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked values so presence probes and repeated getter reads match Node. if ( psym?.name === "ScryptOptions" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) return DYN; // crypto.Hash / crypto.Hmac: native incremental digest handles. Keep @@ -2888,14 +2878,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // retain their ordinary structural/class representation. if ( (psym?.name === "Hash" || psym?.name === "Hmac") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return psym.name === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; } @@ -2905,14 +2893,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // mirrors it. if ( psym?.name === "FSWatcher" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "fsWatcher" }; } @@ -2935,25 +2921,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // scalar; the lowered surface is write(string). if ( (psym?.name === "WriteStream" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "tty") || - (isDeclaredInAmbientModule(d, "process") && - isDeclaredInAmbientNamespace(d, "NodeJS"))), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "tty") || + (isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))), + )) || (psym?.name === "WritableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return PROCSTREAM_T; } @@ -2963,24 +2944,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // of the broad @types class fence where they are constructed. if ( (psym?.name === "ChildStdin" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - )) || + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + )) || (psym?.name === "Writable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - )) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + )) ) { return { kind: "childWriter" }; } @@ -2994,15 +2971,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { if ( psym?.name === rec.lib && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + ) ) { return { kind: "object", className: irName }; } @@ -3012,15 +2987,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && ZLIB_STREAM_MODES.has(psym.name) && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "zlib"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "zlib"), + ) ) { return { kind: "object", className: "%Transform" }; } @@ -3029,37 +3002,31 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // (scr_fs_stream.c). if ( (psym?.name === "ReadStream" || psym?.name === "WriteStream") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "object", className: psym.name === "ReadStream" ? "%Readable" : "%Writable" }; } if ( (psym?.name === "Readable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "stream"), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "stream"), + )) || (psym?.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return { kind: "childStream" }; } @@ -3069,14 +3036,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // inside `declare module "dgram"`. if ( psym?.name === "Socket" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "dgram"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "dgram"), + ) ) { return { kind: "dgramSocket" }; } @@ -3088,14 +3053,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // both spellings). if ( (psym?.name === "TestContext" || psym?.name === "SuiteContext") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { return { kind: "testCtx" }; } @@ -3104,14 +3067,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // { %call: F, mock } record through mapHybridCallableIntersection. if ( (psym?.name === "MockFunctionContext" || psym?.name === "MockFunctionCall") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { const fnTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const fn = fnTs ? mapType(fnTs, ctx) : null; @@ -3127,9 +3088,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like the rest. if ( psym?.name === "ErrnoException" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "object", className: "%Error" }; } @@ -3141,14 +3102,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Agent extends http's; both live in their ambient modules). if ( psym?.name === "Agent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), + ) ) { return { kind: "dyn" }; } @@ -3157,14 +3116,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // rule). if ( (psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; } @@ -3180,14 +3137,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "Http2SecureServer" || psym?.name === "Http2ServerRequest" || psym?.name === "Http2ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2SecureServer" ? { kind: "netServer" } @@ -3206,14 +3161,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ServerHttp2Session" || psym?.name === "ClientHttp2Stream" || psym?.name === "ServerHttp2Stream") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2Server" ? { kind: "netServer" } @@ -3225,14 +3178,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // like IncomingMessage. if ( psym?.name === "ClientRequest" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return { kind: "httpClientReq" }; } @@ -3246,9 +3197,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and interns the SAME shape. if ( psym?.name === "ProcessEnv" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const value = withUndefinedArm(STRING, ctx.unions); if (!value) return null; @@ -3338,9 +3289,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "IteratorResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const targs = widened.getAliasTypeArguments() ?? []; const channels = genChannels(targs[0], targs[1], undefined, ctx); @@ -3475,14 +3426,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ReadOptions" || psym?.name === "ReadOptionsWithBuffer" || psym?.name === "WriteOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), + ) ) { const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { @@ -3506,16 +3455,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "FileWritevResult" || psym?.name === "ReadVResult" || psym?.name === "WriteVResult") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || - ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && - isDeclaredInAmbientModule(d, "fs"))), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || + ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && + isDeclaredInAmbientModule(d, "fs"))), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length !== 1) return null; @@ -3573,13 +3520,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // provenance-checked, the Buffer/URL pattern). if ( psym?.name === "Dirent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length > 1) return null; @@ -3627,18 +3572,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }); if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ts.isPropertySignature(d.parent) && - ts.isIdentifier(d.parent.name) && - d.parent.name.text === "times" && - ts.isInterfaceDeclaration(d.parent.parent) && - d.parent.parent.name.text === "CpuInfo" && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ts.isPropertySignature(d.parent) && + ts.isIdentifier(d.parent.name) && + d.parent.name.text === "times" && + ts.isInterfaceDeclaration(d.parent.parent) && + d.parent.parent.name.text === "CpuInfo" && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return cpuTimes(); if (isStdlibInterface("CpuInfo")) { @@ -3706,16 +3649,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // unmapped. if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "child_process") || - isDeclaredInAmbientModule(d, "util") || - isDeclaredInAmbientModule(d, "dns")), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "child_process") || + isDeclaredInAmbientModule(d, "util") || + isDeclaredInAmbientModule(d, "dns")), + ) ) { const props = checker.getPropertiesOfType(widened); const names = @@ -3755,14 +3696,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // internal-slot object shows {} — the Dirent %dtype edge's cousin. if ( psym?.name === "X509Certificate" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return { kind: "record", @@ -3790,13 +3729,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "PerformanceEntry" || psym?.name === "PerformanceMark" || psym?.name === "PerformanceMeasure") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) }; } @@ -4049,10 +3986,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { flags & ts.TypeFlags.Object && callSigs.length === 0 && psym !== undefined && - checker.declarationsOf(psym).length > 0 && - checker - .declarationsOf(psym) - .every((d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.length > 0 && + psym.declarations.every( + (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()), + ) ) { const CHILD_SURFACE = new Set([ "pid", @@ -4298,7 +4235,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // convention. Detected by the param-count mismatch against the // signature's own declaration, excluding an erased `this` parameter. { - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if ( @@ -4318,7 +4255,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null let typedRest = false; let dynRest = false; for (const p of sig.getParameters()) { - const decl = checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; // arity ABI selected by lambdaSignature. Treating an inferred any[] @@ -4354,9 +4291,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && - checker - .declarationsOf(constructorSymbol) - .some((site) => ctx.isStdlibFile(site.getSourceFile())); + constructorSymbol.declarations.some((site) => ctx.isStdlibFile(site.getSourceFile())); if ( !nativeExecutor && decl && @@ -4410,7 +4345,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // like declaredReturnType does for declarations. JS null/undefined // returns can read mutable fields, so callable values use the same // checked-native return ABI as their implementations. - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -4551,7 +4486,7 @@ function mapClassView( bases = ts.constituentTypes(type); } else { const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4589,7 +4524,7 @@ function mapClassView( let representation: IrType | undefined; for (const base of bases) { const symbol = base.getSymbol(); - const declaration = symbol ? checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const classBase = declaration && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && @@ -4613,8 +4548,8 @@ function mapClassView( const original = checker.getPropertyOfType(baseType, property.name); if (!original) return null; if (original === property) continue; - const originalDeclarations = checker.declarationsOf(original); - const declarations = checker.declarationsOf(property); + const originalDeclarations = original.declarations; + const declarations = property.declarations; // Instantiating an inherited member can create a new symbol without // changing its declaration or ABI. Only refinements need validation. if ( @@ -4652,7 +4587,7 @@ function mapCollectionView( if (!type.isTypeReference()) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4705,16 +4640,14 @@ function mapCollectionView( const native = baseSymbol && ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(baseSymbol.name) && - checker - .declarationsOf(baseSymbol) - .some((decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile())); + baseSymbol.declarations.some( + (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile()), + ); const mapped = native ? mapType(base, baseCtx) : mapCollectionView(base, baseCtx, seen); if (mapped?.kind !== "map" && mapped?.kind !== "set") return null; const hasType = funcOf([mapped.kind === "map" ? mapped.key : mapped.elem], BOOL); for (const property of checker.getPropertiesOfType(type)) { - const own = checker - .declarationsOf(property) - .filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); + const own = property.declarations.filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); if (own.length === 0) continue; if ( property.name !== "has" || @@ -4864,8 +4797,8 @@ export function underConstAssertion(n: ts.Node): boolean { * panic repair): a property symbol declared by a property assignment whose * initializer is an EMPTY array literal inside a const assertion — its * type is provably `readonly []`, whatever the panicked query answered. */ -function constAssertedEmptyArrayProp(p: ts.Symbol, ctx: TypeMapperCtx): boolean { - const d = ctx.checker.valueDeclarationOf(p); +function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { + const d = p.valueDeclaration; if (d === undefined || !ts.isPropertyAssignment(d)) return false; const init = d.initializer; // `a: [] as const` pins the field type by itself, wherever the literal sits. @@ -4888,9 +4821,9 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound) return null; @@ -4954,7 +4887,7 @@ function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null if (!resolveTypeParamTs || !isMappedShape(widened)) return null; const symbol = widened.getSymbol(); const node = symbol - ? checker.declarationsOf(symbol).find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) + ? symbol.declarations.find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) : undefined; const constraint = node?.typeParameter.constraint; if ( @@ -5040,9 +4973,7 @@ function evalMappedTemplate( const alias = type.getAliasSymbol(); const aliasArgs = type.getAliasTypeArguments(); const aliasDecl = alias - ? checker - .declarationsOf(alias) - .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) + ? alias.declarations.find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) : undefined; if ( aliasDecl?.typeParameters?.nodes && @@ -5100,9 +5031,7 @@ function matchInferReference( const want = wantArgs[i]!; const symbol = want.flags & ts.TypeFlags.TypeParameter ? want.getSymbol() : undefined; const isInfer = symbol - ? checker - .declarationsOf(symbol) - .some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) + ? symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) : false; if (isInfer && symbol) inferred.set(symbol, gotArgs[i]!); else if (want !== gotArgs[i]) return undefined; @@ -5136,9 +5065,9 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound || bound.kind !== "record") return null; @@ -5391,7 +5320,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType "AggregateError", "SuppressedError", ].includes(symbol.name) && - ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ); }); if (!hasError) return null; @@ -5533,12 +5462,12 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null if (!isMappedShape(type)) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !CONTRACT_TYPES_FILE.test(decl.getSourceFile().fileName)) return null; const props = checker.getPropertiesOfType(type); const seen = new Set(); for (const prop of props) { - const owner = checker.declarationsOf(prop)[0]?.parent; + const owner = prop.declarations[0]?.parent; if (owner === undefined || seen.has(owner)) continue; seen.add(owner); let source: ts.Type | null = null; @@ -5605,21 +5534,19 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { } if (isMappedShape(t)) return true; const tSym = t.getSymbol(); - const decls = tSym ? checker.declarationsOf(tSym) : undefined; + const decls = tSym ? tSym.declarations : undefined; if (!decls || decls.length === 0) { // Inferred destructuring parameters can have a synthesized anonymous // shape with no declaration or member symbols to trace to a source. return checker .getPropertiesOfType(t) .every((property) => - checker - .declarationsOf(property) - .every( - (decl) => - !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()) || - packageTypeFile(decl.getSourceFile(), ctx), - ), + property.declarations.every( + (decl) => + !decl.getSourceFile().isDeclarationFile || + ctx.isExternalTypeFile(decl.getSourceFile()) || + packageTypeFile(decl.getSourceFile(), ctx), + ), ); } return !decls.some((d) => { @@ -5728,7 +5655,7 @@ export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeCheck ) return false; return sigs.every((sig) => { - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; if (decl === undefined) return false; if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) return false; @@ -5852,7 +5779,7 @@ function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { }; const mapped = mapCallSignature(sig, sigCtx); if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; const which = spelled ? `overload '${spelled}'` : "one overload"; if (mapped?.kind === "func") @@ -6124,7 +6051,7 @@ function mapRecordTypeInner( // representable. const anonSym = widened.getSymbol(); const anonymousEmpty = - (anonSym === undefined || checker.declarationsOf(anonSym).length === 0) && + (anonSym === undefined || anonSym.declarations.length === 0) && !widened.isIntersectionType() && indexValue === undefined && checker.getPropertiesOfType(widened).length === 0; @@ -6163,10 +6090,7 @@ function mapRecordTypeInner( indexValue === undefined && checker.getCallSignatures(widened).length === 0 && checker.getConstructSignatures(widened).length === 0 && - !( - anonSym !== undefined && - checker.declarationsOf(anonSym).some((d) => ts.isObjectLiteralExpression(d)) - ) + !(anonSym !== undefined && anonSym.declarations.some((d) => ts.isObjectLiteralExpression(d))) ) { return DYN; } @@ -6187,7 +6111,7 @@ function mapRecordTypeInner( // their existing paths (the CJS export-table narrowing and the // checked-dynamic fallback), and .d.ts declarations stay out with // the rest of the lib's type worlds. - const decls = checker.declarationsOf(p); + const decls = p.declarations; const getDecl = decls.find((d) => ts.isGetAccessorDeclaration(d)); const setDecl = decls.find((d) => ts.isSetAccessorDeclaration(d)); const accessorOwned = @@ -6237,7 +6161,7 @@ function mapRecordTypeInner( // declarations and pass. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) @@ -6268,7 +6192,7 @@ function mapRecordTypeInner( if ( pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0 && - constAssertedEmptyArrayProp(p, ctx) + constAssertedEmptyArrayProp(p) ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } @@ -6437,9 +6361,9 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): const container = psym !== undefined && Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, psym.name) && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ? psym.name : undefined; if (container !== undefined) { @@ -6570,7 +6494,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if (callSigs.length === 1) { const sig = callSigs[0]!; if (sig.getTypeParameters().length > 0) return null; - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; if ( sigDecl !== undefined && ts.isFunctionLike(sigDecl) && @@ -6620,7 +6544,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx // any one member's type — that story stays with the residual fence. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index c395da593..99c97dded 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -25,7 +25,7 @@ function workerNamespace(program: ts.Program, expression: ts.Expression): boolea if (isBuiltinNamespaceImport(program, expression, "worker_threads")) return true; const checker = program.getTypeChecker(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; return ( declaration !== undefined && ts.isVariableDeclaration(declaration) && @@ -53,7 +53,7 @@ export function isWorkerConstructor(program: ts.Program, expression: ts.Expressi if (isBuiltinMemberImport(program, expression, "worker_threads", "Worker")) return true; const checker = program.getTypeChecker(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; if ( declaration && ts.isBindingElement(declaration) && diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 002212154..7d8d88889 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -528,6 +528,8 @@ async function compileExecutableNative( fileHandle: features.fileHandle, fetch: features.fetch, ...(features.eventTarget ? { eventTarget: true } : {}), + ...(features.warningReplace ? { warningReplace: true } : {}), + ...(features.stdioExtended ? { stdioExtended: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, @@ -555,6 +557,7 @@ async function compileExecutableNative( tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), ...(ffi === null ? {} diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index b32e0991f..697a185a5 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2637,6 +2637,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as + * one dyn array and answers a promise that argument and validation + * failures reject. CryptoKey values are native handles. */ + "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, + "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, @@ -4200,6 +4208,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * warnings and DeprecationWarning emissions read their truthiness. */ "process.deprecationFlag": { argTypes: [F64], result: DYN }, "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, + "process.setEmitWarning": { argTypes: [DYN], result: VOID }, /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 * handles; contexts are immutable fiber-carried snapshots (spawned * fibers inherit the spawner's — Node's init-time capture). run/exit diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 71750713b..b84c0594e 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2606,18 +2606,8 @@ export type IrExpr = | { kind: "logical"; op: "&&" | "||"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strConcat"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strEq"; negated: boolean; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } - /** String ordering. Ordinary source comparisons omit `utf16` and retain - * scriptc's documented code-point order; the default Array sort comparator - * sets it to request ECMAScript's UTF-16 code-unit order. */ - | { - kind: "strCmp"; - op: IrStrCmpOp; - left: IrExpr; - right: IrExpr; - utf16?: boolean; - type: IrType; - loc: SrcLoc; - } + /** String ordering in ECMAScript's UTF-16 code-unit order. */ + | { kind: "strCmp"; op: IrStrCmpOp; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } /** f64|bool → string, JS-exact (Number::toString / "true"/"false"). * Union operands dispatch through the per-union ToString helper (arms * fenced to unit/string/f64/bool by the frontend); a CAUGHT operand is diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 0c57d971c..9bab3e79f 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -17,6 +17,12 @@ export interface RuntimeFeatures { fetch: boolean; eventTarget: boolean; processEvents: boolean; + /** process.emitWarning is assigned (process.setEmitWarning). Declared + * in the literal's order: JSON.stringify of the features record must + * match between the Node-run and self-hosted compilers. */ + warningReplace: boolean; + /** stdin setEncoding / string data listeners / stdio 'resize' events. */ + stdioExtended: boolean; emitter: boolean; stream: boolean; zlib: boolean; @@ -52,6 +58,8 @@ export interface RuntimeFeatures { tls: boolean; tlsCa: boolean; cipher: boolean; + /** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */ + webcrypto: boolean; } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ @@ -78,6 +86,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.emitWarningText", "process.deprecationFlag", "process.setDeprecationFlag", + "process.setEmitWarning", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). "perf.mark", @@ -353,18 +362,41 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } case "object": { // A boxed class instance exposes its fields as dyn values (Object.keys, - // spread, inspection), boxing a promise any field holds. + // spread, inspection), boxing a promise any field holds. It + // materializes inherited fields too, and the instance can be any + // subclass. const key = `class:${t.className}`; if (seen.has(key)) return false; seen.add(key); - const def = tables.classes.get(t.className); - return def?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false; + const related = (def: IrClassDef): boolean => { + for ( + let c: IrClassDef | undefined = def; + c; + c = c.base ? tables.classes.get(c.base) : undefined + ) + if (c.name === t.className) return true; + return false; + }; + for (const def of tables.classes.values()) { + if (!related(def)) continue; + for ( + let c: IrClassDef | undefined = def; + c; + c = c.base ? tables.classes.get(c.base) : undefined + ) + if (c.fields.some((f) => boxesPromise(f.type, tables, seen))) return true; + } + return false; } default: return false; } } +/** Names that reach the extended process-stream surface (the + * SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */ +const STDIO_EXTENDED_NAMES: ReadonlySet = new Set(["setEncoding", "resize"]); + function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures { const features: RuntimeFeatures = { regex: false, @@ -378,6 +410,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, ), processEvents: false, + warningReplace: false, + stdioExtended: false, emitter: mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), @@ -411,6 +445,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -427,6 +462,14 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run if (REGEX_LIB_FNS.has(fn)) features.regex = true; if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; + if (fn === "process.setEmitWarning") features.warningReplace = true; + // A typed string 'data' listener takes setEncoding-decoded chunks. + if ( + fn === "stdin.onData" && + node.args[0]?.type.kind === "func" && + node.args[0].type.params[0]?.kind === "string" + ) + features.stdioExtended = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; @@ -517,6 +560,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.dynAsync = true; features.inspect = true; } + // crypto.subtle and CryptoKey: scr_webcrypto.c. + if (fn.startsWith("webcrypto.")) features.webcrypto = true; } else { switch (node.kind) { case "regexLit": @@ -534,6 +579,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run break; case "dynInvoke": features.dynInvoke = true; + if (STDIO_EXTENDED_NAMES.has(node.method)) features.stdioExtended = true; + break; + // The process stream object's setEncoding and 'resize' members are + // reached by name (dynamic member calls and event-name strings). + case "strLit": + if (STDIO_EXTENDED_NAMES.has(node.value)) features.stdioExtended = true; break; case "awaitExpr": if (node.type.kind === "dyn") features.dynAsync = true; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index fa3b693a3..4a961dfca 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5103,7 +5103,8 @@ function validateFunction( let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1; if (ok && cbT?.kind === "func" && cbT.params.length === 1) { const p = cbT.params[0]!; - if (e.fn === "stdin.onData") ok = p.kind === "bytes" && p.elem === "u8"; + if (e.fn === "stdin.onData") + ok = (p.kind === "bytes" && p.elem === "u8") || p.kind === "string"; else if (e.fn === "stdin.onError") ok = p.kind === "object" && p.className === "%Error"; else ok = p.kind === "f64"; } diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 3c3855e5c..2d1bc85d4 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -51,6 +51,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["dns.", "the node:dns surface"], ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], + ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], ["test.", "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index c0d89bbbf..7d00acbc8 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -100,6 +100,7 @@ async function main(): Promise { nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, options.optimization ?? "release", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index bb901a674..54100a413 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -155,6 +155,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tls", "tlsCa", "cipher", + "webcrypto", ].every((name) => typeof features[name] === "boolean"); } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 87533066b..6ca84af6f 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -76,6 +76,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( youngNursery: false, eventTarget: false, dynTier: false, + warningReplace: false, + stdioExtended: false, regex: true, copying: false, textDecoderLegacy: false, @@ -105,6 +107,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tls: false, tlsCa: false, cipher: false, + webcrypto: false, nativeFetch: false, netEffective: false, httpEffective: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index ab1c3405e..01d12dedc 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5676,6 +5676,20 @@ "code": "SC2020", "note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)" }, + { + "id": "stdlib.webcrypto.CryptoKey", + "kind": "stdlib", + "name": "CryptoKey", + "status": "static", + "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + }, + { + "id": "stdlib.webcrypto.subtle", + "kind": "stdlib", + "name": "globalThis.crypto.subtle", + "status": "static", + "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + }, { "id": "syntax.async-generators", "kind": "syntax", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json new file mode 100644 index 000000000..58a162c8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5639-union-arm-by-discriminant.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json new file mode 100644 index 000000000..091c37141 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5640-generic-structural-class-narrowing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json new file mode 100644 index 000000000..365709f61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6023-string-compare-utf16.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json new file mode 100644 index 000000000..ff4454b22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6080-take-then-flatmap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json new file mode 100644 index 000000000..ac7be42ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-process-emit-warning-assign.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6231-process-emit-warning-assign.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json new file mode 100644 index 000000000..8adab38b7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-process-builtin-module-typed/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6232-process-builtin-module-typed/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json new file mode 100644 index 000000000..89f131b0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-for-await-class-async-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6233-for-await-class-async-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json new file mode 100644 index 000000000..e7e18883a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-weak-collection-class-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6234-weak-collection-class-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json new file mode 100644 index 000000000..eee3db81a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-bare-builtin-shadowed-package/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs", + "/tests/corpus/6235-bare-builtin-shadowed-package/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json new file mode 100644 index 000000000..c7546dd63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-stdin-encoding-tty-probes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6236-stdin-encoding-tty-probes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json new file mode 100644 index 000000000..a48385871 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6237-error-subclass-delete-stack/main.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6237-error-subclass-delete-stack/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json new file mode 100644 index 000000000..bcd9bcf0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6260-js-var-dyn-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json new file mode 100644 index 000000000..3733ef399 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6261-js-destructure-for-of-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json new file mode 100644 index 000000000..2db0c3083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6262-js-file-scope-var-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json new file mode 100644 index 000000000..d0e4d3fcb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6263-js-factory-return-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json new file mode 100644 index 000000000..6b7a65b24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6291-webcrypto-hmac-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json new file mode 100644 index 000000000..a1b096f32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6292-webcrypto-hmac-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json new file mode 100644 index 000000000..7200349f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6293-webcrypto-digest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json new file mode 100644 index 000000000..c68b4b49c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6294-webcrypto-js-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json new file mode 100644 index 000000000..286b2c4bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6295-jose-hs256/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json new file mode 100644 index 000000000..05d9557e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6296-array-position-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json new file mode 100644 index 000000000..3c3818490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6297-library-runtime-idioms.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..ad7513645 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6331-jose-root-entry/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..d848dd493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6332-fluent-this-declaration/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..4bde02942 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6333-fluent-this-declaration-js/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..e510d9e2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6334-discarded-logical-statements.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..fd7b159a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6335-discarded-logical-statements.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json new file mode 100644 index 000000000..89aef0f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6340-prototype-reflection-optional-view-param.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json new file mode 100644 index 000000000..e4f556b43 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6350-globalthis-assertion-state.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json new file mode 100644 index 000000000..2d11062c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6352-globalthis-state-modules/boot.ts", + "/tests/corpus/6352-globalthis-state-modules/state.ts", + "/tests/corpus/6352-globalthis-state-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..b6046aead --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/bound-constructor-inspect-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/bound-constructor-inspect-names.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json new file mode 100644 index 000000000..eb79fc63a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-record-values/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/builtin-function-record-values/native.js", + "/tests/corpus/builtin-function-record-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json new file mode 100644 index 000000000..f13bd6642 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-function-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/builtin-function-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json new file mode 100644 index 000000000..ad1317aa6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-chained-this.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json new file mode 100644 index 000000000..9fb2595c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-chained-this.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-chained-this.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json new file mode 100644 index 000000000..a85c33c1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-instance.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json new file mode 100644 index 000000000..e01b6a426 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-instance.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-instance.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json new file mode 100644 index 000000000..fa81da713 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/constructor-returns-this-super.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/constructor-returns-this-super.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..b5e8ba087 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/es5-constructor-inspect-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/es5-constructor-inspect-names.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json new file mode 100644 index 000000000..b8de10f6a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/exponent-unit-base.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/exponent-unit-base.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json new file mode 100644 index 000000000..5975cbb40 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-late-optional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json new file mode 100644 index 000000000..7b99fa5f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-literal-flows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json new file mode 100644 index 000000000..43b336a1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-spread-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json new file mode 100644 index 000000000..f5706acaa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/let-uninitialized-function-binding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/let-uninitialized-function-binding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json new file mode 100644 index 000000000..2e7388984 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/logical-and-truthy-index.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/logical-and-truthy-index.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json new file mode 100644 index 000000000..86b15f680 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-instance-super.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/constructor-returns-instance-super.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json new file mode 100644 index 000000000..f3415a937 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/constructor-returns-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/constructor-returns-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json new file mode 100644 index 000000000..5f9d95858 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/webcrypto-refusals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json new file mode 100644 index 000000000..27fe582a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/navigator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json new file mode 100644 index 000000000..af889405d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-hmac.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/parity.test.ts b/packages/compiler/test/ts7/parity.test.ts index 872676c81..4904f2720 100644 --- a/packages/compiler/test/ts7/parity.test.ts +++ b/packages/compiler/test/ts7/parity.test.ts @@ -488,7 +488,7 @@ test("symbol declarations resolve back into the same client AST", () => { expect(sym7).toBeDefined(); const aliased = w.c7.getAliasedSymbol(sym7!); expect(aliased.name).toBe("answer"); - const declHandle = aliased.declarations[0]; + const declHandle = aliased.declarationHandles[0]; expect(declHandle).toBeDefined(); const decl = declHandle!.resolve(w.p7.project); expect(decl).toBeDefined(); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index d323bd901..28b63ec34 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { API, type Snapshot } from "typescript/unstable/sync"; +import { API, type Snapshot, type Symbol as SdkSymbol } from "typescript/unstable/sync"; import { Ts7Api, ts7Executable } from "../../src/frontend/ts7/rpc-api.js"; import { Ts7RpcClient } from "../../src/frontend/ts7/rpc-client.js"; import type { Ts7FileSystem } from "../../src/frontend/ts7/rpc-filesystem.js"; @@ -16,6 +16,15 @@ import { SemanticType, } from "../../src/frontend/ts7/semantic-model.js"; +// scriptc's symbols keep their declarations' node handles in +// `declarationHandles`, and the SDK's in `declarations`. +function declarationHandlesOf(symbol: SdkSymbol): SdkSymbol["declarations"] { + return ( + (symbol as unknown as { declarationHandles?: SdkSymbol["declarations"] }).declarationHandles ?? + symbol.declarations + ); +} + function tsgoPath(path: string): string { return process.platform === "win32" ? path.replaceAll("\\", "/") : path; } @@ -43,7 +52,7 @@ function programFacts(snapshot: Snapshot, config: string, path: string) { text: file.text, type: checker.typeToString(type), symbol: symbol.name, - declaration: symbol.declarations[0]!.resolve()!.getText(file), + declaration: declarationHandlesOf(symbol)[0]!.resolve()!.getText(file), syntactic: project.program.getSyntacticDiagnostics(path), semantic: project.program.getSemanticDiagnostics(path), globals: project.program.getGlobalDiagnostics(), @@ -83,10 +92,10 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => expect(project.checker.getTypeAtPosition(path, source.text.indexOf("answer"))).toBe(first); const symbol = project.checker.getSymbolAtPosition(path, source.text.indexOf("answer")); expect(symbol).toBeInstanceOf(SemanticSymbol); - expect(symbol!.declarations[0]).toBeInstanceOf(SemanticNodeHandle); + expect(symbol!.declarationHandles[0]).toBeInstanceOf(SemanticNodeHandle); expect(project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"))).toBe(symbol); - expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve()); - const declaration = symbol!.declarations[0]!.resolve()!; + expect(symbol!.declarationHandles[0]!.resolve()).toBe(symbol!.declarationHandles[0]!.resolve()); + const declaration = symbol!.declarationHandles[0]!.resolve()!; expect(declaration).toBeInstanceOf(AstNode); const variable = source.statements .nodes[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; @@ -205,9 +214,11 @@ test("native semantic registries preserve project scope and snapshot lifetimes", sharedSymbol: symbolA === symbolB, sharedType: typeA === typeB, sharedSignature: signatureA === signatureB, - sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(), + sameDeclaration: + declarationHandlesOf(symbolA)[0]!.resolve() === + declarationHandlesOf(symbolB)[0]!.resolve(), explicitDeclaration: - symbolB.declarations[0]!.resolve(b) === + declarationHandlesOf(symbolB)[0]!.resolve(b) === nodesOf( (nodesOf(fileB.statements)[1] as import("typescript/unstable/ast").VariableStatement) .declarationList.declarations, @@ -237,7 +248,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", expect(next.signatureA).not.toBe(actual.signatureA); first.dispose(); expect(() => actual.typeA.getSymbol()).toThrow("disposed"); - expect(() => actual.symbolA.declarations[0]!.resolve()).toThrow("disposed"); + expect(() => declarationHandlesOf(actual.symbolA)[0]!.resolve()).toThrow("disposed"); expect(next.signatureA.getParameters()[0]!.name).toBe("value"); expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42"); second.dispose(); diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 03c9f9e38..23c803879 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -116,6 +116,7 @@ const optional = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -128,6 +129,23 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // Programs that assign process.emitWarning link the replacement-aware + // warning paths; every other program keeps the default object. + if (source === "scr_async_dyn.c") { + variants.push({ + id: "warning-replace", + when: { warningReplace: true }, + defines: ["SCR_WARNING_REPLACE"], + }); + } + // stdin setEncoding, string data listeners and stdio 'resize' events. + if (source === "scr_events.c") { + variants.push({ + id: "stdio-extended", + when: { stdioExtended: true }, + defines: ["SCR_STDIO_EXTENDED"], + }); + } if (source === "scr_cycle.c") { // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with // nursery collections; programs built without it link the default. diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 18ee441e6..dbde1564f 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -343,7 +343,12 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], + executable: [ + "SCR_TEXT_DECODER_LEGACY", + "SCR_WARNING_REPLACE", + "SCR_STDIO_EXTENDED", + "SCR_YIN", + ], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index d686c0248..d7b4118e8 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -106,6 +106,7 @@ const OPTIONAL = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -118,6 +119,23 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + // Programs that assign process.emitWarning link the replacement-aware + // warning paths; every other program keeps the default object. + if (source === "scr_async_dyn.c") { + variants.push({ + id: "warning-replace", + when: { warningReplace: true }, + defines: ["SCR_WARNING_REPLACE"], + }); + } + // stdin setEncoding, string data listeners and stdio 'resize' events. + if (source === "scr_events.c") { + variants.push({ + id: "stdio-extended", + when: { stdioExtended: true }, + defines: ["SCR_STDIO_EXTENDED"], + }); + } if (source === "scr_cycle.c") { // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with // nursery collections; programs built without it link the default. diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 1bcd7455d..a0f117cec 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -440,7 +440,12 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], + executable: [ + "SCR_TEXT_DECODER_LEGACY", + "SCR_WARNING_REPLACE", + "SCR_STDIO_EXTENDED", + "SCR_YIN", + ], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index d16fd8b9c..6f56758ce 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -216,6 +216,12 @@ void scr_process_on_warning(ScrDyn *fn); void scr_process_off_warning(ScrDyn *fn); void scr_process_emit_warning(ScrDyn *args); void scr_emit_warning(const char *name, const char *code, ScrStr *message); +/* process.emitWarning = fn (borrowed; the runtime keeps its own + * reference). Later process.emitWarning(...) calls apply the replacement + * to their arguments, and runtime-built warnings (Node's own modules call + * process.emitWarning) reach it as (message, type[, code]) instead of the + * report and the 'warning' listeners. */ +void scr_process_set_emit_warning(ScrDyn *fn); /* node:punycode's first load (scr_async_dyn.c): later calls are module * cache hits. The first reports DEP0040 when `report` (false for a require * made from node_modules) — at once for a static ES import, which Node diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index dbddc5b18..1cdd466c3 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -822,10 +822,59 @@ ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code) { return w; } +#ifdef SCR_WARNING_REPLACE +/* The program's process.emitWarning replacement, or NULL while the + * property holds Node's own function. Compiled only into the + * SCR_WARNING_REPLACE variant, which programs assigning process.emitWarning + * link: every other executable keeps the plain warning paths. */ +static SCR_TL ScrDyn *scr_emit_warning_override = NULL; + +static void scr_emit_warning_override_teardown(void) { + scr_dyn_release(scr_emit_warning_override); + scr_emit_warning_override = NULL; +} + +void scr_process_set_emit_warning(ScrDyn *fn) { + if (scr_emit_warning_override == NULL) scr_atexit(scr_emit_warning_override_teardown); + ScrDyn *previous = scr_emit_warning_override; + scr_emit_warning_override = scr_dyn_retain(fn); + scr_dyn_release(previous); + /* Units that report their own warnings without a listener route them + * here once a replacement exists. */ + scr_warning_emit_hook = scr_emit_warning; +} + +/* A runtime-built warning while a replacement is installed: Node's modules + * call process.emitWarning(message, type, code), so the replacement sees + * those arguments; nothing is reported. A throw stays pending. */ +static void scr_emit_warning_replaced(ScrStr *message, ScrStr *type, ScrStr *code) { + ScrDyn *args[3]; + size_t argc = 0; + args[argc++] = scr_dyn_new_str(message); + ScrStr *fallback = type ? NULL : scr_str_new("Warning", 7); + args[argc++] = scr_dyn_new_str(type ? type : fallback); + scr_str_release(fallback); + if (code) args[argc++] = scr_dyn_new_str(code); + ScrDyn *r = scr_dyn_call(scr_emit_warning_override, args, argc, "process.emitWarning"); + scr_dyn_release(r); + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); +} +#endif /* SCR_WARNING_REPLACE */ + /* A warning built from C parts (the runtime deprecation sites): name * defaults to "Warning"; code/detail optional. Reported synchronously. * Borrows message. */ void scr_emit_warning(const char *name, const char *code, ScrStr *message) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + ScrStr *type_text = scr_str_new(name ? name : "Warning", strlen(name ? name : "Warning")); + ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL; + scr_emit_warning_replaced(message, type_text, code_text); + scr_str_release(type_text); + scr_str_release(code_text); + return; + } +#endif ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL; ScrDyn *w = scr_warning_new(message, name ? name : "Warning", code_text); scr_str_release(code_text); @@ -856,6 +905,18 @@ static void scr_warning_defer(ScrDyn *w, void (*fire)(ScrClosure *)) { } void scr_warning_emit_deferred(ScrDyn *w) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + const ScrDyn *em = scr_dyn_obj_get(w, "message", 7); + const ScrDyn *ec = scr_dyn_obj_get(w, "code", 4); + ScrStr *name = scr_warning_name(w); + ScrStr *message = em && em->kind == SCR_DYN_STR ? scr_str_retain(em->v.str) : scr_str_new("", 0); + scr_emit_warning_replaced(message, name, ec && ec->kind == SCR_DYN_STR ? ec->v.str : NULL); + scr_str_release(message); + scr_str_release(name); + return; + } +#endif if (scr_warning_is_deprecation(w)) { if (scr_process_deprecation_flag_on(0)) return; if (scr_process_deprecation_flag_on(1)) { @@ -942,6 +1003,13 @@ static void scr_warn_bad_arg(const char *arg, const char *want) { * ERR_INVALID_ARG_TYPE TypeErrors at the call; non-string details are * ignored. The warning itself is emitted on the next tick. */ void scr_process_emit_warning(ScrDyn *args) { +#ifdef SCR_WARNING_REPLACE + if (scr_emit_warning_override != NULL) { + ScrDyn *r = scr_dyn_apply(scr_emit_warning_override, args, "process.emitWarning"); + scr_dyn_release(r); + return; + } +#endif size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0; ScrDyn *const *items = args->kind == SCR_DYN_ARR ? args->v.arr.items : NULL; ScrDyn *warning = argc >= 1 ? items[0] : NULL; diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 79e6f3d77..4b6b6a6a6 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -1,4 +1,5 @@ -/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by +/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c) and the + * WebCrypto HMAC slice (scr_webcrypto.c). Included by * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_CRYPTO_H #define SCR_CRYPTO_H @@ -193,6 +194,22 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t unsigned char out[64]); size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); +/* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── + * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and + * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * call's argument vector as one dyn array (borrowed) and answers a +1 + * promise that every argument or validation failure rejects with Node's + * TypeError or DOMException; algorithms and formats outside the slice + * reject with a coded SC2020 Error. importKey settles at once with a + * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and + * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key + * answers `value instanceof CryptoKey`. */ +ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_sign(ScrDyn *args); +ScrPromise *scr_webcrypto_verify(ScrDyn *args); +ScrPromise *scr_webcrypto_digest(ScrDyn *args); +bool scr_webcrypto_is_crypto_key(const ScrDyn *value); + /* The composed `new crypto.X509Certificate(data).fingerprint` read (the * handle never materializes): the SHA-1 of the DER, uppercase * colon-separated — PEM or raw-DER input; anything else throws Node's diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 17b68712e..68b2af8d6 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -137,6 +137,7 @@ typedef enum { SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */ SCR_DYNH_CONSOLE = 32, /* stored global console value */ SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */ + SCR_DYNH_CRYPTO_KEY = 56, /* WebCrypto CryptoKey (scr_webcrypto.c) */ SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */ SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */ SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */ @@ -926,6 +927,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); +/* This thread's global object box (borrowed). Every spelling of the global + * object answers it; its symbol-keyed properties are the global object's + * (scr_node_builtin.c). */ +ScrDyn *scr_global_object(void); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */ bool scr_dyn_global_symbol_has(ScrSym *key); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 752c19805..8a1bfa728 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -559,10 +559,8 @@ static ScrDyn *scr_dynh_dispatch(ScrDyn *recv, const char *method, ScrDyn *const * reaching the comparator. The ordered list writes back index by index, so * a comparator that mutates the receiver mid-sort never dangles the items * being ordered. The default comparator compares ToString images - * (scr_dyn_display_buf — join's conversion) bytewise: code-POINT order, - * where JS orders UTF-16 code units — identical through the BMP, divergent - * only across the surrogate boundary (SEMANTICS.md). A comparator result - * converts with ToNumber; NaN counts as 0. */ + * (scr_dyn_display_buf — join's conversion) in UTF-16 code-unit order. A + * comparator result converts with ToNumber; NaN counts as 0. */ static ScrStr *dyn_sort_str(const ScrDyn *e) { ScrJsonBuf b; scr_jb_init(&b); @@ -584,7 +582,7 @@ static bool dyn_sort_less(void *ctx, uint64_t xs, uint64_t ys, bool *failed) { } ScrStr *xstr = dyn_sort_str(x); ScrStr *ystr = dyn_sort_str(y); - int c = scr_str_cmp(xstr, ystr); + int c = scr_str_cmp_u16(xstr, ystr); scr_str_release(xstr); scr_str_release(ystr); return c < 0; diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 54eb87a0d..3391f54b1 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -345,9 +345,23 @@ static void scr_signals_drain(void) { * listener it is treated as EOF (piped fd 0 read errors are not * meaningfully reachable). */ +#ifdef SCR_STDIO_EXTENDED +/* What a 'data' listener receives (the SCR_STDIO_EXTENDED variant, linked + * by programs that use setEncoding, string data listeners or 'resize'): + * raw chunks (typed Buffer listeners and the runtime's own consumers), the + * decoded string (typed string listeners, which need setEncoding), or + * whichever Node delivers (checked-dynamic listeners: the string once an + * encoding is set, else the Buffer). */ +enum { SCR_STDIN_DATA_BYTES, SCR_STDIN_DATA_STRING, SCR_STDIN_DATA_DYN }; + +#endif typedef struct { ScrClosure *cb; /* owned */ void (*fn)(ScrClosure *cb, ScrBytes *chunk); /* adapter: chunk borrowed */ +#ifdef SCR_STDIO_EXTENDED + void (*str_fn)(ScrClosure *cb, ScrStr *text); /* STRING/DYN: text borrowed */ + unsigned char kind; +#endif bool once; } ScrStdinDataL; @@ -377,6 +391,57 @@ static SCR_TL bool scr_stdin_read_eof = false; static SCR_TL ScrBytes *scr_stdin_buffer; static SCR_TL size_t scr_stdin_read_demand; +#ifdef SCR_STDIO_EXTENDED +/* process.stdin.setEncoding(enc): the canonical encoding name (NULL until + * set) and the StringDecoder's packed pending partial sequence, so a + * multibyte character split across reads decodes whole, as Node's + * decoder does. */ +static SCR_TL ScrStr *scr_stdin_encoding = NULL; +static SCR_TL double scr_stdin_dec_pending = 0; + +static void scr_stdin_add_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), + void (*str_fn)(ScrClosure *, ScrStr *), unsigned char kind, + bool once) { + if (scr_stdin_eof || scr_stdin_destroyed) { + scr_closure_release(cb); /* never fires again, like Node post-'end' */ + return; + } + if (scr_stdin_ndata == scr_stdin_data_cap) { + scr_stdin_data_cap = scr_stdin_data_cap ? scr_stdin_data_cap * 2 : 2; + scr_stdin_data = realloc(scr_stdin_data, scr_stdin_data_cap * sizeof *scr_stdin_data); + if (!scr_stdin_data) scr_events_oom(); + } + scr_stdin_data[scr_stdin_ndata].cb = cb; + scr_stdin_data[scr_stdin_ndata].fn = fn; + scr_stdin_data[scr_stdin_ndata].str_fn = str_fn; + scr_stdin_data[scr_stdin_ndata].kind = kind; + scr_stdin_data[scr_stdin_ndata].once = once; + scr_stdin_ndata++; +} + +void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), + bool once) { + scr_stdin_add_data(cb, fn, NULL, SCR_STDIN_DATA_BYTES, once); +} + +void scr_stdin_on_data_str(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrStr *), + bool once) { + scr_stdin_add_data(cb, NULL, fn, SCR_STDIN_DATA_STRING, once); +} + +static void scr_stdin_encoding_teardown(void) { + scr_str_release(scr_stdin_encoding); + scr_stdin_encoding = NULL; +} + +void scr_stdin_set_encoding(ScrStr *enc) { + if (scr_stdin_encoding == NULL) scr_atexit(scr_stdin_encoding_teardown); + ScrStr *previous = scr_stdin_encoding; + scr_stdin_encoding = scr_str_retain(enc); + scr_str_release(previous); + scr_stdin_dec_pending = 0; +} +#else void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *), bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { @@ -393,6 +458,7 @@ void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrByt scr_stdin_data[scr_stdin_ndata].once = once; scr_stdin_ndata++; } +#endif void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { @@ -487,7 +553,20 @@ static void scr_stdin_settle_done(void) { /* EOF/teardown shared tail: 'end' fires (snapshot; once semantics are * moot — everything drops right after), the parked promise settles done, * and every listener is released. */ +#ifdef SCR_STDIO_EXTENDED +static void scr_stdin_deliver_parts(ScrBytes *chunk, ScrStr *text); +#endif static void scr_stdin_finish(bool fire_end) { +#ifdef SCR_STDIO_EXTENDED + if (fire_end && scr_stdin_encoding != NULL && scr_stdin_ndata > 0) { + /* The decoder's end() flush reaches the string listeners before 'end'. */ + ScrStr *tail = scr_strdec_end(scr_stdin_encoding, scr_stdin_dec_pending); + scr_stdin_dec_pending = 0; + if (tail->len > 0) scr_stdin_deliver_parts(NULL, tail); + scr_str_release(tail); + if (scr_exc_pending()) fire_end = false; + } +#endif scr_stdin_eof = true; if (fire_end) { size_t n = scr_stdin_nend; @@ -657,7 +736,25 @@ static void scr_stdin_service(void) { scr_stdin_arrived(chunk); } +#ifdef SCR_STDIO_EXTENDED static void scr_stdin_deliver(ScrBytes *chunk) { + if (scr_stdin_ndata > 0 && scr_stdin_encoding != NULL) { + /* Node's readable decodes once, in the stream: every listener sees + * the same string, and a read holding only a partial sequence emits + * nothing. */ + ScrStr *text = scr_strdec_write(scr_stdin_encoding, scr_stdin_dec_pending, chunk); + scr_stdin_dec_pending = scr_strdec_next(scr_stdin_encoding, scr_stdin_dec_pending, chunk); + if (text->len > 0) scr_stdin_deliver_parts(chunk, text); + scr_str_release(text); + return; + } + scr_stdin_deliver_parts(chunk, NULL); +} + +static void scr_stdin_deliver_parts(ScrBytes *chunk, ScrStr *text) { +#else +static void scr_stdin_deliver(ScrBytes *chunk) { +#endif if (scr_stdin_ndata > 0) { size_t nd = scr_stdin_ndata; ScrStdinDataL *snap = malloc(nd * sizeof *snap); @@ -678,11 +775,31 @@ static void scr_stdin_deliver(ScrBytes *chunk) { } } } +#ifdef SCR_STDIO_EXTENDED + if (!scr_exc_pending()) { + if (snap[i].kind == SCR_STDIN_DATA_BYTES) { + if (chunk != NULL) snap[i].fn(snap[i].cb, chunk); + } else if (text != NULL) { + snap[i].str_fn(snap[i].cb, text); + } else if (snap[i].kind == SCR_STDIN_DATA_DYN) { + if (chunk != NULL) snap[i].fn(snap[i].cb, chunk); + } else { + static const char msg[] = + "stdin data listener type does not match setEncoding state"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + } + } + scr_closure_release(snap[i].cb); + } + free(snap); + } else if (scr_stdin_waiter && chunk != NULL) { +#else if (!scr_exc_pending()) snap[i].fn(snap[i].cb, chunk); scr_closure_release(snap[i].cb); } free(snap); } else if (scr_stdin_waiter) { +#endif ScrPromise *p = scr_stdin_waiter; scr_stdin_waiter = NULL; scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v, @@ -701,14 +818,31 @@ typedef struct { bool *error_once; size_t nerrors; ScrDyn *write_error; +#ifdef SCR_STDIO_EXTENDED + /* 'resize' listeners (stdout/stderr) and the size they last saw. */ + ScrDyn **resizes; + bool *resize_once; + size_t nresizes; + double columns, rows; +#endif } ScrStdio; static SCR_TL ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}}; +#ifdef SCR_STDIO_EXTENDED +enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME, + STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_WINDOW_SIZE, + STDIO_SET_ENCODING, STDIO_METHOD_COUNT }; +static const char *const scr_stdio_names[] = { + "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy", "getWindowSize", + "setEncoding" +}; +#else enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME, STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_WINDOW_SIZE, STDIO_METHOD_COUNT }; static const char *const scr_stdio_names[] = { "write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy", "getWindowSize" }; +#endif static SCR_TL ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT]; static SCR_TL bool scr_stdio_initialized; static SCR_TL bool scr_stdio_raw; @@ -736,6 +870,13 @@ static void scr_stdio_data(ScrClosure *cb, ScrBytes *bytes) { scr_stdio_fire(cb, chunk, 1); scr_dyn_release(chunk); } +#ifdef SCR_STDIO_EXTENDED +static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) { + ScrDyn *chunk = scr_dyn_new_str(text); + scr_stdio_fire(cb, chunk, 1); + scr_dyn_release(chunk); +} +#endif static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } static void scr_stdio_error(ScrClosure *cb, ScrStr *message) { ScrError *error = scr_error_new(SCR_ERR_ERROR, message); @@ -861,6 +1002,95 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { return result; } +#ifdef SCR_STDIO_EXTENDED +/* 'resize' on a TTY stdout/stderr: Node's tty.WriteStream listens for + * SIGWINCH and emits 'resize' when the window size changed. The signal + * listener does not keep the loop alive, and a non-TTY stream never + * emits. */ +static SCR_TL ScrDyn *scr_stdio_winch = NULL; + +static void scr_stdio_resize_emit(ScrStdio *stream) { + size_t n = stream->nresizes; + if (n == 0) return; + ScrDyn **snap = malloc(n * sizeof *snap); + bool *once = malloc(n * sizeof *once); + if (!snap || !once) scr_events_oom(); + for (size_t i = 0; i < n; i++) { + snap[i] = scr_dyn_retain(stream->resizes[i]); + once[i] = stream->resize_once[i]; + } + for (size_t i = 0; i < n; i++) { + if (once[i]) { + for (size_t j = 0; j < stream->nresizes; j++) { + if (stream->resizes[j] == snap[i]) { + scr_dyn_release(stream->resizes[j]); + memmove(stream->resizes + j, stream->resizes + j + 1, (stream->nresizes - j - 1) * sizeof *stream->resizes); + memmove(stream->resize_once + j, stream->resize_once + j + 1, (stream->nresizes - j - 1) * sizeof *stream->resize_once); + stream->nresizes--; + break; + } + } + } + if (!scr_exc_pending()) { + scr_dyn_this_push(stream, SCR_DYNH_STDIO); + ScrDyn *r = scr_dyn_call(snap[i], NULL, 0, "listener"); + scr_dyn_this_pop(); + scr_dyn_release(r); + } + scr_dyn_release(snap[i]); + } + free(snap); + free(once); +} + +static ScrDyn *scr_stdio_winch_fire(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + (void)cb; (void)args; (void)argc; + for (int fd = 1; fd <= 2 && !scr_exc_pending(); fd++) { + ScrStdio *stream = &scr_stdio_streams[fd]; + if (stream->nresizes == 0 || !scr_process_is_tty(fd)) continue; + double columns = scr_process_columns(fd), rows = scr_process_rows(fd); + if (columns == stream->columns && rows == stream->rows) continue; + stream->columns = columns; + stream->rows = rows; + scr_stdio_resize_emit(stream); + } + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static void scr_stdio_resize_listener(ScrStdio *stream, ScrDyn *listener, bool remove, bool once) { + if (remove) { + for (size_t i = stream->nresizes; i > 0; i--) { + if (scr_dyn_strict_eq(stream->resizes[i - 1], listener)) { + scr_dyn_release(stream->resizes[i - 1]); + memmove(stream->resizes + i - 1, stream->resizes + i, (stream->nresizes - i) * sizeof *stream->resizes); + memmove(stream->resize_once + i - 1, stream->resize_once + i, (stream->nresizes - i) * sizeof *stream->resize_once); + stream->nresizes--; + break; + } + } + return; + } + if (stream->nresizes == 0) { + stream->columns = scr_process_columns(stream->fd); + stream->rows = scr_process_rows(stream->fd); + } + stream->resizes = realloc(stream->resizes, (stream->nresizes + 1) * sizeof *stream->resizes); + stream->resize_once = realloc(stream->resize_once, (stream->nresizes + 1) * sizeof *stream->resize_once); + if (!stream->resizes || !stream->resize_once) scr_events_oom(); + stream->resizes[stream->nresizes] = scr_dyn_retain(listener); + stream->resize_once[stream->nresizes++] = once; +#if !defined(_WIN32) && !defined(__wasi__) + if (scr_stdio_winch == NULL && scr_context_is_main()) { + ScrClosure *cb = scr_closure_new(NULL, 0); + scr_stdio_winch = scr_dyn_new_func(cb, scr_stdio_winch_fire, 0, "", "refreshSize"); + ScrStr *name = scr_str_new("SIGWINCH", 8); + scr_signal_on(name, scr_stdio_winch, false); + scr_str_release(name); + } +#endif +} + +#endif static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc) { int method = (int)scr_box_get_f64(cb->caps[0]); ScrDyn *self = scr_dyn_this_get(); @@ -920,14 +1150,30 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0; bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0; bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0; +#ifdef SCR_STDIO_EXTENDED + bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 && + memcmp(event, "resize", 6) == 0; + if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) { +#else if ((!data && !end && !error) || (stream->fd != 0 && !error)) { +#endif result = scr_stdio_refusal("event"); goto done; } bool remove = method == STDIO_OFF, once = method == STDIO_ONCE; +#ifdef SCR_STDIO_EXTENDED + if (resize) { + scr_stdio_resize_listener(stream, listener, remove, once); + } else if (stream->fd == 0) { + if (!remove) { + if (data) + scr_stdin_add_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, + scr_stdio_data_text, SCR_STDIN_DATA_DYN, once); +#else if (stream->fd == 0) { if (!remove) { if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once); +#endif if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once); if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once); } else { @@ -972,6 +1218,15 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc } else if (stream->fd == 0 && method == STDIO_RAW) { scr_process_stdin_set_raw_mode(scr_dyn_truthy(arg)); if (!scr_exc_pending()) { scr_stdio_raw = scr_dyn_truthy(arg); result = scr_dyn_retain(self); } +#ifdef SCR_STDIO_EXTENDED + } else if (stream->fd == 0 && method == STDIO_SET_ENCODING) { + ScrStr *enc = scr_strdec_encoding(arg); + if (enc) { + scr_stdin_set_encoding(enc); + scr_str_release(enc); + result = scr_dyn_retain(self); + } +#endif } else if (stream->fd == 0 && method == STDIO_DESTROY && argc == 0) { scr_stdin_destroy(); result = scr_dyn_retain(self); @@ -1069,7 +1324,16 @@ static void scr_stdio_cleanup(void) { for (size_t j = 0; j < stream->nerrors; j++) scr_dyn_release(stream->errors[j]); free(stream->errors); free(stream->error_once); stream->errors = NULL; stream->error_once = NULL; stream->nerrors = 0; +#ifdef SCR_STDIO_EXTENDED + for (size_t j = 0; j < stream->nresizes; j++) scr_dyn_release(stream->resizes[j]); + free(stream->resizes); free(stream->resize_once); + stream->resizes = NULL; stream->resize_once = NULL; stream->nresizes = 0; +#endif } +#ifdef SCR_STDIO_EXTENDED + scr_dyn_release(scr_stdio_winch); + scr_stdio_winch = NULL; +#endif for (int i = 0; i < STDIO_METHOD_COUNT; i++) { scr_dyn_release(scr_stdio_methods[i]); scr_stdio_methods[i] = NULL; @@ -1213,6 +1477,11 @@ void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) { /* The listener owns its +1 param per the universal convention. */ ((void (*)(ScrClosure *, ScrBytes *))cb->fn)(cb, scr_bytes_retain(chunk)); } +#ifdef SCR_STDIO_EXTENDED +void scr_stdin_data_thunk_str(ScrClosure *cb, ScrStr *text) { + ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, scr_str_retain(text)); +} +#endif /* ── the loop hooks (see scr_async.c) ─────────────────────────────────── */ diff --git a/packages/runtime/src/scr_events.h b/packages/runtime/src/scr_events.h index 7dcf9cda0..a0025783e 100644 --- a/packages/runtime/src/scr_events.h +++ b/packages/runtime/src/scr_events.h @@ -42,6 +42,13 @@ void scr_run_exit_listeners(double code); void scr_exit_thunk0(ScrClosure *cb, double code); void scr_exit_thunk_code(ScrClosure *cb, double code); void scr_stdin_on_data(ScrClosure *cb, void (*fn)(ScrClosure *, ScrBytes *), bool once); +/* A string 'data' listener (moves cb): receives each chunk decoded by the + * stdin's setEncoding decoder; with no encoding set, a delivery throws + * the setEncoding-state TypeError instead (Node would pass a Buffer). */ +void scr_stdin_on_data_str(ScrClosure *cb, void (*fn)(ScrClosure *, ScrStr *), bool once); +/* process.stdin.setEncoding: the canonical encoding (borrowed). */ +void scr_stdin_set_encoding(ScrStr *enc); +void scr_stdin_data_thunk_str(ScrClosure *cb, ScrStr *text); void scr_stdin_on_end(ScrClosure *cb, bool once); void scr_stdin_on_error(ScrClosure *cb, ScrChildErrFn fn, bool once); ScrPromise *scr_stdin_next_chunk(void); /* +1 */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 6174913c0..83b56bea4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4356,6 +4356,60 @@ bool scr_dyn_data_view_is(const ScrDyn *value) { return value->kind == SCR_DYN_BYTES && value->v.bytes->is_data_view; } +/* util.inspect names an object after the function held by the first own + * `constructor` data property on its prototype chain (Node's + * getConstructorName). The dyn printer reads runtime class names + * (v.obj.class_name, set only on prototypes that stay alive), so a + * construction through a plain function stamps that prototype with a copy + * of the function's name and keeps it alive. The copy is the stamp's own: a + * function's name can live in storage the function owns (a bound + * function's "bound "), and the function is released when the + * writable `constructor` property is replaced. The stamps are cleared and + * their copies freed before release at exit, so freelist reuse never sees + * them. */ +typedef struct { + ScrDyn *prototype; + char *name; +} ScrDynNamedPrototype; +static SCR_TL ScrDynNamedPrototype *scr_dyn_named_prototypes; +static SCR_TL size_t scr_dyn_named_count, scr_dyn_named_cap; + +static void scr_dyn_named_prototypes_cleanup(void) { + for (size_t i = 0; i < scr_dyn_named_count; i++) { + scr_dyn_named_prototypes[i].prototype->v.obj.class_name = NULL; + scr_dyn_release(scr_dyn_named_prototypes[i].prototype); + free(scr_dyn_named_prototypes[i].name); + } + free(scr_dyn_named_prototypes); + scr_dyn_named_prototypes = NULL; + scr_dyn_named_count = scr_dyn_named_cap = 0; +} + +static void scr_dyn_name_prototype_chain(const ScrDyn *object) { + for (ScrDyn *p = object->prototype; p; p = p->prototype) { + if (p->kind != SCR_DYN_OBJ || p->v.obj.class_name) return; + ScrDynEntry *entry = scr_dyn_find_entry(p, "constructor", 11); + if (!entry || entry->accessor || !entry->value || entry->value->kind != SCR_DYN_FUNC) continue; + const char *name = entry->value->v.fn.name; + if (!name || !name[0]) continue; + if (scr_dyn_named_count == scr_dyn_named_cap) { + if (scr_dyn_named_cap == 0) scr_atexit(scr_dyn_named_prototypes_cleanup); + size_t cap = scr_dyn_named_cap ? scr_dyn_named_cap * 2 : 8; + ScrDynNamedPrototype *grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); + if (!grown) scr_json_oom(); + scr_dyn_named_prototypes = grown; + scr_dyn_named_cap = cap; + } + size_t len = strlen(name); + char *copy = malloc(len + 1); + if (!copy) scr_json_oom(); + memcpy(copy, name, len + 1); + p->v.obj.class_name = copy; + scr_dyn_named_prototypes[scr_dyn_named_count++] = (ScrDynNamedPrototype){ scr_dyn_retain(p), copy }; + return; + } +} + /* Ordinary native function constructors retain their live prototype table. * A constructor may replace the fresh receiver with another object. */ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { @@ -4366,6 +4420,7 @@ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr ScrDyn *receiver = scr_iterator_object(prototype) ? scr_dyn_obj_create(prototype) : scr_dyn_new_obj(); scr_dyn_release(prototype); if (!receiver) return NULL; + scr_dyn_name_prototype_chain(receiver); scr_dyn_this_push_dyn(receiver); ScrDyn *result = scr_dyn_call(callee, args->v.arr.items, args->v.arr.len, what->data); scr_dyn_this_pop(); diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 34c700350..233617092 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -86,6 +86,10 @@ ScrDyn *scr_util_system_error_entries(void) { static SCR_TL ScrDyn *scr_global_known; static SCR_TL ScrDyn *scr_global_own; +/* The one box denoting this thread's global object. Symbol-keyed properties + * live on the box (string-keyed ones in scr_global_own), so every spelling + * of the global object answers this box. */ +static SCR_TL ScrDyn *scr_global_box; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); @@ -180,6 +184,8 @@ ScrDyn *scr_process_hrtime_value(void) { } static void scr_global_cleanup(void) { + scr_dyn_release(scr_global_box); + scr_global_box = NULL; scr_dyn_release(scr_global_known); scr_dyn_release(scr_global_own); scr_global_known = NULL; @@ -195,7 +201,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { if (own) return scr_dyn_retain(own); if ((length == 10 && memcmp(key, "globalThis", length) == 0) || (length == 6 && memcmp(key, "global", length) == 0)) - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(scr_global_box); if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); @@ -223,26 +229,32 @@ static bool scr_global_set(void *handle, const char *key, size_t length, const S return true; } -ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { +ScrDyn *scr_global_object(void) { static const ScrDynHandleOps ops = { .cls = "Object", .retain = scr_global_retain, .release = scr_global_release, .get = scr_global_get, .set = scr_global_set, }; scr_dyn_handle_install(SCR_DYNH_GLOBAL, &ops); - scr_global_console_get = console_get; if (!scr_global_known) scr_global_known = scr_dyn_new_obj(); if (!scr_global_own) scr_global_own = scr_dyn_new_obj(); + if (!scr_global_box) scr_global_box = scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); if (!scr_global_cleanup_registered) { scr_atexit(scr_global_cleanup); scr_global_cleanup_registered = true; } + return scr_global_box; +} + +ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + ScrDyn *global = scr_global_object(); + scr_global_console_get = console_get; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) scr_dyn_obj_set(scr_global_known, key->data, key->len, scr_dyn_new_bool(true)); scr_str_release(key); } - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(global); } typedef struct ScrBuiltinModule { diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 43cf21b0c..8358658b5 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -6,6 +6,11 @@ #include #include #include +#if defined(__SSE2__) +#include +#elif defined(__ARM_NEON) && defined(__aarch64__) +#include +#endif /* Live heap-string count for the RC audit lane (-DSCR_RC_AUDIT): the test * harness builds with it to prove the emitted retain/release discipline @@ -836,24 +841,93 @@ static __attribute__((noinline)) int scr_str_cmp_u16_units(const ScrStr *a, cons } } +/* The memory index of the first differing byte of two words; x is their + * xor and is not zero. */ +static inline size_t scr_word_mismatch(uint64_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clzll(x) >> 3; +#else + return (size_t)__builtin_ctzll(x) >> 3; +#endif +} + +static inline size_t scr_word32_mismatch(uint32_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clz(x) >> 3; +#else + return (size_t)__builtin_ctz(x) >> 3; +#endif +} + +/* The index of the first byte where the n-byte spans a and b differ, or n + * when they are equal. Spans of at least 8 bytes compare words up to byte + * 32, then 32-byte blocks with SSE2 or NEON where the target has them, + * then words; the last word overlaps bytes already compared. Shorter + * spans compare two overlapping 4-byte halves, or bytes. */ +static inline size_t scr_span_mismatch(const char *a, const char *b, size_t n) { + size_t i = 0; + uint64_t l, r; + if (n < sizeof(uint64_t)) { + if (n >= sizeof(uint32_t)) { + uint32_t l4, r4; + memcpy(&l4, a, 4); + memcpy(&r4, b, 4); + if (l4 != r4) return scr_word32_mismatch(l4 ^ r4); + memcpy(&l4, a + n - 4, 4); + memcpy(&r4, b + n - 4, 4); + return l4 != r4 ? n - 4 + scr_word32_mismatch(l4 ^ r4) : n; + } + while (i < n && a[i] == b[i]) i++; + return i; + } + memcpy(&l, a, 8); + memcpy(&r, b, 8); + if (l != r) return scr_word_mismatch(l ^ r); + size_t head = n < 32 ? n & ~(size_t)7 : 32; + for (i = sizeof(uint64_t); i < head; i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } +#if defined(__SSE2__) + for (; n - i >= 32; i += 32) { + __m128i e0 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i)), + _mm_loadu_si128((const __m128i *)(b + i))); + __m128i e1 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i + 16)), + _mm_loadu_si128((const __m128i *)(b + i + 16))); + uint32_t equal = (uint32_t)_mm_movemask_epi8(e0) | (uint32_t)_mm_movemask_epi8(e1) << 16; + if (equal != UINT32_MAX) return i + (size_t)__builtin_ctz(~equal); + } +#elif defined(__ARM_NEON) && defined(__aarch64__) + /* A differing block leaves the loop; the word loop below finds the byte. */ + for (; n - i >= 32; i += 32) { + uint8x16_t e0 = vceqq_u8(vld1q_u8((const uint8_t *)a + i), vld1q_u8((const uint8_t *)b + i)); + uint8x16_t e1 = + vceqq_u8(vld1q_u8((const uint8_t *)a + i + 16), vld1q_u8((const uint8_t *)b + i + 16)); + if (vminvq_u8(vandq_u8(e0, e1)) != 0xff) break; + } +#endif + for (; n - i >= sizeof(uint64_t); i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } + if (i == n) return n; + size_t j = n - sizeof(uint64_t); + memcpy(&l, a + j, 8); + memcpy(&r, b + j, 8); + return l != r ? j + scr_word_mismatch(l ^ r) : n; +} + /* UTF-16 ordering over canonical WTF-8. Equal bytes can be skipped even * inside a multibyte character: unequal continuation bytes preserve code * point order. Differing lead bytes for U+E000..U+FFFF versus a * supplementary character reverse byte order, because the latter starts * with a UTF-16 high surrogate below U+E000. A lone surrogate against a * supplementary character compares their units. */ -int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { - if (a == b) return 0; +__attribute__((noinline)) int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b) { size_t common = a->len < b->len ? a->len : b->len; - size_t i = 0; - while (common - i >= sizeof(uint64_t)) { - uint64_t left, right; - memcpy(&left, a->data + i, sizeof(left)); - memcpy(&right, b->data + i, sizeof(right)); - if (left != right) break; - i += sizeof(left); - } - while (i < common && a->data[i] == b->data[i]) i++; + size_t i = scr_span_mismatch(a->data, b->data, common); if (i == common) return (a->len > b->len) - (a->len < b->len); unsigned char left = (unsigned char)a->data[i]; unsigned char right = (unsigned char)b->data[i]; @@ -866,6 +940,16 @@ int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { return left < right ? -1 : 1; } +/* Byte order differs from UTF-16 order only where both differing bytes are + * at least 0xED, which a proven-ASCII string never has: such pairs compare + * with memcmp. The scan stays out of line in a global function, so program + * builds can import this body. */ +int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { + if (a == b) return 0; + if (scr_str_known_ascii(a) || scr_str_known_ascii(b)) return scr_str_cmp(a, b); + return scr_str_cmp_u16_bytes(a, b); +} + /* ── interned strings ───────────────────────────────────────────────── * The empty string and every single-character ASCII string are immortal * statics (same layout the emitter uses for literals): charAt/slice churn diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index 425dfaabf..fc2f15562 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -290,16 +290,19 @@ ScrStr *scr_str_concat_f64(ScrStr *a, double x); bool scr_str_eq(ScrStr *a, ScrStr *b); -/* memcmp byte order == code-point order (see SEMANTICS.md: diverges from - * JS UTF-16 code-unit order only for non-BMP vs U+E000..U+FFFF). Returns - * <0, 0, >0. */ +/* memcmp byte order == code-point order, which differs from JS UTF-16 + * code-unit order for non-BMP characters against U+E000..U+FFFF and for + * lone surrogates. Returns <0, 0, >0. Relational string operators use + * scr_str_cmp_u16. */ int scr_str_cmp(ScrStr *a, ScrStr *b); /* a.localeCompare(b) with no locales/options: Node's root-locale order for * Latin-script text, code-point order outside it (scr_string.c). */ double scr_str_locale_compare(ScrStr *a, ScrStr *b); -/* ECMAScript string-list ordering: compare UTF-16 code units even though - * ScrStr stores WTF-8. Returns <0, 0, >0. */ +/* ECMAScript string ordering: compare UTF-16 code units even though ScrStr + * stores WTF-8. Returns <0, 0, >0. */ int scr_str_cmp_u16(ScrStr *a, ScrStr *b); +/* scr_str_cmp_u16 for two distinct strings that are not proven ASCII. */ +int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b); #endif /* SCR_STRING_H */ diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 383b95986..304f2544f 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -181,72 +181,33 @@ void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep, scr_str_release(ib); } -/* Symbol-keyed globals share the symbol unit's link gate. The always-linked - * JSON unit must not depend on optional symbol retain/release functions. */ -typedef struct ScrGlobalSymbolProperty { - ScrSym *key; - ScrDyn *value; - struct ScrGlobalSymbolProperty *next; -} ScrGlobalSymbolProperty; - -static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL; -static SCR_TL bool scr_global_symbols_cleanup_registered = false; - -static void scr_global_symbols_cleanup(void) { - while (scr_global_symbols) { - ScrGlobalSymbolProperty *entry = scr_global_symbols; - scr_global_symbols = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - } -} - +/* Direct `globalThis[symbol]` accesses use the symbol-keyed properties of + * the global object's box (scr_global_object), the storage every other + * spelling of the global object reaches, so all of them see one property. */ ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return scr_dyn_retain(entry->value); - } - return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *k = scr_dyn_new_symbol(key); + ScrDyn *value = scr_dyn_symbol_key_get(scr_global_object(), k, false); + scr_dyn_release(k); + return value; } void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key != key) continue; - ScrDyn *previous = entry->value; - entry->value = scr_dyn_retain(value); - scr_dyn_release(previous); - return; - } - if (!scr_global_symbols_cleanup_registered) { - scr_atexit(scr_global_symbols_cleanup); - scr_global_symbols_cleanup_registered = true; - } - ScrGlobalSymbolProperty *entry = malloc(sizeof *entry); - if (!entry) scr_trap("scriptc: out of memory\n"); - entry->key = scr_sym_retain(key); - entry->value = scr_dyn_retain(value); - entry->next = scr_global_symbols; - scr_global_symbols = entry; + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_symbol_key_set(scr_global_object(), k, value); + scr_dyn_release(k); } bool scr_dyn_global_symbol_has(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return true; - } - return false; + ScrDyn *k = scr_dyn_new_symbol(key); + bool present = scr_dyn_has_own_computed(scr_global_object(), k); + scr_dyn_release(k); + return present; } void scr_dyn_global_symbol_delete(ScrSym *key) { - ScrGlobalSymbolProperty **slot = &scr_global_symbols; - while (*slot) { - ScrGlobalSymbolProperty *entry = *slot; - if (entry->key != key) { slot = &entry->next; continue; } - *slot = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - return; - } + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_key_delete_computed(scr_global_object(), k, false); + scr_dyn_release(k); } /* ── fs.Dirent as a dynamic value ──────────────────────────────────── diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c new file mode 100644 index 000000000..546b24151 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.c @@ -0,0 +1,829 @@ +/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), + * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values + * importKey answers. + * + * This unit is only the WebCrypto layer, modeled on Node 24's + * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument + * conversion, algorithm normalization, the CryptoKey handle, promise + * wrapping and Node's error shapes. The MACs and digests themselves come + * from node:crypto's digest core (scr_crypto_hmac_raw and + * scr_crypto_digest_raw in scr_lib.c), and results settle through the + * deferred crypto queue that node:crypto's callback forms use, so sign, + * verify and digest settle on a later loop turn like Node's thread-pool + * jobs while importKey settles at once. + * + * Every argument and validation failure rejects the returned promise. + * Algorithms Node recognizes but this slice does not implement (RSA, + * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded + * SC2020 Error instead of silently diverging. The compiler links the unit + * only into programs that call these methods (the webcrypto feature). */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +/* ── algorithm registry ─────────────────────────────────────────────── + * The names Node 24 registers per operation (util.js's + * kAlgorithmDefinitions with the conditional algorithms its OpenSSL + * build enables), matched case-insensitively. Only HMAC and the SHA-1/ + * SHA-2 digests are implemented. */ +static const char *const scr_wc_import_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", + "ECDH", "ECDSA", "Ed25519", "Ed448", "HKDF", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "PBKDF2", "RSA-OAEP", "RSA-PSS", + "RSASSA-PKCS1-v1_5", "X25519", "X448", +}; +static const char *const scr_wc_sign_names[] = { + "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", +}; +static const char *const scr_wc_digest_names[] = { + "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", + "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", + "TurboSHAKE256", +}; + +#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) + +/* The implemented hashes: WebCrypto name and node:crypto digest name. */ +static const struct { + const char *name; + const char *native; +} scr_wc_hashes[] = { + {"SHA-1", "sha1"}, + {"SHA-256", "sha256"}, + {"SHA-384", "sha384"}, + {"SHA-512", "sha512"}, +}; + +static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { + size_t n = strlen(name); + if (s->len != n) return false; + for (size_t i = 0; i < n; i++) { + unsigned char a = (unsigned char)s->data[i], b = (unsigned char)name[i]; + if (a >= 'a' && a <= 'z') a = (unsigned char)(a - 32); + if (b >= 'a' && b <= 'z') b = (unsigned char)(b - 32); + if (a != b) return false; + } + return true; +} + +/* The canonical registered spelling of `name`, or NULL. */ +static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { + for (size_t i = 0; i < count; i++) + if (scr_wc_ascii_ieq(name, names[i])) return names[i]; + return NULL; +} + +static const char *scr_wc_native_hash(const char *name) { + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_hashes); i++) + if (strcmp(scr_wc_hashes[i].name, name) == 0) return scr_wc_hashes[i].native; + return NULL; +} + +/* ── errors ────────────────────────────────────────────────────────── */ + +static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; + +static void scr_wc_type_error(const char *code, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +static void scr_wc_type_error(const char *code, const char *fmt, ...) { + char message[512]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(message, sizeof message, fmt, ap); + va_end(ap); + if (n < 0) n = 0; + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, code); +} + +/* An operation or algorithm Node supports that this slice does not. */ +static void scr_wc_unsupported(const char *method, const char *what) { + char message[256]; + int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method, + what); + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); +} + +static void scr_wc_unsupported_algorithm(const char *method, const char *name) { + char what[96]; + snprintf(what, sizeof what, "the %s algorithm", name); + scr_wc_unsupported(method, what); +} + +/* ── Web IDL conversions ────────────────────────────────────────────── + * Each answers NULL/false with the TypeError pending. `prefix` is + * "Failed to execute '' on 'SubtleCrypto'". */ + +static bool scr_wc_is_object(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + return true; + } +} + +/* A typed reference capsule as its plain view; other values retained. */ +static ScrDyn *scr_wc_plain(ScrDyn *v) { + if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); + return scr_dyn_retain(v); +} + +/* BufferSource: the bytes of a TypedArray, DataView, Buffer or + * ArrayBuffer as a u8 view (+1). */ +static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return NULL; + ScrBytes *view = NULL; + if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + view = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or " + "DataView.", + prefix, scr_wc_ordinals[index]); + } + scr_dyn_release(plain); + return view; +} + +/* DOMString conversion (+1). */ +static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } + +static const char *const scr_wc_key_formats[] = { + "raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk", +}; + +/* KeyFormat: the canonical enum spelling, or NULL. */ +static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { + ScrStr *s = scr_wc_dom_string(value); + if (!s) return NULL; + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) { + if (s->len == strlen(scr_wc_key_formats[i]) && + memcmp(s->data, scr_wc_key_formats[i], s->len) == 0) { + scr_str_release(s); + return scr_wc_key_formats[i]; + } + } + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: 1st argument '%.*s' is not a valid enum value of type KeyFormat.", prefix, + (int)(s->len > 200 ? 200 : s->len), s->data); + scr_str_release(s); + return NULL; +} + +/* Node's canonical usage order; the bit is the index. */ +static const char *const scr_wc_usages[] = { + "encrypt", "decrypt", "sign", "verify", + "deriveKey", "deriveBits", "wrapKey", "unwrapKey", + "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", +}; +enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; + +/* sequence over an array: the usage mask (-1 on failure). */ +static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return -1; + if (plain->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.", + prefix, scr_wc_ordinals[index]); + scr_dyn_release(plain); + return -1; + } + long mask = 0; + double n = scr_dyn_arr_len(plain); + for (double i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_arr_at(plain, i); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) { + scr_dyn_release(plain); + return -1; + } + size_t k = 0; + while (k < SCR_WC_COUNT(scr_wc_usages) && + !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) + k++; + if (k == SCR_WC_COUNT(scr_wc_usages)) { + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", + prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), + s->data); + scr_str_release(s); + scr_dyn_release(plain); + return -1; + } + scr_str_release(s); + mask |= 1L << k; + } + scr_dyn_release(plain); + return mask; +} + +/* ── algorithm normalization ────────────────────────────────────────── + * normalizeAlgorithm(algorithm, op) for the members this slice reads: + * the canonical name, and for HMAC imports the hash and length. */ +typedef struct { + const char *name; /* canonical registered name */ + const char *hash; /* HMAC import: the normalized hash name */ + bool has_length; + double length; +} ScrWcAlgorithm; + +static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; + +static void scr_wc_missing_member(const char *dict, const char *member) { + scr_wc_type_error("ERR_MISSING_OPTION", + "%s: passed algorithm cannot be converted to '%s' because '%s' is required in " + "'%s'.", + scr_wc_normalize_prefix, dict, member, dict); +} + +/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ +static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { + if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key)); + return v ? v : scr_dyn_retain(scr_dyn_undefined()); +} + +/* The name an AlgorithmIdentifier carries (+1), with the Algorithm + * dictionary's missing-name TypeError. `dict` names the dictionary the + * value converts to. */ +static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { + if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return NULL; + ScrDyn *name = scr_wc_member(plain, "name"); + scr_dyn_release(plain); + ScrStr *out = NULL; + if (name->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "name"); + else out = scr_wc_dom_string(name); + scr_dyn_release(name); + return out; +} + +/* The digest a HashAlgorithmIdentifier names: canonical name, or NULL + * with NotSupportedError / the SC2020 refusal pending. */ +static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_native_hash(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + return canonical; +} + +/* HmacImportParams over an object: hash (required, a SHA name), then the + * optional [EnforceRange] unsigned long length. */ +static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { + if (!scr_wc_is_object(identifier)) { + scr_wc_missing_member("HmacImportParams", "hash"); + return false; + } + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *hash = scr_wc_member(plain, "hash"); + bool ok = true; + if (hash->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member("HmacImportParams", "hash"); + ok = false; + } + ScrDyn *hash_value = NULL; + if (ok) { + /* HashAlgorithmIdentifier converts to an object or a DOMString, then + * ensureSHA reads the name: a string, or the object's name member. */ + ScrStr *hash_name = NULL; + if (scr_wc_is_object(hash)) { + hash_value = scr_dyn_retain(hash); + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) hash_name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + } else { + hash_name = scr_wc_dom_string(hash); + if (!hash_name) ok = false; + else hash_value = scr_dyn_new_str(hash_name); + } + bool sha = hash_name && hash_name->len >= 3 && + (hash_name->data[0] | 32) == 's' && (hash_name->data[1] | 32) == 'h' && + (hash_name->data[2] | 32) == 'a'; + if (hash_name) scr_str_release(hash_name); + if (ok && !sha) { + scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + ok = false; + } + } + if (ok) { + ScrDyn *length = scr_wc_member(plain, "length"); + if (length->kind != SCR_DYN_UNDEF) { + double x; + if (!scr_dyn_number_coerce_js(length, &x)) ok = false; + else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: length in passed algorithm is not a finite number.", + scr_wc_normalize_prefix); + ok = false; + } else { + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: length in passed algorithm is outside the expected range of 0 to " + "4294967295.", + scr_wc_normalize_prefix); + ok = false; + } else if (x == 0) { + scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); + ok = false; + } else { + out->has_length = true; + out->length = x; + } + } + } + scr_dyn_release(length); + } + if (ok) { + out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + if (!out->hash) ok = false; + } + if (hash_value) scr_dyn_release(hash_value); + scr_dyn_release(hash); + scr_dyn_release(plain); + return ok; +} + +/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with + * the error pending. Only HMAC parameters are read; any other recognized + * algorithm is refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, + bool import, ScrWcAlgorithm *out) { + memset(out, 0, sizeof *out); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + out->name = scr_wc_lookup(names, count, name); + scr_str_release(name); + if (!out->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + return true; +} + +/* ── CryptoKey ───────────────────────────────────────────────────────── + * A secret HMAC key: the bytes, the hash, the bit length, the usage mask + * and the extractable flag. The algorithm and usages getters answer the + * same object on every read (Node caches its clones), built on first + * read. The bytes are cleared when the last reference goes. */ +typedef struct ScrCryptoKey { + size_t rc; + const char *hash; /* WebCrypto hash name */ + double length; /* bits */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; + size_t len; + unsigned char data[]; +} ScrCryptoKey; + +static void *scr_crypto_key_retain(void *p) { + ((ScrCryptoKey *)p)->rc++; + return p; +} + +static void scr_crypto_key_release(void *p) { + ScrCryptoKey *key = p; + if (!key || --key->rc != 0) return; + volatile unsigned char *bytes = key->data; + for (size_t i = 0; i < key->len; i++) bytes[i] = 0; + if (key->algorithm) scr_dyn_release(key->algorithm); + if (key->usage_list) scr_dyn_release(key->usage_list); + free(key); +} + +static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +bool scr_webcrypto_is_crypto_key(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return false; + return scr_crypto_key_of(value) != NULL; +} + +static ScrDyn *scr_wc_cstr(const char *s) { + ScrStr *str = scr_str_new(s, strlen(s)); + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; +} + +static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { + if (!key->algorithm) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + key->algorithm = algorithm; + } + return scr_dyn_retain(key->algorithm); +} + +static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { + if (!key->usage_list) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + key->usage_list = list; + } + return scr_dyn_retain(key->usage_list); +} + +static bool scr_wc_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *scr_crypto_key_get(void *h, const char *name, size_t len) { + ScrCryptoKey *key = h; + if (scr_wc_name_is(name, len, "type")) return scr_wc_cstr("secret"); + if (scr_wc_name_is(name, len, "extractable")) return scr_dyn_new_bool(key->extractable); + if (scr_wc_name_is(name, len, "algorithm")) return scr_crypto_key_algorithm(key); + if (scr_wc_name_is(name, len, "usages")) return scr_crypto_key_usages(key); + return NULL; +} + +/* The four members are prototype getters without setters: an assignment + * in strict code throws Node's TypeError. */ +static bool scr_crypto_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!scr_wc_name_is(name, len, "type") && !scr_wc_name_is(name, len, "extractable") && + !scr_wc_name_is(name, len, "algorithm") && !scr_wc_name_is(name, len, "usages")) + return false; + char message[128]; + int n = snprintf(message, sizeof message, + "Cannot set property %.*s of # which has only a getter", (int)len, + name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *scr_crypto_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)h; + (void)self; + (void)args; + (void)argc; + if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +/* key[Symbol.toStringTag] answers "CryptoKey". */ +static ScrDyn *scr_crypto_key_symbol_get(void *h, const ScrDyn *key) { + (void)h; + if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = description && description->len == 18 && + memcmp(description->data, "Symbol.toStringTag", 18) == 0; + if (description) scr_str_release(description); + return tag ? scr_wc_cstr("CryptoKey") : NULL; +} + +/* No inspect_view: Node's custom inspector prints the members one level + * deeper than a plain object would, which the shared handle inspection + * cannot reproduce, so util.inspect refuses a CryptoKey ('not supported + * yet') rather than printing a different shape. */ +static const ScrDynHandleOps scr_crypto_key_ops = { + .cls = "CryptoKey", + .retain = scr_crypto_key_retain, + .release = scr_crypto_key_release, + .invoke = scr_crypto_key_invoke, + .get = scr_crypto_key_get, + .set = scr_crypto_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = scr_crypto_key_symbol_get, +}; + +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, + double length, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_CRYPTO_KEY, &scr_crypto_key_ops); + } + ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); + if (!key) scr_trap("scriptc: out of memory\n"); + key->rc = 1; + key->hash = hash; + key->length = length; + key->usages = usages; + key->extractable = extractable; + key->algorithm = NULL; + key->usage_list = NULL; + key->len = len; + if (len) memcpy(key->data, data, len); + ScrDyn *box = scr_dyn_new_handle(key, SCR_DYNH_CRYPTO_KEY); + scr_crypto_key_release(key); + return box; +} + +/* ── the operations ─────────────────────────────────────────────────── + * Each takes its argument vector as one dyn array (borrowed) and answers + * a +1 promise. */ + +static ScrPromise *scr_wc_rejected(void) { + ScrPromise *p = scr_promise_new(); + scr_promise_reject_pending(p); + return p; +} + +/* The argument count check every method starts with (after the receiver + * check the compiler's call shape already guarantees). */ +static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { + size_t argc = (size_t)scr_dyn_arr_len(args); + if (argc >= required) return true; + scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix, + required, argc); + return false; +} + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { + for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i); +} + +static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { + for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); +} + +/* importKey(format, keyData, algorithm, extractable, keyUsages): Node's + * conversion order, then macImportKey's checks and importKeySync's empty + * usage check. */ +static ScrDyn *scr_wc_import(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrDyn *result = NULL; + ScrBytes *data = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool jwk = strcmp(format, "jwk") == 0; + /* The JsonWebKey dictionary is not implemented; that format is refused + * after the remaining arguments convert. */ + if (!jwk) { + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + } + if (!scr_wc_is_object(a.v[2])) { + ScrStr *s = scr_wc_dom_string(a.v[2]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm("importKey", algorithm.name); + goto done; + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); + goto done; + } + if (jwk) { + scr_wc_unsupported("importKey", "the 'jwk' key format"); + goto done; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + double length = (double)len * 8; + unsigned char last = view->data[len - 1]; + if (algorithm.has_length) { + double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); + if (bytes != (double)len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + /* A length that is not a whole number of bytes clears the unused + * low bits of the final byte (truncateToBitLength). */ + int extra = (int)fmod(algorithm.length, 8); + if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); + length = algorithm.length; + } + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + goto done; + } + result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, + extractable); + scr_crypto_key_of(result)->data[len - 1] = last; + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 5); + return result; +} + +ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + ScrDyn *key = scr_wc_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* sign(algorithm, key, data) / verify(algorithm, key, signature, data): + * the HMAC over `data`, or NULL with the error pending. *mac_len is the + * digest length. */ +static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char mac[64], + size_t *mac_len, ScrBytes **signature) { + const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'" + : "Failed to execute 'sign' on 'SubtleCrypto'"; + const char *method = verify ? "verify" : "sign"; + size_t n = verify ? 4 : 3; + if (!scr_wc_required(prefix, args, n)) return false; + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + bool ok = false; + ScrBytes *data = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (verify) { + *signature = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!*signature) goto done; + } + data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); + if (!data) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm(method, algorithm.name); + goto done; + } + if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + *mac_len = scr_crypto_hmac_raw(scr_wc_native_hash(key->hash), key->data, key->len, view->data, + (size_t)scr_bytes_byte_len(view), mac); + } + ok = true; +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, n); + return ok; +} + +ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)mac_len); + memcpy(out->data, mac, mac_len); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +/* The verify result's adapter: the deferred value is a dyn boolean. */ +static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + ScrPromise *p; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + ScrDyn *verified = (ScrDyn *)value; + scr_promise_fulfill_bool(p, scr_dyn_truthy(verified)); + scr_dyn_release(verified); +} + +ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + ScrBytes *signature = NULL; + if (!scr_wc_sign_verify(args, true, mac, &mac_len, &signature)) { + if (signature) scr_bytes_release(signature); + return scr_wc_rejected(); + } + bool equal; + { + ScrBytes *view = signature; + SCR_BYTES_SNAPSHOT(view); + /* A signature of another length is simply not valid; equal lengths + * compare in constant time. */ + size_t sig_len = (size_t)scr_bytes_byte_len(view); + equal = sig_len == mac_len; + unsigned char diff = 0; + for (size_t i = 0; equal && i < mac_len; i++) diff |= (unsigned char)(view->data[i] ^ mac[i]); + equal = equal && diff == 0; + } + scr_bytes_release(signature); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(equal), NULL, scr_crypto_promise_callback(p), + scr_wc_settle_verified); + return p; +} + +/* digest(algorithm, data). */ +ScrPromise *scr_webcrypto_digest(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'digest' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrBytes *data = NULL; + ScrBytes *out = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + const char *hash = scr_wc_digest_algorithm(a.v[0], "digest"); + if (!hash) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + unsigned char digest[64]; + size_t len = scr_crypto_digest_raw(scr_wc_native_hash(hash), view->data, + (size_t)scr_bytes_byte_len(view), digest); + out = scr_bytes_new(SCR_BYTES_U8, (double)len); + memcpy(out->data, digest, len); + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 2); + if (!out) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} diff --git a/tests/corpus/1536-string-array-sweep.ts b/tests/corpus/1536-string-array-sweep.ts index 0e5c12ebc..e3d5259ff 100644 --- a/tests/corpus/1536-string-array-sweep.ts +++ b/tests/corpus/1536-string-array-sweep.ts @@ -1,8 +1,7 @@ // The surface-tail sweep trio: string.substring (clamp-and-swap, the // port-suffix idiom), array findIndex (first match or -1, index-aware // callbacks), and the comparator-less sort() on string[] (JS's default -// UTF-16 ordering — a dedicated comparator because relational operators use -// scriptc's documented code-point order). +// UTF-16 code-unit ordering). const localAddr = "127.0.0.1.51823"; const lastColon = localAddr.lastIndexOf("."); console.log(localAddr.substring(lastColon + 1)); diff --git a/tests/corpus/5639-union-arm-by-discriminant.ts b/tests/corpus/5639-union-arm-by-discriminant.ts new file mode 100644 index 000000000..3c29273c5 --- /dev/null +++ b/tests/corpus/5639-union-arm-by-discriminant.ts @@ -0,0 +1,48 @@ +// A record that converts into several arms of a discriminated union takes +// the arm its discriminant names. The `map` callback below builds statement +// records whose `value` is an expression literal; that literal also fits the +// shorter `ref` expression, and the statement also fits `brk` (by dropping +// fields), but the `kind` values pick `get` and `set`, as in Node. +type Expr = + | { kind: "get"; obj: Expr | null; field: string } + | { kind: "ref"; obj: Expr | null } + | { kind: "num"; n: number }; +type Stmt = + | { kind: "set"; obj: Expr; field: string; value: Expr } + | { kind: "brk"; label?: string }; + +function fill(target: Expr, names: string[]): Stmt[] { + return names.map((name) => ({ + kind: "set", + obj: target, + field: name, + value: { kind: "get", obj: target, field: name }, + })); +} + +function describe(s: Stmt): string { + switch (s.kind) { + case "set": + return `set ${s.field} = ${s.value.kind}${s.value.kind === "get" ? `.${s.value.field}` : ""} on ${s.obj.kind}`; + case "brk": + return `brk ${s.label ?? "-"}`; + } +} + +for (const s of fill({ kind: "num", n: 1 }, ["a", "b"])) console.log(describe(s)); + +// The same records with the other discriminant value take the other arm. +function pick(kind: "set" | "brk", label: string): Stmt[] { + return [label].map((l) => + kind === "set" + ? { kind: "set" as const, obj: { kind: "num" as const, n: 2 }, field: l, value: { kind: "ref" as const, obj: null } } + : { kind: "brk" as const, label: l }, + ); +} +console.log(pick("set", "x").map(describe).join("; ")); +console.log(pick("brk", "y").map(describe).join("; ")); + +// A record that fits one arm only, by dropping a field, still takes it. +const extra = { kind: "brk" as const, label: "z", note: 1 }; +const stmts: Stmt[] = [extra]; +console.log(stmts.map(describe).join("; ")); diff --git a/tests/corpus/5640-generic-structural-class-narrowing.ts b/tests/corpus/5640-generic-structural-class-narrowing.ts new file mode 100644 index 000000000..a4144c5df --- /dev/null +++ b/tests/corpus/5640-generic-structural-class-narrowing.ts @@ -0,0 +1,78 @@ +// In a generic function, `node instanceof Identifier || node instanceof +// PrivateIdentifier` narrows `node` to `T & PrivateIdentifier`: Identifier +// has every member PrivateIdentifier has, so TypeScript reduces the union of +// the two intersections to one. The narrowed read still holds either class, +// as in Node, when it is stored, read or called. +abstract class Node { + kind: number; + constructor(kind: number) { + this.kind = kind; + } +} +class Identifier extends Node { + text: string; + flow: number | undefined; + constructor(text: string) { + super(1); + this.text = text; + this.flow = undefined; + } + describe(): string { + return `identifier ${this.text}`; + } +} +class PrivateIdentifier extends Node { + text: string; + constructor(text: string) { + super(2); + this.text = text; + } + describe(): string { + return `private ${this.text}`; + } +} +class Keyword extends Node { + constructor() { + super(3); + } +} + +const names = new Map(); + +function clone(node: T, copy: T): T { + if (node instanceof Identifier || node instanceof PrivateIdentifier) { + const name = names.get(node); + if (name !== undefined) names.set(copy, name + "'"); + } + return copy; +} + +function label(node: T): string { + if (node instanceof Identifier || node instanceof PrivateIdentifier) + return `${node.kind} ${node.text} (${node.describe()})`; + return `${node.kind} -`; +} + +// One instanceof test names one class. +function privateText(node: T): string { + if (node instanceof PrivateIdentifier) return node.text; + return "-"; +} + +const id = new Identifier("a"); +const hash = new PrivateIdentifier("#b"); +const keyword = new Keyword(); +const asNode: Node = id; +names.set(id, "A"); +names.set(hash, "B"); + +console.log(names.get(clone(id, new Identifier("a2")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b2")))); +console.log(names.get(clone(asNode, new Identifier("a3")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b3")))); +console.log(names.get(clone(keyword, new Keyword()))); +console.log(names.size); + +for (const node of [id, hash, keyword] as Node[]) console.log(label(node)); +console.log(label(id), "|", label(hash)); +console.log(privateText(id), privateText(hash), privateText(hash)); diff --git a/tests/corpus/6023-string-compare-utf16.ts b/tests/corpus/6023-string-compare-utf16.ts new file mode 100644 index 000000000..c4b9ccf91 --- /dev/null +++ b/tests/corpus/6023-string-compare-utf16.ts @@ -0,0 +1,72 @@ +// Relational string comparisons order strings by UTF-16 code units, as in +// Node: characters above U+FFFF (surrogate pairs) sort before U+E000..U+FFFF, +// lone surrogates sort by their own code unit, and ASCII and other BMP text +// keep their ordinary order. Typed comparisons, sort comparators and +// comparisons of untyped values agree. +const astral = "😀"; // U+1F600: D83D DE00 +const privateUse = "\ue000"; +const last = "\uffff"; +const highSurrogate = "\ud83d"; +const lowSurrogate = "\ude00"; +const hex = (s: string): string => { + const units: string[] = []; + for (let i = 0; i < s.length; i++) units.push(s.charCodeAt(i).toString(16)); + return units.join(" "); +}; + +const pairs: [string, string][] = [ + [astral, last], + [astral, privateUse], + ["\ud7ff", astral], + [highSurrogate, astral], + [lowSurrogate, astral], + [lowSurrogate, last], + ["é", "e"], + ["abc😀", "abc\uffff"], + ["same", "same"], + ["prefix", "prefix😀"], +]; +for (const [a, b] of pairs) + console.log(hex(a), "|", hex(b), a < b, a <= b, a > b, a >= b); + +const words = ["\uffff", "😀", "é", "e", "\ue000", "z", "\ud83d", "A"]; +const byOperator = [...words].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0)); +const byDefault = [...words].sort(); +console.log(byOperator.map(hex).join(", ")); +console.log(byDefault.map(hex).join(", ")); +console.log(byOperator.join("") === byDefault.join("")); + +const untyped: unknown[] = JSON.parse('["\\uffff", "\\ud83d\\ude00", "a"]'); +const [u0, u1] = untyped as string[]; +console.log(u1! < u0!, [...untyped].sort().map((v) => hex(String(v))).join(", ")); + +let smallest = words[0]!; +for (const word of words) if (word < smallest) smallest = word; +console.log(hex(smallest), words.filter((w) => w > "\ud7ff").length); + +// Every pair of these characters after shared prefixes of 0 to 24 bytes, +// ASCII and multibyte, with and without a suffix: the first differing byte +// falls in the first word, the tail word, the first byte or past 16 bytes. +const chars = ["a", "z", "é", "한", "中", "\ud7ff", "\ue000", "\uffff"]; +chars.push(astral, highSurrogate, lowSurrogate, "\u{10ffff}"); +const hashOf = (text: string): number => { + let h = 0; + for (let i = 0; i < text.length; i++) h = (Math.imul(h, 31) + text.charCodeAt(i)) | 0; + return h >>> 0; +}; +for (const unit of ["p", "é", "中"]) { + const rows: string[] = []; + for (let n = 0; n <= 24; n += 3) { + const prefix = unit.repeat(n); + let marks = ""; + for (const x of chars) + for (const y of chars) + for (const suffix of ["", "q", "tail-of-text"]) { + const a = prefix + x + suffix; + const b = prefix + y; + marks += a < b ? "<" : a > b ? ">" : "="; + } + rows.push(`${n}:${marks.split("<").length - 1}:${hashOf(marks)}`); + } + console.log(hex(unit), rows.join(" ")); +} diff --git a/tests/corpus/6080-take-then-flatmap.ts b/tests/corpus/6080-take-then-flatmap.ts new file mode 100644 index 000000000..1db6edbb4 --- /dev/null +++ b/tests/corpus/6080-take-then-flatmap.ts @@ -0,0 +1,19 @@ +// take(n) followed by flatMap counts n source values: the last value +// taken still flattens in full, and nothing is pulled after it. +const log: string[] = []; +function* gen(): Generator { for (let i = 0; i < 4; i++) { log.push("y" + i); yield i; } } +const flush = (): string => log.splice(0).join(" "); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).toArray(), flush()); +console.log([0, 1, 2, 3].values().take(2).flatMap((x) => [x, x + 10]).toArray()); +console.log(gen().take(3).flatMap((x) => new Set([x, -x])).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).take(3).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).find((v) => v === 10), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).some((v) => v > 10), flush()); +console.log(gen().filter((x) => x > 0).take(2).flatMap((x) => [x, x]).map((v) => v * 2).toArray(), flush()); +const out: number[] = []; +for (const v of gen().take(2).flatMap((x) => [x, x + 10])) out.push(v); +console.log(out, flush()); +const h = gen().take(1).flatMap((x) => [x, x + 1, x + 2]); +console.log(h.next(), h.next(), h.next(), h.next(), flush()); +console.log(gen().take(0).flatMap((x) => [x]).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x]).flatMap((x) => [x, -x]).toArray(), flush()); diff --git a/tests/corpus/6231-process-emit-warning-assign.ts b/tests/corpus/6231-process-emit-warning-assign.ts new file mode 100644 index 000000000..1cae44f82 --- /dev/null +++ b/tests/corpus/6231-process-emit-warning-assign.ts @@ -0,0 +1,35 @@ +// process.emitWarning and process.title are ordinary writable properties +// (pi's CLI setup assigns both). After the replacement, process.emitWarning +// calls reach it with their own arguments, no report reaches stderr, and +// the warnings Node's own modules emit through process.emitWarning (a +// util.deprecate wrapper's first call) reach it too. +import { deprecate } from "node:util"; + +process.title = "pi"; +console.log("title", process.title); +process.env.PI_CODING_AGENT = "true"; +console.log("env", process.env.PI_CODING_AGENT); + +const seen: string[] = []; +process.on("warning", (w) => seen.push(`listener ${w.name}`)); +function show(a: unknown): string { + if (a instanceof Error) return `Error(${a.message})`; + return typeof a === "object" ? JSON.stringify(a) : String(a); +} +process.emitWarning = ((warning: unknown, type: unknown, code: unknown) => { + seen.push(`${show(warning)}|${show(type)}|${show(code)}`); +}) as typeof process.emitWarning; + +process.emitWarning("plain"); +process.emitWarning("typed", "CustomWarning", "CW_1"); +process.emitWarning("options", { type: "OptWarning", code: "OW_1", detail: "d" }); +process.emitWarning(new Error("as error")); +const old = deprecate(() => "result", "old() is deprecated", "DEP_SCRIPTC_1"); +console.log("deprecated call", old()); +console.log("second call", old()); +console.log("sync", seen.join(" ; ")); + +// A replacement that does nothing silences warnings entirely. +process.emitWarning = (() => {}) as typeof process.emitWarning; +process.emitWarning("silenced"); +setTimeout(() => console.log("after tick", seen.length), 0); diff --git a/tests/corpus/6232-process-builtin-module-typed/main.ts b/tests/corpus/6232-process-builtin-module-typed/main.ts new file mode 100644 index 000000000..7765bd228 --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/main.ts @@ -0,0 +1,28 @@ +// process.getBuiltinModule and process.features under @types/node: a +// literal id types as the module namespace, so the result is the same +// object `import * as os` binds (pi's user-agent probe casts process and +// calls the member optionally), and process.features is an object a +// feature probe can test with `in` (pi's single-executable check). +import * as os from "node:os"; +import type * as NodeOs from "node:os"; + +const direct = process.getBuiltinModule("node:os"); +console.log("direct", direct.platform() === os.platform(), direct.EOL === os.EOL); +console.log("bare id", process.getBuiltinModule("os").arch() === os.arch()); + +type ProcessWithOsBuiltinModule = typeof process & { + getBuiltinModule?: (id: "node:os") => typeof NodeOs; +}; +function loadNodeOs(): typeof NodeOs | null { + if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) { + return null; + } + return (process as ProcessWithOsBuiltinModule).getBuiltinModule?.("node:os") ?? null; +} +const nodeOs = loadNodeOs(); +const agent = nodeOs ? `pi (${nodeOs.platform()} ${nodeOs.release()}; ${nodeOs.arch()})` : "pi (browser)"; +console.log("agent", agent === `pi (${os.platform()} ${os.release()}; ${os.arch()})`); +console.log("present", nodeOs !== null, nodeOs ? typeof nodeOs.homedir() : "none"); + +const isNodeSeaBinary = "sea" in process.features && process.features.sea === true; +console.log("sea", isNodeSeaBinary, "sea" in process.features); diff --git a/tests/corpus/6232-process-builtin-module-typed/package.json b/tests/corpus/6232-process-builtin-module-typed/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6232-process-builtin-module-typed/tsconfig.json b/tests/corpus/6232-process-builtin-module-typed/tsconfig.json new file mode 100644 index 000000000..ae4eda194 --- /dev/null +++ b/tests/corpus/6232-process-builtin-module-typed/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true + } +} diff --git a/tests/corpus/6233-for-await-class-async-generator.ts b/tests/corpus/6233-for-await-class-async-generator.ts new file mode 100644 index 000000000..1a88df7e8 --- /dev/null +++ b/tests/corpus/6233-for-await-class-async-generator.ts @@ -0,0 +1,168 @@ +// `for await` over a class instance whose [Symbol.asyncIterator] is an +// async generator method — pi-ai's EventStream, which is how every +// provider stream is consumed: a generic push/end queue, a subclass, the +// protocol-interface return annotations, producers that run between +// iterations, early exit (the generator's finally runs), and errors. +class FifoQueue { + private incoming: T[] = []; + private outgoing: T[] = []; + + get length(): number { + return this.incoming.length + this.outgoing.length; + } + + enqueue(value: T): void { + this.incoming.push(value); + } + + dequeue(): T | undefined { + if (this.outgoing.length === 0) { + while (this.incoming.length > 0) { + this.outgoing.push(this.incoming.pop()!); + } + } + return this.outgoing.pop(); + } +} + +class EventStream implements AsyncIterable { + private queue = new FifoQueue(); + private waiting = new FifoQueue<(value: IteratorResult) => void>(); + protected done = false; + private finalResultPromise: Promise; + private resolveFinalResult!: (result: R) => void; + private isComplete: (event: T) => boolean; + private extractResult: (event: T) => R; + + constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) { + this.isComplete = isComplete; + this.extractResult = extractResult; + this.finalResultPromise = new Promise((resolve) => { + this.resolveFinalResult = resolve; + }); + } + + push(event: T): void { + if (this.done) return; + if (this.isComplete(event)) { + this.done = true; + this.resolveFinalResult(this.extractResult(event)); + } + const waiter = this.waiting.dequeue(); + if (waiter) { + waiter({ value: event, done: false }); + } else { + this.queue.enqueue(event); + } + } + + end(result?: R): void { + this.done = true; + if (result !== undefined) { + this.resolveFinalResult(result); + } + while (this.waiting.length > 0) { + const waiter = this.waiting.dequeue()!; + waiter({ value: undefined as any, done: true }); + } + } + + async *[Symbol.asyncIterator](): AsyncIterator { + while (true) { + if (this.queue.length > 0) { + yield this.queue.dequeue()!; + } else if (this.done) { + return; + } else { + const result = await new Promise>((resolve) => this.waiting.enqueue(resolve)); + if (result.done) return; + yield result.value; + } + } + } + + result(): Promise { + return this.finalResultPromise; + } +} + +type Event = { type: "text"; delta: string } | { type: "done"; text: string }; + +class TextStream extends EventStream { + constructor() { + super( + (event) => event.type === "done", + (event) => (event.type === "done" ? event.text : ""), + ); + } +} + +function produce(stream: TextStream, words: string[]): void { + let i = 0; + const step = (): void => { + if (i < words.length) { + stream.push({ type: "text", delta: words[i]! }); + i++; + setTimeout(step, 1); + } else { + stream.push({ type: "done", text: words.join("") }); + } + }; + step(); +} + +class Ticker { + log: string[] = []; + async *[Symbol.asyncIterator](): AsyncIterableIterator { + try { + for (let n = 1; n <= 5; n++) { + this.log.push(`yield ${n}`); + yield n; + } + } finally { + this.log.push("closed"); + } + } +} + +class Failing { + async *[Symbol.asyncIterator](): AsyncGenerator { + yield "first"; + throw new Error("stream failed"); + } +} + +async function main(): Promise { + const stream = new TextStream(); + produce(stream, ["Hello", " from", " the", " stub"]); + let text = ""; + for await (const event of stream) { + if (event.type === "text") text += event.delta; + else console.log("done event", event.text); + } + console.log("streamed", JSON.stringify(text), "result", await stream.result()); + + // Events queued before iteration starts, then end() with no consumer waiting. + const early = new TextStream(); + early.push({ type: "text", delta: "a" }); + early.push({ type: "text", delta: "b" }); + early.end("ab"); + const seen: string[] = []; + for await (const event of early) seen.push(event.type === "text" ? event.delta : "?"); + console.log("queued", seen.join(","), await early.result()); + + // break closes the generator: its finally runs before the loop exits. + const ticker = new Ticker(); + for await (const n of ticker) { + if (n === 3) break; + } + console.log("ticker", ticker.log.join(" | ")); + + // A throw from the generator rejects the loop. + try { + for await (const s of new Failing()) console.log("got", s); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +main(); diff --git a/tests/corpus/6234-weak-collection-class-fields.ts b/tests/corpus/6234-weak-collection-class-fields.ts new file mode 100644 index 000000000..d7a085921 --- /dev/null +++ b/tests/corpus/6234-weak-collection-class-fields.ts @@ -0,0 +1,39 @@ +// WeakMap- and WeakSet-typed class fields (pi's AgentSession keys entry ids by +// message object): initialized, optional, and constructor-assigned fields hold +// the native weak collections like locals of those types. +class Session { + private readonly _entryIdsByMessage = new WeakMap(); + private seen = new WeakSet(); + cache: WeakMap | undefined; + record(message: object, id: string): void { + this._entryIdsByMessage.set(message, id); + this.seen.add(message); + } + idOf(message: object): string | undefined { + return this._entryIdsByMessage.get(message); + } + has(message: object): boolean { + return this.seen.has(message) && this._entryIdsByMessage.has(message); + } +} +const s = new Session(); +const a = { role: "user" }; +const b = { role: "assistant" }; +s.record(a, "e1"); +console.log(s.idOf(a), s.idOf(b), s.has(a), s.has(b)); +s.cache = new WeakMap(); +s.cache.set(a, 1); +console.log(s.cache.get(a), s.cache.has(b), s.cache instanceof WeakMap); +class Registry { + private readonly names: WeakMap; + constructor(names: WeakMap) { + this.names = names; + } + name(o: object): string { + return this.names.get(o) ?? "(unnamed)"; + } +} +const names = new WeakMap(); +names.set(a, "alice"); +const registry = new Registry(names); +console.log(registry.name(a), registry.name(b)); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs b/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs new file mode 100644 index 000000000..8e0a0457c --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/helper.cjs @@ -0,0 +1,6 @@ +// A CommonJS require of the bare names resolves to the builtins too. +const { StringDecoder } = require("string_decoder"); +exports.decodeEuro = () => { + const d = new StringDecoder("utf8"); + return d.write(Buffer.from([0xe2, 0x82])) + d.end(Buffer.from([0xac])); +}; diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/main.ts b/tests/corpus/6235-bare-builtin-shadowed-package/main.ts new file mode 100644 index 000000000..e9e83ea87 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/main.ts @@ -0,0 +1,17 @@ +// A bare core-module specifier names the Node builtin even when an npm +// package of the same name is installed (pi depends on string_decoder +// transitively): Node checks its builtins before node_modules. +import { StringDecoder } from "string_decoder"; +import { EventEmitter } from "events"; +import { createRequire } from "node:module"; + +const decoder = new StringDecoder("utf8"); +console.log(decoder.write(Buffer.from([0xe2, 0x82])) + decoder.end(Buffer.from([0xac]))); + +const emitter = new EventEmitter(); +emitter.on("ping", (n: number) => console.log("ping", n)); +emitter.emit("ping", 1); + +const require = createRequire(import.meta.url); +const helper = require("./helper.cjs") as { decodeEuro(): string }; +console.log("cjs", helper.decodeEuro()); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js new file mode 100644 index 000000000..f1a344ef1 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/events.js @@ -0,0 +1 @@ +throw new Error("the npm events package was loaded"); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json new file mode 100644 index 000000000..0fb1a3c8a --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/events/package.json @@ -0,0 +1 @@ +{ "name": "events", "version": "3.3.0", "main": "events.js" } diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js new file mode 100644 index 000000000..02af46d5a --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/index.js @@ -0,0 +1,3 @@ +// An installed package named like the core module: Node never loads it +// for the bare specifier. +throw new Error("the npm string_decoder package was loaded"); diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json new file mode 100644 index 000000000..ad07adde8 --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/node_modules/string_decoder/package.json @@ -0,0 +1 @@ +{ "name": "string_decoder", "version": "1.3.0", "main": "index.js" } diff --git a/tests/corpus/6235-bare-builtin-shadowed-package/package.json b/tests/corpus/6235-bare-builtin-shadowed-package/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6235-bare-builtin-shadowed-package/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/main.ts b/tests/corpus/6236-stdin-encoding-tty-probes/main.ts new file mode 100644 index 000000000..0fbe7eb99 --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/main.ts @@ -0,0 +1,62 @@ +// pi-tui's terminal setup on a piped stdin: the `if (process.stdin.setRawMode)` +// presence check (the method exists only on a TTY), setEncoding with a +// string 'data' listener whose chunks keep multibyte characters split +// across writes whole, and stdout 'resize' listeners (registered, +// removed, never fired off a TTY). The program re-runs itself with a +// pipe so the parent controls the write boundaries. +import { spawn } from "node:child_process"; + +if (process.argv[2] === "child") { + console.log("raw present", typeof process.stdin.setRawMode, Boolean(process.stdin.setRawMode)); + if (process.stdin.setRawMode) process.stdin.setRawMode(true); + process.stdin.setEncoding("utf8"); + const onResize = (): void => console.log("resize", process.stdout.columns); + process.stdout.on("resize", onResize); + process.stdout.once("resize", onResize); + process.stdout.removeListener("resize", onResize); + let total = ""; + let replacement = false; + process.stdin.on("data", (data: string) => { + if (typeof data !== "string") console.log("not a string"); + if (data.includes("�")) replacement = true; + total += data; + }); + process.stdin.on("end", () => { + console.log("end", JSON.stringify(total), "replacement", replacement); + process.stdout.removeListener("resize", onResize); + }); + process.stdin.resume(); + // Off a TTY no stream listens for SIGWINCH and its default is to ignore. + process.kill(process.pid, "SIGWINCH"); +} else { + const child = spawn(process.execPath, [process.argv[1]!, "child"], { + stdio: ["pipe", "pipe", "inherit"], + }); + let out = ""; + child.stdout!.setEncoding("utf8"); + child.stdout!.on("data", (chunk: string) => { + out += chunk; + }); + child.on("close", (code: number | null) => { + process.stdout.write(out); + console.log("child exit", code); + }); + const parts = [ + Buffer.from([0x68, 0xc3]), + Buffer.from([0xa9, 0x6c, 0x6c, 0x6f, 0x20, 0xe2, 0x82]), + Buffer.from([0xac, 0x20, 0xf0, 0x9f]), + Buffer.from([0x98, 0x80, 0x21]), + ]; + let i = 0; + const next = (): void => { + if (i < parts.length) { + const part: Buffer = parts[i]!; + i++; + child.stdin!.write(part); + setTimeout(next, 30); + } else { + child.stdin!.end(); + } + }; + next(); +} diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/package.json b/tests/corpus/6236-stdin-encoding-tty-probes/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json b/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json new file mode 100644 index 000000000..ae4eda194 --- /dev/null +++ b/tests/corpus/6236-stdin-encoding-tty-probes/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true + } +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/main.js b/tests/corpus/6237-error-subclass-delete-stack/main.js new file mode 100644 index 000000000..96ca7593d --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/main.js @@ -0,0 +1,12 @@ +// Deleting `stack` on an Error subclass that declares a stack accessor +// lets reads reach the accessor (the package's guest exceptions). +import { GuestError } from "guest-error"; + +try { + throw new GuestError(7); +} catch (e) { + console.log(e instanceof GuestError, e instanceof Error, e.handle, e.message); + console.log(e.stack); + e.stack = "rewritten"; + console.log(e.stack, Object.keys(e)); +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts new file mode 100644 index 000000000..7f3b83f94 --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.d.ts @@ -0,0 +1,6 @@ +export declare class GuestError extends Error { + handle: number; + constructor(handle: number); + get stack(): string; + set stack(v: string); +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js new file mode 100644 index 000000000..693c06b51 --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/index.js @@ -0,0 +1,18 @@ +// quickjs-wasi's JSException shape: an Error subclass whose stack +// accessor pair the constructor exposes by deleting V8's own `stack`. +export class GuestError extends Error { + handle; + #stack; + constructor(handle) { + super("guest error " + handle); + this.handle = handle; + delete this.stack; + this.#stack = "guest stack " + handle; + } + get stack() { + return this.#stack; + } + set stack(v) { + this.#stack = v; + } +} diff --git a/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json new file mode 100644 index 000000000..99f59d07b --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/node_modules/guest-error/package.json @@ -0,0 +1 @@ +{"name": "guest-error", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts"} diff --git a/tests/corpus/6237-error-subclass-delete-stack/package.json b/tests/corpus/6237-error-subclass-delete-stack/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6237-error-subclass-delete-stack/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6260-js-var-dyn-identity.js b/tests/corpus/6260-js-var-dyn-identity.js new file mode 100644 index 000000000..f22ce2888 --- /dev/null +++ b/tests/corpus/6260-js-var-dyn-identity.js @@ -0,0 +1,103 @@ +// JavaScript: a function-scoped `var` initialized with an object that +// already exists (an element of an array built with push, a find result, +// a member of another object) holds that object, not a copy: writes +// through the binding are visible through the array, and `===` holds. +function make(i) { + return { id: i, x: i % 17 }; +} + +function updateInPlace() { + var all = []; + for (var i = 0; i < 3; i++) all.push(make(i)); + var same = 0; + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x += 1; + if (o === all[j]) same++; + } + console.log(JSON.stringify(all), same); +} +updateInPlace(); + +function literalElements() { + var all = []; + for (var i = 0; i < 3; i++) all.push({ id: i, x: i }); + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x *= 10; + } + console.log(JSON.stringify(all)); +} +literalElements(); + +function initializerForms(flag) { + var all = []; + all.push({ n: 0, inner: { v: 1 } }); + all.push({ n: 1, inner: { v: 2 } }); + var found = all.find(function (e) { + return e.n === 1; + }); + found.n = 11; + var picked = flag ? all[0] : null; + picked.n = 10; + var either = all[5] || all[0]; + either.inner.v += 100; + var inner = all[1].inner; + inner.v = 20; + var last = all.pop(); + console.log(JSON.stringify(all), last === found, picked === either); + console.log(JSON.stringify(last)); +} +initializerForms(true); + +function arraysAndClosures() { + var grid = []; + grid.push([1, 2]); + var row = grid[0]; + row[0] = 9; + row.push(3); + var list = []; + list.push({ count: 0 }); + var target = list[0]; + var bump = function () { + target.count++; + }; + bump(); + bump(); + console.log(JSON.stringify(grid), JSON.stringify(list), target === list[0]); +} +arraysAndClosures(); + +function redeclaredAndReassigned() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var o = all[0]; + o.x = 10; + var o = all[1]; + o.x = 20; + o = { x: 30 }; + all.push(o); + o.x = 40; + console.log(JSON.stringify(all), o === all[2]); +} +redeclaredAndReassigned(); + +const arrow = () => { + var all = []; + all.push({ x: 1 }); + var o = all[0]; + o.x = 5; + console.log(JSON.stringify(all)); +}; +arrow(); + +// An annotated binding keeps its declared type. +function annotated() { + var all = []; + all.push({ x: 1 }); + /** @type {{ x: number }} */ + var o = all[0]; + console.log(o.x); +} +annotated(); diff --git a/tests/corpus/6261-js-destructure-for-of-identity.js b/tests/corpus/6261-js-destructure-for-of-identity.js new file mode 100644 index 000000000..a8821b3b4 --- /dev/null +++ b/tests/corpus/6261-js-destructure-for-of-identity.js @@ -0,0 +1,46 @@ +// JavaScript: array destructuring (var, let and const) and `for (var ... +// of ...)` bind the existing element objects of an array built with push, +// so writes through the bindings reach the array's objects. +function destructure() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var [first] = all; + first.x = 10; + let [, second] = all; + second.x = 20; + const [again] = all; + again.x += 1; + console.log(JSON.stringify(all), first === all[0], again === first, second === all[1]); +} +destructure(); + +function forVarOf() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var seen = []; + for (var o of all) { + o.x *= 10; + seen.push(o === all[seen.length]); + } + console.log(JSON.stringify(all), seen.join()); + for (let p of all) p.x += 1; + console.log(JSON.stringify(all)); +} +forVarOf(); + +function* tagged(list) { + for (var o of list) { + o.seen = true; + yield o; + } +} +function generators() { + var items = []; + items.push({ seen: false }); + items.push({ seen: false }); + for (var it of tagged(items)) it.extra = items.indexOf(it); + console.log(JSON.stringify(items)); +} +generators(); diff --git a/tests/corpus/6262-js-file-scope-var-identity.js b/tests/corpus/6262-js-file-scope-var-identity.js new file mode 100644 index 000000000..cb8678b5f --- /dev/null +++ b/tests/corpus/6262-js-file-scope-var-identity.js @@ -0,0 +1,40 @@ +// JavaScript: file-scope `var` and `let` bindings initialized with an +// existing object (an element of an array built with push) hold that +// object, including bindings declared in a loop body, by destructuring or +// by for-of, and functions declared above them read the same object. +function show() { + return typeof o === "undefined" ? "unset" : o.x; +} +console.log(show()); + +var all = []; +all.push({ x: 1 }); +all.push({ x: 2 }); + +var o = all[0]; +o.x = 10; +console.log(show(), JSON.stringify(all), o === all[0]); + +var same = 0; +for (var j = 0; j < all.length; j++) { + var item = all[j]; + item.x += 5; + if (item === all[j]) same++; +} +console.log(JSON.stringify(all), same); + +var [head] = all; +head.x = 100; +for (var each of all) { + each.x += 1; + if (each === all[1]) console.log("last", each.x); +} +console.log(JSON.stringify(all), head === o); + +let current = all[1]; +current.x = 7; +function touch() { + current.x++; +} +touch(); +console.log(JSON.stringify(all), current === all[1]); diff --git a/tests/corpus/6263-js-factory-return-identity.cjs b/tests/corpus/6263-js-factory-return-identity.cjs new file mode 100644 index 000000000..eb3f77557 --- /dev/null +++ b/tests/corpus/6263-js-factory-return-identity.cjs @@ -0,0 +1,56 @@ +// JavaScript: an object a function returns is the object the function +// holds elsewhere (a cache entry, a registry element). Bindings that +// receive it and array or object literals built from those bindings keep +// that object, so writes through them reach the cache and registry. +'use strict'; +const cache = { n: 0 }; +function get() { + return cache; +} + +const registry = []; +function make(name) { + const o = { name: name, hits: 0 }; + registry.push(o); + return o; +} + +function identity(v) { + return v; +} +function wrapped(i) { + return identity({ id: i, x: i % 17 }); +} + +function main() { + var a = get(); + a.n = 5; + console.log(cache.n, a === cache); + var list = [a]; + list[0].n = 6; + var holder = { item: a }; + holder.item.n += 1; + console.log(cache.n, list[0] === cache, holder.item === cache); + + var x = make("x"); + let y = make("y"); + const z = make("z"); + x.hits++; + y.hits += 2; + z.hits += 3; + var group = [x, y, z]; + for (var j = 0; j < group.length; j++) group[j].hits *= 10; + console.log(JSON.stringify(registry), group[0] === registry[0]); + + var all = []; + for (var i = 0; i < 4; i++) all.push(wrapped(i)); + var sum = 0; + for (var r = 0; r < 3; r++) + for (var k = 0; k < all.length; k++) { + var e = all[k]; + e.x += 1; + sum += e.x; + } + console.log(all.length, sum, JSON.stringify(all.slice(0, 2))); +} +main(); diff --git a/tests/corpus/6291-webcrypto-hmac-sign-verify.ts b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts new file mode 100644 index 000000000..ddb480a4d --- /dev/null +++ b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts @@ -0,0 +1,94 @@ +// WebCrypto HMAC through the crypto global: importKey('raw') for SHA-1 and +// SHA-2 keys, sign and verify (a wrong or short signature answers false), +// the CryptoKey members, instanceof CryptoKey, the key sources importKey +// and sign accept, and that results settle asynchronously. +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("header.payload"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const key = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], +); +console.log(key.type, key.extractable, key.usages.join(",")); +const algorithm = key.algorithm as HmacKeyAlgorithm; +console.log(algorithm.name, algorithm.length, algorithm.hash.name); +console.log(key instanceof CryptoKey, key.algorithm === key.algorithm, key.usages === key.usages); + +const signature = await crypto.subtle.sign("HMAC", key, data); +console.log(signature.byteLength, hex(signature)); +console.log(await crypto.subtle.verify("HMAC", key, signature, data)); +console.log(await crypto.subtle.verify({ name: "HMAC" }, key, signature, encoder.encode("header.payloaD"))); +const tampered = new Uint8Array(signature); +tampered[0] ^= 1; +console.log(await crypto.subtle.verify("HMAC", key, tampered, data)); +console.log(await crypto.subtle.verify("HMAC", key, signature.slice(0, 16), data)); +console.log(await crypto.subtle.verify("HMAC", key, new Uint8Array(0), data)); + +for (const hash of ["SHA-1", "SHA-384", "SHA-512"]) { + const k = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash }, true, ["sign"]); + const a = k.algorithm as HmacKeyAlgorithm; + const mac = await crypto.subtle.sign({ name: "hmac" }, k, data); + console.log(hash, a.hash.name, a.length, k.extractable, k.usages.join(","), mac.byteLength, hex(mac)); +} + +// Names normalize case-insensitively; usages come back in Node's order. +const lower = await crypto.subtle.importKey( + "raw", + secret, + { name: "hmac", hash: { name: "sha-256" } }, + true, + ["verify", "sign"], +); +const lowerAlgorithm = lower.algorithm as HmacKeyAlgorithm; +console.log(lowerAlgorithm.name, lowerAlgorithm.hash.name, lower.usages.join(",")); + +// An explicit length in bits must match the key bytes. +const sized = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256", length: secret.length * 8 }, + false, + ["verify"], +); +console.log((sized.algorithm as HmacKeyAlgorithm).length, sized.usages.join(",")); + +// Key and data sources: ArrayBuffer, Buffer, DataView and subarray views. +const fromBuffer = await crypto.subtle.importKey( + "raw", + secret.slice().buffer, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(hex(await crypto.subtle.sign("HMAC", fromBuffer, Buffer.from("header.payload"))) === hex(signature)); +const padded = encoder.encode("xxheader.payloadyy"); +console.log(hex(await crypto.subtle.sign("HMAC", key, padded.subarray(2, 16))) === hex(signature)); +console.log(hex(await crypto.subtle.sign("HMAC", key, new DataView(data.buffer))) === hex(signature)); + +// The data is read when sign is called. +const mutable = encoder.encode("header.payload"); +const pending = crypto.subtle.sign("HMAC", key, mutable); +mutable[0] = 0; +console.log(hex(await pending) === hex(signature)); + +// Results settle after the synchronous code that started them. +const order: string[] = []; +const signed = crypto.subtle.sign("HMAC", key, data).then(() => { + order.push("sign"); +}); +const imported = crypto.subtle + .importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + .then(() => { + order.push("importKey"); + }); +order.push("sync"); +await Promise.all([signed, imported]); +console.log(order.join(",")); diff --git a/tests/corpus/6292-webcrypto-hmac-errors.ts b/tests/corpus/6292-webcrypto-hmac-errors.ts new file mode 100644 index 000000000..95e33ae84 --- /dev/null +++ b/tests/corpus/6292-webcrypto-hmac-errors.ts @@ -0,0 +1,83 @@ +// WebCrypto HMAC failures reject with Node's TypeErrors (argument count, +// enum values, buffer sources, CryptoKey arguments, required members) and +// DOMExceptions (usages, key length, algorithm names, key access). +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("payload"); + +function report(label: string, error: Error): void { + if (error instanceof DOMException) { + const dom = error as DOMException; + console.log(label, dom.name, dom.code, dom.message, "DOMException"); + } else { + const coded = error as Error & { code?: string }; + console.log(label, coded.name, coded.code, coded.message); + } +} + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + report(label, e as Error); + } +} + +async function attemptVerify(label: string, run: () => Promise): Promise { + try { + console.log(label, "resolved", await run()); + } catch (e) { + report(label, e as Error); + } +} + +const signOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const verifyOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-384" }, false, ["verify"]); + +await attempt("usage", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "encrypt"]), +); +await attempt("empty usages", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, []), +); +await attempt("zero-length key", () => + crypto.subtle.importKey("raw", new Uint8Array(0), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("length mismatch", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 64 }, false, ["sign"]), +); +await attempt("length zero", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 0 }, false, ["sign"]), +); +await attempt("missing hash", () => + crypto.subtle.importKey("raw", secret, "HMAC", false, ["sign"]), +); +await attempt("md5 hash", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "MD5" }, false, ["sign"]), +); +await attempt("unknown sha", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-999" }, false, ["sign"]), +); +await attempt("unknown algorithm", () => + crypto.subtle.importKey("raw", secret, { name: "NOPE", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("pkcs8 format", () => + crypto.subtle.importKey("pkcs8", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("string key", () => + crypto.subtle.importKey("raw", "secret" as unknown as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); + +await attempt("sign with verify key", () => crypto.subtle.sign("HMAC", verifyOnly, data)); +await attemptVerify("verify with sign key", () => crypto.subtle.verify("HMAC", signOnly, new Uint8Array(32), data)); +await attempt("sign without key", () => + crypto.subtle.sign("HMAC", secret as unknown as CryptoKey, data), +); +await attempt("sign unknown algorithm", () => crypto.subtle.sign("HMAC-SHA256", signOnly, data)); +await attempt("sign string data", () => + crypto.subtle.sign("HMAC", signOnly, "payload" as unknown as BufferSource), +); +await attemptVerify("verify string signature", () => + crypto.subtle.verify("HMAC", verifyOnly, "sig" as unknown as BufferSource, data), +); diff --git a/tests/corpus/6293-webcrypto-digest.ts b/tests/corpus/6293-webcrypto-digest.ts new file mode 100644 index 000000000..ae107c6f0 --- /dev/null +++ b/tests/corpus/6293-webcrypto-digest.ts @@ -0,0 +1,35 @@ +// crypto.subtle.digest over the SHA-1 and SHA-2 hashes: string and object +// algorithm identifiers, typed-array, Buffer and ArrayBuffer input, and +// Node's rejections for unknown names and non-buffer data. +const encoder = new TextEncoder(); +const input = encoder.encode("scriptc digest input"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +for (const name of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const digest = await crypto.subtle.digest(name, input); + console.log(name, digest.byteLength, hex(digest)); +} +console.log(hex(await crypto.subtle.digest({ name: "sha-256" }, Buffer.from("scriptc digest input")))); +console.log(hex(await crypto.subtle.digest("SHA-256", input.slice().buffer))); +console.log(hex(await crypto.subtle.digest("SHA-256", new Uint8Array(0)))); + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + if (e instanceof DOMException) { + const error = e as DOMException; + console.log(label, error.name, error.code, error.message, "DOMException"); + } else { + const error = e as Error & { code?: string }; + console.log(label, error.name, error.code, error.message); + } + } +} + +await attempt("md5", () => crypto.subtle.digest("MD5", input)); +await attempt("string data", () => crypto.subtle.digest("SHA-256", "text" as unknown as BufferSource)); diff --git a/tests/corpus/6294-webcrypto-js-values.mjs b/tests/corpus/6294-webcrypto-js-values.mjs new file mode 100644 index 000000000..93071b39d --- /dev/null +++ b/tests/corpus/6294-webcrypto-js-values.mjs @@ -0,0 +1,52 @@ +// WebCrypto from JavaScript: keys and algorithms carried as dynamic +// values, the CryptoKey brand checks libraries use (Symbol.toStringTag, +// Object.prototype.toString and instanceof), JSON.stringify of a key, and +// rejections for missing arguments and non-key values. +const encoder = new TextEncoder(); +const secret = encoder.encode("dynamic-secret"); +const table = { + HS256: { name: "HMAC", hash: "SHA-256" }, + HS512: { name: "HMAC", hash: "SHA-512" }, +}; + +async function importFor(alg, usage) { + const params = table[alg]; + return crypto.subtle.importKey("raw", secret, params, false, [usage]); +} + +const key = await importFor("HS256", "sign"); +console.log(key[Symbol.toStringTag], Object.prototype.toString.call(key), String(key)); +console.log(key instanceof CryptoKey, secret instanceof CryptoKey, {} instanceof CryptoKey); +console.log(JSON.stringify(key), typeof key, key.type, key.algorithm.hash.name, key.usages.includes("sign")); +const sig = new Uint8Array(await crypto.subtle.sign(table.HS256, key, encoder.encode("a.b"))); +console.log(sig.length, Buffer.from(sig).toString("base64url")); +const verifier = await importFor("HS256", "verify"); +let verified = false; +try { + verified = await crypto.subtle.verify(table.HS256, verifier, sig, encoder.encode("a.b")); +} catch {} +console.log(verified); +const other = await importFor("HS512", "verify"); +console.log(await crypto.subtle.verify(table.HS512, other, sig, encoder.encode("a.b"))); + +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("too few", () => crypto.subtle.sign("HMAC", key)); +await attempt("import too few", () => crypto.subtle.importKey("raw", secret, table.HS256, false)); +await attempt("verify too few", () => crypto.subtle.verify("HMAC", key, sig)); +await attempt("digest too few", () => crypto.subtle.digest("SHA-256")); +const lookalike = JSON.parse('{"type":"secret","usages":["sign"]}'); +await attempt("not a key", () => crypto.subtle.sign("HMAC", lookalike, secret)); +await attempt("bad format", () => crypto.subtle.importKey("nope", secret, table.HS256, false, ["sign"])); +await attempt("bad usage", () => crypto.subtle.importKey("raw", secret, table.HS256, false, ["bogus"])); +await attempt("usages not a sequence", () => crypto.subtle.importKey("raw", secret, table.HS256, false, "sign")); +await attempt("algorithm without name", () => crypto.subtle.sign({ hash: "SHA-256" }, key, secret)); +await attempt("hash object without name", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: {} }, false, ["sign"]), +); diff --git a/tests/corpus/6295-jose-hs256/main.mjs b/tests/corpus/6295-jose-hs256/main.mjs new file mode 100644 index 000000000..a8a669a57 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -0,0 +1,103 @@ +// The real jose@6.2.12 HS256 path: SignJWT and jwtVerify from jose's +// public jose/jwt/sign, jose/jwt/verify and jose/errors entry points, over +// crypto.subtle importKey, sign and verify. node_modules/jose vendors the +// dist/webapi files those entry points load and their declarations, +// unmodified (MIT, LICENSE.md), with a package.json that exports only +// those entry points. +import { SignJWT } from "jose/jwt/sign"; +import { jwtVerify } from "jose/jwt/verify"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-hs256-shared-secret"); +const now = new Date(1_800_000_000_000); +const iat = 1_799_999_000; + +async function sign(claims, alg = "HS256", key = secret) { + const jwt = new SignJWT(claims); + jwt.setProtectedHeader({ alg, typ: "JWT" }); + jwt.setIssuedAt(iat); + jwt.setIssuer("urn:scriptc:issuer"); + jwt.setAudience("urn:scriptc:audience"); + jwt.setSubject("alice"); + return jwt.sign(key); +} + +async function verify(label, token, options = {}, key = secret) { + try { + const { payload, protectedHeader } = await jwtVerify(token, key, { + algorithms: ["HS256"], + currentDate: now, + ...options, + }); + console.log(label, "ok", JSON.stringify(protectedHeader), JSON.stringify(payload)); + } catch (e) { + console.log(label, e.code, e.message, e instanceof errors.JOSEError, e.name, e.claim, e.reason); + } +} + +const token = await sign({ role: "admin", exp: iat + 3600 }); +console.log(token); +await verify("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await verify("bytes token", encoder.encode(token)); +await verify("wrong secret", token, {}, encoder.encode("another-secret")); +const [header, body, signature] = token.split("."); +const forged = Buffer.from(JSON.stringify({ role: "root", sub: "mallory" })).toString("base64url"); +await verify("tampered payload", `${header}.${forged}.${signature}`); +await verify("tampered signature", `${header}.${body}.${signature.slice(0, -2)}AA`); +await verify("malformed", "not-a-jwt"); +await verify("bad signature encoding", `${header}.${body}.***`); +await verify("expired", token, { currentDate: new Date((iat + 7200) * 1000) }); +await verify("clock tolerance", token, { currentDate: new Date((iat + 3610) * 1000), clockTolerance: 30 }); +await verify("not yet valid", await sign({ nbf: iat + 600 })); +await verify("issuer", token, { issuer: "urn:other" }); +await verify("audience", token, { audience: "urn:other" }); +await verify("required claim", token, { requiredClaims: ["jti"] }); +await verify("alg not allowed", token, { algorithms: ["HS512"] }); +await verify("max token age", token, { maxTokenAge: 600 }); + +const hs384 = await sign({ scope: "read" }, "HS384"); +await verify("hs384", hs384, { algorithms: ["HS384"] }); +const hs512 = await sign({ scope: "write" }, "HS512"); +await verify("hs512", hs512, { algorithms: ["HS256", "HS512"] }); +await verify("hs512 as hs256 only", hs512); +await verify("none", `${Buffer.from('{"alg":"none"}').toString("base64url")}.${body}.`, { algorithms: ["none"] }); + +// The same key imported once and reused as a CryptoKey. +const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); +await verify("crypto key", token, {}, cryptoKey); +const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]); +await verify("wrong key hash", token, {}, signingKey); + +// Chained builder calls: jose's declarations type every setter's result as +// `this`, implemented by the JWTClaimsBuilder base SignJWT extends. +const chained = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience(["urn:scriptc:audience", "urn:other"]) + .setSubject("bob") + .setJti("jti-2") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 60) + .sign(secret); +console.log(chained); +await verify("chained", chained, { audience: "urn:scriptc:audience" }); + +// String durations: jose's secs() parses them. +for (const maxTokenAge of ["10 minutes", "1 hour", "1.5h", "2 days", "+20m", "1 week", "1y", "900 secs", "3 HOURS", "2h ago", "-2h ago", "10 fortnights", ""]) { + await verify(`max token age ${JSON.stringify(maxTokenAge)}`, token, { maxTokenAge }); +} +for (const clockTolerance of ["30s", "5 sec", "1 min from now", "1 min ago"]) { + await verify(`clock tolerance ${clockTolerance}`, token, { currentDate: new Date((iat + 3620) * 1000), clockTolerance }); +} +const relative = new SignJWT({ role: "reader" }); +relative.setProtectedHeader({ alg: "HS256" }).setIssuedAt("-1 minute").setExpirationTime("2h"); +const claims = JSON.parse(Buffer.from((await relative.sign(secret)).split(".")[1], "base64url").toString()); +const clock = Math.floor(Date.now() / 1000); +console.log(Math.abs(claims.exp - clock - 7200) <= 2, Math.abs(claims.iat - clock + 60) <= 2); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (e) { + console.log(e.name, e.message); +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/package.json b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json new file mode 100644 index 000000000..bcb353430 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json @@ -0,0 +1,17 @@ +{ + "name": "jose", + "version": "6.2.12", + "license": "MIT", + "type": "module", + "sideEffects": false, + "exports": { + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + } + } +} diff --git a/tests/corpus/6296-array-position-patterns.ts b/tests/corpus/6296-array-position-patterns.ts new file mode 100644 index 000000000..8c55b6152 --- /dev/null +++ b/tests/corpus/6296-array-position-patterns.ts @@ -0,0 +1,23 @@ +// Object patterns over arrays that name positions and `length` (jose's +// `const { 0: header, 1: payload, 2: signature, length } = jws.split(".")`): +// each position is the element or undefined past the end, defaults apply +// to missing positions, and `length` is the array's length. +const { 0: header, 1: payload, 2: signature, length } = "aaa.bbb.ccc".split("."); +console.log(header, payload, signature, length); + +const short = "aaa.bbb".split("."); +const { 0: first, 2: third, length: count } = short; +console.log(first, third === undefined, count); + +const { 1: second = "fallback", 3: fourth = "missing" } = short; +console.log(second, fourth); + +function parts(values: number[]): string { + const { 0: a, 1: b, length: n } = values; + return `${a}:${b}:${n}`; +} +console.log(parts([1, 2, 3]), parts([7])); + +let renamed = ""; +for (const { 0: key, 1: value } of [["x", "1"], ["y", "2"]]) renamed += `${key}=${value};`; +console.log(renamed); diff --git a/tests/corpus/6297-library-runtime-idioms.mjs b/tests/corpus/6297-library-runtime-idioms.mjs new file mode 100644 index 000000000..11d5d5e67 --- /dev/null +++ b/tests/corpus/6297-library-runtime-idioms.mjs @@ -0,0 +1,34 @@ +// Runtime idioms from jose's dist build: the Uint8Array base64/hex +// feature probes (absent in Node 24), String.fromCharCode.apply over byte +// chunks and number lists (the btoa fallback), and structuredClone of a +// parameter whose default is `{}` called with differently shaped objects. +console.log(typeof Uint8Array.prototype.toBase64, typeof Uint8Array.prototype.toHex); +console.log(typeof Uint8Array.prototype.setFromBase64, typeof Uint8Array.prototype.setFromHex); + +function encodeBase64(input) { + if (Uint8Array.prototype.toBase64) return input.toBase64(); + const CHUNK_SIZE = 4, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + return btoa(arr.join("")); +} +console.log(encodeBase64(new TextEncoder().encode("hello, base64 chunks"))); +console.log(encodeBase64(new Uint8Array([0, 255, 128, 1, 2]))); + +const bytes = new Uint8Array([104, 105]); +console.log(String.fromCharCode.apply(null, bytes), String.fromCharCode.apply(undefined, [72, 73])); +const untyped = JSON.parse("[79, 75]"); +console.log(String.fromCharCode.apply(null, untyped), String.fromCharCode.apply(null, []).length); + +let store; +class Builder { + constructor(payload = {}) { + (store ||= new WeakMap()).set(this, structuredClone(payload)); + } + claims() { + return JSON.stringify(store.get(this)); + } +} +console.log(new Builder({ role: "admin", exp: 5 }).claims()); +console.log(new Builder({ scope: "read" }).claims()); +console.log(new Builder().claims()); diff --git a/tests/corpus/6331-jose-root-entry/main.ts b/tests/corpus/6331-jose-root-entry/main.ts new file mode 100644 index 000000000..598154040 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/main.ts @@ -0,0 +1,110 @@ +// The real jose@6.2.12 through its root entry, from TypeScript, the way a +// Next.js proxy uses it: `import { jwtVerify, errors } from "jose"`. +// node_modules/jose vendors the published package's dist/ (every webapi +// module the root entry loads, and its declarations), package.json and +// LICENSE.md unmodified (MIT). Loading the root runs jwks/remote.js, which +// reads Node's navigator global at module load. Covers chained SignJWT +// builders, string durations (jose's secs()), destructured jwtVerify +// results and `instanceof errors.JOSEError` brand checks. +import { SignJWT, jwtVerify, errors, cryptoRuntime, decodeJwt } from "jose"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-root-shared-secret"); +const now = 1_800_000_000; +const iat = 1_799_999_000; + +interface Options { + issuer?: string; + audience?: string; + algorithms?: string[]; + at?: number; + maxTokenAge?: string; + clockTolerance?: string; +} + +async function check(label: string, token: string, options: Options = {}): Promise { + try { + const { payload, protectedHeader } = await jwtVerify(token, secret, { + algorithms: options.algorithms ?? ["HS256"], + currentDate: new Date((options.at ?? now) * 1000), + issuer: options.issuer, + audience: options.audience, + maxTokenAge: options.maxTokenAge, + clockTolerance: options.clockTolerance, + }); + const role = payload.role; + console.log(label, "ok", protectedHeader.alg, payload.sub ?? null, role, payload.exp ?? null); + } catch (error) { + if (error instanceof errors.JOSEError) { + console.log(label, error.code, error.message, error instanceof errors.JWTExpired); + } else if (error instanceof TypeError) { + console.log(label, "TypeError", error.message); + } else { + throw error; + } + } +} + +console.log(cryptoRuntime, errors.JWTExpired.code, errors.JWSSignatureVerificationFailed.code); + +const token = await new SignJWT({ role: "admin" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience("urn:scriptc:audience") + .setSubject("alice") + .setJti("jti-1") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 3600) + .sign(secret); +console.log(token); +console.log(JSON.stringify(decodeJwt(token))); + +await check("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await check("issuer", token, { issuer: "urn:other" }); +await check("audience", token, { audience: "urn:other" }); +await check("expired", token, { at: iat + 7200 }); +await check("wrong secret", await new SignJWT({ role: "admin" }).setProtectedHeader({ alg: "HS256" }).sign(encoder.encode("x"))); +await check("malformed", "not.a.jwt"); +await check("alg", token, { algorithms: ["HS512"] }); + +// String durations: maxTokenAge and clockTolerance against the fixed date. +await check("max age 10 minutes", token, { maxTokenAge: "10 minutes" }); +await check("max age 1 hour", token, { maxTokenAge: "1 hour" }); +await check("max age 2 days", token, { maxTokenAge: "2 days" }); +await check("max age 1.5h", token, { maxTokenAge: "1.5h" }); +await check("max age +20m", token, { maxTokenAge: "+20m" }); +await check("max age 1 week", token, { maxTokenAge: "1 week" }); +await check("max age 1y", token, { maxTokenAge: "1y" }); +await check("max age 900 secs", token, { maxTokenAge: "900 secs" }); +await check("max age 3 HOURS", token, { maxTokenAge: "3 HOURS" }); +await check("tolerance 30s", token, { at: iat + 3620, clockTolerance: "30s" }); +await check("tolerance 5 sec", token, { at: iat + 3620, clockTolerance: "5 sec" }); +await check("tolerance 1 min from now", token, { at: iat + 3620, clockTolerance: "1 min from now" }); +await check("tolerance 1 min ago", token, { at: iat + 3620, clockTolerance: "1 min ago" }); +await check("bad duration", token, { maxTokenAge: "10 fortnights" }); +await check("signed ago", token, { maxTokenAge: "-2h ago" }); +await check("empty duration", token, { maxTokenAge: "" }); + +// String durations on the builder are relative to the clock: check the +// distance from now instead of printing it. +const relative = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt("-1 minute") + .setNotBefore("0s") + .setExpirationTime("2h") + .sign(secret); +const claims = decodeJwt(relative); +const clock = Math.floor(Date.now() / 1000); +console.log( + Math.abs((claims.exp ?? 0) - clock - 7200) <= 2, + Math.abs((claims.iat ?? 0) - clock + 60) <= 2, + Math.abs((claims.nbf ?? 0) - clock) <= 2, +); +const { payload } = await jwtVerify(relative, secret, { maxTokenAge: "5 minutes" }); +console.log(payload.role); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts new file mode 100644 index 000000000..730247dd1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts @@ -0,0 +1,55 @@ +export { compactDecrypt } from './jwe/compact/decrypt.js'; +export type { CompactDecryptGetKey } from './jwe/compact/decrypt.js'; +export { flattenedDecrypt } from './jwe/flattened/decrypt.js'; +export type { FlattenedDecryptGetKey } from './jwe/flattened/decrypt.js'; +export { generalDecrypt } from './jwe/general/decrypt.js'; +export type { GeneralDecryptGetKey } from './jwe/general/decrypt.js'; +export { GeneralEncrypt } from './jwe/general/encrypt.js'; +export type { Recipient } from './jwe/general/encrypt.js'; +export { compactVerify } from './jws/compact/verify.js'; +export type { CompactVerifyGetKey } from './jws/compact/verify.js'; +export { flattenedVerify } from './jws/flattened/verify.js'; +export type { FlattenedVerifyGetKey } from './jws/flattened/verify.js'; +export { generalVerify } from './jws/general/verify.js'; +export type { GeneralVerifyGetKey } from './jws/general/verify.js'; +export { jwtVerify } from './jwt/verify.js'; +export type { JWTVerifyOptions, JWTVerifyGetKey } from './jwt/verify.js'; +export { jwtDecrypt } from './jwt/decrypt.js'; +export type { JWTDecryptOptions, JWTDecryptGetKey } from './jwt/decrypt.js'; +export { CompactEncrypt } from './jwe/compact/encrypt.js'; +export { FlattenedEncrypt } from './jwe/flattened/encrypt.js'; +export { CompactSign } from './jws/compact/sign.js'; +export { FlattenedSign } from './jws/flattened/sign.js'; +export { GeneralSign } from './jws/general/sign.js'; +export type { Signature } from './jws/general/sign.js'; +export { SignJWT } from './jwt/sign.js'; +export { EncryptJWT } from './jwt/encrypt.js'; +export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js'; +export { EmbeddedJWK } from './jwk/embedded.js'; +export { createLocalJWKSet } from './jwks/local.js'; +export type { LocalJWKSet } from './jwks/local.js'; +export { createRemoteJWKSet, jwksCache, customFetch } from './jwks/remote.js'; +export type { RemoteJWKSet, RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache, FetchImplementation, } from './jwks/remote.js'; +export { UnsecuredJWT } from './jwt/unsecured.js'; +export type { UnsecuredResult } from './jwt/unsecured.js'; +export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js'; +export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js'; +export type { KeyImportOptions } from './key/import.js'; +export { decodeProtectedHeader } from './util/decode_protected_header.js'; +export { decodeJwt } from './util/decode_jwt.js'; +export type { ProtectedHeaderParameters } from './util/decode_protected_header.js'; +import * as errors from './util/errors.js'; +export { errors }; +export { generateKeyPair } from './key/generate_key_pair.js'; +export type { GenerateKeyPairAlgorithm, GenerateKeyPairResult, GenerateKeyPairOptions, } from './key/generate_key_pair.js'; +export { generateSecret } from './key/generate_secret.js'; +export type { GenerateSecretAlgorithm, GenerateSecretOptions } from './key/generate_secret.js'; +import * as base64url from './util/base64url.js'; +export { base64url }; +export type { AnyJWK, CompactDecryptResult, CompactJWEHeaderParameters, CompactJWSHeaderParameters, CompactVerifyResult, CritOption, CryptoKey, DecryptOptions, EncryptOptions, FlattenedDecryptResult, FlattenedJWE, FlattenedJWS, FlattenedJWSInput, FlattenedVerifyResult, GeneralDecryptResult, GeneralJWE, GeneralJWS, GeneralJWSInput, GeneralVerifyResult, GetKeyFunction, JoseHeaderParameters, JSONWebKeySet, JWEContentEncryptionAlgorithm, JWEHeaderParameters, JWEKeyManagementAlgorithm, JWEKeyManagementHeaderParameters, JWK_AKP_Private, JWK_AKP_Public, JWK_EC_Private, JWK_EC_Public, JWK_oct, JWK_OKP_Private, JWK_OKP_Public, JWK_RSA_Private, JWK_RSA_Public, JWK, JWKKeyType, JWKParameters, JWSAlgorithm, JWSHeaderParameters, JWTClaimVerificationOptions, JWTDecryptResult, JWTHeaderParameters, JWTPayload, JWTVerifyResult, KeyInput, KeyObject, ProduceJWT, ResolvedKey, SignOptions, VerifyOptions, } from './types.d.ts'; +/** + * Identified the loaded runtime-specific build in prior releases; now always `"WebCryptoAPI"`. + * + * @deprecated Remove any runtime branching on this value; it is always `"WebCryptoAPI"`. + */ +export declare const cryptoRuntime = "WebCryptoAPI"; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts new file mode 100644 index 000000000..abbac7ac2 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWE decryption from unverified headers and token data. */ +export interface CompactDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Compact JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, getKey: CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Compact JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput | CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts new file mode 100644 index 000000000..966f65c3f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts @@ -0,0 +1,40 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Compact JWE strings. */ +export declare class CompactEncrypt { + #private; + /** + * Creates a Compact JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Encrypts the plaintext as a Compact JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts new file mode 100644 index 000000000..b166f8f40 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWE decryption from unverified headers and token data. */ +export interface FlattenedDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Flattened JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Flattened JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, getKey: FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Flattened JWE with a key or key resolver. The result includes `key` only when a + * resolver is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput | FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts new file mode 100644 index 000000000..9d1d3e3fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts @@ -0,0 +1,46 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Flattened JWE objects. */ +export declare class FlattenedEncrypt { + #private; + /** + * Creates a Flattened JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Encrypts the plaintext as a Flattened JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts new file mode 100644 index 000000000..67a396c16 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts @@ -0,0 +1,28 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWE decryption from unverified headers and token data. */ +export interface GeneralDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a General JWE. + * + * > Note: Returns plaintext and headers from the first recipient that decrypts successfully. Other + * > recipients may be inspected to enforce serialization rules, but their headers are not included in + * > the result. Rely only on the returned data. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a General JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, getKey: GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a General JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput | GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts new file mode 100644 index 000000000..e738a3a14 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts @@ -0,0 +1,51 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual recipient in a General JWE. */ +export interface Recipient { + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): Recipient; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): Recipient; + /** + * Adds another recipient to the enclosing {@link GeneralEncrypt} and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** + * Encrypts for all recipients on the enclosing {@link GeneralEncrypt}, using their configured + * keys. + */ + encrypt(): Promise; + /** Returns the enclosing {@link GeneralEncrypt} instance. */ + done(): GeneralEncrypt; +} +/** Builds and encrypts General JWE objects. */ +export declare class GeneralEncrypt { + #private; + /** + * Creates a General JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Adds a recipient and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** Encrypts the plaintext as a General JWE. */ + encrypt(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts new file mode 100644 index 000000000..d02ffcd49 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** + * Resolves a verification key from an embedded "jwk" (JSON Web Key) Header Parameter. This key + * resolver opts JWS and JWT verification into trusting a public key supplied by the token. Use the + * verification function's `algorithms` option to restrict accepted algorithms. + * + * @param token The consumed JWS token. + * @returns The public key from the JWS "jwk" (JSON Web Key) Header Parameter. + */ +export declare function EmbeddedJWK(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts new file mode 100644 index 000000000..674d7cbe6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts @@ -0,0 +1,15 @@ +import type * as t from '../types.d.ts'; +/** + * Calculates a base64url-encoded JWK Thumbprint. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprint(key: t.JWK | t.CryptoKey | t.KeyObject, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; +/** + * Calculates a JWK Thumbprint URI. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprintUri(key: t.CryptoKey | t.KeyObject | t.JWK, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts new file mode 100644 index 000000000..57057c105 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts @@ -0,0 +1,17 @@ +import type * as t from '../types.d.ts'; +/** A key resolver created by {@link createLocalJWKSet}. */ +export interface LocalJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Returns a structured clone of the original JSON Web Key Set. */ + jwks: () => t.JSONWebKeySet; +} +/** + * Creates a resolver for a locally available JSON Web Key Set. Selection uses the header's "alg" + * (Algorithm) and "kid" (Key ID), and respects the JWK's "use" (Public Key Use) and "key_ops" (Key + * Operations). Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param jwks JSON Web Key Set formatted object. + */ +export declare function createLocalJWKSet(jwks: t.JSONWebKeySet): LocalJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts new file mode 100644 index 000000000..0b7055e84 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts @@ -0,0 +1,94 @@ +import type * as t from '../types.d.ts'; +/** + * Configures a custom fetch implementation for remote JWKS retrieval. + * + * > Note: Fetch-like libraries may have incompatible TypeScript signatures even when they accept the + * > supplied arguments at runtime. + */ +export declare const customFetch: unique symbol; +/** Custom fetch function. Must return HTTP 200 with a JSON JWKS. See {@link customFetch}. */ +export type FetchImplementation = ( +/** JWKS URL, passed as the fetch resource. */ +url: string, +/** Options passed to fetch. */ +options: { + headers: Headers; + method: 'GET'; + /** See {@link !Request.redirect} */ + redirect: 'manual'; + signal: AbortSignal; +}) => Promise; +/** + * Symbol used to configure an externally persisted remote JWKS cache. + * + * > Warning: Only trusted application code must be allowed to write this cache; its keys are trusted for + * > signature verification. + */ +export declare const jwksCache: unique symbol; +/** Remote JWKS resolver options. */ +export interface RemoteJWKSetOptions { + /** + * Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and + * the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds). + */ + timeoutDuration?: number; + /** + * Time in milliseconds after a successful fetch before a missing key can trigger another fetch. + * Must not be `NaN`. Defaults to 30000 (30 seconds). + */ + cooldownDuration?: number; + /** + * Maximum age of cached keys in milliseconds. Defaults to 600000 (10 minutes); `Infinity` + * disables expiry. Must not be `NaN`. + */ + cacheMaxAge?: number | typeof Infinity; + /** Headers to be sent with the HTTP request. */ + headers?: Record; + /** See {@link jwksCache}. */ + [jwksCache]?: JWKSCacheInput; + /** See {@link customFetch}. */ + [customFetch]?: FetchImplementation; +} +/** Shape of an externally persisted remote JWKS cache. */ +export interface ExportedJWKSCache { + /** Current cached JSON Web Key Set */ + jwks: t.JSONWebKeySet; + /** Last successful fetch, in milliseconds since Unix epoch. */ + uat: number; +} +/** Values accepted by the {@link jwksCache} option. */ +export type JWKSCacheInput = ExportedJWKSCache | Record; +/** A key resolver created by {@link createRemoteJWKSet}. */ +export interface RemoteJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Whether the cooldown window following the last successful fetch is still in effect. */ + readonly coolingDown: boolean; + /** + * Whether the currently cached JSON Web Key Set is within its + * {@link RemoteJWKSetOptions.cacheMaxAge}. + */ + readonly fresh: boolean; + /** Whether a JSON Web Key Set fetch is currently in flight. */ + readonly reloading: boolean; + /** + * Triggers a JSON Web Key Set fetch, bypassing + * {@link RemoteJWKSetOptions.cooldownDuration the cooldown}. + */ + reload: () => Promise; + /** + * Returns a structured clone of the cached JSON Web Key Set, or `undefined` before keys have been + * fetched or seeded via {@link jwksCache}. + */ + jwks: () => t.JSONWebKeySet | undefined; +} +/** + * Creates a resolver for a JSON Web Key Set available at an HTTP(S) URL. Fetches the JSON Web Key + * Set when the cache is missing or stale. An unmatched key triggers another fetch only when + * `cooldownDuration` has elapsed since the last successful fetch. Selection uses the header's "alg" + * and "kid" and respects the JWK's "use" and "key_ops". Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param url URL to fetch the JSON Web Key Set from. + */ +export declare function createRemoteJWKSet(url: URL, options?: RemoteJWKSetOptions): RemoteJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts new file mode 100644 index 000000000..15d7e062b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts @@ -0,0 +1,20 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Compact JWS strings. */ +export declare class CompactSign { + #private; + /** + * Creates a Compact JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWSHeaderParameters): this; + /** + * Signs the payload as a Compact JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts new file mode 100644 index 000000000..0f21744cb --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWS verification from unverified headers and token data. */ +export interface CompactVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Compact JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function compactVerify(jws: string | Uint8Array, getKey: CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Compact JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput | CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts new file mode 100644 index 000000000..3bc861803 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts @@ -0,0 +1,22 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Flattened JWS objects. */ +export declare class FlattenedSign { + #private; + /** + * Creates a Flattened JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): this; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): this; + /** + * Signs the payload as a Flattened JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts new file mode 100644 index 000000000..151c59b9a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWS verification from unverified headers and token data. */ +export interface FlattenedVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Flattened JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Flattened JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, getKey: FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Flattened JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput | FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts new file mode 100644 index 000000000..ad1cd73dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts @@ -0,0 +1,38 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual signature in a General JWS. */ +export interface Signature { + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): Signature; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): Signature; + /** + * Adds another signature to the enclosing {@link GeneralSign} and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Creates all signatures on the enclosing {@link GeneralSign}, using their configured keys. */ + sign(): Promise; + /** Returns the enclosing {@link GeneralSign} instance. */ + done(): GeneralSign; +} +/** Builds and signs General JWS objects. */ +export declare class GeneralSign { + #private; + /** + * Creates a General JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** + * Adds a signature and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Signs the payload as a General JWS. */ + sign(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts new file mode 100644 index 000000000..620325d00 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts @@ -0,0 +1,29 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWS verification from unverified headers and token data. */ +export interface GeneralVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a General JWS signature and decodes its payload. + * + * > Note: Returns payload and headers from the first signature that verifies successfully. Other entries' + * > headers may be inspected to reject inconsistent use of the JWS Unencoded Payload Option, but are + * > not included in the result. Rely only on the returned data. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a General JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, getKey: GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a General JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput | GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 000000000..cdf6a372a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts @@ -0,0 +1,34 @@ +import type * as t from '../types.d.ts'; +/** JWE decryption and JWT Claims Set validation options. */ +export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT decryption key. No token components have been authenticated when this function is + * called. + */ +export interface JWTDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE-formatted JWT and validates its Claims Set. + * + * @param key Private key or shared secret to decrypt and verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise>; +/** + * Decrypts a JWT and validates its claims, returning the dynamically + * {@link t.ResolvedKey.key resolved key}. + * + * @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT + * with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, getKey: JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Private key or shared secret, or a function resolving one, to decrypt and verify the + * JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 000000000..c39e4c994 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts @@ -0,0 +1,57 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */ +export declare class EncryptJWT extends EncryptJWT_base { + #private; + /** + * Sets the JWE Protected Header. May only be called once. + * + * @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc" + * (JWE Encryption Algorithm) properties. + */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds + * them to the appropriate JOSE Header. Use this instead of the header setters for algorithm + * inputs. May only be called once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Sets the content encryption key. By default, a suitable random key is generated for the JWE + * "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the Initialization Vector for content encryption. By default, a suitable random IV is + * generated for the JWE "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateIssuerAsHeader(): this; + /** + * Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateSubjectAsHeader(): this; + /** + * Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateAudienceAsHeader(): this; + /** + * Encrypts and returns the JWT. + * + * @param key Public key or shared secret to encrypt the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts new file mode 100644 index 000000000..6c3454eef --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** Decoded Unsecured JWT. */ +export interface UnsecuredResult { + /** JWT Claims Set. */ + payload: PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); + /** The decoded JOSE Header; always `{ "alg": "none" }` for an Unsecured JWT. */ + header: t.JWSHeaderParameters; +} +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const UnsecuredJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Encodes and decodes `{ "alg": "none" }` Unsecured JWTs. */ +export declare class UnsecuredJWT extends UnsecuredJWT_base { + private jwt; + /** Encodes the Unsecured JWT. */ + encode(): string; + /** Decodes an unsecured JWT and validates its claims without authenticating the token. */ + static decode(jwt: string, options?: t.JWTClaimVerificationOptions): UnsecuredResult; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts new file mode 100644 index 000000000..bbc7ef7d4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** + * Exports a public key to PEM-encoded SPKI. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportSPKI(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a private key to PEM-encoded PKCS#8. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportPKCS8(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a key to JWK. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportJWK(key: t.CryptoKey | t.KeyObject | Uint8Array): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts new file mode 100644 index 000000000..ecc53339c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts @@ -0,0 +1,33 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateKeyPair}, subject to runtime support. */ +export type GenerateKeyPairAlgorithm = 'PS256' | 'PS384' | 'PS512' | 'RS256' | 'RS384' | 'RS512' | 'RSA-OAEP' | 'RSA-OAEP-256' | 'RSA-OAEP-384' | 'RSA-OAEP-512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | (string & {}); +/** Generated asymmetric key pair. */ +export interface GenerateKeyPairResult { + privateKey: t.CryptoKey; + publicKey: t.CryptoKey; +} +/** Asymmetric key pair generation options. */ +export interface GenerateKeyPairOptions { + /** + * EC curve or OKP key subtype. Must be supported by the algorithm and runtime. ECDH-ES defaults + * to P-256. + */ + crv?: string; + /** RSA modulus length in bits. Must be an integer of at least 2048; defaults to 2048. */ + modulusLength?: number; + /** + * Whether the private key is extractable. Defaults to false; the public key is always + * extractable. + */ + extractable?: boolean; +} +/** + * Generates an asymmetric key pair for a JWA algorithm identifier. + * + * > Note: Private keys are not extractable by default. Set {@link GenerateKeyPairOptions.extractable} to + * > export them; public keys are always extractable. + * + * @param alg JWA Algorithm Identifier to be used with the generated key pair. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateKeyPair(alg: GenerateKeyPairAlgorithm, options?: GenerateKeyPairOptions): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts new file mode 100644 index 000000000..da1db236e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts @@ -0,0 +1,30 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateSecret}, subject to runtime support. */ +export type GenerateSecretAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW' | 'A128GCM' | 'A192GCM' | 'A256GCM' | (string & {}); +/** + * Maps a JWA algorithm identifier to the value returned by {@link generateSecret}. AES-CBC-HMAC + * algorithms return {@link !Uint8Array}; other supported algorithms return + * {@link t.CryptoKey CryptoKey}. When the algorithm is not statically known, the result is their + * union. + */ +export type GeneratedSecret = Alg extends 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' ? Uint8Array : string extends Alg ? t.CryptoKey | Uint8Array : t.CryptoKey; +/** Secret generation options. */ +export interface GenerateSecretOptions { + /** + * Whether the generated CryptoKey is extractable. Defaults to false; has no effect for + * A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512, which return raw bytes. + */ + extractable?: boolean; +} +/** + * Generates a symmetric secret key for a given JWA algorithm identifier. + * + * > Note: The secret key is generated with `extractable` set to `false` by default. + * + * > Note: A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512 return {@link !Uint8Array} because these secrets + * > have no CryptoKey representation. + * + * @param alg JWA Algorithm Identifier to be used with the generated secret. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateSecret(alg: Alg, options?: GenerateSecretOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts new file mode 100644 index 000000000..88ba06a28 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts @@ -0,0 +1,67 @@ +import type * as t from '../types.d.ts'; +/** + * Maps a JWK key type to the value returned by {@link importJWK}. An "oct" JWK returns + * {@link !Uint8Array}; other supported key types return {@link t.CryptoKey CryptoKey}. A JWK + * whose "kty" is not statically known resolves to their union. + */ +export type ImportedJWK = JWKType extends { + kty: 'oct'; +} ? Uint8Array : JWKType extends { + kty: 'AKP' | 'EC' | 'OKP' | 'RSA'; +} ? t.CryptoKey : t.CryptoKey | Uint8Array; +/** Key import options. */ +export interface KeyImportOptions { + /** + * Whether the imported CryptoKey is extractable. Overrides JWK "ext" when set. Without either, + * defaults to false for private keys, true otherwise. + */ + extractable?: boolean; +} +/** + * Imports a PEM-encoded SPKI string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param spki PEM-encoded SPKI string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importSPKI(spki: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded X.509 certificate's public key as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param x509 X.509 certificate string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importX509(x509: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded PKCS#8 string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param pkcs8 PEM-encoded PKCS#8 string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importPKCS8(pkcs8: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a JWK as a {@link !CryptoKey} or {@link !Uint8Array}. Asymmetric imports require either the + * "alg" argument or JWK "alg" parameter. For AKP keys, the JWK "alg" parameter is required and must + * match the argument when provided. + * + * > Note: CryptoKey imports honor the JWK "key_ops" and "ext" parameters. + * + * > Note: Symmetric JWKs (`kty: "oct"`) return {@link !Uint8Array} instead of CryptoKey. + * + * @param jwk JSON Web Key. + * @param alg JSON Web Algorithm identifier to be used with the imported key. Default is the "alg" + * property on the JWK. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importJWK(jwk: JWKType, alg?: string, options?: KeyImportOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts new file mode 100644 index 000000000..6d8a665fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts @@ -0,0 +1,16 @@ +/** + * Decodes base64url text or its UTF-8 bytes. + * + * @param input Base64url-encoded string or its UTF-8 bytes. + * @returns The decoded bytes. + * @throws {TypeError} If the input is not valid base64url. Standard Base64 `+` and `/` are + * rejected. + */ +export declare function decode(input: Uint8Array | string): Uint8Array; +/** + * Encodes unpadded base64url; strings are first encoded as UTF-8. + * + * @param input Bytes or a string to encode. Strings are first encoded as UTF-8. + * @returns The unpadded base64url representation of the input. + */ +export declare function encode(input: Uint8Array | string): string; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts new file mode 100644 index 000000000..63ce31f0e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts @@ -0,0 +1,8 @@ +import type * as t from '../types.d.ts'; +/** + * Decodes the Claims Set of a JWT in Compact JWS serialization without checking its signature or + * validating claim types and values. + * + * @returns The parsed JWT Claims Set. + */ +export declare function decodeJwt(jwt: string): PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts new file mode 100644 index 000000000..2d74643d3 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** JWE and JWS Header Parameters returned by {@link decodeProtectedHeader}. */ +export type ProtectedHeaderParameters = t.JWSHeaderParameters & t.JWEHeaderParameters; +/** + * Decodes the Protected Header of a JWE, JWS, or JWT without authenticating the token. + * + * @param token Compact token or JSON serialization object with a `protected` member. + * @returns The parsed Protected Header. + */ +export declare function decodeProtectedHeader(token: string | object): ProtectedHeaderParameters; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js new file mode 100644 index 000000000..a41eeb626 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js @@ -0,0 +1,70 @@ +import { compactDecrypt } from "./jwe/compact/decrypt.js"; +import { flattenedDecrypt } from "./jwe/flattened/decrypt.js"; +import { generalDecrypt } from "./jwe/general/decrypt.js"; +import { GeneralEncrypt } from "./jwe/general/encrypt.js"; +import { compactVerify } from "./jws/compact/verify.js"; +import { flattenedVerify } from "./jws/flattened/verify.js"; +import { generalVerify } from "./jws/general/verify.js"; +import { jwtVerify } from "./jwt/verify.js"; +import { jwtDecrypt } from "./jwt/decrypt.js"; +import { CompactEncrypt } from "./jwe/compact/encrypt.js"; +import { FlattenedEncrypt } from "./jwe/flattened/encrypt.js"; +import { CompactSign } from "./jws/compact/sign.js"; +import { FlattenedSign } from "./jws/flattened/sign.js"; +import { GeneralSign } from "./jws/general/sign.js"; +import { SignJWT } from "./jwt/sign.js"; +import { EncryptJWT } from "./jwt/encrypt.js"; +import { calculateJwkThumbprint, calculateJwkThumbprintUri } from "./jwk/thumbprint.js"; +import { EmbeddedJWK } from "./jwk/embedded.js"; +import { createLocalJWKSet } from "./jwks/local.js"; +import { createRemoteJWKSet, jwksCache, customFetch } from "./jwks/remote.js"; +import { UnsecuredJWT } from "./jwt/unsecured.js"; +import { exportPKCS8, exportSPKI, exportJWK } from "./key/export.js"; +import { importSPKI, importPKCS8, importX509, importJWK } from "./key/import.js"; +import { decodeProtectedHeader } from "./util/decode_protected_header.js"; +import { decodeJwt } from "./util/decode_jwt.js"; +import * as errors from "./util/errors.js"; +import { generateKeyPair } from "./key/generate_key_pair.js"; +import { generateSecret } from "./key/generate_secret.js"; +import * as base64url from "./util/base64url.js"; +const cryptoRuntime = "WebCryptoAPI"; +export { + CompactEncrypt, + CompactSign, + EmbeddedJWK, + EncryptJWT, + FlattenedEncrypt, + FlattenedSign, + GeneralEncrypt, + GeneralSign, + SignJWT, + UnsecuredJWT, + base64url, + calculateJwkThumbprint, + calculateJwkThumbprintUri, + compactDecrypt, + compactVerify, + createLocalJWKSet, + createRemoteJWKSet, + cryptoRuntime, + customFetch, + decodeJwt, + decodeProtectedHeader, + errors, + exportJWK, + exportPKCS8, + exportSPKI, + flattenedDecrypt, + flattenedVerify, + generalDecrypt, + generalVerify, + generateKeyPair, + generateSecret, + importJWK, + importPKCS8, + importSPKI, + importX509, + jwksCache, + jwtDecrypt, + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js new file mode 100644 index 000000000..3c20fe5df --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js @@ -0,0 +1,7 @@ +import { prepareDecrypt, decryptCompact } from "../../lib/jwe_decrypt.js"; +async function compactDecrypt(jwe, key, options) { + return decryptCompact(jwe, prepareDecrypt(options), key); +} +export { + compactDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js new file mode 100644 index 000000000..a500eccd6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js @@ -0,0 +1,26 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { compactJWE, createJWE } from "../../lib/jwe_encrypt.js"; +class CompactEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + async encrypt(key, options) { + return compactJWE(await createJWE([...this.#input], key, options)); + } +} +export { + CompactEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js new file mode 100644 index 000000000..cba368907 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js @@ -0,0 +1,15 @@ +import { JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { prepareDecrypt, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +async function flattenedDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("Flattened JWE must be an object"); + const shared = snapshotSharedJWE(jwe), [recipient, , error] = snapshotRecipientJWE(jwe); + if (!recipient) + throw error; + const snapshot = { ...shared, ...recipient }; + return checkRecipient(snapshot), decryptJWE(snapshot, prepareDecrypt(options), key); +} +export { + flattenedDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js new file mode 100644 index 000000000..7ee1b91ed --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js @@ -0,0 +1,35 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { createJWE } from "../../lib/jwe_encrypt.js"; +class FlattenedEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#input[3], "setSharedUnprotectedHeader"), this.#input[3] = sharedUnprotectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#input[4] = aad, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + async encrypt(key, options) { + return createJWE([...this.#input], key, options); + } +} +export { + FlattenedEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js new file mode 100644 index 000000000..dfdb66003 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js @@ -0,0 +1,40 @@ +import { prepareDecrypt, shareJWE, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { JWE, isJWECEKTransport } from "../../lib/jwe_algorithms.js"; +async function generalDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("General JWE must be an object"); + const inputRecipients = jwe.recipients; + if (!Array.isArray(inputRecipients)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + const recipients = Array.from(inputRecipients); + if (!recipients.every(isObject)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + if (!recipients.length) + throw new JWEInvalid("JWE Recipients has no members"); + let shared, sharedJwe, token; + try { + shared = prepareDecrypt(options), sharedJwe = snapshotSharedJWE(jwe), token = shareJWE(sharedJwe); + } catch { + throw new JWEDecryptionFailed(); + } + const recipientSnapshots = recipients.map((recipient) => snapshotRecipientJWE(recipient)); + if (recipients.length > 1) + for (const [, headerAlg] of recipientSnapshots) { + const alg = token[0]?.alg ?? headerAlg ?? sharedJwe.unprotected?.alg, algEntry = typeof alg == "string" ? JWE[alg] : void 0; + if (algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + } + for (const [recipient] of recipientSnapshots) + if (recipient) + try { + const flattened = { ...sharedJwe, ...recipient }; + return checkRecipient(flattened), await decryptJWE(flattened, shared, key, token); + } catch { + } + throw new JWEDecryptionFailed(); +} +export { + generalDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js new file mode 100644 index 000000000..9fe068a8a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js @@ -0,0 +1,105 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { JWEInvalid } from "../../util/errors.js"; +import { generateCek } from "../../lib/content_encryption.js"; +import { encode as b64u } from "../../util/base64url.js"; +import { checkDisjoint, checkEncryptHeaders, encryptJWE } from "../../lib/jwe_encrypt.js"; +import { encryptKeyManagement } from "../../lib/key_management.js"; +import { isJWECEKTransport, jweAlgorithm } from "../../lib/jwe_algorithms.js"; +class IndividualRecipient { + #parent; + state; + constructor(enc, key, crit) { + this.#parent = enc, this.state = [void 0, void 0, key, crit]; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[0], "setUnprotectedHeader"), this.state[0] = unprotectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.state[1], "setKeyManagementParameters"), this.state[1] = parameters, this; + } + addRecipient(...args) { + return this.#parent.addRecipient(...args); + } + encrypt(...args) { + return this.#parent.encrypt(...args); + } + done() { + return this.#parent; + } +} +class GeneralEncrypt { + #plaintext; + #recipients = []; + #protectedHeader; + #unprotectedHeader; + #aad; + constructor(plaintext) { + this.#plaintext = plaintext; + } + addRecipient(key, options) { + const recipient = new IndividualRecipient(this, key, options?.crit); + return this.#recipients.push(recipient), recipient; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#unprotectedHeader, "setSharedUnprotectedHeader"), this.#unprotectedHeader = sharedUnprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#aad = aad, this; + } + async encrypt() { + if (!this.#recipients.length) + throw new JWEInvalid("at least one recipient must be added"); + assertUint8Array(this.#plaintext, "plaintext"); + const multiple = this.#recipients.length > 1; + let enc, protectedHeader = this.#protectedHeader, sharedUnprotectedHeader = this.#unprotectedHeader; + const recipients = []; + for (const recipient of this.#recipients) { + const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state, input = [ + this.#plaintext, + protectedHeader, + unprotectedHeader, + sharedUnprotectedHeader, + this.#aad, + void 0, + void 0, + keyManagementParameters, + crit, + multiple + ], headers = checkEncryptHeaders(input, void 0, recipients.length > 0); + recipients.length || (protectedHeader = input[1], sharedUnprotectedHeader = input[3]), recipients.push([input, headers, key]); + const [{ alg, enc: recipientEnc }, , algEntry] = headers; + if (multiple && algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + if (!enc) + enc = recipientEnc; + else if (enc !== recipientEnc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); + } + for (const [, headers] of recipients) + headers[2] ??= jweAlgorithm(headers[0].alg); + const [firstInput, firstHeaders, firstKey] = recipients[0], cek = multiple ? generateCek(firstHeaders[1]) : void 0; + firstInput[5] = cek; + const { encrypted_key, header, ...shared } = await encryptJWE(firstInput, firstHeaders, firstKey), jwe = { + ...shared, + recipients: [{}] + }; + encrypted_key && (jwe.recipients[0].encrypted_key = encrypted_key), header && (jwe.recipients[0].header = header); + for (let i = 1; i < recipients.length; i++) { + const [input, [joseHeader, encEntry, algEntry], key] = recipients[i], unprotectedHeader = input[2], [, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, cek, input[7]), target = { + encrypted_key: b64u(encryptedKey) + }; + if (unprotectedHeader || parameters) { + const header2 = { ...unprotectedHeader, ...parameters }; + parameters && checkDisjoint(input[1], header2, input[3]), target.header = header2; + } + jwe.recipients.push(target); + } + return jwe; + } +} +export { + GeneralEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js new file mode 100644 index 000000000..1e1dd89c4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js @@ -0,0 +1,30 @@ +import { jwkToKey, normalizeJwk } from "../lib/key.js"; +import { jwsAlgorithm } from "../lib/jws_algorithms.js"; +import { isObject } from "../lib/validate.js"; +import { JWSInvalid } from "../util/errors.js"; +async function EmbeddedJWK(protectedHeader, token) { + const joseHeader = { + ...protectedHeader, + ...token?.header + }; + if (!isObject(joseHeader.jwk)) + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object'); + let jwk; + try { + jwk = normalizeJwk(joseHeader.jwk); + } catch (cause) { + throw new JWSInvalid("Invalid Embedded JWK", { cause }); + } + const entry = jwsAlgorithm(joseHeader.alg); + if (jwk.use !== void 0 && jwk.use !== "sig") + throw new JWSInvalid('Invalid Embedded JWK, its "use" must be "sig" when present'); + if (jwk.alg !== void 0 && jwk.alg !== entry.alg) + throw new JWSInvalid(`Invalid Embedded JWK, its "alg" must be "${entry.alg}" when present`); + const key = await jwkToKey(entry, jwk, !0); + if (key.type !== "public") + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key'); + return key; +} +export { + EmbeddedJWK +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js new file mode 100644 index 000000000..ec93e222e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js @@ -0,0 +1,55 @@ +import { digest, encode } from "../lib/buffer_utils.js"; +import { encode as b64u } from "../util/base64url.js"; +import { JOSENotSupported, JWKInvalid } from "../util/errors.js"; +import { isKeyLike, invalidKeyInput, snapshotJwk } from "../lib/key.js"; +import { isObject } from "../lib/validate.js"; +import { exportJWK } from "../key/export.js"; +const check = (value, description) => { + if (typeof value != "string" || !value) + throw new JWKInvalid(`${description} missing or invalid`); +}; +async function calculateJwkThumbprint(key, digestAlgorithm) { + let jwk; + if (isObject(key)) { + if (jwk = snapshotJwk(key), typeof jwk.kty != "string") + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + } else if (isKeyLike(key)) + jwk = snapshotJwk(await exportJWK(key)); + else + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + if (digestAlgorithm ??= "sha256", digestAlgorithm !== "sha256" && digestAlgorithm !== "sha384" && digestAlgorithm !== "sha512") + throw new TypeError('digestAlgorithm must one of "sha256", "sha384", or "sha512"'); + let components; + switch (jwk.kty) { + case "AKP": + check(jwk.alg, '"alg" (Algorithm) Parameter'), check(jwk.pub, '"pub" (Public key) Parameter'), components = { alg: jwk.alg, kty: jwk.kty, pub: jwk.pub }; + break; + case "EC": + check(jwk.crv, '"crv" (Curve) Parameter'), check(jwk.x, '"x" (X Coordinate) Parameter'), check(jwk.y, '"y" (Y Coordinate) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + break; + case "OKP": + check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter'), check(jwk.x, '"x" (Public Key) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; + break; + case "RSA": + check(jwk.e, '"e" (Exponent) Parameter'), check(jwk.n, '"n" (Modulus) Parameter'), components = { e: jwk.e, kty: jwk.kty, n: jwk.n }; + break; + case "oct": + if (typeof jwk.k != "string") + throw new JWKInvalid('"k" (Key Value) Parameter missing or invalid'); + components = { k: jwk.k, kty: jwk.kty }; + break; + default: + throw new JOSENotSupported('"kty" (Key Type) Parameter missing or unsupported'); + } + const data = encode(JSON.stringify(components)); + return b64u(await digest(digestAlgorithm, data)); +} +async function calculateJwkThumbprintUri(key, digestAlgorithm) { + digestAlgorithm ??= "sha256"; + const thumbprint = await calculateJwkThumbprint(key, digestAlgorithm); + return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`; +} +export { + calculateJwkThumbprint, + calculateJwkThumbprintUri +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js new file mode 100644 index 000000000..448757265 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js @@ -0,0 +1,59 @@ +import { jwkToKey, snapshotJwk } from "../lib/key.js"; +import { JWS } from "../lib/jws_algorithms.js"; +import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys } from "../util/errors.js"; +import { isJwkSet } from "../lib/validate.js"; +function isUsableJWK(jwk, entry, alg, kid) { + const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk; + return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv); +} +async function importWithAlgCache(cache, jwk, entry) { + const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk), { alg } = entry; + if (cached[alg] === void 0) { + const pending = jwkToKey(entry, jwk, !0).then((key) => { + if (key.type !== "public") + throw new JWKSInvalid("JSON Web Key Set members must be public keys"); + return cached[alg] = key, key; + }).catch((error) => { + throw cached[alg] === pending && delete cached[alg], error; + }); + cached[alg] = pending; + } + return cached[alg]; +} +function createLocalJWKSet(jwks) { + let snapshot; + try { + snapshot = structuredClone(jwks); + } catch { + } + if (!isJwkSet(snapshot)) + throw new JWKSInvalid("JSON Web Key Set malformed"); + const metadata = snapshot.keys.map((jwk) => { + const normalized = snapshotJwk(jwk); + return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized; + }), cached = /* @__PURE__ */ new WeakMap(); + return Object.defineProperty(async (protectedHeader, token) => { + const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry || entry.secret) + throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); + const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates; + if (!length) + throw new JWKSNoMatchingKey(); + if (length !== 1) { + const error = new JWKSMultipleMatchingKeys(); + throw error[Symbol.asyncIterator] = async function* () { + for (const jwk2 of candidates) + try { + yield await importWithAlgCache(cached, jwk2, entry); + } catch { + } + }, error; + } + return importWithAlgCache(cached, jwk, entry); + }, "jwks", { + value: () => structuredClone(snapshot) + }); +} +export { + createLocalJWKSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js new file mode 100644 index 000000000..39519f27b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js @@ -0,0 +1,101 @@ +import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js"; +import { createLocalJWKSet } from "./local.js"; +import { isJwkSet } from "../lib/validate.js"; +function isCloudflareWorkers() { + return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel"; +} +let USER_AGENT; +(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12"); +const customFetch = /* @__PURE__ */ Symbol(); +async function fetchJwks(url, headers, signal, fetchImpl = fetch) { + const response = await fetchImpl(url, { + method: "GET", + signal, + redirect: "manual", + headers + }).catch((err) => { + throw err.name === "TimeoutError" ? new JWKSTimeout() : err; + }); + if (response.status !== 200) + throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); + try { + return await response.json(); + } catch { + throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); + } +} +const jwksCache = /* @__PURE__ */ Symbol(); +function isFreshFor(timestamp, duration) { + return Number.isFinite(timestamp) && Date.now() < timestamp + duration; +} +function validateDuration(value, fallback, option) { + if (Number.isNaN(value)) + throw new TypeError(`"${option}" option must not be NaN`); + return typeof value == "number" ? value : fallback; +} +function createRemoteJWKSet(url, options) { + if (!(url instanceof URL)) + throw new TypeError("url must be an instance of URL"); + const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration; + if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) + throw new TypeError('"timeoutDuration" option must be a non-negative integer'); + const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers); + USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json"); + const fetchImpl = opts[customFetch], cache = opts[jwksCache]; + let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local; + if (cache && typeof cache == "object") { + const { uat, jwks } = cache; + isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks)); + } + const reload = async () => { + if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) { + const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => { + const next = createLocalJWKSet(json); + if (sequence <= appliedSequence) + return; + local = next; + const updatedAt = Date.now(); + cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence; + }).finally(() => { + pendingFetch === current && (pendingFetch = void 0); + }); + } + await pendingFetch; + }; + return Object.defineProperties(async (protectedHeader, token) => { + (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload(); + try { + return await local(protectedHeader, token); + } catch (err) { + if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) + return await reload(), local(protectedHeader, token); + throw err; + } + }, { + coolingDown: { + get: () => isFreshFor(jwksTimestamp, cooldownDuration), + enumerable: !0 + }, + fresh: { + get: () => isFreshFor(jwksTimestamp, cacheMaxAge), + enumerable: !0 + }, + reload: { + value: reload, + enumerable: !0 + }, + reloading: { + get: () => !!pendingFetch, + enumerable: !0 + }, + jwks: { + value: () => local?.jwks(), + enumerable: !0 + } + }); +} +export { + createRemoteJWKSet, + customFetch, + jwksCache +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js new file mode 100644 index 000000000..f3288d2f0 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js @@ -0,0 +1,20 @@ +import { createCompactSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class CompactSign { + #payload; + #protectedHeader; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#payload = payload; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(this.#payload, this.#protectedHeader, options?.crit, key, () => { + throw new TypeError("use the flattened module for creating JWS with b64: false"); + }); + } +} +export { + CompactSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js new file mode 100644 index 000000000..49625ee85 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js @@ -0,0 +1,8 @@ +import { prepareVerify, verifyCompact } from "../../lib/jws_verify.js"; +async function compactVerify(jws, key, options) { + const [result] = await verifyCompact(jws, prepareVerify(options), key); + return result; +} +export { + compactVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js new file mode 100644 index 000000000..7e321402f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js @@ -0,0 +1,23 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class FlattenedSign { + #input; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#input = [payload]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + async sign(key, options) { + const input = [...this.#input]; + input[3] = options?.crit; + const [jws] = await createSignature(input, key); + return jws; + } +} +export { + FlattenedSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js new file mode 100644 index 000000000..f4a7c9541 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js @@ -0,0 +1,12 @@ +import { JWSInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { encodeJsonUnencodedPayload, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +async function flattenedVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("Flattened JWS must be an object"); + const snapshot = snapshotJws(jws), [result] = await verifySignature(snapshot, prepareVerify(options), key, encodeJsonUnencodedPayload); + return result; +} +export { + flattenedVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js new file mode 100644 index 000000000..8f76dd00b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js @@ -0,0 +1,58 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { JWSInvalid } from "../../util/errors.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class IndividualSignature { + #parent; + state; + constructor(sig, key, options) { + this.#parent = sig, this.state = [void 0, void 0, key, options?.crit]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.state[0], "setProtectedHeader"), this.state[0] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[1], "setUnprotectedHeader"), this.state[1] = unprotectedHeader, this; + } + addSignature(...args) { + return this.#parent.addSignature(...args); + } + sign(...args) { + return this.#parent.sign(...args); + } + done() { + return this.#parent; + } +} +class GeneralSign { + #payload; + #signatures = []; + constructor(payload) { + this.#payload = payload; + } + addSignature(key, options) { + const signature = new IndividualSignature(this, key, options); + return this.#signatures.push(signature), signature; + } + async sign() { + if (!this.#signatures.length) + throw new JWSInvalid("at least one signature must be added"); + assertUint8Array(this.#payload, "payload"); + const jws = { + signatures: [], + payload: "" + }, encoded = []; + let b64; + for (const signature of this.#signatures) { + const [protectedHeader, unprotectedHeader, key, crit] = signature.state, [{ payload, ...rest }, signatureB64] = await createSignature([this.#payload, protectedHeader, unprotectedHeader, crit, encoded], key); + if (b64 === void 0) + b64 = signatureB64, jws.payload = payload; + else if (b64 !== signatureB64) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + jws.signatures.push(rest); + } + return jws; + } +} +export { + GeneralSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js new file mode 100644 index 000000000..3243408b8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js @@ -0,0 +1,48 @@ +import { encodeJsonUnencodedPayload, parseProtectedHeader, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +import { JWSInvalid, JWSSignatureVerificationFailed } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +function snapshotSignature(signature, payload) { + try { + const jws = snapshotJws(signature, [payload]), protectedHeader = parseProtectedHeader(jws.protected), { b64, crit } = protectedHeader; + return [ + jws, + protectedHeader, + Array.isArray(crit) && crit.includes("b64") ? typeof b64 == "boolean" ? b64 ? 1 : 2 : 0 : 1 + ]; + } catch { + return; + } +} +async function generalVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("General JWS must be an object"); + const { signatures, payload: inputPayload } = jws; + if (!Array.isArray(signatures)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + const signatureEntries = Array.from(signatures); + if (!signatureEntries.every(isObject)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + let shared; + try { + if (inputPayload === void 0) + throw new Error(); + shared = prepareVerify(options); + } catch { + throw new JWSSignatureVerificationFailed(); + } + const payload = inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload, candidates = signatureEntries.map((signature) => snapshotSignature(signature, payload)).filter((candidate) => candidate !== void 0); + let modes = 0; + for (const [, , mode] of candidates) + if (modes |= mode, modes === 3) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + for (const candidate of candidates) + try { + const [result] = await verifySignature(candidate[0], shared, key, encodeJsonUnencodedPayload, candidate[1]); + return result; + } catch { + } + throw new JWSSignatureVerificationFailed(); +} +export { + generalVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 000000000..cfbe4730c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js @@ -0,0 +1,13 @@ +import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTClaimValidationFailed } from "../util/errors.js"; +async function jwtDecrypt(jwt, key, options) { + const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options); + for (const claim of ["iss", "sub", "aud"]) + if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim])) + throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch"); + return { payload, ...result }; +} +export { + jwtDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 000000000..c26908857 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js @@ -0,0 +1,45 @@ +import { compactJWE, createJWE } from "../lib/jwe_encrypt.js"; +import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const EncryptJWT_base = JWTClaimsBuilder; +class EncryptJWT extends EncryptJWT_base { + #input = [void 0]; + #replicateIssuerAsHeader; + #replicateSubjectAsHeader; + #replicateAudienceAsHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + replicateIssuerAsHeader() { + return this.#replicateIssuerAsHeader = !0, this; + } + replicateSubjectAsHeader() { + return this.#replicateSubjectAsHeader = !0, this; + } + replicateAudienceAsHeader() { + return this.#replicateAudienceAsHeader = !0, this; + } + async encrypt(key, options) { + const plaintext = jwtData(this); + this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = { + ...this.#input[1], + iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0, + sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0, + aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0 + }); + const input = [...this.#input]; + return input[0] = plaintext, compactJWE(await createJWE(input, key, options)); + } +} +export { + EncryptJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js new file mode 100644 index 000000000..0f01cc7a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js @@ -0,0 +1,34 @@ +import * as b64u from "../util/base64url.js"; +import { decodeBase64url, parseJoseHeader, JWS_RECOGNIZED, validateB64, validateCrit } from "../lib/validate.js"; +import { JWSInvalid, JWTInvalid } from "../util/errors.js"; +import { validateClaimsSet, JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +const UnsecuredJWT_base = JWTClaimsBuilder; +class UnsecuredJWT extends UnsecuredJWT_base { + encode() { + const header = b64u.encode(JSON.stringify({ alg: "none" })), payload = b64u.encode(jwtData(this)); + return `${header}.${payload}.`; + } + static decode(jwt, options) { + if (typeof jwt != "string") + throw new JWTInvalid("Unsecured JWT must be a string"); + const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split("."); + if (length !== 3 || signature !== "") + throw new JWTInvalid("Invalid Unsecured JWT"); + let header, b64; + try { + header = parseJoseHeader(encodedHeader, JWSInvalid, "JWS Protected Header is invalid"); + const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, void 0, header, header); + b64 = validateB64(header, extensions); + } catch (cause) { + throw cause instanceof JWSInvalid ? new JWTInvalid("Invalid Unsecured JWT", { cause }) : cause; + } + if (header.alg !== "none") + throw new JWTInvalid("Invalid Unsecured JWT"); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + return { payload: validateClaimsSet(header, decodeBase64url(encodedPayload, "payload", JWTInvalid), options), header }; + } +} +export { + UnsecuredJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js new file mode 100644 index 000000000..49016c5a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js @@ -0,0 +1,35 @@ +import { toSPKI as exportPublic, toPKCS8 as exportPrivate } from "../lib/asn1.js"; +import { invalidKeyInput, isCryptoKey, isKeyObject } from "../lib/key.js"; +import { encode as b64u } from "../util/base64url.js"; +function exportSPKI(key) { + return exportPublic(key); +} +function exportPKCS8(key) { + return exportPrivate(key); +} +async function exportJWK(key) { + if (isKeyObject(key)) + if (key.type === "secret") + key = key.export(); + else + return key.export({ format: "jwk" }); + if (key instanceof Uint8Array) + return { + kty: "oct", + k: b64u(key) + }; + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "Uint8Array")); + if (!key.extractable) + throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK"); + const jwk = await crypto.subtle.exportKey("jwk", key); + delete jwk.ext, delete jwk.key_ops, delete jwk.use, jwk.kty !== "AKP" && delete jwk.alg; + for (const parameter of Object.keys(jwk)) + jwk[parameter] === void 0 && delete jwk[parameter]; + return jwk; +} +export { + exportJWK, + exportPKCS8, + exportSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js new file mode 100644 index 000000000..c687f420d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js @@ -0,0 +1,35 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { validateExtractableOption } from "../lib/key.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +function getModulusLengthOption(options) { + const modulusLength = options?.modulusLength ?? 2048; + if (typeof modulusLength != "number" || !Number.isInteger(modulusLength) || modulusLength < 2048) + throw new JOSENotSupported("Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used"); + return modulusLength; +} +async function generateKeyPair(alg, options) { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + let algorithm; + if (entry.resolve) { + const crv = options?.crv ?? "P-256"; + if (!["P-256", "P-384", "P-521", "X25519"].includes(crv)) + throw new JOSENotSupported("Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519"); + algorithm = entry.resolve({ crv }); + } else { + if (entry.crv !== void 0 && options?.crv !== void 0 && options.crv !== entry.crv) + throw new JOSENotSupported(`Invalid or unsupported crv option provided, the only supported value for ${alg} is ${entry.crv}`); + algorithm = entry.kty[0] === "RSA" ? { + ...entry.subtle, + publicExponent: Uint8Array.of(1, 0, 1), + modulusLength: getModulusLengthOption(options) + } : entry.subtle; + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, [ + ...entry.usages[1], + ...entry.usages[0] + ]); +} +export { + generateKeyPair +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js new file mode 100644 index 000000000..b085b4919 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js @@ -0,0 +1,36 @@ +import { unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +import { validateExtractableOption } from "../lib/key.js"; +async function generateSecret(alg, options) { + const extractable = validateExtractableOption(options?.extractable); + let length, algorithm, keyUsages; + switch (alg) { + case "HS256": + case "HS384": + case "HS512": + length = +alg.slice(-3), algorithm = { name: "HMAC", hash: `SHA-${length}`, length }, keyUsages = ["sign", "verify"]; + break; + case "A128CBC-HS256": + case "A192CBC-HS384": + case "A256CBC-HS512": + return crypto.getRandomValues(new Uint8Array(+alg.slice(-3) >> 3)); + case "A128KW": + case "A192KW": + case "A256KW": + length = +alg.slice(1, 4), algorithm = { name: "AES-KW", length }, keyUsages = ["wrapKey", "unwrapKey"]; + break; + case "A128GCMKW": + case "A192GCMKW": + case "A256GCMKW": + case "A128GCM": + case "A192GCM": + case "A256GCM": + length = +alg.slice(1, 4), algorithm = { name: "AES-GCM", length }, keyUsages = ["encrypt", "decrypt"]; + break; + default: + unsupportedAlg(algArgument); + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, keyUsages); +} +export { + generateSecret +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js new file mode 100644 index 000000000..b171ae5de --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js @@ -0,0 +1,53 @@ +import { decode as decodeBase64URL } from "../util/base64url.js"; +import { fromSPKI, fromPKCS8, fromX509 } from "../lib/asn1.js"; +import { jwkToKey, validateExtractableOption, normalizeJwk } from "../lib/key.js"; +import { keyAlgorithm } from "../lib/key_algorithm.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { isObject } from "../lib/validate.js"; +async function importSPKI(spki, alg, options) { + if (typeof spki != "string" || spki.indexOf("-----BEGIN PUBLIC KEY-----") !== 0) + throw new TypeError('"spki" must be SPKI formatted string'); + return fromSPKI(spki, alg, options); +} +async function importX509(x509, alg, options) { + if (typeof x509 != "string" || x509.indexOf("-----BEGIN CERTIFICATE-----") !== 0) + throw new TypeError('"x509" must be X.509 formatted string'); + return fromX509(x509, alg, options); +} +async function importPKCS8(pkcs8, alg, options) { + if (typeof pkcs8 != "string" || pkcs8.indexOf("-----BEGIN PRIVATE KEY-----") !== 0) + throw new TypeError('"pkcs8" must be PKCS#8 formatted string'); + return fromPKCS8(pkcs8, alg, options); +} +async function importJWK(jwk, alg, options) { + if (!isObject(jwk)) + throw new TypeError("JWK must be an object"); + const normalized = normalizeJwk(jwk), extractable = validateExtractableOption(options?.extractable), { alg: jwkAlg } = normalized; + if (alg ??= jwkAlg, normalized.kty !== "oct" && !alg) + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + switch (normalized.kty) { + case "oct": + if (typeof normalized.k != "string") + throw new TypeError('missing "k" (Key Value) Parameter value'); + return decodeBase64URL(normalized.k); + case "AKP": { + if (typeof jwkAlg != "string" || !jwkAlg) + throw new TypeError('missing "alg" (Algorithm) Parameter value'); + if (alg !== jwkAlg) + throw new TypeError("JWK alg and alg option value mismatch"); + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + } + case "RSA": + case "EC": + case "OKP": + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } +} +export { + importJWK, + importPKCS8, + importSPKI, + importX509 +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js new file mode 100644 index 000000000..51f781b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js @@ -0,0 +1,138 @@ +import { invalidKeyInput, isCryptoKey, isKeyObject, validateExtractableOption } from "./key.js"; +import { encodeBase64, decodeBase64 } from "./buffer_utils.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "./key_algorithm.js"; +const formatPEM = (b64, descriptor) => { + const newlined = (b64.match(/.{1,64}/g) || []).join(` +`); + return `-----BEGIN ${descriptor}----- +${newlined} +-----END ${descriptor}-----`; +}, genericExport = async (keyType, keyFormat, key) => { + if (isKeyObject(key)) { + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return key.export({ format: "pem", type: keyFormat }); + } + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject")); + if (!key.extractable) + throw new TypeError("CryptoKey is not extractable"); + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`); +}, toSPKI = (key) => genericExport("public", "spki", key), toPKCS8 = (key) => genericExport("private", "pkcs8", key), bytesEqual = (a, b) => { + if (a.byteLength !== b.length) + return !1; + for (let i = 0; i < a.byteLength; i++) + if (a[i] !== b[i]) + return !1; + return !0; +}, createASN1State = (data) => ({ data, pos: 0 }), readByte = (state) => { + const byte = state.data[state.pos++]; + if (byte === void 0) + throw new Error("Unexpected end of ASN.1 input"); + return byte; +}, parseLength = (state) => { + const first = readByte(state); + if (first & 128) { + const lengthOfLen = first & 127; + let length = 0; + for (let i = 0; i < lengthOfLen; i++) + length = length << 8 | readByte(state); + return length; + } + return first; +}, skipElement = (state, count = 1) => { + for (; count-- > 0; ) { + state.pos++; + const length = parseLength(state); + state.pos += length; + } +}, expectTag = (state, expectedTag, errorMessage) => { + if (readByte(state) !== expectedTag) + throw new Error(errorMessage); +}, getSubarray = (state, length) => { + if (length < 0 || state.pos + length > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + const result = state.data.subarray(state.pos, state.pos + length); + return state.pos += length, result; +}, parseAlgorithmOID = (state) => { + expectTag(state, 6, "Expected algorithm OID"); + const oidLen = parseLength(state); + return getSubarray(state, oidLen); +}; +function parseKeyHeader(state, keyFormat) { + if (expectTag(state, 48, `Invalid ${keyFormat === "spki" ? "SPKI" : "PKCS#8"} structure`), parseLength(state), keyFormat === "pkcs8") { + expectTag(state, 2, "Expected version field"); + const length = parseLength(state); + state.pos += length; + } + expectTag(state, 48, "Expected algorithm identifier"), parseLength(state); +} +const parseECAlgorithmIdentifier = (state) => { + const algOid = parseAlgorithmOID(state); + if (bytesEqual(algOid, [43, 101, 110])) + return "X25519"; + if (!bytesEqual(algOid, [42, 134, 72, 206, 61, 2, 1])) + throw new Error("Unsupported key algorithm"); + expectTag(state, 6, "Expected curve OID"); + const curveOidLen = parseLength(state), curveOid = getSubarray(state, curveOidLen); + if (bytesEqual(curveOid, [42, 134, 72, 206, 61, 3, 1, 7])) + return "P-256"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 34])) + return "P-384"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 35])) + return "P-521"; + throw new Error("Unsupported named curve"); +}, genericImport = async (keyFormat, keyData, alg, options) => { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + const isPublic = keyFormat === "spki"; + let algorithm; + if (entry.resolve) + try { + const state = createASN1State(keyData); + parseKeyHeader(state, keyFormat), algorithm = entry.resolve({ crv: parseECAlgorithmIdentifier(state) }); + } catch { + throw new JOSENotSupported("Invalid or unsupported key format"); + } + else + algorithm = entry.subtle; + return crypto.subtle.importKey(keyFormat, keyData, algorithm, extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); +}, processPEMData = (pem, pattern) => decodeBase64(pem.replace(pattern, "")), fromPKCS8 = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g); + return genericImport("pkcs8", keyData, alg, options); +}, fromSPKI = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g); + return genericImport("spki", keyData, alg, options); +}; +function spkiFromX509(buf) { + const state = createASN1State(buf); + expectTag(state, 48, "Invalid certificate structure"); + const certificateLength = parseLength(state); + if (certificateLength < 0 || state.pos + certificateLength > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + expectTag(state, 48, "Invalid tbsCertificate structure"), parseLength(state), buf[state.pos] === 160 ? skipElement(state, 6) : skipElement(state, 5); + const spkiStart = state.pos; + expectTag(state, 48, "Invalid SPKI structure"); + const spkiContentLen = parseLength(state); + return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart)); +} +const fromX509 = (pem, alg, options) => { + let spki; + try { + const certificate = processPEMData(pem, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); + spki = spkiFromX509(certificate); + } catch (cause) { + throw new TypeError("Failed to parse the X.509 certificate", { cause }); + } + return genericImport("spki", spki, alg, options); +}; +export { + fromPKCS8, + fromSPKI, + fromX509, + toPKCS8, + toSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 000000000..1ea355e91 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js @@ -0,0 +1,90 @@ +import { concat, uint64be } from "./buffer_utils.js"; +import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js"; +const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3)); +function checkCekLength(cek, expected) { + const actual = cek.byteLength << 3; + if (actual !== expected) + throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`); +} +const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3)); +function checkIvLength(enc, iv) { + if (iv.length << 3 !== enc.ivBits) + throw new JWEInvalid("Invalid Initialization Vector length"); +} +async function cbcKeySetup(enc, cek, usage) { + if (!(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "Uint8Array")); + const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: "HMAC" + }, !1, ["sign"]); + return [encKey, macKey, keySize]; +} +async function cbcHmacTag(macKey, macData, keySize) { + return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3)); +} +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: "AES-CBC" + }, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize); + return { ciphertext, tag, iv }; +} +async function timingSafeEqual(a, b) { + const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a); + return crypto.subtle.verify(algorithm, key, aHmac, b); +} +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize); + try { + if (await timingSafeEqual(tag, expectedTag)) + return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext)); + } catch { + } + throw new JWEDecryptionFailed(); +} +async function encrypt(enc, plaintext, cek, iv, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcEncrypt(enc, plaintext, cek, iv, aad); + const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, plaintext)); + return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv }; +} +async function decrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (!iv) + throw new JWEInvalid("JWE Initialization Vector missing"); + if (!tag) + throw new JWEInvalid("JWE Authentication Tag missing"); + if (!enc.cbc && tag.length !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + const encKey = await rawKey(cek, enc.subtle, "decrypt"); + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, concat(ciphertext, tag))); + } catch { + throw new JWEDecryptionFailed(); + } +} +export { + checkCekLength, + checkIvLength, + decrypt, + encrypt, + generateCek, + generateIv +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 000000000..27ffba278 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js @@ -0,0 +1,40 @@ +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { concat } from "./buffer_utils.js"; +function validateZip(joseHeader, protectedHeader) { + if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") + throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); + if (joseHeader.zip !== void 0 && !protectedHeader?.zip) + throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); +} +function supported(name) { + if (typeof globalThis[name] > "u") + throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`); +} +async function transform(stream, input, maxLength = 1 / 0) { + const writer = stream.writable.getWriter(); + writer.write(input).catch(() => { + }), writer.close().catch(() => { + }); + const chunks = []; + let length = 0; + const reader = stream.readable.getReader(); + for (; ; ) { + const { value, done } = await reader.read(); + if (done) + break; + if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength) + throw new JWEInvalid("Decompressed plaintext exceeded the configured limit"); + } + return concat(...chunks); +} +async function compress(input) { + return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input); +} +async function decompress(input, maxLength) { + return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength); +} +export { + compress, + decompress, + validateZip +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 000000000..a87af4eb5 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js @@ -0,0 +1,116 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const wrap = [ + ["encrypt", "wrapKey"], + ["decrypt", "unwrapKey"] +], derive = [[], ["deriveBits"]], none = [[], []]; +function rsaes(bits) { + return { + kty: ["RSA"], + mode: "key-encryption", + subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` }, + usages: wrap, + ops: ["wrapKey", "unwrapKey"] + }; +} +function ecdh(mode) { + return { + kty: ["EC", "OKP"], + mode, + subtle: { name: "ECDH" }, + resolve: ({ kty, crv, asymmetricKeyType }) => { + if (crv === "X25519" || asymmetricKeyType === "x25519") + return { name: "X25519" }; + if (kty === "OKP") + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + return { name: "ECDH", namedCurve: crv }; + }, + usages: derive, + ops: [void 0, "deriveBits"] + }; +} +function aeskw(bits, gcm = !1) { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits }, + usages: none, + ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"] + }; +} +function pbes2() { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: "PBKDF2" }, + usages: none, + ops: ["deriveBits", "deriveBits"] + }; +} +const JWE = table({ + dir: { + kty: ["oct"], + mode: "direct-encryption", + secret: !0, + subtle: { name: "AES-GCM" }, + usages: none, + ops: ["encrypt", "decrypt"] + }, + "RSA-OAEP": rsaes(1), + "RSA-OAEP-256": rsaes(256), + "RSA-OAEP-384": rsaes(384), + "RSA-OAEP-512": rsaes(512), + "ECDH-ES": ecdh("direct-key-agreement"), + "ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"), + A128KW: aeskw(128), + A192KW: aeskw(192), + A256KW: aeskw(256), + A128GCMKW: aeskw(128, !0), + A192GCMKW: aeskw(192, !0), + A256GCMKW: aeskw(256, !0), + "PBES2-HS256+A128KW": pbes2(), + "PBES2-HS384+A192KW": pbes2(), + "PBES2-HS512+A256KW": pbes2() +}), contentOps = ["encrypt", "decrypt"]; +function contentEncryption(bits, cbc = !1) { + return { + kty: ["oct"], + secret: !0, + subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits }, + usages: none, + ops: contentOps, + cekBits: bits, + ivBits: cbc ? 128 : 96, + cbc + }; +} +const ENC = table({ + A128GCM: contentEncryption(128), + A192GCM: contentEncryption(192), + A256GCM: contentEncryption(256), + "A128CBC-HS256": contentEncryption(256, !0), + "A192CBC-HS384": contentEncryption(384, !0), + "A256CBC-HS512": contentEncryption(512, !0) +}); +function unsupported(parameter, name) { + throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`); +} +function jweAlgorithm(alg) { + return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm"); +} +function isJWECEKTransport(algorithm) { + return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping"; +} +function jweEncryption(enc) { + return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm"); +} +export { + JWE, + isJWECEKTransport, + jweAlgorithm, + jweEncryption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 000000000..e5453b9ba --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js @@ -0,0 +1,187 @@ +import { decrypt, generateCek } from "./content_encryption.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js"; +import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decryptKeyManagement } from "./key_management.js"; +import { decoder } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { decompress, validateZip } from "./deflate.js"; +function snapshotSharedJWE(jwe) { + const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; + if (iv !== void 0 && (typeof iv != "string" || !iv)) + throw new JWEInvalid("JWE Initialization Vector incorrect type"); + if (typeof ciphertext != "string") + throw new JWEInvalid("JWE Ciphertext missing or incorrect type"); + if (tag !== void 0 && (typeof tag != "string" || !tag)) + throw new JWEInvalid("JWE Authentication Tag incorrect type"); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWEInvalid("JWE Protected Header incorrect type"); + if (aad !== void 0 && (typeof aad != "string" || !aad)) + throw new JWEInvalid("JWE AAD incorrect type"); + if (unprotected !== void 0 && !isObject(unprotected)) + throw new JWEInvalid("JWE Shared Unprotected Header incorrect type"); + return { + aad, + ciphertext, + iv, + protected: encodedProtected, + tag, + unprotected: unprotected === void 0 ? void 0 : { ...unprotected } + }; +} +function snapshotRecipientJWE(recipient) { + let header, headerAlg; + try { + const { header: inputHeader } = recipient; + if (isObject(inputHeader)) { + headerAlg = inputHeader.alg; + const parameters = Object.keys(inputHeader); + parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [ + parameter, + parameter === "alg" ? headerAlg : inputHeader[parameter] + ])); + } else + header = inputHeader; + const { encrypted_key: encryptedKey } = recipient; + return [{ encrypted_key: encryptedKey, header }, headerAlg]; + } catch (error) { + return [void 0, headerAlg, error]; + } +} +function checkRecipient(jwe) { + const { encrypted_key: encryptedKey, header } = jwe; + if (encryptedKey !== void 0 && typeof encryptedKey != "string") + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type"); + if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0) + throw new JWEInvalid("JOSE Header missing"); +} +function shareJWE(jwe) { + const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; + let parsedProt; + return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [ + parsedProt, + decodeBase64url(ciphertext, "ciphertext", JWEInvalid), + iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0, + tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0, + encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid) + ]; +} +function prepareDecrypt(options) { + return [ + options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms), + options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms), + options?.crit, + options?.maxPBES2Count, + options?.maxDecompressedLength + ]; +} +async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) { + const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe; + let joseHeader; + if (header !== void 0 || unprotected !== void 0) { + if (!isDisjoint(parsedProt, header, unprotected)) + throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint"); + joseHeader = { ...parsedProt, ...header, ...unprotected }; + } else + joseHeader = parsedProt ?? {}; + const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe; + validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header"); + const selected = JWE[alg]; + if (encodedKey === "" && (!selected || !isJWECEKTransport(selected))) + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + const integrated = selected?.mode === "integrated-encryption"; + if (!integrated && (typeof enc != "string" || !enc)) + throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header"); + if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2")) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + let encEntry; + if (integrated) { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (iv?.byteLength) + throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption"); + if (tag?.byteLength) + throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption"); + } else { + if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) + throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); + encEntry = jweEncryption(enc); + } + let encryptedKey; + if (encodedKey !== void 0) + try { + encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid); + } catch (error) { + if (!selected || !isJWECEKTransport(selected)) + throw error; + encryptedKey = new Uint8Array(); + } + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0); + const algEntry = selected ?? jweAlgorithm(alg); + isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array()); + const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt"); + let plaintext; + if (algEntry.mode === "integrated-encryption") + plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader); + else { + const encryption = encEntry; + let cek; + try { + cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption)); + } catch (err) { + if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) + throw err; + cek = generateCek(encryption); + } + plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData); + } + if (joseHeader.zip === "DEF") { + const decompressionLimit = maxDecompressedLength ?? 25e4; + if (decompressionLimit === 0) + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); + if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) + throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity"); + plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { + throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause }); + }); + } + return { + plaintext, + ...parsedProt && { protectedHeader: parsedProt }, + ...aad !== void 0 && { + additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid) + }, + ...unprotected && { sharedUnprotectedHeader: unprotected }, + ...header && { unprotectedHeader: header }, + ...resolvedKey && { key: k } + }; +} +async function decryptCompact(jwe, shared, key) { + if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string") + throw new JWEInvalid("Compact JWE must be a string or Uint8Array"); + const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split("."); + if (length !== 5) + throw new JWEInvalid("Invalid Compact JWE"); + return decryptJWE({ + ciphertext, + iv: iv || void 0, + protected: protectedHeader, + tag: tag || void 0, + encrypted_key: encryptedKey || void 0 + }, shared, key); +} +export { + checkRecipient, + decryptCompact, + decryptJWE, + prepareDecrypt, + shareJWE, + snapshotRecipientJWE, + snapshotSharedJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 000000000..5ab33766b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js @@ -0,0 +1,75 @@ +import { encode as b64u } from "../util/base64url.js"; +import { encrypt } from "./content_encryption.js"; +import { encryptKeyManagement } from "./key_management.js"; +import { JWEInvalid } from "../util/errors.js"; +import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js"; +import { encode } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { compress, validateZip } from "./deflate.js"; +function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { + if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) + throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint"); +} +function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) { + if (!input[1] && !input[2] && !input[3]) + throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()"); + options !== void 0 && (input[8] = options?.crit); + let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input; + if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters)) + throw new TypeError("JWE Key Management Parameters must be an object"); + checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); + const joseHeader = { + ...protectedHeader, + ...unprotectedHeader, + ...sharedUnprotectedHeader + }; + validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); + const algEntry = JWE[alg]; + if (algEntry?.mode === "integrated-encryption") { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (cek !== void 0) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + if (iv !== void 0) + throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + return [joseHeader, void 0, algEntry]; + } + if (typeof enc != "string" || !enc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); + return [joseHeader, jweEncryption(enc), algEntry]; +} +async function encryptJWE(input, checked, key) { + const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input; + let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader; + const algEntry = selected ?? jweAlgorithm(joseHeader.alg); + let encryptedKey, parameters, cek; + algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)); + const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS); + let plaintext = inputPlaintext; + joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => { + throw new JWEInvalid("Failed to compress plaintext", { cause }); + })); + let ciphertext, tag, iv; + algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); + const jwe = { + ciphertext: b64u(ciphertext) + }; + return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe; +} +async function createJWE(input, key, options) { + return encryptJWE(input, checkEncryptHeaders(input, options), key); +} +function compactJWE(jwe) { + return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join("."); +} +export { + checkDisjoint, + checkEncryptHeaders, + compactJWE, + createJWE, + encryptJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js new file mode 100644 index 000000000..8a4d3fc93 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js @@ -0,0 +1,15 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { JWS } from "./jws_algorithms.js"; +import { JWE } from "./jwe_algorithms.js"; +const algArgument = '"alg" (Algorithm)'; +function unsupportedAlg(source = 'JWK "alg" (Algorithm) Parameter') { + throw new JOSENotSupported(`Invalid or unsupported ${source} value`); +} +function keyAlgorithm(alg, source) { + return (typeof alg == "string" ? JWS[alg] ?? JWE[alg] : void 0) ?? unsupportedAlg(source); +} +export { + algArgument, + keyAlgorithm, + unsupportedAlg +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 000000000..7b7533b70 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js @@ -0,0 +1,196 @@ +import { encode as b64u } from "../util/base64url.js"; +import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js"; +import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js"; +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decodeBase64url, assertUint8Array, isObject } from "./validate.js"; +import { digest, concat, encode, uint32be } from "./buffer_utils.js"; +import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js"; +function checkEcdhCryptoKey(key, usage) { + if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") + throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519"); + checkUsage(key, usage); +} +async function aeskwWrap(alg, key, cek) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW")); +} +async function aeskwUnwrap(alg, key, encryptedKey) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek)); +} +function checkRsaKey(alg, key, usage) { + checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key); +} +async function deriveKey(p2s, alg, p2c, key) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) + throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets"); + if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) + throw new JWEInvalid("PBES2 Count Input must be a positive integer"); + const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: "PBKDF2", + salt + }, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits"); + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); +} +function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +async function concatKdf(Z, L, OtherInfo) { + const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen); + for (let i = 1; i <= reps; i++) { + const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo)); + dk.set(hashResult, (i - 1) * hashLen); + } + return dk.slice(0, dkLen); +} +async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) { + checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits"); + const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({ + name: publicKey.algorithm.name, + public: publicKey + }, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(Z, keyLength, otherInfo); +} +function assertEcdhKey(key) { + assertCryptoKey(key); + const curve = key.algorithm.namedCurve; + if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519") + throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime"); +} +function partyInfo(joseHeader, name) { + const value = joseHeader[name]; + if (value !== void 0) { + if (typeof value != "string") + throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`); + return decodeBase64url(value, name, JWEInvalid); + } +} +function checkPartyInfo(apu, apv) { + if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) { + for (let i = 0; i < apu.byteLength; i++) + if (apu[i] !== apv[i]) + return; + throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct'); + } +} +function assertEncryptedKey(encryptedKey) { + if (encryptedKey === void 0) + throw new JWEInvalid("JWE Encrypted Key missing"); +} +function assertNoEncryptedKey(encryptedKey) { + if (encryptedKey !== void 0) + throw new JWEInvalid("Encountered unexpected JWE Encrypted Key"); +} +function validateMaxPBES2Count(value) { + if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1)) + throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity"); +} +async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) { + const { alg } = entry, mode = entry.mode; + if (mode === "direct-encryption") + return assertNoEncryptedKey(encryptedKey), key; + const direct = mode === "direct-key-agreement"; + switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) { + case "ECDH": { + const { epk } = joseHeader; + if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter))) + throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid'); + assertEcdhKey(key); + const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv"); + checkPartyInfo(partyUInfo, partyVInfo); + const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (direct) + return sharedSecret; + key = sharedSecret; + break; + } + case "RSA-OAEP": + return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey)); + case "PBKDF2": { + if (typeof joseHeader.p2c != "number") + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid'); + validateMaxPBES2Count(maxPBES2Count); + const p2cLimit = maxPBES2Count ?? 1e4; + if (joseHeader.p2c > p2cLimit) + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds'); + if (typeof joseHeader.p2s != "string") + throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid'); + const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid); + key = await deriveKey(p2s, alg, joseHeader.p2c, key); + break; + } + case "AES-GCM": { + if (typeof joseHeader.iv != "string") + throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid'); + if (typeof joseHeader.tag != "string") + throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid'); + const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid); + if (iv.byteLength !== 12) + throw new JWEInvalid("Invalid Initialization Vector length"); + if (tag.byteLength !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); + } + } + return aeskwUnwrap(alg.slice(-6), key, encryptedKey); +} +async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) { + const { alg, mode } = entry, transport = isJWECEKTransport(entry); + if (providedCek !== void 0 && !transport) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt"); + if (mode === "direct-encryption") + return [key, void 0, void 0]; + const cek = transport ? providedCek ?? generateCek(enc) : void 0; + cek && checkCekLength(cek, enc.cekBits); + let encryptedKey, parameters; + switch (entry.subtle.name) { + case "ECDH": { + assertEcdhKey(key); + const { apu: providedApu, apv: providedApv } = providedParameters; + providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"'); + const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv"); + checkPartyInfo(apu, apv); + let ephemeralKey; + providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey; + const subtle = crypto.subtle; + let exportableEpk = ephemeralKey; + if (!exportableEpk.extractable) { + if (typeof subtle.getPublicKey != "function") + throw new TypeError('CryptoKey for "epk" must be extractable'); + exportableEpk = await subtle.getPublicKey(ephemeralKey, []); + } + const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty }; + if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct) + return [sharedSecret, void 0, parameters]; + key = sharedSecret; + break; + } + case "RSA-OAEP": { + assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek)); + break; + } + case "PBKDF2": { + const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; + key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) }; + break; + } + case "AES-GCM": { + const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv; + if (!(iv instanceof Uint8Array)) + throw new TypeError('"iv" must be an instance of Uint8Array'); + const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); + encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; + } + } + if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength) + throw new TypeError("JWE key management algorithm did not produce an Encrypted Key"); + return [cek, encryptedKey, parameters]; +} +export { + decryptKeyManagement, + encryptKeyManagement, + validateMaxPBES2Count +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js new file mode 100644 index 000000000..a4b277f18 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js @@ -0,0 +1,33 @@ +import { decode as b64u } from "./base64url.js"; +import { strictDecoder } from "../lib/buffer_utils.js"; +import { isObject } from "../lib/validate.js"; +import { JWTInvalid } from "./errors.js"; +function decodeJwt(jwt) { + if (typeof jwt != "string") + throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string"); + const { 1: payload, length } = jwt.split("."); + if (length === 5) + throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded"); + if (length !== 3) + throw new JWTInvalid("Invalid JWT"); + if (!payload) + throw new JWTInvalid("JWTs must contain a payload"); + let decoded; + try { + decoded = b64u(payload); + } catch { + throw new JWTInvalid("Failed to base64url decode the payload"); + } + let result; + try { + result = JSON.parse(strictDecoder.decode(decoded)); + } catch { + throw new JWTInvalid("Failed to parse the decoded payload as JSON"); + } + if (!isObject(result)) + throw new JWTInvalid("Invalid JWT Claims Set"); + return result; +} +export { + decodeJwt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js new file mode 100644 index 000000000..274e03617 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js @@ -0,0 +1,19 @@ +import { parseJoseHeader } from "../lib/validate.js"; +function decodeProtectedHeader(token) { + let protectedB64u; + if (typeof token == "string") { + const parts = token.split("."); + (parts.length === 3 || parts.length === 5) && ([protectedB64u] = parts); + } else if (typeof token == "object" && token) + if ("protected" in token) + protectedB64u = token.protected; + else + throw new TypeError("Token does not contain a Protected Header"); + const invalid = "Invalid Token or Protected Header formatting"; + if (typeof protectedB64u != "string" || !protectedB64u) + throw new TypeError(invalid); + return parseJoseHeader(protectedB64u, TypeError, invalid); +} +export { + decodeProtectedHeader +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json new file mode 100644 index 000000000..2d7d92f72 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json @@ -0,0 +1,134 @@ +{ + "name": "jose", + "version": "6.2.12", + "description": "JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes", + "keywords": [ + "akp", + "browser", + "bun", + "cloudflare", + "compact", + "decode", + "decrypt", + "deno", + "detached", + "ec", + "ecdsa", + "ed25519", + "eddsa", + "edge", + "electron", + "embedded", + "encrypt", + "flattened", + "general", + "jose", + "json web token", + "jsonwebtoken", + "jwa", + "jwe", + "jwk", + "jwks", + "jws", + "jwt-decode", + "jwt", + "ml-dsa", + "netlify", + "next", + "nextjs", + "oct", + "okp", + "payload", + "pem", + "pkcs8", + "rsa", + "sign", + "signature", + "spki", + "validate", + "vercel", + "verify", + "webcrypto", + "workerd", + "workers", + "x509" + ], + "homepage": "https://github.com/panva/jose", + "repository": "panva/jose", + "funding": { + "url": "https://github.com/sponsors/panva" + }, + "license": "MIT", + "author": "Filip Skokan ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/webapi/index.js" + }, + "./jwk/*": { + "types": "./dist/types/jwk/*.d.ts", + "default": "./dist/webapi/jwk/*.js" + }, + "./key/import": { + "types": "./dist/types/key/import.d.ts", + "default": "./dist/webapi/key/import.js" + }, + "./key/export": { + "types": "./dist/types/key/export.d.ts", + "default": "./dist/webapi/key/export.js" + }, + "./key/generate/keypair": { + "types": "./dist/types/key/generate_key_pair.d.ts", + "default": "./dist/webapi/key/generate_key_pair.js" + }, + "./key/generate/secret": { + "types": "./dist/types/key/generate_secret.d.ts", + "default": "./dist/webapi/key/generate_secret.js" + }, + "./jwks/*": { + "types": "./dist/types/jwks/*.d.ts", + "default": "./dist/webapi/jwks/*.js" + }, + "./jwt/decode": { + "types": "./dist/types/util/decode_jwt.d.ts", + "default": "./dist/webapi/util/decode_jwt.js" + }, + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./decode/protected_header": { + "types": "./dist/types/util/decode_protected_header.d.ts", + "default": "./dist/webapi/util/decode_protected_header.js" + }, + "./jws/*": { + "types": "./dist/types/jws/*.d.ts", + "default": "./dist/webapi/jws/*.js" + }, + "./jwe/*": { + "types": "./dist/types/jwe/*.d.ts", + "default": "./dist/webapi/jwe/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + }, + "./base64url": { + "types": "./dist/types/util/base64url.d.ts", + "default": "./dist/webapi/util/base64url.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/webapi/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/webapi/**/*.js", + "dist/types/**/*.d.ts", + "!dist/**/*.bundle.js", + "!dist/**/*.umd.js", + "!dist/**/*.min.js", + "!dist/types/lib/**" + ] +} diff --git a/tests/corpus/6332-fluent-this-declaration/main.ts b/tests/corpus/6332-fluent-this-declaration/main.ts new file mode 100644 index 000000000..10804083f --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/main.ts @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s: Signer): string { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/main.mjs b/tests/corpus/6333-fluent-this-declaration-js/main.mjs new file mode 100644 index 000000000..44ac698cc --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/main.mjs @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s) { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6334-discarded-logical-statements.mjs b/tests/corpus/6334-discarded-logical-statements.mjs new file mode 100644 index 000000000..2e1df91a8 --- /dev/null +++ b/tests/corpus/6334-discarded-logical-statements.mjs @@ -0,0 +1,42 @@ +// Minified `a && b;` / `a || b;` statements whose operands share no value +// representation lower as the control flow they are (jose's secs()), and +// element reads on a RegExpExecArray | null the code ruled non-null +// without narrowing answer undefined for groups that did not participate. +const multipliers = { s: 1, m: 60, h: 3600, d: 86400 }; +const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|s|minutes?|m|hours?|h|days?|d)(?: (ago|from now))?$/i; +function invalid() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalid(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalid(); + const value = parseFloat(matched[2]); + const n = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(n) || invalid(), matched[1] === "-" || matched[4] === "ago" ? -n : n; +} +for (const input of ["10 minutes", "2h", "-1d", "+3 s", "1.5 hours ago", "4 days from now", "-2h ago", "bogus", "", 7]) { + try { + console.log(JSON.stringify(input), secs(input)); + } catch (e) { + console.log(JSON.stringify(input), e.name, e.message); + } +} + +const log = []; +function note(v) { + log.push(v); + return v; +} +const values = [0, 1, "", "x", undefined, null, true, false]; +for (const v of values) { + (note(v) || typeof v === "number") && note("and"); + (v && typeof v === "string") || note("or"); + !v || (v === true && note("bool")) || note("rest"); +} +console.log(JSON.stringify(log)); + +const m = /(a)?(b)(c)?/.exec("b"); +console.log(m[1], m[2], m[3], m[7], m[1] === undefined, m.length); +const none = /(z)/.exec("b"); +console.log(none?.[1], none === null); diff --git a/tests/corpus/6335-discarded-logical-statements.ts b/tests/corpus/6335-discarded-logical-statements.ts new file mode 100644 index 000000000..84b89f7b1 --- /dev/null +++ b/tests/corpus/6335-discarded-logical-statements.ts @@ -0,0 +1,27 @@ +// `a && b;` / `a || b;` statements whose operands share no value +// representation lower as control flow: only the deciding test runs. +function invalid(): never { + throw new TypeError("invalid"); +} +function check(input: string): string { + const matched = /^(\+|-)?(\d+)(s|m)( ago)?$/.exec(input); + (!matched || (matched[4] && matched[1])) && invalid(); + return matched ? `${matched[2]}${matched[3]}` : "none"; +} +for (const input of ["5s", "-5m", "5m ago", "-5m ago", "x"]) { + try { + console.log(input, check(input)); + } catch (e) { + console.log(input, (e as Error).message); + } +} +const seen: string[] = []; +function flag(s: string, v: boolean | string | undefined): boolean | string | undefined { + seen.push(s); + return v; +} +for (const v of [true, false, "", "y", undefined]) { + (flag("a", v) || flag("b", v === undefined ? "u" : false)) && seen.push("then"); + (flag("c", v) && flag("d", typeof v === "string")) || seen.push("else"); +} +console.log(seen.join(",")); diff --git a/tests/corpus/6340-prototype-reflection-optional-view-param.ts b/tests/corpus/6340-prototype-reflection-optional-view-param.ts new file mode 100644 index 000000000..58e21164d --- /dev/null +++ b/tests/corpus/6340-prototype-reflection-optional-view-param.ts @@ -0,0 +1,42 @@ +// A generic method whose optional parameter is an interface view (or an +// erased type parameter) reflects through the prototype of an instance +// that crossed into checked-dynamic code, and calls directly with the +// argument omitted. +interface Handle { + close(): string; +} +class Plain implements Handle { + close(): string { + return "closed"; + } +} +class Registry { + private current: Handle | null = null; + use(handle: Handle | null): void { + this.current = handle; + } + wrap(fn: F, handle?: Handle | null): F { + if (handle) this.current = handle; + return fn; + } + tag(value: T, label?: T): string { + return `${String(value)}:${label === undefined ? "none" : String(label)}`; + } + size(): number { + return this.current === null ? 0 : 1; + } +} + +const registry = new Registry(); +const inc = registry.wrap((x: number) => x + 1); +console.log(inc(1), registry.size()); +const dec = registry.wrap((x: number) => x - 1, new Plain()); +console.log(dec(1), registry.size()); +console.log(registry.tag(1), registry.tag("a", "b")); + +const boxed: unknown = registry; +const proto = Object.getPrototypeOf(boxed); +console.log(Object.getOwnPropertyNames(proto).join(",")); +console.log(typeof proto.wrap, typeof proto.tag, proto.size.call(registry)); +const loose: any = registry; +console.log(loose.size(), proto.tag.call(registry, 2), proto.tag.call(registry, 2, 3)); diff --git a/tests/corpus/6350-globalthis-assertion-state.ts b/tests/corpus/6350-globalthis-assertion-state.ts new file mode 100644 index 000000000..4a7f5a990 --- /dev/null +++ b/tests/corpus/6350-globalthis-assertion-state.ts @@ -0,0 +1,50 @@ +// Process-global state on the global object through a type assertion: +// every assertion of globalThis denotes the global object itself, so a +// write through one view is visible through every other view and through +// a view bound before the write. +const KEY = Symbol.for("example.state"); +interface State { + n: number; + log: string[]; +} +type Holder = { [KEY]?: State; label?: string }; + +const early = globalThis as Holder; + +function get(): State | undefined { + return early[KEY]; +} + +early[KEY] = { n: 1, log: [] }; +console.log(get()?.n, (globalThis as Holder)[KEY]?.n); + +function touch(entry: string): void { + const view = globalThis as Holder; + const state = view[KEY] ?? (view[KEY] = { n: 0, log: [] }); + state.n++; + state.log.push(entry); +} +touch("a"); +touch("b"); +console.log(early[KEY]?.n, early[KEY]?.log.join(","), get() === early[KEY]); + +// string-keyed members of the same object +(globalThis as Holder).label = "ready"; +console.log(early.label, (globalThis as unknown as { label?: string }).label); + +// ??= initializes once; delete removes the member for every view +const COUNTER = Symbol.for("example.counter"); +type Counters = { [COUNTER]?: { hits: number } }; +(globalThis as Counters)[COUNTER] ??= { hits: 0 }; +(globalThis as Counters)[COUNTER] ??= { hits: 100 }; +const counters = globalThis as Counters; +counters[COUNTER]!.hits += 2; +console.log((globalThis as Counters)[COUNTER]?.hits, COUNTER in globalThis); +delete counters[COUNTER]; +console.log((globalThis as Counters)[COUNTER], COUNTER in globalThis); + +// an index-signature view +const dict = globalThis as unknown as Record; +dict.exampleTotal = 1; +dict["exampleTotal"] = (dict["exampleTotal"] ?? 0) + 1; +console.log((globalThis as unknown as { exampleTotal?: number }).exampleTotal); diff --git a/tests/corpus/6352-globalthis-state-modules/boot.ts b/tests/corpus/6352-globalthis-state-modules/boot.ts new file mode 100644 index 000000000..856ff1baa --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/boot.ts @@ -0,0 +1,14 @@ +// The boot module writes the state through its own assertion of globalThis. +const STATE = Symbol.for("example.boot-state"); +const PENDING = Symbol.for("example.boot-pending"); + +interface Early { + startedAt: number; + steps: string[]; +} + +export function boot(): void { + const view = globalThis as { [STATE]?: Early; [PENDING]?: Promise }; + view[STATE] = { startedAt: 42, steps: ["boot"] }; + view[PENDING] = Promise.resolve("container-1"); +} diff --git a/tests/corpus/6352-globalthis-state-modules/main.ts b/tests/corpus/6352-globalthis-state-modules/main.ts new file mode 100644 index 000000000..5b5aee7eb --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/main.ts @@ -0,0 +1,18 @@ +// Boot writes the state; main reads it through a binding exported by +// another module, then takes the pending value out of the global object. +import { boot } from "./boot.ts"; +import { PENDING_KEY, STATE_KEY, globalState, readState } from "./state.ts"; + +console.log(readState() === undefined); +boot(); +const state = readState(); +console.log(state?.startedAt, state?.steps.join(",")); +state?.steps.push("main"); +console.log(globalState[STATE_KEY]?.steps.join(",")); + +async function main(): Promise { + const pending = globalState[PENDING_KEY]; + globalState[PENDING_KEY] = undefined; + console.log(await pending, globalState[PENDING_KEY]); +} +void main(); diff --git a/tests/corpus/6352-globalthis-state-modules/state.ts b/tests/corpus/6352-globalthis-state-modules/state.ts new file mode 100644 index 000000000..cef92c25f --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/state.ts @@ -0,0 +1,18 @@ +// Shared by the boot and main modules: the state lives on the global +// object, keyed by registered symbols. +export const STATE_KEY = Symbol.for("example.boot-state"); +export const PENDING_KEY = Symbol.for("example.boot-pending"); + +export interface BootState { + startedAt: number; + steps: string[]; +} + +export const globalState = globalThis as { + [STATE_KEY]?: BootState; + [PENDING_KEY]?: Promise; +}; + +export function readState(): BootState | undefined { + return globalState[STATE_KEY]; +} diff --git a/tests/corpus/bound-constructor-inspect-names.cjs b/tests/corpus/bound-constructor-inspect-names.cjs new file mode 100644 index 000000000..52faa689c --- /dev/null +++ b/tests/corpus/bound-constructor-inspect-names.cjs @@ -0,0 +1,36 @@ +'use strict'; +// util.inspect names an object after the function in the first own +// `constructor` property on its prototype chain. Here that function is a +// bound function, whose name ("bound " and the target's name) is built at +// bind time, and the property is replaced after construction, which +// releases the first bound function. The instance keeps the name. The +// targets are anonymous, so the bound names are "bound " and "bound bound ". +const util = require('util'); + +// Array elements give the functions no name. +const [Point, Pair] = [ + function (x) { + this.x = x; + }, + function (a, b) { + this.a = a; + this.b = b; + }, +]; +Point.prototype.constructor = Point.bind(null); +const p = new Point(1); +console.log(util.inspect(p)); + +Point.prototype.constructor = Point.bind(null); +const filler = []; +for (let i = 0; i < 200; i++) filler.push('bound '.repeat(i % 5) + i); +console.log(util.inspect(p)); +console.log(p); +console.log(filler.length); + +// A bound function of a bound function, replaced the same way. +Pair.prototype.constructor = Pair.bind(null).bind(null); +const q = new Pair(1, 2); +Pair.prototype.constructor = Pair.bind(null).bind(null); +console.log(util.inspect(q)); +console.log(q); diff --git a/tests/corpus/builtin-function-record-values/main.ts b/tests/corpus/builtin-function-record-values/main.ts new file mode 100644 index 000000000..c742acc16 --- /dev/null +++ b/tests/corpus/builtin-function-record-values/main.ts @@ -0,0 +1,10 @@ +// A builtin function stored in a JavaScript record is a real function: +// TypeScript consumers call it through the record. +import native from "./native.js"; +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const id = native.randomUUID(); +console.log(typeof id, id.length, shape.test(id)); +const ids = new Set(); +for (let i = 0; i < 50; i++) ids.add(native.randomUUID()); +console.log(ids.size); +console.log(shape.test(native.randomUUID({ disableEntropyCache: true }))); diff --git a/tests/corpus/builtin-function-record-values/native.js b/tests/corpus/builtin-function-record-values/native.js new file mode 100644 index 000000000..7009a2cd9 --- /dev/null +++ b/tests/corpus/builtin-function-record-values/native.js @@ -0,0 +1,3 @@ +// uuid's native.js layout: a record exporting a builtin function value. +import { randomUUID } from "crypto"; +export default { randomUUID }; diff --git a/tests/corpus/builtin-function-values.mjs b/tests/corpus/builtin-function-values.mjs new file mode 100644 index 000000000..ad0331291 --- /dev/null +++ b/tests/corpus/builtin-function-values.mjs @@ -0,0 +1,17 @@ +// Builtin functions used as JavaScript values are functions: typeof, +// identity, their name, and calls through the value. +import { randomUUID, scryptSync } from "crypto"; +const table = { randomUUID, scryptSync }; +console.log(typeof table.randomUUID, table.randomUUID === randomUUID); +console.log(/^[0-9a-f-]{36}$/.test(table.randomUUID())); +console.log(/^[0-9a-f-]{36}$/.test(table.randomUUID(undefined))); +try { + table.randomUUID(5); +} catch (e) { + console.log(e.code, e.name); +} +console.log(table.randomUUID); +console.log(typeof table.scryptSync, table.scryptSync === scryptSync); +console.log(new Set([scryptSync]).has(table.scryptSync), table.scryptSync.name); +const pick = (useNative) => (useNative && table.randomUUID ? table.randomUUID() : "fallback"); +console.log(pick(true).length, pick(false)); diff --git a/tests/corpus/constructor-returns-chained-this.js b/tests/corpus/constructor-returns-chained-this.js new file mode 100644 index 000000000..c3a2a0469 --- /dev/null +++ b/tests/corpus/constructor-returns-chained-this.js @@ -0,0 +1,40 @@ +// The JavaScript shape three.js ships: the constructor returns a chainable +// setter's result and other classes extend it. +class Color { + constructor(r, g, b) { + this.isColor = true; + this.r = 1; + this.g = 1; + this.b = 1; + return this.set(r, g, b); + } + set(r, g, b) { + if (g === undefined && b === undefined) { + if (r !== undefined) this.setScalar(r); + } else { + this.r = r; + this.g = g; + this.b = b; + } + return this; + } + setScalar(s) { + this.r = this.g = this.b = s; + return this; + } +} +class Tinted extends Color { + constructor(r, g, b, tint) { + super(r, g, b); + this.tint = tint; + } + set(r, g, b, a = 1) { + this.a = a; + return super.set(r, g, b); + } +} +const c = new Color(0.5); +console.log(c.r, c.g, c.b, c instanceof Color); +const t = new Tinted(0.1, 0.2, 0.3, 'warm'); +console.log(t.r, t.g, t.b, t.a, t.tint, t instanceof Tinted, t instanceof Color); +console.log(new Color().r); diff --git a/tests/corpus/constructor-returns-chained-this.ts b/tests/corpus/constructor-returns-chained-this.ts new file mode 100644 index 000000000..d2a9f3955 --- /dev/null +++ b/tests/corpus/constructor-returns-chained-this.ts @@ -0,0 +1,38 @@ +// A constructor that ends with `return this.set(...)`, where every +// reachable `set` returns `this`, answers the constructed instance, so +// subclasses still construct through super() (three.js's Color). +class Color { + r = 0; + g = 0; + b = 0; + constructor(r?: number, g?: number, b?: number) { + return this.set(r ?? 0, g ?? 0, b ?? 0); + } + set(r: number, g: number, b: number): this { + if (r < 0) return this.clear(); + this.r = r; + this.g = g; + this.b = b; + return this; + } + clear(): this { + this.r = this.g = this.b = 0; + return this; + } +} +class NamedColor extends Color { + name: string; + constructor(name: string, r: number, g: number, b: number) { + super(r, g, b); + this.name = name; + } + override set(r: number, g: number, b: number): this { + super.set(r * 2, g * 2, b * 2); + return this; + } +} +const plain = new Color(1, 2, 3); +console.log(plain instanceof Color, plain.r, plain.g, plain.b); +console.log(new Color(-1, 5, 5).r, new Color().b); +const named = new NamedColor("teal", 0, 1, 2); +console.log(named instanceof NamedColor, named instanceof Color, named.name, named.r, named.g, named.b); diff --git a/tests/corpus/constructor-returns-instance.js b/tests/corpus/constructor-returns-instance.js new file mode 100644 index 000000000..ab7af8b2c --- /dev/null +++ b/tests/corpus/constructor-returns-instance.js @@ -0,0 +1,45 @@ +// JavaScript constructors whose returned value is typed only at run time: +// an instance of the class replaces `this`, a primitive keeps it. +class Connection { + constructor(url) { + if (Connection.open && Connection.open.url === url) return Connection.open; + this.url = url; + this.uses = 0; + Connection.open = this; + } + use() { + this.uses++; + return this; + } +} +const c1 = new Connection("db://a").use(); +const c2 = new Connection("db://a").use(); +const c3 = new Connection("db://b").use(); +console.log(c1 === c2, c2.uses, c3 === c1, c3.uses, c3.url); + +class Counter { + constructor(start, answer) { + this.count = start; + return answer; + } +} +console.log(new Counter(1, 5).count, new Counter(2, "x").count, new Counter(3, null).count); +console.log(new Counter(4, undefined).count, new Counter(5, true).count); +const kept = new Counter(6, 0); +console.log(new Counter(7, kept) === kept, kept.count); + +const memoCache = new Map(); +let memoCreated = 0; +class Memo { + constructor(key) { + const hit = memoCache.get(key); + if (hit !== undefined) return hit; + this.key = key; + this.created = ++memoCreated; + memoCache.set(key, this); + } +} +const m1 = new Memo("k"); +const m2 = new Memo("k"); +const m3 = new Memo("j"); +console.log(m1 === m2, m3 === m1, m2.created, m3.created, memoCreated); diff --git a/tests/corpus/constructor-returns-instance.ts b/tests/corpus/constructor-returns-instance.ts new file mode 100644 index 000000000..81e25a12d --- /dev/null +++ b/tests/corpus/constructor-returns-instance.ts @@ -0,0 +1,112 @@ +// A constructor's returned object replaces the constructed instance when it +// is an object; primitive, bare and `this` returns answer the instance. +class Registry { + static current: Registry | undefined = undefined; + readonly id: number = 0; + entries: string[] = []; + constructor(id: number) { + if (Registry.current) return Registry.current; + this.id = id; + Registry.current = this; + } + add(entry: string): this { + this.entries.push(entry); + return this; + } +} +const first = new Registry(1).add("a"); +const second = new Registry(2).add("b"); +console.log(first === second, second.id, second.entries.join(",")); + +// An instance returned only on some paths. +class Slot { + static previous: Slot | undefined = undefined; + label: string; + constructor(label: string, reuse: boolean) { + this.label = label; + const previous = Slot.previous; + Slot.previous = this; + if (reuse && previous) return previous; + } +} +const s1 = new Slot("one", true); +const s2 = new Slot("two", true); +const s3 = new Slot("three", false); +console.log(s1.label, s2.label, s3.label, s2 === s1, s3 === s2); + +// Constructing the class again inside its own constructor. +class Node2 { + static made = 0; + value: number; + child: Node2 | null = null; + constructor(depth: number, share: Node2 | null) { + Node2.made++; + this.value = depth; + if (share !== null) return share; + if (depth > 0) this.child = new Node2(depth - 1, null); + } +} +const root = new Node2(2, null); +const alias = new Node2(9, root); +console.log(alias === root, alias.value, root.child?.value, root.child?.child?.value, Node2.made); + +// A constructor that throws leaves later constructions unaffected. +class Guarded { + static kept: Guarded | undefined = undefined; + n: number; + constructor(n: number) { + if (n < 0) throw new RangeError(`negative: ${n}`); + this.n = n; + if (n === 0 && Guarded.kept) return Guarded.kept; + Guarded.kept = this; + } +} +try { + new Guarded(-1); +} catch (e) { + console.log((e as Error).message); +} +const g1 = new Guarded(5); +const g2 = new Guarded(0); +const g3 = new Guarded(7); +console.log(g1.n, g2.n, g3.n, g1 === g2, g3 === g1); + +// Primitive, bare and `this` returns keep the constructed instance. +class Plain { + v = 3; + constructor(mode: number) { + if (mode === 1) return; + if (mode === 2) return this; + } +} +const plain = new Plain(2); +console.log(new Plain(0).v, new Plain(1).v, plain.v, plain instanceof Plain); + +// The derived class's own constructor may return an instance of itself. +class Animal { + sound = "..."; +} +class Dog extends Animal { + static pack: Dog[] = []; + name: string; + constructor(name: string) { + super(); + this.name = name; + this.sound = "woof"; + const known = Dog.pack.find((d) => d.name === name); + if (known) return known; + Dog.pack.push(this); + } +} +const rex = new Dog("rex"); +const again = new Dog("rex"); +const fido = new Dog("fido"); +console.log(rex === again, fido === rex, Dog.pack.length, again.sound, again instanceof Animal); + +// Constructions through class values take the returned instance too. +const Ctor: typeof Registry = Registry; +console.log(new Ctor(3) === first); +function build(make: new (id: number) => T, id: number): T { + return new make(id); +} +console.log(build(Registry, 4) === first, build(Registry, 5).entries.length); diff --git a/tests/corpus/constructor-returns-this-super.js b/tests/corpus/constructor-returns-this-super.js new file mode 100644 index 000000000..3901c5553 --- /dev/null +++ b/tests/corpus/constructor-returns-this-super.js @@ -0,0 +1,33 @@ +// A JavaScript base constructor that returns `this` through a method, as +// three.js's Color does: `super()` from a subclass keeps the subclass's own +// instance. +class Color { + constructor(r, g, b) { + this.r = 1; + this.g = 1; + this.b = 1; + return this.set(r, g, b); + } + set(r, g, b) { + if (g === undefined) { + this.r = this.g = this.b = r; + } else { + this.r = r; + this.g = g; + this.b = b; + } + return this; + } +} +class Color4 extends Color { + constructor(r, g, b, a = 1) { + super(r, g, b); + this.a = a; + } +} +const c = new Color(0.5); +const c4 = new Color4(0.1, 0.2, 0.3); +const c4b = new Color4(1, 0, 0, 0.5); +console.log(c.r, c.g, c.b, c instanceof Color); +console.log(c4.r, c4.g, c4.b, c4.a, c4 instanceof Color4, c4 instanceof Color); +console.log(c4b.a, c4b !== c4, Object.keys(c4b).join(",")); diff --git a/tests/corpus/es5-constructor-inspect-names.cjs b/tests/corpus/es5-constructor-inspect-names.cjs new file mode 100644 index 000000000..b7cc84349 --- /dev/null +++ b/tests/corpus/es5-constructor-inspect-names.cjs @@ -0,0 +1,41 @@ +// util.inspect names objects built by plain constructor functions after +// the constructor their prototype chain records, as Node does. +'use strict'; +const util = require('util'); +function Point(x, y) { + this.x = x; + this.y = y; +} +Point.prototype.norm = function () { + return Math.hypot(this.x, this.y); +}; +const p = new Point(3, 4); +console.log(p, p.norm()); +p.z = 0; +console.log(p); +function Empty() {} +console.log(new Empty(), [new Empty()], { inner: new Empty() }); +console.log(util.inspect(new Point(1, 2))); +console.log(`${util.inspect({ list: [new Point(0, 1)] })}`); +// A subclass whose prototype was replaced without a constructor reports +// the nearest constructor on the chain. +function Base(n) { + this.n = n; +} +function Derived(n) { + Base.call(this, n); + this.extra = true; +} +Derived.prototype = Object.create(Base.prototype); +console.log(new Derived(1)); +Derived.prototype.constructor = Derived; +console.log(new Derived(2)); +// A replaced prototype object without a constructor prints plainly. +function Plain() { + this.v = 1; +} +Plain.prototype = { get() { return this.v; } }; +console.log(new Plain()); +const made = Object.create(Point.prototype); +made.x = 9; +console.log(made); diff --git a/tests/corpus/exponent-unit-base.ts b/tests/corpus/exponent-unit-base.ts new file mode 100644 index 000000000..d8d4cd1ac --- /dev/null +++ b/tests/corpus/exponent-unit-base.ts @@ -0,0 +1,16 @@ +// `**` answers NaN for a base of ±1 with a NaN or infinite exponent, where +// libm's pow (and LLVM's folding of pow(1, x)) answers 1. +const values = [Infinity, -Infinity, NaN, 0, -0, 2, 0.5]; +const read = (i: number): number => values[i % values.length]!; +for (let i = 0; i < values.length; i++) { + const x = read(i); + console.log(x, 1 ** x, (-1) ** x, x ** 0, x ** 1, 2 ** x, x ** -1); +} +let base = 1; +let exponent = Infinity; +console.log(base ** exponent, (-base) ** exponent, base ** NaN); +exponent = 3; +console.log(base ** exponent, (-1) ** exponent); +let acc = 1; +acc **= Infinity; +console.log(acc, Math.pow(1, Infinity), NaN ** 0, (-0) ** -1, 0 ** -1); diff --git a/tests/corpus/key-order-late-optional.ts b/tests/corpus/key-order-late-optional.ts new file mode 100644 index 000000000..f4742acb0 --- /dev/null +++ b/tests/corpus/key-order-late-optional.ts @@ -0,0 +1,84 @@ +// An optional key that no literal creates is appended when it is first +// assigned, after every key the value was built with, and straight-line +// assignments of several such keys keep their program order, including +// assignments behind conditions. Assigning a key that already exists never +// moves it. +interface Item { + label?: string; + id: number; + done: boolean; +} +function makeItem(id: number): Item { + return { id, done: false }; +} +const item = makeItem(1); +item.label = "first"; +console.log(Object.keys(item).join(","), JSON.stringify(item)); +console.log(item); +const direct: Item = { id: 2, done: true }; +direct.label = "second"; +console.log(Object.keys(direct).join(","), JSON.stringify(direct)); +const untouched = makeItem(3); +console.log(JSON.stringify(untouched), Object.keys(untouched).join(",")); + +// Two late keys assigned in reverse declaration order. +interface Pos { + id: number; + x?: number; + y?: number; +} +function makePos(): Pos { + return { id: 0 }; +} +const pos = makePos(); +pos.y = 1; +pos.x = 2; +console.log(Object.keys(pos).join(","), JSON.stringify(pos)); +console.log(pos); +pos.id = 3; +console.log(JSON.stringify(pos)); + +// An options object filled key by key, some keys conditionally. +interface Opts { + verbose?: boolean; + depth?: number; + name?: string; +} +function build(deep: boolean, loud: boolean): Opts { + const o: Opts = {}; + o.name = "n"; + if (deep) o.depth = 2; + if (loud) o.verbose = true; + return o; +} +for (const deep of [true, false]) + for (const loud of [true, false]) console.log(JSON.stringify(build(deep, loud))); +console.log(build(true, true)); +const forIn: string[] = []; +for (const k in build(true, true)) forIn.push(k); +console.log(forIn.join(",")); + +// Late keys on records stored in arrays and Maps. +interface Task { + owner?: string; + title: string; + priority: number; +} +const tasks: Task[] = []; +for (let i = 0; i < 3; i++) tasks.push({ title: "t" + i, priority: i }); +for (const t of tasks) if (t.priority > 0) t.owner = "o" + t.priority; +console.log(JSON.stringify(tasks)); +const byTitle = new Map(); +for (const t of tasks) byTitle.set(t.title, t); +console.log(byTitle); + +// An explicit undefined is a present key at its literal position. +interface Maybe { + a?: number; + b: number; +} +function makeMaybe(): Maybe { + return { b: 1, a: undefined }; +} +console.log(Object.keys(makeMaybe()).join(","), JSON.stringify(makeMaybe())); +console.log(makeMaybe()); diff --git a/tests/corpus/key-order-literal-flows.ts b/tests/corpus/key-order-literal-flows.ts new file mode 100644 index 000000000..350e78ffa --- /dev/null +++ b/tests/corpus/key-order-literal-flows.ts @@ -0,0 +1,150 @@ +// Records enumerate their keys in the order their object literals create +// them, even when every literal writes the keys in another order than the +// type declares them and the value is observed far from the literal: +// factory returns, arrays, Maps, nested records, generic factories, union +// arms, records widened to unknown/any, structuredClone, and every +// order-observing surface (keys, values, entries, for...in, JSON.stringify +// with replacers and indentation, inspect, console.log, util.format). +import { format, inspect } from "node:util"; + +interface Point { + x: number; + y: number; + label: string; +} +function makePoint(i: number): Point { + return { label: "p" + i, y: i * 2, x: i }; +} + +const p = makePoint(1); +console.log(Object.keys(p).join(",")); +console.log(Object.values(p).join(",")); +console.log(JSON.stringify(Object.entries(p))); +const forIn: string[] = []; +for (const k in p) forIn.push(k); +console.log(forIn.join(",")); +console.log(JSON.stringify(p)); +console.log(JSON.stringify(p, null, 2)); +console.log(JSON.stringify(p, ["y", "x"])); +const visited: string[] = []; +JSON.stringify(p, (key: string, value: unknown) => { + visited.push(key); + return value; +}); +console.log(visited.join(",")); +console.log(p); +console.log(inspect(p)); +console.log(format("%o|%j|%O", p, p, p)); + +// Arrays of records: literals pushed in a loop and produced by map. +interface Row { + id: number; + name: string; + score: number; +} +const rows: Row[] = []; +for (let i = 0; i < 3; i++) rows.push({ name: "n" + i, score: i * 10, id: i }); +for (const r of rows) console.log(Object.values(r).join("|")); +console.log(JSON.stringify(rows)); +const mapped: Row[] = [4, 5].map((i) => ({ name: "m" + i, score: i, id: i * 100 })); +console.log(JSON.stringify(mapped)); +console.log(mapped); + +// Records stored in a Map, read back, serialized and inspected. +const byName = new Map(); +for (const r of rows) byName.set(r.name, r); +byName.set("extra", { name: "extra", score: 9, id: 99 }); +console.log(byName); +console.log(JSON.stringify([...byName.values()])); +console.log(Object.keys(byName.get("extra")!).join(",")); + +// Nested records: every level keeps its own literal's order. +interface Inner { + b: number; + a: number; +} +interface Outer { + inner: Inner; + id: number; + tags: string[]; +} +function makeOuter(id: number): Outer { + return { id, tags: ["t" + id], inner: { a: id, b: id + 1 } }; +} +console.log(JSON.stringify(makeOuter(1))); +console.log(makeOuter(2)); +console.log(JSON.stringify([makeOuter(3), makeOuter(4)], null, 1)); + +// Generic factories instantiate per type argument. +function boxed(value: T): { value: T; tag: string; count: number } { + return { tag: "box", count: 1, value }; +} +console.log(JSON.stringify(boxed(7))); +console.log(JSON.stringify(boxed("s"))); +console.log(Object.keys(boxed(true)).join(",")); + +// Union arms keep their own literal orders. +type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number }; +function shapeAt(i: number): Shape { + return i % 2 === 0 ? { r: i, kind: "circle" } : { h: i, kind: "rect", w: i + 1 }; +} +const shapes: Shape[] = [shapeAt(0), shapeAt(1), shapeAt(2)]; +for (const s of shapes) console.log(JSON.stringify(s)); +console.log(shapes); + +// Widened to unknown and any, and cloned. +const u: unknown = makePoint(3); +console.log(JSON.stringify(u)); +const loose: any = makePoint(4); +console.log(Object.keys(loose).join(",")); +const cloned = structuredClone(makePoint(5)); +console.log(JSON.stringify(cloned), Object.keys(cloned).join(",")); + +// Integer-like keys still come first, ascending, ahead of the literal order. +interface Indexed { + name: string; + "10": number; + "2": number; + "01": number; +} +function makeIndexed(): Indexed { + return { "01": 1, name: "n", "10": 10, "2": 2 }; +} +console.log(JSON.stringify(makeIndexed())); +console.log(Object.keys(makeIndexed()).join(",")); + +// A literal whose fields are flipped by later assignments: assigning an +// existing key never moves it (the pattern of scriptc's own +// RuntimeFeatures scan). +interface Features { + regex: boolean; + eventTarget: boolean; + warningReplace: boolean; + stdioExtended: boolean; + processEvents: boolean; + emitter: boolean; +} +function scanFeatures(classes: string[], workers: boolean): Features { + const features: Features = { + regex: false, + eventTarget: classes.some((c) => c === "EventTarget"), + processEvents: false, + warningReplace: false, + stdioExtended: false, + emitter: workers, + }; + if (classes.length > 1) features.regex = true; + features.stdioExtended = workers; + return features; +} +console.log(JSON.stringify(scanFeatures(["EventTarget", "X"], true))); +console.log(JSON.stringify(scanFeatures([], false))); + +// Literals written in declaration order keep it. +interface Ordered { + first: string; + second: number; + third: boolean; +} +const ordered: Ordered = { first: "f", second: 2, third: true }; +console.log(JSON.stringify(ordered), Object.keys(ordered).join(",")); diff --git a/tests/corpus/key-order-spread-copies.ts b/tests/corpus/key-order-spread-copies.ts new file mode 100644 index 000000000..b677eb0a4 --- /dev/null +++ b/tests/corpus/key-order-spread-copies.ts @@ -0,0 +1,107 @@ +// Copies keep the key order of the values they copy: a spread places the +// source's keys at the spread's position in the source's own order, an +// override keeps the position the spread gave it, and new keys follow. +// Rest bindings, width copies, Object.assign and structuredClone keep the +// source order too, including when the source literal was written in +// another order than its type declares. +interface Source { + a: number; + b: number; + c: number; +} +function makeSource(i: number): Source { + return { c: i, b: i + 1, a: i + 2 }; +} + +// Spread followed by a new key, and by an override. +interface Extended { + a: number; + b: number; + c: number; + d: number; +} +const extended: Extended = { ...makeSource(1), d: 4 }; +console.log(JSON.stringify(extended), Object.keys(extended).join(",")); +const overridden: Source = { ...makeSource(2), b: 0 }; +console.log(JSON.stringify(overridden)); +interface Led { + a: number; + b: number; + c: number; + lead: number; +} +const leading: Led = { lead: 0, ...makeSource(3) }; +console.log(JSON.stringify(leading)); + +// A spread whose overrides change field types builds a new shape through +// the field-by-field desugar (scriptc's own frontend-cache writer). +interface Entry { + ok: true; + llvm: string; + ir: string[] | null; + sidecarJson: string | null; + features: number; + sources: [string, string][]; + warnings?: string[]; +} +function makeEntry(): Entry { + return { ok: true, llvm: "L", ir: ["x"], sidecarJson: null, features: 1, sources: [] }; +} +const entry = makeEntry(); +console.log(JSON.stringify({ schema: "v3", input: { ...entry, llvm: "", ir: null } })); + +// Same-shape updates keep every key in place. +interface State { + name: string; + count: number; + last: string | undefined; +} +let state: State = { count: 0, last: undefined, name: "s" }; +for (let i = 0; i < 3; i++) state = { ...state, count: state.count + 1, last: "i" + i }; +console.log(JSON.stringify(state), Object.keys(state).join(",")); +console.log({ ...state, name: "shown" }); + +// Rest bindings copy the remaining keys in the source's order. +const { b, ...rest } = makeSource(5); +console.log(b, JSON.stringify(rest), Object.keys(rest).join(",")); +let picked: number; +let restAgain: { a: number; c: number }; +({ b: picked, ...restAgain } = makeSource(6)); +console.log(picked, JSON.stringify(restAgain), Object.keys(restAgain).join(",")); + +// Width copies into an optional-widened type and assignment into a fresh +// target. +interface Wider { + a: number; + b: number; + c: number; + note?: string; +} +function widen(s: Source): Wider { + return s; +} +console.log(JSON.stringify(widen(makeSource(7)))); +const assigned = Object.assign({ z: 0 }, makeSource(8)); +console.log(JSON.stringify(assigned)); +console.log(JSON.stringify(structuredClone(makeSource(9)))); + +// Nested spreads of nested records. +interface Holder { + inner: Source; + id: number; +} +const holder: Holder = { id: 1, inner: makeSource(10) }; +const copied: Holder = { ...holder, id: 2 }; +console.log(JSON.stringify(copied)); +console.log(copied); + +// Conditional spreads place their key at the spread's position. +interface Options { + mode: string; + depth?: number; + verbose: boolean; +} +function options(deep: boolean): Options { + return { mode: "m", ...(deep ? { depth: 3 } : {}), verbose: deep }; +} +console.log(JSON.stringify(options(true)), JSON.stringify(options(false))); diff --git a/tests/corpus/let-uninitialized-function-binding.ts b/tests/corpus/let-uninitialized-function-binding.ts new file mode 100644 index 000000000..ade6748d3 --- /dev/null +++ b/tests/corpus/let-uninitialized-function-binding.ts @@ -0,0 +1,46 @@ +// A module-scope `let` declared without an initializer is initialized (to +// undefined) where the declaration runs, so a later assignment of a closure +// that calls the binding itself is an ordinary write. +type Step = (n: number) => number; + +// A write that runs before the declaration is still in the dead zone. +function early(): void { + late = () => 1; +} +try { + early(); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +let late: () => number; +late = () => 2; +early(); +console.log(late()); + +let countdown: Step; +countdown = (n) => (n <= 0 ? 0 : 1 + countdown(n - 1)); +console.log(countdown(5)); + +let log: (s: string) => void; +log = (s) => { + if (s.length < 3) log(s + "!"); + else console.log(s); +}; +log("a"); + +// Reassignment and assignment from a function body after the declaration. +let pick: (xs: number[]) => number; +function install(largest: boolean): void { + pick = largest ? (xs) => Math.max(...xs) : (xs) => Math.min(...xs); +} +install(true); +console.log(pick!([3, 9, 4])); +install(false); +console.log(pick!([3, 9, 4])); + +// Mutual recursion through two such bindings. +let isEven: (n: number) => boolean; +let isOdd: (n: number) => boolean; +isEven = (n) => (n === 0 ? true : isOdd(n - 1)); +isOdd = (n) => (n === 0 ? false : isEven(n - 1)); +console.log(isEven(10), isOdd(7), isEven(3)); diff --git a/tests/corpus/logical-and-truthy-index.ts b/tests/corpus/logical-and-truthy-index.ts new file mode 100644 index 000000000..b89e4d78d --- /dev/null +++ b/tests/corpus/logical-and-truthy-index.ts @@ -0,0 +1,44 @@ +// `&&` whose left operand is an array, object or function value: every +// object is truthy, so the result is the right operand, here an element +// read that may be undefined at run time. +const list = [3, 1, 2]; +const head = list && list[0]; +console.log(head); +const words: string[] = ["x"]; +console.log(words && words[0], words && words[5]); +const empty: number[] = []; +console.log(empty && empty[0]); +function first(xs: (string | number)[]): string | number { + return xs && xs[0]; +} +console.log(first([1]), first(["s"])); +type Item = { n: number } | string; +const items: Item[] = [{ n: 1 }, "z"]; +console.log(items && items[0], items && items[1]); +const nested: number[][] = [[1]]; +console.log(nested && nested[0]); +const flags: boolean[] = [false]; +console.log(flags && flags[0]); +const maybe: (number | null)[] = [null]; +console.log(maybe && maybe[0]); +const holes: (number | undefined)[] = [undefined, 4]; +console.log(holes && holes[0], holes && holes[1]); +const box = { k: [1, 2] }; +console.log(box.k && box.k[1]); +// The left operand still evaluates exactly once. +let reads = 0; +const source = (): number[] => { + reads++; + return [7, 8]; +}; +console.log(source() && source()[1], reads); +const table: Record = { a: [5] }; +const pick = (key: string): number | undefined => table[key] && table[key][0]; +console.log(pick("a"), reads); +class Holder { + xs = [1, 2]; + second(): number | undefined { + return this.xs && this.xs[1]; + } +} +console.log(new Holder().second()); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index e13701a93..10b0a12e7 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,14 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: the literal's order differs from the declaration and the -// program observes it. +// Key order: values of one type are built in two orders and the program +// observes them, so the type keeps its declaration order. interface Point { x: number; y: number; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point)); +console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/coverage-fixtures/mixed.ts b/tests/coverage-fixtures/mixed.ts index 73a36ce47..1ae9db26d 100644 --- a/tests/coverage-fixtures/mixed.ts +++ b/tests/coverage-fixtures/mixed.ts @@ -10,7 +10,7 @@ const handler = (x: number) => x * 2; const anyone: unknown = score(1); const flags = anyone == score(2); // unknown/object coercion keeps the fence; static primitive pairs lower -const { length: firstItem } = items; // object patterns over arrays keep the fence (patterns and defaults compile) +const { length: firstItem } = score(3) > 99 ? items : "abc"; // a union source keeps the object-pattern fence (narrow first) let label = "score: "; label += score(10); console.log(label); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 9571add6e..3739336e3 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts and ISO dates report no -// divergence. +// Latin-script comparisons, related-class casts, ISO dates and views of the +// global object report no divergence. interface Item { name: string; qty: number; @@ -32,3 +32,9 @@ function wingsOf(a: Animal): number { console.log(wingsOf(new Bird()), wingsOf(new Animal())); console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(",")); console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime()); + +// A type assertion of the global object denotes the global object itself. +const COUNT = Symbol.for("no-divergences.count"); +const globalState = globalThis as { [COUNT]?: { n: number } }; +globalState[COUNT] = { n: 1 }; +console.log(globalState[COUNT]?.n); diff --git a/tests/diagnostics/constructor-returns-instance-super.ts b/tests/diagnostics/constructor-returns-instance-super.ts new file mode 100644 index 000000000..c5e34c7a1 --- /dev/null +++ b/tests/diagnostics/constructor-returns-instance-super.ts @@ -0,0 +1,15 @@ +// `super()` into a constructor that can return an existing instance would +// rebind the subclass's `this` to that instance. +class Shared { + static one: Shared | undefined = undefined; + constructor() { + if (Shared.one) return Shared.one; + Shared.one = this; + } +} +class Derived extends Shared { + constructor() { + super(); + } +} +console.log(new Derived() instanceof Derived); diff --git a/tests/diagnostics/constructor-returns-object.ts b/tests/diagnostics/constructor-returns-object.ts new file mode 100644 index 000000000..63abf4c4d --- /dev/null +++ b/tests/diagnostics/constructor-returns-object.ts @@ -0,0 +1,10 @@ +// Node's `new Point()` answers the object a constructor returns. A plain +// object has no representation as a Point instance, so the return refuses +// instead of answering the constructed instance. +class Point { + x = 1; + constructor() { + return { x: 2 }; + } +} +console.log(new Point().x); diff --git a/tests/diagnostics/deferred-class-receiver/shelf.ts b/tests/diagnostics/deferred-class-receiver/shelf.ts index 1308ce5dc..d0fba73b3 100644 --- a/tests/diagnostics/deferred-class-receiver/shelf.ts +++ b/tests/diagnostics/deferred-class-receiver/shelf.ts @@ -1,6 +1,6 @@ export class Shelf { readonly name: string; - private readonly notes = new WeakMap(); + private readonly cursor = new Map().keys(); constructor(name: string) { this.name = name; } diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index b79836920..e42f8a07a 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -48,8 +48,8 @@ function fromUnion(u: E1 | E2): string { console.log(fromUnion({ kind: "one", b: "s", count: 1 })); const xs = [1, 2, 3]; -const { length: len } = xs; // object patterns over arrays keep the fence -console.log(len); +const { push: append } = xs; // object patterns over arrays read positions and length only +console.log(typeof append); const indexed: Record = { a: 1 }; const { ...allOfIt } = indexed; // index-signature rest needs overflow packing diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts new file mode 100644 index 000000000..d162872fb --- /dev/null +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -0,0 +1,18 @@ +// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, +// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' +// key format and crypto.subtle or its methods taken as values are refused +// by name; the bundled declarations omit the other SubtleCrypto methods +// (node-types/fenced.ts pins their fence under @types/node). +const fromJwk = await crypto.subtle.importKey( + "jwk", + { kty: "oct", k: "c2VjcmV0" }, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(fromJwk.type); + +const subtle = crypto.subtle; +console.log(typeof subtle); + +console.log(typeof crypto.subtle.sign); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index b884de3ce..78fbf2af7 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -97,4 +97,15 @@ void callbackified; * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); cipherStream.on("data", () => {}); +/* WebCrypto beyond the HMAC slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside importKey, sign, verify + * and digest fence by name. */ +async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +} +void encryptWithWebCrypto; +/* Node's navigator global: typeof navigator and navigator.userAgent + * compile; the other members fence by name. */ +const cores = navigator.hardwareConcurrency; +void cores; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/navigator.ts b/tests/fixtures/node-types/navigator.ts new file mode 100644 index 000000000..2f1ee2d9f --- /dev/null +++ b/tests/fixtures/node-types/navigator.ts @@ -0,0 +1,6 @@ +/* Node 24's navigator global under @types/node: the runtime probe + * libraries use (jose's user-agent check) folds to Node's values. */ +const browser = !navigator.userAgent.startsWith("Mozilla/5.0 "); +console.log(typeof navigator, navigator.userAgent, browser); +console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"); +console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length); diff --git a/tests/fixtures/node-types/webcrypto-hmac.ts b/tests/fixtures/node-types/webcrypto-hmac.ts new file mode 100644 index 000000000..4b37d7de4 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-hmac.ts @@ -0,0 +1,25 @@ +/* WebCrypto's HMAC slice under @types/node: the crypto global's subtle + * methods and CryptoKey members lower to the same native values as under + * the bundled declarations. @types/node names the dictionary and key + * types in node:crypto's webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const encoder = new TextEncoder(); + const params: webcrypto.HmacImportParams = { name: "HMAC", hash: "SHA-512" }; + const key: webcrypto.CryptoKey = await crypto.subtle.importKey("raw", encoder.encode("node-types"), params, true, [ + "verify", + "sign", + ]); + const algorithm = key.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(key.type, key.extractable, key.usages.join(","), algorithm.name, algorithm.length, algorithm.hash.name); + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode("message")); + console.log(signature.byteLength, Buffer.from(signature).toString("base64")); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("message"))); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("massage"))); + const digest = await crypto.subtle.digest("SHA-384", encoder.encode("message")); + console.log(Buffer.from(digest).toString("hex")); + console.log(key instanceof CryptoKey); +} + +void main(); diff --git a/tests/fixtures/npm/cases/chalk5/main.ts b/tests/fixtures/npm/cases/chalk5/main.ts index 6a33a1466..0bfa806c1 100644 --- a/tests/fixtures/npm/cases/chalk5/main.ts +++ b/tests/fixtures/npm/cases/chalk5/main.ts @@ -15,6 +15,13 @@ console.log(modifierNames.join(",")); console.log(foregroundColorNames.length + " " + backgroundColorNames.length + " " + colorNames.length); show("red", chalk.red("red")); +// Root members named like String.prototype's HTML methods, called from +// TypeScript (pi's `chalk.bold(APP_NAME)`): the checker types chalk's +// JavaScript default export as a plain function, so the member is `any`. +show("bold", chalk.bold("bold")); +show("bold template", `${chalk.bold("Usage:")} pi`); +const alias = chalk; +show("bold alias", alias.bold("alias") + alias.italic("i")); show("red.bold", chalk.red.bold("bold red")); show("nested", chalk.blue("a " + chalk.red("b") + " c")); show("multi-arg", chalk.green("one", "two", 3)); @@ -36,6 +43,7 @@ show("gray-grey", chalk.gray("x") + chalk.grey("y")); for (const level of [0, 1, 2, 3] as const) { const c = new Chalk({ level }); show(`L${level} red.bold`, c.red.bold("x")); + show(`L${level} bold`, c.bold("x")); show(`L${level} hex`, c.hex("#123456")("x")); show(`L${level} rgb`, c.rgb(255, 255, 255).bgRgb(0, 0, 0)("x")); show(`L${level} ansi256`, c.ansi256(100).bgAnsi256(200)("x")); diff --git a/tests/fixtures/self-hosting/ts7-checker-cases.ts b/tests/fixtures/self-hosting/ts7-checker-cases.ts index 52cb6981e..da0a3dc52 100644 --- a/tests/fixtures/self-hosting/ts7-checker-cases.ts +++ b/tests/fixtures/self-hosting/ts7-checker-cases.ts @@ -50,8 +50,8 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getBaseTypeOfLiteralType(answerType) === number, "number literal base"); check(facade.getTypeOfSymbol(answerSymbol) === answerType, "symbol type"); check(facade.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location type"); - check(facade.declarationsOf(answerSymbol)[0] === answer, "declaration identity"); - check(facade.valueDeclarationOf(answerSymbol) === answer, "value declaration"); + check(answerSymbol.declarations[0] === answer, "declaration identity"); + check(answerSymbol.valueDeclaration === answer, "value declaration"); check(facade.getContextualType(answerName) === undefined, "absent contextual type"); check(facade.getSymbolAtLocation(root.endOfFileToken) === undefined, "absent symbol"); check(facade.getUnknownType().flags === TypeFlags.Unknown, "unknown intrinsic"); @@ -77,14 +77,14 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque const identityType = facade.getTypeAtLocation(identity.name!); const signature = facade.getSignatureFromDeclaration(identity)!; check(facade.getCallSignatures(identityType)[0] === signature, "call signature identity"); - check(facade.signatureDeclaration(signature) === identity, "signature declaration identity"); + check(signature.declaration === identity, "signature declaration identity"); check(facade.typeToString(facade.getReturnTypeOfSignature(signature)) === "T", "generic return type"); check(facade.getTypePredicateOfSignature(signature) === undefined, "absent predicate"); const isBox = facade.getSignatureFromDeclaration(declaration(root, "isBox"))!; check(facade.getTypePredicateOfSignature(isBox)!.type === boxType, "predicate type identity"); const call = declaration(root, "called").initializer!; const resolved = facade.getResolvedSignature(call)!; - check(facade.signatureDeclaration(resolved) === identity, "instantiated signature declaration"); + check(resolved.declaration === identity, "instantiated signature declaration"); check(facade.typeToString(facade.getReturnTypeOfSignature(resolved)).includes("n: number"), "instantiated return type"); const ref = declaration(root, "ref").initializer!.properties?.nodes[0]!; @@ -140,11 +140,11 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getTypeOfSymbol(answerSymbol) === answerType, "warm symbol type"); check(facade.getDeclaredTypeOfSymbol(boxSymbol) === boxType, "warm declared type"); check(facade.getAliasedSymbol(alias) === answerSymbol, "warm alias"); - check(facade.declarationsOf(answerSymbol)[0] === answer, "warm declarations"); - check(facade.valueDeclarationOf(answerSymbol) === answer, "warm value declaration"); + check(answerSymbol.declarations[0] === answer, "warm declarations"); + check(answerSymbol.valueDeclaration === answer, "warm value declaration"); check(facade.getResolvedSignature(call) === resolved, "warm resolved signature"); check(facade.getSignatureFromDeclaration(identity) === signature, "warm signature"); - check(facade.signatureDeclaration(signature) === identity, "warm signature declaration"); + check(signature.declaration === identity, "warm signature declaration"); check(facade.getTypePredicateOfSignature(signature) === undefined, "warm absent predicate"); check(facade.getTypePredicateOfSignature(isBox)!.type === boxType, "warm predicate"); check(facade.getCallSignatures(identityType)[0] === signature, "warm call signatures"); diff --git a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts index 949eff4b7..710255433 100644 --- a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts +++ b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts @@ -60,7 +60,7 @@ export function checkCheckerSnapshots(session: Ts7Session, client: Ts7RpcClient, check(oldArms.length === 2 && siblingArms.length === 2, "original constituents"); check(oldArms !== siblingArms && oldArms[0] !== siblingArms[0], "project-local constituents"); const oldSymbol = old.getSymbolAtLocation(name)!; - const oldDeclaration = old.valueDeclarationOf(oldSymbol)!; + const oldDeclaration = oldSymbol.valueDeclaration!; check(oldDeclaration.name === name, "resolved declaration identity"); value = 'export const value: string | null = "new";'; @@ -73,7 +73,7 @@ export function checkCheckerSnapshots(session: Ts7Session, client: Ts7RpcClient, const newSymbol = next.getSymbolAtLocation(name)!; check(next.typeToString(newType) === "string | null", "new snapshot result"); check(newType !== oldType && newSymbol !== oldSymbol, "independent semantic identities"); - check(next.valueDeclarationOf(newSymbol) === oldDeclaration, "declarations retain shared syntax identity"); + check(newSymbol.valueDeclaration === oldDeclaration, "declarations retain shared syntax identity"); check(next.getTypeOfSymbol(newSymbol) === newType, "new symbol answer"); check(old.getTypeOfSymbol(oldSymbol) === oldType, "old symbol answer"); check(old.getTypeAtLocation(name) === oldType && old.typeToString(oldType) === "number | null", "old snapshot stays immutable"); diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index c98869785..d97f90892 100644 --- a/tests/fixtures/self-hosting/ts7-client-cases.ts +++ b/tests/fixtures/self-hosting/ts7-client-cases.ts @@ -267,7 +267,7 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st const typeText = checker.typeToString(ownedType); check(checker.getTypeAtPosition(file, content.indexOf("answer")) === ownedType, "semantic type identity"); check(checker.getSymbolAtPosition(file, content.indexOf("answer")) === ownedSymbol, "semantic symbol identity"); - check(ownedSymbol.declarations[0]!.resolve() === declaration, "semantic declaration identity"); + check(ownedSymbol.declarationHandles[0]!.resolve() === declaration, "semantic declaration identity"); check(typeText === "42" && ownedType.isNumberLiteralType() && ownedType.value === 42, "semantic literal metadata"); // The semantic exercise disposes its graph; the session still owns the // server snapshot and its program until explicit snapshot disposal. diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index 3fd34cefd..41b61b824 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -81,8 +81,8 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(checker.getTypeOfSymbol(answerSymbol) === answerType, "symbol type identity"); check(checker.getTypeOfSymbol([answerSymbol, answerSymbol])[1] === answerType, "batch symbol types"); check(checker.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location-dependent symbol type"); - check(answerSymbol.valueDeclaration?.resolve() === answer && answerSymbol.declarations[0]!.resolve() === answer, "default declaration resolution"); - check(answerSymbol.declarations[0]!.resolve(checker.project) === answer, "explicit declaration project"); + check(answerSymbol.valueDeclarationHandle?.resolve() === answer && answerSymbol.declarationHandles[0]!.resolve() === answer, "default declaration resolution"); + check(answerSymbol.declarationHandles[0]!.resolve(checker.project) === answer, "explicit declaration project"); check(checker.getTypeAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerType, "position type batch"); check(checker.getSymbolAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerSymbol, "position symbol batch"); check(checker.getResolvedSymbol(answerName)!.getExportSymbol() === answerSymbol, "resolved local/export symbol identity"); @@ -123,7 +123,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic // the binder's member symbol is distinct but refers to the same AST node. const boundMember = boxSymbol.getMembers().get("value")!; const checkedMember = checker.getPropertyOfType(boxType, "value")!; - check(boundMember !== checkedMember && boundMember.declarations[0]!.resolve() === checkedMember.declarations[0]!.resolve(), "bound and instantiated member declarations"); + check(boundMember !== checkedMember && boundMember.declarationHandles[0]!.resolve() === checkedMember.declarationHandles[0]!.resolve(), "bound and instantiated member declarations"); check(boxSymbol.getMembers() === boxSymbol.getMembers() && boxSymbol.getExports() === boxSymbol.getExports(), "symbol table cache"); check(boxType.getBaseTypes()!.length === 0 && checker.getBaseTypes(boxType).length === 0, "empty base types"); const child = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Child").name!)); @@ -147,8 +147,8 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const signature = signatureAt(checker, identity); const signatureTypes = signature.getTypeParameters(); check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters"); - check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); - check(signature.declaration!.resolve() === identity, "signature declaration"); + check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); + check(signature.declarationHandle!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); const constraint = checker.getConstraintOfTypeParameter(signatureTypes[0]!)!; @@ -179,7 +179,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(checker.isUndefinedSymbol(undefinedSymbol), "well-known undefined symbol"); const shorthand = declaration(root, "ref").initializer!.properties?.nodes[0]!; check(checker.getShorthandAssignmentValueSymbol(shorthand) === answerSymbol, "shorthand symbol"); - const exportSpecifier = renamed.declarations[0]!.resolve()!; + const exportSpecifier = renamed.declarationHandles[0]!.resolve()!; check(checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) === answerSymbol, "export specifier symbol"); check(checker.getConstantValue(declaration(root, "Mode").members?.nodes[0]!) === 3, "enum constant"); check(checker.getConstantValue(answerName) === undefined, "missing constant"); @@ -235,7 +235,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic try { answerType.getRegularType(); } catch { refused = true; } check(refused, "type access after disposal"); refused = false; - try { answerSymbol.declarations[0]!.resolve(); } catch { refused = true; } + try { answerSymbol.declarationHandles[0]!.resolve(); } catch { refused = true; } check(refused, "declaration access after disposal"); } diff --git a/tests/harness/__snapshots__/constructor-returns-instance-super.ts.txt b/tests/harness/__snapshots__/constructor-returns-instance-super.ts.txt new file mode 100644 index 000000000..53a01d431 --- /dev/null +++ b/tests/harness/__snapshots__/constructor-returns-instance-super.ts.txt @@ -0,0 +1,6 @@ +constructor-returns-instance-super.ts:11:3 - error SC1090: extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance 'Shared' returns) is not supported yet + + 10 | class Derived extends Shared { + 11 | constructor() { + | ^~~~~~~~~~~~~~~ + 12 | super(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/constructor-returns-object.ts.txt b/tests/harness/__snapshots__/constructor-returns-object.ts.txt new file mode 100644 index 000000000..fe2220d56 --- /dev/null +++ b/tests/harness/__snapshots__/constructor-returns-object.ts.txt @@ -0,0 +1,6 @@ +constructor-returns-object.ts:7:5 - error SC1090: returning an object other than an instance of the class from a constructor (Node's 'new Point()' answers the returned object, which has no representation as a 'Point' instance) is not supported yet + + 6 | constructor() { + 7 | return { x: 2 }; + | ^~~~~~~~~~~~~~~~ + 8 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-mixed.txt b/tests/harness/__snapshots__/coverage-mixed.txt index c3b348f9c..cf78aa9e0 100644 --- a/tests/harness/__snapshots__/coverage-mixed.txt +++ b/tests/harness/__snapshots__/coverage-mixed.txt @@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/mixed.ts in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) blockers: - ×1 SC1031 object destructuring of non-record values (the source is array-typed) + ×1 SC1031 object destructuring of non-record values (the source is union-typed) at tests/coverage-fixtures/mixed.ts:13:7 hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/deferred-class-receiver/main.ts.txt b/tests/harness/__snapshots__/deferred-class-receiver/main.ts.txt index 0e9cab090..909795174 100644 --- a/tests/harness/__snapshots__/deferred-class-receiver/main.ts.txt +++ b/tests/harness/__snapshots__/deferred-class-receiver/main.ts.txt @@ -5,9 +5,9 @@ deferred-class-receiver/main.ts:4:10 - error SC1090: constructing through a clas | ^~~~~~~~~~~~~~~ 5 | } -deferred-class-receiver/shelf.ts:3:20 - error SC1090: 'WeakMap'-typed class fields are not supported yet +deferred-class-receiver/shelf.ts:3:20 - error SC1090: 'MapIterator'-typed class fields are not supported yet 2 | readonly name: string; - 3 | private readonly notes = new WeakMap(); - | ^~~~~ + 3 | private readonly cursor = new Map().keys(); + | ^~~~~~ 4 | constructor(name: string) { \ No newline at end of file diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 65d9c293b..743bb5657 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -38,9 +38,9 @@ destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on destructuring-fences.ts:51:7 - error SC1031: object destructuring of non-record values (the source is array-typed) are not supported yet 50 | const xs = [1, 2, 3]; - 51 | const { length: len } = xs; // object patterns over arrays keep the fence - | ^~~~~~~~~~~~~~~ - 52 | console.log(len); + 51 | const { push: append } = xs; // object patterns over arrays read positions and length only + | ^~~~~~~~~~~~~~~~ + 52 | console.log(typeof append); destructuring-fences.ts:55:9 - error SC1031: rest bindings over index-signature shapes (the undeclared entries would need overflow packing) are not supported yet diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 29e4a2ee9..81db57e65 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -210,6 +210,24 @@ fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); 99 | cipherStream.on("data", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | // End of the declared-but-not-lowered surface. + 100 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not \ No newline at end of file + hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not + +fenced.ts:104:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet + + 103 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 104 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + | ^~~~~~~~~~~~~~~~~~~~~ + 105 | } + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +fenced.ts:109:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet + + 108 | * compile; the other members fence by name. */ + 109 | const cores = navigator.hardwareConcurrency; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 110 | void cores; + + hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt new file mode 100644 index 000000000..521edff3c --- /dev/null +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -0,0 +1,26 @@ +webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet + + 6 | const fromJwk = await crypto.subtle.importKey( + 7 | "jwk", + | ^~~~~ + 8 | { kty: "oct", k: "c2VjcmV0" }, + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet + + 14 | + 15 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 16 | console.log(typeof subtle); + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet + + 17 | + 18 | console.log(typeof crypto.subtle.sign); + | ^~~~~~~~~~~~~~~~~~ + 19 | + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 1cb2f1a14..30899d72b 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2255,6 +2255,7 @@ "6077-generator-helpers-untyped.js": 2550, "6078-destructure-untyped-iterables.js": 2500, "6079-generator-helpers-any.ts": 2550, + "6080-take-then-flatmap.ts": 2600, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -2521,6 +2522,7 @@ "constant-module-globals/main.ts": 2455, "constant-number-remainders.ts": 2350, "constructed-class-dispatch.ts": 3014, + "constructor-returns-this-super.js": 2431, "contextual-array-union-write.ts": 5379, "contextual-return-layouts.ts": 2919, "counted-loop-numbers.ts": 2515, @@ -5347,6 +5349,7 @@ "6077-generator-helpers-untyped.js": 3050, "6078-destructure-untyped-iterables.js": 2550, "6079-generator-helpers-any.ts": 2700, + "6080-take-then-flatmap.ts": 2750, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, @@ -5613,6 +5616,7 @@ "constant-module-globals/main.ts": 2911, "constant-number-remainders.ts": 2372, "constructed-class-dispatch.ts": 3382, + "constructor-returns-this-super.js": 4629, "contextual-array-union-write.ts": 6417, "contextual-return-layouts.ts": 3746, "counted-loop-numbers.ts": 2724, diff --git a/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json b/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json new file mode 100644 index 000000000..4203ad979 --- /dev/null +++ b/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 1900 } diff --git a/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json b/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json new file mode 100644 index 000000000..0b3994fcd --- /dev/null +++ b/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 3300 } diff --git a/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json b/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json new file mode 100644 index 000000000..2d2f07e08 --- /dev/null +++ b/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 5400 } diff --git a/tests/harness/corpus-costs/6146-jsval-operators.js.json b/tests/harness/corpus-costs/6146-jsval-operators.js.json new file mode 100644 index 000000000..b47e718e6 --- /dev/null +++ b/tests/harness/corpus-costs/6146-jsval-operators.js.json @@ -0,0 +1 @@ +{ "plain": 5693, "san": 34396 } diff --git a/tests/harness/corpus-costs/6147-jsval-any-operators.ts.json b/tests/harness/corpus-costs/6147-jsval-any-operators.ts.json new file mode 100644 index 000000000..0a0d95f23 --- /dev/null +++ b/tests/harness/corpus-costs/6147-jsval-any-operators.ts.json @@ -0,0 +1 @@ +{ "plain": 2623, "san": 3590 } diff --git a/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..c6ea1665b --- /dev/null +++ b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json @@ -0,0 +1 @@ +{ "plain": 20300, "san": 40800 } diff --git a/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json b/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json new file mode 100644 index 000000000..ab2bc177b --- /dev/null +++ b/tests/harness/corpus-costs/bound-constructor-inspect-names.cjs.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 2700 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 8cf5d6719..b1e25dead 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -421,7 +421,7 @@ test("a rejected class reached only through a receiver probe reports its own dia const { coverage } = analyze(join(repoRoot, "tests/diagnostics/deferred-class-receiver/main.ts")); const messages = coverage.diagnostics.map((d) => d.message); expect( - messages.filter((m) => m.startsWith("'WeakMap'-typed class fields")), + messages.filter((m) => m.startsWith("'MapIterator'-typed class fields")), ).toHaveLength(1); expect(messages.some((m) => m.startsWith("constructing through a class value"))).toBe(true); expect(coverage.unreached?.diagnostics ?? []).toEqual([]); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 92ac8f5ab..26dfd3a5c 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1051,6 +1051,107 @@ console.log(typeof Derived); }); }); +describe("constructors returning objects (JS lane)", () => { + test("a returned plain object is a named deferred fence", async () => { + // Node's `new Box(2)` answers `{ size: 99 }`; a Box instance cannot + // hold that object, so the return refuses instead of answering `this`. + const r = await compileAndRun( + "constructor-returns-object", + `'use strict'; +class Box { + constructor(size) { + this.size = size; + if (size > 1) return { size: 99 }; + } +} +console.log(new Box(1).size); +console.log(new Box(2)); +`, + "cjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("1\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: returning an object other than an instance of the class from a constructor \(Node's 'new Box\(\)' answers the returned object, .*\) is not supported yet \[SC1090 at .*constructor-returns-object\.cjs:5\]\n$/, + ); + }); + + test("an untyped returned value fences only when it holds a foreign object", async () => { + // Primitive results answer the constructed instance, as in Node; an + // object that is not a Wrap instance refuses. + const r = await compileAndRun( + "constructor-returns-untyped", + `'use strict'; +class Wrap { + constructor(value) { + this.value = value; + return value; + } +} +console.log(new Wrap(1).value, new Wrap('s').value, new Wrap(null).value); +const w = new Wrap(0); +console.log(new Wrap(w) === w); +console.log(new Wrap({ a: 1 })); +`, + "cjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("1 s null\ntrue\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: returning an object other than an instance of the class from a constructor .* is not supported yet \[SC1090 at .*constructor-returns-untyped\.cjs:5\]\n$/, + ); + }); +}); + +describe("module exports that read this, called through the module binding (JS lane)", () => { + test("a require binding's member call is a named deferred fence", async () => { + // Node passes the exports object as `this` to `m.hello()`; the direct + // call has none to pass, so it refuses instead of reading another one. + const r = await compileAndRun( + "require-member-this", + `'use strict'; +const m = require('./lib.cjs'); +console.log(m.plain(21)); +console.log(m.hello()); +`, + "cjs", + { + "lib.cjs": `'use strict'; +exports.name = 'lib'; +exports.hello = function () { return 'hi from ' + this.name; }; +exports.plain = function (x) { return x * 2; }; +`, + }, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("42\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: calling a module export that reads 'this' through its module binding \('hello': .*\) is not supported yet \[SC1090 at .*require-member-this\.cjs:4\]\n$/, + ); + }); + + test("a namespace import's member call is a named deferred fence", async () => { + const r = await compileAndRun( + "namespace-member-this", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.who()); +`, + "mjs", + { + "lib.mjs": `export function who() { return typeof this; } +export function plain(x) { return x + 1; } +`, + }, + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("2\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: calling a module export that reads 'this' through its module binding \('who': .*\) is not supported yet \[SC1090 at .*namespace-member-this\.mjs:3\]\n$/, + ); + }); +}); + // CommonJS forms around the module's exports that keep a refusal name the // construct itself (they used to report an unlowerable `require` value, a // "binding form with no lowering", or the ES-module `module` message). @@ -1130,3 +1231,65 @@ console.log(count(require("./lib.cjs"))); ); }); }); + +// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and +// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice +// reject with a coded SC2020 Error instead of a Node-shaped success, and a +// JavaScript call with the 'jwk' format meets its named fence when it runs. +// Node's own rejections (unknown names, bad usages) are corpus-pinned +// (6292, 6294); these refusals have no Node twin, so they live here. +describe("WebCrypto refusals (JS lane)", () => { + test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + const r = await compileAndRun( + "webcrypto-unsupported-algorithms", + `const bytes = new TextEncoder().encode("secret"); +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("rsa", () => + crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]), +); +const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); +await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); +await attempt("sha3 hmac", () => + crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), +); +const format = JSON.parse('"jwk"'); +await attempt("computed jwk", () => + crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +`, + "mjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" + + "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + + "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + ); + }); + + test("a literal 'jwk' import is a named fence where it runs", async () => { + const r = await compileAndRun( + "webcrypto-jwk-literal", + `console.log("before"); +await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +`, + "mjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("before\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + ); + }); +}); diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts index c0bb5b2be..bcd1d6ae0 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -57,6 +57,9 @@ export const programs = [ "class-array-optional-return.ts", "contextual-array-union-write.ts", "6020-class-extends-function-constructor.js", + // Re-chosen key orders rebuild the inspect helpers after lowering. + "key-order-late-optional.ts", + "5888-inspect-sorted.ts", ]; interface LowerRequest { diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index bf57a830a..802fbd3e1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -839,6 +839,42 @@ test("node-types: Cipheriv and Decipheriv match Node under @types/node", async ( expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-hmac"); + const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-hmac"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { + const outDir = outDirFor("node-navigator"); + const entry = join(nodeTypesDir, "navigator.ts"); + const result = await compile(entry, { + outPath: join(outDir, "navigator"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: declared-but-not-lowered surface fences, naming @types/node", async () => { const outDir = outDirFor("node-fenced"); const result = await compile(join(nodeTypesDir, "fenced.ts"), { diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c22e8a839..fa71b5a08 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -42,6 +42,7 @@ const features: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("native toolchain configuration, staging and helper validation are static", async () => { diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 08eb45754..c95d6d9a0 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -43,6 +43,7 @@ const base: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("runtime manifest validation and selection are entirely static", async () => { diff --git a/tests/harness/stdlib-surface.test.ts b/tests/harness/stdlib-surface.test.ts index 53c2704bf..d2ffab6cb 100644 --- a/tests/harness/stdlib-surface.test.ts +++ b/tests/harness/stdlib-surface.test.ts @@ -119,7 +119,7 @@ function calleeSymbol(decl: ts.VariableDeclaration): ts.Symbol | undefined { function isStdlibDeclared(symbol: ts.Symbol | undefined): boolean { if (!symbol) return false; - return checker.declarationsOf(symbol).some((d) => { + return symbol.declarations.some((d) => { const file = d.getSourceFile(); return ( file.fileName === ambient ||