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 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
141 lines
5.4 KiB
TypeScript
141 lines
5.4 KiB
TypeScript
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"]),
|
|
}),
|
|
]),
|
|
);
|
|
},
|
|
);
|