From 4f0975cbbe0d43e5ce2a3c2be936472227ff253c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 12:01:26 -0500 Subject: [PATCH 1/2] Compile more npm and CommonJS JavaScript dependencies statically --- .../builtin-apis/intl-segmenter.mdx | 5 +- .../src/backend/executable-features.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 3 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../src/executable/executable-cache.ts | 1 + .../src/frontend/lowering/binding-analysis.ts | 17 +- .../frontend/lowering/builtins/reflection.ts | 34 +++- .../lowering/class-dynamic-dispatch.ts | 1 + .../lowering/expressions/identifiers.ts | 32 +++ .../src/frontend/lowering/function-returns.ts | 17 ++ .../src/frontend/lowering/lower-calls.ts | 12 +- .../src/frontend/lowering/lower-classes.ts | 183 ++++++++++++------ .../src/frontend/lowering/lower-namespaces.ts | 50 ++++- .../src/frontend/lowering/lower-stmts.ts | 138 ++++++++++++- .../compiler/src/frontend/lowering/lowerer.ts | 35 +++- .../lowering/module-namespace-elements.ts | 124 +++++++++++- .../compiler/src/frontend/package-shims.ts | 47 +++-- packages/compiler/src/frontend/program.ts | 121 +++++++++--- packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 6 + packages/compiler/src/ir/runtime-features.ts | 4 + .../compiler/src/native/sanitizer.test.ts | 1 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../8218-cjs-namespace-imports/main.mjs.json | 9 + .../8219-cjs-stable-var-exports/main.js.json | 9 + .../main.js.json | 9 + .../main.ts.json | 6 + .../8222-js-class-static-reflection.js.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_blob.h | 12 +- packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_grapheme.c | 55 ++++++ packages/runtime/src/scr_json.c | 39 ++++ packages/runtime/src/scr_module_namespace.c | 121 ++++++++++++ .../8214-unshimmed-package-version/main.ts | 5 + .../node_modules/chalk/package.json | 1 + .../node_modules/chalk/source/index.d.ts | 3 + .../node_modules/chalk/source/index.js | 16 ++ .../node_modules/chalk/source/styles.js | 1 + .../8216-module-namespace-live-values/main.ts | 11 ++ .../node_modules/ns-registry/counter.js | 7 + .../node_modules/ns-registry/index.d.ts | 5 + .../node_modules/ns-registry/index.js | 40 ++++ .../node_modules/ns-registry/package.json | 1 + .../main.ts | 8 + .../node_modules/text-width/index.d.ts | 2 + .../node_modules/text-width/index.js | 11 ++ .../node_modules/text-width/package.json | 1 + .../client/index.mjs | 12 ++ .../client/package.json | 1 + .../client/transport.js | 26 +++ .../8218-cjs-namespace-imports/main.mjs | 13 ++ .../8218-cjs-namespace-imports/table.cjs | 10 + .../8219-cjs-stable-var-exports/lib/core.js | 31 +++ .../8219-cjs-stable-var-exports/lib/index.js | 8 + .../lib/options.js | 12 ++ .../8219-cjs-stable-var-exports/main.js | 21 ++ .../8219-cjs-stable-var-exports/package.json | 3 + .../classes/comparator.js | 22 +++ .../formdata.js | 26 +++ .../main.js | 39 ++++ .../package.json | 3 + .../ranges/outside.js | 13 ++ .../main.ts | 10 + .../node_modules/token-parser/dist/index.d.ts | 4 + .../node_modules/token-parser/dist/index.js | 131 +++++++++++++ .../node_modules/token-parser/package.json | 1 + .../corpus/8222-js-class-static-reflection.js | 94 +++++++++ .../cjs-mutable-exports/main.js | 5 + .../cjs-mutable-exports/package.json | 1 + .../cjs-mutable-exports/reassigned.js | 4 + .../cjs-mutable-exports/redeclared.js | 4 + .../cjs-mutable-exports/stable.js | 3 + .../cjs-mutable-exports/written.js | 4 + tests/diagnostics/intl-segmenter.ts | 4 +- .../node_modules/word-segments/index.d.ts | 1 + .../node_modules/word-segments/index.js | 9 + .../node_modules/word-segments/package.json | 1 + .../fixtures/npm-static/word-segments-cli.ts | 3 + .../__snapshots__/intl-segmenter.ts.txt | 10 +- .../main.ts.json | 1 + .../main.ts.json | 1 + .../main.ts.json | 1 + .../8218-cjs-namespace-imports/main.mjs.json | 1 + .../8219-cjs-stable-var-exports/main.js.json | 1 + .../main.js.json | 1 + .../main.ts.json | 1 + .../8222-js-class-static-reflection.js.json | 1 + tests/harness/coverage.test.ts | 10 + tests/harness/npm-static.test.ts | 22 +++ tests/harness/package-shims.test.ts | 30 ++- 97 files changed, 1725 insertions(+), 119 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json create mode 100644 packages/runtime/src/scr_module_namespace.c create mode 100644 tests/corpus/8214-unshimmed-package-version/main.ts create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js create mode 100644 tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js create mode 100644 tests/corpus/8216-module-namespace-live-values/main.ts create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js create mode 100644 tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/main.ts create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js create mode 100644 tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/index.mjs create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/package.json create mode 100644 tests/corpus/8218-cjs-namespace-imports/client/transport.js create mode 100644 tests/corpus/8218-cjs-namespace-imports/main.mjs create mode 100644 tests/corpus/8218-cjs-namespace-imports/table.cjs create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/core.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/index.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/lib/options.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/main.js create mode 100644 tests/corpus/8219-cjs-stable-var-exports/package.json create mode 100644 tests/corpus/8220-js-class-static-destructuring/classes/comparator.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/formdata.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/main.js create mode 100644 tests/corpus/8220-js-class-static-destructuring/package.json create mode 100644 tests/corpus/8220-js-class-static-destructuring/ranges/outside.js create mode 100644 tests/corpus/8221-js-recursive-generator-returns/main.ts create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js create mode 100644 tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json create mode 100644 tests/corpus/8222-js-class-static-reflection.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/main.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/package.json create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/reassigned.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/redeclared.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/stable.js create mode 100644 tests/coverage-fixtures/cjs-mutable-exports/written.js create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/index.js create mode 100644 tests/fixtures/npm-static/node_modules/word-segments/package.json create mode 100644 tests/fixtures/npm-static/word-segments-cli.ts create mode 100644 tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json create mode 100644 tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json create mode 100644 tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json create mode 100644 tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json create mode 100644 tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json create mode 100644 tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json create mode 100644 tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json create mode 100644 tests/harness/corpus-costs/8222-js-class-static-reflection.js.json diff --git a/docs/content/limitations/builtin-apis/intl-segmenter.mdx b/docs/content/limitations/builtin-apis/intl-segmenter.mdx index 41a61711c..2682acaf0 100644 --- a/docs/content/limitations/builtin-apis/intl-segmenter.mdx +++ b/docs/content/limitations/builtin-apis/intl-segmenter.mdx @@ -1,2 +1,3 @@ -- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile. - Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. +- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale). + In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error. + Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported. diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 2afb887ea..b51f957a3 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -19,6 +19,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7a16c37e8..de2fbaa2e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -40,6 +40,7 @@ export const LIB_FN_SYMS: Record = { "insp.jsonDyn": "scr_util_format_j", "http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw", "intl.segmenterNew": "scr_intl_segmenter_new", + "intl.segmenterRefusing": "scr_intl_segmenter_refusing", "weakMap.is": "scr_weak_map_is", "weakSet.is": "scr_weak_set_is", "weakMap.new": "scr_weak_map_new", @@ -254,6 +255,7 @@ export const LIB_FN_SYMS: Record = { "global.native": "scr_global_native", "process.builtinModule": "scr_process_builtin_module", "process.builtinUnsupported": "scr_process_builtin_unsupported", + "module.namespaceValue": "scr_module_namespace_value", "fs.callbackValue": "scr_fs_callback_value", "fs.callbackCall": "scr_fs_callback_call", "process.hrtimeValue": "scr_process_hrtime_value", @@ -987,6 +989,7 @@ export const LIB_FN_SYMS: Record = { "dyn.keySetComputed": "scr_dyn_key_set_computed", "dyn.keyDelete": "scr_dyn_key_delete", "dyn.keyDeleteComputed": "scr_dyn_key_delete_computed", + "dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property", "dyn.hasKeyComputed": "scr_dyn_has_key_computed", "dyn.hasOwnComputed": "scr_dyn_has_own_computed", "dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 1e9994422..6af58fb0e 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -34,6 +34,9 @@ export interface NativeLinkFeatures { /** The program uses process.stdin.setEncoding, string 'data' listeners * or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */ stdioExtended?: boolean; + /** Module namespace objects as checked-dynamic values link + * scr_module_namespace.c; absent (false) otherwise. */ + moduleNamespace?: boolean; zlib: boolean; /** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */ brotli?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 40bb2352b..ce89cb829 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -147,6 +147,9 @@ export interface CcOptions { warningReplace?: boolean; /** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */ stdioExtended?: boolean; + /** Module namespace objects as checked-dynamic values: compiles + * scr_module_namespace.c. */ + moduleNamespace?: 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 diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index ceb162bcc..1dd88117e 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -194,6 +194,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), unit((u) => u.opts.wasm, "scr_wasm.c"), + // Module namespace objects as checked-dynamic values. + unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"), unit( (u) => u.opts.dynAsync || diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 49165d8df..22bba85d2 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -94,6 +94,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { warningReplace: boolean; stdioExtended: boolean; brotli: boolean; + moduleNamespace: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -293,6 +294,7 @@ export function effectiveRuntimeFeatures( warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, brotli: features.brotli === true, + moduleNamespace: features.moduleNamespace === true, nativeFetch, netEffective, httpEffective, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 754c20648..e641b8db0 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -62,6 +62,7 @@ export interface EarlyExecutableNativeFeatures { wasm?: boolean; warningReplace?: boolean; stdioExtended?: boolean; + moduleNamespace?: boolean; zlib: boolean; brotli?: boolean; assert: boolean; diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 1987c86ba..c88c06c90 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -19,6 +19,18 @@ export function bindingNeverReassigned( ): boolean { const cached = lowerer.neverReassignedCache.get(sym); if (cached !== undefined) return cached; + const written = fileWritesBinding(sym, ts.getSourceFileOfNode(decl), resolve); + lowerer.neverReassignedCache.set(sym, !written); + return !written; +} + +/** True when something in `file` writes `sym` (the writes + * bindingNeverReassigned counts), resolving identifiers with `resolve`. */ +export function fileWritesBinding( + sym: ts.Symbol, + file: ts.SourceFile, + resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined, +): boolean { let written = false; // Text pre-check keeps the file walk cheap: only same-named // identifiers pay a symbol resolution. @@ -62,9 +74,8 @@ export function bindingNeverReassigned( } ts.forEachChild(n, visit); }; - ts.forEachChild(ts.getSourceFileOfNode(decl), visit); - lowerer.neverReassignedCache.set(sym, !written); - return !written; + ts.forEachChild(file, visit); + return written; } /** Strips the value-preserving wrappers off an expression: parens, diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8e1e6fd94..2ce07ea2f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -3,7 +3,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { builtinModuleFnOf } from "../surfaces.js"; -import { BOOL, DYN, type IrExpr, STRING, arrayOf } from "../../../ir/ir.js"; +import { hasRuntimeStatics } from "../class-runtime-statics.js"; +import { BOOL, DYN, type IrExpr, STRING, arrayOf, isUnitType } from "../../../ir/ir.js"; /** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target, * thisArg, argsList) where the TARGET is a builtin @@ -63,6 +64,37 @@ export function lowerReflectCall( loc, }; } + // Reflect.deleteProperty over a value with a live property table: a + // checked-dynamic value, or a JavaScript class whose statics are runtime + // properties of its constructor + // (`Reflect.deleteProperty(Response, "getResponseState")`). + if ( + reflectMember === "deleteProperty" && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) + ) { + const loc = locOf(call); + let target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const info = + target.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + // A primitive target throws the TypeError, so it boxes as is. + const primitive = + isUnitType(target.type) || + ["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind); + if ( + primitive || + (info !== undefined && hasRuntimeStatics(info) && lowerer.dynConvertible(target.type)) + ) + target = lowerer.coerceToExpected(target, DYN); + if (target.type.kind !== "dyn") return null; + return { + kind: "libCall", + fn: "dyn.reflectDeleteProperty", + args: [target, lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN)], + type: BOOL, + loc, + }; + } if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fc6d0a922..a943b9156 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -262,6 +262,7 @@ const KEY_CALLS = new Map([ ["dyn.propertyIsEnumerableComputed", "any"], ["dyn.hasOwnComputed", "value"], ["dyn.keyDeleteComputed", "value"], + ["dyn.reflectDeleteProperty", "object"], ["dyn.keySetComputed", "value"], ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index f7ba7a115..db1c1938a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -57,6 +57,7 @@ import { contextualUndefReadType, fenceEarlyAliasUse, isTemporalGlobalRoot, + cjsNamespaceImportDep, lowerNsIdentifierValue, nsUndefRead, } from "../lower-namespaces.js"; @@ -958,6 +959,37 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // representation (ambient namespaces compile to Node's // ReferenceError instead — the object never exists at runtime). { + // `ns.name` through a namespace import of a CommonJS module, where Node's + // namespace has no `name` property (its lexer does not detect it), or + // `ns.default` (module.exports itself). + const cjsNamespace = cjsNamespaceImportDep(lowerer, expr); + if ( + cjsNamespace !== null && + ts.isPropertyAccessExpression(expr.parent) && + expr.parent.expression === expr && + ts.isIdentifier(expr.parent.name) + ) { + const name = expr.parent.name.text; + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjsNamespace); + if (name === "default") + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of 'default' through namespace imports of CommonJS modules (module.exports itself: read it through a default import)`, + ); + if (!names.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules (Node's lexer does not detect '${name}' among the module's exports, so Node reads undefined)`, + ); + if (assignedLater === "all" || assignedLater.has(name)) + lowerer.unsupported( + "SC1090", + expr.parent, + `reads of '${name}' through namespace imports of CommonJS modules whose functions assign exports (Node's namespace keeps the values from the end of the module's evaluation)`, + ); + } const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (moduleNs?.kind === "moduleNs") { return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc }; diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 6d0989e2b..c4563a234 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -175,6 +175,23 @@ export function declaredReturnType( // `() => void`), and throw-only callbacks are ordinary code // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; + // A JS generator whose result the checker cannot infer (mutually + // recursive methods returning `yield*` of each other, as in yaml's + // Parser) has the return type `any`: a generator over untyped values. + if ( + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + decl.asteriskToken && + retTsType.flags & ts.TypeFlags.Any + ) + return { + kind: "generator", + ...(decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + ? { async: true as const } + : {}), + yieldT: DYN, + retT: DYN, + nextT: DYN, + }; let mappedReturn = lowerer.mapTypeOf(retTsType); // A generator declared to return Iterable returns a generator over T. if (mappedReturn && decl.asteriskToken) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 14565aebc..4b833ee50 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8464,7 +8464,17 @@ function lowerObjectStaticCall( ...(fnName !== null ? { fnName } : {}), }; } - if (target && isUnitType(target.type)) { + // A class value whose statics are runtime properties of its constructor + // boxes to that constructor function, as for Object.defineProperty above. + const runtimeStaticClass = + target?.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined; + if ( + target && + (isUnitType(target.type) || + (runtimeStaticClass !== undefined && + hasRuntimeStatics(runtimeStaticClass) && + lowerer.dynConvertible(target.type))) + ) { target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; } if (target?.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index b829b6ddc..e50466114 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -11,7 +11,7 @@ import { lowerDateStaticCall } from "./builtins/dates.js"; import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; -import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; +import { dynUndefinedExpr, nodeThrowExpr, strLit, varRef } from "../../ir/build.js"; import { constructedFromValue, constructedValue, @@ -124,6 +124,7 @@ import { lowerWorkerNew, } from "./builtins/workers.js"; import { + noLoweringDiag, unsupportedPackageValueDiag, unsupportedDiag, type ScrDiagnostic, @@ -5687,15 +5688,22 @@ export function lowerStaticFieldRead( ): IrExpr | null { if (expr.name.text === "prototype") return null; if (expr.questionDotToken) return null; + const info = staticReceiverClass(lowerer, expr.expression); + if (!info) return null; + return lowerClassStaticRead(lowerer, info, expr.name.text, expr.expression, expr); +} + +/** The class a static member read through `receiver` reads exactly: the + * class a name binds (directly, through a require binding, or through a + * const alias), or null. */ +export function staticReceiverClass(lowerer: Lowerer, receiver: ts.Expression): ClassInfo | null { if ( - !ts.isIdentifier(expr.expression) && - expr.expression.kind !== ts.SyntaxKind.ThisKeyword && - exactClassOfReceiver(lowerer, expr.expression) === null + !ts.isIdentifier(receiver) && + receiver.kind !== ts.SyntaxKind.ThisKeyword && + exactClassOfReceiver(lowerer, receiver) === null ) return null; - const symbol = ts.isIdentifier(expr.expression) - ? lowerer.resolveValueSymbol(expr.expression) - : null; + const symbol = ts.isIdentifier(receiver) ? lowerer.resolveValueSymbol(receiver) : null; const info = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // A require binding over `module.exports = class {…}` (the alias @@ -5703,7 +5711,7 @@ export function lowerStaticFieldRead( // or `C.name` below would fall through to paths that answer for // stdlib globals instead of this class. propertyAssignedClassInfoOf(lowerer, symbol) ?? - exactClassOfReceiver(lowerer, expr.expression) ?? + exactClassOfReceiver(lowerer, receiver) ?? undefined; if (!info) return null; // A decorated name that can REBIND (a replacing decorator): the @@ -5712,22 +5720,36 @@ export function lowerStaticFieldRead( // whose devirtualization and .name rules answer for every legal // runtime value. if (info.classDecorators?.valueGlobalId !== undefined) return null; - const loc = locOf(expr); - const found = findStaticOn(lowerer, info, expr.name.text); + return info; +} + +/** The static member `name` of `info` read through `receiver` (an exact + * reference to the class, see staticReceiverClass): `C.name` for the + * property access `site`, or a destructuring element over `C` whose + * initializer is `site`. Null when the class has no such member. */ +export function lowerClassStaticRead( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: ts.Expression, + site: ts.Expression, +): IrExpr | null { + const loc = locOf(site); + const found = findStaticOn(lowerer, info, name); // A Date static read as a value through a class declared `extends Date`: // the statics lower only as calls. if ( !found && dateRootOf(info) && - ["now", "parse", "UTC"].includes(expr.name.text) && - !(ts.isCallExpression(expr.parent) && expr.parent.expression === expr) + ["now", "parse", "UTC"].includes(name) && + !(ts.isCallExpression(site.parent) && site.parent.expression === site) ) - lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); + lowerer.noLowering(`Date.${name} read through a class that extends Date`, site); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; @@ -5735,11 +5757,11 @@ export function lowerStaticFieldRead( // name: in JS the brand lives on that one constructor object, so // `D.#s` (a subclass receiver) throws Node's TypeError instead of // reaching up the chain — reported as SC1090 instead of resolved. - if (found && expr.name.text.startsWith("#") && found.declarer !== info) { + if (found && name.startsWith("#") && found.declarer !== info) { lowerer.unsupported( "SC1090", - expr, - `reading the private static '${expr.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, + site, + `reading the private static '${name}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } if (found?.field !== undefined) { @@ -5749,29 +5771,31 @@ export function lowerStaticFieldRead( type: found.field.type, loc, }; - return found.field.runtimeOptional ? value : lowerer.maybeNarrow(value, expr); + return found.field.runtimeOptional || !ts.isPropertyAccessExpression(site) + ? value + : lowerer.maybeNarrow(value, site); } if (found) { - return staticMethodValue(lowerer, found.declarer, expr.name.text, found.method, expr, loc); + return staticMethodValue(lowerer, found.declarer, name, found.method, site, loc); } // A GENERIC static method as a VALUE: the pinned-value rule verbatim // (lowerGenericFnValue) — a slot spelling one concrete signature names // an instance, an unpinned reference fences by name. { - const gfound = findGenericStaticOn(lowerer, info, expr.name.text); - if (gfound) return lowerer.lowerGenericFnValue(expr, gfound.info); + const gfound = findGenericStaticOn(lowerer, info, name); + if (gfound) return lowerer.lowerGenericFnValue(site, gfound.info); } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the // chain above can never shadow it in TypeScript sources). A class of an // import cycle reads it from its class object instead, whose reference // checks the class's initialization (checkCycleBindingAccesses). - if (expr.name.text === "name" && info.def.jsName !== undefined) { + if (name === "name" && info.def.jsName !== undefined) { if (lowerer.classInitFlags.get(info.def.name)?.cycle === true) return { kind: "libCall", fn: "class.name", - args: [classValueRef(lowerer, info, expr.expression)], + args: [classValueRef(lowerer, info, receiver)], type: STRING, loc, }; @@ -5780,26 +5804,26 @@ export function lowerStaticFieldRead( if (info.callableBase) return { kind: "dynKeyGet", - value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN), + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; - const data = classStaticDataFor(lowerer, info, expr.name.text, loc); + const data = classStaticDataFor(lowerer, info, name, loc); if (data) { const value: IrExpr = { kind: "dynKeyGet", value: data, - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc, }; // JavaScript often assigns public statics after the class declaration. // Their storage stays mutable and checked, while reads retain the // checker's concrete type instead of losing the method receiver. - const type = isJsSourceFile(ts.getSourceFileOfNode(expr)) + const type = isJsSourceFile(ts.getSourceFileOfNode(site)) ? null - : lowerer.mapTypeOf(lowerer.typeOf(expr)); + : lowerer.mapTypeOf(lowerer.typeOf(site)); return type ? lowerer.coerceToExpected(value, type) : value; } return null; @@ -6625,7 +6649,7 @@ function staticCallOn( kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, - calleeName: access.name.text, + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc, @@ -9902,34 +9926,47 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const args = expr.arguments?.nodes ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; - if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { + // A literal language tag: Unicode grapheme clusters have no locale + // tailoring. Only tags of the form language[-script][-region] are + // accepted; each is well-formed, so Node constructs the segmenter + // without the RangeError a malformed tag throws. + const literalLocale = (arg: ts.Expression): boolean => + ts.isStringLiteralLike(arg) && + /^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/.test(arg.text); + if (args.length > 2 || (args[0] && !undefinedArg(args[0]) && !literalLocale(args[0]))) { lowerer.noLowering( "Intl.Segmenter locale negotiation", expr, - "omit the locale or pass undefined for default Unicode grapheme segmentation", + "omit the locale, pass undefined, or pass one language tag as a string literal", ); } + // JavaScript dependencies may construct word and sentence segmenters + // that they use only on some paths (pi-tui's shared word segmenter): + // the construction compiles and their segment() throws SC2020. + // TypeScript sources keep the build-time refusal. + const granularities = isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ["grapheme", "word", "sentence"] + : ["grapheme"]; const options = args[1]; - if ( - options && - !undefinedArg(options) && - !( - ts.isObjectLiteralExpression(options) && - options.properties.nodes.every( - (property) => - ts.isPropertyAssignment(property) && - (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && - property.name.text === "granularity" && - ts.isStringLiteral(property.initializer) && - property.initializer.text === "grapheme", - ) - ) - ) { - lowerer.noLowering( - "Intl.Segmenter options", - options, - 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', - ); + let granularity = "grapheme"; + if (options && !undefinedArg(options)) { + const properties = ts.isObjectLiteralExpression(options) ? options.properties.nodes : null; + const property = properties?.length === 1 ? properties[0] : undefined; + if ( + property === undefined || + !ts.isPropertyAssignment(property) || + !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || + property.name.text !== "granularity" || + !ts.isStringLiteral(property.initializer) || + !granularities.includes(property.initializer.text) + ) { + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + } + granularity = property.initializer.text; } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { @@ -9940,7 +9977,28 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc: locOf(arg), }; }); - const result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + let result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; + // A word or sentence segmenter carries its segment() refusal, which + // coverage lists as deferred to runtime. + if (granularity !== "grapheme") { + const surface = `Intl.Segmenter segmentation with granularity '${granularity}'`; + lowerer.runtimeFences.push( + noLoweringDiag( + surface, + loc, + "only grapheme segmenters segment; word and sentence segmenters construct", + ), + ); + const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(expr), loc.start).line; + const refusal = `${surface} is not supported yet [SC2020 at ${loc.file}:${line + 1}]`; + result = { + kind: "libCall", + fn: "intl.segmenterRefusing", + args: [strLit(refusal, loc)], + type: DYN, + loc, + }; + } return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; } // A package declaration without a compiled constructor cannot provide @@ -11266,13 +11324,18 @@ function genericFieldFnNodeOf( } /** Whether a JavaScript class's own binding is the target of - * `Object.defineProperty`/`defineProperties` in its file - * (`Object.defineProperty(Definition, "name", { value })`): its constructor + * `Object.defineProperty`/`defineProperties` or `Reflect.deleteProperty` in + * its file (`Object.defineProperty(Definition, "name", { value })`, + * `Reflect.deleteProperty(Response, "getResponseState")`): its constructor * then needs a live own-property table. */ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean { const name = decl.name; const sf = ts.getSourceFileOfNode(decl); - if (name === undefined || !sf.text.includes("definePropert")) return false; + if ( + name === undefined || + (!sf.text.includes("definePropert") && !sf.text.includes("deleteProperty")) + ) + return false; const symbol = lowerer.checker.getSymbolAtLocation(name); if (symbol === undefined) return false; let found = false; @@ -11281,9 +11344,11 @@ function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaratio if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && - (node.expression.name.text === "defineProperty" || + (((node.expression.name.text === "defineProperty" || node.expression.name.text === "defineProperties") && - lowerer.stdlibGlobalMember(node.expression, "Object") !== null + lowerer.stdlibGlobalMember(node.expression, "Object") !== null) || + (node.expression.name.text === "deleteProperty" && + lowerer.stdlibGlobalMember(node.expression, "Reflect") !== null)) ) { const target = node.arguments.nodes[0]; if ( diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index ea844ab1e..13d1c29fc 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -42,7 +42,7 @@ import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../packa import type { Lowerer } from "./lowerer.js"; import { boundIdentifiersOf } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; -import { locOf, resolveImport } from "../program.js"; +import { isCjsJsFile, isJsSourceFile, locOf, resolveImport } from "../program.js"; import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js"; import { TEMPORAL_GLOBAL } from "../builtin-modules.js"; @@ -311,6 +311,30 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile return null; } +/** The CommonJS module a JavaScript namespace import binding names + * (`import * as ns from "./m.cjs"`, `ns`), or null. */ +export function cjsNamespaceImportDep(lowerer: Lowerer, e: ts.Expression): ts.SourceFile | null { + if (!ts.isIdentifier(e)) return null; + const decl = lowerer.checker.getSymbolAtLocation(e)?.declarations.find(ts.isNamespaceImport); + const stmt = decl?.parent?.parent; + if ( + decl === undefined || + stmt === undefined || + !ts.isImportDeclaration(stmt) || + !ts.isStringLiteral(stmt.moduleSpecifier) + ) + return null; + const from = ts.getSourceFileOfNode(decl); + if (!isJsSourceFile(from)) return null; + const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text); + return dep !== null && + !dep.isDeclarationFile && + lowerer.fileTag.has(dep) && + isCjsJsFile(dep, lowerer.program) + ? dep + : null; +} + /** The member identifier of a qualified namespace reference, when `access` * reads a member declared in a lowered (or type-only) namespace block — * or an EXPORT of a program module read through its namespace-import @@ -330,6 +354,21 @@ export function nsMemberIdentOf( if (!ts.isIdentifier(access.name)) return null; const memberSym = lowerer.checker.getSymbolAtLocation(access.name); if (!memberSym) return null; + // A CommonJS module's namespace has the names Node's lexer detects, with + // the values they had when the module finished evaluating; the member + // then reads like the named import `import { name }`. `default` is + // module.exports itself. + const cjs = cjsNamespaceImportDep(lowerer, access.expression); + if (cjs !== null) { + const { names, assignedLater } = lowerer.cjsNamespaceOf(cjs); + const name = access.name.text; + return name !== "default" && + names.has(name) && + assignedLater !== "all" && + !assignedLater.has(name) + ? access.name + : null; + } if (nsBlockKindOfSymbol(lowerer, memberSym) !== null) return access.name; if (moduleNsSourceFileOf(lowerer, access.expression) !== null) return access.name; return null; @@ -857,6 +896,15 @@ export function nsWritableTarget( const nsMember = nsMemberIdentOf(lowerer, access); if (!nsMember) return null; const memberSym = lowerer.checker.getSymbolAtLocation(nsMember); + // An export read through a module namespace object (`import * as ns`): + // the object is read-only, and Node's assignment throws a TypeError. + if (memberSym && nsBlockKindOfSymbol(lowerer, memberSym) === null) { + lowerer.unsupported( + "SC1090", + access, + `assignment to the module namespace member '${nsMember.text}' (module namespace objects are read-only: Node throws a TypeError)`, + ); + } if (memberSym) fenceEarlyNsMemberRef(lowerer, access, memberSym); const resolved = lowerer.resolveValueSymbol(nsMember); const g = resolved ? lowerer.globalsBySymbol.get(resolved) : undefined; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c50676863..95e4f7e80 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -168,6 +168,7 @@ import { import { bindingNeverReassigned, deadUnmappableBinding, + fileWritesBinding, nullishExprUnitOf, nullishGenericBindingUnitOf, } from "./binding-analysis.js"; @@ -181,7 +182,12 @@ import { classStaticDataFor } from "./class-static-data.js"; import { objectFactorySignature } from "./object-factory-new.js"; import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js"; import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js"; -import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js"; +import { + genericIfaceBindingKeepsClass, + lowerClassStaticRead, + staticReceiverClass, + superConditionLead, +} from "./lower-classes.js"; import { lowerStreamUnderscoreAssign, streamClassAliasDecl, @@ -3053,6 +3059,18 @@ export function lowerBindingPattern( ); return; } + // A class in a JavaScript file, destructured through its own name + // (`const { ANY, parse } = Comparator`): each element is the static + // read `Comparator.ANY` — the static getter, field or method as Node + // reads it, with the element's default applied when the read answers + // undefined. Other class-valued sources keep the refusal below. + if ( + srcType.kind === "classval" && + ts.isObjectBindingPattern(pattern) && + isJsSourceFile(ts.getSourceFileOfNode(pattern)) && + lowerClassValuePattern(lowerer, pattern, isLet, out) + ) + return; if (srcType.kind === "dyn") { // A runtime computed first key evaluates after RequireObjectCoercible: // a nullish source throws before the key's side effects run. Later @@ -4364,6 +4382,60 @@ function stdlibGlobalTokenDestructure( return out; } +/** `const { ANY, parse } = Comparator` in a JavaScript file, where the + * declaration's initializer names the class exactly (staticReceiverClass): + * one static read per element, left to right. False, with nothing + * emitted, for other sources (a parameter or nested pattern over a class + * value), which keep the class-value refusal. */ +function lowerClassValuePattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + isLet: boolean, + out: IrStmt[], +): boolean { + const decl = pattern.parent; + if (!ts.isVariableDeclaration(decl) || decl.initializer === undefined) return false; + let receiver: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + const info = staticReceiverClass(lowerer, receiver); + if (info === null) return false; + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) + lowerer.unsupported( + "SC1031", + el, + "rest bindings over class values (the rest object copies the class's own enumerable statics)", + ); + const prop = el.propertyName ?? el.name; + const name = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + if (name === null) + lowerer.unsupported( + "SC1031", + el, + "destructuring with computed keys that do not fold to one property name", + ); + if (name === "prototype" || name.startsWith("#")) + lowerer.unsupported("SC1031", el, `destructuring '${name}' from a class value`); + let value = lowerClassStaticRead(lowerer, info, name, receiver, receiver); + if (value === null) + lowerer.unsupported( + "SC1031", + el, + `destructuring the property '${name}' the class '${info.def.jsName ?? info.def.name}' does not declare as a static`, + ); + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } + return true; +} + /** `x = dflt` on a pattern position whose source value always EXISTS * (tuple positions, record fields): JS evaluates the default exactly * when the value is undefined — the undefined-arm ternary, lazy, so @@ -7262,6 +7334,66 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt ); } +/** True when the `var`/`let` binding `nameNode` names cannot change after + * the top-level CommonJS export statement `exportNode` that exports it by + * reference, so importers read the value Node copies at the export: its + * module's top level declares it once, with an initializer that runs + * before the export, and nothing assigns it. The initializer runs earlier + * in the same module, or in a module the exporting module required before + * the export (`var hljs = require('./core'); module.exports = hljs;` over + * highlight.js's `var highlight = HLJS({}); module.exports = highlight;`). */ +function unchangedAfterExport( + lowerer: Lowerer, + nameNode: ts.Identifier, + exportNode: ts.Node, +): boolean { + const topLevelDeclaration = (symbol: ts.Symbol): ts.VariableDeclaration | null => { + const decl = symbol.declarations.length === 1 ? symbol.declarations[0]! : undefined; + return decl !== undefined && + ts.isVariableDeclaration(decl) && + ts.isIdentifier(decl.name) && + decl.initializer !== undefined && + ts.isVariableDeclarationList(decl.parent) && + ts.isVariableStatement(decl.parent.parent) && + ts.isSourceFile(decl.parent.parent.parent) + ? decl + : null; + }; + const exportFile = ts.getSourceFileOfNode(exportNode); + let exportStatement: ts.Node = exportNode; + for (let parent = exportNode.parent; parent !== undefined && !ts.isSourceFile(parent);) { + if (ts.isFunctionLike(parent) || ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) + return false; + exportStatement = parent; + parent = parent.parent; + } + const runsBeforeExport = (decl: ts.VariableDeclaration): boolean => { + const statement = decl.parent?.parent; + return statement?.parent === exportFile && statement.end <= exportStatement.pos; + }; + const sym = lowerer.resolveValueSymbol(nameNode); + const decl = sym ? topLevelDeclaration(sym) : null; + if (!sym || !decl || !bindingNeverReassigned(lowerer, sym, decl)) return false; + if (ts.getSourceFileOfNode(decl) === exportFile) return runsBeforeExport(decl); + // A binding of another module: the exporting module's own binding + // `nameNode` names holds that module's require, evaluated before the + // export, and this file does not assign the binding either. + const local = lowerer.checker.getSymbolAtLocation(nameNode); + const localDecl = local ? topLevelDeclaration(local) : null; + if (!localDecl || !runsBeforeExport(localDecl)) return false; + let init: ts.Expression = localDecl.initializer!; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if ( + !ts.isCallExpression(init) || + !ts.isIdentifier(init.expression) || + init.expression.text !== "require" || + init.arguments.nodes.length !== 1 || + !ts.isStringLiteralLike(init.arguments.nodes[0]!) + ) + return false; + return !fileWritesBinding(sym, exportFile, (ident) => lowerer.resolveValueSymbol(ident)); +} + /** A top-level CommonJS export statement (see cjsExportAssignmentOf): * `exports.f = ` and each expression-valued property of * `module.exports = { ... }` assign their pre-registered export globals @@ -7445,7 +7577,7 @@ function lowerCjsExportStatement( const fenceMutable = (nameNode: ts.Identifier): void => { const sym = lowerer.resolveValueSymbol(nameNode); const g = sym ? lowerer.globalsBySymbol.get(sym) : undefined; - if (g?.mutable) { + if (g?.mutable && !unchangedAfterExport(lowerer, nameNode, cjs.expr)) { lowerer.unsupported( "SC1090", nameNode, @@ -7605,7 +7737,7 @@ function lowerCjsSingleValueExport( } const g = lowerer.globalsBySymbol.get(sym); if (g) { - if (g.mutable) { + if (g.mutable && !unchangedAfterExport(lowerer, rhs, expr)) { lowerer.unsupported( "SC1090", rhs, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 78980d322..59dda3d08 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,6 +1,6 @@ import { isDeclBoundaryFile } from "../decl-projection.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; -import { moduleNamespaceObject } from "./module-namespace-elements.js"; +import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js"; import { keyedHelperName } from "./stable-names.js"; import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; @@ -269,6 +269,8 @@ import { type StartupCrash, cjsExportAssignmentOf, cjsExportDiscardReason, + cjsExportsAssignedInFunctions, + cjsNamespaceExportNames, fallbackDtsPath, isCheckedJsSourceFile, isCjsExportTableLiteral, @@ -3998,6 +4000,31 @@ export class Lowerer { return null; } + /** For each CommonJS module read through a JavaScript namespace import: + * the property names of Node's namespace object + * (cjsNamespaceExportNames), and the exports the module assigns inside + * functions, which the namespace does not see + * (cjsExportsAssignedInFunctions). */ + private readonly cjsNamespaces = new Map< + ts.SourceFile, + { names: ReadonlySet; assignedLater: ReadonlySet | "all" } + >(); + + cjsNamespaceOf(dep: ts.SourceFile): { + names: ReadonlySet; + assignedLater: ReadonlySet | "all"; + } { + let namespace = this.cjsNamespaces.get(dep); + if (namespace === undefined) { + namespace = { + names: cjsNamespaceExportNames(this.program, dep), + assignedLater: cjsExportsAssignedInFunctions(dep), + }; + this.cjsNamespaces.set(dep, namespace); + } + return namespace; + } + sourceFileOfModuleNamespace(type: IrType): ts.SourceFile | null { if (type.kind !== "moduleNs" || !type.moduleId.startsWith("file:")) return null; const path = type.moduleId.slice("file:".length); @@ -9792,10 +9819,8 @@ export class Lowerer { // so its module binding is too (`ReleaseEmitter = EventEmitter` over // `const EventEmitter = require("events")`). if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) { - const object = moduleNamespaceObject( - this, - expr as IrExpr & { type: IrType & { kind: "moduleNs" } }, - ); + const ns = expr as IrExpr & { type: IrType & { kind: "moduleNs" } }; + const object = moduleNamespaceObject(this, ns) ?? moduleNamespaceValue(this, ns); if (object) return object; } if ( diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index cf828c6f0..2c8e6a475 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js"; +import { arrayOf, funcOf } from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import { DYN, @@ -9,7 +10,7 @@ import { type IrType, } from "../../ir/ir.js"; import { locOf } from "../program.js"; -import { newFnCtx, type Lowerer } from "./lowerer.js"; +import { newFnCtx, PoisonError, type Lowerer } from "./lowerer.js"; /** Computed namespace reads select live exports instead of copying a * namespace into a registry snapshot. Constructor registries may contain @@ -171,3 +172,124 @@ export function moduleNamespaceObject( loc, }; } + +/** A module namespace taken as an untyped value whose exports a copy + * cannot represent: a binding that can change after initialization (`let`, + * `var`), or an export the copy cannot name. The value is the runtime's + * namespace object (scr_module_namespace_value): one per module, whose + * property reads call a compiled getter for the export's current value and + * whose writes throw Node's TypeError. An export whose value has no + * checked-dynamic form throws its fence when it is read, and listing the + * object's keys or inspecting it throws the "not supported yet" error. */ +export function moduleNamespaceValue( + lowerer: Lowerer, + ns: IrExpr & { type: IrType & { kind: "moduleNs" } }, +): IrExpr | null { + const source = lowerer.sourceFileOfModuleNamespace(ns.type); + if (!source) return null; + const symbol = lowerer.checker.getSymbolAtLocation(source); + if (!symbol) return null; + const exports: { key: string; declaration: ts.Node | undefined }[] = []; + for (const exported of lowerer.checker.getExportsOfModule(symbol)) { + const target = + exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; + if (!(target.flags & ts.SymbolFlags.Value)) continue; + exports.push({ key: ts.symbolName(exported), declaration: target.valueDeclaration }); + } + exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + const name = `%module.namespace:${ns.type.moduleId}`; + const loc = ns.loc; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const context = newFnCtx(false, null, null, DYN); + const previousClass = lowerer.currentClass; + lowerer.currentClass = null; + lowerer.fnStack.push(context); + try { + const body: IrStmt[] = []; + for (const { key, declaration } of exports) { + const node = declaration ?? source; + const diagsBefore = lowerer.diags.length; + let read: IrStmt[]; + try { + const identifier = declaration?.name as ts.Node | undefined; + if (!identifier || !ts.isIdentifier(identifier)) { + lowerer.unsupported( + "SC1090", + node, + `reading the unnamed export '${key}' through a module namespace object`, + ); + } + const value = lowerer.lowerExprExpecting(identifier, DYN); + if (value.type.kind !== "dyn") { + lowerer.unsupported( + "SC1101", + identifier, + `reading the export '${key}' ('${lowerer.fmt(value.type)}') through a module namespace object`, + ); + } + read = [{ kind: "return", value, loc }]; + } catch (err) { + if (!(err instanceof PoisonError)) throw err; + const fence = lowerer.deferToRuntimeFence(diagsBefore, node, { kind: "statement" }); + if (!fence) throw err; + read = [fence]; + } + body.push({ + kind: "if", + cond: { + kind: "strEq", + negated: false, + left: varRef("key", STRING, loc), + right: strLit(key, loc), + type: { kind: "bool" }, + loc, + }, + then: read, + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "key", name: "key", type: STRING }], + returnType: DYN, + locals: [{ id: "key", name: "key", type: STRING, mutable: false }, ...context.locals], + body, + loc, + }); + } finally { + lowerer.fnStack.pop(); + lowerer.currentClass = previousClass; + } + } + const value: IrExpr = { + kind: "libCall", + fn: "module.namespaceValue", + args: [ + strLit(ns.type.moduleId, loc), + { + kind: "arrayLit", + elems: exports.map(({ key }) => strLit(key, loc)), + type: arrayOf(STRING), + loc, + }, + { + kind: "dynFrom", + value: { kind: "closure", fnName: name, captures: [], type: funcOf([STRING], DYN), loc }, + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }; + if (ns.kind === "moduleNsRef") return value; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: ns, loc }], + result: value, + type: DYN, + loc, + }; +} diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 10beeaae8..06a6c4eca 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -55,6 +55,10 @@ interface ImplementationShim { /** Supported major versions and their entry modules, relative to the * shim root. */ versions: ReadonlyMap; + /** Major versions without an entry module that `--npm-static` compiles + * from the package's own published JavaScript. An installed version that + * is in neither list stays out of the program. */ + sourceVersions?: ReadonlySet; /** The entry has a default export (a CommonJS package's whole export * object, which a default import reads). */ defaultExport?: boolean; @@ -98,6 +102,7 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map< mainFile: "source/index.js", typesFile: "source/index.d.ts", versions: new Map([[5, "chalk/v5.ts"]]), + sourceVersions: new Set([6]), }, ], [ @@ -233,10 +238,17 @@ export function configurePackageShims(entryPath: string): void { unloadedPackages = packages; } -/** True for a package name the registry implements or the project marks - * never loaded: such a package never compiles its published JavaScript. */ -export function isPackageShimName(name: string): boolean { - return IMPLEMENTATION_SHIMS.has(name) || unloadedPackages.has(name); +/** True when `--npm-static` leaves the package `name` out of its package + * list: the project marks it never loaded, or the registry implements it + * and the version installed for `entryPath` has an entry module or is not + * one of the entry's `sourceVersions`. */ +export function npmStaticOmitsPackage(entryPath: string, name: string): boolean { + if (unloadedPackages.has(name)) return true; + const shim = IMPLEMENTATION_SHIMS.get(name); + if (shim === undefined) return false; + const dir = installedPackageDir(entryPath, name); + const major = dir === null ? null : installedMajorVersion(dir, name); + return major === null || shim.versions.has(major) || !shim.sourceVersions?.has(major); } /** True when `specifier` names a package the project marks never loaded. */ @@ -272,9 +284,9 @@ function installedPackageDir(fromFile: string, name: string): string | null { return found; } -/** The shim entry for an installed package directory, or null when the - * directory is not a registered package at a supported major version. */ -function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { +/** The installed package's name and major version from its package.json, + * or null when either is missing. */ +function installedPackageVersion(pkgDir: string): { name: string; major: number } | null { const text = trackedReadFile(join(pkgDir, "package.json")); if (text === null) return null; let pkg: { name?: unknown; version?: unknown }; @@ -284,11 +296,24 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st return null; } if (typeof pkg.name !== "string" || typeof pkg.version !== "string") return null; - if (expectedName !== null && pkg.name !== expectedName) return null; - const shim = IMPLEMENTATION_SHIMS.get(pkg.name); + return { name: pkg.name, major: Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN) }; +} + +/** The major version of the package `name` installed at `pkgDir`, or null. */ +function installedMajorVersion(pkgDir: string, name: string): number | null { + const installed = installedPackageVersion(pkgDir); + return installed === null || installed.name !== name ? null : installed.major; +} + +/** The shim entry for an installed package directory, or null when the + * directory is not a registered package at a supported major version. */ +function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null { + const installed = installedPackageVersion(pkgDir); + if (installed === null) return null; + if (expectedName !== null && installed.name !== expectedName) return null; + const shim = IMPLEMENTATION_SHIMS.get(installed.name); if (shim === undefined) return null; - const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN); - const entry = shim.versions.get(major); + const entry = shim.versions.get(installed.major); if (entry === undefined) return null; return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry); } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index bf75127c4..ce5f3c2bc 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -96,8 +96,8 @@ import type { import { configurePackageShims, isPackageShimFile, - isPackageShimName, isShimmedImport, + npmStaticOmitsPackage, } from "./package-shims.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; @@ -1072,12 +1072,13 @@ export function loadProgram( } externalTypes.set(specifier, declarationPath); } - // Packages scriptc implements itself or the project marks never loaded - // (package-shims.ts) never compile their published JavaScript, even when - // named. + // Packages scriptc implements itself, and packages the project marks + // never loaded (package-shims.ts), never compile their published + // JavaScript, even when named. A registry entry's `sourceVersions` + // (chalk 6) compile from their own source. configurePackageShims(entryPath); const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( - (name) => !isPackageShimName(name), + (name) => !npmStaticOmitsPackage(entryPath, name), ); // Resolve authored declaration entries before enabling the JS-only // package resolver. Safe overload metadata binds through the runtime @@ -5941,9 +5942,15 @@ function preflight7(load: LoadResult): { // to Node's no-op, the module still evaluates through its // ordinary init edge, and no member is ever read through the // lexer facade (self-imports of a CJS module included — the - // partially-built exports are never observed). Any OTHER use - // keeps the fence. - if (!nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name)) { + // partially-built exports are never observed). In JavaScript + // sources every use passes here: member reads lower through the + // names Node's lexer detects, and the other reads refuse at + // their site (lower-namespaces.ts, cjsNamespaceImportDep). Any + // OTHER use in TypeScript keeps the fence. + if ( + !isJsSourceFileName(sf.fileName) && + !nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name) + ) { diags.push( unsupportedDiag( "SC1013", @@ -6372,6 +6379,89 @@ function preflight7(load: LoadResult): { * Node refuses the program). This walk asks Node's question at compile * time, in Node's exact ORDER, and answers with Node's exact message. */ +/** The CommonJS program file a re-export (`module.exports = require(spec)`) + * of `from` resolves to, or null: reexport targets union in only when they + * resolve to CommonJS program files (Node's cjsPreparseModuleExports rule). */ +function cjsReexportDep7(program: ts.Program) { + return (from: ts.SourceFile, spec: string): ts.SourceFile | null => { + const dep = + resolveImport7(program, from, spec, "require") ?? + npmStaticDepSf7(program, from, spec, "require"); + return dep !== null && isCjsJsFile7(dep, program) ? dep : null; + }; +} + +/** Node's lexer analysis of a CommonJS file. A bundler rewrite spells some + * lexer-visible names as accessors. */ +function lexWithAccessorNames(sf: ts.SourceFile): ReturnType { + const lexed = cjsLexedExportsOfFile(sf); + for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); + return lexed; +} + +/** The property names of Node's namespace object for the CommonJS module + * `dep`: the names its lexer detects, CommonJS re-exports included (the + * rule of the named-import link check), plus `default` and + * `module.exports`. */ +export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set { + const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map()); + return new Set([...names, "default", "module.exports"]); +} + +/** The export names a CommonJS module assigns inside functions + * (`exports.current = value` in a function body), or "all" when such code + * writes a computed name, replaces module.exports or redefines exports + * through Object.defineProperty. Node's namespace object of the module + * holds the values the exports had when the module finished evaluating, + * so it does not see those writes. */ +export function cjsExportsAssignedInFunctions(sf: ts.SourceFile): ReadonlySet | "all" { + const names = new Set(); + let all = false; + const exportsObject = (e: ts.Expression): boolean => + (ts.isIdentifier(e) && e.text === "exports") || isModuleExportsAccess(e); + const target = (e: ts.Expression): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (isModuleExportsAccess(e)) all = true; + else if (ts.isPropertyAccessExpression(e) && exportsObject(e.expression)) + names.add(e.name.text); + else if (ts.isElementAccessExpression(e) && exportsObject(e.expression)) { + if (ts.isStringLiteralLike(e.argumentExpression)) names.add(e.argumentExpression.text); + else all = true; + } + }; + const visit = (node: ts.Node, inFunction: boolean): void => { + if (all) return; + const nested = inFunction || ts.isFunctionLike(node); + if (nested) { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + target(node.left); + else if ( + (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) + ) + target(node.operand); + else if (ts.isDeleteExpression(node)) target(node.expression); + else if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === "Object" && + node.arguments.nodes[0] !== undefined && + exportsObject(node.arguments.nodes[0]) + ) + all = true; + } + ts.forEachChild(node, (child) => visit(child, nested)); + }; + visit(sf, false); + return all ? "all" : names; +} + interface BadCjsImport { /** The export name requested from the CJS module. */ exportName: string; @@ -6412,20 +6502,7 @@ function cjsNamedImportLinkCheck( const npmStatic = npmStaticDepSf7(program, from, spec); return npmStatic; }; - // Reexport targets union in only when they resolve to CommonJS program - // files (Node's cjsPreparseModuleExports rule). - const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { - const dep = - resolveImport7(program, from, spec, "require") ?? - npmStaticDepSf7(program, from, spec, "require"); - return dep !== null && isCjsJsFile7(dep, program) ? dep : null; - }; - // A bundler rewrite spells some lexer-visible names as accessors. - const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => { - const lexed = cjsLexedExportsOfFile(sf); - for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); - return lexed; - }; + const resolveCjsDep = cjsReexportDep7(program); const visible = (dep: ts.SourceFile, name: string): boolean => // `default` is the module.exports binding itself — always provided. // The program already owns native source ASTs; reuse them without diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e2d1c97cc..033d98d3d 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -595,6 +595,7 @@ async function compileExecutableNative( ...(features.wasm ? { wasm: true } : {}), ...(features.warningReplace ? { warningReplace: true } : {}), ...(features.stdioExtended ? { stdioExtended: true } : {}), + ...(features.moduleNamespace ? { moduleNamespace: true } : {}), zlib: features.zlib, ...(features.brotli ? { brotli: true } : {}), assert: features.assert, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 390c7fbc3..69b2d8dd0 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -683,6 +683,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fs.callbackCall", "dyn.keyDelete", "dyn.keyDeleteComputed", + "dyn.reflectDeleteProperty", "dyn.hasKeyComputed", "dyn.hasOwnComputed", "dyn.propertyIsEnumerableComputed", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 216e46a4d..072c006e9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -196,6 +196,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * arguments; other receiver representations retain a runtime refusal. */ "dyn.keyDelete": { argTypes: [DYN, STRING, BOOL], result: VOID }, "dyn.keyDeleteComputed": { argTypes: [DYN, DYN, BOOL], result: VOID }, + "dyn.reflectDeleteProperty": { argTypes: [DYN, DYN], result: BOOL }, "dyn.hasKeyComputed": { argTypes: [DYN, DYN], result: BOOL }, "dyn.hasOwnComputed": { argTypes: [DYN, DYN], result: BOOL }, "dyn.propertyIsEnumerableComputed": { argTypes: [DYN, DYN], result: BOOL }, @@ -3425,6 +3426,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "process.builtinId": { argTypes: [DYN, arrayOf(STRING)], result: STRING }, "process.builtinModule": { argTypes: [STRING, DYN], result: DYN }, "process.builtinUnsupported": { argTypes: [STRING, STRING], result: DYN }, + /** A module namespace object as a checked-dynamic value + * (scr_module_namespace_value): the module id, its sorted export names + * and the compiled getter. One object per module. Never throws. */ + "module.namespaceValue": { argTypes: [STRING, arrayOf(STRING), DYN], result: DYN }, "fs.callbackValue": { argTypes: [STRING], result: DYN }, "fs.callbackCall": { argTypes: [STRING, DYN], result: DYN }, "process.hrtimeValue": { argTypes: [], result: DYN }, @@ -3710,6 +3715,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "ffi.argument": { argTypes: [DYN, STRING], result: DYN }, "ffi.memoryModule": { argTypes: [DYN], result: DYN }, "intl.segmenterNew": { argTypes: [], result: DYN }, + "intl.segmenterRefusing": { argTypes: [STRING], result: DYN }, "arrayBuffer.is": { argTypes: [DYN], result: BOOL }, "arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85b21af7b..bddb3f78b 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1124,6 +1124,9 @@ const RUNTIME_FEATURE_RULES = { utilPromisify: rule({ fns: ["util.promisifyDyn"] }), /** process.emitWarning is assigned (process.setEmitWarning). */ warningReplace: rule({ fns: ["process.setEmitWarning"] }), + /** Module namespace objects as checked-dynamic values + * (scr_module_namespace.c). */ + moduleNamespace: rule({ fns: ["module.namespaceValue"] }), /** The WebAssembly API (scr_wasm.c). */ wasm: rule({ prefixes: ["wasm."] }), /** crypto.subtle importKey/sign/verify/digest and CryptoKey: @@ -1188,6 +1191,7 @@ function noRuntimeFeatures(): RuntimeFeatures { searchParams: false, urlIdna: false, stdioExtended: false, + moduleNamespace: false, stream: false, streamDyn: false, handleProps: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 1415f2971..689d67367 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -87,6 +87,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( warningReplace: false, stdioExtended: false, brotli: false, + moduleNamespace: false, regex: true, copying: false, textDecoderLegacy: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json new file mode 100644 index 000000000..6a6a885dc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8214-unshimmed-package-version/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8214-unshimmed-package-version/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json new file mode 100644 index 000000000..d856e307e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8216-module-namespace-live-values/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8216-module-namespace-live-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json new file mode 100644 index 000000000..d2ad44948 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8217-intl-segmenter-word-construct/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8217-intl-segmenter-word-construct/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json new file mode 100644 index 000000000..d627f9153 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8218-cjs-namespace-imports/main.mjs.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8218-cjs-namespace-imports/table.cjs", + "/tests/corpus/8218-cjs-namespace-imports/client/transport.js", + "/tests/corpus/8218-cjs-namespace-imports/client/index.mjs", + "/tests/corpus/8218-cjs-namespace-imports/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json new file mode 100644 index 000000000..71e59739d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8219-cjs-stable-var-exports/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8219-cjs-stable-var-exports/lib/core.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/index.js", + "/tests/corpus/8219-cjs-stable-var-exports/lib/options.js", + "/tests/corpus/8219-cjs-stable-var-exports/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json new file mode 100644 index 000000000..7a2ede696 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8220-js-class-static-destructuring/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js", + "/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js", + "/tests/corpus/8220-js-class-static-destructuring/formdata.js", + "/tests/corpus/8220-js-class-static-destructuring/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json new file mode 100644 index 000000000..003137231 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8221-js-recursive-generator-returns/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8221-js-recursive-generator-returns/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json new file mode 100644 index 000000000..2e961ad08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8222-js-class-static-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8222-js-class-static-reflection.js" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 600637ba6..88348bdc2 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -80,6 +80,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. ["scr_perf_loop.c", "perfLoop"], + // Module namespace objects as checked-dynamic values. + ["scr_module_namespace.c", "moduleNamespace"], // Blob and File box their reads as dyn promises through scr_async_dyn.c. ["scr_blob.c", "dynAsync"], ["scr_zlib.c", "zlib"], diff --git a/packages/runtime/src/scr_blob.h b/packages/runtime/src/scr_blob.h index 15f4a10d4..0882e41b6 100644 --- a/packages/runtime/src/scr_blob.h +++ b/packages/runtime/src/scr_blob.h @@ -1,5 +1,6 @@ -/* scriptc runtime — Blob and File (scr_blob.c). Included by scr_runtime.h, - * which says where new declarations go. */ +/* scriptc runtime — Blob and File (scr_blob.c), and module namespace + * objects as checked-dynamic values (scr_module_namespace.c). Included by + * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_BLOB_H #define SCR_BLOB_H @@ -23,9 +24,16 @@ ScrPromise *scr_blob_array_buffer(ScrDyn *self); bool scr_blob_is(const ScrDyn *value, double kind); ScrDyn *scr_intl_segmenter_new(void); +/* A word or sentence Intl.Segmenter whose segment() throws `refusal` + * (borrowed), the compiler's SC2020 message. */ +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal); ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known); ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter); ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member); +/* A module namespace object as a checked-dynamic value (scr_module_namespace.c): + * one handle per module `id`, whose reads call `getter` with the export + * name when `keys` lists it. Borrows all three; +1 handle. */ +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter); ScrDyn *scr_global_native(ScrArr *known); ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)); ScrDyn *scr_console_native(void); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 82fe1a6d9..cc2506b90 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -186,6 +186,7 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ + X(MODULE_NAMESPACE, 78) /* module namespace object read through its exports (scr_module_namespace.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ @@ -195,6 +196,7 @@ typedef enum { X(REGEXP, 26) /* native RegExp, shared compiled pattern */ \ X(SEARCH_PARAMS, 33) /* live WHATWG URLSearchParams */ \ X(SEGMENTER, 19) /* default Unicode grapheme segmentation */ \ + X(SEGMENTER_REFUSING, 80) /* word or sentence Segmenter whose segment() throws (scr_grapheme.c) */ \ X(SEGMENTS, 20) /* iterable immutable segmentation result */ \ X(SET, 25) /* native Set, shared backing map */ \ X(SHARED_ARRAY_BUFFER, 37) \ @@ -875,6 +877,7 @@ bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key); void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict); +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *key); ScrStr *scr_dyn_string_constructor(const ScrDyn *value); ScrDyn *scr_dyn_property_key_value(const ScrDyn *value); ScrDyn *scr_dyn_to_numeric(const ScrDyn *value); diff --git a/packages/runtime/src/scr_grapheme.c b/packages/runtime/src/scr_grapheme.c index 2c93bc395..f9e5ca28d 100644 --- a/packages/runtime/src/scr_grapheme.c +++ b/packages/runtime/src/scr_grapheme.c @@ -201,3 +201,58 @@ ScrDyn *scr_intl_segmenter_new(void) { scr_dyn_handle_install(SCR_DYNH_SEGMENTS, &segments); return scr_graphemes_box(NULL, SCR_DYNH_SEGMENTER); } + +/* A word or sentence Segmenter, which JavaScript dependencies may build at + * module load and use on some paths only: it constructs, and its segment() + * throws the compiler's SC2020 message. */ +typedef struct { + size_t rc; + ScrStr *refusal; /* retained */ +} ScrRefusingSegmenter; + +static void *scr_refusing_segmenter_retain(void *ptr) { + ScrRefusingSegmenter *value = ptr; + value->rc++; + return value; +} + +static void scr_refusing_segmenter_release(void *ptr) { + ScrRefusingSegmenter *value = ptr; + if (--value->rc) return; + scr_str_release(value->refusal); + free(value); +} + +static ScrDyn *scr_refusing_segmenter_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrRefusingSegmenter *value = ptr; + if (strcmp(method, "segment") == 0) { + ScrStr *input = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!input) return NULL; + scr_str_release(input); + scr_throw_error_msg_code(SCR_ERR_ERROR, value->refusal->data, value->refusal->len, "SC2020"); + return NULL; + } + return scr_graphemes_invoke(&(ScrGraphemes){ .rc = 1 }, self, method, args, argc, what); +} + +static ScrDyn *scr_refusing_segmenter_get(void *ptr, const char *key, size_t len) { + (void)ptr; + return scr_graphemes_get(&(ScrGraphemes){ .rc = 1 }, key, len); +} + +ScrDyn *scr_intl_segmenter_refusing(ScrStr *refusal) { + static const ScrDynHandleOps segmenter = { + .cls = "Segmenter", .retain = scr_refusing_segmenter_retain, + .release = scr_refusing_segmenter_release, .invoke = scr_refusing_segmenter_invoke, + .get = scr_refusing_segmenter_get, .set = scr_graphemes_set, + }; + scr_dyn_handle_install(SCR_DYNH_SEGMENTER_REFUSING, &segmenter); + ScrRefusingSegmenter *value = calloc(1, sizeof *value); + if (!value) scr_trap("scriptc: out of memory\n"); + value->rc = 1; + value->refusal = scr_str_retain(refusal); + ScrDyn *boxed = scr_dyn_new_handle(value, SCR_DYNH_SEGMENTER_REFUSING); + scr_refusing_segmenter_release(value); + return boxed; +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 49af072ed..4eb53bbec 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8017,6 +8017,45 @@ void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool stri scr_dyn_release(key); } +/* Reflect.deleteProperty(target, key): a TypeError for a primitive target, + * then the key's ToPropertyKey and a sloppy-mode delete. Answers whether + * the property is absent afterwards (false when it is non-configurable). + * A Proxy would observe the own-property check through its traps, and a + * compiled object's native fields keep their values after a delete through + * its checked-dynamic view, so both refuse. */ +bool scr_dyn_reflect_delete_property(ScrDyn *target, const ScrDyn *raw_key) { + switch (target->kind) { + case SCR_DYN_NULL: + case SCR_DYN_UNDEF: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: { + static const char message[] = "Reflect.deleteProperty called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return false; + } + default: + break; + } + if (target->kind == SCR_DYN_PROXY) { + scr_dyn_proxy_unsupported("Reflect.deleteProperty"); + return false; + } + if (target->kind == SCR_DYN_TYPED_REF) { + static const char message[] = "Reflect.deleteProperty on a compiled object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return false; + } + ScrDyn *key = scr_dyn_property_key_value(raw_key); + if (!key) return false; + scr_dyn_key_delete_computed(target, key, false); + bool deleted = !scr_exc_pending() && !scr_dyn_has_own_computed(target, key); + scr_dyn_release(key); + return deleted; +} + /* Native call/apply/bind lowering still owns these prototype operations. * Refuse overrides until every typed call site observes the property table. */ static bool scr_dyn_fn_property_fence(const ScrStr *key) { diff --git a/packages/runtime/src/scr_module_namespace.c b/packages/runtime/src/scr_module_namespace.c new file mode 100644 index 000000000..b0d7e80c4 --- /dev/null +++ b/packages/runtime/src/scr_module_namespace.c @@ -0,0 +1,121 @@ +/* Module namespace objects as checked-dynamic values + * (module-namespace-elements.ts lowers `import * as ns` values here). */ +#include "scr_runtime.h" + +#include +#include + +/* A module namespace object taken as a checked-dynamic value + * (module-namespace-elements.ts). The compiled getter answers an export's + * current value by name, so reads see live bindings; `keys` lists the + * namespace's export names. One object per module keeps its identity. + * Writes throw Node's strict-mode TypeError for a frozen namespace. */ +typedef struct ScrModuleNamespace { + size_t rc; + ScrStr *id; + ScrDyn *getter; + ScrArr *keys; + struct ScrModuleNamespace *next; +} ScrModuleNamespace; + +static SCR_TL ScrModuleNamespace *scr_module_namespaces; +static SCR_TL bool scr_module_namespace_cleanup_registered; + +static void *scr_module_namespace_retain(void *handle) { + ScrModuleNamespace *ns = handle; + ns->rc++; + return ns; +} + +static void scr_module_namespace_release(void *handle) { + ScrModuleNamespace *ns = handle; + if (--ns->rc) return; + scr_str_release(ns->id); + scr_dyn_release(ns->getter); + scr_arr_release(ns->keys); + free(ns); +} + +static void scr_module_namespace_cleanup(void) { + while (scr_module_namespaces) { + ScrModuleNamespace *ns = scr_module_namespaces; + scr_module_namespaces = ns->next; + scr_module_namespace_release(ns); + } +} + +static bool scr_module_namespace_has(const ScrModuleNamespace *ns, const char *key, size_t length) { + for (size_t i = 0; i < ns->keys->len; i++) { + ScrStr *entry = (ScrStr *)scr_arr_get_ref(ns->keys, (double)i); + bool same = entry->len == length && memcmp(entry->data, key, length) == 0; + scr_str_release(entry); + if (same) return true; + } + return false; +} + +static ScrDyn *scr_module_namespace_get(void *handle, const char *key, size_t length) { + ScrModuleNamespace *ns = handle; + if (!scr_module_namespace_has(ns, key, length)) return NULL; + ScrStr *name = scr_str_new(key, length); + ScrDyn *arg = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *result = scr_dyn_call(ns->getter, &arg, 1, "module namespace export"); + scr_dyn_release(arg); + return result; +} + +static bool scr_module_namespace_set(void *handle, const char *key, size_t length, const ScrDyn *value) { + (void)value; + ScrModuleNamespace *ns = handle; + ScrJsonBuf text; + scr_jb_init(&text); + if (scr_module_namespace_has(ns, key, length)) { + scr_jb_puts(&text, "Cannot assign to read only property '"); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, "' of object '[object Module]'"); + } else { + scr_jb_puts(&text, "Cannot add property "); + scr_jb_write(&text, key, length); + scr_jb_puts(&text, ", object is not extensible"); + } + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len); + scr_str_release(message); + return true; +} + +static ScrDyn *scr_module_namespace_invoke(void *handle, ScrDyn *self, const char *key, + ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *fn = scr_module_namespace_get(handle, key, strlen(key)); + if (fn == NULL) { + if (scr_exc_pending()) return NULL; + fn = scr_dyn_retain(scr_dyn_undefined()); + } + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +ScrDyn *scr_module_namespace_value(ScrStr *id, ScrArr *keys, ScrDyn *getter) { + static const ScrDynHandleOps ops = { + .cls = "Module", .retain = scr_module_namespace_retain, .release = scr_module_namespace_release, + .get = scr_module_namespace_get, .set = scr_module_namespace_set, .invoke = scr_module_namespace_invoke, + }; + scr_dyn_handle_install(SCR_DYNH_MODULE_NAMESPACE, &ops); + for (ScrModuleNamespace *ns = scr_module_namespaces; ns; ns = ns->next) { + if (ns->id->len == id->len && memcmp(ns->id->data, id->data, id->len) == 0) + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); + } + if (!scr_module_namespace_cleanup_registered) { + scr_atexit(scr_module_namespace_cleanup); + scr_module_namespace_cleanup_registered = true; + } + ScrModuleNamespace *ns = malloc(sizeof(*ns)); + if (!ns) scr_trap("scriptc: out of memory\n"); + *ns = (ScrModuleNamespace){ 1, scr_str_retain(id), scr_dyn_retain(getter), scr_arr_retain(keys), scr_module_namespaces }; + scr_module_namespaces = ns; + return scr_dyn_new_handle(ns, SCR_DYNH_MODULE_NAMESPACE); +} diff --git a/tests/corpus/8214-unshimmed-package-version/main.ts b/tests/corpus/8214-unshimmed-package-version/main.ts new file mode 100644 index 000000000..d582a339b --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/main.ts @@ -0,0 +1,5 @@ +// A package that scriptc implements at some major versions (chalk 5) +// compiles from its own source at a version without an implementation. +import chalk, { level } from "chalk"; + +console.log(chalk.version, level, chalk.red("red"), chalk.bold("bold")); diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json new file mode 100644 index 000000000..7ab814eb0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/package.json @@ -0,0 +1 @@ +{ "name": "chalk", "version": "6.0.0", "type": "module", "exports": { "types": "./source/index.d.ts", "default": "./source/index.js" }, "imports": { "#styles": "./source/styles.js" } } diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts new file mode 100644 index 000000000..84dd75758 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.d.ts @@ -0,0 +1,3 @@ +export declare const level: number; +declare const chalk: { version: number; red(text: string): string; bold(text: string): string }; +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js new file mode 100644 index 000000000..2d85a4ae3 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/index.js @@ -0,0 +1,16 @@ +import { styles } from "#styles"; + +export const level = 0; + +function paint(name, text) { + const [open, close] = styles[name]; + return level > 0 ? `\u001B[${open}m${text}\u001B[${close}m` : `${text}`; +} + +const chalk = { + version: 6, + red: (text) => paint("red", text), + bold: (text) => paint("bold", text), +}; + +export default chalk; diff --git a/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js new file mode 100644 index 000000000..03e60b5e0 --- /dev/null +++ b/tests/corpus/8214-unshimmed-package-version/node_modules/chalk/source/styles.js @@ -0,0 +1 @@ +export const styles = { red: [31, 39], bold: [1, 22] }; diff --git a/tests/corpus/8216-module-namespace-live-values/main.ts b/tests/corpus/8216-module-namespace-live-values/main.ts new file mode 100644 index 000000000..419ce772e --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/main.ts @@ -0,0 +1,11 @@ +// A module namespace object stored as an untyped value, here in a registry +// keyed by module name (pi-coding-agent's VIRTUAL_MODULES): reads see the +// exports' current values, both registry entries are the same object, and +// writes throw Node's TypeError. +import { addThrough, bumpThrough, describe, same, writeThrough } from "ns-registry"; + +console.log(describe()); +bumpThrough(); +bumpThrough(); +console.log(describe()); +console.log(same(), writeThrough(), addThrough()); diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js new file mode 100644 index 000000000..b971201d8 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/counter.js @@ -0,0 +1,7 @@ +export let count = 0; +export var legacy = "legacy"; +export const label = "counter"; +export function bump() { + count++; + legacy = `legacy ${count}`; +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts new file mode 100644 index 000000000..451d646f7 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.d.ts @@ -0,0 +1,5 @@ +export declare function describe(): string; +export declare function bumpThrough(): void; +export declare function same(): boolean; +export declare function writeThrough(): string; +export declare function addThrough(): string; diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js new file mode 100644 index 000000000..51558bcba --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/index.js @@ -0,0 +1,40 @@ +import * as counter from "./counter.js"; + +export const registry = { + counter: counter, + "alias/counter": counter, +}; + +export function describe() { + const ns = registry["counter"]; + return `${ns.count} ${ns.legacy} ${ns.label} ${typeof ns.bump} ${typeof ns}`; +} + +export function bumpThrough() { + registry.counter.bump(); +} + +export function same() { + return registry.counter === registry["alias/counter"]; +} + +function assign(target, key, value) { + try { + target[key] = value; + } catch (error) { + return `${error.name}: ${error.message}`; + } + return "no error"; +} + +function lookup(name) { + return registry[name]; +} + +export function writeThrough() { + return assign(lookup("counter"), "count", 5); +} + +export function addThrough() { + return assign(lookup("alias/counter"), "extra", 5); +} diff --git a/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json new file mode 100644 index 000000000..30fae6921 --- /dev/null +++ b/tests/corpus/8216-module-namespace-live-values/node_modules/ns-registry/package.json @@ -0,0 +1 @@ +{ "name": "ns-registry", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8217-intl-segmenter-word-construct/main.ts b/tests/corpus/8217-intl-segmenter-word-construct/main.ts new file mode 100644 index 000000000..5a2a42e08 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/main.ts @@ -0,0 +1,8 @@ +// A package builds its segmenters at module load (pi-tui, grok-mermaid): +// a grapheme segmenter with a locale tag, and a word segmenter that only +// some paths use. Grapheme segmentation matches Node; the word segmenter +// constructs, and this program never segments with it. +import { columns, describe } from "text-width"; + +console.log(describe()); +console.log(columns("éa👍🏽🇫🇷"), columns("abc")); diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts new file mode 100644 index 000000000..471b4fd0a --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts @@ -0,0 +1,2 @@ +export declare function columns(text: string): string; +export declare function describe(): string; diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js new file mode 100644 index 000000000..d58b4ccbb --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js @@ -0,0 +1,11 @@ +const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" }); +const words = new Intl.Segmenter(undefined, { granularity: "word" }); +const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" }); + +export function columns(text) { + return [...graphemes.segment(text)].map((part) => part.segment).join("|"); +} + +export function describe() { + return `${typeof words} ${typeof sentences}`; +} diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json new file mode 100644 index 000000000..4f3af53f4 --- /dev/null +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/package.json @@ -0,0 +1 @@ +{ "name": "text-width", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/index.mjs b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs new file mode 100644 index 000000000..f0d49451f --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/index.mjs @@ -0,0 +1,12 @@ +import * as transport from './transport.js' + +export function connect (origin) { + const pool = new transport.Pool(origin, transport.defaults) + console.log(transport.setGlobalDispatcher(pool), pool.connections, transport.defaults.keepAlive) + return pool +} + +export function describe (pool) { + const format = transport.format + return format(pool.request('/status')) +} diff --git a/tests/corpus/8218-cjs-namespace-imports/client/package.json b/tests/corpus/8218-cjs-namespace-imports/client/package.json new file mode 100644 index 000000000..c866a30a1 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/package.json @@ -0,0 +1 @@ +{ "name": "ns-client", "version": "1.0.0", "main": "index.mjs" } diff --git a/tests/corpus/8218-cjs-namespace-imports/client/transport.js b/tests/corpus/8218-cjs-namespace-imports/client/transport.js new file mode 100644 index 000000000..8d9efbe36 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/client/transport.js @@ -0,0 +1,26 @@ +'use strict' +// A CommonJS module of the package: member exports, a class, and a table +// whose getter stops Node's lexer before the names after it. +class Pool { + constructor (origin, options) { + this.origin = origin + this.connections = options.connections + this.requests = 0 + } + + request (path) { + this.requests += 1 + return `${this.origin}${path} #${this.requests}` + } +} + +function setGlobalDispatcher (dispatcher) { + exports.current = dispatcher + return dispatcher.origin +} + +exports.Pool = Pool +exports.setGlobalDispatcher = setGlobalDispatcher +exports.current = null +exports.defaults = { connections: 4, keepAlive: true } +exports.format = function format (value) { return `[${value}]` } diff --git a/tests/corpus/8218-cjs-namespace-imports/main.mjs b/tests/corpus/8218-cjs-namespace-imports/main.mjs new file mode 100644 index 000000000..7d2d0e862 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/main.mjs @@ -0,0 +1,13 @@ +// Namespace imports of CommonJS modules in JavaScript: member reads, calls, +// constructions and a method read as a value, through a package's ES +// module and directly. Each name is one Node's lexer detects. +import * as table from './table.cjs' +import { connect, describe } from './client/index.mjs' + +console.log(table.add(20, 22), table.scale) +const add = table.add +console.log(add(1, 2)) + +const pool = connect('http://localhost') +console.log(describe(pool)) +console.log(describe(pool)) diff --git a/tests/corpus/8218-cjs-namespace-imports/table.cjs b/tests/corpus/8218-cjs-namespace-imports/table.cjs new file mode 100644 index 000000000..38696c6f9 --- /dev/null +++ b/tests/corpus/8218-cjs-namespace-imports/table.cjs @@ -0,0 +1,10 @@ +'use strict' +const add = (a, b) => a + b +const scale = 10 +let calls = 0 +module.exports = { + add, + scale, + get calls () { return calls }, + bump () { calls += 1 } +} diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/core.js b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js new file mode 100644 index 000000000..1210659ac --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/core.js @@ -0,0 +1,31 @@ +// highlight.js's lib/core.js shape: the module builds one instance and +// exports the var that holds it. +'use strict'; + +const HLJS = function (options) { + const languages = new Map(); + const prefix = options.classPrefix || 'hljs-'; + + function registerLanguage(name, keywords) { + languages.set(name, keywords); + } + + function listLanguages() { + return Array.from(languages.keys()); + } + + function highlight(code, name) { + const keywords = languages.get(name); + if (keywords === undefined) throw new Error(`Unknown language: "${name}"`); + return code + .split(' ') + .map((word) => (keywords.includes(word) ? `${word}` : word)) + .join(' '); + } + + return { registerLanguage, listLanguages, highlight, versionString: '10.7.3' }; +}; + +var highlight = HLJS({}); + +module.exports = highlight; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/index.js b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js new file mode 100644 index 000000000..5f7726ea2 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/index.js @@ -0,0 +1,8 @@ +// highlight.js's lib/index.js shape: require the core instance, register +// languages on it and export the same var. +var hljs = require('./core'); + +hljs.registerLanguage('javascript', ['const', 'let', 'function', 'return']); +hljs.registerLanguage('python', ['def', 'return', 'lambda']); + +module.exports = hljs; diff --git a/tests/corpus/8219-cjs-stable-var-exports/lib/options.js b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js new file mode 100644 index 000000000..17512396f --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/lib/options.js @@ -0,0 +1,12 @@ +// A let that is never reassigned, exported in a table. +'use strict'; + +let tabReplace = ' '; +let calls = []; + +function render(text) { + calls.push(text); + return text.split('\t').join(tabReplace); +} + +module.exports = { tabReplace, calls, render }; diff --git a/tests/corpus/8219-cjs-stable-var-exports/main.js b/tests/corpus/8219-cjs-stable-var-exports/main.js new file mode 100644 index 000000000..8607165a6 --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/main.js @@ -0,0 +1,21 @@ +// A var and a let exported by reference, where neither changes after the +// export: importers see the same instance and value as Node. +'use strict'; + +const hljs = require('./lib'); +const core = require('./lib/core'); +const options = require('./lib/options'); + +console.log(hljs === core, hljs.versionString); +console.log(hljs.listLanguages()); +console.log(hljs.highlight('const x = function', 'javascript')); +hljs.registerLanguage('ruby', ['def', 'end']); +console.log(core.listLanguages().length, core.highlight('def run end', 'ruby')); +try { + hljs.highlight('x', 'cobol'); +} catch (error) { + console.log(error.message); +} + +console.log(JSON.stringify(options.tabReplace), options.render('a\tb'), options.render('\tc')); +console.log(options.calls); diff --git a/tests/corpus/8219-cjs-stable-var-exports/package.json b/tests/corpus/8219-cjs-stable-var-exports/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8219-cjs-stable-var-exports/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js new file mode 100644 index 000000000..3b128847c --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/classes/comparator.js @@ -0,0 +1,22 @@ +'use strict' +// semver's Comparator: a static getter returning a module symbol. +const ANY = Symbol('SemVer ANY') + +class Comparator { + static get ANY () { + return ANY + } + + static loose = false + + static parse (text) { + return new Comparator(text) + } + + constructor (text) { + this.semver = text === '' ? ANY : text + this.value = text === '' ? '' : `>=${text}` + } +} + +module.exports = Comparator diff --git a/tests/corpus/8220-js-class-static-destructuring/formdata.js b/tests/corpus/8220-js-class-static-destructuring/formdata.js new file mode 100644 index 000000000..851d3ba28 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/formdata.js @@ -0,0 +1,26 @@ +'use strict' +// undici's FormData: statics that read private instance state, taken off +// the class before the class is exposed. +class FormData { + #state = [] + + append (name, value) { + this.#state.push({ name, value }) + } + + static getFormDataState (formData) { + return formData.#state + } + + static setFormDataState (formData, state) { + formData.#state = state + } + + static get boundary () { + return undefined + } +} + +const { getFormDataState, setFormDataState, boundary = '----formdata-default' } = FormData + +module.exports = { FormData, getFormDataState, setFormDataState, boundary } diff --git a/tests/corpus/8220-js-class-static-destructuring/main.js b/tests/corpus/8220-js-class-static-destructuring/main.js new file mode 100644 index 000000000..6cc17b94e --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/main.js @@ -0,0 +1,39 @@ +'use strict' +// Object patterns over a class value read its static getters, fields and +// methods as Node does, defaults included. +const Comparator = require('./classes/comparator') +const { outside, ANY, any, isLoose, name } = require('./ranges/outside') +const { FormData, getFormDataState, setFormDataState, boundary } = require('./formdata') + +console.log(typeof ANY, ANY === Comparator.ANY, any === ANY, isLoose, name) +console.log(outside(''), outside('1.2.3')) +console.log(Comparator.parse('').semver === ANY, Comparator.parse('2.0.0').semver === ANY) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form), boundary) + +class Defaults { + static count = 0 + static missing = undefined + static nested = { inner: 'value' } +} +const { count = 5, missing = 'fallback', nested: { inner, other = 'other' } } = Defaults +console.log(count, missing, inner, other) + +// Inherited statics, and a static getter reading the class it is read +// through. +class Base { + static tag = 'base' + static get label () { return `label:${this.tag}` } + static make () { return this.tag } +} +class Sub extends Base { + static tag = 'sub' +} +const { tag, label, make, name: subName } = Sub +const { label: baseLabel } = Base +console.log(tag, label, typeof make, subName, baseLabel) diff --git a/tests/corpus/8220-js-class-static-destructuring/package.json b/tests/corpus/8220-js-class-static-destructuring/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js new file mode 100644 index 000000000..a8ffe06f2 --- /dev/null +++ b/tests/corpus/8220-js-class-static-destructuring/ranges/outside.js @@ -0,0 +1,13 @@ +'use strict' +// semver's ranges/outside.js and ranges/subset.js destructure the class's +// statics at module load. +const Comparator = require('../classes/comparator') +const { ANY } = Comparator +const { ANY: any, loose: isLoose = true, parse, name } = Comparator + +const outside = (text) => { + const comparator = parse(text) + return comparator.semver === ANY ? 'any' : comparator.value +} + +module.exports = { outside, ANY, any, isLoose, name } diff --git a/tests/corpus/8221-js-recursive-generator-returns/main.ts b/tests/corpus/8221-js-recursive-generator-returns/main.ts new file mode 100644 index 000000000..72b9a7b10 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/main.ts @@ -0,0 +1,10 @@ +// A JavaScript package's parser of mutually recursive generator methods that +// return `yield*` of each other (yaml 2.9.0's parse/parser.js): the checker +// cannot infer their results. Tokens, values passed to next() and return +// values reach each generator in the same order as in Node. +import { parseTokens, drive, spreadTypes, walkAll } from "token-parser"; + +for (const token of parseTokens("a { b [ c d ] e } f")) console.log(token); +console.log(drive("{ x } y ! z")); +console.log(spreadTypes("[ 1 2 3 ]")); +console.log(await walkAll("a ( b c ) d ( e )")); diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts new file mode 100644 index 000000000..763a34ae4 --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.d.ts @@ -0,0 +1,4 @@ +export declare function parseTokens(source: string): string[]; +export declare function drive(source: string): string; +export declare function spreadTypes(source: string): string; +export declare function walkAll(source: string): Promise; diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js new file mode 100644 index 000000000..aab64a1ac --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/dist/index.js @@ -0,0 +1,131 @@ +'use strict' + +class Parser { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + this.stack = [] + } + + * parse () { + while (this.index < this.tokens.length) { + const done = yield * this.step() + if (done) return 'stopped' + } + return yield * this.end() + } + + * step () { + const token = this.tokens[this.index++] + if (token === '{') return yield * this.blockMap(token) + if (token === '[') return yield * this.flowSeq(token) + if (token === '!') return true + return yield * this.scalar(token) + } + + * blockMap (open) { + this.stack.push(open) + yield { type: 'map-start', depth: this.stack.length } + while (this.index < this.tokens.length && this.tokens[this.index] !== '}') { + const stop = yield * this.step() + if (stop) return stop + } + this.index++ + this.stack.pop() + const reply = yield { type: 'map-end', depth: this.stack.length + 1 } + if (reply !== undefined) yield { type: 'reply', value: reply } + return false + } + + * flowSeq (open) { + this.stack.push(open) + let count = 0 + while (this.index < this.tokens.length && this.tokens[this.index] !== ']') { + yield * this.step() + count++ + } + this.index++ + this.stack.pop() + yield { type: 'seq', count } + return false + } + + * scalar (token) { + const value = yield { type: 'scalar', source: token } + if (value !== undefined) return yield * this.scalar(String(value)) + return false + } + + * end () { + yield { type: 'end', depth: this.stack.length } + return this.index + } +} + +function parseTokens (source) { + const tokens = [] + for (const token of new Parser(source).parse()) tokens.push(JSON.stringify(token)) + return tokens +} + +// next(value) reaches the innermost delegated generator, and return values +// pass back through each yield*. +function drive (source) { + const parser = new Parser(source).parse() + const log = [] + let result = parser.next() + while (!result.done) { + const value = result.value + log.push(JSON.stringify(value)) + result = parser.next(value.type === 'map-end' ? 'ack' : value.source === 'x' ? 'x2' : undefined) + } + log.push('return ' + result.value) + return log.join('\n') +} + +function spreadTypes (source) { + const tokens = [...new Parser(source).parse()] + return tokens.length + ' ' + tokens.map((token) => token.type).join(',') +} + +// Async generator methods with the same mutual recursion. +class AsyncWalker { + constructor (source) { + this.tokens = source.split(' ') + this.index = 0 + } + + async * walk () { + while (this.index < this.tokens.length) yield * this.next() + return this.index + } + + async * next () { + const token = this.tokens[this.index++] + await null + if (token === '(') return yield * this.group() + yield token + return token + } + + async * group () { + const inner = [] + while (this.tokens[this.index] !== ')') inner.push(yield * this.next()) + this.index++ + yield 'group:' + inner.join('+') + return inner.length + } +} + +async function walkAll (source) { + const seen = [] + const walker = new AsyncWalker(source).walk() + let result = await walker.next() + while (!result.done) { + seen.push(result.value) + result = await walker.next() + } + return seen.join(' ') + ' -> ' + result.value +} + +module.exports = { parseTokens, drive, spreadTypes, walkAll } diff --git a/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json new file mode 100644 index 000000000..1fa77c2eb --- /dev/null +++ b/tests/corpus/8221-js-recursive-generator-returns/node_modules/token-parser/package.json @@ -0,0 +1 @@ +{ "name": "token-parser", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts" } diff --git a/tests/corpus/8222-js-class-static-reflection.js b/tests/corpus/8222-js-class-static-reflection.js new file mode 100644 index 000000000..74027cc64 --- /dev/null +++ b/tests/corpus/8222-js-class-static-reflection.js @@ -0,0 +1,94 @@ +'use strict' +// A class's statics extracted, then removed with Reflect.deleteProperty, and +// redefined with Object.defineProperties (undici's FormData and Response). + +const kEnumerableProperty = { __proto__: null, enumerable: true } + +class FormData { + #state = [] + append (name, value) { this.#state.push({ name, value }) } + static getFormDataState (formData) { return formData.#state } + static setFormDataState (formData, state) { formData.#state = state } +} + +const getFormDataState = FormData.getFormDataState +const setFormDataState = FormData.setFormDataState +console.log(Reflect.deleteProperty(FormData, 'getFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'setFormDataState')) +console.log(Reflect.deleteProperty(FormData, 'missing')) +console.log(typeof FormData.getFormDataState, typeof FormData.setFormDataState) + +const form = new FormData() +form.append('a', 1) +form.append('b', 'two') +console.log(getFormDataState(form)) +setFormDataState(form, [{ name: 'c', value: true }]) +console.log(getFormDataState(form).length, getFormDataState(form)[0].name) +try { + FormData.getFormDataState(form) +} catch (error) { + console.log(error.constructor.name, error.message) +} + +class Response { + #status + constructor (status) { this.#status = status } + get status () { return this.#status } + static json () { return new Response(200) } + static error () { return new Response(0) } + static getResponseState (response) { return response.#status } +} + +const getResponseState = Response.getResponseState +Reflect.deleteProperty(Response, 'getResponseState') +Object.defineProperties(Response, { + json: kEnumerableProperty, + error: kEnumerableProperty +}) +const response = Response.json() +console.log(response.status, Response.error().status, getResponseState(response)) +for (const key of ['json', 'error', 'getResponseState']) { + const descriptor = Object.getOwnPropertyDescriptor(Response, key) + console.log(key, descriptor === undefined ? 'absent' : [descriptor.enumerable, descriptor.writable, descriptor.configurable, typeof descriptor.value]) +} + +// Statics a subclass inherits, a static field and a static accessor. +class Base { + static helper () { return 'helper' } + static count = 3 + static get label () { return 'label' } +} +class Child extends Base {} +console.log(Child.helper(), Child.count, Child.label) +console.log(Reflect.deleteProperty(Base, 'helper'), Reflect.deleteProperty(Base, 'count')) +console.log(typeof Child.helper, Child.count, typeof Base.helper, Base.count) +console.log(Reflect.deleteProperty(Base, 'label'), Base.label, Child.label) +Base.helper = () => 'replaced' +console.log(Base.helper(), Child.helper()) +Base.count = 4 +const countKey = { toString () { return 'count' } } +console.log(Reflect.deleteProperty(Base, countKey), Base.count) +class Key { + constructor (name) { this.name = name } + toString () { return this.name } +} +Base.count = 5 +console.log(Reflect.deleteProperty(Base, new Key('count')), Base.count) + +// Checked-dynamic objects and primitive targets. +const parsed = JSON.parse('{"a":1,"b":2}') +console.log(Reflect.deleteProperty(parsed, 'a'), parsed) +const frozen = Object.freeze(JSON.parse('{"a":1}')) +console.log(Reflect.deleteProperty(frozen, 'a'), frozen) +const attempt = (run) => { + try { + run() + } catch (error) { + console.log(error.constructor.name, error.message) + } +} +attempt(() => Reflect.deleteProperty(1, 'a')) +attempt(() => Reflect.deleteProperty('text', 'a')) +attempt(() => Reflect.deleteProperty(null, 'a')) +attempt(() => Reflect.deleteProperty(undefined, 'a')) +attempt(() => Reflect.deleteProperty(true, 'a')) diff --git a/tests/coverage-fixtures/cjs-mutable-exports/main.js b/tests/coverage-fixtures/cjs-mutable-exports/main.js new file mode 100644 index 000000000..ac16cc4ec --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/main.js @@ -0,0 +1,5 @@ +const reassigned = require('./reassigned'); +const written = require('./written'); +const redeclared = require('./redeclared'); +const stable = require('./stable'); +console.log(reassigned.n, written.tab, redeclared.twice, stable.n); diff --git a/tests/coverage-fixtures/cjs-mutable-exports/package.json b/tests/coverage-fixtures/cjs-mutable-exports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js new file mode 100644 index 000000000..6c77f3b43 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/reassigned.js @@ -0,0 +1,4 @@ +// Reassigned after the export: Node's importers keep the first object. +var counter = { n: 1 }; +module.exports = counter; +counter = { n: 2 }; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js new file mode 100644 index 000000000..3150903f6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/redeclared.js @@ -0,0 +1,4 @@ +// Declared twice: the second declaration assigns the binding again. +var twice = 1; +module.exports = { twice }; +var twice = 2; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/stable.js b/tests/coverage-fixtures/cjs-mutable-exports/stable.js new file mode 100644 index 000000000..c2846bbc9 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/stable.js @@ -0,0 +1,3 @@ +// Initialized before the export and never assigned. +var stable = { n: 3 }; +module.exports = stable; diff --git a/tests/coverage-fixtures/cjs-mutable-exports/written.js b/tests/coverage-fixtures/cjs-mutable-exports/written.js new file mode 100644 index 000000000..8e0c29cd2 --- /dev/null +++ b/tests/coverage-fixtures/cjs-mutable-exports/written.js @@ -0,0 +1,4 @@ +// Written by a function: the table holds the value at the export. +let tab = ' '; +function setTab(next) { tab = next; } +module.exports = { tab, setTab }; diff --git a/tests/diagnostics/intl-segmenter.ts b/tests/diagnostics/intl-segmenter.ts index 42bdae846..0cf30eabb 100644 --- a/tests/diagnostics/intl-segmenter.ts +++ b/tests/diagnostics/intl-segmenter.ts @@ -1,5 +1,5 @@ -// The native implementation covers default Unicode grapheme segmentation. -new Intl.Segmenter("en-US"); +// The native implementation covers Unicode grapheme segmentation. +new Intl.Segmenter(["en-US", "de"]); new Intl.Segmenter(undefined, { granularity: "word" }); new Intl.Segmenter(undefined, { granularity: "sentence" }); // End of the diagnostic fixture. diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts new file mode 100644 index 000000000..2fd45310e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.d.ts @@ -0,0 +1 @@ +export declare function splitWords(text: string): string; diff --git a/tests/fixtures/npm-static/node_modules/word-segments/index.js b/tests/fixtures/npm-static/node_modules/word-segments/index.js new file mode 100644 index 000000000..a67aa5381 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/index.js @@ -0,0 +1,9 @@ +const words = new Intl.Segmenter(undefined, { granularity: "word" }); + +export function splitWords(text) { + try { + return [...words.segment(text)].map((part) => part.segment).join("|"); + } catch (error) { + return `${error.code}: ${error.message}`; + } +} diff --git a/tests/fixtures/npm-static/node_modules/word-segments/package.json b/tests/fixtures/npm-static/node_modules/word-segments/package.json new file mode 100644 index 000000000..37b0d6f73 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/word-segments/package.json @@ -0,0 +1 @@ +{ "name": "word-segments", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm-static/word-segments-cli.ts b/tests/fixtures/npm-static/word-segments-cli.ts new file mode 100644 index 000000000..28e3bd5e1 --- /dev/null +++ b/tests/fixtures/npm-static/word-segments-cli.ts @@ -0,0 +1,3 @@ +import { splitWords } from "word-segments"; + +console.log(splitWords("two words")); diff --git a/tests/harness/__snapshots__/intl-segmenter.ts.txt b/tests/harness/__snapshots__/intl-segmenter.ts.txt index e50205eec..19a13ef4f 100644 --- a/tests/harness/__snapshots__/intl-segmenter.ts.txt +++ b/tests/harness/__snapshots__/intl-segmenter.ts.txt @@ -1,15 +1,15 @@ intl-segmenter.ts:2:1 - error SC2020: 'Intl.Segmenter locale negotiation' is part of the standard library types but has no scriptc lowering yet - 1 | // The native implementation covers default Unicode grapheme segmentation. - 2 | new Intl.Segmenter("en-US"); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 | // The native implementation covers Unicode grapheme segmentation. + 2 | new Intl.Segmenter(["en-US", "de"]); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | new Intl.Segmenter(undefined, { granularity: "word" }); - hint: omit the locale or pass undefined for default Unicode grapheme segmentation + hint: omit the locale, pass undefined, or pass one language tag as a string literal intl-segmenter.ts:3:31 - error SC2020: 'Intl.Segmenter options' is part of the standard library types but has no scriptc lowering yet - 2 | new Intl.Segmenter("en-US"); + 2 | new Intl.Segmenter(["en-US", "de"]); 3 | new Intl.Segmenter(undefined, { granularity: "word" }); | ^~~~~~~~~~~~~~~~~~~~~~~ 4 | new Intl.Segmenter(undefined, { granularity: "sentence" }); diff --git a/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json b/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8214-unshimmed-package-version/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json b/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8216-module-namespace-live-values/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json b/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/8217-intl-segmenter-word-construct/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json b/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json new file mode 100644 index 000000000..ffabf028f --- /dev/null +++ b/tests/harness/corpus-costs/8218-cjs-namespace-imports/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 7000, "san": 7200 } diff --git a/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json b/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json new file mode 100644 index 000000000..f8d95630e --- /dev/null +++ b/tests/harness/corpus-costs/8219-cjs-stable-var-exports/main.js.json @@ -0,0 +1 @@ +{ "plain": 8300, "san": 7400 } diff --git a/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json b/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json new file mode 100644 index 000000000..8b730689b --- /dev/null +++ b/tests/harness/corpus-costs/8220-js-class-static-destructuring/main.js.json @@ -0,0 +1 @@ +{ "plain": 10100, "san": 9400 } diff --git a/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json b/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json new file mode 100644 index 000000000..e6c42bf47 --- /dev/null +++ b/tests/harness/corpus-costs/8221-js-recursive-generator-returns/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6600, "san": 6000 } diff --git a/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json b/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json new file mode 100644 index 000000000..7f7902ca1 --- /dev/null +++ b/tests/harness/corpus-costs/8222-js-class-static-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 4100 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index e1831c609..5b541cc54 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -543,6 +543,16 @@ test("settled generic rest-order fences count each source statement once", () => expect(coverage.stats.statementsFailed).toBe(2); }); +test("CommonJS var and let exports that can change after the export keep their refusal", () => { + // JavaScript refusals defer to run time: they are the runtime fences. + const { coverage } = analyze(fixture("cjs-mutable-exports/main.js")); + const refused = [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])] + .map((d) => /exporting the mutable 'let' binding '(\w+)'/.exec(d.message)?.[1]) + .filter((name) => name !== undefined) + .sort(); + expect(refused).toEqual(["counter", "tab", "twice"]); +}); + test("inferred JavaScript compiles natively", async () => { // The js-gap fixture turns noImplicitAny off. Its inferred parameter // uses checked native values alongside the JSDoc-typed neighbor. diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 388da2138..838302af4 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -523,6 +523,28 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { } }); + test("a word segmenter in a package constructs, and its segment() throws SC2020", async () => { + const entry = join(pilotRoot, "word-segments-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: "auto" }); + expect(coverage.npmStatic).toEqual([{ package: "word-segments", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const outDir = join(cacheDir, "word-segments", sanitize ? "san" : "plain"); + const result = await compile(entry, { + backend: "llvm", + npmStatic: "auto", + sanitize, + outDir, + outPath: join(outDir, "program"), + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const nativeRes = await runBinary(result.binaryPath, []); + expect(nativeRes.stdout.toString()).toMatch( + /^SC2020: Intl\.Segmenter segmentation with granularity 'word' is not supported yet \[SC2020 at .*index\.js:1\]\n$/, + ); + expect(nativeRes.exitCode).toBe(0); + }); + test.each([ 'import { Console as Logger } from "node:console"; new Logger({ stdout: process.stdout }).dir(1);', 'import * as output from "console"; new output.Console(process.stdout).table([1, 2]);', diff --git a/tests/harness/package-shims.test.ts b/tests/harness/package-shims.test.ts index ca3d1589b..a21c4e768 100644 --- a/tests/harness/package-shims.test.ts +++ b/tests/harness/package-shims.test.ts @@ -8,7 +8,8 @@ * stays unloaded: its require throws MODULE_NOT_FOUND; * - a shimmed package that is not installed, or whose major version has no * shim, resolves exactly as before (here: refused like any missing or - * uncompiled package); + * uncompiled package), and `--npm-static` does not compile a version the + * registry entry does not list in `sourceVersions`; * - the undici shims keep memory bounded against a peer that sends endless * chunk-size lines and large or compressed bodies, and the 5.x profile * rejects a Host value with CR or LF as undici 5.29 does (the vendored @@ -253,6 +254,33 @@ describe("package shims", () => { } }, 180_000); + test("--npm-static leaves out an implemented package at a version it does not list", async () => { + const dir = join(scratch, "undici-v6-npm-static"); + const pkg = join(dir, "node_modules/undici"); + mkdirSync(pkg, { recursive: true }); + writeFileSync( + join(pkg, "package.json"), + '{ "name": "undici", "version": "6.21.0", "main": "index.js" }\n', + ); + writeFileSync(join(pkg, "index.js"), "exports.version = 6;\n"); + writeFileSync( + join(dir, "main.mjs"), + 'import { version } from "undici";\nconsole.log(version);\n', + ); + const result = await compile(join(dir, "main.mjs"), { + outPath: join(dir, ".scriptc/program"), + outDir: join(dir, ".scriptc"), + npmStatic: ["undici"], + sanitize, + }); + // The package's JavaScript is not part of the program, so the import + // has no declarations. + expect(result.ok).toBe(false); + expect(result.diagnostics.map((d) => d.message).join("\n")).toContain( + "Could not find a declaration file for module 'undici'", + ); + }, 180_000); + for (const name of ["undici7", "undici5"]) { test(`${name}: endless chunk-size lines and large bodies stay in bounded memory`, async () => { const { dir, binary } = await buildInCase(name, "undici-bounded.ts"); From 02ec0e86f607ce56a9a2241bb13aab08e25f23e6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 24 Oct 2026 16:54:24 -0500 Subject: [PATCH 2/2] Address review findings in the npm and CommonJS interop batch - Intl.Segmenter accepts an options object literal made only of `granularity` entries again, including the empty `{}` main accepted; with none the segmenter is a grapheme segmenter, and a repeated key takes its last value, as in JavaScript. - Namespace reads of a CommonJS module refuse a name that any module it re-exports assigns inside a function, not only the module itself: Node's namespace keeps the values from the end of evaluation. - The named-import link check treats `module.exports` as an export every CommonJS module provides, as Node 24 does, instead of throwing its missing-export SyntaxError. - The npm cases cjs-esm-named and cjs-lexer-aligned, which the batch no longer refuses at build time, are listed with the run-time SC1090 their CommonJS exports-object reads report. --- .../src/frontend/lowering/lower-classes.ts | 29 ++++++++++++------- .../compiler/src/frontend/lowering/lowerer.ts | 14 ++++++++- packages/compiler/src/frontend/program.ts | 27 +++++++++++++++-- .../main.ts | 10 ++++--- .../node_modules/text-width/index.d.ts | 1 + .../node_modules/text-width/index.js | 5 ++++ .../cjs-namespace-reexports/impl.cjs | 6 ++++ .../cjs-namespace-reexports/main.mjs | 7 +++++ .../cjs-namespace-reexports/package.json | 1 + .../cjs-namespace-reexports/wrapper.cjs | 2 ++ tests/harness/coverage.test.ts | 16 ++++++++++ tests/harness/npm.test.ts | 4 +-- 12 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/main.mjs create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/package.json create mode 100644 tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index e50466114..80ad97090 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -9950,23 +9950,30 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const options = args[1]; let granularity = "grapheme"; if (options && !undefinedArg(options)) { - const properties = ts.isObjectLiteralExpression(options) ? options.properties.nodes : null; - const property = properties?.length === 1 ? properties[0] : undefined; - if ( - property === undefined || - !ts.isPropertyAssignment(property) || - !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || - property.name.text !== "granularity" || - !ts.isStringLiteral(property.initializer) || - !granularities.includes(property.initializer.text) - ) { + // An object literal of `granularity` entries (none: the default; a + // repeated key: the last one, as in JavaScript). + if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering( "Intl.Segmenter options", options, 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', ); + for (const property of options.properties.nodes) { + if ( + !ts.isPropertyAssignment(property) || + !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || + property.name.text !== "granularity" || + !ts.isStringLiteral(property.initializer) || + !granularities.includes(property.initializer.text) + ) { + lowerer.noLowering( + "Intl.Segmenter options", + options, + 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported', + ); + } + granularity = property.initializer.text; } - granularity = property.initializer.text; } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 59dda3d08..b62c024f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -270,6 +270,7 @@ import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportsAssignedInFunctions, + cjsReexportedModules, cjsNamespaceExportNames, fallbackDtsPath, isCheckedJsSourceFile, @@ -4016,9 +4017,20 @@ export class Lowerer { } { let namespace = this.cjsNamespaces.get(dep); if (namespace === undefined) { + // A re-exported name reads the module that assigns it, so the writes + // after evaluation of every re-exported module count too. + let assignedLater: Set | "all" = new Set(); + for (const mod of cjsReexportedModules(this.program, dep)) { + const later = cjsExportsAssignedInFunctions(mod); + if (later === "all") { + assignedLater = "all"; + break; + } + for (const name of later) assignedLater.add(name); + } namespace = { names: cjsNamespaceExportNames(this.program, dep), - assignedLater: cjsExportsAssignedInFunctions(dep), + assignedLater, }; this.cjsNamespaces.set(dep, namespace); } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index ce5f3c2bc..755eff830 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -6403,6 +6403,25 @@ function lexWithAccessorNames(sf: ts.SourceFile): ReturnType([dep]); + const order = [dep]; + for (let i = 0; i < order.length; i++) { + const mod = order[i]!; + for (const spec of lexWithAccessorNames(mod).reexports) { + const next = resolveDep(mod, spec); + if (next !== null && !seen.has(next)) { + seen.add(next); + order.push(next); + } + } + } + return order; +} + export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set { const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map()); return new Set([...names, "default", "module.exports"]); @@ -6504,10 +6523,12 @@ function cjsNamedImportLinkCheck( }; const resolveCjsDep = cjsReexportDep7(program); const visible = (dep: ts.SourceFile, name: string): boolean => - // `default` is the module.exports binding itself — always provided. - // The program already owns native source ASTs; reuse them without - // starting another syntax session or reparsing their source text. + // `default` and `module.exports` are the module.exports binding itself + // — always provided. The program already owns native source ASTs; + // reuse them without starting another syntax session or reparsing + // their source text. name === "default" || + name === "module.exports" || cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name); /** The statement's resolved LOCAL CommonJS dependency, when it is an diff --git a/tests/corpus/8217-intl-segmenter-word-construct/main.ts b/tests/corpus/8217-intl-segmenter-word-construct/main.ts index 5a2a42e08..d0c2247e7 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/main.ts +++ b/tests/corpus/8217-intl-segmenter-word-construct/main.ts @@ -1,8 +1,10 @@ // A package builds its segmenters at module load (pi-tui, grok-mermaid): -// a grapheme segmenter with a locale tag, and a word segmenter that only -// some paths use. Grapheme segmentation matches Node; the word segmenter -// constructs, and this program never segments with it. -import { columns, describe } from "text-width"; +// a grapheme segmenter with a locale tag, one with empty options, and a +// word segmenter that only some paths use. Grapheme segmentation matches +// Node; the word segmenter constructs, and this program never segments +// with it. +import { columns, describe, plainColumns } from "text-width"; console.log(describe()); console.log(columns("éa👍🏽🇫🇷"), columns("abc")); +console.log(plainColumns("a\u0301b\u{1F44D}\u{1F3FD}")); diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts index 471b4fd0a..89b16ddc0 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.d.ts @@ -1,2 +1,3 @@ export declare function columns(text: string): string; export declare function describe(): string; +export declare function plainColumns(text: string): number; diff --git a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js index d58b4ccbb..c7619bd81 100644 --- a/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js +++ b/tests/corpus/8217-intl-segmenter-word-construct/node_modules/text-width/index.js @@ -1,11 +1,16 @@ const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" }); const words = new Intl.Segmenter(undefined, { granularity: "word" }); const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" }); +const plain = new Intl.Segmenter(undefined, {}); export function columns(text) { return [...graphemes.segment(text)].map((part) => part.segment).join("|"); } +export function plainColumns(text) { + return [...plain.segment(text)].length; +} + export function describe() { return `${typeof words} ${typeof sentences}`; } diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs new file mode 100644 index 000000000..3172d6685 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/impl.cjs @@ -0,0 +1,6 @@ +"use strict"; +exports.version = 1; +exports.current = null; +exports.set = (value) => { + exports.current = value; +}; diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs new file mode 100644 index 000000000..2ce528d48 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/main.mjs @@ -0,0 +1,7 @@ +// A namespace import of a CommonJS wrapper that re-exports another module: +// Node's namespace keeps impl.cjs's values from the end of its evaluation, +// so `ns.current` reads null after `ns.set(5)`. +import * as ns from "./wrapper.cjs"; + +ns.set(5); +console.log(ns.current, ns.version); diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/package.json b/tests/coverage-fixtures/cjs-namespace-reexports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs new file mode 100644 index 000000000..ab637bab5 --- /dev/null +++ b/tests/coverage-fixtures/cjs-namespace-reexports/wrapper.cjs @@ -0,0 +1,2 @@ +"use strict"; +module.exports = require("./impl.cjs"); diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 5b541cc54..ffb30b1ce 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -553,6 +553,22 @@ test("CommonJS var and let exports that can change after the export keep their r expect(refused).toEqual(["counter", "tab", "twice"]); }); +test("CommonJS namespace reads of exports a re-exported module assigns later keep their refusal", () => { + // wrapper.cjs re-exports impl.cjs, whose set() assigns `current` after + // evaluation; `version` never changes. + const { coverage } = analyze(fixture("cjs-namespace-reexports/main.mjs")); + const refused = [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])] + .map( + (d) => + /reads of '(\w+)' through namespace imports of CommonJS modules whose functions assign exports/.exec( + d.message, + )?.[1], + ) + .filter((name) => name !== undefined) + .sort(); + expect(refused).toEqual(["current"]); +}); + test("inferred JavaScript compiles natively", async () => { // The js-gap fixture turns noImplicitAny off. Its inferred parameter // uses checked native values alongside the JSDoc-typed neighbor. diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 27b27b8e3..d1bdcf190 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -38,8 +38,8 @@ const KNOWN: Record = { "assert-shims": { run: "SC1043" }, "buffer-shims": { run: "SC2020" }, "bytes-exit": { build: "SC2004" }, - "cjs-esm-named": { build: "SC2013" }, - "cjs-lexer-aligned": { build: "SC2013" }, + "cjs-esm-named": { run: "SC1090" }, + "cjs-lexer-aligned": { run: "SC1090" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, "create-require": { run: "SC1090" },