Merge pull request #836 from morluto/fix/empty-static-key-consumers

fix(javascript): keep exact empty env keys and method names
This commit is contained in:
Kaoru0822 2026-10-06 20:54:50 -07:00 committed by GitHub
commit 522c2d0ecb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 138 additions and 17 deletions

View File

@ -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>)",

View File

@ -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,

View File

@ -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>;

View File

@ -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

View File

@ -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: {

View File

@ -31,3 +31,75 @@ 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" },
]);
});
it("retains exact empty require paths without dynamic-key links", () => {
const ir = analyzeJavaScriptSemantics(`
const { '': fromPattern } = require("./table.js");
const fromMember = require("./table.js")[''];
const { [key]: dynamicPattern } = require("./table.js");
const dynamicMember = require("./table.js")[key];
module.exports.root = require("./table.js")[''];
`);
expect(
ir.moduleLinks.map(({ kind, importedName, localName, exportedName }) => ({
kind,
importedName,
localName,
exportedName,
})),
).toEqual([
{
kind: "require",
importedName: "",
localName: "fromPattern",
exportedName: null,
},
{
kind: "require",
importedName: "",
localName: "fromMember",
exportedName: null,
},
{
kind: "commonjs-export",
importedName: "",
localName: null,
exportedName: "root",
},
]);
});
it("retains exact empty request field names without dynamic fields", () => {
const ir = analyzeJavaScriptSemantics(`
fetch("https://example.test", { '': value, [key]: 1, body: "x" });
`);
expect(
ir.requestOperations.flatMap(({ fields }) =>
fields.map(({ name }) => name),
),
).toEqual(["", "body"]);
});

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,
@ -401,8 +405,8 @@ const requestFields = (
if (!t.isObjectProperty(property)) return [];
// `{[key]: 1}` sends no knowable field name; reporting `key` would invent
// a request field that may not exist.
const name = semanticStaticPropertyName(property.key, property.computed);
if (name.length === 0) return [];
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) return [];
return [
{
name,

View File

@ -25,10 +25,7 @@ import {
range,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import {
semanticStaticPropertyKey,
semanticStaticPropertyName,
} from "./javascriptAstValues.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
interface CollectCallableInput {
readonly node: t.Node;
@ -181,8 +178,8 @@ const semanticModuleOrigin = (
const nested = t.isNode(node.object)
? semanticModuleOrigin(node.object, projection, state)
: undefined;
const member = semanticStaticPropertyName(node.property, node.computed);
return nested === undefined || member === ""
const member = semanticStaticPropertyKey(node.property, node.computed);
return nested === undefined || member === null
? undefined
: { ...nested, importedPath: [...nested.importedPath, member] };
}
@ -240,9 +237,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)}]`
);
}
@ -340,8 +338,8 @@ const requirePatternBindings = (
if (t.isObjectPattern(pattern))
return pattern.properties.flatMap((property) => {
if (t.isRestElement(property)) return [];
const name = semanticStaticPropertyName(property.key, property.computed);
return name === ""
const name = semanticStaticPropertyKey(property.key, property.computed);
return name === null
? []
: requirePatternBindings(property.value, [...path, name]);
});