From 46d30c86718d81ad0527f693cb0359d96616c027 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 26 Sep 2026 08:00:23 -0500 Subject: [PATCH] Preserve class own-key lowering and verify npm static calls (#459) --- .../src/frontend/lowering/lower-calls.ts | 2 +- .../fixtures/npm-static/colors-hasown-cli.ts | 3 +++ tests/harness/npm-static.test.ts | 22 ++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/npm-static/colors-hasown-cli.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ddd93499..3fe2a0dd 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4621,7 +4621,6 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerTestMethodCall(expr, expr.expression) ?? lowerer.lowerTimeoutMethodCall(expr, expr.expression) ?? lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ?? - lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ?? lowerObjectPrototypeCall(lowerer, expr, expr.expression) ?? lowerStringPrototypeCall(lowerer, expr, expr.expression) ?? lowerStringMethodCallWithOptionalArgs(lowerer, expr, expr.expression) ?? @@ -4653,6 +4652,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // hasOwnProperty on a program class CONSTRUCTOR — own statics are // compile-time-known, so a literal key folds to a constant. lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ?? + lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ?? // Response constructor-object operations are unsupported in both // tiers. Keep their SC2020 inventory contract ahead of the island // and generic-call fallbacks (Response.json otherwise reports the diff --git a/tests/fixtures/npm-static/colors-hasown-cli.ts b/tests/fixtures/npm-static/colors-hasown-cli.ts new file mode 100644 index 00000000..dc6e73fd --- /dev/null +++ b/tests/fixtures/npm-static/colors-hasown-cli.ts @@ -0,0 +1,3 @@ +import pc from "picocolors"; + +console.log(pc.hasOwnProperty("bold"), pc.hasOwnProperty("missing")); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 4c9c07c3..e123c7d2 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -169,19 +169,35 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 180_000); - test("picocolors inherited Object methods retain the prototype-method fence", () => { + test("picocolors inherited valueOf retains the prototype-method fence", () => { const entry = join(pilotRoot, "colors-prototype-cli.ts"); const { coverage } = analyze(entry, { npmStatic: ["picocolors"] }); expect(coverage.npmStatic).toEqual([{ package: "picocolors", status: "static" }]); expect(coverage.preflightFailed).toBe(false); expect(coverage.runtimeFences ?? []).toHaveLength(0); - expect(coverage.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020", "SC2020"]); + expect(coverage.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020"]); expect(coverage.diagnostics.map((diagnostic) => diagnostic.message)).toEqual([ - expect.stringContaining(".hasOwnProperty' is part of the standard library types"), expect.stringContaining(".valueOf' is part of the standard library types"), ]); }, 120_000); + test("picocolors inherited hasOwnProperty matches Node", async () => { + const entry = join(pilotRoot, "colors-hasown-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: ["picocolors"] }); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toHaveLength(0); + expect(coverage.runtimeFences ?? []).toHaveLength(0); + + const binary = await buildStatic(entry, ["picocolors"]); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout).toEqual(nodeRes.stdout); + expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + // Tier 1, auto mode: the eligibility heuristics pick escape-string-regexp // (own .d.ts, unminified, no transform markers) without naming it. test("--npm-static=auto opts the eligible pilot in", () => {