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,1295 @@
|
|
|
1
|
+
// Guard classification and reachability values (§7 Values, §14 `guard`). A write's reachability is judged from the
|
|
2
|
+
// guards between it and the start of the function it is in: the tests of the `if`, `?:`, `&&`, `||` and `??` arms it
|
|
3
|
+
// sits in, the early `return`s and `throw`s before it in each statement list on its way, and its own `||=`, `??=`,
|
|
4
|
+
// `x = x || p` and `x = x ? x : p` forms. Each guard is classified and gives a value of its own; values combine along a
|
|
5
|
+
// path (AND) and across alternatives (OR).
|
|
6
|
+
//
|
|
7
|
+
// Along call chains (§7 Where reachability is judged from), the function a write is in is reached through its resolved
|
|
8
|
+
// calls, out to the **install entry**: the outermost function on the chain with no resolved caller, or the top-level
|
|
9
|
+
// code. The guards between each call and the start of the function it is in count along the path, judged in the
|
|
10
|
+
// caller's frame when the resolver has one, so a guard inside a helper summary counts too. Several callers are
|
|
11
|
+
// alternatives, and the most-reaching wins.
|
|
12
|
+
import { walkTree } from "./ast.js";
|
|
13
|
+
import { mayThrow } from "./catch-arms.js";
|
|
14
|
+
import { argumentDecided, isSwept, passesLiteral, settlingLiterals, truthiness, } from "./global-object.js";
|
|
15
|
+
import { memberName, symbolMember, toPropertyKey } from "./keys.js";
|
|
16
|
+
import { reachedAfter } from "./early-exits.js";
|
|
17
|
+
import { locate } from "./line-starts.js";
|
|
18
|
+
import { printed } from "./print.js";
|
|
19
|
+
import { boundIdentifiers } from "./scope.js";
|
|
20
|
+
import { frameOf, isFunction } from "./summaries.js";
|
|
21
|
+
/** The order along one path (AND), the strongest last: `guarded-native-absence` > `unknown` > `conditional` > none. */
|
|
22
|
+
const alongOrder = ['unconditional', 'conditional', 'unknown', 'guarded-native-absence'];
|
|
23
|
+
/** The order across alternatives (OR), the most-reaching last: `unconditional` > `conditional` > `unknown` > absence. */
|
|
24
|
+
const acrossOrder = ['guarded-native-absence', 'unknown', 'conditional', 'unconditional'];
|
|
25
|
+
/** Two guards on one path (AND): `config.polyfill && !window.fetch` is `guarded-native-absence` (§7). */
|
|
26
|
+
export function along(a, b) {
|
|
27
|
+
return alongOrder.indexOf(a) >= alongOrder.indexOf(b) ? a : b;
|
|
28
|
+
}
|
|
29
|
+
/** Two alternatives (OR): the most-reaching, so `!window.fetch || force` is `conditional` (§7). */
|
|
30
|
+
export function across(a, b) {
|
|
31
|
+
return acrossOrder.indexOf(a) >= acrossOrder.indexOf(b) ? a : b;
|
|
32
|
+
}
|
|
33
|
+
/** The value each class of guard gives on its own (§7, §14). */
|
|
34
|
+
const results = {
|
|
35
|
+
transparent: 'unconditional',
|
|
36
|
+
absence: 'guarded-native-absence',
|
|
37
|
+
'broken-native': 'conditional',
|
|
38
|
+
config: 'conditional',
|
|
39
|
+
'other-target': 'conditional',
|
|
40
|
+
undecided: 'conditional',
|
|
41
|
+
unknown: 'unknown',
|
|
42
|
+
};
|
|
43
|
+
const unconditional = { reachability: 'unconditional', steps: [], tests: new Map() };
|
|
44
|
+
/**
|
|
45
|
+
* The reachability of a write, judged from the guards between it and the start of its function (§7). `path` is the
|
|
46
|
+
* walk's ancestors of the node that makes the write, outermost first, ending with that node.
|
|
47
|
+
*/
|
|
48
|
+
export function judge(path, write, at, context) {
|
|
49
|
+
const guards = [];
|
|
50
|
+
let from = 0;
|
|
51
|
+
// The site itself, which is a function when it is where a function was created, opens no function of its own.
|
|
52
|
+
for (let index = path.length - 2; index >= 0; index--) {
|
|
53
|
+
if (isFunction(path[index])) {
|
|
54
|
+
from = index;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// A `catch` arm on the path whose `try` block can't throw is never reached (§5 Resolver capabilities 9).
|
|
59
|
+
let unreachedArm;
|
|
60
|
+
for (let index = from + 1; index < path.length; index++) {
|
|
61
|
+
const parent = path[index - 1];
|
|
62
|
+
const child = path[index];
|
|
63
|
+
guards.push(...edgeGuards(parent, child));
|
|
64
|
+
if (unreachedArm === undefined && parent.type === 'TryStatement' && child === parent['handler']) {
|
|
65
|
+
if (!mayThrow(parent['block'], throwContext(at, context)))
|
|
66
|
+
unreachedArm = child;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
guards.push(...ownGuards(write, context));
|
|
70
|
+
const classifier = classifierOf(write, at, context);
|
|
71
|
+
let reachability = 'unconditional';
|
|
72
|
+
const steps = [];
|
|
73
|
+
const tests = new Map();
|
|
74
|
+
let dead = unreachedArm;
|
|
75
|
+
let by;
|
|
76
|
+
if (unreachedArm !== undefined)
|
|
77
|
+
unreachedArms.add(unreachedArm);
|
|
78
|
+
// `with` and direct-`eval` taint on the write's own names (§5 post-pass 3).
|
|
79
|
+
for (const site of write.taints) {
|
|
80
|
+
steps.push(classifier.taintStep(site));
|
|
81
|
+
reachability = along(reachability, 'unknown');
|
|
82
|
+
}
|
|
83
|
+
for (const guard of guards) {
|
|
84
|
+
const classified = classifier.condition(guard);
|
|
85
|
+
reachability = along(reachability, classified.result);
|
|
86
|
+
steps.push(...classified.steps);
|
|
87
|
+
classified.tests.forEach((test, index) => tests.set(test, classified.steps[index]));
|
|
88
|
+
if (dead === undefined && classified.dead !== undefined)
|
|
89
|
+
({ dead, by } = classified);
|
|
90
|
+
}
|
|
91
|
+
if (steps.length === 0 && dead === undefined)
|
|
92
|
+
return unconditional;
|
|
93
|
+
return { reachability, steps, tests, ...(dead === undefined ? {} : { dead }), ...(by === undefined ? {} : { by }) };
|
|
94
|
+
}
|
|
95
|
+
/** Two tests on one path (AND): it is dead when either is. */
|
|
96
|
+
function bothOf(a, b) {
|
|
97
|
+
const { dead, by } = a.dead !== undefined ? a : b;
|
|
98
|
+
return {
|
|
99
|
+
result: along(a.result, b.result),
|
|
100
|
+
steps: [...a.steps, ...b.steps],
|
|
101
|
+
tests: [...a.tests, ...b.tests],
|
|
102
|
+
...(dead === undefined ? {} : { dead }),
|
|
103
|
+
...(by === undefined ? {} : { by }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Two alternatives (OR): a dead one never holds, so the other decides; it is dead only when both are. */
|
|
107
|
+
function eitherOf(a, b) {
|
|
108
|
+
const steps = [...a.steps, ...b.steps];
|
|
109
|
+
const tests = [...a.tests, ...b.tests];
|
|
110
|
+
if (a.dead !== undefined && b.dead !== undefined) {
|
|
111
|
+
return { result: a.result, steps, tests, dead: a.dead, ...(a.by === undefined ? {} : { by: a.by }) };
|
|
112
|
+
}
|
|
113
|
+
const result = a.dead !== undefined ? b.result : b.dead !== undefined ? a.result : across(a.result, b.result);
|
|
114
|
+
return { result, steps, tests };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The guards an edge of the path adds: an arm's test, or for a statement in a list, the condition under which every
|
|
118
|
+
* statement before it fell through (the early `return`s and `throw`s before it).
|
|
119
|
+
*/
|
|
120
|
+
function edgeGuards(parent, child) {
|
|
121
|
+
switch (parent.type) {
|
|
122
|
+
case 'IfStatement':
|
|
123
|
+
case 'ConditionalExpression':
|
|
124
|
+
if (child === parent['consequent'])
|
|
125
|
+
return [{ test: parent['test'], outcome: true }];
|
|
126
|
+
if (child === parent['alternate'])
|
|
127
|
+
return [{ test: parent['test'], outcome: false }];
|
|
128
|
+
return [];
|
|
129
|
+
case 'LogicalExpression':
|
|
130
|
+
if (child !== parent['right'])
|
|
131
|
+
return [];
|
|
132
|
+
return [{ test: parent['left'], outcome: parent['operator'] === '&&' }];
|
|
133
|
+
default: {
|
|
134
|
+
const reached = reachedAfter(parent, child);
|
|
135
|
+
return reached === true || reached === false ? [] : [reached];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The guards a write's own form adds: `x ||= p` and `x ??= p` run when `x` is missing and `x &&= p` when it is there,
|
|
141
|
+
* and `x = x || p`, `x = x ?? p` and `x = c ? x : p` change `x` only on the arm that doesn't keep it (§7).
|
|
142
|
+
*/
|
|
143
|
+
function ownGuards({ site }, { text }) {
|
|
144
|
+
if (site.type !== 'AssignmentExpression')
|
|
145
|
+
return [];
|
|
146
|
+
const left = site['left'];
|
|
147
|
+
switch (site['operator']) {
|
|
148
|
+
case '||=':
|
|
149
|
+
case '??=':
|
|
150
|
+
return [{ test: left, outcome: false }];
|
|
151
|
+
case '&&=':
|
|
152
|
+
return [{ test: left, outcome: true }];
|
|
153
|
+
case '=':
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const same = (node) => sourceOf(node, text) === sourceOf(left, text);
|
|
159
|
+
let right = site['right'];
|
|
160
|
+
while (right.type === 'SequenceExpression')
|
|
161
|
+
right = right['expressions'].at(-1);
|
|
162
|
+
if (right.type === 'LogicalExpression' && right['operator'] !== '&&' && same(right['left'])) {
|
|
163
|
+
return [{ test: right['left'], outcome: false }];
|
|
164
|
+
}
|
|
165
|
+
if (right.type === 'ConditionalExpression') {
|
|
166
|
+
const test = right['test'];
|
|
167
|
+
if (same(right['consequent']))
|
|
168
|
+
return [{ test, outcome: false }];
|
|
169
|
+
if (same(right['alternate']))
|
|
170
|
+
return [{ test, outcome: true }];
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
/** An expression's source text, without its whitespace. */
|
|
175
|
+
const sourceOf = (node, text) => text.slice(node.start, node.end).replace(/\s+/g, '');
|
|
176
|
+
/** The comparison operators a `typeof` or `null` test is written with. */
|
|
177
|
+
const equalities = new Set(['==', '!=', '===', '!==']);
|
|
178
|
+
const comparisons = new Set([...equalities, '<', '>', '<=', '>=']);
|
|
179
|
+
/** Names whose source marks a test that judges a native broken rather than missing (§7): core-js, `isNative…`. */
|
|
180
|
+
const brokenNames = /\bFORCED\b|\bisForced\b|\bisNative\w*|native code/;
|
|
181
|
+
/** Names whose source marks a writability check (§7): `isReadOnly`, `isPropertyWritable`, a descriptor's `writable`. */
|
|
182
|
+
const writabilityNames = /\bis\w*(?:Writable|ReadOnly)\b|\.writable\b/i;
|
|
183
|
+
function classifierOf(write, at, context) {
|
|
184
|
+
const { resolution, scope, catalog, realm, global, text, starts } = context;
|
|
185
|
+
// The most reads of a test the classifier evaluates to find a native read in it.
|
|
186
|
+
const maxReads = context.caps.testReads;
|
|
187
|
+
const targets = catalog.targets[realm];
|
|
188
|
+
const holders = catalog.holders[realm];
|
|
189
|
+
const instanceMembers = catalog.instanceMembers[realm];
|
|
190
|
+
const dropped = catalog.retained.dropped[realm];
|
|
191
|
+
const evaluated = new Map();
|
|
192
|
+
const evaluate = (node) => {
|
|
193
|
+
let values = evaluated.get(node);
|
|
194
|
+
if (values === undefined)
|
|
195
|
+
evaluated.set(node, (values = resolution.evaluate(node, at)));
|
|
196
|
+
return values;
|
|
197
|
+
};
|
|
198
|
+
/** The step of a `with` statement or direct `eval` call on a name's resolution path: `unknown` (§7). */
|
|
199
|
+
const taintStep = (generated) => {
|
|
200
|
+
const expression = text.startsWith('with', generated.start) ? 'with (…)' : 'eval(…)';
|
|
201
|
+
return {
|
|
202
|
+
kind: 'guard',
|
|
203
|
+
expression,
|
|
204
|
+
generated,
|
|
205
|
+
class: 'unknown',
|
|
206
|
+
result: 'unknown',
|
|
207
|
+
text: `\`${expression}\` at ${generated.line}:${generated.column} lies on the resolution path of the write's names, ` +
|
|
208
|
+
'so where the write lands is control flow the analyzer does not model: unknown',
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
const step = (node, cls, tested) => {
|
|
212
|
+
const generated = locate(starts, node.start, node.end);
|
|
213
|
+
const expression = printed(node);
|
|
214
|
+
const result = results[cls];
|
|
215
|
+
const what = tested === undefined ? '' : ` on \`${tested}\``;
|
|
216
|
+
return {
|
|
217
|
+
kind: 'guard',
|
|
218
|
+
expression,
|
|
219
|
+
generated,
|
|
220
|
+
class: cls,
|
|
221
|
+
...(tested === undefined ? {} : { tested }),
|
|
222
|
+
result,
|
|
223
|
+
text: `the guard \`${expression}\` at ${generated.line}:${generated.column} is ${wording[cls]}${what}, so it gives ${result}`,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const leaf = (node, cls, tested) => ({
|
|
227
|
+
result: results[cls],
|
|
228
|
+
steps: [step(node, cls, tested)],
|
|
229
|
+
tests: [node],
|
|
230
|
+
});
|
|
231
|
+
/** The catalog name of the native a value is, if it is one: a target (`Window.fetch`) or a holder. */
|
|
232
|
+
const nativeName = (values) => {
|
|
233
|
+
for (const value of values) {
|
|
234
|
+
if (value.kind === 'member')
|
|
235
|
+
return value.target;
|
|
236
|
+
if (value.kind === 'native')
|
|
237
|
+
return value.name;
|
|
238
|
+
if (value.kind === 'holder') {
|
|
239
|
+
if (value.holder === global)
|
|
240
|
+
return global;
|
|
241
|
+
const name = memberName(global, value.holder);
|
|
242
|
+
return Object.hasOwn(targets, name) ? name : value.holder;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
};
|
|
247
|
+
/** `typeof` of a native, as its catalog entry says: a constructor or function is a function. */
|
|
248
|
+
const nativeType = (values) => {
|
|
249
|
+
for (const value of values) {
|
|
250
|
+
if (value.kind === 'member' || value.kind === 'native')
|
|
251
|
+
return 'function';
|
|
252
|
+
if (value.kind === 'holder')
|
|
253
|
+
return holders[value.holder]?.kind === 'constructor' ? 'function' : 'object';
|
|
254
|
+
}
|
|
255
|
+
return 'object';
|
|
256
|
+
};
|
|
257
|
+
const flagged = (values) => values.some((value) => 'folds' in value && (value.folds?.some((fold) => fold.flag === true) ?? false));
|
|
258
|
+
/** Whether a test's values are a once-flag's: each read through the flag, but those the analyzer can't know at all. */
|
|
259
|
+
const flagTest = (values) => flagged(values) && values.every((value) => value.kind === 'unknown' || flagged([value]));
|
|
260
|
+
const heapRead = (values) => values.some((value) => 'folds' in value && (value.folds?.some((fold) => fold.heap === true && fold.settled !== true) ?? false));
|
|
261
|
+
const decided = (values) => values.length > 0 && values.every((value) => value.kind === 'constant') && !heapRead(values) && !flagged(values);
|
|
262
|
+
/** Whether a test's values are all objects the code made, which are truthy whatever their fields (ECMA-262 ToBoolean). */
|
|
263
|
+
const objects = (values) => values.length > 0 && values.every((value) => value.kind === 'object') && !heapRead(values) && !flagged(values);
|
|
264
|
+
/** Whether the tested native is the written target, or a holder on its way: its own presence is transparent. */
|
|
265
|
+
const ownsTarget = (tested) => {
|
|
266
|
+
if (tested === global)
|
|
267
|
+
return true;
|
|
268
|
+
const bare = tested.startsWith(`${global}.`) ? tested.slice(global.length + 1) : tested;
|
|
269
|
+
return [write.target.name, write.target.shadows].some((name) => name !== undefined &&
|
|
270
|
+
(name === tested || name === bare || name.startsWith(`${tested}.`) || name.startsWith(`${bare}.`)));
|
|
271
|
+
};
|
|
272
|
+
const presence = (node, { tested, present }) => present
|
|
273
|
+
? leaf(node, ownsTarget(tested) ? 'transparent' : 'other-target', tested)
|
|
274
|
+
: leaf(node, 'absence', tested);
|
|
275
|
+
/** Whether an identifier read in a test resolves through a `with` or a direct `eval` (§5 post-pass 3). */
|
|
276
|
+
const tainted = (node) => {
|
|
277
|
+
let found = false;
|
|
278
|
+
walkReads(node, (read) => {
|
|
279
|
+
if (read.type !== 'Identifier')
|
|
280
|
+
return;
|
|
281
|
+
const binding = scope.resolve(read);
|
|
282
|
+
if (binding !== 'local' && (binding.withs.length > 0 || binding.evals.length > 0))
|
|
283
|
+
found = true;
|
|
284
|
+
});
|
|
285
|
+
return found;
|
|
286
|
+
};
|
|
287
|
+
/** Whether a test reads a native: a holder, a native function or a catalog member, anywhere in it. */
|
|
288
|
+
const readsNative = (node) => {
|
|
289
|
+
let found = false;
|
|
290
|
+
let count = 0;
|
|
291
|
+
walkReads(node, (read) => {
|
|
292
|
+
if (found)
|
|
293
|
+
return;
|
|
294
|
+
if (count++ >= maxReads)
|
|
295
|
+
return context.capHit('testReads', node);
|
|
296
|
+
if (read.type === 'Identifier' || read.type === 'MemberExpression' || read.type === 'ThisExpression') {
|
|
297
|
+
if (nativeName(evaluate(read)) !== undefined)
|
|
298
|
+
found = true;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return found;
|
|
302
|
+
};
|
|
303
|
+
/** Whether a test calls something the analyzer can't resolve: no native and no function it knows. */
|
|
304
|
+
const unresolvedCall = (node) => {
|
|
305
|
+
let found = false;
|
|
306
|
+
walkReads(node, (read) => {
|
|
307
|
+
if (found || (read.type !== 'CallExpression' && read.type !== 'NewExpression'))
|
|
308
|
+
return;
|
|
309
|
+
const callee = evaluate(read['callee']);
|
|
310
|
+
if (!callee.some((value) => value.kind !== 'unknown'))
|
|
311
|
+
found = true;
|
|
312
|
+
});
|
|
313
|
+
return found;
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Whether a test calls a catalog method of a native constructor's prototype on an object the code made with `new` of
|
|
317
|
+
* that constructor (`done.has(level)` with `done = new Set()`): a native call whose answer the analyzer doesn't model,
|
|
318
|
+
* a recognized guard it can't decide (§7 `conditional`).
|
|
319
|
+
*/
|
|
320
|
+
const nativeMethodCall = (node) => {
|
|
321
|
+
if (node.type !== 'CallExpression')
|
|
322
|
+
return false;
|
|
323
|
+
const read = memberRead(node['callee']);
|
|
324
|
+
if (read === undefined)
|
|
325
|
+
return false;
|
|
326
|
+
const keys = read.key === undefined ? [] : evaluate(read.key);
|
|
327
|
+
const member = read.name ?? (keys.length === 1 && keys[0].kind === 'constant' && !heapRead(keys) ? keys[0].value : null);
|
|
328
|
+
if (member === null)
|
|
329
|
+
return false;
|
|
330
|
+
const objects = evaluate(read.object);
|
|
331
|
+
return (objects.length > 0 &&
|
|
332
|
+
objects.every((value) => {
|
|
333
|
+
if (value.kind !== 'object' || value.heap?.kind !== 'instance' || value.heap.node.type !== 'NewExpression')
|
|
334
|
+
return false;
|
|
335
|
+
const made = resolution.evaluate(value.heap.node['callee'], value.heap.at);
|
|
336
|
+
return (made.length > 0 &&
|
|
337
|
+
made.every((ctor) => {
|
|
338
|
+
if (ctor.kind !== 'holder' || holders[ctor.holder]?.kind !== 'constructor')
|
|
339
|
+
return false;
|
|
340
|
+
const proto = `${ctor.holder}.prototype`;
|
|
341
|
+
return Object.hasOwn(holders, proto) && catalogCheck([{ kind: 'holder', holder: proto, leaves: [] }], toPropertyKey(member), true) === 'catalogued';
|
|
342
|
+
}));
|
|
343
|
+
}));
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Whether a test reads anything at all: a name, a member or `this`, as a config or option flag does, or calls a function
|
|
347
|
+
* the code made in place (Rollup's inlined `!function(){ … }()`), as it would call it by name.
|
|
348
|
+
*/
|
|
349
|
+
const readsAnything = (node) => {
|
|
350
|
+
let found = false;
|
|
351
|
+
walkReads(node, (read) => {
|
|
352
|
+
if (read.type === 'Identifier' || read.type === 'MemberExpression' || read.type === 'ThisExpression')
|
|
353
|
+
found = true;
|
|
354
|
+
else if (read.type === 'CallExpression' && calledSources(read).length > 0)
|
|
355
|
+
found = true;
|
|
356
|
+
});
|
|
357
|
+
return found;
|
|
358
|
+
};
|
|
359
|
+
/** The free name `typeof name` tests, when the realm's catalog has no global of that name (§7). */
|
|
360
|
+
const uncatalogued = (operand) => {
|
|
361
|
+
if (operand.type !== 'Identifier')
|
|
362
|
+
return undefined;
|
|
363
|
+
const binding = scope.resolve(operand);
|
|
364
|
+
if (binding === 'local' || binding.declaration !== undefined)
|
|
365
|
+
return undefined;
|
|
366
|
+
const name = operand['name'];
|
|
367
|
+
return Object.hasOwn(targets, memberName(global, name)) ? undefined : name;
|
|
368
|
+
};
|
|
369
|
+
/** `typeof x <op> "s"` in any spelling: the native `x` tested, or undefined when it is no such test. */
|
|
370
|
+
const typeofTest = (node, outcome, shown) => {
|
|
371
|
+
if (node.type !== 'BinaryExpression' || !comparisons.has(node['operator']))
|
|
372
|
+
return undefined;
|
|
373
|
+
const left = node['left'];
|
|
374
|
+
const right = node['right'];
|
|
375
|
+
const operand = typeofOperand(left) ?? typeofOperand(right);
|
|
376
|
+
if (operand === undefined)
|
|
377
|
+
return undefined;
|
|
378
|
+
const other = typeofOperand(left) === undefined ? left : right;
|
|
379
|
+
const [string] = evaluate(other);
|
|
380
|
+
if (string?.kind !== 'constant' || typeof string.value !== 'string')
|
|
381
|
+
return undefined;
|
|
382
|
+
if (uncatalogued(operand) !== undefined)
|
|
383
|
+
return leaf(shown, 'undecided');
|
|
384
|
+
const constant = string.value;
|
|
385
|
+
const holds = (type) => other === right
|
|
386
|
+
? compareStrings(node['operator'], type, constant)
|
|
387
|
+
: compareStrings(node['operator'], constant, type);
|
|
388
|
+
const whenMissing = holds('undefined');
|
|
389
|
+
// `typeof h.m` of a native holder `h` and a constant `m`, checked against the catalog along `h`'s chain (§7).
|
|
390
|
+
const read = memberRead(operand);
|
|
391
|
+
if (read !== undefined) {
|
|
392
|
+
const objects = evaluate(read.object);
|
|
393
|
+
const holder = objects.find((value) => value.kind === 'holder');
|
|
394
|
+
const keys = read.key === undefined ? [] : evaluate(read.key);
|
|
395
|
+
const member = read.name ?? (keys.length === 1 && keys[0].kind === 'constant' && !heapRead(keys) ? keys[0].value : null);
|
|
396
|
+
if (holder?.kind === 'holder' && member !== null) {
|
|
397
|
+
const key = toPropertyKey(member);
|
|
398
|
+
const tested = memberName(holder.holder, key);
|
|
399
|
+
const checked = catalogCheck(objects, key, true);
|
|
400
|
+
if (checked === 'uncatalogued')
|
|
401
|
+
return leaf(shown, 'undecided', tested);
|
|
402
|
+
if (checked === 'never')
|
|
403
|
+
return never(node, shown, tested, whenMissing !== outcome);
|
|
404
|
+
if (checked === 'always')
|
|
405
|
+
return always(node, shown, tested, holds(key === 'length' ? 'number' : 'string') === outcome);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const values = evaluate(operand);
|
|
409
|
+
const tested = nativeName(values);
|
|
410
|
+
if (tested === undefined)
|
|
411
|
+
return undefined;
|
|
412
|
+
const whenPresent = holds(nativeType(values));
|
|
413
|
+
if (whenMissing === whenPresent)
|
|
414
|
+
return leaf(shown, 'transparent', tested);
|
|
415
|
+
return presence(shown, { tested, present: whenPresent === outcome });
|
|
416
|
+
};
|
|
417
|
+
/** `x == null`, `x === undefined`, `void 0 === x`: the native `x` tested, or undefined when it is no such test. */
|
|
418
|
+
const nullTest = (node, outcome, shown) => {
|
|
419
|
+
if (node.type !== 'BinaryExpression' || !equalities.has(node['operator']))
|
|
420
|
+
return undefined;
|
|
421
|
+
const left = node['left'];
|
|
422
|
+
const right = node['right'];
|
|
423
|
+
const operand = isNullish(right) ? left : isNullish(left) ? right : undefined;
|
|
424
|
+
if (operand === undefined)
|
|
425
|
+
return undefined;
|
|
426
|
+
const values = evaluate(operand);
|
|
427
|
+
const tested = nativeName(values);
|
|
428
|
+
if (tested === undefined)
|
|
429
|
+
return undefined;
|
|
430
|
+
const equal = node['operator'].startsWith('=');
|
|
431
|
+
const present = equal !== outcome;
|
|
432
|
+
if (!present && resolution.onceFlag(operand)) {
|
|
433
|
+
// A once-flag that saves the native (Clarity's `if (original === null) { original = history[method]; … }`) holds
|
|
434
|
+
// its own nullish value until it does, which takes the arm whether or not the native exists: that alternative is
|
|
435
|
+
// the flag's test, transparent (§5 Resolver capabilities 6, §7), and the most-reaching wins (§7 OR, Principle 3).
|
|
436
|
+
// The native alternative stays the absence test it is. A heap-read nullish value can't be decided.
|
|
437
|
+
const literal = operand === left ? right : left;
|
|
438
|
+
const strict = node['operator'].length === 3;
|
|
439
|
+
const matches = (value) => strict ? value === (literal.type === 'Literal' ? null : undefined) : value === null || value === undefined;
|
|
440
|
+
const own = values.filter((value) => value.kind === 'constant' && matches(value.value));
|
|
441
|
+
if (own.length > 0)
|
|
442
|
+
return leaf(shown, own.some((value) => !heapRead([value])) ? 'transparent' : 'config', tested);
|
|
443
|
+
}
|
|
444
|
+
return presence(shown, { tested, present });
|
|
445
|
+
};
|
|
446
|
+
/** `"k" in o`, `o.hasOwnProperty("k")`, `Object.hasOwn(o, "k")` on a native holder: the member tested. */
|
|
447
|
+
const memberTest = (node, outcome, shown) => {
|
|
448
|
+
let object;
|
|
449
|
+
let key;
|
|
450
|
+
if (node.type === 'BinaryExpression' && node['operator'] === 'in') {
|
|
451
|
+
object = node['right'];
|
|
452
|
+
key = node['left'];
|
|
453
|
+
}
|
|
454
|
+
else if (node.type === 'CallExpression') {
|
|
455
|
+
const parts = resolution.ownTestParts(node, at);
|
|
456
|
+
if (parts === undefined)
|
|
457
|
+
return undefined;
|
|
458
|
+
object = parts.object;
|
|
459
|
+
key = parts.key ?? undefined;
|
|
460
|
+
}
|
|
461
|
+
if (object === undefined)
|
|
462
|
+
return undefined;
|
|
463
|
+
const objects = evaluate(object);
|
|
464
|
+
const holder = objects.find((value) => value.kind === 'holder');
|
|
465
|
+
if (holder?.kind !== 'holder')
|
|
466
|
+
return undefined;
|
|
467
|
+
const keys = key === undefined ? [{ kind: 'constant', value: undefined }] : evaluate(key);
|
|
468
|
+
const [constant] = keys;
|
|
469
|
+
if (keys.length !== 1 || constant?.kind !== 'constant' || heapRead(keys))
|
|
470
|
+
return leaf(shown, 'undecided');
|
|
471
|
+
const member = toPropertyKey(constant.value);
|
|
472
|
+
const tested = memberName(holder.holder, member);
|
|
473
|
+
const checked = catalogCheck(objects, member, node.type === 'BinaryExpression');
|
|
474
|
+
if (checked === 'uncatalogued')
|
|
475
|
+
return leaf(shown, 'undecided', tested);
|
|
476
|
+
if (checked === 'never')
|
|
477
|
+
return never(node, shown, tested, outcome);
|
|
478
|
+
if (checked === 'always')
|
|
479
|
+
return always(node, shown, tested, outcome);
|
|
480
|
+
return presence(shown, { tested, present: outcome });
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* A test whose member no catalog version places where it looks (§7): the arm where it is missing always runs, so the
|
|
484
|
+
* test is transparent there, and the arm where it is present never does, so it is dead. Its step is the presence test
|
|
485
|
+
* it reads as.
|
|
486
|
+
*/
|
|
487
|
+
const never = (node, shown, tested, present) => present ? { ...presence(shown, { tested, present }), dead: node } : leaf(shown, 'transparent', tested);
|
|
488
|
+
/**
|
|
489
|
+
* A test of a member every holder it can be has, a function's own `length` or `name` (§7): the arm where it is
|
|
490
|
+
* present always runs, so the test is transparent there, and the arm where it is missing never does, so it is dead.
|
|
491
|
+
* Its step is the absence test it reads as. `holds` is whether the arm holds when the member is present.
|
|
492
|
+
*/
|
|
493
|
+
const always = (node, shown, tested, holds) => holds ? leaf(shown, 'transparent', tested) : { ...presence(shown, { tested, present: false }), dead: node };
|
|
494
|
+
/**
|
|
495
|
+
* What the catalog says of `member` on the holders a tested object can be (§7 Checking guards against the catalog):
|
|
496
|
+
* `always` for a function's own `length` or `name` when every holder it can be is a function, as ECMA-262 gives every
|
|
497
|
+
* function both; `uncatalogued` when no holder in any realm has a member of that name, `never` when no catalog
|
|
498
|
+
* version has it on any of them (directly, or for `in` and `typeof` along the modelled prototype chain), and
|
|
499
|
+
* otherwise `catalogued`.
|
|
500
|
+
*/
|
|
501
|
+
const catalogCheck = (objects, member, chain) => {
|
|
502
|
+
if ((member === 'length' || member === 'name') &&
|
|
503
|
+
objects.length > 0 &&
|
|
504
|
+
objects.every((value) => value.kind === 'holder' && isFunctionHolder(value.holder))) {
|
|
505
|
+
return 'always';
|
|
506
|
+
}
|
|
507
|
+
if (!names(catalog).has(typeof member === 'symbol' ? symbolMember(member) : member))
|
|
508
|
+
return 'uncatalogued';
|
|
509
|
+
if (objects.length === 0)
|
|
510
|
+
return 'catalogued';
|
|
511
|
+
for (const value of objects) {
|
|
512
|
+
// Looking a member up on `null` or `undefined` throws (ECMA-262 ToObject), so the test never holds for it: a
|
|
513
|
+
// native holder that may be missing (§7).
|
|
514
|
+
const nullish = value.kind === 'constant' && (value.value === null || value.value === undefined);
|
|
515
|
+
if (nullish && !heapRead([value]) && !flagged([value]))
|
|
516
|
+
continue;
|
|
517
|
+
// An object that may be no native holder can't be checked.
|
|
518
|
+
if (value.kind !== 'holder')
|
|
519
|
+
return 'catalogued';
|
|
520
|
+
let holder = value.holder;
|
|
521
|
+
const seen = new Set();
|
|
522
|
+
while (holder !== null && !seen.has(holder)) {
|
|
523
|
+
seen.add(holder);
|
|
524
|
+
const entry = holders[holder];
|
|
525
|
+
// A holder the catalog doesn't model can't be checked.
|
|
526
|
+
if (entry === undefined)
|
|
527
|
+
return 'catalogued';
|
|
528
|
+
// A target a later version dropped was still placed there by an earlier one.
|
|
529
|
+
const name = memberName(holder, member);
|
|
530
|
+
if (Object.hasOwn(targets, name) || Object.hasOwn(dropped, name))
|
|
531
|
+
return 'catalogued';
|
|
532
|
+
const own = instanceMembers[entry.owner];
|
|
533
|
+
if (entry.kind === 'instance' && own !== undefined && typeof member === 'string' && Object.hasOwn(own, member)) {
|
|
534
|
+
return 'catalogued';
|
|
535
|
+
}
|
|
536
|
+
holder = chain ? entry.proto : null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return 'never';
|
|
540
|
+
};
|
|
541
|
+
/** Whether a holder is a function: an interface object or constructor, or `Function.prototype` (ECMA-262 20.2.3). */
|
|
542
|
+
const isFunctionHolder = (holder) => holder === 'Function.prototype' || (Object.hasOwn(holders, holder) && holders[holder].kind === 'constructor');
|
|
543
|
+
/** The source of the functions a call may run that the analyzer knows: the user functions its callee can be. */
|
|
544
|
+
const calledSources = (node) => node.type !== 'CallExpression'
|
|
545
|
+
? []
|
|
546
|
+
: evaluate(node['callee']).flatMap((value) => value.kind === 'object' && value.heap?.kind === 'function'
|
|
547
|
+
? [text.slice(value.heap.node.start, value.heap.node.end)]
|
|
548
|
+
: []);
|
|
549
|
+
/**
|
|
550
|
+
* A writability check (`isReadOnly`, `isPropertyWritable`, a descriptor's `writable`): transparent (§7). It is named
|
|
551
|
+
* so, or it calls a function that reads a descriptor's `writable`.
|
|
552
|
+
*/
|
|
553
|
+
const writabilityTest = (node, shown) => {
|
|
554
|
+
const writability = writabilityNames.test(text.slice(node.start, node.end)) ||
|
|
555
|
+
calledSources(node).some((source) => /getOwnPropertyDescriptor/.test(source) && /\.writable\b/.test(source));
|
|
556
|
+
return writability ? leaf(shown, 'transparent') : undefined;
|
|
557
|
+
};
|
|
558
|
+
/** A brand check (`[object Promise]`) or a native-code check: the native judged, when one is passed. */
|
|
559
|
+
const brokenTest = (node, shown) => {
|
|
560
|
+
const source = text.slice(node.start, node.end);
|
|
561
|
+
let broken = brokenNames.test(source) || /["'`]\[object /.test(source);
|
|
562
|
+
if (!broken)
|
|
563
|
+
broken = calledSources(node).some((source) => /native code/.test(source));
|
|
564
|
+
if (!broken)
|
|
565
|
+
return undefined;
|
|
566
|
+
let tested;
|
|
567
|
+
walkReads(node, (read) => {
|
|
568
|
+
if (tested !== undefined || read.type !== 'CallExpression')
|
|
569
|
+
return;
|
|
570
|
+
for (const argument of read['arguments']) {
|
|
571
|
+
tested ??= nativeName(evaluate(argument));
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
return leaf(shown, 'broken-native', tested);
|
|
575
|
+
};
|
|
576
|
+
/** Whether a test reads a key a member sweep gave (§5 Resolver capabilities 9), anywhere in it. */
|
|
577
|
+
const readsSwept = (node) => {
|
|
578
|
+
let found = false;
|
|
579
|
+
let count = 0;
|
|
580
|
+
walkReads(node, (read) => {
|
|
581
|
+
if (found)
|
|
582
|
+
return;
|
|
583
|
+
if (count++ >= maxReads)
|
|
584
|
+
return context.capHit('testReads', node);
|
|
585
|
+
if (read.type === 'Identifier' || read.type === 'MemberExpression') {
|
|
586
|
+
if (evaluate(read).some(isSwept))
|
|
587
|
+
found = true;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
return found;
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* A filter on a swept key (§5 Resolver capabilities 9): a test the analyzer evaluates on the key. Decided, it
|
|
594
|
+
* keeps the member (transparent) or drops it, so the write never happens for that key and gives nothing. Undecided
|
|
595
|
+
* (`cfg.shouldPatch(k)`), it keeps every member, and the guard is `undecided`.
|
|
596
|
+
*/
|
|
597
|
+
const sweepFilter = (node, outcome, shown) => {
|
|
598
|
+
if (!resolution.swept())
|
|
599
|
+
return undefined;
|
|
600
|
+
const found = sweepTest(node, outcome, shown);
|
|
601
|
+
if (found !== undefined)
|
|
602
|
+
sweepTests.add(node);
|
|
603
|
+
return found;
|
|
604
|
+
};
|
|
605
|
+
const sweepTest = (node, outcome, shown) => {
|
|
606
|
+
const values = evaluate(node);
|
|
607
|
+
if (decided(values) && values.some(isSwept)) {
|
|
608
|
+
const taken = truthiness(values);
|
|
609
|
+
if (taken === undefined)
|
|
610
|
+
return leaf(shown, 'undecided');
|
|
611
|
+
if (taken === outcome)
|
|
612
|
+
return leaf(shown, 'transparent');
|
|
613
|
+
sweepFilters.add(node);
|
|
614
|
+
return { ...leaf(shown, 'transparent'), dead: node };
|
|
615
|
+
}
|
|
616
|
+
return readsSwept(node) ? leaf(shown, 'undecided') : undefined;
|
|
617
|
+
};
|
|
618
|
+
/** A test's classification, made for the test itself however it is negated: the guard a `prune` step names. */
|
|
619
|
+
const atom = (node, outcome, shown) => ({
|
|
620
|
+
...atomOf(node, outcome, shown),
|
|
621
|
+
tests: [node],
|
|
622
|
+
});
|
|
623
|
+
const atomOf = (node, outcome, shown) => {
|
|
624
|
+
if (tainted(node))
|
|
625
|
+
return leaf(shown, 'unknown');
|
|
626
|
+
const filter = sweepFilter(node, outcome, shown);
|
|
627
|
+
if (filter !== undefined)
|
|
628
|
+
return filter;
|
|
629
|
+
// A test a call argument literal's field decides: the arm it rules out is never reached, and on the arm it
|
|
630
|
+
// lets through the test is transparent. A recognized test of a native (`void 0 !== o.win.WebGLRenderingContext`)
|
|
631
|
+
// is the native's presence, which no literal decides (§7): it keeps its own classification and tested native on
|
|
632
|
+
// both arms, so an absence arm's polyfill stays a finding. Any other recognized test keeps its classification on
|
|
633
|
+
// the arm the literal lets through.
|
|
634
|
+
const taken = argumentTaken(node);
|
|
635
|
+
const recognized = typeofTest(node, outcome, shown) ??
|
|
636
|
+
nullTest(node, outcome, shown) ??
|
|
637
|
+
memberTest(node, outcome, shown) ??
|
|
638
|
+
writabilityTest(node, shown) ??
|
|
639
|
+
brokenTest(node, shown);
|
|
640
|
+
const native = recognized?.steps.some((each) => each.tested !== undefined) ?? false;
|
|
641
|
+
if (taken !== undefined && taken.taken !== outcome && !native) {
|
|
642
|
+
ruledOutTests.add(node);
|
|
643
|
+
return { ...leaf(shown, 'transparent'), dead: node, ...(taken.by === undefined ? {} : { by: taken.by }) };
|
|
644
|
+
}
|
|
645
|
+
if (recognized !== undefined)
|
|
646
|
+
return recognized;
|
|
647
|
+
if (taken !== undefined) {
|
|
648
|
+
const through = leaf(shown, 'transparent');
|
|
649
|
+
for (const step of through.steps)
|
|
650
|
+
argumentGuards.set(step, { test: node, literal: taken.by });
|
|
651
|
+
return through;
|
|
652
|
+
}
|
|
653
|
+
const values = evaluate(node);
|
|
654
|
+
// A once-flag's test is transparent (§5 Resolver capabilities 6), and so is a test decided as a constant, or as an
|
|
655
|
+
// object, which is always truthy. A value that mixes a flag's constant with anything else (Clarity's `check()`:
|
|
656
|
+
// `status === false && typeof Promise !== "undefined" && …`) is no flag's test, and constants that differ in
|
|
657
|
+
// truthiness (`!!cfg.disable`) decide nothing (§7 `conditional`).
|
|
658
|
+
if (flagTest(values) || (decided(values) && truthiness(values) !== undefined) || objects(values)) {
|
|
659
|
+
return leaf(shown, 'transparent');
|
|
660
|
+
}
|
|
661
|
+
if (node.type !== 'BinaryExpression') {
|
|
662
|
+
const tested = nativeName(values);
|
|
663
|
+
if (tested !== undefined)
|
|
664
|
+
return presence(shown, { tested, present: outcome });
|
|
665
|
+
}
|
|
666
|
+
if (readsNative(node) || nativeMethodCall(node))
|
|
667
|
+
return leaf(shown, 'undecided');
|
|
668
|
+
if (unresolvedCall(node))
|
|
669
|
+
return leaf(shown, 'unknown');
|
|
670
|
+
if (readsAnything(node))
|
|
671
|
+
return leaf(shown, 'config');
|
|
672
|
+
return leaf(shown, 'unknown');
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* How a test comes out when a call argument literal's field decides it (§5 Resolver capabilities 5): in the
|
|
676
|
+
* write's frame, or, for a path judged under a call with no frame, in that call's frame.
|
|
677
|
+
* Undefined for any other test.
|
|
678
|
+
*/
|
|
679
|
+
const argumentTaken = (node) => {
|
|
680
|
+
const decided = (values) => {
|
|
681
|
+
const taken = argumentDecided(values);
|
|
682
|
+
return taken === undefined ? undefined : { taken, by: settlingLiterals(values)[0] };
|
|
683
|
+
};
|
|
684
|
+
const here = decided(resolution.settled(evaluate(node)));
|
|
685
|
+
if (here !== undefined)
|
|
686
|
+
return here;
|
|
687
|
+
const call = context.call;
|
|
688
|
+
const fn = context.parentOf === undefined ? undefined : enclosingFunction(node, context.parentOf);
|
|
689
|
+
if (call === undefined || fn === undefined || call.site.fn !== fn || frameOf(at.frame, fn) !== undefined)
|
|
690
|
+
return undefined;
|
|
691
|
+
if (!passesLiteral(call.site.args) || !resolution.argumentTest(node, fn))
|
|
692
|
+
return undefined;
|
|
693
|
+
const frame = resolution
|
|
694
|
+
.framesOf(fn)
|
|
695
|
+
.find((each) => each.bindings.some((binding) => binding.site.call === call.site.call && binding.caller?.id === call.caller?.id));
|
|
696
|
+
return frame === undefined ? undefined : decided(resolution.settled(resolution.evaluate(node, { ...at, frame })));
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* The argument a test that reads a parameter of the frame's function takes, where the call is: a guard inside a helper
|
|
700
|
+
* summary is judged with the arguments of the call (§7). Undefined unless the frame is one call's that lists it.
|
|
701
|
+
*/
|
|
702
|
+
const argumentOf = (node) => {
|
|
703
|
+
const fn = context.parentOf === undefined ? undefined : enclosingFunction(node, context.parentOf);
|
|
704
|
+
if (fn === undefined)
|
|
705
|
+
return undefined;
|
|
706
|
+
const frame = frameOf(at.frame, fn);
|
|
707
|
+
const [binding, ...others] = frame === undefined ? (context.call?.site.fn === fn ? [context.call] : []) : frame.how === 'call' ? frame.bindings : [];
|
|
708
|
+
if (binding === undefined || others.length > 0)
|
|
709
|
+
return undefined;
|
|
710
|
+
const index = fn['params'].findIndex((param) => param.type === 'Identifier' && param['name'] === node['name']);
|
|
711
|
+
// Only the parameter itself, never reassigned in the function: otherwise its value is not the argument's.
|
|
712
|
+
if (index < 0 || declarationOf(node, context.scope) !== fn || reassigned(fn, node['name'], context.scope)) {
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
const { site } = binding;
|
|
716
|
+
const argument = site.args[index];
|
|
717
|
+
if (argument === undefined || argument.type === 'SpreadElement')
|
|
718
|
+
return undefined;
|
|
719
|
+
if (site.unlisted !== undefined && index >= site.unlisted)
|
|
720
|
+
return undefined;
|
|
721
|
+
return { node: argument, at: positionOf(binding) };
|
|
722
|
+
};
|
|
723
|
+
const classify = (node, outcome, shown = node) => {
|
|
724
|
+
switch (node.type) {
|
|
725
|
+
case 'Identifier': {
|
|
726
|
+
const argument = argumentOf(node);
|
|
727
|
+
if (argument !== undefined) {
|
|
728
|
+
return classifierOf(write, argument.at, { ...context, call: undefined }).classify(argument.node, outcome);
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case 'UnaryExpression':
|
|
733
|
+
if (node['operator'] === '!')
|
|
734
|
+
return classify(node['argument'], !outcome, shown);
|
|
735
|
+
break;
|
|
736
|
+
case 'SequenceExpression':
|
|
737
|
+
return classify(node['expressions'].at(-1), outcome, shown);
|
|
738
|
+
case 'ChainExpression':
|
|
739
|
+
return classify(node['expression'], outcome, shown);
|
|
740
|
+
case 'LogicalExpression': {
|
|
741
|
+
const operator = node['operator'];
|
|
742
|
+
if (operator === '??')
|
|
743
|
+
break;
|
|
744
|
+
const left = classify(node['left'], outcome);
|
|
745
|
+
const right = classify(node['right'], outcome);
|
|
746
|
+
// `a && b` holds when both do, and `a || b` fails when both do: one path. Otherwise either decides it.
|
|
747
|
+
const both = (operator === '&&') === outcome;
|
|
748
|
+
return both ? bothOf(left, right) : eitherOf(left, right);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return atom(node, outcome, shown);
|
|
752
|
+
};
|
|
753
|
+
/** A condition's value: its tests along one path (all), or across alternatives (any). */
|
|
754
|
+
const condition = (when) => {
|
|
755
|
+
if (typeof when === 'boolean')
|
|
756
|
+
return { result: 'unconditional', steps: [], tests: [] };
|
|
757
|
+
if ('test' in when)
|
|
758
|
+
return classify(when.test, when.outcome);
|
|
759
|
+
const parts = ('all' in when ? when.all : when.any).map(condition);
|
|
760
|
+
return parts.reduce('all' in when ? bothOf : eitherOf);
|
|
761
|
+
};
|
|
762
|
+
return { classify, condition, taintStep };
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* The `catch` arms whose `try` block can't throw (§5 Resolver capabilities 9): a
|
|
766
|
+
* write only such an arm reaches never happens, so it gives nothing, but the resolver counts it as a `prune`.
|
|
767
|
+
*/
|
|
768
|
+
export const unreachedArms = new WeakSet();
|
|
769
|
+
/** What judging a `try` block where a write's path enters its `catch` needs, at the write's position. */
|
|
770
|
+
function throwContext(at, { resolution, scope, catalog, realm, caps, capHit }) {
|
|
771
|
+
const targets = catalog.targets[realm];
|
|
772
|
+
const holders = catalog.holders[realm];
|
|
773
|
+
return {
|
|
774
|
+
evaluate: (node) => resolution.evaluate(node, at),
|
|
775
|
+
declaration: (id) => scope.declaration(id),
|
|
776
|
+
maxDepth: caps.throwDepth,
|
|
777
|
+
depthCut: (fn) => capHit('throwDepth', fn),
|
|
778
|
+
target: (holder, key) => {
|
|
779
|
+
for (let on = holder; on !== null; on = Object.hasOwn(holders, on) ? holders[on].proto : null) {
|
|
780
|
+
const name = memberName(on, key);
|
|
781
|
+
if (Object.hasOwn(targets, name))
|
|
782
|
+
return targets[name];
|
|
783
|
+
}
|
|
784
|
+
return undefined;
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* The tests that are filters on a swept key (§5 Resolver capabilities 9): a write such a test drops is no write of
|
|
790
|
+
* that key at all, so it gives nothing, and no `prune` counts it.
|
|
791
|
+
*/
|
|
792
|
+
export const sweepFilters = new WeakSet();
|
|
793
|
+
/**
|
|
794
|
+
* The tests a call argument literal's field decides against the path (§5 Resolver capabilities 5): the arm they
|
|
795
|
+
* rule out is not reached, so a write there gives nothing, but the resolver counts it as a `prune`.
|
|
796
|
+
*/
|
|
797
|
+
export const ruledOutTests = new WeakSet();
|
|
798
|
+
/**
|
|
799
|
+
* The `guard` steps of tests a call argument literal's field settled the way the path needs, each with the test
|
|
800
|
+
* and that literal: a write that only the arm the test rules out reaches leaves a `prune` step after it.
|
|
801
|
+
*/
|
|
802
|
+
export const argumentGuards = new WeakMap();
|
|
803
|
+
/** The tests that are filters on a swept key, which the sweep's `enumeration` step records (§14). */
|
|
804
|
+
export const sweepTests = new WeakSet();
|
|
805
|
+
/** Every member name the catalog has on any holder in any realm, per catalog. */
|
|
806
|
+
const catalogNames = new WeakMap();
|
|
807
|
+
function names(catalog) {
|
|
808
|
+
let found = catalogNames.get(catalog);
|
|
809
|
+
if (found === undefined) {
|
|
810
|
+
const all = new Set();
|
|
811
|
+
for (const realm of Object.values(catalog.targets)) {
|
|
812
|
+
for (const target of Object.values(realm))
|
|
813
|
+
all.add(target.member);
|
|
814
|
+
}
|
|
815
|
+
for (const realm of Object.values(catalog.instanceMembers)) {
|
|
816
|
+
for (const members of Object.values(realm))
|
|
817
|
+
for (const member of Object.keys(members))
|
|
818
|
+
all.add(member);
|
|
819
|
+
}
|
|
820
|
+
catalogNames.set(catalog, (found = all));
|
|
821
|
+
}
|
|
822
|
+
return found;
|
|
823
|
+
}
|
|
824
|
+
/** How each class reads in a step's text. */
|
|
825
|
+
const wording = {
|
|
826
|
+
transparent: 'transparent',
|
|
827
|
+
absence: 'an absence test',
|
|
828
|
+
'broken-native': 'a broken-native check',
|
|
829
|
+
config: 'a config or option flag',
|
|
830
|
+
'other-target': 'a presence test of another native',
|
|
831
|
+
unknown: 'a guard the analyzer does not recognize',
|
|
832
|
+
undecided: 'a guard that cannot be decided statically',
|
|
833
|
+
};
|
|
834
|
+
/** The object and key of a member read `o.m` or `o[k]`: the name `m`, or the key expression `k`. */
|
|
835
|
+
function memberRead(node) {
|
|
836
|
+
if (node.type !== 'MemberExpression' || node['object'].type === 'Super')
|
|
837
|
+
return undefined;
|
|
838
|
+
const object = node['object'];
|
|
839
|
+
const property = node['property'];
|
|
840
|
+
if (node['computed'] === true)
|
|
841
|
+
return { object, key: property };
|
|
842
|
+
return property.type === 'Identifier' ? { object, name: property['name'] } : undefined;
|
|
843
|
+
}
|
|
844
|
+
/** Where a call on a chain is: its guards, whether it is top-level, and its caller's frame. */
|
|
845
|
+
function positionOf({ site, caller }) {
|
|
846
|
+
return { guard: (name) => site.guards.get(name), topLevel: site.topLevel, frame: caller };
|
|
847
|
+
}
|
|
848
|
+
/** The declaration a reference resolves to, or none: a name the seam recorded no reference for. */
|
|
849
|
+
function declarationOf(id, scope) {
|
|
850
|
+
try {
|
|
851
|
+
return scope.declaration(id);
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** Per function, the names of its parameters that code in it assigns (`reassigned`). */
|
|
858
|
+
const assignedParameters = new WeakMap();
|
|
859
|
+
/** Whether code in `fn` assigns its parameter `name`: by `=` and its compound forms, `++`/`--`, or a `for`-`in`/`of` head. */
|
|
860
|
+
function reassigned(fn, name, scope) {
|
|
861
|
+
let names = assignedParameters.get(fn);
|
|
862
|
+
if (names === undefined) {
|
|
863
|
+
const found = new Set();
|
|
864
|
+
const targets = (pattern) => {
|
|
865
|
+
const ids = pattern.type === 'Identifier' ? [pattern] : pattern.type.endsWith('Pattern') ? boundIdentifiers(pattern) : [];
|
|
866
|
+
for (const id of ids)
|
|
867
|
+
if (declarationOf(id, scope) === fn)
|
|
868
|
+
found.add(id['name']);
|
|
869
|
+
};
|
|
870
|
+
walkTree(fn['body'], {
|
|
871
|
+
enter(node) {
|
|
872
|
+
if (node.type === 'AssignmentExpression')
|
|
873
|
+
targets(node['left']);
|
|
874
|
+
else if (node.type === 'UpdateExpression')
|
|
875
|
+
targets(node['argument']);
|
|
876
|
+
else if ((node.type === 'ForInStatement' || node.type === 'ForOfStatement') && node['left'] !== undefined) {
|
|
877
|
+
targets(node['left']);
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
assignedParameters.set(fn, (names = found));
|
|
882
|
+
}
|
|
883
|
+
return names.has(name);
|
|
884
|
+
}
|
|
885
|
+
/** The path from the top of the function a node is in down to the node, outermost first. */
|
|
886
|
+
export function functionPath(node, parentOf) {
|
|
887
|
+
const path = [node];
|
|
888
|
+
for (let at = parentOf(node); at !== undefined; at = parentOf(at)) {
|
|
889
|
+
path.push(at);
|
|
890
|
+
if (isFunction(at))
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
return path.reverse();
|
|
894
|
+
}
|
|
895
|
+
/** The innermost function around a node. */
|
|
896
|
+
function enclosingFunction(node, parentOf) {
|
|
897
|
+
for (let at = parentOf(node); at !== undefined; at = parentOf(at))
|
|
898
|
+
if (isFunction(at))
|
|
899
|
+
return at;
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
/** `x` for `typeof x`. */
|
|
903
|
+
function typeofOperand(node) {
|
|
904
|
+
return node.type === 'UnaryExpression' && node['operator'] === 'typeof' ? node['argument'] : undefined;
|
|
905
|
+
}
|
|
906
|
+
/** `null`, `undefined` or `void …`. */
|
|
907
|
+
function isNullish(node) {
|
|
908
|
+
if (node.type === 'Literal')
|
|
909
|
+
return node['value'] === null && node['regex'] === undefined && node['raw'] === 'null';
|
|
910
|
+
if (node.type === 'Identifier')
|
|
911
|
+
return node['name'] === 'undefined';
|
|
912
|
+
return node.type === 'UnaryExpression' && node['operator'] === 'void';
|
|
913
|
+
}
|
|
914
|
+
/** A comparison of two strings, as ECMA-262 compares them. */
|
|
915
|
+
function compareStrings(operator, a, b) {
|
|
916
|
+
switch (operator) {
|
|
917
|
+
case '==':
|
|
918
|
+
case '===':
|
|
919
|
+
return a === b;
|
|
920
|
+
case '!=':
|
|
921
|
+
case '!==':
|
|
922
|
+
return a !== b;
|
|
923
|
+
case '<':
|
|
924
|
+
return a < b;
|
|
925
|
+
case '>':
|
|
926
|
+
return a > b;
|
|
927
|
+
case '<=':
|
|
928
|
+
return a <= b;
|
|
929
|
+
default:
|
|
930
|
+
return a >= b;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Visits a test's nodes, without entering the functions it creates, and without the names that are no reads: a
|
|
935
|
+
* member's or property's key written as a name.
|
|
936
|
+
*/
|
|
937
|
+
function walkReads(node, visit) {
|
|
938
|
+
let reads = testReads.get(node);
|
|
939
|
+
if (reads === undefined) {
|
|
940
|
+
const found = [];
|
|
941
|
+
walkTree(node, {
|
|
942
|
+
enter(each, parent) {
|
|
943
|
+
if (each !== node && isFunction(each))
|
|
944
|
+
return false;
|
|
945
|
+
if (parent !== null && each.type === 'Identifier' && isKeyName(each, parent))
|
|
946
|
+
return false;
|
|
947
|
+
found.push(each);
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
testReads.set(node, (reads = found));
|
|
951
|
+
}
|
|
952
|
+
for (const read of reads)
|
|
953
|
+
visit(read);
|
|
954
|
+
}
|
|
955
|
+
/** The nodes `walkReads` visits in each test, in its order, found once: the syntax doesn't change (§18 step 1). */
|
|
956
|
+
const testReads = new WeakMap();
|
|
957
|
+
function isKeyName(id, parent) {
|
|
958
|
+
if (parent.type === 'MemberExpression')
|
|
959
|
+
return parent['property'] === id && parent['computed'] !== true;
|
|
960
|
+
if (parent.type === 'Property')
|
|
961
|
+
return parent['key'] === id && parent['computed'] !== true && parent['shorthand'] !== true;
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Judges writes along their call chains (§7): the guards on a write's path in the function it is in, AND the chain out
|
|
966
|
+
* from there, with the `guard` steps of the alternative that gives it, outermost first. The chain ends at top-level
|
|
967
|
+
* code, the install entry, with basis `call-path`.
|
|
968
|
+
*
|
|
969
|
+
* A function with no resolved caller, or one reached only through itself, is no install entry: it is judged from its
|
|
970
|
+
* **creation site** (ADR 0013), the reachability of the code that created it AND the guards in force where it was
|
|
971
|
+
* created, then its own guards, with basis `creation-site` and a `creation-site` step after the creation path's guards.
|
|
972
|
+
* A creation that no code reaches (after an unconditional `return` or `throw`), or that only the function itself
|
|
973
|
+
* creates, was never evaluated, and gives `unknown`.
|
|
974
|
+
*
|
|
975
|
+
* The guards in a function are judged in its frame when the write is instantiated in one. Otherwise each resolved call
|
|
976
|
+
* of the function is an alternative of its own, and a test of a parameter is judged as the argument that call passes.
|
|
977
|
+
*/
|
|
978
|
+
export function chainJudge(context) {
|
|
979
|
+
// The chain out from each call, by call, caller frame and target, when its walk met nothing being judged.
|
|
980
|
+
const memo = new Map();
|
|
981
|
+
// What is being judged, each with its place, outermost first: the calls (by memo key), whose alternatives through
|
|
982
|
+
// themselves (a recursion) add nothing, and the functions whose creation is being judged, whose creation through
|
|
983
|
+
// themselves is never evaluated.
|
|
984
|
+
const open = new Map();
|
|
985
|
+
// How many judgements met an open one, whose result then holds only for walks that meet the same.
|
|
986
|
+
let cycles = 0;
|
|
987
|
+
// The walks of the calls being judged, innermost last.
|
|
988
|
+
const walks = [];
|
|
989
|
+
// The chain out from each call whose walk met something being judged (a cycle), by memo key: a walk's result holds
|
|
990
|
+
// wherever the same things it met are open and none it opened is, since it then takes the same way.
|
|
991
|
+
// Without it, every way into a recursion judged the recursion again, exponentially in its size.
|
|
992
|
+
const walked = new Map();
|
|
993
|
+
const pathTo = (node) => functionPath(node, context.parentOf);
|
|
994
|
+
/**
|
|
995
|
+
* The most-reaching of alternatives, with the steps of the first that gives it; none when there are none. A dead
|
|
996
|
+
* alternative drops out, and the result is dead only when every alternative is. A `call-path` alternative is taken
|
|
997
|
+
* before a `creation-site` one that gives the same value.
|
|
998
|
+
*/
|
|
999
|
+
const best = (alternatives) => {
|
|
1000
|
+
if (alternatives.length === 0)
|
|
1001
|
+
return undefined;
|
|
1002
|
+
const live = alternatives.filter((each) => each.dead === undefined);
|
|
1003
|
+
const from = live.length === 0 ? alternatives : live;
|
|
1004
|
+
const reachability = from.map((each) => each.reachability).reduce(across);
|
|
1005
|
+
const giving = from.filter((each) => each.reachability === reachability);
|
|
1006
|
+
const found = giving.find((each) => each.basis !== 'creation-site') ?? giving[0];
|
|
1007
|
+
// `load` when any of them reaches top-level code, else `deferred` from the first one's install entry (§7).
|
|
1008
|
+
if (!from.some((each) => each.entry === undefined))
|
|
1009
|
+
return { ...found, entry: from[0].entry };
|
|
1010
|
+
if (found.entry === undefined)
|
|
1011
|
+
return found;
|
|
1012
|
+
const { reachability: value, steps, tests, dead, by, basis } = found;
|
|
1013
|
+
return {
|
|
1014
|
+
reachability: value,
|
|
1015
|
+
steps,
|
|
1016
|
+
tests,
|
|
1017
|
+
...(dead === undefined ? {} : { dead }),
|
|
1018
|
+
...(by === undefined ? {} : { by }),
|
|
1019
|
+
...(basis === undefined ? {} : { basis }),
|
|
1020
|
+
};
|
|
1021
|
+
};
|
|
1022
|
+
/** Two parts of one path (AND): the chain out, then the guards after it. The install entry is the chain out's. */
|
|
1023
|
+
const then = (out, guards, step) => {
|
|
1024
|
+
const { dead, by } = out.dead !== undefined ? out : guards;
|
|
1025
|
+
const basis = out.basis ?? guards.basis;
|
|
1026
|
+
return {
|
|
1027
|
+
reachability: along(out.reachability, guards.reachability),
|
|
1028
|
+
steps: [...out.steps, ...(step === undefined ? [] : [step]), ...guards.steps],
|
|
1029
|
+
tests: new Map([...out.tests, ...guards.tests]),
|
|
1030
|
+
...(dead === undefined ? {} : { dead }),
|
|
1031
|
+
...(by === undefined ? {} : { by }),
|
|
1032
|
+
...(out.entry === undefined ? {} : { entry: out.entry }),
|
|
1033
|
+
...(basis === undefined ? {} : { basis }),
|
|
1034
|
+
};
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* The guards on a path in `fn`, from its start (`inside`, given the call `fn` is judged under), after the chain out
|
|
1038
|
+
* from `fn`: its calls in `frame` when there is one, else every resolved call of it, else its creation site.
|
|
1039
|
+
* Undefined when every way out comes back through code being judged.
|
|
1040
|
+
*/
|
|
1041
|
+
const within = (fn, frame, inside) => {
|
|
1042
|
+
if (fn === undefined)
|
|
1043
|
+
return inside(undefined);
|
|
1044
|
+
// A direct eval's code runs at its call, in its caller's frame: the call's guards and the chain out from it count.
|
|
1045
|
+
const call = context.runBy?.(fn);
|
|
1046
|
+
if (call !== undefined) {
|
|
1047
|
+
const out = reaching(pathTo(call), frame);
|
|
1048
|
+
return out === undefined ? undefined : then(out, inside(undefined));
|
|
1049
|
+
}
|
|
1050
|
+
// Code a dynamic import loads runs where an import is, guards and all, and waits on it: its install entry is the
|
|
1051
|
+
// import's own, or, for an import at load, the code's (§6, §7 Install context).
|
|
1052
|
+
const loads = context.loadedBy?.(fn);
|
|
1053
|
+
if (loads !== undefined) {
|
|
1054
|
+
const alternatives = loads.flatMap((load) => {
|
|
1055
|
+
const out = reaching(pathTo(load), undefined);
|
|
1056
|
+
return out === undefined ? [] : [{ ...then(out, inside(undefined)), entry: out.entry ?? fn }];
|
|
1057
|
+
});
|
|
1058
|
+
return best(alternatives);
|
|
1059
|
+
}
|
|
1060
|
+
const own = frameOf(frame, fn);
|
|
1061
|
+
if (own?.how === 'creation')
|
|
1062
|
+
return created(fn, own, inside);
|
|
1063
|
+
const bindings = own === undefined ? context.callers(fn) : own.bindings;
|
|
1064
|
+
const alternatives = bindings.flatMap((binding) => {
|
|
1065
|
+
const out = outOf(binding);
|
|
1066
|
+
return out === undefined ? [] : [then(out, inside(binding))];
|
|
1067
|
+
});
|
|
1068
|
+
// A function no resolved call reaches, or reached only through itself, is judged from where it was created.
|
|
1069
|
+
const found = best(alternatives) ?? created(fn, own, inside);
|
|
1070
|
+
// A lazy chunk's code waits on its chunk's load (§6, §7 Install context).
|
|
1071
|
+
const chunk = context.chunkOf?.(fn);
|
|
1072
|
+
return chunk !== undefined && found !== undefined && found.entry === undefined ? { ...found, entry: chunk } : found;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* A caller-less function judged from its creation site (§7, ADR 0013): the chain out from the code that created it,
|
|
1076
|
+
* the guards in force there, then its own. A function declaration is created when its scope is entered, before any
|
|
1077
|
+
* statement of it runs. Undefined while the function's own creation is being judged.
|
|
1078
|
+
*/
|
|
1079
|
+
const created = (fn, frame, inside) => {
|
|
1080
|
+
if (frame === undefined) {
|
|
1081
|
+
const framed = inCreationFrames(fn, inside);
|
|
1082
|
+
if (framed !== undefined)
|
|
1083
|
+
return framed;
|
|
1084
|
+
}
|
|
1085
|
+
if (open.has(fn)) {
|
|
1086
|
+
meet(fn);
|
|
1087
|
+
return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
opened(fn);
|
|
1090
|
+
try {
|
|
1091
|
+
const made = frame?.created ?? fn;
|
|
1092
|
+
const where = made.type === 'FunctionDeclaration' ? (context.parentOf(made) ?? made) : made;
|
|
1093
|
+
const path = pathTo(where);
|
|
1094
|
+
const guards = inside(undefined);
|
|
1095
|
+
const step = creationStep(fn, made, frame?.receiverNode);
|
|
1096
|
+
const out = unreached(path) ? undefined : reaching(path, frame?.outer);
|
|
1097
|
+
// A creation no code reaches was never evaluated: there is no creation path to judge from.
|
|
1098
|
+
// Either way it is the install entry its writes wait on: no resolved call reaches it (§7 Install context).
|
|
1099
|
+
if (out === undefined) {
|
|
1100
|
+
return { ...guards, reachability: 'unknown', steps: [step(false), ...guards.steps], entry: fn, basis: 'creation-site' };
|
|
1101
|
+
}
|
|
1102
|
+
return { ...then({ ...out, basis: 'creation-site' }, guards, step(true)), entry: fn };
|
|
1103
|
+
}
|
|
1104
|
+
finally {
|
|
1105
|
+
open.delete(fn);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* A caller-less function whose own guards read what only a frame binds (its parameters, `this`, or the variables of
|
|
1110
|
+
* the code around it) judged in each of its creation frames (§5 Resolver capabilities 3: a closure is analyzed with
|
|
1111
|
+
* the bindings of the code that made it; §7 Creation site), the most-reaching of them. Undefined when its guards read
|
|
1112
|
+
* nothing so, or it has no creation frame.
|
|
1113
|
+
*/
|
|
1114
|
+
const inCreationFrames = (fn, inside) => {
|
|
1115
|
+
const { functions } = context.resolution.contextual(() => inside(undefined));
|
|
1116
|
+
if (functions.size === 0)
|
|
1117
|
+
return undefined;
|
|
1118
|
+
const frames = context.resolution.framesOf(fn, true).filter((frame) => frame.how === 'creation' && frame.fn === fn);
|
|
1119
|
+
return best(frames.flatMap((frame) => {
|
|
1120
|
+
const found = created(fn, frame, (call) => inside(call, frame));
|
|
1121
|
+
return found === undefined ? [] : [found];
|
|
1122
|
+
}));
|
|
1123
|
+
};
|
|
1124
|
+
/**
|
|
1125
|
+
* The reachability of the point a path from the top of its function ends at (a direct eval's call, or where a function
|
|
1126
|
+
* was created): the guards on the path in that function, after the chain out from it, judged in `frame` when given.
|
|
1127
|
+
* Undefined when every way out comes back through code being judged.
|
|
1128
|
+
*/
|
|
1129
|
+
const reaching = (path, frame) => {
|
|
1130
|
+
const site = path.at(-1);
|
|
1131
|
+
const owner = path.length > 1 && isFunction(path[0]) ? path[0] : undefined;
|
|
1132
|
+
return within(owner, frame, (call, own) => {
|
|
1133
|
+
const framed = frame ?? own;
|
|
1134
|
+
return judge(path, { site, target: target.target, taints: [] }, { guard: () => undefined, topLevel: owner === undefined, ...(framed === undefined ? {} : { frame: framed }) }, { ...context, call });
|
|
1135
|
+
});
|
|
1136
|
+
};
|
|
1137
|
+
/** Whether a path goes through a statement that no code reaches: one after an unconditional `return` or `throw`. */
|
|
1138
|
+
const unreached = (path) => path.some((node, index) => index > 0 && reachedAfter(path[index - 1], node) === false);
|
|
1139
|
+
/** The `creation-site` step (§14) of a function judged from where `made` created it (or the object it is held on). */
|
|
1140
|
+
const creationStep = (fn, made, receiver) => (evaluated) => {
|
|
1141
|
+
const { starts } = context;
|
|
1142
|
+
const at = locate(starts, fn.start, fn.end);
|
|
1143
|
+
const generated = locate(starts, made.start, made.end);
|
|
1144
|
+
const shown = receiver === undefined ? undefined : printed(receiver);
|
|
1145
|
+
return {
|
|
1146
|
+
kind: 'creation-site',
|
|
1147
|
+
function: at,
|
|
1148
|
+
generated,
|
|
1149
|
+
...(shown === undefined ? {} : { this: shown }),
|
|
1150
|
+
text: `no resolved call reaches the function at ${at.line}:${at.column} made at ${generated.line}:${generated.column}, ` +
|
|
1151
|
+
(evaluated
|
|
1152
|
+
? 'so it is judged from where it was made'
|
|
1153
|
+
: 'and no code that runs makes it, so its creation was never evaluated: unknown') +
|
|
1154
|
+
(shown === undefined ? '' : `, with \`this\` = \`${shown}\`, the object it is held on`),
|
|
1155
|
+
};
|
|
1156
|
+
};
|
|
1157
|
+
/** The chain out from a call, the guards around the call included: undefined for a cycle. */
|
|
1158
|
+
const outOf = (binding) => {
|
|
1159
|
+
const { site, caller } = binding;
|
|
1160
|
+
const key = `${site.call.start}:${site.fn.start}<${caller?.id ?? ''}|${target.key}`;
|
|
1161
|
+
const known = memo.get(key);
|
|
1162
|
+
if (known !== undefined)
|
|
1163
|
+
return known;
|
|
1164
|
+
if (open.has(key)) {
|
|
1165
|
+
meet(key);
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
const again = rewalked(key);
|
|
1169
|
+
if (again !== undefined)
|
|
1170
|
+
return again.found;
|
|
1171
|
+
const walk = { at: opened(key), met: new Map(), opened: new Set() };
|
|
1172
|
+
walks.push(walk);
|
|
1173
|
+
const before = cycles;
|
|
1174
|
+
let found;
|
|
1175
|
+
try {
|
|
1176
|
+
const path = pathTo(site.call);
|
|
1177
|
+
found = within(site.within, caller, (call, own) => {
|
|
1178
|
+
const at = positionOf(binding);
|
|
1179
|
+
const framed = at.frame === undefined && own !== undefined ? { ...at, frame: own } : at;
|
|
1180
|
+
return judge(path, { site: site.call, target: target.target, taints: [] }, framed, { ...context, call });
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
finally {
|
|
1184
|
+
open.delete(key);
|
|
1185
|
+
walks.pop();
|
|
1186
|
+
}
|
|
1187
|
+
if (cycles === before && found !== undefined) {
|
|
1188
|
+
memo.set(key, found);
|
|
1189
|
+
// A call once judged only within a cycle may now be judged without one, so a walk that judged it there would no
|
|
1190
|
+
// longer take the same way.
|
|
1191
|
+
if (walked.has(key))
|
|
1192
|
+
walked.clear();
|
|
1193
|
+
return found;
|
|
1194
|
+
}
|
|
1195
|
+
walk.opened.add(key);
|
|
1196
|
+
const result = { found, cyclic: cycles !== before, met: [...walk.met.keys()], opened: walk.opened };
|
|
1197
|
+
const results = walked.get(key);
|
|
1198
|
+
if (results === undefined)
|
|
1199
|
+
walked.set(key, [result]);
|
|
1200
|
+
else
|
|
1201
|
+
results.push(result);
|
|
1202
|
+
passOn(result);
|
|
1203
|
+
return found;
|
|
1204
|
+
};
|
|
1205
|
+
/** Opens a call or creation for judging, as part of the walk around it, and gives its place. */
|
|
1206
|
+
const opened = (judged) => {
|
|
1207
|
+
const at = open.size;
|
|
1208
|
+
open.set(judged, at);
|
|
1209
|
+
walks.at(-1)?.opened.add(judged);
|
|
1210
|
+
return at;
|
|
1211
|
+
};
|
|
1212
|
+
/** A walk met `judged`, which is open: a cycle, which the walks opened since it was can't be kept apart from. */
|
|
1213
|
+
const meet = (judged) => {
|
|
1214
|
+
cycles++;
|
|
1215
|
+
const walk = walks.at(-1);
|
|
1216
|
+
const at = open.get(judged);
|
|
1217
|
+
if (walk !== undefined && at < walk.at)
|
|
1218
|
+
walk.met.set(judged, at);
|
|
1219
|
+
};
|
|
1220
|
+
/** Passes what a walk met and opened on to the walk it was taken in. */
|
|
1221
|
+
const passOn = (result) => {
|
|
1222
|
+
const walk = walks.at(-1);
|
|
1223
|
+
if (walk === undefined)
|
|
1224
|
+
return;
|
|
1225
|
+
for (const judged of result.opened)
|
|
1226
|
+
walk.opened.add(judged);
|
|
1227
|
+
for (const judged of result.met) {
|
|
1228
|
+
const at = open.get(judged);
|
|
1229
|
+
if (at < walk.at)
|
|
1230
|
+
walk.met.set(judged, at);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
/** An earlier walk of the call `key` that would take the same way now: everything it met is open, nothing it opened is. */
|
|
1234
|
+
const rewalked = (key) => {
|
|
1235
|
+
for (const result of walked.get(key) ?? []) {
|
|
1236
|
+
if (!result.met.every((judged) => open.has(judged)))
|
|
1237
|
+
continue;
|
|
1238
|
+
if ([...open.keys()].some((judged) => result.opened.has(judged)))
|
|
1239
|
+
continue;
|
|
1240
|
+
// Taken again, it meets what it met, as the walk around it does.
|
|
1241
|
+
if (result.cyclic)
|
|
1242
|
+
cycles++;
|
|
1243
|
+
passOn(result);
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
1246
|
+
return undefined;
|
|
1247
|
+
};
|
|
1248
|
+
// The target of the write being judged, whose own presence a guard on the chain may test, and its memo key.
|
|
1249
|
+
let target;
|
|
1250
|
+
return (path, write, at, fn) => {
|
|
1251
|
+
target = { target: write.target, key: `${write.target.name}/${write.target.shadows ?? ''}` };
|
|
1252
|
+
const inside = (call, own) => judge(path, write, at.frame === undefined && own !== undefined ? { ...at, frame: own } : at, { ...context, call });
|
|
1253
|
+
// Every way out coming back through itself leaves the function as its own install entry (§7 Install context).
|
|
1254
|
+
return within(fn, at.frame, inside) ?? { ...inside(undefined), ...(fn === undefined ? {} : { entry: fn }) };
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The name of an install entry (§7 Install context): the function's own name, else the name it is bound to where it is
|
|
1259
|
+
* created (a variable, an assigned member, an object's property or a class's method), else null.
|
|
1260
|
+
*/
|
|
1261
|
+
export function entryName(fn, parentOf) {
|
|
1262
|
+
const own = fn['id'];
|
|
1263
|
+
if (own?.type === 'Identifier')
|
|
1264
|
+
return own['name'];
|
|
1265
|
+
const parent = parentOf(fn);
|
|
1266
|
+
if (parent === undefined)
|
|
1267
|
+
return null;
|
|
1268
|
+
switch (parent.type) {
|
|
1269
|
+
case 'VariableDeclarator':
|
|
1270
|
+
return parent['init'] === fn ? nameOf(parent['id'], false) : null;
|
|
1271
|
+
case 'AssignmentExpression':
|
|
1272
|
+
return parent['right'] === fn ? nameOf(parent['left'], false) : null;
|
|
1273
|
+
case 'Property':
|
|
1274
|
+
case 'MethodDefinition':
|
|
1275
|
+
case 'PropertyDefinition':
|
|
1276
|
+
return parent['value'] === fn ? nameOf(parent['key'], parent['computed'] === true) : null;
|
|
1277
|
+
case 'ExportDefaultDeclaration':
|
|
1278
|
+
return 'default';
|
|
1279
|
+
default:
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/** A binding's name: an identifier, a member's property, or a constant key. */
|
|
1284
|
+
function nameOf(node, computed) {
|
|
1285
|
+
if (node.type === 'MemberExpression')
|
|
1286
|
+
return nameOf(node['property'], node['computed'] === true);
|
|
1287
|
+
if (node.type === 'Identifier' && !computed)
|
|
1288
|
+
return node['name'];
|
|
1289
|
+
if (node.type === 'PrivateIdentifier')
|
|
1290
|
+
return `#${node['name']}`;
|
|
1291
|
+
if (node.type === 'Literal' && (typeof node['value'] === 'string' || typeof node['value'] === 'number')) {
|
|
1292
|
+
return String(node['value']);
|
|
1293
|
+
}
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|