fix(semantics): isolate named function expression bindings (#776)
Squash-merge: reviewed, no issues. Clean semantics fix with thorough binding-matrix coverage.
This commit is contained in:
1 parent
c945931961
commit
ae81f64d2a
3 files changed
+149
-11
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
|
||||
it.each(["execute", "{execute}", "...execute", "execute = () => 'DEFAULT'"])(
|
||||
"lets %s parameters shadow a named function expression",
|
||||
(parameter) => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`const task = function execute(${parameter}) { return execute(); };`,
|
||||
);
|
||||
const call = ir.callSites[0];
|
||||
expect(call?.calleeCallableIds).toEqual([]);
|
||||
expect(call?.resolution).toBe("unresolved");
|
||||
const bindings = ir.bindings.filter(({ name }) => name === "execute");
|
||||
expect(bindings).toHaveLength(2);
|
||||
expect(bindings.map(({ kind }) => kind).sort()).toEqual([
|
||||
"function",
|
||||
"parameter",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["var", "let", "const"])(
|
||||
"lets body %s declarations shadow a named function expression",
|
||||
(kind) => {
|
||||
const ir = analyzeJavaScriptSemantics(`const task = function execute() {
|
||||
${kind} execute = () => 'LOCAL';
|
||||
return execute();
|
||||
};`);
|
||||
const local = ir.callables.find(
|
||||
({ location }) => location.start.line === 2,
|
||||
);
|
||||
expect(ir.callSites[0]?.calleeCallableIds).toEqual([local?.callableId]);
|
||||
expect(ir.callSites[0]?.resolution).toBe("exact");
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves recursive function-name resolution without leaking the name", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`const task = function execute(depth) {
|
||||
return depth === 0 ? 'DONE' : execute(depth - 1);
|
||||
};
|
||||
execute();`);
|
||||
const own = ir.callables.find(({ name }) => name === "execute");
|
||||
expect(ir.callSites[0]?.calleeCallableIds).toEqual([own?.callableId]);
|
||||
expect(ir.callSites[1]?.calleeCallableIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains an unshadowed function name in parameter defaults", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`const task = function execute(value = execute()) { return value; };`,
|
||||
);
|
||||
const own = ir.callables.find(({ name }) => name === "execute");
|
||||
expect(ir.callSites[0]?.calleeCallableIds).toEqual([own?.callableId]);
|
||||
});
|
||||
@@ -310,7 +310,9 @@ const nestedScope = (
|
||||
if (kind === undefined) return undefined;
|
||||
const scope: JavaScriptSemanticScopeState = {
|
||||
scopeId: semanticScopeId(kind, switchOwner ?? node),
|
||||
parentScopeId: parentScope.scopeId,
|
||||
parentScopeId:
|
||||
functionNameScope(node, parentScope, state)?.scopeId ??
|
||||
parentScope.scopeId,
|
||||
kind,
|
||||
location: range(switchOwner ?? node),
|
||||
bindingsComplete: true,
|
||||
@@ -321,6 +323,37 @@ const nestedScope = (
|
||||
return scope;
|
||||
};
|
||||
|
||||
const functionNameScope = (
|
||||
node: t.Node,
|
||||
parentScope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticScopeState | undefined => {
|
||||
if (!t.isFunctionExpression(node) || !t.isIdentifier(node.id))
|
||||
return undefined;
|
||||
// A named expression has a private name environment outside its parameters
|
||||
// and body. Parameters and local declarations may shadow that name.
|
||||
const scope: JavaScriptSemanticScopeState = {
|
||||
scopeId: `${semanticScopeId("block", node)}:function-name`,
|
||||
parentScopeId: parentScope.scopeId,
|
||||
kind: "block",
|
||||
location: range(node),
|
||||
bindingsComplete: true,
|
||||
bindings: new Map(),
|
||||
};
|
||||
state.scopes.push(scope);
|
||||
state.scopesById.set(scope.scopeId, scope);
|
||||
addBinding({
|
||||
state,
|
||||
scope,
|
||||
name: node.id.name,
|
||||
kind: "function",
|
||||
mutable: false,
|
||||
definitionNode: node.id,
|
||||
initializer: node,
|
||||
});
|
||||
return scope;
|
||||
};
|
||||
|
||||
const scopeKind = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
@@ -377,16 +410,6 @@ const bindFunctionLocals = (
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
if (t.isFunctionExpression(node) && t.isIdentifier(node.id))
|
||||
addBinding({
|
||||
state,
|
||||
scope,
|
||||
name: node.id.name,
|
||||
kind: "function",
|
||||
mutable: false,
|
||||
definitionNode: node.id,
|
||||
initializer: node,
|
||||
});
|
||||
for (const parameter of node.params)
|
||||
bindPattern({
|
||||
pattern: t.isTSParameterProperty(parameter)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"callback",
|
||||
`const task = function execute(execute) {
|
||||
return execute();
|
||||
}; task(() => 'CALLBACK');`,
|
||||
2,
|
||||
null,
|
||||
],
|
||||
[
|
||||
"body-local",
|
||||
`const task = function execute() {
|
||||
var execute = () => 'LOCAL';
|
||||
return execute();
|
||||
}; task();`,
|
||||
3,
|
||||
2,
|
||||
],
|
||||
])(
|
||||
"keeps named expression %s calls distinct in the public CLI",
|
||||
async (name, source, callLine, targetLine) => {
|
||||
const root = await createTestTempDirectory("rea-function-name-cli-");
|
||||
await writeFile(join(root, "app.js"), source);
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
|
||||
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
parseEvidence(JSON.parse(stdout)).normalized_result,
|
||||
);
|
||||
const graph = result.semantic_graph;
|
||||
const targets = graph.relations
|
||||
.filter(
|
||||
({ relation, source_node_id }) =>
|
||||
relation === "calls" &&
|
||||
graph.nodes.find(({ node_id }) => node_id === source_node_id)
|
||||
?.identity.source_range?.start.line === callLine,
|
||||
)
|
||||
.map(
|
||||
({ target_node_id }) =>
|
||||
graph.nodes.find(({ node_id }) => node_id === target_node_id)
|
||||
?.identity.source_range?.start.line,
|
||||
);
|
||||
expect(targets).toEqual(targetLine === null ? [] : [targetLine]);
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
Reference in new issue
Block a user