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:
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" },
|
||||
]);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}]`
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user