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:
commit
522c2d0ecb
|
|
@ -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,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"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue