mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
refactor(javascript): classify implicit execution in one owner (#10605)
* refactor(javascript): classify implicit execution in one owner Add javascriptSemanticExecutionPoints, a total classification of every runtime Babel node type into the ways its evaluation can run code without call syntax. The member-mutation escape walk, the accessor operand collector and the catch-source reachability check consult it instead of their own syntax lists. Kinds those collectors have never applied are now named in exhaustive switches rather than absent. A guard test fails when the installed @babel/types defines a node type the table does not classify, and unknown node types classify as unsupported syntax rather than inert. * test(hopper): retry cleanup after confirmed application exit * fix(javascript): classify enumeration and catch binding effects * fix(javascript): retain catch uncertainty for identifier reads
This commit is contained in:
1 parent
4502bd44bb
commit
a874fcc4e3
6 files changed
+947
-240
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import * as t from "@babel/types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import {
|
||||
javaScriptClassifiedNodeTypes,
|
||||
javaScriptExecutions,
|
||||
javaScriptTypeOnlyAliases,
|
||||
} from "./javascriptSemanticExecutionPoints.js";
|
||||
import { childNodes } from "./javascriptSemanticTraversal.js";
|
||||
import { semanticPropertyReadSources } from "./javascriptSemanticIterationSources.js";
|
||||
|
||||
const installedNodeTypes = [
|
||||
...new Set([...Object.keys(t.VISITOR_KEYS), ...Object.keys(t.NODE_FIELDS)]),
|
||||
].sort();
|
||||
|
||||
const typeOnly = new Set<string>(
|
||||
javaScriptTypeOnlyAliases.flatMap(
|
||||
(alias) => t.FLIPPED_ALIAS_KEYS[alias] ?? [],
|
||||
),
|
||||
);
|
||||
|
||||
describe("JavaScript execution classification", () => {
|
||||
it("classifies every installed Babel node type", () => {
|
||||
const classified = new Set(javaScriptClassifiedNodeTypes());
|
||||
const unclassified = installedNodeTypes.filter(
|
||||
(type) =>
|
||||
!classified.has(type) &&
|
||||
(!typeOnly.has(type) || type === "EnumDeclaration"),
|
||||
);
|
||||
expect(unclassified).toEqual([]);
|
||||
});
|
||||
|
||||
it("names no node type the installed Babel version lacks", () => {
|
||||
const known = new Set([
|
||||
...installedNodeTypes,
|
||||
...Object.keys(t.DEPRECATED_KEYS),
|
||||
]);
|
||||
expect(
|
||||
javaScriptClassifiedNodeTypes().filter((type) => !known.has(type)),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats a node type it does not know as code that may run", () => {
|
||||
const future = { type: "FutureExpression" } as unknown as t.Node;
|
||||
expect(javaScriptExecutions(future, null)).toMatchObject([
|
||||
{ kind: "unsupported-syntax", node: future },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const parsed = (source: string): t.Program => {
|
||||
const file = parseJavaScriptSource(source, "classification.jsx");
|
||||
if (file === null) throw new Error(`Fixture did not parse: ${source}`);
|
||||
return file.program;
|
||||
};
|
||||
|
||||
/** Kinds per node type for every node in one parsed program. */
|
||||
const kindsByType = (source: string): Map<string, string[]> => {
|
||||
const kinds = new Map<string, string[]>();
|
||||
const pending: { node: t.Node; parent: t.Node | null }[] = [
|
||||
{ node: parsed(source), parent: null },
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const { node, parent } = pending.pop()!;
|
||||
for (const { kind } of javaScriptExecutions(node, parent))
|
||||
kinds.set(node.type, [...(kinds.get(node.type) ?? []), kind]);
|
||||
for (const child of childNodes(node))
|
||||
pending.push({ node: child, parent: node });
|
||||
}
|
||||
return kinds;
|
||||
};
|
||||
|
||||
it.each([
|
||||
["value === other; !value; typeof value; void value; value && other;", {}],
|
||||
["value + other;", { BinaryExpression: ["coercion"] }],
|
||||
["`${value}`;", { TemplateLiteral: ["coercion"] }],
|
||||
["tag`${value}`;", { TaggedTemplateExpression: ["tagged-template"] }],
|
||||
["value.key;", { MemberExpression: ["property-read"] }],
|
||||
[
|
||||
"value.key = 1;",
|
||||
{
|
||||
AssignmentExpression: ["property-write"],
|
||||
MemberExpression: ["property-read"],
|
||||
},
|
||||
],
|
||||
["binding = value;", {}],
|
||||
["const { key } = value;", { VariableDeclarator: ["destructuring"] }],
|
||||
["const [first] = value;", { VariableDeclarator: ["iteration"] }],
|
||||
["const copy = [...value];", { SpreadElement: ["iteration"] }],
|
||||
["const copy = { ...value };", { SpreadElement: ["property-copy"] }],
|
||||
["for (const item of value);", { ForOfStatement: ["iteration"] }],
|
||||
["for (const key in value);", { ForInStatement: ["enumeration"] }],
|
||||
[
|
||||
"try { throw source; } catch ({ value }) {}",
|
||||
{ CatchClause: ["destructuring"] },
|
||||
],
|
||||
["try { throw source; } catch ([value]) {}", { CatchClause: ["iteration"] }],
|
||||
["async function f() { await value; }", { AwaitExpression: ["thenable"] }],
|
||||
["function* f() { yield value; }", { YieldExpression: ["suspension"] }],
|
||||
["function* f() { yield* value; }", { YieldExpression: ["iteration"] }],
|
||||
["{ using resource = value; }", { VariableDeclarator: ["disposal"] }],
|
||||
["class C extends Base {}", { ClassDeclaration: ["class-definition"] }],
|
||||
[
|
||||
"@decorate class C {}",
|
||||
{ ClassDeclaration: ["class-definition"], Decorator: ["decorator"] },
|
||||
],
|
||||
["import(value);", { ImportExpression: ["dynamic-import"] }],
|
||||
["with (value) key;", { WithStatement: ["dynamic-scope"] }],
|
||||
["<Element prop={value} />;", { JSXElement: ["jsx-element"] }],
|
||||
['import "side-effect";', { ImportDeclaration: ["module-evaluation"] }],
|
||||
["new Factory(value);", { NewExpression: ["construction"] }],
|
||||
["call(value);", { CallExpression: ["call"] }],
|
||||
])("classifies %s", (source, expected) => {
|
||||
expect(Object.fromEntries(kindsByType(source))).toEqual(expected);
|
||||
});
|
||||
|
||||
it("exposes the enumerated object", () => {
|
||||
const node = parsed("for (const key in value);").body[0];
|
||||
if (!t.isForInStatement(node)) throw new Error("Expected for-in statement");
|
||||
expect(javaScriptExecutions(node, null)).toContainEqual(
|
||||
expect.objectContaining({ kind: "enumeration", operands: [node.right] }),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["!trigger", "typeof trigger", "void trigger", "trigger === 1"])(
|
||||
"retains catch property reads when %s may invoke a global accessor",
|
||||
(expression) => {
|
||||
const statement = parsed(`try { ${expression}; } catch ({ value }) {}`)
|
||||
.body[0];
|
||||
if (!t.isTryStatement(statement) || statement.handler == null)
|
||||
throw new Error("Expected catch clause");
|
||||
expect(semanticPropertyReadSources(statement.handler, statement)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projection: expect.arrayContaining(["value"]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,576 @@
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
javaScriptComputedDefinitionKey,
|
||||
semanticWriteSiteReadsFirst,
|
||||
semanticWriteSiteTarget,
|
||||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
/**
|
||||
* How evaluating one syntax node can run code that the analyzed source does
|
||||
* not spell as a call. Each kind names the language mechanism; operands are
|
||||
* the values that mechanism exposes to the code it may run.
|
||||
*
|
||||
* - `call`, `construction`, `tagged-template`: explicit invocation. Direct
|
||||
* `eval` is a `call`; its rebinding authority belongs to the opaque-effect
|
||||
* owner because it depends on scope resolution, not syntax.
|
||||
* - `property-read`, `property-write`: accessors (and Proxy traps) on member
|
||||
* reads and on member or destructuring stores. Member nodes that are only
|
||||
* written are still listed as reads; the enclosing `property-write`
|
||||
* identifies them through its target.
|
||||
* - `destructuring`, `property-copy`: getters run by object patterns and by
|
||||
* object spread/rest. Projections belong to `semanticPropertyReadSources`.
|
||||
* - `iteration`: the iterator protocol (for-of, array spread, spread
|
||||
* arguments, array patterns, `yield*`).
|
||||
* - `enumeration`: for-in enumeration may invoke Proxy traps.
|
||||
* - `thenable`: `await` reads and calls `then` on its operand.
|
||||
* - `suspension`: `yield` hands its operand to whoever resumes the generator.
|
||||
* - `coercion`: user-defined primitive conversion and operator hooks
|
||||
* (`Symbol.toPrimitive`, `valueOf`, `toString`, `Symbol.hasInstance`).
|
||||
* - `disposal`: `using` declarations call `Symbol.dispose` at scope exit.
|
||||
* - `class-definition`: heritage, computed keys, static blocks and static
|
||||
* field values run when a class is defined.
|
||||
* - `decorator`: decorators are called with the decorated value.
|
||||
* - `dynamic-import`: `import()` converts its specifier and reads its options.
|
||||
* - `dynamic-scope`: identifier lookups inside `with` read object properties.
|
||||
* - `jsx-element`: JSX compiles to a call into the configured runtime.
|
||||
* - `module-evaluation`: static imports and re-exports evaluate other modules
|
||||
* before this module body runs.
|
||||
* - `unsupported-syntax`: proposal or flow-runtime syntax the parser does not
|
||||
* enable; it may run code in ways this table does not describe.
|
||||
*/
|
||||
export type JavaScriptExecutionKind =
|
||||
| "call"
|
||||
| "construction"
|
||||
| "tagged-template"
|
||||
| "property-read"
|
||||
| "property-write"
|
||||
| "destructuring"
|
||||
| "property-copy"
|
||||
| "iteration"
|
||||
| "enumeration"
|
||||
| "thenable"
|
||||
| "suspension"
|
||||
| "coercion"
|
||||
| "disposal"
|
||||
| "class-definition"
|
||||
| "decorator"
|
||||
| "dynamic-import"
|
||||
| "dynamic-scope"
|
||||
| "jsx-element"
|
||||
| "module-evaluation"
|
||||
| "unsupported-syntax";
|
||||
|
||||
export type JavaScriptExecution =
|
||||
| {
|
||||
readonly kind: "property-write";
|
||||
readonly node: t.Node;
|
||||
readonly operands: readonly t.Node[];
|
||||
/** The direct or destructuring target, from the write-site owner. */
|
||||
readonly target: t.Node;
|
||||
/** Whether the site reads the previous target value before writing. */
|
||||
readonly readsFirst: boolean;
|
||||
}
|
||||
| {
|
||||
readonly kind: Exclude<JavaScriptExecutionKind, "property-write">;
|
||||
readonly node: t.Node;
|
||||
readonly operands: readonly t.Node[];
|
||||
};
|
||||
|
||||
export interface JavaScriptExecutionOptions {
|
||||
/**
|
||||
* Proof that an operand is an object. Loose equality between two objects
|
||||
* compares identity and performs no conversion.
|
||||
*/
|
||||
readonly isObject?: (operand: t.Node) => boolean;
|
||||
}
|
||||
|
||||
type Classifier<Node extends t.Node> = (
|
||||
node: Node,
|
||||
parent: t.Node | null,
|
||||
options: JavaScriptExecutionOptions,
|
||||
) => readonly JavaScriptExecution[];
|
||||
|
||||
/** Syntax with no runtime representation: TypeScript and Flow type nodes. */
|
||||
type TypeOnlyNode = Exclude<
|
||||
t.Flow | t.TSType | t.TSTypeElement,
|
||||
t.EnumDeclaration
|
||||
>;
|
||||
type ClassifiedType = Exclude<t.Node["type"], TypeOnlyNode["type"]>;
|
||||
|
||||
const typeOnlyAliases = ["Flow", "TSType", "TSTypeElement"] as const;
|
||||
|
||||
const none = (): readonly JavaScriptExecution[] => [];
|
||||
|
||||
const execution = (
|
||||
kind: Exclude<JavaScriptExecutionKind, "property-write">,
|
||||
node: t.Node,
|
||||
operands: readonly (t.Node | null | undefined)[],
|
||||
): JavaScriptExecution => ({
|
||||
kind,
|
||||
node,
|
||||
operands: operands.filter((operand): operand is t.Node => operand != null),
|
||||
});
|
||||
|
||||
const memberReceiver = (callee: t.Node): t.Node | undefined => {
|
||||
const expression = unwrapJavaScriptExpression(callee).node;
|
||||
return t.isMemberExpression(expression) ||
|
||||
t.isOptionalMemberExpression(expression)
|
||||
? expression.object
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const argumentValues = (
|
||||
values: readonly (t.Node | null)[],
|
||||
): readonly t.Node[] =>
|
||||
values.flatMap((value) =>
|
||||
value === null ? [] : [t.isSpreadElement(value) ? value.argument : value],
|
||||
);
|
||||
|
||||
/** Member stores may run setters on their receivers; binding stores cannot. */
|
||||
const writeSite = (node: t.Node): readonly JavaScriptExecution[] => {
|
||||
const target = semanticWriteSiteTarget(node);
|
||||
if (target === null) return [];
|
||||
const receivers = [...semanticWriteTargets(target)].flatMap((leaf) =>
|
||||
t.isIdentifier(leaf) ? [] : [leaf.object],
|
||||
);
|
||||
return receivers.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "property-write",
|
||||
node,
|
||||
target,
|
||||
readsFirst: semanticWriteSiteReadsFirst(node),
|
||||
operands: receivers,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** Pattern-owning sites evaluate getters or the iterator of their source. */
|
||||
const patternSource = (
|
||||
node: t.Node,
|
||||
pattern: t.Node,
|
||||
source: t.Node | null | undefined,
|
||||
): readonly JavaScriptExecution[] =>
|
||||
t.isObjectPattern(pattern)
|
||||
? [execution("destructuring", node, [source])]
|
||||
: t.isArrayPattern(pattern)
|
||||
? [execution("iteration", node, [source])]
|
||||
: [];
|
||||
|
||||
/** User-defined primitive conversion and operator hooks for one node. */
|
||||
const coercionOperands = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
isObject: (operand: t.Node) => boolean,
|
||||
): readonly t.Node[] => {
|
||||
if (t.isBinaryExpression(node)) {
|
||||
if (["===", "!=="].includes(node.operator)) return [];
|
||||
if (node.operator === "instanceof") return [node.left, node.right];
|
||||
if (
|
||||
["==", "!="].includes(node.operator) &&
|
||||
((t.isThisExpression(node.left) && t.isThisExpression(node.right)) ||
|
||||
(isObject(node.left) && isObject(node.right)))
|
||||
)
|
||||
return [];
|
||||
return node.operator === "in" ? [node.left] : [node.left, node.right];
|
||||
}
|
||||
if (t.isUnaryExpression(node) && ["+", "-", "~"].includes(node.operator))
|
||||
return [node.argument];
|
||||
if (t.isUpdateExpression(node)) return [node.argument];
|
||||
if (
|
||||
t.isAssignmentExpression(node) &&
|
||||
!["=", "&&=", "||=", "??="].includes(node.operator)
|
||||
)
|
||||
return [node.left, node.right];
|
||||
if (
|
||||
(t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node) ||
|
||||
t.isObjectProperty(node) ||
|
||||
t.isObjectMethod(node) ||
|
||||
t.isClassProperty(node) ||
|
||||
t.isClassMethod(node) ||
|
||||
t.isClassAccessorProperty(node)) &&
|
||||
node.computed
|
||||
)
|
||||
return [
|
||||
t.isMemberExpression(node) || t.isOptionalMemberExpression(node)
|
||||
? node.property
|
||||
: node.key,
|
||||
];
|
||||
if (t.isTemplateLiteral(node) && !t.isTaggedTemplateExpression(parent))
|
||||
return node.expressions;
|
||||
return [];
|
||||
};
|
||||
|
||||
const coercion: Classifier<t.Node> = (node, parent, options) => {
|
||||
const operands = coercionOperands(
|
||||
node,
|
||||
parent,
|
||||
options.isObject ?? (() => false),
|
||||
);
|
||||
return operands.length === 0 ? [] : [execution("coercion", node, operands)];
|
||||
};
|
||||
|
||||
/** Spread arguments are iterated before the call; consumers that model the
|
||||
* iterator protocol read them from the invocation node. */
|
||||
const call: Classifier<t.CallExpression | t.OptionalCallExpression> = (
|
||||
node,
|
||||
) => [
|
||||
execution("call", node, [
|
||||
memberReceiver(node.callee),
|
||||
...argumentValues(node.arguments),
|
||||
]),
|
||||
];
|
||||
|
||||
const member: Classifier<t.MemberExpression | t.OptionalMemberExpression> = (
|
||||
node,
|
||||
parent,
|
||||
options,
|
||||
) => [
|
||||
execution("property-read", node, [node.object]),
|
||||
...coercion(node, parent, options),
|
||||
];
|
||||
|
||||
const computedMember: Classifier<t.Node> = coercion;
|
||||
|
||||
/** Members whose computed keys a definition evaluates and converts immediately. */
|
||||
const computedDefinitionMembers = (node: t.Function | t.Class): t.Node[] => {
|
||||
if (t.isObjectMethod(node))
|
||||
return javaScriptComputedDefinitionKey(node, node.key) ? [node] : [];
|
||||
if (!t.isClass(node)) return [];
|
||||
return node.body.body.filter(
|
||||
(member) =>
|
||||
"key" in member && javaScriptComputedDefinitionKey(member, member.key),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Expressions a definition runs immediately: heritage, computed keys, static
|
||||
* blocks and static field values. Deferred bodies and instance initializers
|
||||
* stay out; `this` inside static work is the class, not the enclosing receiver.
|
||||
*/
|
||||
export const javaScriptDefinitionTimeExpressions = (
|
||||
node: t.Function | t.Class,
|
||||
): t.Node[] => [
|
||||
...(t.isClass(node) && node.superClass != null ? [node.superClass] : []),
|
||||
...computedDefinitionMembers(node).flatMap((member) =>
|
||||
"key" in member ? [member.key] : [],
|
||||
),
|
||||
...(t.isClass(node)
|
||||
? node.body.body.flatMap((member): t.Node[] =>
|
||||
t.isStaticBlock(member)
|
||||
? [member]
|
||||
: (t.isClassProperty(member) || t.isClassPrivateProperty(member)) &&
|
||||
member.static &&
|
||||
member.value != null
|
||||
? [member.value]
|
||||
: [],
|
||||
)
|
||||
: []),
|
||||
];
|
||||
|
||||
/** Computed definition keys converted when a function or class is defined. */
|
||||
export const javaScriptDefinitionKeyCoercions = (
|
||||
node: t.Function | t.Class,
|
||||
): readonly t.Node[] =>
|
||||
computedDefinitionMembers(node).flatMap((member) =>
|
||||
coercionOperands(member, node, () => false),
|
||||
);
|
||||
|
||||
const classDefinition: Classifier<t.ClassDeclaration | t.ClassExpression> = (
|
||||
node,
|
||||
) => [
|
||||
// Evaluating heritage reads `prototype` from the superclass.
|
||||
execution("class-definition", node, [node.superClass]),
|
||||
];
|
||||
|
||||
const jsxElement: Classifier<t.JSXElement | t.JSXFragment> = (node) => {
|
||||
const operands: t.Node[] = [];
|
||||
const attributes = t.isJSXElement(node) ? node.openingElement.attributes : [];
|
||||
for (const attribute of attributes)
|
||||
if (t.isJSXSpreadAttribute(attribute)) operands.push(attribute.argument);
|
||||
else if (t.isJSXExpressionContainer(attribute.value))
|
||||
operands.push(attribute.value.expression);
|
||||
for (const child of node.children)
|
||||
if (t.isJSXExpressionContainer(child) || t.isJSXSpreadChild(child))
|
||||
operands.push(child.expression);
|
||||
return [
|
||||
execution(
|
||||
"jsx-element",
|
||||
node,
|
||||
operands.filter((operand) => !t.isJSXEmptyExpression(operand)),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const unsupported: Classifier<t.Node> = (node) => [
|
||||
execution("unsupported-syntax", node, []),
|
||||
];
|
||||
|
||||
const moduleEvaluation: Classifier<t.Node> = (node) => [
|
||||
execution("module-evaluation", node, []),
|
||||
];
|
||||
|
||||
/**
|
||||
* One classifier per runtime Babel node type. The mapped type makes a new
|
||||
* Babel node type a compile error; the guard test checks the installed
|
||||
* runtime definitions as well.
|
||||
*/
|
||||
const classifiers: {
|
||||
readonly [Type in ClassifiedType]: Classifier<
|
||||
Extract<t.Node, { type: Type }>
|
||||
>;
|
||||
} = {
|
||||
// Programs, statements and declarations without implicit work.
|
||||
File: none,
|
||||
Program: none,
|
||||
InterpreterDirective: none,
|
||||
Directive: none,
|
||||
DirectiveLiteral: none,
|
||||
BlockStatement: none,
|
||||
BreakStatement: none,
|
||||
ContinueStatement: none,
|
||||
DebuggerStatement: none,
|
||||
DoWhileStatement: none,
|
||||
EmptyStatement: none,
|
||||
ExpressionStatement: none,
|
||||
ForStatement: none,
|
||||
IfStatement: none,
|
||||
LabeledStatement: none,
|
||||
ReturnStatement: none,
|
||||
// Strict-equality case matching performs no conversion.
|
||||
SwitchStatement: none,
|
||||
SwitchCase: none,
|
||||
ThrowStatement: none,
|
||||
TryStatement: none,
|
||||
CatchClause: (node) =>
|
||||
node.param == null ? [] : patternSource(node, node.param, undefined),
|
||||
WhileStatement: none,
|
||||
VariableDeclaration: none,
|
||||
StaticBlock: none,
|
||||
ClassBody: none,
|
||||
// Literals and plain references.
|
||||
StringLiteral: none,
|
||||
NumericLiteral: none,
|
||||
NullLiteral: none,
|
||||
BooleanLiteral: none,
|
||||
RegExpLiteral: none,
|
||||
BigIntLiteral: none,
|
||||
TemplateElement: none,
|
||||
// A binding lookup itself is plain; `with` lookups are `dynamic-scope`.
|
||||
Identifier: none,
|
||||
ThisExpression: none,
|
||||
Super: none,
|
||||
MetaProperty: none,
|
||||
PrivateName: none,
|
||||
// Expressions whose own evaluation is plain; operands classify separately.
|
||||
ArrayExpression: none,
|
||||
ObjectExpression: none,
|
||||
ConditionalExpression: none,
|
||||
LogicalExpression: none,
|
||||
SequenceExpression: none,
|
||||
ParenthesizedExpression: none,
|
||||
ArrowFunctionExpression: none,
|
||||
FunctionDeclaration: none,
|
||||
FunctionExpression: none,
|
||||
ClassPrivateMethod: none,
|
||||
ClassPrivateProperty: none,
|
||||
// Pattern leaves; their sources are classified at the owning site.
|
||||
AssignmentPattern: none,
|
||||
ArrayPattern: none,
|
||||
ObjectPattern: none,
|
||||
RestElement: (node, parent) =>
|
||||
t.isObjectPattern(parent) ? [execution("property-copy", node, [])] : [],
|
||||
VoidPattern: none,
|
||||
// Module syntax.
|
||||
ImportDeclaration: moduleEvaluation,
|
||||
ExportAllDeclaration: moduleEvaluation,
|
||||
ExportNamedDeclaration: (node, parent, options) =>
|
||||
node.source == null ? none() : moduleEvaluation(node, parent, options),
|
||||
ExportDefaultDeclaration: none,
|
||||
ExportSpecifier: none,
|
||||
ExportDefaultSpecifier: none,
|
||||
ExportNamespaceSpecifier: none,
|
||||
ImportDefaultSpecifier: none,
|
||||
ImportNamespaceSpecifier: none,
|
||||
ImportSpecifier: none,
|
||||
ImportAttribute: none,
|
||||
Import: none,
|
||||
ImportExpression: (node) => [
|
||||
execution("dynamic-import", node, [node.source, node.options]),
|
||||
],
|
||||
// Explicit invocation.
|
||||
CallExpression: call,
|
||||
OptionalCallExpression: call,
|
||||
NewExpression: (node) => [
|
||||
execution("construction", node, argumentValues(node.arguments)),
|
||||
],
|
||||
TaggedTemplateExpression: (node) => [
|
||||
execution("tagged-template", node, [
|
||||
memberReceiver(node.tag),
|
||||
...node.quasi.expressions,
|
||||
]),
|
||||
],
|
||||
// The decorator expression is the callee; the decorated declaration escapes.
|
||||
Decorator: (node, parent) => [execution("decorator", node, [parent])],
|
||||
// Accessors, conversion and protocols.
|
||||
MemberExpression: member,
|
||||
OptionalMemberExpression: member,
|
||||
AssignmentExpression: (node, parent, options) => [
|
||||
...writeSite(node),
|
||||
...coercion(node, parent, options),
|
||||
...patternSource(node, node.left, node.right),
|
||||
],
|
||||
UpdateExpression: (node, parent, options) => [
|
||||
...writeSite(node),
|
||||
...coercion(node, parent, options),
|
||||
],
|
||||
UnaryExpression: (node, parent, options) =>
|
||||
node.operator === "delete"
|
||||
? writeSite(node)
|
||||
: coercion(node, parent, options),
|
||||
BinaryExpression: coercion,
|
||||
TemplateLiteral: coercion,
|
||||
ObjectProperty: computedMember,
|
||||
ObjectMethod: computedMember,
|
||||
ClassMethod: computedMember,
|
||||
ClassProperty: computedMember,
|
||||
ClassAccessorProperty: computedMember,
|
||||
ForInStatement: (node) => [
|
||||
...writeSite(node),
|
||||
execution("enumeration", node, [node.right]),
|
||||
],
|
||||
ForOfStatement: (node) => [
|
||||
...writeSite(node),
|
||||
execution(node.await ? "thenable" : "iteration", node, [node.right]),
|
||||
],
|
||||
SpreadElement: (node, parent) =>
|
||||
t.isObjectExpression(parent)
|
||||
? [execution("property-copy", node, [node.argument])]
|
||||
: t.isArrayExpression(parent)
|
||||
? [execution("iteration", node, [node.argument])]
|
||||
: // Call and construction arguments: classified by the invocation.
|
||||
[],
|
||||
VariableDeclarator: (node, parent) => [
|
||||
...(t.isVariableDeclaration(parent) &&
|
||||
(parent.kind === "using" || parent.kind === "await using")
|
||||
? [execution("disposal", node, [node.init ?? node.id])]
|
||||
: []),
|
||||
...patternSource(node, node.id, node.init),
|
||||
],
|
||||
AwaitExpression: (node) => [execution("thenable", node, [node.argument])],
|
||||
YieldExpression: (node) => [
|
||||
execution(node.delegate ? "iteration" : "suspension", node, [
|
||||
node.argument,
|
||||
]),
|
||||
],
|
||||
ClassDeclaration: classDefinition,
|
||||
ClassExpression: classDefinition,
|
||||
WithStatement: (node) => [execution("dynamic-scope", node, [node.object])],
|
||||
// JSX: the element is the runtime call; its parts are its operands.
|
||||
JSXElement: jsxElement,
|
||||
JSXFragment: jsxElement,
|
||||
JSXAttribute: none,
|
||||
JSXClosingElement: none,
|
||||
JSXEmptyExpression: none,
|
||||
JSXExpressionContainer: none,
|
||||
JSXSpreadChild: none,
|
||||
JSXIdentifier: none,
|
||||
JSXMemberExpression: none,
|
||||
JSXNamespacedName: none,
|
||||
JSXOpeningElement: none,
|
||||
JSXSpreadAttribute: none,
|
||||
JSXText: none,
|
||||
JSXOpeningFragment: none,
|
||||
JSXClosingFragment: none,
|
||||
// TypeScript declarations and erased wrappers.
|
||||
TSAsExpression: none,
|
||||
TSSatisfiesExpression: none,
|
||||
TSTypeAssertion: none,
|
||||
TSNonNullExpression: none,
|
||||
TSInstantiationExpression: none,
|
||||
TSDeclareFunction: none,
|
||||
TSDeclareMethod: none,
|
||||
TSInterfaceDeclaration: none,
|
||||
TSInterfaceBody: none,
|
||||
TSTypeAliasDeclaration: none,
|
||||
TSTypeAnnotation: none,
|
||||
TSTypeParameterInstantiation: none,
|
||||
TSTypeParameterDeclaration: none,
|
||||
TSTypeParameter: none,
|
||||
TSQualifiedName: none,
|
||||
TSNamedTupleMember: none,
|
||||
TSNamespaceExportDeclaration: none,
|
||||
TSExportAssignment: none,
|
||||
TSExternalModuleReference: none,
|
||||
TSEnumDeclaration: none,
|
||||
TSEnumBody: none,
|
||||
TSEnumMember: none,
|
||||
TSModuleDeclaration: none,
|
||||
TSModuleBlock: none,
|
||||
// `import x = require(...)` evaluates a module; `import x = N.y` reads N.y.
|
||||
TSImportEqualsDeclaration: (node) => [
|
||||
t.isTSExternalModuleReference(node.moduleReference)
|
||||
? execution("module-evaluation", node, [])
|
||||
: execution("property-read", node, []),
|
||||
],
|
||||
// Assignment semantics store the parameter on `this`.
|
||||
TSParameterProperty: (node) => [
|
||||
{
|
||||
kind: "property-write",
|
||||
node,
|
||||
target: node.parameter,
|
||||
readsFirst: false,
|
||||
operands: [],
|
||||
},
|
||||
],
|
||||
// Deprecated Babel 6 spellings the parser no longer produces.
|
||||
NumberLiteral: none,
|
||||
RegexLiteral: none,
|
||||
RestProperty: (node) => [execution("property-copy", node, [])],
|
||||
SpreadProperty: (node) => [execution("property-copy", node, [node.argument])],
|
||||
// Flow enums compile to a runtime call; the parser enables no Flow syntax.
|
||||
EnumDeclaration: unsupported,
|
||||
// Proposals the parser does not enable.
|
||||
Placeholder: unsupported,
|
||||
V8IntrinsicIdentifier: unsupported,
|
||||
ArgumentPlaceholder: unsupported,
|
||||
BindExpression: unsupported,
|
||||
DoExpression: unsupported,
|
||||
ModuleExpression: unsupported,
|
||||
TopicReference: unsupported,
|
||||
};
|
||||
|
||||
const isTypeOnly = (node: t.Node): node is TypeOnlyNode =>
|
||||
!t.isEnumDeclaration(node) &&
|
||||
typeOnlyAliases.some((alias) => t.is(alias, node));
|
||||
|
||||
/**
|
||||
* The single owner for "may evaluating this node run code it does not spell
|
||||
* as a call". An empty result is a proof that the node's own evaluation runs
|
||||
* no user code; operands still classify separately. Unknown node types are
|
||||
* classified as unsupported syntax rather than as inert.
|
||||
*/
|
||||
export const javaScriptExecutions = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
options: JavaScriptExecutionOptions = {},
|
||||
): readonly JavaScriptExecution[] => {
|
||||
if (isTypeOnly(node)) return [];
|
||||
const classify = (classifiers as Partial<Record<string, Classifier<t.Node>>>)[
|
||||
node.type
|
||||
];
|
||||
return classify === undefined
|
||||
? unsupported(node, parent, options)
|
||||
: classify(node, parent, options);
|
||||
};
|
||||
|
||||
/** Node types with an explicit classifier, for the coverage guard. */
|
||||
export const javaScriptClassifiedNodeTypes = (): readonly string[] =>
|
||||
Object.keys(classifiers);
|
||||
|
||||
/** Babel aliases whose members have no runtime representation. */
|
||||
export const javaScriptTypeOnlyAliases = typeOnlyAliases;
|
||||
@@ -14,6 +14,7 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
|
||||
import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js";
|
||||
import { semanticMethodDefinitionExpression } from "./javascriptSemanticEvaluationOrder.js";
|
||||
import { javaScriptExecutions } from "./javascriptSemanticExecutionPoints.js";
|
||||
import { childNodes } from "./javascriptSemanticTraversal.js";
|
||||
import type {
|
||||
IterationSourceValue,
|
||||
@@ -294,21 +295,13 @@ const guardedBodyMayCall = (body: t.BlockStatement): boolean => {
|
||||
for (const child of children)
|
||||
pending.push({ node: child, parent: node, finish: false });
|
||||
} else {
|
||||
// Without scope proof an identifier may resolve through a global accessor,
|
||||
// including when its enclosing operator itself performs no coercion.
|
||||
const direct =
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node) ||
|
||||
t.isTaggedTemplateExpression(node) ||
|
||||
t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node) ||
|
||||
t.isBinaryExpression(node) ||
|
||||
t.isUnaryExpression(node) ||
|
||||
t.isUpdateExpression(node) ||
|
||||
t.isSpreadElement(node) ||
|
||||
t.isForOfStatement(node) ||
|
||||
t.isAwaitExpression(node) ||
|
||||
((t.isVariableDeclarator(node) || t.isAssignmentExpression(node)) &&
|
||||
semanticSourceFacts(node, parent).propertyReads.length > 0);
|
||||
javaScriptExecutions(node, parent).length > 0 ||
|
||||
(t.isIdentifier(node) &&
|
||||
parent !== null &&
|
||||
t.isReferenced(node, parent));
|
||||
guardedExecutions.set(
|
||||
node,
|
||||
direct ||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
/** Operands exposed to user-defined primitive conversion by language operations. */
|
||||
export const semanticCoercionOperands = (
|
||||
node: t.Node,
|
||||
isObject: (operand: t.Node) => boolean = () => false,
|
||||
): readonly t.Node[] => {
|
||||
if (t.isBinaryExpression(node)) {
|
||||
if (["===", "!=="].includes(node.operator)) return [];
|
||||
if (node.operator === "instanceof") return [node.left, node.right];
|
||||
if (
|
||||
["==", "!="].includes(node.operator) &&
|
||||
((t.isThisExpression(node.left) && t.isThisExpression(node.right)) ||
|
||||
(isObject(node.left) && isObject(node.right)))
|
||||
)
|
||||
return [];
|
||||
return node.operator === "in" ? [node.left] : [node.left, node.right];
|
||||
}
|
||||
if (t.isUnaryExpression(node) && ["+", "-", "~"].includes(node.operator))
|
||||
return [node.argument];
|
||||
if (t.isUpdateExpression(node)) return [node.argument];
|
||||
if (
|
||||
t.isAssignmentExpression(node) &&
|
||||
!["=", "&&=", "||=", "??="].includes(node.operator)
|
||||
)
|
||||
return [node.left, node.right];
|
||||
if (
|
||||
(t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node) ||
|
||||
t.isObjectProperty(node) ||
|
||||
t.isObjectMethod(node) ||
|
||||
t.isClassProperty(node) ||
|
||||
t.isClassMethod(node) ||
|
||||
t.isClassAccessorProperty(node)) &&
|
||||
node.computed
|
||||
)
|
||||
return [
|
||||
t.isMemberExpression(node) || t.isOptionalMemberExpression(node)
|
||||
? node.property
|
||||
: node.key,
|
||||
];
|
||||
return [];
|
||||
};
|
||||
@@ -69,7 +69,11 @@ import {
|
||||
semanticCaptureOrderingIdentity,
|
||||
semanticScopeExitEffect,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import { semanticCoercionOperands } from "./javascriptSemanticLanguageInvocations.js";
|
||||
import {
|
||||
javaScriptDefinitionKeyCoercions,
|
||||
javaScriptDefinitionTimeExpressions,
|
||||
javaScriptExecutions,
|
||||
} from "./javascriptSemanticExecutionPoints.js";
|
||||
import {
|
||||
semanticKnownArrayIteration,
|
||||
semanticReferenceSourcesInactive,
|
||||
@@ -89,10 +93,7 @@ import {
|
||||
childNodes,
|
||||
} from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
javaScriptComputedDefinitionKey,
|
||||
semanticStaticPropertyKey,
|
||||
semanticWriteSiteTarget,
|
||||
semanticWriteSiteReadsFirst,
|
||||
semanticWriteTargets,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
@@ -394,40 +395,6 @@ const accessorOperands = new WeakMap<
|
||||
readonly AccessorEffectOperand[]
|
||||
>();
|
||||
|
||||
/** Members whose computed keys a definition evaluates and converts immediately. */
|
||||
const computedDefinitionMembers = (node: t.Function | t.Class): t.Node[] => {
|
||||
if (t.isObjectMethod(node))
|
||||
return javaScriptComputedDefinitionKey(node, node.key) ? [node] : [];
|
||||
if (!t.isClass(node)) return [];
|
||||
return node.body.body.filter(
|
||||
(member) =>
|
||||
"key" in member && javaScriptComputedDefinitionKey(member, member.key),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Expressions a definition runs immediately: heritage, computed keys, static
|
||||
* blocks and static field values. Deferred bodies and instance initializers
|
||||
* stay out; `this` inside static work is the class, not the enclosing receiver.
|
||||
*/
|
||||
const definitionTimeExpressions = (node: t.Function | t.Class): t.Node[] => [
|
||||
...(t.isClass(node) && node.superClass != null ? [node.superClass] : []),
|
||||
...computedDefinitionMembers(node).flatMap((member) =>
|
||||
"key" in member ? [member.key] : [],
|
||||
),
|
||||
...(t.isClass(node)
|
||||
? node.body.body.flatMap((member): t.Node[] =>
|
||||
t.isStaticBlock(member)
|
||||
? [member]
|
||||
: (t.isClassProperty(member) || t.isClassPrivateProperty(member)) &&
|
||||
member.static &&
|
||||
member.value != null
|
||||
? [member.value]
|
||||
: [],
|
||||
)
|
||||
: []),
|
||||
];
|
||||
|
||||
/** Collect invocation operands once; receiver interpretation belongs to the getter owner. */
|
||||
const accessorEffectOperands = (
|
||||
method: t.Function,
|
||||
@@ -435,6 +402,9 @@ const accessorEffectOperands = (
|
||||
const cached = accessorOperands.get(method);
|
||||
if (cached !== undefined) return cached;
|
||||
const operands: AccessorEffectOperand[] = [];
|
||||
const escape = (nodes: readonly t.Node[]): void => {
|
||||
for (const node of nodes) operands.push({ kind: "escape", node });
|
||||
};
|
||||
const pending: { node: t.Node; parent: t.Node | null }[] = [
|
||||
{ node: method.body, parent: method },
|
||||
];
|
||||
@@ -444,66 +414,75 @@ const accessorEffectOperands = (
|
||||
// Defining a nested callable does not invoke it, but the definition itself
|
||||
// evaluates heritage and computed keys against the enclosing receiver.
|
||||
if (t.isFunction(node) || t.isClass(node)) {
|
||||
// Defining a computed member also converts its key to a property name.
|
||||
for (const member of computedDefinitionMembers(node))
|
||||
for (const operand of semanticCoercionOperands(member))
|
||||
operands.push({ kind: "escape", node: operand });
|
||||
for (const child of definitionTimeExpressions(node).reverse())
|
||||
escape(javaScriptDefinitionKeyCoercions(node));
|
||||
for (const child of javaScriptDefinitionTimeExpressions(node).reverse())
|
||||
pending.push({ node: child, parent: node });
|
||||
continue;
|
||||
}
|
||||
const write = semanticWriteSiteTarget(node);
|
||||
if (write !== null) {
|
||||
for (const target of semanticWriteTargets(write)) {
|
||||
if (!semanticWriteSiteReadsFirst(node)) writeOnly.add(target);
|
||||
if (!t.isIdentifier(target))
|
||||
operands.push({ kind: "escape", node: target.object });
|
||||
for (const execution of javaScriptExecutions(node, parent))
|
||||
switch (execution.kind) {
|
||||
case "property-write":
|
||||
if (!execution.readsFirst)
|
||||
for (const target of semanticWriteTargets(execution.target))
|
||||
writeOnly.add(target);
|
||||
escape(execution.operands);
|
||||
break;
|
||||
case "property-read":
|
||||
if (
|
||||
(t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node)) &&
|
||||
!writeOnly.has(node)
|
||||
)
|
||||
operands.push({
|
||||
kind: "read",
|
||||
node: node.object,
|
||||
path: [semanticStaticPropertyKey(node.property, node.computed)],
|
||||
lookup: "prototype",
|
||||
});
|
||||
break;
|
||||
case "call":
|
||||
case "construction":
|
||||
case "tagged-template":
|
||||
if (t.isTaggedTemplateExpression(node))
|
||||
operands.push({ kind: "invoke", node: node.tag });
|
||||
else if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
operands.push({
|
||||
kind: "invoke",
|
||||
node: node.callee,
|
||||
...(t.isNewExpression(node) ? {} : { invocation: node }),
|
||||
});
|
||||
escape(execution.operands);
|
||||
break;
|
||||
case "coercion":
|
||||
case "disposal":
|
||||
escape(execution.operands);
|
||||
break;
|
||||
// Projected getter and `then` reads belong to the read-source owner.
|
||||
case "destructuring":
|
||||
case "property-copy":
|
||||
case "thenable":
|
||||
// Definitions are handled above without entering deferred bodies.
|
||||
case "class-definition":
|
||||
break;
|
||||
// Not applied yet: these may run code with their operands, but this
|
||||
// collector has never modelled them. Listed so the gap is explicit.
|
||||
case "iteration":
|
||||
case "enumeration":
|
||||
case "suspension":
|
||||
case "decorator":
|
||||
case "dynamic-import":
|
||||
case "dynamic-scope":
|
||||
case "jsx-element":
|
||||
case "module-evaluation":
|
||||
case "unsupported-syntax":
|
||||
break;
|
||||
default:
|
||||
execution satisfies never;
|
||||
}
|
||||
}
|
||||
if (
|
||||
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
|
||||
!writeOnly.has(node)
|
||||
)
|
||||
operands.push({
|
||||
kind: "read",
|
||||
node: node.object,
|
||||
path: [semanticStaticPropertyKey(node.property, node.computed)],
|
||||
lookup: "prototype",
|
||||
});
|
||||
if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
) {
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
operands.push({
|
||||
kind: "invoke",
|
||||
node: node.callee,
|
||||
...(t.isNewExpression(node) ? {} : { invocation: node }),
|
||||
});
|
||||
if (
|
||||
!t.isNewExpression(node) &&
|
||||
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee))
|
||||
)
|
||||
operands.push({ kind: "escape", node: callee.object });
|
||||
for (const argument of node.arguments)
|
||||
operands.push({
|
||||
kind: "escape",
|
||||
node: t.isSpreadElement(argument) ? argument.argument : argument,
|
||||
});
|
||||
} else if (t.isTaggedTemplateExpression(node)) {
|
||||
operands.push({ kind: "invoke", node: node.tag });
|
||||
const tag = unwrapJavaScriptExpression(node.tag).node;
|
||||
if (t.isMemberExpression(tag) || t.isOptionalMemberExpression(tag))
|
||||
operands.push({ kind: "escape", node: tag.object });
|
||||
for (const expression of node.quasi.expressions)
|
||||
operands.push({ kind: "escape", node: expression });
|
||||
}
|
||||
for (const operand of semanticCoercionOperands(node))
|
||||
operands.push({ kind: "escape", node: operand });
|
||||
if (t.isTemplateLiteral(node) && !t.isTaggedTemplateExpression(parent))
|
||||
for (const expression of node.expressions)
|
||||
operands.push({ kind: "escape", node: expression });
|
||||
for (const source of semanticPropertyReadSources(node, parent))
|
||||
operands.push({
|
||||
kind: "read",
|
||||
@@ -513,12 +492,6 @@ const accessorEffectOperands = (
|
||||
...(source.invoke ? { invoke: true } : {}),
|
||||
guards: source,
|
||||
});
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
t.isVariableDeclaration(parent) &&
|
||||
(parent.kind === "using" || parent.kind === "await using")
|
||||
)
|
||||
operands.push({ kind: "escape", node: node.init ?? node.id });
|
||||
for (const child of childNodes(node).reverse())
|
||||
pending.push({ node: child, parent: node });
|
||||
}
|
||||
@@ -1778,13 +1751,72 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
sourcesByTarget.get(target) ?? [],
|
||||
);
|
||||
};
|
||||
/** Disposal runs when the declaration's lexical scope exits. */
|
||||
const markDisposal = (
|
||||
node: t.Node,
|
||||
readAncestors: () => readonly t.Node[],
|
||||
): void => {
|
||||
if (!t.isVariableDeclarator(node)) return;
|
||||
const scope = readAncestors().findLast(
|
||||
(ancestor) =>
|
||||
t.isBlockStatement(ancestor) ||
|
||||
t.isProgram(ancestor) ||
|
||||
t.isStaticBlock(ancestor) ||
|
||||
t.isSwitchStatement(ancestor) ||
|
||||
t.isLoop(ancestor),
|
||||
);
|
||||
if (scope === undefined) return;
|
||||
const exit = semanticScopeExitEffect(scope);
|
||||
if (t.isIdentifier(node.id))
|
||||
markValue(
|
||||
node.id,
|
||||
[],
|
||||
new Set(),
|
||||
"escape",
|
||||
exit,
|
||||
node.id,
|
||||
node.init ?? undefined,
|
||||
);
|
||||
else if (node.init != null) markEscaped(node.init, exit);
|
||||
};
|
||||
const markInvocation = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
): void => {
|
||||
for (const source of returnedReferences(node.callee).references)
|
||||
markEscaped(source.node, node, source.projection);
|
||||
for (const argument of node.arguments)
|
||||
if (t.isSpreadElement(argument))
|
||||
deferIterable({
|
||||
node: argument.argument,
|
||||
projection: [],
|
||||
path: [null],
|
||||
fallbackPath: [],
|
||||
bindings: new Set(),
|
||||
effect: "escape",
|
||||
mutation: node,
|
||||
originAt: node,
|
||||
});
|
||||
else markEscaped(argument, node);
|
||||
// Constructing a member does not pass its container as `this`.
|
||||
if (!t.isNewExpression(node)) {
|
||||
const mapLookup = isIntrinsicMapLookup(node, state);
|
||||
if (mapLookup === undefined) deferredMapReceivers.add(node);
|
||||
else if (!mapLookup) markReceiver(node.callee, node);
|
||||
}
|
||||
};
|
||||
const writeOnlyMembers = new WeakSet<t.Node>();
|
||||
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
const writeTarget = semanticWriteSiteTarget(node);
|
||||
if (writeTarget !== null && !semanticWriteSiteReadsFirst(node))
|
||||
for (const target of semanticWriteTargets(writeTarget))
|
||||
writeOnlyMembers.add(target);
|
||||
const executions = javaScriptExecutions(node, parent, {
|
||||
isObject: (operand) => {
|
||||
const value = evaluateSemanticExpression(operand, state, node);
|
||||
return value.status === "object" || value.status === "array";
|
||||
},
|
||||
});
|
||||
for (const execution of executions)
|
||||
if (execution.kind === "property-write" && !execution.readsFirst)
|
||||
for (const target of semanticWriteTargets(execution.target))
|
||||
writeOnlyMembers.add(target);
|
||||
for (const source of semanticIterationSources(node, parent)) {
|
||||
// Local stores retain their sources on member writes. Only the ambient
|
||||
// identity owner can establish that storing itself exposes a reference.
|
||||
@@ -1816,42 +1848,6 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
iterationOnly: source.kind === "iteration",
|
||||
});
|
||||
}
|
||||
for (const operand of semanticCoercionOperands(node, (operand) => {
|
||||
const value = evaluateSemanticExpression(operand, state, node);
|
||||
return value.status === "object" || value.status === "array";
|
||||
}))
|
||||
if (!semanticKnownSymbolKey(operand, state)) markEscaped(operand, node);
|
||||
if (t.isTemplateLiteral(node) && !t.isTaggedTemplateExpression(parent))
|
||||
for (const expression of node.expressions)
|
||||
markEscaped(expression, node);
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
t.isVariableDeclaration(parent) &&
|
||||
(parent.kind === "using" || parent.kind === "await using")
|
||||
) {
|
||||
const scope = readAncestors().findLast(
|
||||
(ancestor) =>
|
||||
t.isBlockStatement(ancestor) ||
|
||||
t.isProgram(ancestor) ||
|
||||
t.isStaticBlock(ancestor) ||
|
||||
t.isSwitchStatement(ancestor) ||
|
||||
t.isLoop(ancestor),
|
||||
);
|
||||
if (scope !== undefined) {
|
||||
const exit = semanticScopeExitEffect(scope);
|
||||
if (t.isIdentifier(node.id))
|
||||
markValue(
|
||||
node.id,
|
||||
[],
|
||||
new Set(),
|
||||
"escape",
|
||||
exit,
|
||||
node.id,
|
||||
node.init ?? undefined,
|
||||
);
|
||||
else if (node.init != null) markEscaped(node.init, exit);
|
||||
}
|
||||
}
|
||||
for (const source of semanticPropertyReadSources(node, parent, state)) {
|
||||
if (semanticReferenceSourcesInactive(source, state)) continue;
|
||||
for (const effect of propertyReadEffects(
|
||||
@@ -1862,48 +1858,72 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
))
|
||||
markEscaped(effect.node, source.mutation, effect.path);
|
||||
}
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
// The write owner classifies direct and destructuring stores once.
|
||||
if (!writeOnlyMembers.has(node))
|
||||
for (const effect of propertyReadEffects(node.object, [
|
||||
semanticStaticPropertyKey(node.property, node.computed),
|
||||
]))
|
||||
markEscaped(effect.node, node, effect.path);
|
||||
}
|
||||
if (writeTarget !== null) markTarget(writeTarget, node);
|
||||
else if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
) {
|
||||
for (const source of returnedReferences(node.callee).references)
|
||||
markEscaped(source.node, node, source.projection);
|
||||
for (const argument of node.arguments)
|
||||
if (t.isSpreadElement(argument))
|
||||
deferIterable({
|
||||
node: argument.argument,
|
||||
projection: [],
|
||||
path: [null],
|
||||
fallbackPath: [],
|
||||
bindings: new Set(),
|
||||
effect: "escape",
|
||||
mutation: node,
|
||||
originAt: node,
|
||||
});
|
||||
else markEscaped(argument, node);
|
||||
// Constructing a member does not pass its container as `this`.
|
||||
if (!t.isNewExpression(node)) {
|
||||
const mapLookup = isIntrinsicMapLookup(node, state);
|
||||
if (mapLookup === undefined) deferredMapReceivers.add(node);
|
||||
else if (!mapLookup) markReceiver(node.callee, node);
|
||||
for (const execution of executions)
|
||||
switch (execution.kind) {
|
||||
case "coercion":
|
||||
for (const operand of execution.operands)
|
||||
if (!semanticKnownSymbolKey(operand, state))
|
||||
markEscaped(operand, node);
|
||||
break;
|
||||
case "disposal":
|
||||
markDisposal(node, readAncestors);
|
||||
break;
|
||||
case "property-read":
|
||||
// The write owner classifies direct and destructuring stores once.
|
||||
if (
|
||||
(t.isMemberExpression(node) ||
|
||||
t.isOptionalMemberExpression(node)) &&
|
||||
!writeOnlyMembers.has(node)
|
||||
)
|
||||
for (const effect of propertyReadEffects(node.object, [
|
||||
semanticStaticPropertyKey(node.property, node.computed),
|
||||
]))
|
||||
markEscaped(effect.node, node, effect.path);
|
||||
break;
|
||||
case "property-write":
|
||||
markTarget(execution.target, node);
|
||||
break;
|
||||
case "call":
|
||||
case "construction":
|
||||
if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
markInvocation(node);
|
||||
break;
|
||||
case "tagged-template":
|
||||
if (t.isTaggedTemplateExpression(node)) {
|
||||
for (const source of returnedReferences(node.tag).references)
|
||||
markEscaped(source.node, node, source.projection);
|
||||
markReceiver(node.tag, node);
|
||||
for (const expression of node.quasi.expressions)
|
||||
markEscaped(expression, node);
|
||||
}
|
||||
break;
|
||||
// Projected getter, iterator and `then` reads belong to the
|
||||
// read-source and iteration owners above.
|
||||
case "destructuring":
|
||||
case "property-copy":
|
||||
case "iteration":
|
||||
case "thenable":
|
||||
// Definition-time code is traversed as ordinary code.
|
||||
case "class-definition":
|
||||
break;
|
||||
// Not applied yet: these may run code with their operands, but this
|
||||
// collector has never modelled them. Listed so the gap is explicit.
|
||||
case "suspension":
|
||||
case "enumeration":
|
||||
case "decorator":
|
||||
case "dynamic-import":
|
||||
case "dynamic-scope":
|
||||
case "jsx-element":
|
||||
case "module-evaluation":
|
||||
case "unsupported-syntax":
|
||||
break;
|
||||
default:
|
||||
execution satisfies never;
|
||||
}
|
||||
} else if (t.isTaggedTemplateExpression(node)) {
|
||||
for (const source of returnedReferences(node.tag).references)
|
||||
markEscaped(source.node, node, source.projection);
|
||||
markReceiver(node.tag, node);
|
||||
for (const expression of node.quasi.expressions)
|
||||
markEscaped(expression, node);
|
||||
}
|
||||
},
|
||||
});
|
||||
// A later effect can make a previously defined destructuring source uncertain.
|
||||
|
||||
Reference in new issue
Block a user