fix(javascript): invalidate stale facts across direct eval
Index unshadowed direct-eval sites once at the semantic owner. Distinguish lexical rebinding, non-strict eval-introduced names, and possible mutation of reachable objects. Reuse existing effect timing and capture proofs; retain original definitions and module origins without inventing assignments or parsing or executing eval payloads. Query object uncertainty conservatively across the owning activation so aliases remain covered when names are shadowed or leave block scope. Retain no binding-by-eval table. Use the shared facts for values, read provenance, reference effects and child argument shapes. Preserve indirect/optional/shadowed eval controls, unrelated activations, immutable shapes, strict-mode bindings and prior primitive captures. Remove obsolete reference plumbing and the source-wide eval blocker. Validation: 306 focused checks across 16 files pass, including compiled CLI and stdio MCP return projections, constructor uncertainty and child-process consumers. Against the preceding SQLite commit, 9 of 23 selected checks fail and 14 controls pass. Static/docs/test discovery (1559 files) pass. Two source reviews found no remaining scoped blocker after alias and sloppy-name fixes. No analyzed JavaScript, SQLite engine, heavy test or profiler was executed. Conservative object reachability can intentionally lose exactness; large-case opaque-query CPU and memory remain unmeasured. Fixes #4875
This commit is contained in:
1 parent
fc847ffe35
commit
9ac76d3d94
13 files changed
+446
-46
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
const returned = (body: string) =>
|
||||
onlyCallable(analyzeJavaScriptSemantics(body), "run").returnSites[0]?.value;
|
||||
|
||||
it.each([
|
||||
'export function run() { const value={n:1}; const alias=value; {const value=0;eval("alias.n=2");} return value.n; }',
|
||||
'const value=1; function run(){eval("var value=2");return value;}',
|
||||
'export function run(){let alias;let result;{const value={n:1};alias=value;eval("alias.n=2");result=value.n;}return result;}',
|
||||
'function run(){function value(){} eval("value=2");return value;}',
|
||||
'function run(){class value {} eval("value=2");return value;}',
|
||||
|
||||
'export function run() { let value=1; eval("value=2"); return value; }',
|
||||
'export function run() { const value={n:1}; eval("value.n=2"); return value.n; }',
|
||||
'export function run() { const value={n:1}; const alias=value; eval("value.n=2"); return alias.n; }',
|
||||
'export function run() { let value=1; function change(){ eval("value=2"); } change(); return value; }',
|
||||
'export function run() { let value=1; change(); return value; function change(){ eval("value=2"); } }',
|
||||
])("does not retain stale exact values after direct eval: %s", (source) => {
|
||||
expect(returned(source)).toMatchObject({ status: "unknown" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
'eval("var value=2"); function run(){const value=1;return value;}',
|
||||
'export function run(){const value={n:1};const saved=value.n;eval("value.n=2");return saved;}',
|
||||
'const value=1; function run(){"use strict";eval("var value=2");return value;}',
|
||||
'function run(){const value=1;eval("var other=2");return value;}',
|
||||
'function other(){const value={n:2};eval("value.n=3");} export function run(){const value={n:1};return value.n;}',
|
||||
'export function run() { let value=1; (0,eval)("value=2"); return value; }',
|
||||
'export function run() { let value=1; eval?.("value=2"); return value; }',
|
||||
'function run() { let value=1; function eval(x) {} eval("value=2"); return value; }',
|
||||
'function other() { let value=3; eval("value=4"); } export function run() { let value=1; return value; }',
|
||||
'export function run() { let value=1; function other() { let value=3; eval("value=4"); } other(); return value; }',
|
||||
'export function run() { let value=1; const saved=value; eval("value=2"); return saved; }',
|
||||
])(
|
||||
"preserves unaffected lexical facts and primitive captures: %s",
|
||||
(source) => {
|
||||
expect(returned(source)).toEqual({ status: "literal", value: 1 });
|
||||
},
|
||||
);
|
||||
|
||||
it("invalidates object facts captured in a block when an alias reaches eval outside it", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`export function run(){
|
||||
let alias;
|
||||
{const value={n:1};alias=value; function read(){return value.n;}}
|
||||
eval("alias.n=2");
|
||||
}`);
|
||||
expect(onlyCallable(ir, "read").returnSites[0]?.value).toMatchObject({
|
||||
status: "unknown",
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
indexSemanticOpaqueEffects,
|
||||
semanticOpaqueBindingEffects,
|
||||
semanticBindingCanBeRebound,
|
||||
} from "./javascriptSemanticOpaqueEffects.js";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
@@ -69,7 +74,10 @@ import {
|
||||
resolveSemanticModuleCallables,
|
||||
} from "./javascriptSemanticReturns.js";
|
||||
import { collectJavaScriptDerivedSemanticsSteps } from "./javascriptSemanticDerivedAnalysis.js";
|
||||
import { evaluateSemanticProvenance } from "./javascriptSemanticValues.js";
|
||||
import {
|
||||
evaluateSemanticProvenance,
|
||||
clearSemanticValueCaches,
|
||||
} from "./javascriptSemanticValues.js";
|
||||
import {
|
||||
collectJavaScriptIdentityWritesSteps,
|
||||
electronExportPath,
|
||||
@@ -218,6 +226,15 @@ const visitSemanticReferenceEffectsAtSite = (
|
||||
});
|
||||
};
|
||||
if (binding !== undefined) {
|
||||
if (semanticBindingCanBeRebound(binding))
|
||||
for (const node of semanticOpaqueBindingEffects(binding, state))
|
||||
if (!retain("binding-write", node)) return metadata;
|
||||
for (const node of semanticOpaqueBindingEffects(
|
||||
binding,
|
||||
state,
|
||||
"reference",
|
||||
))
|
||||
if (!retain("escape", node, [])) return metadata;
|
||||
for (const node of state.bindingWrites.get(binding.bindingId) ?? [])
|
||||
if (!retain("binding-write", node)) return metadata;
|
||||
if (indexedMemberMutationStates.has(state)) {
|
||||
@@ -901,6 +918,12 @@ const createState = (
|
||||
};
|
||||
return {
|
||||
program,
|
||||
opaqueEffects: {
|
||||
sites: new WeakSet(),
|
||||
sloppySites: new WeakSet(),
|
||||
byScope: new Map(),
|
||||
children: new Map(),
|
||||
},
|
||||
staticImportResolutionMode,
|
||||
valueResourceLimits: new Set(),
|
||||
getterResourceLimits: new Set(),
|
||||
@@ -939,6 +962,7 @@ function* collectDefinitionsSteps(
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly conditionallySkipped: boolean;
|
||||
}[] = [];
|
||||
const opaqueCandidates: { node: t.CallExpression; scopeId: string }[] = [];
|
||||
let needsPrototypeFacts = false;
|
||||
const functionBodies: {
|
||||
readonly body: t.BlockStatement;
|
||||
@@ -1016,6 +1040,13 @@ function* collectDefinitionsSteps(
|
||||
state,
|
||||
});
|
||||
bindInnerDeclaration(node, parent, scope, state);
|
||||
if (
|
||||
t.isCallExpression(node) &&
|
||||
t.isIdentifier(unwrapJavaScriptExpression(node.callee).node, {
|
||||
name: "eval",
|
||||
})
|
||||
)
|
||||
opaqueCandidates.push({ node, scopeId: scope.scopeId });
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
node.init != null &&
|
||||
@@ -1064,6 +1095,8 @@ function* collectDefinitionsSteps(
|
||||
if (t.isIdentifier(identifier))
|
||||
copyParameterToBody({ identifier, body, scope, state });
|
||||
}
|
||||
indexSemanticOpaqueEffects(opaqueCandidates, state);
|
||||
if (opaqueCandidates.length > 0) clearSemanticValueCaches(state);
|
||||
}
|
||||
|
||||
type PrototypeOwner = "Object" | "Array" | "String";
|
||||
|
||||
@@ -375,8 +375,8 @@ const collectDynamicScope = (
|
||||
): void => {
|
||||
const reason = t.isWithStatement(node)
|
||||
? "`with` introduces a runtime binding environment; names inside are not statically resolvable."
|
||||
: t.isCallExpression(node) && isUnshadowedGlobal(node.callee, state, "eval")
|
||||
? "`eval` can declare bindings at runtime; names inside the evaluated source are not statically resolvable."
|
||||
: state.opaqueEffects.sites.has(node)
|
||||
? "`eval` can change bindings and reachable objects at runtime; its effects are not statically resolvable."
|
||||
: null;
|
||||
if (reason === null) return;
|
||||
addFrontier(
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticChildProcessInteraction,
|
||||
JavaScriptSemanticChildProcessSpawn,
|
||||
JavaScriptSemanticReference,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
|
||||
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
|
||||
@@ -68,10 +67,9 @@ export const collectJavaScriptSemanticChildProcesses = (
|
||||
program: t.Program,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
callables: readonly JavaScriptSemanticCallable[],
|
||||
references: readonly JavaScriptSemanticReference[],
|
||||
): ChildProcessAnalysis => {
|
||||
const candidates = collectSpawns(program, state, callables);
|
||||
const context = createSemanticExpressionShapeResolver(state, references);
|
||||
const context = createSemanticExpressionShapeResolver(state);
|
||||
const childProcessSpawns = candidates.map((candidate) =>
|
||||
immutableSpawn(candidate, context),
|
||||
);
|
||||
|
||||
@@ -66,7 +66,6 @@ export function* collectJavaScriptDerivedSemanticsSteps(
|
||||
program,
|
||||
state,
|
||||
callables,
|
||||
references,
|
||||
);
|
||||
yield;
|
||||
const dataEffects = collectJavaScriptSemanticDataEffects(
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
indexJavaScriptBindingsSteps,
|
||||
} from "./javascriptSemanticAnalysis.js";
|
||||
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
|
||||
import { collectSemanticReferences } from "./javascriptSemanticProjection.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import {
|
||||
completeSemanticSteps,
|
||||
@@ -32,6 +31,8 @@ it.each([
|
||||
["var a=null;frames.a={};read(a);", ["unknown"]],
|
||||
["function f(){const a=[];a.push(1);read(a);}", ["array"]],
|
||||
["function f(){const a=[];const saved=a;read(saved);}", ["array"]],
|
||||
['const a=[];function f(){eval("var a={}");read(a)}', ["unknown"]],
|
||||
['function f(){const a=[];eval("a={}");read(a)}', ["array"]],
|
||||
["function f(){let a=[];eval('a={}');read(a)}", ["unknown"]],
|
||||
["function f(){eval('var undefined={}');read(undefined)}", ["unknown"]],
|
||||
["var a=[];Function('a={}');read(a);", ["unknown"]],
|
||||
@@ -45,10 +46,7 @@ it.each([
|
||||
const state = completeSemanticSteps(
|
||||
indexJavaScriptBindingsSteps(file.program),
|
||||
);
|
||||
const resolver = createSemanticExpressionShapeResolver(
|
||||
state,
|
||||
collectSemanticReferences(state.program, state),
|
||||
);
|
||||
const resolver = createSemanticExpressionShapeResolver(state);
|
||||
const shapes: string[] = [];
|
||||
traverseJavaScriptAst(file.program, {
|
||||
enter(node) {
|
||||
@@ -83,10 +81,7 @@ it("bounds long callback aliases without invoking the shared callable resolver",
|
||||
const state = completeSemanticSteps(
|
||||
indexJavaScriptBindingsSteps(file.program),
|
||||
);
|
||||
const resolver = createSemanticExpressionShapeResolver(
|
||||
state,
|
||||
collectSemanticReferences(state.program, state),
|
||||
);
|
||||
const resolver = createSemanticExpressionShapeResolver(state);
|
||||
const results: boolean[] = [];
|
||||
traverseJavaScriptAst(file.program, {
|
||||
enter(node) {
|
||||
@@ -123,7 +118,7 @@ it.each([
|
||||
[
|
||||
"fixture.cts",
|
||||
"const argv=[];const done=()=>{};eval(input);",
|
||||
["dynamic", "dynamic"],
|
||||
["inherit", "default"],
|
||||
],
|
||||
[
|
||||
"fixture.cts",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
semanticOpaqueBindingReadMayHaveChanged,
|
||||
semanticOpaqueNameMayHaveChanged,
|
||||
} from "./javascriptSemanticOpaqueEffects.js";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
@@ -9,7 +13,6 @@ import {
|
||||
isUnmodeledJavaScriptRuntimeScope,
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import type { JavaScriptSemanticReference } from "./javascriptSemanticIr.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
isUnshadowedGlobal,
|
||||
@@ -32,7 +35,6 @@ const commonJsWrapperParameterNames = new Set([
|
||||
/** Bounded child-argument identity only; never recover named object contents. */
|
||||
export const createSemanticExpressionShapeResolver = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
references: readonly JavaScriptSemanticReference[],
|
||||
) => {
|
||||
const hoistedFunction = (node: t.Node): boolean => {
|
||||
const parent = state.parentsByNode.get(node);
|
||||
@@ -49,22 +51,21 @@ export const createSemanticExpressionShapeResolver = (
|
||||
const blockFunctionNames = new Set<string>();
|
||||
const namedProofBlocked = (): boolean => {
|
||||
if (blocked !== undefined) return blocked;
|
||||
blocked = references.some(({ name }) => name === "eval");
|
||||
if (!blocked)
|
||||
traverseJavaScriptAst(state.program, {
|
||||
enter(node) {
|
||||
blocked ||=
|
||||
isErasedJavaScriptDeclaration(node) ||
|
||||
isUnmodeledJavaScriptRuntimeScope(node);
|
||||
if (
|
||||
javaScriptRuntimeContext(state.program) !== "module" &&
|
||||
t.isFunctionDeclaration(node) &&
|
||||
node.id != null &&
|
||||
!hoistedFunction(node)
|
||||
)
|
||||
blockFunctionNames.add(node.id.name);
|
||||
},
|
||||
});
|
||||
blocked = false;
|
||||
traverseJavaScriptAst(state.program, {
|
||||
enter(node) {
|
||||
blocked ||=
|
||||
isErasedJavaScriptDeclaration(node) ||
|
||||
isUnmodeledJavaScriptRuntimeScope(node);
|
||||
if (
|
||||
javaScriptRuntimeContext(state.program) !== "module" &&
|
||||
t.isFunctionDeclaration(node) &&
|
||||
node.id != null &&
|
||||
!hoistedFunction(node)
|
||||
)
|
||||
blockFunctionNames.add(node.id.name);
|
||||
},
|
||||
});
|
||||
return blocked;
|
||||
};
|
||||
const shape = (input: t.Node): Shape => {
|
||||
@@ -103,6 +104,7 @@ export const createSemanticExpressionShapeResolver = (
|
||||
// assignment. Keep same-named vars unknown rather than model that copy.
|
||||
if (
|
||||
namedProofBlocked() ||
|
||||
semanticOpaqueBindingReadMayHaveChanged(binding, node, state) ||
|
||||
(binding.declarationKinds.has("var") &&
|
||||
blockFunctionNames.has(binding.name))
|
||||
)
|
||||
@@ -128,7 +130,9 @@ export const createSemanticExpressionShapeResolver = (
|
||||
const { node } = unwrapJavaScriptExpression(input);
|
||||
return (
|
||||
isNullishJavaScriptSyntax(node) ||
|
||||
(isUnshadowedGlobal(node, state, "undefined") && !namedProofBlocked())
|
||||
(isUnshadowedGlobal(node, state, "undefined") &&
|
||||
!namedProofBlocked() &&
|
||||
!semanticOpaqueNameMayHaveChanged(node, state))
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -896,6 +896,21 @@ export const semanticBindingPrecedesCapture = (
|
||||
);
|
||||
};
|
||||
|
||||
/** A preceding initializer and capture in one body precede its later effect. */
|
||||
export const semanticBindingEffectFollowsCapture = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
capture: t.Node,
|
||||
effect: t.Node,
|
||||
parents: WeakMap<t.Node, t.Node>,
|
||||
): boolean => {
|
||||
if (!semanticBindingPrecedesCapture(binding, capture, parents)) return false;
|
||||
const captured = semanticCapturePosition(capture, parents);
|
||||
const written = directMutationPosition(effect, parents);
|
||||
return (
|
||||
captured !== null && written !== null && positionPrecedes(captured, written)
|
||||
);
|
||||
};
|
||||
|
||||
/** Ignore later escapes only while reading earlier references in one activation. */
|
||||
export const semanticEscapeFollowsCapture = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import * as t from "@babel/types";
|
||||
import { unwrapJavaScriptExpression } from "./javascriptAstValues.js";
|
||||
import {
|
||||
semanticEffectTiming,
|
||||
semanticBindingEffectFollowsCapture,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import {
|
||||
isUnshadowedGlobal,
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
|
||||
/** One source owns sites and the minimal scope tree reaching them; O(AST) storage. */
|
||||
export interface JavaScriptOpaqueScopeEffects {
|
||||
readonly sites: WeakSet<t.Node>;
|
||||
readonly sloppySites: WeakSet<t.Node>;
|
||||
readonly byScope: Map<string, t.CallExpression[]>;
|
||||
readonly children: Map<string, Set<string>>;
|
||||
}
|
||||
|
||||
export const indexSemanticOpaqueEffects = (
|
||||
candidates: readonly { node: t.CallExpression; scopeId: string }[],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
for (const { node, scopeId } of candidates) {
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
if (!isUnshadowedGlobal(callee, state, "eval")) continue;
|
||||
state.opaqueEffects.sites.add(node);
|
||||
if (!strictEval(node, state)) state.opaqueEffects.sloppySites.add(node);
|
||||
const sites = state.opaqueEffects.byScope.get(scopeId) ?? [];
|
||||
sites.push(node);
|
||||
state.opaqueEffects.byScope.set(scopeId, sites);
|
||||
let scope = state.scopesById.get(scopeId);
|
||||
while (scope?.parentScopeId != null) {
|
||||
const children =
|
||||
state.opaqueEffects.children.get(scope.parentScopeId) ??
|
||||
new Set<string>();
|
||||
if (children.has(scope.scopeId)) break;
|
||||
children.add(scope.scopeId);
|
||||
state.opaqueEffects.children.set(scope.parentScopeId, children);
|
||||
scope = state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Rebinding requires lexical visibility. Object references can remain reachable
|
||||
* through aliases even where the original name is shadowed or out of block scope.
|
||||
* Without a disjointness proof, consider their enclosing activation and closures.
|
||||
* Queries retain no binding-by-eval table. */
|
||||
export function* semanticOpaqueBindingEffects(
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
kind: "binding" | "reference" = "binding",
|
||||
): Generator<t.CallExpression> {
|
||||
if (state.opaqueEffects.byScope.size === 0) return;
|
||||
const pending = [
|
||||
kind === "reference" ? activation(binding.scopeId, state) : binding.scopeId,
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const scope = pending.pop()!;
|
||||
for (const node of state.opaqueEffects.byScope.get(scope) ?? [])
|
||||
if (
|
||||
kind === "reference" ||
|
||||
resolveSemanticBindingState(state, node, binding.name)?.bindingId ===
|
||||
binding.bindingId
|
||||
)
|
||||
yield node;
|
||||
for (const child of state.opaqueEffects.children.get(scope) ?? [])
|
||||
pending.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
export const semanticBindingMayHaveOpaqueEffect = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
read?: t.Node,
|
||||
kind: "binding" | "reference" = "binding",
|
||||
): boolean => {
|
||||
if (state.opaqueEffects.byScope.size === 0) return false;
|
||||
for (const effect of semanticOpaqueBindingEffects(binding, state, kind))
|
||||
if (
|
||||
read === undefined ||
|
||||
(semanticEffectTiming(read, effect, state) !== "after" &&
|
||||
!semanticBindingEffectFollowsCapture(
|
||||
binding,
|
||||
read,
|
||||
effect,
|
||||
state.parentsByNode,
|
||||
))
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Opaque eval may introduce an otherwise unbound name in a visible activation. */
|
||||
export const semanticOpaqueNameMayHaveChanged = (
|
||||
read: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
binding?: JavaScriptSemanticBindingState,
|
||||
): boolean => {
|
||||
if (state.opaqueEffects.byScope.size === 0) return false;
|
||||
const visible = new Set<string>();
|
||||
let node: t.Node | undefined = read;
|
||||
while (node !== undefined) {
|
||||
const scope = state.scopeByNode.get(node);
|
||||
if (scope !== undefined) {
|
||||
let current: typeof scope | undefined = scope;
|
||||
while (current !== undefined) {
|
||||
visible.add(current.scopeId);
|
||||
current =
|
||||
current.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(current.parentScopeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
node = state.parentsByNode.get(node);
|
||||
}
|
||||
for (const [scopeId, sites] of state.opaqueEffects.byScope) {
|
||||
let scope = state.scopesById.get(scopeId);
|
||||
while (
|
||||
scope !== undefined &&
|
||||
scope.kind !== "function" &&
|
||||
scope.kind !== "program"
|
||||
)
|
||||
scope =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
if (
|
||||
scope !== undefined &&
|
||||
visible.has(scope.scopeId) &&
|
||||
(binding === undefined ||
|
||||
activation(binding.scopeId, state) !== scope.scopeId) &&
|
||||
sites.some(
|
||||
(effect) =>
|
||||
state.opaqueEffects.sloppySites.has(effect) &&
|
||||
(binding === undefined ||
|
||||
resolveSemanticBindingState(state, effect, binding.name)
|
||||
?.bindingId === binding.bindingId) &&
|
||||
semanticEffectTiming(read, effect, state) !== "after",
|
||||
)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Declaration assignability is independent of observed explicit writes. */
|
||||
export const semanticBindingCanBeRebound = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): boolean =>
|
||||
binding.mutable ||
|
||||
(binding.definitions.some(
|
||||
({ kind }) => kind === "function" || kind === "class",
|
||||
) &&
|
||||
binding.initializers.some(
|
||||
({ node }) => t.isFunctionDeclaration(node) || t.isClassDeclaration(node),
|
||||
));
|
||||
|
||||
export const semanticOpaqueBindingReadMayHaveChanged = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
read: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
(semanticBindingCanBeRebound(binding) &&
|
||||
semanticBindingMayHaveOpaqueEffect(binding, state, read)) ||
|
||||
semanticOpaqueNameMayHaveChanged(read, state, binding);
|
||||
|
||||
const activation = (
|
||||
scopeId: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): string => {
|
||||
let scope = state.scopesById.get(scopeId);
|
||||
while (
|
||||
scope !== undefined &&
|
||||
scope.kind !== "function" &&
|
||||
scope.kind !== "program" &&
|
||||
scope.parentScopeId !== null
|
||||
)
|
||||
scope = state.scopesById.get(scope.parentScopeId);
|
||||
return scope?.scopeId ?? scopeId;
|
||||
};
|
||||
|
||||
const strictEval = (
|
||||
site: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
for (
|
||||
let node: t.Node | undefined = site;
|
||||
node !== undefined;
|
||||
node = state.parentsByNode.get(node)
|
||||
) {
|
||||
if (t.isClass(node) || (t.isProgram(node) && node.sourceType === "module"))
|
||||
return true;
|
||||
const directives = t.isProgram(node)
|
||||
? node.directives
|
||||
: t.isFunction(node) && t.isBlockStatement(node.body)
|
||||
? node.body.directives
|
||||
: [];
|
||||
if (directives.some(({ value }) => value.value === "use strict"))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptOpaqueScopeEffects } from "./javascriptSemanticOpaqueEffects.js";
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
@@ -101,6 +102,7 @@ export interface JavaScriptSemanticScopeState {
|
||||
/** Shared state for semantic collection and evaluation. */
|
||||
export interface JavaScriptSemanticAnalysisState {
|
||||
readonly program: t.Program;
|
||||
readonly opaqueEffects: JavaScriptOpaqueScopeEffects;
|
||||
/** Effective loader for static imports in this source artifact. */
|
||||
readonly staticImportResolutionMode: JavaScriptModuleResolutionMode;
|
||||
/** Bounds encountered during this analysis, independent of later projections. */
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
semanticBindingMayHaveOpaqueEffect,
|
||||
semanticBindingCanBeRebound,
|
||||
semanticOpaqueBindingReadMayHaveChanged,
|
||||
} from "./javascriptSemanticOpaqueEffects.js";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import {
|
||||
@@ -196,14 +201,20 @@ export const evaluateSemanticBinding = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticValue =>
|
||||
evaluateBinding(binding, {
|
||||
state,
|
||||
bindings: new Map(),
|
||||
expressionDepth: 0,
|
||||
primitiveBindingValues: primitiveBindingValuesFor(state),
|
||||
dependence: { lowest: CALLER_INDEPENDENT },
|
||||
memo: createEvaluationMemo(state),
|
||||
});
|
||||
semanticBindingCanBeRebound(binding) &&
|
||||
semanticBindingMayHaveOpaqueEffect(binding, state)
|
||||
? {
|
||||
status: "unknown",
|
||||
reason: "Direct eval may have changed this lexical binding.",
|
||||
}
|
||||
: evaluateBinding(binding, {
|
||||
state,
|
||||
bindings: new Map(),
|
||||
expressionDepth: 0,
|
||||
primitiveBindingValues: primitiveBindingValuesFor(state),
|
||||
dependence: { lowest: CALLER_INDEPENDENT },
|
||||
memo: createEvaluationMemo(state),
|
||||
});
|
||||
|
||||
/** Evaluate an inert expression, optionally at an earlier reference capture. */
|
||||
export const evaluateSemanticExpression = (
|
||||
@@ -245,8 +256,8 @@ export const evaluateSemanticExpressionProvenance = (
|
||||
export const evaluateSemanticProvenance = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptBindingProvenance =>
|
||||
provenanceForBinding(binding, {
|
||||
): JavaScriptBindingProvenance => {
|
||||
const provenance = provenanceForBinding(binding, {
|
||||
state,
|
||||
bindings: new Map(),
|
||||
expressionDepth: 0,
|
||||
@@ -254,6 +265,14 @@ export const evaluateSemanticProvenance = (
|
||||
dependence: { lowest: CALLER_INDEPENDENT },
|
||||
memo: createEvaluationMemo(state),
|
||||
});
|
||||
return semanticBindingCanBeRebound(binding) &&
|
||||
semanticBindingMayHaveOpaqueEffect(binding, state)
|
||||
? semanticAmbiguousProvenance(
|
||||
provenance.origins,
|
||||
"Direct eval may have changed this runtime value.",
|
||||
)
|
||||
: provenance;
|
||||
};
|
||||
|
||||
/** Follow a namespace binding as it was read at a specific source position. */
|
||||
export const evaluateSemanticProvenanceAtRead = (
|
||||
@@ -509,6 +528,19 @@ const evaluateBinding = (
|
||||
continue;
|
||||
projected = invalidateSemanticEscapedPath(projected, escape.path);
|
||||
}
|
||||
if (
|
||||
(projected.status === "object" || projected.status === "array") &&
|
||||
semanticBindingMayHaveOpaqueEffect(
|
||||
binding,
|
||||
context.state,
|
||||
context.capturePoint,
|
||||
"reference",
|
||||
)
|
||||
)
|
||||
projected = {
|
||||
status: "unknown",
|
||||
reason: "Direct eval may have mutated this object reference.",
|
||||
};
|
||||
if (
|
||||
callerIndependent &&
|
||||
independent !== undefined &&
|
||||
@@ -606,6 +638,17 @@ const evaluateExpressionValue = (
|
||||
};
|
||||
}
|
||||
const atRead = read?.binding ?? binding;
|
||||
if (
|
||||
semanticOpaqueBindingReadMayHaveChanged(
|
||||
binding,
|
||||
context.capturePoint ?? node,
|
||||
context.state,
|
||||
)
|
||||
)
|
||||
return {
|
||||
status: "unknown",
|
||||
reason: "Direct eval may have changed this lexical binding.",
|
||||
};
|
||||
return evaluateBinding(atRead, nestedContext(context), atRead === binding);
|
||||
}
|
||||
if (t.isTemplateLiteral(node)) return evaluateTemplate(node, context);
|
||||
@@ -1150,6 +1193,11 @@ const provenanceForBindingAtRead = (
|
||||
read: t.Identifier,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptBindingProvenance => {
|
||||
if (semanticOpaqueBindingReadMayHaveChanged(binding, read, context.state))
|
||||
return semanticAmbiguousProvenance(
|
||||
provenanceForBinding(binding, context).origins,
|
||||
"Direct eval may have changed this runtime value.",
|
||||
);
|
||||
const { binding: snapshot } = semanticBindingAtRead(
|
||||
binding,
|
||||
read,
|
||||
|
||||
@@ -13,6 +13,7 @@ it.each([
|
||||
it.each([
|
||||
'new Database("shared");',
|
||||
'import type Database = require("better-sqlite3"); new Database("shared");',
|
||||
'let Database = require("better-sqlite3"); eval("Database = Other"); new Database("shared");',
|
||||
'import * as sqlite from "sqlite3"; const alias = sqlite; alias.Database = Other; new sqlite.Database("shared");',
|
||||
'import Database from "better-sqlite3"; expose(Database); new Database("shared");',
|
||||
'import type TypeDatabase from "better-sqlite3"; const Database = TypeDatabase; new Database("shared");',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect } from "vitest";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { verifyJavaScriptReturnShapes } from "../../fixtures/javascriptReturnShapes.js";
|
||||
|
||||
cliTest(
|
||||
"retains eval uncertainty through CLI and stdio MCP return projections",
|
||||
async ({ cli }) => {
|
||||
await verifyJavaScriptReturnShapes(
|
||||
cli,
|
||||
`
|
||||
export function direct() { let value=1; eval("value=2"); return {result:value}; }
|
||||
export function member() { const value={n:1}; eval("value.n=2"); return {result:value.n}; }
|
||||
export function alias() { const value={n:1}; const copied=value; eval("value.n=2"); return {result:copied.n}; }
|
||||
export function shadowedAlias() { const value={n:1};const copied=value; {const value=0;eval("copied.n=2");} return {result:value.n}; }
|
||||
export function nested() { let value=1; change(); return {result:value}; function change(){eval("value=2");} }
|
||||
export function indirect() { let value=1; (0,eval)("value=2"); return {result:value}; }
|
||||
export function optional() { let value=1; eval?.("value=2"); return {result:value}; }
|
||||
export function captured() { let value=1; const saved=value; eval("value=2"); return {result:saved}; }
|
||||
export function unrelated() { let value=1; return {result:value}; }
|
||||
`,
|
||||
(fields) => {
|
||||
for (const name of [
|
||||
"direct",
|
||||
"member",
|
||||
"alias",
|
||||
"nested",
|
||||
"shadowedAlias",
|
||||
])
|
||||
expect(fields(name)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: "/result",
|
||||
state: "unknown",
|
||||
value: null,
|
||||
}),
|
||||
);
|
||||
for (const name of ["indirect", "optional", "captured", "unrelated"])
|
||||
expect(fields(name)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: "/result",
|
||||
state: "literal",
|
||||
value: 1,
|
||||
}),
|
||||
);
|
||||
},
|
||||
"app.mjs",
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user