From f7954a8fd692f262ef3ce1507b957681150ce9c4 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 20:35:02 -0700 Subject: [PATCH] fix(javascript): keep exact empty env keys and method names `process.env['']` now projects an exact empty environment key with complete resolution instead of the same null/partial result as a dynamic `process.env[key]`. `{ ['']() {} }` keeps its exact empty method name in the IR instead of a generated `[computed@N]` name. Semantic graph labels stay nonempty: a shared `semanticNameLabel` helper displays the empty key as `""` for property slots, config sources, and function nodes, while `properties.key` keeps the exact value. Refs #783 Co-Authored-By: Claude Opus 5.5 --- .../JavaScriptSemanticGraphBuilder.test.ts | 32 +++++++++++++++++++ .../JavaScriptSemanticGraphBuilder.ts | 4 ++- .../JavaScriptSemanticGraphConstruction.ts | 8 +++++ .../JavaScriptSemanticGraphDataProjection.ts | 6 +++- .../JavaScriptSemanticGraphValueProjection.ts | 3 +- ...ascriptSemanticAnalysis.empty_keys.test.ts | 24 ++++++++++++++ src/domain/javascriptSemanticDataEffects.ts | 8 +++-- src/domain/javascriptSemanticProjection.ts | 5 +-- 8 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/application/JavaScriptSemanticGraphBuilder.test.ts b/src/application/JavaScriptSemanticGraphBuilder.test.ts index 8b0c5ec90a..d50720b411 100644 --- a/src/application/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/JavaScriptSemanticGraphBuilder.test.ts @@ -349,6 +349,38 @@ it.each([ ); }, ); +it("keeps exact empty environment keys distinct from dynamic keys", () => { + const graph = graphFor(` + const empty = process.env['']; + const named = process.env.HOME; + const dynamic = process.env[name]; + `); + const sources = graph.nodes + .filter(({ kind }) => kind === "config-source") + .map(({ label, properties }) => ({ label, key: properties.key })); + expect(sources).toEqual( + expect.arrayContaining([ + { label: '""', key: "" }, + { label: "HOME", key: "HOME" }, + { label: "environment", key: null }, + ]), + ); + expect(sources).toHaveLength(3); +}); + +it("labels exact empty method names without inventing computed names", () => { + const graph = graphFor(` + const target = { ['']() { return 42; }, [key]() { return 1; } }; + `); + const labels = graph.nodes + .filter(({ kind }) => kind === "function") + .map(({ label }) => label); + expect(labels).toContain('""'); + expect( + labels.filter((label) => label?.startsWith("[computed@")), + ).toHaveLength(1); +}); + it.each([ "Promise.resolve(1)", "(Promise.resolve(1) as Promise)", diff --git a/src/application/JavaScriptSemanticGraphBuilder.ts b/src/application/JavaScriptSemanticGraphBuilder.ts index a9df118cf2..74be8f6540 100644 --- a/src/application/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/JavaScriptSemanticGraphBuilder.ts @@ -48,6 +48,7 @@ import { addSemanticGraphRelation as addRelation, constructSemanticGraphNode as semanticNode, createSemanticGraphProjectionState as emptyState, + semanticNameLabel, type SemanticGraphProjectionState as BuilderState, } from "./JavaScriptSemanticGraphConstruction.js"; @@ -165,7 +166,8 @@ const projectFile = ( kind: "function", roleKey: `callable:${callable.callableId}`, location: callable.location, - label: callable.name, + label: + callable.name === null ? null : semanticNameLabel(callable.name), functionNodeId: null, }, state, diff --git a/src/application/JavaScriptSemanticGraphConstruction.ts b/src/application/JavaScriptSemanticGraphConstruction.ts index fa6192e679..f52f5374fa 100644 --- a/src/application/JavaScriptSemanticGraphConstruction.ts +++ b/src/application/JavaScriptSemanticGraphConstruction.ts @@ -18,6 +18,14 @@ import { unavailableSemanticRootEvidence, } from "./JavaScriptSemanticGraphEvidence.js"; +/** + * Display an exact JavaScript key or name as a nonempty graph label. The legal + * empty key is shown as `""`; callers keep the exact value where the node + * carries it. + */ +export const semanticNameLabel = (name: string): string => + name === "" ? '""' : name; + /** Mutable local projection state hidden from graph callers. */ export interface SemanticGraphProjectionState { readonly nodes: Map; diff --git a/src/application/JavaScriptSemanticGraphDataProjection.ts b/src/application/JavaScriptSemanticGraphDataProjection.ts index 2bc1a94132..fb2db56e4c 100644 --- a/src/application/JavaScriptSemanticGraphDataProjection.ts +++ b/src/application/JavaScriptSemanticGraphDataProjection.ts @@ -10,6 +10,7 @@ import { addSemanticGraphRelation, addSemanticGraphUnknown, constructSemanticGraphNode, + semanticNameLabel, } from "./JavaScriptSemanticGraphConstruction.js"; import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; import { @@ -78,7 +79,10 @@ const addConfigurationNode = ( kind: "config-source", roleKey: operation.configId, location: operation.location, - label: operation.key ?? operation.kind, + label: + operation.key === null + ? operation.kind + : semanticNameLabel(operation.key), functionNodeId: operation.ownerCallableId === null ? null diff --git a/src/application/JavaScriptSemanticGraphValueProjection.ts b/src/application/JavaScriptSemanticGraphValueProjection.ts index d8bedec20f..ad49c9ec8a 100644 --- a/src/application/JavaScriptSemanticGraphValueProjection.ts +++ b/src/application/JavaScriptSemanticGraphValueProjection.ts @@ -7,6 +7,7 @@ import { addSemanticGraphNode, addSemanticGraphRelation, constructSemanticGraphNode, + semanticNameLabel, } from "./JavaScriptSemanticGraphConstruction.js"; import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js"; @@ -116,7 +117,7 @@ export const semanticPropertySlot = ( kind: "property-slot", roleKey: `property:${objectBindingId}:${name}`, location: null, - label: name === "" ? '""' : name, + label: semanticNameLabel(name), functionNodeId: context.bindingNodes.get(objectBindingId)?.function_node_id ?? null, properties: { diff --git a/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts b/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts index 969f02707f..0149340663 100644 --- a/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts +++ b/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts @@ -31,3 +31,27 @@ it("keeps a computed identifier key unresolved", () => { ]), ); }); + +it("retains exact empty method names and environment keys", () => { + const ir = analyzeJavaScriptSemantics(` + const target = { ['']() { return 42; } }; + class Holder { ""() { return 2; } } + const empty = process.env['']; + const dynamic = process.env[key]; + `); + expect( + ir.callables + .filter(({ kind }) => kind === "method") + .map(({ name }) => name), + ).toEqual(["", ""]); + expect( + ir.configurationOperations.map(({ kind, key, resolution }) => ({ + kind, + key, + resolution, + })), + ).toEqual([ + { kind: "environment", key: "", resolution: "complete" }, + { kind: "environment", key: null, resolution: "partial" }, + ]); +}); diff --git a/src/domain/javascriptSemanticDataEffects.ts b/src/domain/javascriptSemanticDataEffects.ts index 6cebc959e8..830d2e94ba 100644 --- a/src/domain/javascriptSemanticDataEffects.ts +++ b/src/domain/javascriptSemanticDataEffects.ts @@ -9,7 +9,10 @@ import type { JavaScriptSemanticRequestOperation, } from "./javascriptSemanticIr.js"; import { compareCodePoints } from "./canonicalOrdering.js"; -import { semanticStaticPropertyName } from "./javascriptSemanticProjection.js"; +import { + semanticStaticPropertyKey, + semanticStaticPropertyName, +} from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, semanticResolutionBlocked, @@ -145,7 +148,8 @@ const processConfigurationMember = ( if (!isProcessObject(object.object, state)) return null; const family = semanticStaticPropertyName(object.property, object.computed); if (family !== "env" && family !== "argv") return null; - const key = semanticStaticPropertyName(node.property, node.computed) || null; + // `process.env[""]` names an exact (empty) key; only a dynamic key is unknown. + const key = semanticStaticPropertyKey(node.property, node.computed); return { kind: family === "env" ? "environment" : "argv", key, diff --git a/src/domain/javascriptSemanticProjection.ts b/src/domain/javascriptSemanticProjection.ts index c44b95f6e3..c6dc3cd02c 100644 --- a/src/domain/javascriptSemanticProjection.ts +++ b/src/domain/javascriptSemanticProjection.ts @@ -240,9 +240,10 @@ const callableName = (node: t.Node, parent: t.Node | null): string | null => { ) { if (t.isPrivateName(node.key)) return `#${node.key.id.name}`; // `{[key]() {}}` names nothing statically; reading `key` off the key node - // would report the variable name as the method name. + // would report the variable name as the method name. `{[""]() {}}` names + // the exact empty key, which is retained rather than replaced. return ( - semanticStaticPropertyName(node.key, node.computed) || + semanticStaticPropertyKey(node.key, node.computed) ?? `[computed@${String(node.start ?? -1)}]` ); }