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 <noreply@anthropic.com>
This commit is contained in:
Iwakurakaoru0822andClaude Opus 5.5 committed 2026-10-06 20:35:02 -07:00
1 parent ef535b81fe
commit f7954a8fd6
8 files changed
+83 -7

No files matched your search

@@ -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<number>)",
@@ -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,
@@ -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<string, JavaScriptSemanticGraphNode>;
@@ -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
@@ -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: {
@@ -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" },
]);
});
+6 -2
View File
@@ -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,
+3 -2
View File
@@ -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)}]`
);
}