monkeypatchlist 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +61 -0
- package/README.md +183 -2
- package/catalog/catalog.json +552211 -0
- package/catalog/catalog.schema.json +787 -0
- package/catalog/changelog.json +3 -0
- package/dist/bin/monkeypatchlist.d.ts +2 -0
- package/dist/bin/monkeypatchlist.js +3 -0
- package/dist/catalog.d.ts +561 -0
- package/dist/catalog.js +58 -0
- package/dist/cli.d.ts +84 -0
- package/dist/cli.js +470 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lockfile-input.d.ts +9 -0
- package/dist/lockfile-input.js +91 -0
- package/dist/packages/analysis/binding.d.ts +2 -0
- package/dist/packages/analysis/binding.js +5 -0
- package/dist/packages/analysis/index.d.ts +10 -0
- package/dist/packages/analysis/index.js +13 -0
- package/dist/packages/analysis/job.d.ts +33 -0
- package/dist/packages/analysis/job.js +85 -0
- package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
- package/dist/packages/analysis/lib/absence-tags.js +340 -0
- package/dist/packages/analysis/lib/ast.d.ts +28 -0
- package/dist/packages/analysis/lib/ast.js +64 -0
- package/dist/packages/analysis/lib/binding.d.ts +36 -0
- package/dist/packages/analysis/lib/binding.js +66 -0
- package/dist/packages/analysis/lib/budget.d.ts +80 -0
- package/dist/packages/analysis/lib/budget.js +31 -0
- package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
- package/dist/packages/analysis/lib/catch-arms.js +351 -0
- package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
- package/dist/packages/analysis/lib/chunk-loads.js +882 -0
- package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
- package/dist/packages/analysis/lib/chunk-registry.js +334 -0
- package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
- package/dist/packages/analysis/lib/code-routes.js +921 -0
- package/dist/packages/analysis/lib/collisions.d.ts +22 -0
- package/dist/packages/analysis/lib/collisions.js +103 -0
- package/dist/packages/analysis/lib/definitions.d.ts +76 -0
- package/dist/packages/analysis/lib/definitions.js +782 -0
- package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
- package/dist/packages/analysis/lib/early-exits.js +161 -0
- package/dist/packages/analysis/lib/effect.d.ts +138 -0
- package/dist/packages/analysis/lib/effect.js +800 -0
- package/dist/packages/analysis/lib/global-object.d.ts +581 -0
- package/dist/packages/analysis/lib/global-object.js +8240 -0
- package/dist/packages/analysis/lib/keys.d.ts +33 -0
- package/dist/packages/analysis/lib/keys.js +82 -0
- package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
- package/dist/packages/analysis/lib/line-starts.js +79 -0
- package/dist/packages/analysis/lib/link-group.d.ts +51 -0
- package/dist/packages/analysis/lib/link-group.js +487 -0
- package/dist/packages/analysis/lib/module-links.d.ts +117 -0
- package/dist/packages/analysis/lib/module-links.js +1060 -0
- package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
- package/dist/packages/analysis/lib/nested-code.js +200 -0
- package/dist/packages/analysis/lib/numeric.d.ts +16 -0
- package/dist/packages/analysis/lib/numeric.js +216 -0
- package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
- package/dist/packages/analysis/lib/once-flags.js +190 -0
- package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
- package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
- package/dist/packages/analysis/lib/parse.d.ts +10 -0
- package/dist/packages/analysis/lib/parse.js +39 -0
- package/dist/packages/analysis/lib/partial.d.ts +60 -0
- package/dist/packages/analysis/lib/partial.js +518 -0
- package/dist/packages/analysis/lib/print.d.ts +3 -0
- package/dist/packages/analysis/lib/print.js +150 -0
- package/dist/packages/analysis/lib/reachability.d.ts +137 -0
- package/dist/packages/analysis/lib/reachability.js +1295 -0
- package/dist/packages/analysis/lib/resolver.d.ts +13 -0
- package/dist/packages/analysis/lib/resolver.js +2886 -0
- package/dist/packages/analysis/lib/restores.d.ts +42 -0
- package/dist/packages/analysis/lib/restores.js +482 -0
- package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
- package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
- package/dist/packages/analysis/lib/scope.d.ts +110 -0
- package/dist/packages/analysis/lib/scope.js +443 -0
- package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
- package/dist/packages/analysis/lib/script-loads.js +359 -0
- package/dist/packages/analysis/lib/summaries.d.ts +177 -0
- package/dist/packages/analysis/lib/summaries.js +498 -0
- package/dist/packages/analysis/lib/types.d.ts +632 -0
- package/dist/packages/analysis/lib/types.js +128 -0
- package/dist/packages/analysis/lib/wrap.d.ts +34 -0
- package/dist/packages/analysis/lib/wrap.js +407 -0
- package/dist/packages/analysis/lib/writes.d.ts +358 -0
- package/dist/packages/analysis/lib/writes.js +1612 -0
- package/dist/packages/analysis/visit-order.d.ts +3 -0
- package/dist/packages/analysis/visit-order.js +23 -0
- package/dist/packages/attribution/index.d.ts +233 -0
- package/dist/packages/attribution/index.js +444 -0
- package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
- package/dist/packages/attribution/lib/containing-package.js +304 -0
- package/dist/packages/attribution/lib/first-party.d.ts +37 -0
- package/dist/packages/attribution/lib/first-party.js +153 -0
- package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
- package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
- package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
- package/dist/packages/attribution/lib/lockfile.js +330 -0
- package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
- package/dist/packages/attribution/lib/path-rules.js +84 -0
- package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
- package/dist/packages/attribution/lib/pnpm.js +133 -0
- package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
- package/dist/packages/attribution/lib/remote-url.js +24 -0
- package/dist/packages/attribution/lib/source-map.d.ts +51 -0
- package/dist/packages/attribution/lib/source-map.js +125 -0
- package/dist/packages/attribution/lib/yaml.d.ts +9 -0
- package/dist/packages/attribution/lib/yaml.js +277 -0
- package/dist/packages/attribution/lib/yarn.d.ts +7 -0
- package/dist/packages/attribution/lib/yarn.js +227 -0
- package/dist/packages/baseline/index.d.ts +45 -0
- package/dist/packages/baseline/index.js +41 -0
- package/dist/packages/baseline/lib/error.d.ts +15 -0
- package/dist/packages/baseline/lib/error.js +25 -0
- package/dist/packages/baseline/lib/format.d.ts +49 -0
- package/dist/packages/baseline/lib/format.js +22 -0
- package/dist/packages/baseline/lib/migrations.d.ts +19 -0
- package/dist/packages/baseline/lib/migrations.js +7 -0
- package/dist/packages/baseline/lib/previous.d.ts +23 -0
- package/dist/packages/baseline/lib/previous.js +64 -0
- package/dist/packages/baseline/lib/read.d.ts +39 -0
- package/dist/packages/baseline/lib/read.js +219 -0
- package/dist/packages/baseline/lib/report.d.ts +25 -0
- package/dist/packages/baseline/lib/report.js +104 -0
- package/dist/packages/baseline/lib/skew.d.ts +51 -0
- package/dist/packages/baseline/lib/skew.js +75 -0
- package/dist/packages/compare/index.d.ts +120 -0
- package/dist/packages/compare/index.js +274 -0
- package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
- package/dist/packages/compare/lib/catalog-notes.js +224 -0
- package/dist/packages/completeness/index.d.ts +246 -0
- package/dist/packages/completeness/index.js +249 -0
- package/dist/packages/confidence/index.d.ts +54 -0
- package/dist/packages/confidence/index.js +71 -0
- package/dist/packages/config/index.d.ts +107 -0
- package/dist/packages/config/index.js +95 -0
- package/dist/packages/config/lib/command-line.d.ts +23 -0
- package/dist/packages/config/lib/command-line.js +239 -0
- package/dist/packages/config/lib/config-file.d.ts +11 -0
- package/dist/packages/config/lib/config-file.js +119 -0
- package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
- package/dist/packages/config/lib/scan-config-id.js +54 -0
- package/dist/packages/config/lib/schema.d.ts +2 -0
- package/dist/packages/config/lib/schema.js +31 -0
- package/dist/packages/config/lib/types.d.ts +64 -0
- package/dist/packages/config/lib/types.js +6 -0
- package/dist/packages/config/lib/urls.d.ts +6 -0
- package/dist/packages/config/lib/urls.js +9 -0
- package/dist/packages/discovery/index.d.ts +277 -0
- package/dist/packages/discovery/index.js +1950 -0
- package/dist/packages/discovery/lib/import-map.d.ts +68 -0
- package/dist/packages/discovery/lib/import-map.js +180 -0
- package/dist/packages/discovery/lib/requests.d.ts +58 -0
- package/dist/packages/discovery/lib/requests.js +591 -0
- package/dist/packages/discovery/lib/scopes.d.ts +6 -0
- package/dist/packages/discovery/lib/scopes.js +286 -0
- package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
- package/dist/packages/discovery/lib/source-maps.js +209 -0
- package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
- package/dist/packages/discovery/lib/swept-code.js +107 -0
- package/dist/packages/discovery/lib/syntax.d.ts +17 -0
- package/dist/packages/discovery/lib/syntax.js +39 -0
- package/dist/packages/findings/index.d.ts +106 -0
- package/dist/packages/findings/index.js +254 -0
- package/dist/packages/findings/lib/scope.d.ts +38 -0
- package/dist/packages/findings/lib/scope.js +110 -0
- package/dist/packages/parallel/index.d.ts +2 -0
- package/dist/packages/parallel/index.js +5 -0
- package/dist/packages/parallel/lib/protocol.d.ts +15 -0
- package/dist/packages/parallel/lib/protocol.js +2 -0
- package/dist/packages/parallel/lib/runner.d.ts +36 -0
- package/dist/packages/parallel/lib/runner.js +96 -0
- package/dist/packages/parallel/lib/schedule.d.ts +16 -0
- package/dist/packages/parallel/lib/schedule.js +52 -0
- package/dist/packages/parallel/lib/worker.d.ts +1 -0
- package/dist/packages/parallel/lib/worker.js +18 -0
- package/dist/packages/policy/index.d.ts +36 -0
- package/dist/packages/policy/index.js +22 -0
- package/dist/packages/report/explain.d.ts +26 -0
- package/dist/packages/report/explain.js +99 -0
- package/dist/packages/report/index.d.ts +210 -0
- package/dist/packages/report/index.js +388 -0
- package/dist/packages/report/terminal.d.ts +11 -0
- package/dist/packages/report/terminal.js +331 -0
- package/dist/packages/source-store/fixture.d.ts +22 -0
- package/dist/packages/source-store/fixture.js +58 -0
- package/dist/packages/source-store/index.d.ts +278 -0
- package/dist/packages/source-store/index.js +322 -0
- package/dist/packages/source-store/lib/addresses.d.ts +6 -0
- package/dist/packages/source-store/lib/addresses.js +91 -0
- package/dist/packages/source-store/lib/connection.d.ts +15 -0
- package/dist/packages/source-store/lib/connection.js +135 -0
- package/dist/packages/source-store/lib/content-types.d.ts +16 -0
- package/dist/packages/source-store/lib/content-types.js +75 -0
- package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
- package/dist/packages/source-store/lib/data-urls.js +85 -0
- package/dist/packages/source-store/lib/deadline.d.ts +18 -0
- package/dist/packages/source-store/lib/deadline.js +44 -0
- package/dist/packages/source-store/lib/exchange.d.ts +29 -0
- package/dist/packages/source-store/lib/exchange.js +112 -0
- package/dist/packages/source-store/lib/integrity.d.ts +19 -0
- package/dist/packages/source-store/lib/integrity.js +86 -0
- package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
- package/dist/packages/source-store/lib/outcomes.js +22 -0
- package/dist/packages/source-store/lib/proxy.d.ts +11 -0
- package/dist/packages/source-store/lib/proxy.js +53 -0
- package/dist/packages/source-store/lib/session.d.ts +12 -0
- package/dist/packages/source-store/lib/session.js +139 -0
- package/dist/packages/source-store/lib/urls.d.ts +6 -0
- package/dist/packages/source-store/lib/urls.js +15 -0
- package/dist/packages/source-store/resolve.d.ts +59 -0
- package/dist/packages/source-store/resolve.js +45 -0
- package/dist/scan.d.ts +65 -0
- package/dist/scan.js +592 -0
- package/package.json +95 -4
- package/schema/config.schema.json +133 -0
- package/schema/report.schema.json +1905 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type AstNode } from './ast.ts';
|
|
2
|
+
/** A piece of code, and where each of its UTF-16 offsets lies in the host artifact. */
|
|
3
|
+
export interface CodeText {
|
|
4
|
+
text: string;
|
|
5
|
+
/** The host artifact's offset for an offset in `text`, from 0 to `text.length`. It never decreases. */
|
|
6
|
+
host(offset: number): number;
|
|
7
|
+
}
|
|
8
|
+
/** Direct-eval code grafted into the host's AST in place of its literal, with the eval call it is the code of. */
|
|
9
|
+
export interface Graft {
|
|
10
|
+
call: AstNode;
|
|
11
|
+
}
|
|
12
|
+
export interface NestedCode {
|
|
13
|
+
/** The code a code literal holds, located in the host artifact. */
|
|
14
|
+
code(literal: AstNode): CodeText;
|
|
15
|
+
/**
|
|
16
|
+
* The code a number, bigint, boolean or `null` literal given to `Function` stands for: its value converted with ToString
|
|
17
|
+
* (ECMA-262 CreateDynamicFunction, Principle 2), every offset of it located at the literal.
|
|
18
|
+
*/
|
|
19
|
+
converted(literal: AstNode): CodeText;
|
|
20
|
+
/** The program nested code parses to as a script, re-based into the host artifact, or none when it doesn't parse. */
|
|
21
|
+
program(code: CodeText): AstNode | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The program the `Function` constructor compiles from its arguments' code (the parameters, then the body), re-based
|
|
24
|
+
* into the host artifact, or none when the parameters and body don't parse as they are.
|
|
25
|
+
*/
|
|
26
|
+
functionProgram(parts: readonly CodeText[]): AstNode | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Grafts the code of every direct eval of a code literal in `program` (`eval("…")`) in place of its literal, as an
|
|
29
|
+
* arrow function whose body is that code, and records each graft in `grafts`. Code that doesn't parse is left alone:
|
|
30
|
+
* the eval would throw a SyntaxError. The names are not resolved here, so a graft only stands for eval code when
|
|
31
|
+
* its call's `eval` turns out to be the global one.
|
|
32
|
+
*/
|
|
33
|
+
graftDirectEvals(program: AstNode, grafts: Map<AstNode, Graft>): void;
|
|
34
|
+
}
|
|
35
|
+
/** Whether a node is a code literal: a string literal, or a template literal without substitutions. */
|
|
36
|
+
export declare function isCodeLiteral(node: AstNode): boolean;
|
|
37
|
+
/** Nested code in one host artifact, whose source text is `hostText`. */
|
|
38
|
+
export declare function nestedCode(hostText: string): NestedCode;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Nested code (§2 Content that isn't JavaScript, §19 Raw occurrences): the code a literal `eval`, `new Function` or
|
|
2
|
+
// string timer compiles is parsed and analyzed inside the resolver, and located inside its host artifact.
|
|
3
|
+
//
|
|
4
|
+
// Every nested program is re-based into its host artifact's offsets: each UTF-16 offset of the code a literal holds is
|
|
5
|
+
// mapped through the literal's escapes to where that character is written in the host. So a nested node's range lies
|
|
6
|
+
// inside its literal, its line and column come from the host's line-start table, and code nested in nested code maps
|
|
7
|
+
// through each literal in turn.
|
|
8
|
+
//
|
|
9
|
+
// A direct eval's code runs in its caller's scope, so it is grafted into the host's AST in place of its literal, as an
|
|
10
|
+
// arrow function the scope rules treat as eval code (`Graft`). Its names then resolve through the binding-resolution
|
|
11
|
+
// seam exactly as the caller's would. Other nested code is global code, analyzed as a program of its own.
|
|
12
|
+
import { isEvalCall, walkTree } from "./ast.js";
|
|
13
|
+
import { parse } from "./parse.js";
|
|
14
|
+
/** Whether a node is a code literal: a string literal, or a template literal without substitutions. */
|
|
15
|
+
export function isCodeLiteral(node) {
|
|
16
|
+
if (node.type === 'Literal')
|
|
17
|
+
return typeof node['value'] === 'string';
|
|
18
|
+
if (node.type !== 'TemplateLiteral' || node['expressions'].length > 0)
|
|
19
|
+
return false;
|
|
20
|
+
return cooked(node) !== null;
|
|
21
|
+
}
|
|
22
|
+
/** Nested code in one host artifact, whose source text is `hostText`. */
|
|
23
|
+
export function nestedCode(hostText) {
|
|
24
|
+
// Where each code literal inside nested code was written. A literal in the host's own code is written in the host.
|
|
25
|
+
const sources = new WeakMap();
|
|
26
|
+
const sourceOf = (literal) => sources.get(literal) ?? { raw: hostText.slice(literal.start, literal.end), host: (offset) => literal.start + offset };
|
|
27
|
+
function code(literal) {
|
|
28
|
+
const source = sourceOf(literal);
|
|
29
|
+
const text = literal.type === 'Literal' ? literal['value'] : cooked(literal);
|
|
30
|
+
// Both kinds of literal have a one-character delimiter on each side.
|
|
31
|
+
const raw = rawOffsets(source.raw.slice(1, -1), text, literal.type === 'TemplateLiteral');
|
|
32
|
+
return { text, host: (offset) => source.host(1 + raw[offset]) };
|
|
33
|
+
}
|
|
34
|
+
function converted(literal) {
|
|
35
|
+
const text = String(literal['value']);
|
|
36
|
+
const { start, end } = literal;
|
|
37
|
+
return { text, host: (offset) => (offset < text.length ? start : end) };
|
|
38
|
+
}
|
|
39
|
+
/** Parses `code` as a script and re-bases it into the host, if it parses and has the shape `shaped` wants. */
|
|
40
|
+
function located(code, shaped = () => true) {
|
|
41
|
+
const parsed = parse(code.text, 'script');
|
|
42
|
+
if ('error' in parsed || !shaped(parsed.program))
|
|
43
|
+
return undefined;
|
|
44
|
+
walkTree(parsed.program, {
|
|
45
|
+
enter(node) {
|
|
46
|
+
const { start, end } = node;
|
|
47
|
+
if (isCodeLiteral(node)) {
|
|
48
|
+
sources.set(node, { raw: code.text.slice(start, end), host: (offset) => code.host(start + offset) });
|
|
49
|
+
}
|
|
50
|
+
node.start = code.host(start);
|
|
51
|
+
node.end = code.host(end);
|
|
52
|
+
if (Array.isArray(node['range']))
|
|
53
|
+
node['range'] = [node.start, node.end];
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
return parsed.program;
|
|
57
|
+
}
|
|
58
|
+
function functionProgram(parts) {
|
|
59
|
+
// ECMA-262 CreateDynamicFunction: the source text is `function anonymous(<parameters>\n) {\n<body>\n}`, which is
|
|
60
|
+
// parsed here as a parenthesized function expression.
|
|
61
|
+
const body = parts.at(-1);
|
|
62
|
+
if (body === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const segments = [];
|
|
65
|
+
let text = '(function anonymous(';
|
|
66
|
+
parts.slice(0, -1).forEach((part, index) => {
|
|
67
|
+
if (index > 0)
|
|
68
|
+
text += ',';
|
|
69
|
+
segments.push({ from: text.length, code: part });
|
|
70
|
+
text += part.text;
|
|
71
|
+
});
|
|
72
|
+
text += '\n) ';
|
|
73
|
+
const brace = text.length;
|
|
74
|
+
text += '{\n';
|
|
75
|
+
segments.push({ from: text.length, code: body });
|
|
76
|
+
text += `${body.text}\n})`;
|
|
77
|
+
// The glue between the parts is located at the start of the part after it, or at the end of the last part.
|
|
78
|
+
const host = (offset) => {
|
|
79
|
+
for (const { from, code } of segments) {
|
|
80
|
+
if (offset < from)
|
|
81
|
+
return code.host(0);
|
|
82
|
+
if (offset <= from + code.text.length)
|
|
83
|
+
return code.host(offset - from);
|
|
84
|
+
}
|
|
85
|
+
return body.host(body.text.length);
|
|
86
|
+
};
|
|
87
|
+
// The parameters and the body parse on their own, so neither can close the function early and add code after it.
|
|
88
|
+
return located({ text, host }, (program) => {
|
|
89
|
+
const [statement, ...more] = program['body'];
|
|
90
|
+
const made = statement?.['expression'];
|
|
91
|
+
return (more.length === 0 &&
|
|
92
|
+
made?.type === 'FunctionExpression' &&
|
|
93
|
+
made.start === 1 &&
|
|
94
|
+
made.end === text.length - 1 &&
|
|
95
|
+
made['body'].start === brace);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function graftDirectEvals(program, grafts) {
|
|
99
|
+
const calls = [];
|
|
100
|
+
walkTree(program, {
|
|
101
|
+
enter(node) {
|
|
102
|
+
if (!isEvalCall(node))
|
|
103
|
+
return;
|
|
104
|
+
const [argument] = node['arguments'];
|
|
105
|
+
if (argument !== undefined && isCodeLiteral(argument))
|
|
106
|
+
calls.push(node);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
for (const call of calls) {
|
|
110
|
+
const args = call['arguments'];
|
|
111
|
+
const literal = args[0];
|
|
112
|
+
const nested = located(code(literal));
|
|
113
|
+
if (nested === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
graftDirectEvals(nested, grafts);
|
|
116
|
+
const { start, end } = literal;
|
|
117
|
+
const graft = {
|
|
118
|
+
type: 'ArrowFunctionExpression',
|
|
119
|
+
start,
|
|
120
|
+
end,
|
|
121
|
+
range: [start, end],
|
|
122
|
+
id: null,
|
|
123
|
+
expression: false,
|
|
124
|
+
generator: false,
|
|
125
|
+
async: false,
|
|
126
|
+
params: [],
|
|
127
|
+
body: { type: 'BlockStatement', start, end, range: [start, end], body: nested['body'] },
|
|
128
|
+
};
|
|
129
|
+
args[0] = graft;
|
|
130
|
+
grafts.set(graft, { call });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { code, converted, program: (code) => located(code), functionProgram, graftDirectEvals };
|
|
134
|
+
}
|
|
135
|
+
/** A template literal's cooked text, or null when an escape in it is invalid. */
|
|
136
|
+
function cooked(template) {
|
|
137
|
+
const [quasi] = template['quasis'];
|
|
138
|
+
return quasi['value'].cooked;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* For each UTF-16 offset of a literal's cooked text, from 0 to its length, the offset in `content` (the literal's
|
|
142
|
+
* source between its delimiters) where the character is written. Every unit an escape gives is at the escape's
|
|
143
|
+
* backslash, a line continuation gives nothing, and in a template a CR or CRLF gives one LF.
|
|
144
|
+
*/
|
|
145
|
+
function rawOffsets(content, text, template) {
|
|
146
|
+
const offsets = [];
|
|
147
|
+
let at = 0;
|
|
148
|
+
while (at < content.length) {
|
|
149
|
+
const char = content[at];
|
|
150
|
+
if (char === '\\') {
|
|
151
|
+
const [length, units] = escape(content, at);
|
|
152
|
+
for (let unit = 0; unit < units; unit++)
|
|
153
|
+
offsets.push(at);
|
|
154
|
+
at += length;
|
|
155
|
+
}
|
|
156
|
+
else if (template && char === '\r') {
|
|
157
|
+
offsets.push(at);
|
|
158
|
+
at += content[at + 1] === '\n' ? 2 : 1;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
offsets.push(at);
|
|
162
|
+
at++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
offsets.push(content.length);
|
|
166
|
+
// The escapes were read as the parser reads them, so the lengths agree. Were they ever not to, locations would still
|
|
167
|
+
// stay inside the literal and never decrease.
|
|
168
|
+
if (offsets.length !== text.length + 1) {
|
|
169
|
+
return Array.from({ length: text.length + 1 }, (_, offset) => Math.min(offset, content.length));
|
|
170
|
+
}
|
|
171
|
+
return offsets;
|
|
172
|
+
}
|
|
173
|
+
/** The escape sequence at `at` in a literal's source: how many characters it takes, and how many UTF-16 units it gives. */
|
|
174
|
+
function escape(content, at) {
|
|
175
|
+
const next = content[at + 1] ?? '';
|
|
176
|
+
// A line continuation: a backslash before a line terminator gives nothing.
|
|
177
|
+
if (next === '\r')
|
|
178
|
+
return [content[at + 2] === '\n' ? 3 : 2, 0];
|
|
179
|
+
if (next === '\n' || next === '\u2028' || next === '\u2029')
|
|
180
|
+
return [2, 0];
|
|
181
|
+
if (next === 'x')
|
|
182
|
+
return [4, 1];
|
|
183
|
+
if (next === 'u') {
|
|
184
|
+
if (content[at + 2] !== '{')
|
|
185
|
+
return [6, 1];
|
|
186
|
+
const close = content.indexOf('}', at + 3);
|
|
187
|
+
return [close + 1 - at, Number.parseInt(content.slice(at + 3, close), 16) > 0xffff ? 2 : 1];
|
|
188
|
+
}
|
|
189
|
+
// `\0` and the legacy octal escapes of sloppy strings: up to three octal digits from 0-3, or two from 4-7.
|
|
190
|
+
if (next >= '0' && next <= '7') {
|
|
191
|
+
const most = next <= '3' ? 3 : 2;
|
|
192
|
+
let digits = 1;
|
|
193
|
+
while (digits < most && (content[at + 1 + digits] ?? '') >= '0' && (content[at + 1 + digits] ?? '') <= '7')
|
|
194
|
+
digits++;
|
|
195
|
+
return [1 + digits, 1];
|
|
196
|
+
}
|
|
197
|
+
// Any other escaped character stands for itself: one code point, which may be a surrogate pair.
|
|
198
|
+
const point = content.codePointAt(at + 1) ?? 0;
|
|
199
|
+
return point > 0xffff ? [3, 2] : [2, 1];
|
|
200
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Primitive } from './global-object.ts';
|
|
2
|
+
/** What an operation on constants gives: its value, or that it throws, or that its value passes the `fold-length` cap. */
|
|
3
|
+
export type Outcome = {
|
|
4
|
+
value: Primitive;
|
|
5
|
+
} | 'throws' | 'too-long';
|
|
6
|
+
/** The unary operators that convert their operand to a numeric value (ECMA-262 13.5). */
|
|
7
|
+
export declare const numericUnary: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* The binary operators that convert both operands to numeric values (ECMA-262 ApplyStringOrNumericBinaryOperator),
|
|
10
|
+
* besides `+`, which concatenates when either operand is a string.
|
|
11
|
+
*/
|
|
12
|
+
export declare const numericBinary: ReadonlySet<string>;
|
|
13
|
+
/** `op value` for one of `numericUnary`. */
|
|
14
|
+
export declare function unaryOperation(op: string, value: Primitive, maxLength: number): Outcome;
|
|
15
|
+
/** `a op b` for `+` or one of `numericBinary`. */
|
|
16
|
+
export declare function binaryOperation(op: string, a: Primitive, b: Primitive, maxLength: number): Outcome;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Numeric folding (Principle 2, §5 Resolver capabilities 1): the unary and binary operators on primitive constants, as
|
|
2
|
+
// ECMA-262 defines them. The operation itself is JavaScript's own, so its result, and whether it throws, are the
|
|
3
|
+
// engine's. Two guards come first, because a BigInt can be far too large to compute:
|
|
4
|
+
//
|
|
5
|
+
// - A BigInt result whose decimal string would pass the `fold-length` cap is `too-long`, judged from the operands' bit
|
|
6
|
+
// lengths before it is computed (`10n ** 2000000n`), or from its own once it is (§5, §18).
|
|
7
|
+
// - A BigInt result over V8's own limit, 2^30 bits, makes Chromium throw a RangeError, so it `throws` (Principle 1).
|
|
8
|
+
//
|
|
9
|
+
// A BigInt literal is bounded by its source, so only an operator's result is judged.
|
|
10
|
+
/** The unary operators that convert their operand to a numeric value (ECMA-262 13.5). */
|
|
11
|
+
export const numericUnary = new Set(['-', '+', '~']);
|
|
12
|
+
/**
|
|
13
|
+
* The binary operators that convert both operands to numeric values (ECMA-262 ApplyStringOrNumericBinaryOperator),
|
|
14
|
+
* besides `+`, which concatenates when either operand is a string.
|
|
15
|
+
*/
|
|
16
|
+
export const numericBinary = new Set(['-', '*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>']);
|
|
17
|
+
/** V8's largest BigInt, in bits (`BigInt::kMaxLengthBits`): a result past it throws a RangeError. */
|
|
18
|
+
const maxBigIntBits = 2 ** 30;
|
|
19
|
+
const log10Of2 = Math.log10(2);
|
|
20
|
+
/** `op value` for one of `numericUnary`. */
|
|
21
|
+
export function unaryOperation(op, value, maxLength) {
|
|
22
|
+
const numeric = toNumeric(value);
|
|
23
|
+
if (numeric === 'throws')
|
|
24
|
+
return 'throws';
|
|
25
|
+
// Unary `+` is ToNumber, which a BigInt throws on.
|
|
26
|
+
if (op === '+')
|
|
27
|
+
return typeof numeric === 'bigint' ? 'throws' : { value: numeric };
|
|
28
|
+
if (typeof numeric === 'number')
|
|
29
|
+
return { value: op === '-' ? -numeric : ~numeric };
|
|
30
|
+
return judged(op === '-' ? -numeric : ~numeric, maxLength);
|
|
31
|
+
}
|
|
32
|
+
/** `a op b` for `+` or one of `numericBinary`. */
|
|
33
|
+
export function binaryOperation(op, a, b, maxLength) {
|
|
34
|
+
if (op === '+') {
|
|
35
|
+
// ToPrimitive of a primitive is itself. A symbol throws in ToString and in ToNumeric alike.
|
|
36
|
+
if (typeof a === 'symbol' || typeof b === 'symbol')
|
|
37
|
+
return 'throws';
|
|
38
|
+
if (typeof a === 'string' || typeof b === 'string')
|
|
39
|
+
return { value: String(a) + String(b) };
|
|
40
|
+
}
|
|
41
|
+
const x = toNumeric(a);
|
|
42
|
+
const y = toNumeric(b);
|
|
43
|
+
if (x === 'throws' || y === 'throws')
|
|
44
|
+
return 'throws';
|
|
45
|
+
// Mixing a BigInt and a Number is a TypeError.
|
|
46
|
+
if (typeof x !== typeof y)
|
|
47
|
+
return 'throws';
|
|
48
|
+
if (typeof x === 'number')
|
|
49
|
+
return { value: numberOperation(op, x, y) };
|
|
50
|
+
return bigintOperation(op, x, y, maxLength);
|
|
51
|
+
}
|
|
52
|
+
/** ECMA-262 ToNumeric of a primitive: a BigInt stays one, a symbol throws, and anything else is ToNumber'd. */
|
|
53
|
+
function toNumeric(value) {
|
|
54
|
+
if (typeof value === 'bigint')
|
|
55
|
+
return value;
|
|
56
|
+
if (typeof value === 'symbol')
|
|
57
|
+
return 'throws';
|
|
58
|
+
return Number(value);
|
|
59
|
+
}
|
|
60
|
+
function numberOperation(op, x, y) {
|
|
61
|
+
switch (op) {
|
|
62
|
+
case '+':
|
|
63
|
+
return x + y;
|
|
64
|
+
case '-':
|
|
65
|
+
return x - y;
|
|
66
|
+
case '*':
|
|
67
|
+
return x * y;
|
|
68
|
+
case '/':
|
|
69
|
+
return x / y;
|
|
70
|
+
case '%':
|
|
71
|
+
return x % y;
|
|
72
|
+
case '**':
|
|
73
|
+
return x ** y;
|
|
74
|
+
case '&':
|
|
75
|
+
return x & y;
|
|
76
|
+
case '|':
|
|
77
|
+
return x | y;
|
|
78
|
+
case '^':
|
|
79
|
+
return x ^ y;
|
|
80
|
+
case '<<':
|
|
81
|
+
return x << y;
|
|
82
|
+
case '>>':
|
|
83
|
+
return x >> y;
|
|
84
|
+
default:
|
|
85
|
+
return x >>> y;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A BigInt operation. One that can grow its result past what the operands' sizes show (`*`, `**`, `<<`, a `>>` by a
|
|
90
|
+
* negative amount) is judged from bit lengths first.
|
|
91
|
+
*/
|
|
92
|
+
function bigintOperation(op, x, y, maxLength) {
|
|
93
|
+
const before = beforeComputing(op, x, y, maxLength);
|
|
94
|
+
if (before !== undefined)
|
|
95
|
+
return before;
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = computed(op, x, y);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// `/` or `%` by zero, a negative exponent, or `>>>`, which BigInts don't have.
|
|
102
|
+
return 'throws';
|
|
103
|
+
}
|
|
104
|
+
return judged(value, maxLength);
|
|
105
|
+
}
|
|
106
|
+
function computed(op, x, y) {
|
|
107
|
+
switch (op) {
|
|
108
|
+
case '+':
|
|
109
|
+
return x + y;
|
|
110
|
+
case '-':
|
|
111
|
+
return x - y;
|
|
112
|
+
case '*':
|
|
113
|
+
return x * y;
|
|
114
|
+
case '/':
|
|
115
|
+
return x / y;
|
|
116
|
+
case '%':
|
|
117
|
+
return x % y;
|
|
118
|
+
case '**':
|
|
119
|
+
return x ** y;
|
|
120
|
+
case '&':
|
|
121
|
+
return x & y;
|
|
122
|
+
case '|':
|
|
123
|
+
return x | y;
|
|
124
|
+
case '^':
|
|
125
|
+
return x ^ y;
|
|
126
|
+
case '<<':
|
|
127
|
+
return x << y;
|
|
128
|
+
case '>>':
|
|
129
|
+
return x >> y;
|
|
130
|
+
default:
|
|
131
|
+
// `>>>` throws a TypeError on BigInts.
|
|
132
|
+
throw new TypeError('BigInts have no unsigned right shift');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* What a growing BigInt operation gives before it is computed: `throws` when its result's bit length is surely past
|
|
137
|
+
* V8's limit, `too-long` when its decimal string surely passes the cap, and undefined when it is small enough to compute.
|
|
138
|
+
* `bits` is the least bit length the result can have.
|
|
139
|
+
*/
|
|
140
|
+
function beforeComputing(op, x, y, maxLength) {
|
|
141
|
+
const bits = leastBits(op, x, y);
|
|
142
|
+
if (bits === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
if (bits > maxBigIntBits)
|
|
145
|
+
return 'throws';
|
|
146
|
+
return leastDigits(bits) > maxLength ? 'too-long' : undefined;
|
|
147
|
+
}
|
|
148
|
+
/** The least bit length the result of a growing operation can have, or undefined when it can't grow much. */
|
|
149
|
+
function leastBits(op, x, y) {
|
|
150
|
+
switch (op) {
|
|
151
|
+
case '*':
|
|
152
|
+
return x === 0n || y === 0n ? undefined : bitLength(x) + bitLength(y) - 1;
|
|
153
|
+
case '**': {
|
|
154
|
+
// A negative exponent throws, and a base of 0, 1 or -1 stays small.
|
|
155
|
+
if (y <= 0n || x === 0n || x === 1n || x === -1n)
|
|
156
|
+
return undefined;
|
|
157
|
+
// V8 refuses an exponent of 2^30 or more for any other base, whatever the result's size.
|
|
158
|
+
if (y >= BigInt(maxBigIntBits))
|
|
159
|
+
return Infinity;
|
|
160
|
+
const exponent = Number(y);
|
|
161
|
+
const magnitude = x < 0n ? -x : x;
|
|
162
|
+
// A power of 2, 2^k, gives exactly k * y + 1 bits.
|
|
163
|
+
if ((magnitude & (magnitude - 1n)) === 0n)
|
|
164
|
+
return (bitLength(magnitude) - 1) * exponent + 1;
|
|
165
|
+
// Any other |x| ** y has floor(y * log2|x|) + 1 bits. The estimate's floating-point error is below a relative
|
|
166
|
+
// 1e-14, so lowering it by a relative 1e-12 keeps it from rising past the exact count.
|
|
167
|
+
const estimate = exponent * log2(x);
|
|
168
|
+
return Math.floor(estimate * (1 - 1e-12)) + 1;
|
|
169
|
+
}
|
|
170
|
+
case '<<':
|
|
171
|
+
case '>>': {
|
|
172
|
+
const shift = op === '<<' ? y : -y;
|
|
173
|
+
if (shift <= 0n || x === 0n)
|
|
174
|
+
return undefined;
|
|
175
|
+
// Exactly, since shifting left multiplies by a power of 2.
|
|
176
|
+
return bitLength(x) + Number(shift);
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** A computed BigInt: `too-long` when its decimal string passes the cap, else its value. */
|
|
183
|
+
function judged(value, maxLength) {
|
|
184
|
+
const bits = bitLength(value);
|
|
185
|
+
// A string that surely fits, sign included, isn't made.
|
|
186
|
+
if (mostDigits(bits) + 1 <= maxLength)
|
|
187
|
+
return { value };
|
|
188
|
+
if (leastDigits(bits) > maxLength)
|
|
189
|
+
return 'too-long';
|
|
190
|
+
return String(value).length > maxLength ? 'too-long' : { value };
|
|
191
|
+
}
|
|
192
|
+
/** The bit length of |x|: 0 for 0n. */
|
|
193
|
+
function bitLength(x) {
|
|
194
|
+
if (x === 0n)
|
|
195
|
+
return 0;
|
|
196
|
+
const hex = (x < 0n ? -x : x).toString(16);
|
|
197
|
+
return (hex.length - 1) * 4 + Number.parseInt(hex[0], 16).toString(2).length;
|
|
198
|
+
}
|
|
199
|
+
/** log2 |x| of a BigInt other than 0, from its leading 52 bits. */
|
|
200
|
+
function log2(x) {
|
|
201
|
+
const bits = bitLength(x);
|
|
202
|
+
const magnitude = x < 0n ? -x : x;
|
|
203
|
+
const shift = Math.max(0, bits - 53);
|
|
204
|
+
return shift + Math.log2(Number(magnitude >> BigInt(shift)));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The fewest and most decimal digits a number of `bits` bits can have: |x| is in [2^(bits-1), 2^bits). The margin keeps
|
|
208
|
+
* floating-point error from deciding either bound wrongly, so a result between them is measured.
|
|
209
|
+
*/
|
|
210
|
+
const margin = 1e-6;
|
|
211
|
+
function leastDigits(bits) {
|
|
212
|
+
return bits === 0 ? 1 : Math.floor((bits - 1) * log10Of2 - margin) + 1;
|
|
213
|
+
}
|
|
214
|
+
function mostDigits(bits) {
|
|
215
|
+
return bits === 0 ? 1 : Math.floor(bits * log10Of2 + margin) + 1;
|
|
216
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AstNode } from './ast.ts';
|
|
2
|
+
import type { Definitions, VariableKey } from './definitions.ts';
|
|
3
|
+
import { type CallGraph } from './summaries.ts';
|
|
4
|
+
export interface OnceFlags {
|
|
5
|
+
/** Whether a variable is a once-flag: some function tests it and then assigns it on the path the test lets through. */
|
|
6
|
+
isFlag(key: VariableKey): boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `maxCallDepth` is how many calls deep an assignment is followed from the path a test lets through (`flagDepth`, §18),
|
|
10
|
+
* and `depthCut` hears each function it keeps from being followed, a hit of that cap.
|
|
11
|
+
*/
|
|
12
|
+
export declare function onceFlags(program: AstNode, definitions: Definitions, calls: CallGraph, maxCallDepth: number, depthCut?: (fn: AstNode) => void): OnceFlags;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Once-flags (§5 Resolver capabilities 6). A once-flag is recognized by use: a variable or parameter is a flag when a
|
|
2
|
+
// function tests it (`if`, `?:`, `&&`, `||`, `!`, an early return) and then assigns it on the path that test lets
|
|
3
|
+
// through, directly or in a call on that path, whatever value it assigns. A flag's tests are never decided as
|
|
4
|
+
// constants, so Rollup's `if (Ki) return Xi; Ki = 1`, `ge || (ge = 1, …)` and esbuild's `t && (e = t(t = 0))` keep the
|
|
5
|
+
// code after them. A variable that no function tests before assigning it (esbuild's init-thunk constants, `Nl = 4`) is
|
|
6
|
+
// no flag, and its tests still decide. Whether the variable reads a catalog native, which keeps its test a §7 guard, is
|
|
7
|
+
// the evaluator's to judge from its values.
|
|
8
|
+
import { walkTree } from "./ast.js";
|
|
9
|
+
import { isFunction } from "./summaries.js";
|
|
10
|
+
/**
|
|
11
|
+
* `maxCallDepth` is how many calls deep an assignment is followed from the path a test lets through (`flagDepth`, §18),
|
|
12
|
+
* and `depthCut` hears each function it keeps from being followed, a hit of that cap.
|
|
13
|
+
*/
|
|
14
|
+
export function onceFlags(program, definitions, calls, maxCallDepth, depthCut = () => { }) {
|
|
15
|
+
/** The nodes that test a name (an `if`, a `?:`, a `&&`/`||`, a `||=`/`&&=`), by the names they test. */
|
|
16
|
+
let testers;
|
|
17
|
+
/** Each statement's list and index in it, for the statements after an early return. */
|
|
18
|
+
const lists = new Map();
|
|
19
|
+
const memo = new Map();
|
|
20
|
+
function indexed() {
|
|
21
|
+
if (testers !== undefined)
|
|
22
|
+
return testers;
|
|
23
|
+
const found = new Map();
|
|
24
|
+
const add = (tester, test) => {
|
|
25
|
+
for (const id of testedNames(test)) {
|
|
26
|
+
const name = id['name'];
|
|
27
|
+
const list = found.get(name);
|
|
28
|
+
if (list === undefined)
|
|
29
|
+
found.set(name, [tester]);
|
|
30
|
+
else if (list.at(-1) !== tester)
|
|
31
|
+
list.push(tester);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walkTree(program, {
|
|
35
|
+
enter(node) {
|
|
36
|
+
for (const key of ['body', 'consequent']) {
|
|
37
|
+
const list = node[key];
|
|
38
|
+
if (Array.isArray(list))
|
|
39
|
+
list.forEach((statement, at) => lists.set(statement, { list: list, at }));
|
|
40
|
+
}
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case 'IfStatement':
|
|
43
|
+
case 'ConditionalExpression':
|
|
44
|
+
add(node, node['test']);
|
|
45
|
+
break;
|
|
46
|
+
case 'LogicalExpression':
|
|
47
|
+
if (node['operator'] !== '??')
|
|
48
|
+
add(node, node['left']);
|
|
49
|
+
break;
|
|
50
|
+
case 'AssignmentExpression':
|
|
51
|
+
if (node['operator'] === '||=' || node['operator'] === '&&=')
|
|
52
|
+
add(node, node['left']);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return (testers = found);
|
|
58
|
+
}
|
|
59
|
+
/** Whether a test reads the variable `key` as a test does: itself, under `!`, `typeof` or a comparison. */
|
|
60
|
+
const tests = (test, key) => testedNames(test).some((id) => {
|
|
61
|
+
const read = definitions.keyOf(id);
|
|
62
|
+
return read !== undefined && read.scope === key.scope && read.name === key.name;
|
|
63
|
+
});
|
|
64
|
+
/** The code a test lets through: its arms, the right side, and the statements after an arm that always leaves. */
|
|
65
|
+
function letThrough(tester) {
|
|
66
|
+
switch (tester.type) {
|
|
67
|
+
case 'IfStatement': {
|
|
68
|
+
const consequent = tester['consequent'];
|
|
69
|
+
const alternate = tester['alternate'];
|
|
70
|
+
const arms = alternate === null ? [consequent] : [consequent, alternate];
|
|
71
|
+
const place = lists.get(tester);
|
|
72
|
+
const early = place !== undefined && arms.some(leaves);
|
|
73
|
+
return early ? [...arms, ...place.list.slice(place.at + 1)] : arms;
|
|
74
|
+
}
|
|
75
|
+
case 'ConditionalExpression':
|
|
76
|
+
return [tester['consequent'], tester['alternate']];
|
|
77
|
+
case 'LogicalExpression':
|
|
78
|
+
return [tester['right']];
|
|
79
|
+
default:
|
|
80
|
+
// `k ||= …` assigns what it tests.
|
|
81
|
+
return [tester];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isFlag(key) {
|
|
85
|
+
let byName = memo.get(key.scope);
|
|
86
|
+
if (byName === undefined)
|
|
87
|
+
memo.set(key.scope, (byName = new Map()));
|
|
88
|
+
let flag = byName.get(key.name);
|
|
89
|
+
if (flag === undefined)
|
|
90
|
+
byName.set(key.name, (flag = recognized(key)));
|
|
91
|
+
return flag;
|
|
92
|
+
}
|
|
93
|
+
function recognized(key) {
|
|
94
|
+
const candidates = indexed().get(key.name);
|
|
95
|
+
if (candidates === undefined)
|
|
96
|
+
return false;
|
|
97
|
+
const all = definitions.of(key);
|
|
98
|
+
if (all.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
/** The functions whose own code assigns the variable. */
|
|
101
|
+
const owners = new Set(all.map((definition) => calls.functionOf(definition.site) ?? program));
|
|
102
|
+
const assigning = new Map();
|
|
103
|
+
/** Whether a call of `fn` assigns the variable, in `fn`'s own code or in a call it makes, `depth` calls deep. */
|
|
104
|
+
const assigns = (fn, depth) => {
|
|
105
|
+
if (owners.has(fn))
|
|
106
|
+
return true;
|
|
107
|
+
if (depth >= maxCallDepth) {
|
|
108
|
+
depthCut(fn);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const known = assigning.get(fn);
|
|
112
|
+
if (known !== undefined)
|
|
113
|
+
return known;
|
|
114
|
+
assigning.set(fn, false);
|
|
115
|
+
const body = fn['body'];
|
|
116
|
+
const result = callsIn(body, fn).some((call) => calls.sitesOf(call).some((site) => assigns(site.fn, depth + 1)));
|
|
117
|
+
assigning.set(fn, result);
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
for (const tester of candidates) {
|
|
121
|
+
if (!tests(tester.type === 'IfStatement' || tester.type === 'ConditionalExpression' ? tester['test'] : tester['left'], key)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fn = calls.functionOf(tester) ?? program;
|
|
125
|
+
for (const region of letThrough(tester)) {
|
|
126
|
+
// An assignment in the tester's own code on that path.
|
|
127
|
+
const direct = all.some((definition) => region.start <= definition.site.start &&
|
|
128
|
+
definition.site.end <= region.end &&
|
|
129
|
+
(calls.functionOf(definition.site) ?? program) === fn);
|
|
130
|
+
if (direct)
|
|
131
|
+
return true;
|
|
132
|
+
// Or in a call made on that path.
|
|
133
|
+
if (callsIn(region, fn).some((call) => calls.sitesOf(call).some((site) => assigns(site.fn, 1))))
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return { isFlag };
|
|
140
|
+
}
|
|
141
|
+
/** The calls and `new`s in `node` that are the own code of `fn`: not in a function nested in it. */
|
|
142
|
+
function callsIn(node, fn) {
|
|
143
|
+
const found = [];
|
|
144
|
+
walkTree(node, {
|
|
145
|
+
enter(child) {
|
|
146
|
+
if (child !== node && child !== fn && isFunction(child))
|
|
147
|
+
return false;
|
|
148
|
+
if (child.type === 'CallExpression' || child.type === 'NewExpression')
|
|
149
|
+
found.push(child);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
/** The identifiers a test reads as a test does: the test itself, under `!` or `typeof`, compared, or in `&&`/`||`. */
|
|
155
|
+
function testedNames(test) {
|
|
156
|
+
switch (test.type) {
|
|
157
|
+
case 'Identifier':
|
|
158
|
+
return [test];
|
|
159
|
+
case 'UnaryExpression':
|
|
160
|
+
return test['operator'] === '!' || test['operator'] === 'typeof' ? testedNames(test['argument']) : [];
|
|
161
|
+
case 'BinaryExpression':
|
|
162
|
+
return ['==', '!=', '===', '!==', '<', '>', '<=', '>='].includes(test['operator'])
|
|
163
|
+
? [...testedNames(test['left']), ...testedNames(test['right'])].filter((id) => !isLiteralName(id))
|
|
164
|
+
: [];
|
|
165
|
+
case 'LogicalExpression':
|
|
166
|
+
return [...testedNames(test['left']), ...testedNames(test['right'])];
|
|
167
|
+
case 'SequenceExpression':
|
|
168
|
+
return testedNames(test['expressions'].at(-1));
|
|
169
|
+
default:
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** `undefined`, `NaN` and `Infinity`, which a comparison reads as values rather than tests. */
|
|
174
|
+
const isLiteralName = (id) => ['undefined', 'NaN', 'Infinity'].includes(id['name']);
|
|
175
|
+
/** Whether a statement always leaves the code after it: a `return` or `throw`, or a block or `if` that always does. */
|
|
176
|
+
function leaves(statement) {
|
|
177
|
+
switch (statement.type) {
|
|
178
|
+
case 'ReturnStatement':
|
|
179
|
+
case 'ThrowStatement':
|
|
180
|
+
return true;
|
|
181
|
+
case 'BlockStatement':
|
|
182
|
+
return statement['body'].some(leaves);
|
|
183
|
+
case 'IfStatement': {
|
|
184
|
+
const alternate = statement['alternate'];
|
|
185
|
+
return alternate !== null && leaves(statement['consequent']) && leaves(alternate);
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|