fix(javascript): retain uncertainty for conditionally skipped writes
Own skipped-write interpretation at definition collection, including conditional branches, loop iterations and optional-chain arguments. Remove the narrower initializer-only representation. Fixes #2562
This commit is contained in:
1 parent
36c91428fc
commit
410537667e
7 files changed
+172
-33
No files matched your search
@@ -685,7 +685,7 @@ const createState = (
|
||||
moduleLinks: [],
|
||||
linkedRequireCalls: new WeakSet(),
|
||||
moduleLinkBindings: new WeakMap(),
|
||||
conditionalInitializers: new WeakSet(),
|
||||
conditionallySkippedExpressions: new WeakSet(),
|
||||
requiredModuleEffects: new Map(),
|
||||
ambientBindingWrites: new Map(),
|
||||
ambientWrites: new SemanticPropertyPathCoverage(),
|
||||
@@ -704,6 +704,7 @@ function* collectDefinitionsSteps(
|
||||
const assignments: {
|
||||
readonly node: t.Node;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly conditionallySkipped: boolean;
|
||||
}[] = [];
|
||||
const functionBodies: {
|
||||
readonly body: t.BlockStatement;
|
||||
@@ -779,16 +780,23 @@ function* collectDefinitionsSteps(
|
||||
node.init != null &&
|
||||
parent !== null &&
|
||||
t.isVariableDeclaration(parent, { kind: "var" }) &&
|
||||
hasConditionalInitializer(readAncestors())
|
||||
writeMayBeSkipped(node, readAncestors())
|
||||
)
|
||||
state.conditionalInitializers.add(node.init);
|
||||
state.conditionallySkippedExpressions.add(node.init);
|
||||
if (
|
||||
t.isAssignmentExpression(node) ||
|
||||
t.isUpdateExpression(node) ||
|
||||
t.isForOfStatement(node) ||
|
||||
t.isForInStatement(node)
|
||||
)
|
||||
assignments.push({ node, scope });
|
||||
assignments.push({
|
||||
node,
|
||||
scope,
|
||||
conditionallySkipped:
|
||||
t.isForOfStatement(node) ||
|
||||
t.isForInStatement(node) ||
|
||||
writeMayBeSkipped(node, readAncestors()),
|
||||
});
|
||||
},
|
||||
exit: (node) => {
|
||||
for (let count = openedScopes.get(node) ?? 0; count > 0; count--)
|
||||
@@ -797,9 +805,12 @@ function* collectDefinitionsSteps(
|
||||
});
|
||||
// All declarations must exist before resolving writes, including hoisted
|
||||
// functions and local declarations that shadow an outer binding.
|
||||
for (const [index, { node, scope }] of assignments.entries()) {
|
||||
for (const [
|
||||
index,
|
||||
{ node, scope, conditionallySkipped },
|
||||
] of assignments.entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
bindAssignment(node, scope, state);
|
||||
bindAssignment(node, scope, state, conditionallySkipped);
|
||||
}
|
||||
for (const [index, { body, scope, parameters }] of functionBodies.entries()) {
|
||||
if (index % 64 === 0) yield;
|
||||
@@ -853,29 +864,72 @@ const copyParameterToBody = (input: {
|
||||
}
|
||||
};
|
||||
|
||||
const hasConditionalInitializer = (ancestors: readonly t.Node[]): boolean => {
|
||||
// A hoisted var can remain undefined when control flow skips its initializer.
|
||||
// Conditions outside the owning callable/static block do not govern its body.
|
||||
const writeMayBeSkipped = (
|
||||
node: t.Node,
|
||||
ancestors: readonly t.Node[],
|
||||
): boolean => {
|
||||
let child = node;
|
||||
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
||||
const node = ancestors[index];
|
||||
if (t.isFunction(node) || t.isProgram(node) || t.isStaticBlock(node))
|
||||
const parent = ancestors[index];
|
||||
if (
|
||||
parent === undefined ||
|
||||
t.isFunction(parent) ||
|
||||
t.isProgram(parent) ||
|
||||
t.isStaticBlock(parent)
|
||||
)
|
||||
return false;
|
||||
if (t.isIfStatement(node) || t.isSwitchCase(node) || t.isTryStatement(node))
|
||||
return true;
|
||||
if (t.isLoop(node)) {
|
||||
const bodyOrTest = ancestors[index + 1];
|
||||
if (
|
||||
(t.isForStatement(node) && bodyOrTest === node.init) ||
|
||||
(t.isDoWhileStatement(node) &&
|
||||
(bodyOrTest === node.body || bodyOrTest === node.test))
|
||||
)
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
if (isSkippedBranch(parent, child)) return true;
|
||||
child = parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSkippedBranch = (parent: t.Node, child: t.Node): boolean => {
|
||||
if (t.isIfStatement(parent))
|
||||
return child === parent.consequent || child === parent.alternate;
|
||||
if (t.isSwitchCase(parent)) return true;
|
||||
if (t.isTryStatement(parent))
|
||||
return child === parent.block || child === parent.handler;
|
||||
if (t.isForStatement(parent))
|
||||
return child === parent.body || child === parent.update;
|
||||
if (t.isWhileStatement(parent)) return child === parent.body;
|
||||
if (t.isDoWhileStatement(parent)) return false;
|
||||
if (t.isForInStatement(parent) || t.isForOfStatement(parent))
|
||||
return child === parent.left || child === parent.body;
|
||||
if (t.isLogicalExpression(parent)) return child === parent.right;
|
||||
if (t.isConditionalExpression(parent))
|
||||
return child === parent.consequent || child === parent.alternate;
|
||||
if (t.isOptionalCallExpression(parent) || t.isCallExpression(parent))
|
||||
return (
|
||||
parent.arguments.includes(child as t.Expression) &&
|
||||
optionalCallArgumentsMayBeSkipped(parent)
|
||||
);
|
||||
if (t.isOptionalMemberExpression(parent))
|
||||
return parent.optional && parent.computed && child === parent.property;
|
||||
return false;
|
||||
};
|
||||
|
||||
const optionalCallArgumentsMayBeSkipped = (
|
||||
call: t.CallExpression | t.OptionalCallExpression,
|
||||
): boolean => {
|
||||
if (t.isOptionalCallExpression(call) && call.optional) return true;
|
||||
let callee: t.Node = call.callee;
|
||||
while (true) {
|
||||
// Parentheses terminate optional-chain short-circuiting. In particular,
|
||||
// `(foo?.bar)(arg)` evaluates arg before attempting the call.
|
||||
if (callee.extra?.parenthesized === true) return false;
|
||||
if (t.isOptionalMemberExpression(callee)) {
|
||||
if (callee.optional) return true;
|
||||
callee = callee.object;
|
||||
} else if (t.isMemberExpression(callee)) callee = callee.object;
|
||||
else if (t.isOptionalCallExpression(callee)) {
|
||||
if (callee.optional) return true;
|
||||
callee = callee.callee;
|
||||
} else if (t.isCallExpression(callee)) callee = callee.callee;
|
||||
else return false;
|
||||
}
|
||||
};
|
||||
|
||||
const bindOuterDeclaration = (
|
||||
node: t.Node,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
@@ -983,6 +1037,7 @@ const bindAssignment = (
|
||||
node: t.Node,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
conditionallySkipped: boolean,
|
||||
): void => {
|
||||
if (t.isAssignmentExpression(node)) {
|
||||
const left = unwrapJavaScriptExpression(node.left).node;
|
||||
@@ -996,21 +1051,23 @@ const bindAssignment = (
|
||||
: node,
|
||||
scope,
|
||||
state,
|
||||
conditionallySkipped,
|
||||
);
|
||||
else
|
||||
for (const identifier of semanticWriteTargets(node.left))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
addAssignment(identifier, node, scope, state, conditionallySkipped);
|
||||
} else if (t.isUpdateExpression(node)) {
|
||||
const argument = unwrapJavaScriptExpression(node.argument).node;
|
||||
if (t.isIdentifier(argument)) addAssignment(argument, node, scope, state);
|
||||
if (t.isIdentifier(argument))
|
||||
addAssignment(argument, node, scope, state, conditionallySkipped);
|
||||
} else if (t.isForOfStatement(node) || t.isForInStatement(node))
|
||||
for (const pattern of t.isVariableDeclaration(node.left)
|
||||
? node.left.declarations.map(({ id }) => id)
|
||||
: [node.left]) {
|
||||
for (const identifier of semanticWriteTargets(pattern))
|
||||
if (t.isIdentifier(identifier))
|
||||
addAssignment(identifier, node, scope, state);
|
||||
addAssignment(identifier, node, scope, state, conditionallySkipped);
|
||||
if (t.isForOfStatement(node))
|
||||
// Keep the loop value unknown, but preserve references yielded into
|
||||
// each bound slot so writes/escapes can reach their iterable origins.
|
||||
@@ -1400,6 +1457,7 @@ const addAssignment = (
|
||||
initializer: t.Node,
|
||||
scope: JavaScriptSemanticScopeState,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
conditionallySkipped: boolean,
|
||||
): void => {
|
||||
const binding = resolveSemanticBindingFromScope(
|
||||
scope,
|
||||
@@ -1417,4 +1475,6 @@ const addAssignment = (
|
||||
binding.mutable = true;
|
||||
binding.definitions.push({ kind: "assignment", location: range(identifier) });
|
||||
binding.initializers.push({ node: initializer, projection: [] });
|
||||
if (conditionallySkipped)
|
||||
state.conditionallySkippedExpressions.add(initializer);
|
||||
};
|
||||
Reference in new issue
Block a user