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,2886 @@
|
|
|
1
|
+
import { mutationConfidence, mutationReasons, targetConfidence, targetReasons, } from "../../confidence/index.js";
|
|
2
|
+
import { absenceTags } from "./absence-tags.js";
|
|
3
|
+
import { calleeExpression, isEvalCall, walkTree } from "./ast.js";
|
|
4
|
+
import { eslintScopeBindings } from "./binding.js";
|
|
5
|
+
import { Folder } from "./chunk-loads.js";
|
|
6
|
+
import { scriptLoads, scriptTexts as injectedTexts } from "./script-loads.js";
|
|
7
|
+
import { codeRoutes, isGlobalCompiler, templateStrings } from "./code-routes.js";
|
|
8
|
+
import { bindsOwn, definitionsOf } from "./definitions.js";
|
|
9
|
+
import { candidateTargets, descriptorFieldsOf, isIneffective, outcomesOf, prefixMembers, wildcardOutcome, narrowedMatches, } from "./effect.js";
|
|
10
|
+
import { globalResolution, inDocumentOrder, instanceOf, mergedFolds, onePerNode, opaque, recordable, globalHolder, } from "./global-object.js";
|
|
11
|
+
import { memberName, patternName, symbolMember, toPropertyKey } from "./keys.js";
|
|
12
|
+
import { moduleLinks } from "./module-links.js";
|
|
13
|
+
import { GroupLines, locate } from "./line-starts.js";
|
|
14
|
+
import { linkGroup } from "./link-group.js";
|
|
15
|
+
import { isCodeLiteral, nestedCode } from "./nested-code.js";
|
|
16
|
+
import { parse } from "./parse.js";
|
|
17
|
+
import { printed } from "./print.js";
|
|
18
|
+
import { along, argumentGuards, chainJudge, entryName, functionPath, ruledOutTests, sweepFilters, sweepTests, unreachedArms, } from "./reachability.js";
|
|
19
|
+
import { boundNames, globalLexicalDeclarations, globalLexicalNames, scopeRules } from "./scope.js";
|
|
20
|
+
import { callGraph, cutIn, isFunction, isLoop } from "./summaries.js";
|
|
21
|
+
import { patchShapes, slotSignatures } from "./partial.js";
|
|
22
|
+
import { alwaysRewritten } from "./rewritten-deletes.js";
|
|
23
|
+
import { restoreJudge, sameChain } from "./restores.js";
|
|
24
|
+
import { wrapEvidence } from "./wrap.js";
|
|
25
|
+
import { candidateName, sitesAt, } from "./writes.js";
|
|
26
|
+
import { codeCallees, } from "./types.js";
|
|
27
|
+
/**
|
|
28
|
+
* The resolver's version, recorded as `tool.analyzerVersion` (§19, ADR 0018). Bump it whenever a change to this
|
|
29
|
+
* resolver can change what it returns for the same bytes and catalog.
|
|
30
|
+
*/
|
|
31
|
+
const analyzerVersion = 'js-98';
|
|
32
|
+
/**
|
|
33
|
+
* The JS resolver, with `bindings` behind the binding-resolution seam (§19). `eslint-scope` is the implementation
|
|
34
|
+
* monkeypatchlist ships.
|
|
35
|
+
*/
|
|
36
|
+
export function createJsResolver(bindings = eslintScopeBindings, visit) {
|
|
37
|
+
return { analyzerVersion, analyzeGroup: (input) => analyzeGroup(input, bindings, visit) };
|
|
38
|
+
}
|
|
39
|
+
/** The call graph with its lists visited in `visit`'s order, each list permuted once. */
|
|
40
|
+
function visited(graph, visit) {
|
|
41
|
+
if (visit === undefined)
|
|
42
|
+
return graph;
|
|
43
|
+
const callsOf = new Map();
|
|
44
|
+
let all;
|
|
45
|
+
return {
|
|
46
|
+
...graph,
|
|
47
|
+
callsOf(fn) {
|
|
48
|
+
let found = callsOf.get(fn);
|
|
49
|
+
if (found === undefined)
|
|
50
|
+
callsOf.set(fn, (found = visit(graph.callsOf(fn))));
|
|
51
|
+
return found;
|
|
52
|
+
},
|
|
53
|
+
allCalls: () => (all ??= visit(graph.allCalls())),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Analyzes one link group (§6, §19): the entry and every artifact its imports reach, joined into one program
|
|
58
|
+
* (`linkGroup`), with one heap and one realm, the entry's.
|
|
59
|
+
*/
|
|
60
|
+
function analyzeGroup(input, bindings, visit) {
|
|
61
|
+
const { entry, artifact, catalog, budget } = input;
|
|
62
|
+
const output = {
|
|
63
|
+
members: [
|
|
64
|
+
{ url: entry.url, hash: entry.hash, sourceType: entry.sourceType, realm: entry.realm, realmBasis: entry.realmBasis },
|
|
65
|
+
],
|
|
66
|
+
occurrences: [],
|
|
67
|
+
ineffectiveWrites: [],
|
|
68
|
+
limitations: [],
|
|
69
|
+
references: [],
|
|
70
|
+
moduleLinking: {},
|
|
71
|
+
stats: { steps: 0, budget: 0, exhausted: false, caps: {} },
|
|
72
|
+
};
|
|
73
|
+
const entrySource = artifact(entry.url);
|
|
74
|
+
if (entrySource === undefined)
|
|
75
|
+
throw new Error(`the resolver was given no source for its group entry ${entry.url}`);
|
|
76
|
+
// The resolver analyzes the entry as its group's entry: in the group's realm, as the group's reference runs it.
|
|
77
|
+
const source = { ...entrySource, sourceType: entry.sourceType, realm: entry.realm, realmBasis: entry.realmBasis };
|
|
78
|
+
// The group's step budget scales with the source bytes of its members, and depends on nothing else (§18).
|
|
79
|
+
let bytes = Buffer.byteLength(source.text);
|
|
80
|
+
output.stats.budget = Math.floor(bytes * budget.stepsPerByte);
|
|
81
|
+
const { group, failures, pulled } = linkGroup(source, input);
|
|
82
|
+
for (const failure of failures) {
|
|
83
|
+
// A partially recovered AST is never used (§13).
|
|
84
|
+
const { url, hash } = failure.source;
|
|
85
|
+
output.limitations.push({ kind: 'parse-failure', artifact: { url, hash }, detail: failure.error });
|
|
86
|
+
}
|
|
87
|
+
// A group whose entry's module graph fails gives nothing, but every module it pulled in is its member.
|
|
88
|
+
output.members = (group?.members ?? pulled).map(({ url, hash, sourceType }) => ({
|
|
89
|
+
url,
|
|
90
|
+
hash,
|
|
91
|
+
sourceType,
|
|
92
|
+
realm: entry.realm,
|
|
93
|
+
realmBasis: entry.realmBasis,
|
|
94
|
+
}));
|
|
95
|
+
if (group === undefined)
|
|
96
|
+
return output;
|
|
97
|
+
bytes = group.members.reduce((sum, member) => sum + Buffer.byteLength(member.text), 0);
|
|
98
|
+
const steps = Math.floor(bytes * budget.stepsPerByte);
|
|
99
|
+
output.stats.budget = steps;
|
|
100
|
+
// The scripts each member's code loads that only the resolver can see (§19 References only the resolver can see), in
|
|
101
|
+
// member order: the chunk URLs its loaders build, the scripts it injects and its worker URLs that aren't literals.
|
|
102
|
+
for (const member of group.members) {
|
|
103
|
+
const loads = scriptLoads(member.text, member.sourceType, bindings, member.url);
|
|
104
|
+
const artifact = member.url;
|
|
105
|
+
for (const { load, location, urls, prefix, integrity } of loads.chunks) {
|
|
106
|
+
for (const url of urls) {
|
|
107
|
+
const sri = integrity?.[url];
|
|
108
|
+
output.references.push({ url, kind: 'chunk', artifact, location, load, ...(sri === undefined ? {} : { integrity: sri }) });
|
|
109
|
+
}
|
|
110
|
+
if (prefix !== undefined)
|
|
111
|
+
output.references.push({ url: prefix, kind: 'chunk', artifact, location, load, partial: true });
|
|
112
|
+
}
|
|
113
|
+
for (const { location, urls, unknown, types, integrity, noModule } of loads.injected) {
|
|
114
|
+
const carried = {
|
|
115
|
+
...(integrity.length === 0 ? {} : { integrityAlternatives: integrity }),
|
|
116
|
+
...(noModule === 'no' ? {} : { noModule }),
|
|
117
|
+
};
|
|
118
|
+
for (const url of urls) {
|
|
119
|
+
for (const type of types) {
|
|
120
|
+
output.references.push({ url, kind: 'injected', artifact, location, ...(type === undefined ? {} : { type }), ...carried });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// An unknown alternative carries each `type`, since an element of a type a browser doesn't run loads nothing.
|
|
124
|
+
for (const alternative of unknown) {
|
|
125
|
+
for (const type of types) {
|
|
126
|
+
output.references.push({
|
|
127
|
+
url: '',
|
|
128
|
+
kind: 'injected',
|
|
129
|
+
artifact,
|
|
130
|
+
location,
|
|
131
|
+
partial: true,
|
|
132
|
+
alternative,
|
|
133
|
+
...(type === undefined ? {} : { type }),
|
|
134
|
+
...(noModule === 'no' ? {} : { noModule }),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const { location, urls, unknown } of loads.workers) {
|
|
140
|
+
for (const url of urls)
|
|
141
|
+
output.references.push({ url, kind: 'worker', artifact, location });
|
|
142
|
+
for (const alternative of unknown)
|
|
143
|
+
output.references.push({ url: '', kind: 'worker', artifact, location, partial: true, alternative });
|
|
144
|
+
// A URL with no value at all throws before any worker starts (`new URL("x.js")`), which gives nothing (Principle 1):
|
|
145
|
+
// the site is still recorded, so that discovery knows it was folded.
|
|
146
|
+
if (urls.length === 0 && unknown.length === 0)
|
|
147
|
+
output.references.push({ url: '', kind: 'worker', artifact, location, none: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const { program } = group;
|
|
151
|
+
const nested = nestedCode(group.text);
|
|
152
|
+
const grafts = new Map();
|
|
153
|
+
if (evalName.test(group.text))
|
|
154
|
+
nested.graftDirectEvals(program, grafts);
|
|
155
|
+
const global = globalHolder(catalog, source.realm);
|
|
156
|
+
const starts = group.lines;
|
|
157
|
+
// The scan-wide lexical-name check (§3) needs every classic script's top-level lexical names, whatever the budget.
|
|
158
|
+
const declared = globalLexicalDeclarations(program, source.sourceType);
|
|
159
|
+
if (declared.length > 0) {
|
|
160
|
+
output.lexicalDeclarations = declared.map(({ name, kind, id }) => ({
|
|
161
|
+
artifact: source.url,
|
|
162
|
+
name,
|
|
163
|
+
kind,
|
|
164
|
+
generated: locate(starts, id.start, id.end),
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
const analysis = {
|
|
168
|
+
source,
|
|
169
|
+
text: group.text,
|
|
170
|
+
group,
|
|
171
|
+
catalog,
|
|
172
|
+
targets: catalog.targets[source.realm],
|
|
173
|
+
global,
|
|
174
|
+
starts,
|
|
175
|
+
bindings,
|
|
176
|
+
visit,
|
|
177
|
+
nested,
|
|
178
|
+
lexicalGlobals: globalLexicalNames(program, source.sourceType),
|
|
179
|
+
output,
|
|
180
|
+
caps: budget.caps,
|
|
181
|
+
steps: 0,
|
|
182
|
+
budget: steps,
|
|
183
|
+
foldsTooLong: new Set(),
|
|
184
|
+
callsCut: new Set(),
|
|
185
|
+
holdersUnresolved: new Set(),
|
|
186
|
+
loopsCut: new Set(),
|
|
187
|
+
descriptorsCut: new Map(),
|
|
188
|
+
candidatesCut: new Map(),
|
|
189
|
+
capsHit: new Map(),
|
|
190
|
+
requireLinks: new Map(),
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
analyzeCode(analysis, {
|
|
194
|
+
program,
|
|
195
|
+
sourceType: source.sourceType,
|
|
196
|
+
grafts,
|
|
197
|
+
globalCode: false,
|
|
198
|
+
evalCode: false,
|
|
199
|
+
around: realmOf(entry),
|
|
200
|
+
instance: 'current',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!(error instanceof BudgetExhausted))
|
|
205
|
+
throw error;
|
|
206
|
+
// The writes whose resolution finished inside the budget, in walk order, are the ones reported, each as the
|
|
207
|
+
// unbudgeted run reports it: the analysis stopped inside the next one, which is dropped with everything after it
|
|
208
|
+
// (§18). Each member of the group is named.
|
|
209
|
+
output.stats.exhausted = true;
|
|
210
|
+
for (const member of output.members) {
|
|
211
|
+
output.limitations.push({
|
|
212
|
+
kind: 'analysis-budget',
|
|
213
|
+
artifact: { url: member.url, hash: member.hash },
|
|
214
|
+
detail: `the link group's step budget of ${steps} steps, ${budget.stepsPerByte} per source byte of its ${bytes} ` +
|
|
215
|
+
'bytes, ran out, so the writes after the last one it finished are not analyzed',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A record located in the group's text names the member its location is in.
|
|
220
|
+
for (const limitation of output.limitations) {
|
|
221
|
+
const member = limitation.location === undefined ? undefined : starts.artifactOf(limitation.location);
|
|
222
|
+
if (member !== undefined)
|
|
223
|
+
limitation.artifact = { url: member.url, hash: member.hash };
|
|
224
|
+
}
|
|
225
|
+
output.stats.steps = analysis.steps;
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A member's module-linking idioms (§6), as its own code uses them. A member whose code doesn't parse is in no group's
|
|
230
|
+
* program, so this is never asked of one.
|
|
231
|
+
*/
|
|
232
|
+
function memberLinks(member, bindings) {
|
|
233
|
+
const parsed = parse(member.text, member.sourceType);
|
|
234
|
+
if ('error' in parsed)
|
|
235
|
+
throw new Error(`a link group's member ${member.url} doesn't parse`);
|
|
236
|
+
const scope = scopeRules(parsed.program, member.sourceType, bindings);
|
|
237
|
+
return moduleLinks(parsed.program, definitionsOf(parsed.program, scope));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* A trace whose steps each name the artifact their location is in, when that isn't the occurrence's own: a step in
|
|
241
|
+
* another member of the link group (§6, §14).
|
|
242
|
+
*/
|
|
243
|
+
function elsewhere(trace, artifact, starts) {
|
|
244
|
+
return trace.map((step) => {
|
|
245
|
+
if (step.kind === 'module-link' || !('generated' in step))
|
|
246
|
+
return step;
|
|
247
|
+
const at = starts.artifactOf(step.generated);
|
|
248
|
+
return at === undefined || at.url === artifact ? step : { ...step, artifact: at.url };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Thrown when a link group's step budget runs out, wherever the analysis is: it abandons the group's analysis. */
|
|
252
|
+
class BudgetExhausted extends Error {
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Counts one step of the group's step budget (§18): each node below a program's root the walk enters, and each value
|
|
256
|
+
* evaluated. Once the budget is spent, it throws `BudgetExhausted` instead. Steps count work, never time, so where the
|
|
257
|
+
* budget runs out depends only on the group's bytes.
|
|
258
|
+
*/
|
|
259
|
+
function step(analysis) {
|
|
260
|
+
if (analysis.steps >= analysis.budget)
|
|
261
|
+
throw new BudgetExhausted('the step budget ran out');
|
|
262
|
+
analysis.steps++;
|
|
263
|
+
}
|
|
264
|
+
/** The name `eval`, spelt out or with Unicode escapes, as a direct eval's callee must be written. */
|
|
265
|
+
const evalName = /(?:e|\\u(?:0065|\{0*65\}))(?:v|\\u(?:0076|\{0*76\}))(?:a|\\u(?:0061|\{0*61\}))(?:l|\\u(?:006[cC]|\{0*6[cC]\}))/;
|
|
266
|
+
const none = { steps: [], reasons: [] };
|
|
267
|
+
/** Walks one program, reporting the writes in it and in the code nested in it. */
|
|
268
|
+
function analyzeCode(analysis, code) {
|
|
269
|
+
const { source, catalog, global, starts, bindings, output } = analysis;
|
|
270
|
+
const { program, sourceType, grafts } = code;
|
|
271
|
+
const { group } = analysis;
|
|
272
|
+
// The group's own program, whose imports are bindings of the variables they name (§6 Link groups).
|
|
273
|
+
const host = program === group.program;
|
|
274
|
+
const scope = scopeRules(program, sourceType, bindings, {
|
|
275
|
+
directEvals: new Set(grafts.keys()),
|
|
276
|
+
...(code.globalCode ? { lexicalGlobals: analysis.lexicalGlobals } : {}),
|
|
277
|
+
evalCode: code.evalCode,
|
|
278
|
+
...(host ? { imports: group.bound } : {}),
|
|
279
|
+
});
|
|
280
|
+
const { instance } = code;
|
|
281
|
+
const { caps } = output.stats;
|
|
282
|
+
// Every cap hit is counted (§18). A `fold-length` hit raises `analysis-budget` only when a write's key, holder or
|
|
283
|
+
// descriptor passed it and the write reaches a native holder (Principle 5, §13): `budget`.
|
|
284
|
+
const cap = (name) => {
|
|
285
|
+
caps[name] = (caps[name] ?? 0) + 1;
|
|
286
|
+
};
|
|
287
|
+
const budget = (folds) => {
|
|
288
|
+
for (const fold of folds)
|
|
289
|
+
foldTooLong(analysis, fold);
|
|
290
|
+
};
|
|
291
|
+
// The descriptor fields a holder supplies as a descriptor's prototype (§3 Undecidable outcomes).
|
|
292
|
+
const descriptorFields = (holder) => descriptorFieldsOf(holder, catalog, source.realm);
|
|
293
|
+
const definitions = definitionsOf(program, scope);
|
|
294
|
+
// The call graph takes each call's `typeof` guards from the resolution, which takes its calls from the graph.
|
|
295
|
+
let guardsOf = () => new Map();
|
|
296
|
+
const calls = visited(callGraph(program, scope, definitions, (parent, child) => guardsOf(parent, child)), analysis.visit);
|
|
297
|
+
// The module-linking idioms (§6). Only the host program's are the artifacts' summaries: each member's own, with the
|
|
298
|
+
// imports of its code that name no member of the group.
|
|
299
|
+
const links = moduleLinks(program, definitions);
|
|
300
|
+
for (const [call, link] of links.links)
|
|
301
|
+
analysis.requireLinks.set(call, link);
|
|
302
|
+
if (host) {
|
|
303
|
+
for (const member of group.members) {
|
|
304
|
+
const own = group.members.length === 1 ? links : memberLinks(member, bindings);
|
|
305
|
+
// A require call in the member's code links through its group's tables, which chunks a loader links add to
|
|
306
|
+
// (§6 idiom 6): what the member's code alone can't link may link in its group.
|
|
307
|
+
const inMember = (node) => starts.artifactOf(locate(starts, node.start, node.end))?.url === member.url;
|
|
308
|
+
const idioms = new Set(own.idioms);
|
|
309
|
+
const linked = [...links.factories].some(([fn, { require, module }]) => inMember(fn) && links.firsts.get(require)?.has(module));
|
|
310
|
+
if (linked || [...links.links.keys()].some(inMember))
|
|
311
|
+
idioms.add('factory-table');
|
|
312
|
+
if (own.pushed > 0)
|
|
313
|
+
idioms.add('factory-table');
|
|
314
|
+
output.moduleLinking[member.url] = {
|
|
315
|
+
idioms: [...idioms].sort(),
|
|
316
|
+
modules: own.modules + own.pushed,
|
|
317
|
+
unlinked: links.broken.filter((broken) => inMember(broken.call)).length + (group.unlinked.get(member.url) ?? 0),
|
|
318
|
+
linkGroups: 1,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const resolution = globalResolution({
|
|
323
|
+
links,
|
|
324
|
+
program,
|
|
325
|
+
catalog,
|
|
326
|
+
realm: source.realm,
|
|
327
|
+
global,
|
|
328
|
+
sourceType,
|
|
329
|
+
scope,
|
|
330
|
+
definitions,
|
|
331
|
+
instance,
|
|
332
|
+
cap,
|
|
333
|
+
capHit: (name, node) => capHit(analysis, name, node),
|
|
334
|
+
calls,
|
|
335
|
+
budget: (site, why) => callCut(analysis, site, why),
|
|
336
|
+
caps: analysis.caps,
|
|
337
|
+
step: () => step(analysis),
|
|
338
|
+
});
|
|
339
|
+
guardsOf = resolution.guards;
|
|
340
|
+
resolution.prepare();
|
|
341
|
+
const routes = codeRoutes({ catalog, realm: source.realm, global, program, scope, resolution });
|
|
342
|
+
const context = {
|
|
343
|
+
analysis,
|
|
344
|
+
scope,
|
|
345
|
+
resolution,
|
|
346
|
+
routes,
|
|
347
|
+
grafts,
|
|
348
|
+
instance,
|
|
349
|
+
deferralAt: (node, timer) => deferralAt(node, timer),
|
|
350
|
+
};
|
|
351
|
+
// What is known where the walk is: the names `typeof` guards around it show are defined, and whether `this` there is
|
|
352
|
+
// the script's top-level `this`. Each frame is the node that opened it.
|
|
353
|
+
const frames = [];
|
|
354
|
+
const guards = new Map();
|
|
355
|
+
let boundaries = 0;
|
|
356
|
+
const at = {
|
|
357
|
+
guard: (name) => guards.get(name)?.at(-1),
|
|
358
|
+
get topLevel() {
|
|
359
|
+
return boundaries === 0;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
// The direct-eval code the walk is in, innermost last, each with the nesting it adds.
|
|
363
|
+
const evals = [];
|
|
364
|
+
// The functions and loops the walk is in, innermost last.
|
|
365
|
+
const contexts = [];
|
|
366
|
+
// The nodes the walk is in, outermost first, for the guards on a write's path (§7).
|
|
367
|
+
const ancestors = [];
|
|
368
|
+
const guardContext = {
|
|
369
|
+
resolution,
|
|
370
|
+
scope,
|
|
371
|
+
catalog,
|
|
372
|
+
realm: source.realm,
|
|
373
|
+
global,
|
|
374
|
+
text: analysis.text,
|
|
375
|
+
starts,
|
|
376
|
+
parentOf: (node) => resolution.parentOf(node),
|
|
377
|
+
caps: analysis.caps,
|
|
378
|
+
capHit: (name, node) => capHit(analysis, name, node),
|
|
379
|
+
};
|
|
380
|
+
// The factories lazy chunks push, each with the function its chunk runs in (§6 idiom 6).
|
|
381
|
+
const chunkWrappers = new Map();
|
|
382
|
+
if (host && group.chunks.size > 0) {
|
|
383
|
+
const chunks = [...group.wrappers].filter(([, member]) => group.chunks.has(member.url)).map(([wrapper]) => wrapper);
|
|
384
|
+
for (const pushed of links.chunkFactories.values()) {
|
|
385
|
+
for (const factory of [...pushed.values()].flat()) {
|
|
386
|
+
const wrapper = chunks.find((each) => each.start <= factory.start && factory.end <= each.end);
|
|
387
|
+
if (wrapper !== undefined)
|
|
388
|
+
chunkWrappers.set(factory, wrapper);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// The chains out from the functions writes are in to their install entries (§7).
|
|
393
|
+
const judgeChain = chainJudge({
|
|
394
|
+
...guardContext,
|
|
395
|
+
callers: (fn) => resolution.callersOf(fn),
|
|
396
|
+
// A chunk's pushed callback runs where it is pushed, called by the loader that reads the push (§6 idiom 6).
|
|
397
|
+
runBy: (fn) => grafts.get(fn)?.call ?? links.pushCallbacks.get(fn),
|
|
398
|
+
loadedBy: (fn) => analysis.group.loadedBy.get(fn),
|
|
399
|
+
chunkOf: (fn) => chunkWrappers.get(fn),
|
|
400
|
+
});
|
|
401
|
+
/**
|
|
402
|
+
* Whether code a call at `node` compiles runs only later (§7 Install context): when this program does, when only a
|
|
403
|
+
* later call reaches the call, or when a timer runs the code. The outermost install entry is the one recorded.
|
|
404
|
+
*/
|
|
405
|
+
const deferralAt = (node, timer) => {
|
|
406
|
+
// Only the chain's install entry is wanted, so the call is judged as a write of no target, on its own path.
|
|
407
|
+
const path = ancestors.slice(0, ancestors.lastIndexOf(node) + 1);
|
|
408
|
+
const position = { guard: at.guard, topLevel: at.topLevel };
|
|
409
|
+
let judged;
|
|
410
|
+
const judge = () => {
|
|
411
|
+
const outer = code.deferral?.();
|
|
412
|
+
if (outer !== undefined)
|
|
413
|
+
return outer;
|
|
414
|
+
const fn = calls.functionOf(node);
|
|
415
|
+
const site = { site: node, target: { name: '' }, taints: [] };
|
|
416
|
+
const chain = fn === undefined ? undefined : judgeChain(path, site, position, fn);
|
|
417
|
+
return deferralOf(chain?.entry) ?? (timer ? { entry: null } : undefined);
|
|
418
|
+
};
|
|
419
|
+
return () => (judged ??= { deferral: judge() }).deferral;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* The install entry a chain ends at, unless it reaches top-level code (§7 Install context). A direct eval runs its
|
|
423
|
+
* grafted code where it stands, so code that ends at a graft waits on whatever the eval call waits on.
|
|
424
|
+
*/
|
|
425
|
+
const deferralOf = (entry) => {
|
|
426
|
+
const outer = code.deferral?.();
|
|
427
|
+
if (outer !== undefined)
|
|
428
|
+
return outer;
|
|
429
|
+
if (entry === undefined)
|
|
430
|
+
return undefined;
|
|
431
|
+
const graft = grafts.get(entry);
|
|
432
|
+
if (graft !== undefined)
|
|
433
|
+
return deferralAt(graft.call, false)();
|
|
434
|
+
// A link group's member that only dynamic imports load waits on an import at load, which has no name.
|
|
435
|
+
if (analysis.group.wrappers.has(entry))
|
|
436
|
+
return { entry: null };
|
|
437
|
+
return { entry: entryName(entry, (each) => resolution.parentOf(each)) };
|
|
438
|
+
};
|
|
439
|
+
// The occurrences instantiated in frames, which two frames give alike when the write doesn't depend on them.
|
|
440
|
+
const instantiated = new Map();
|
|
441
|
+
// Each occurrence given, with the write and frame of each instantiation of it, for rewritten deletes (§3).
|
|
442
|
+
const placed = [];
|
|
443
|
+
// The occurrences each statically false catalog check pruned, by their JSON, and the tests on each occurrence given
|
|
444
|
+
// with their `guard` steps, for the `prune` steps (§7, §14).
|
|
445
|
+
const prunedBy = new Map();
|
|
446
|
+
const guarded = [];
|
|
447
|
+
// The writes each `try` that can't throw kept its `catch` arm from making, by their records' JSON, and each occurrence
|
|
448
|
+
// given in a `try` block, with where its `prune` steps go (§5 Resolver capabilities 9).
|
|
449
|
+
const armPrunes = { prunedBy: new Map(), tried: [] };
|
|
450
|
+
const dropArm = (arm, record) => {
|
|
451
|
+
const statement = resolution.parentOf(arm);
|
|
452
|
+
const pruned = armPrunes.prunedBy.get(statement);
|
|
453
|
+
if (pruned === undefined)
|
|
454
|
+
armPrunes.prunedBy.set(statement, new Set([JSON.stringify(record)]));
|
|
455
|
+
else
|
|
456
|
+
pruned.add(JSON.stringify(record));
|
|
457
|
+
};
|
|
458
|
+
// The writes the argument-literal rule dropped, by the test that ruled out their arm and the literal that settled
|
|
459
|
+
// it, and each occurrence given with the tests and literals that decided its write, with where its `prune` steps go.
|
|
460
|
+
const argumentPrunes = { prunedBy: new Map(), traced: [] };
|
|
461
|
+
const dropRuledOut = (test, literal, record) => {
|
|
462
|
+
const key = decisionKey(test, literal);
|
|
463
|
+
let pruned = argumentPrunes.prunedBy.get(key);
|
|
464
|
+
if (pruned === undefined)
|
|
465
|
+
argumentPrunes.prunedBy.set(key, (pruned = { test, literal, records: new Set() }));
|
|
466
|
+
pruned.records.add(JSON.stringify(record));
|
|
467
|
+
};
|
|
468
|
+
// The frames whose writes a call argument literal's field decided, each with what it gave, at the walk's place.
|
|
469
|
+
const undecided = [];
|
|
470
|
+
// An occurrence's `prune` steps for the tests a call argument literal settled on its path go after their `guard`
|
|
471
|
+
// steps, and those for the tests that decided its write's values after all its guards, at `at`.
|
|
472
|
+
const argumentTraced = (occurrence, tests) => {
|
|
473
|
+
for (const guard of tests.values()) {
|
|
474
|
+
const settled = argumentGuards.get(guard);
|
|
475
|
+
if (settled !== undefined)
|
|
476
|
+
argumentPrunes.traced.push({ occurrence, key: decisionKey(settled.test, settled.literal), after: guard });
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const descriptorsCut = (sites, native) => {
|
|
480
|
+
for (const site of sites)
|
|
481
|
+
descriptorCut(analysis, site, native);
|
|
482
|
+
};
|
|
483
|
+
const writeContext = {
|
|
484
|
+
scope,
|
|
485
|
+
resolution,
|
|
486
|
+
global,
|
|
487
|
+
instance,
|
|
488
|
+
budget,
|
|
489
|
+
descriptorsCut,
|
|
490
|
+
candidatesCut: (node, native) => candidatesCut(analysis, node, native),
|
|
491
|
+
capHit: (name, node) => capHit(analysis, name, node),
|
|
492
|
+
descriptorFields,
|
|
493
|
+
caps: analysis.caps,
|
|
494
|
+
patternPrefix: (holder, prefix) => prefixMembers(holder, prefix, catalog, source.realm).length === 0,
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* The writes a node makes, each with the frame it was instantiated in (§5 Resolver capabilities 3): in each frame of
|
|
498
|
+
* the function it is in, when they read that function's parameters and a call of it is known.
|
|
499
|
+
*/
|
|
500
|
+
const writesOf = (node) => {
|
|
501
|
+
const bare = (sites) => ({
|
|
502
|
+
writes: sites.writes.map((write) => ({ write })),
|
|
503
|
+
open: sites.open,
|
|
504
|
+
wild: sites.wild.map((write) => ({ write })),
|
|
505
|
+
wide: sites.wide,
|
|
506
|
+
decided: new Map(),
|
|
507
|
+
});
|
|
508
|
+
if (contexts.length === 0)
|
|
509
|
+
return bare(sitesAt(node, at, writeContext));
|
|
510
|
+
const { value, functions: read } = resolution.contextual(() => sitesAt(node, at, writeContext));
|
|
511
|
+
// The innermost loop, or the function the write is in, whose parameters, `this` or iterations the write reads. A
|
|
512
|
+
// write that reads the bindings of code around its function, which the function closes over, or the `this` of a
|
|
513
|
+
// constructor it runs in (a field initializer's), is instantiated in the frames of its function, whose closures
|
|
514
|
+
// hold them (§5 Resolver capabilities 3).
|
|
515
|
+
let context;
|
|
516
|
+
let fn;
|
|
517
|
+
for (let index = contexts.length - 1; index >= 0; index--) {
|
|
518
|
+
const each = contexts[index];
|
|
519
|
+
if (read.has(each)) {
|
|
520
|
+
context = fn ?? each;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
if (isFunction(each))
|
|
524
|
+
fn ??= each;
|
|
525
|
+
}
|
|
526
|
+
if (context === undefined && read.size > 0)
|
|
527
|
+
context = fn ?? [...read].find((each) => !isLoop(each));
|
|
528
|
+
if (context === undefined)
|
|
529
|
+
return bare(value);
|
|
530
|
+
// Iterations and callbacks run within the frames of the code around them when the write reads that code too. A
|
|
531
|
+
// read the write makes only once an iteration binds its key (`p[m]` reads `p` once `m` folds) shows up only in
|
|
532
|
+
// the iterations, which are then made again within those frames.
|
|
533
|
+
const { guard, topLevel } = at;
|
|
534
|
+
const instantiate = (deep) => resolution.framesOf(context, deep).map((frame) => {
|
|
535
|
+
const { value: found, decisions } = resolution.decided(() => resolution.contextual(() => sitesAt(node, { guard, topLevel, frame }, writeContext)));
|
|
536
|
+
return { frame, sites: found.value, reads: found.functions, decisions };
|
|
537
|
+
});
|
|
538
|
+
const deep = [...read].some((each) => each !== context);
|
|
539
|
+
let instances = instantiate(deep);
|
|
540
|
+
if (!deep && instances.some(({ reads }) => contexts.some((each) => each !== context && reads.has(each)))) {
|
|
541
|
+
instances = instantiate(true);
|
|
542
|
+
}
|
|
543
|
+
if (instances.length === 0)
|
|
544
|
+
return bare(value);
|
|
545
|
+
return {
|
|
546
|
+
writes: instances.flatMap(({ frame, sites }) => sites.writes.map((write) => ({ write, frame }))),
|
|
547
|
+
open: instances.flatMap(({ sites }) => sites.open),
|
|
548
|
+
wild: instances.flatMap(({ frame, sites }) => sites.wild.map((write) => ({ write, frame }))),
|
|
549
|
+
wide: instances.flatMap(({ sites }) => sites.wide),
|
|
550
|
+
decided: new Map(instances.filter(({ decisions }) => decisions.length > 0).map(({ frame, decisions }) => [frame, decisions])),
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
const foldKey = (key) => {
|
|
554
|
+
const values = resolution.evaluate(key, at);
|
|
555
|
+
const [value] = values;
|
|
556
|
+
return values.length === 1 && value?.kind === 'constant' ? toPropertyKey(value.value) : undefined;
|
|
557
|
+
};
|
|
558
|
+
// Whether a write site whose holder the analyzer can't resolve keeps the original of its slot (§5).
|
|
559
|
+
const shapes = patchShapes({
|
|
560
|
+
program,
|
|
561
|
+
definitions,
|
|
562
|
+
fold: foldKey,
|
|
563
|
+
isCatalogNative: (callee) => catalogNative(callee, resolution, at, catalog, source.realm),
|
|
564
|
+
isDescriptorReader: (callee) => resolution
|
|
565
|
+
.evaluate(callee, at)
|
|
566
|
+
.some((value) => value.kind === 'native' && descriptorReaders.has(value.name)),
|
|
567
|
+
aliasDepth: analysis.caps.aliasDepth,
|
|
568
|
+
aliasCut: (id) => capHit(analysis, 'aliasDepth', id),
|
|
569
|
+
calleeFunctions: (callee) => resolution
|
|
570
|
+
.evaluate(callee, at)
|
|
571
|
+
.flatMap((value) => value.kind === 'object' && value.heap?.kind === 'function' && isFunction(value.heap.node) ? [value.heap.node] : []),
|
|
572
|
+
});
|
|
573
|
+
// Whether a replacement is shown to call the original (§5 Resolver capabilities 10).
|
|
574
|
+
const evidence = wrapEvidence({
|
|
575
|
+
resolution,
|
|
576
|
+
definitions,
|
|
577
|
+
catalog,
|
|
578
|
+
realm: source.realm,
|
|
579
|
+
global,
|
|
580
|
+
isCatalogNative: (callee, where) => catalogNative(callee, resolution, where, catalog, source.realm),
|
|
581
|
+
});
|
|
582
|
+
// Whether a write puts back the native of its slot (§3 Restores).
|
|
583
|
+
const judgeRestore = restoreJudge({ resolution, definitions, catalog, realm: source.realm, global, text: source.text });
|
|
584
|
+
// The proven restores, which give no occurrence, and the occurrences of unproven ones, for their installs (§3).
|
|
585
|
+
const provenRestores = [];
|
|
586
|
+
const unprovenRestores = [];
|
|
587
|
+
// The absence tags a write's holder or value carries (§5 Resolver capabilities 7, §7).
|
|
588
|
+
const tagsOf = absenceTags({
|
|
589
|
+
resolution,
|
|
590
|
+
definitions,
|
|
591
|
+
catalog,
|
|
592
|
+
realm: source.realm,
|
|
593
|
+
global,
|
|
594
|
+
functionOf: (node) => calls.functionOf(node),
|
|
595
|
+
judge: (site, where, target) => judgeChain(functionPath(site, (node) => resolution.parentOf(node)), { site, target, taints: [] }, where, calls.functionOf(site)),
|
|
596
|
+
evidence,
|
|
597
|
+
});
|
|
598
|
+
const nesting = () => evals.length === 0 ? code.around : joined(code.around, ...evals.map((level) => level.nesting));
|
|
599
|
+
// The literal code of the script elements the program inserts, by the call that inserts each. Only the window
|
|
600
|
+
// has `document.createElement`.
|
|
601
|
+
const scriptTexts = source.realm === 'window' && /createElement/.test(analysis.text)
|
|
602
|
+
? injectedTexts(program, new Folder(program, definitions, new Map()))
|
|
603
|
+
: undefined;
|
|
604
|
+
walkTree(program, {
|
|
605
|
+
enter(node, parent) {
|
|
606
|
+
// A program's root is no work of its own, so an empty artifact, whose step budget is 0, takes no step.
|
|
607
|
+
if (parent !== null)
|
|
608
|
+
step(analysis);
|
|
609
|
+
const graft = grafts.get(node);
|
|
610
|
+
if (graft !== undefined) {
|
|
611
|
+
// Direct-eval code is analyzed only when the call's `eval` is the global one; otherwise it is just a string.
|
|
612
|
+
const callee = nativeCallee(graft.call, context, at);
|
|
613
|
+
if (callee?.name !== 'eval')
|
|
614
|
+
return false;
|
|
615
|
+
evals.push({ node, nesting: nestingOf('eval', 'caller', graft.call, callee.leaves, analysis, [node]) });
|
|
616
|
+
}
|
|
617
|
+
if (parent !== null) {
|
|
618
|
+
const guarded = resolution.guards(parent, node);
|
|
619
|
+
const boundary = isThisBoundary(node, parent);
|
|
620
|
+
if (guarded.size > 0 || boundary) {
|
|
621
|
+
frames.push({ node, guards: guarded, boundary });
|
|
622
|
+
for (const [name, test] of guarded) {
|
|
623
|
+
const tests = guards.get(name);
|
|
624
|
+
if (tests === undefined)
|
|
625
|
+
guards.set(name, [test]);
|
|
626
|
+
else
|
|
627
|
+
tests.push(test);
|
|
628
|
+
}
|
|
629
|
+
if (boundary)
|
|
630
|
+
boundaries++;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
ancestors.push(node);
|
|
634
|
+
if (node.type === 'CallExpression' || node.type === 'NewExpression')
|
|
635
|
+
codeSite(node, context, at, nesting());
|
|
636
|
+
else if (node.type === 'TaggedTemplateExpression')
|
|
637
|
+
indirectSite(node, context, at);
|
|
638
|
+
const texts = scriptTexts?.get(node);
|
|
639
|
+
if (texts !== undefined)
|
|
640
|
+
scriptTextSite(node, texts, context, nesting());
|
|
641
|
+
if (isFunction(node) || isLoop(node))
|
|
642
|
+
contexts.push(node);
|
|
643
|
+
const { writes, open, wild, wide, decided: decidedIn } = writesOf(node);
|
|
644
|
+
// Each frame's outcomes, by `outcomeKey`, which a frame's writes give without the argument-literal rule's
|
|
645
|
+
// decisions are compared with.
|
|
646
|
+
const given = new Map();
|
|
647
|
+
const giving = (write, frame, decided) => {
|
|
648
|
+
if (frame === undefined || !decidedIn.has(frame))
|
|
649
|
+
return;
|
|
650
|
+
let keys = given.get(frame);
|
|
651
|
+
if (keys === undefined)
|
|
652
|
+
given.set(frame, (keys = new Set()));
|
|
653
|
+
keys.add(outcomeKey(write, decided));
|
|
654
|
+
};
|
|
655
|
+
// The sites at this node that give a finding: a site gives findings or a record, never both (§5).
|
|
656
|
+
const found = new Set();
|
|
657
|
+
// An occurrence whose write's values a call argument literal's field decided in its frame gets a `prune` step
|
|
658
|
+
// for each such test that dropped writes, after its guards.
|
|
659
|
+
const decidedTraced = (occurrence, frame, outcomes) => {
|
|
660
|
+
const decisions = frame === undefined ? undefined : decidedIn.get(frame);
|
|
661
|
+
if (decisions === undefined)
|
|
662
|
+
return;
|
|
663
|
+
const index = occurrence.trace.length - outcomes - 1;
|
|
664
|
+
const after = index < 0 ? 0 : occurrence.trace[index];
|
|
665
|
+
const ordered = [...decisions].sort((a, b) => a.test.start - b.test.start || a.literal.start - b.literal.start);
|
|
666
|
+
for (const { test, literal } of ordered)
|
|
667
|
+
argumentPrunes.traced.push({ occurrence, key: decisionKey(test, literal), after });
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Reports one outcome of a write: an occurrence, or an ineffective write. `extra` are the confidence-reason steps
|
|
671
|
+
* the outcome carries beyond `outcome-undecidable`.
|
|
672
|
+
*/
|
|
673
|
+
const emit = (write, frame, decided, extra = [], restore) => {
|
|
674
|
+
found.add(write.site);
|
|
675
|
+
// A wildcard names no member, so no rewritten delete judges it, nor rewrites one.
|
|
676
|
+
const wildcard = 'key' in write;
|
|
677
|
+
// Whether the group's realm is assumed, the nested code the write is in, then each leaf's binding step, and
|
|
678
|
+
// each fold the write comes from, each followed by the evidence for the taint on its name's resolution path
|
|
679
|
+
// (§9, §14). Last, the evidence that the outcome can't be decided, or the outcome of an ineffective write.
|
|
680
|
+
const around = nesting();
|
|
681
|
+
const { steps: made, reasons } = joined(around, creationSteps(frame, starts), stepsOf(write.leaves, analysis), foldSteps(write.folds, analysis), prefixSteps(write, starts), candidateSteps(write, starts));
|
|
682
|
+
const { name, holder, shadows, effect } = decided;
|
|
683
|
+
// The guards between the write and the start of its function (§7), evaluated in the write's frame, and the
|
|
684
|
+
// `with` and direct-`eval` taint its confidence-reason steps show.
|
|
685
|
+
const judgedWrite = {
|
|
686
|
+
site: write.site,
|
|
687
|
+
target: { name, ...(shadows === undefined ? {} : { shadows }) },
|
|
688
|
+
taints: taintSites(made),
|
|
689
|
+
};
|
|
690
|
+
// With the guards along the call chain out to its install entry, which come first in the trace (§7).
|
|
691
|
+
const position = { guard: at.guard, topLevel: at.topLevel, frame };
|
|
692
|
+
let judged = judgeChain(ancestors, judgedWrite, position, functionAround(ancestors, write.site));
|
|
693
|
+
// A filter on a swept key that drops it: the sweep never writes that key (§5 Resolver capabilities 9).
|
|
694
|
+
if (judged.dead !== undefined && sweepFilters.has(judged.dead))
|
|
695
|
+
return;
|
|
696
|
+
// An arm a call argument literal's field rules out is not reached there (§5 Resolver capabilities 5), but
|
|
697
|
+
// each write it drops leaves a counted, traced `prune`, counted as a `catch` arm's are.
|
|
698
|
+
if (judged.dead !== undefined && ruledOutTests.has(judged.dead)) {
|
|
699
|
+
const { start, end } = write.site;
|
|
700
|
+
return dropRuledOut(judged.dead, judged.by, { start, end, holder, name, shadows, effect, mechanism: write.mechanism });
|
|
701
|
+
}
|
|
702
|
+
// A `catch` arm whose `try` can't throw never runs, so its writes give nothing, but each leaves a counted,
|
|
703
|
+
// traced `prune` (§5 Resolver capabilities 9).
|
|
704
|
+
// It is dropped before any more work, so the arm spends no more of the step budget than before (Principle 5),
|
|
705
|
+
// and counted once per write site, target and effect, however many frames reach it.
|
|
706
|
+
if (judged.dead !== undefined && unreachedArms.has(judged.dead)) {
|
|
707
|
+
const { start, end } = write.site;
|
|
708
|
+
return dropArm(judged.dead, { start, end, holder, name, shadows, effect, mechanism: write.mechanism });
|
|
709
|
+
}
|
|
710
|
+
// A filter that may have dropped a swept key makes the write `conditional`, along the path.
|
|
711
|
+
const undecided = undecidedFilterSteps(write.folds, starts);
|
|
712
|
+
if (undecided.length > 0) {
|
|
713
|
+
judged = { ...judged, reachability: along(judged.reachability, 'conditional'), steps: [...judged.steps, ...undecided] };
|
|
714
|
+
}
|
|
715
|
+
// A holder or value chosen on a native-absent branch keeps `guarded-native-absence` (§5 capability 7).
|
|
716
|
+
if (judged.reachability !== 'guarded-native-absence') {
|
|
717
|
+
const tagged = tagsOf(write, decided, position);
|
|
718
|
+
if (tagged !== undefined) {
|
|
719
|
+
const steps = [...judged.steps, ...unique(tagged, judged.steps)];
|
|
720
|
+
judged = { ...judged, reachability: 'guarded-native-absence', steps };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Nested code that runs only later waits on its compile site's install entry, the outermost one (§7).
|
|
724
|
+
const deferral = deferralOf(judged.entry);
|
|
725
|
+
// A function object the chain is judged from the creation site of shows its `creation-site` step there, after
|
|
726
|
+
// the guards in force where it was made, and not again among the binding steps.
|
|
727
|
+
const shown = new Set(judged.steps.flatMap((step) => (step.kind === 'creation-site' ? [creationKey(step)] : [])));
|
|
728
|
+
const steps = made.filter((step) => step.kind !== 'creation-site' || !shown.has(creationKey(step)));
|
|
729
|
+
// The filters on the path that test a swept key are the sweep's too: its `enumeration` step records them.
|
|
730
|
+
const traced = withPathFilters(steps, judged.tests, starts);
|
|
731
|
+
const generated = locate(starts, write.site.start, write.site.end);
|
|
732
|
+
const where = starts.artifactOf(generated);
|
|
733
|
+
// A wildcard narrowed to a candidate set matches only those candidates, for `--surface` too.
|
|
734
|
+
const { members, prefixes } = wildcard ? write : {};
|
|
735
|
+
const matches = members === undefined && prefixes === undefined
|
|
736
|
+
? {}
|
|
737
|
+
: { matches: narrowedMatches(write.holder, members, prefixes ?? [], catalog, source.realm).map(({ target }) => target) };
|
|
738
|
+
const written = {
|
|
739
|
+
artifact: where.url,
|
|
740
|
+
generated,
|
|
741
|
+
lookup: lookupOf(write, around, starts, resolution, where),
|
|
742
|
+
target: { realm: source.realm, holder, name, ...(shadows === undefined ? {} : { shadows }) },
|
|
743
|
+
effect,
|
|
744
|
+
mechanism: write.mechanism,
|
|
745
|
+
reachability: judged.reachability,
|
|
746
|
+
reachabilityBasis: judged.basis ?? 'call-path',
|
|
747
|
+
installContext: deferral === undefined ? 'load' : 'deferred',
|
|
748
|
+
installEntry: deferral?.entry ?? null,
|
|
749
|
+
realmInstance: write.realmInstance,
|
|
750
|
+
...matches,
|
|
751
|
+
};
|
|
752
|
+
if (isIneffective(decided)) {
|
|
753
|
+
// A write that can't change its target is never a finding (§3), and a throwing one prunes nothing after it.
|
|
754
|
+
const { reason, throws } = decided;
|
|
755
|
+
const ineffective = {
|
|
756
|
+
...written,
|
|
757
|
+
trace: elsewhere([...traced, ...judged.steps, outcomeStep(decided)], where.url, starts),
|
|
758
|
+
reason,
|
|
759
|
+
throws,
|
|
760
|
+
strictness: scope.isStrict(write.site.start) ? 'strict' : 'sloppy',
|
|
761
|
+
};
|
|
762
|
+
if (frame !== undefined) {
|
|
763
|
+
const key = JSON.stringify(ineffective);
|
|
764
|
+
if (instantiated.has(key))
|
|
765
|
+
return;
|
|
766
|
+
instantiated.set(key, undefined);
|
|
767
|
+
}
|
|
768
|
+
output.ineffectiveWrites.push(ineffective);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// A proven restore is never a finding or an occurrence: it is a step on its install's trace, if any (§3).
|
|
772
|
+
if (restore?.proven === true) {
|
|
773
|
+
provenRestores.push({ restore, target: written.target, generated: written.generated });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const { locks, undecidable } = decided;
|
|
777
|
+
const outcome = [...(undecidable === undefined ? [] : [undecidableStep(name, undecidable, starts)]), ...extra];
|
|
778
|
+
const occurrence = {
|
|
779
|
+
...written,
|
|
780
|
+
locks,
|
|
781
|
+
confidence: {
|
|
782
|
+
target: targetConfidence(reasons),
|
|
783
|
+
mutation: mutationConfidence(outcome.map((step) => step.reason)),
|
|
784
|
+
},
|
|
785
|
+
trace: elsewhere([...traced, ...judged.steps, ...outcome], where.url, starts),
|
|
786
|
+
};
|
|
787
|
+
// A write only a statically false catalog check reaches gives no occurrence, but a wildcard finding stays
|
|
788
|
+
// (§7). The prune is counted once per occurrence it would have given.
|
|
789
|
+
if (judged.dead !== undefined && !wildcard) {
|
|
790
|
+
const pruned = prunedBy.get(judged.dead);
|
|
791
|
+
if (pruned === undefined)
|
|
792
|
+
prunedBy.set(judged.dead, new Set([JSON.stringify(occurrence)]));
|
|
793
|
+
else
|
|
794
|
+
pruned.add(JSON.stringify(occurrence));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (frame !== undefined) {
|
|
798
|
+
const key = JSON.stringify(occurrence);
|
|
799
|
+
const same = instantiated.get(key);
|
|
800
|
+
if (same !== undefined) {
|
|
801
|
+
decidedTraced(same, frame, outcome.length);
|
|
802
|
+
// One occurrence for alike instantiations, each of which a rewritten delete judges apart.
|
|
803
|
+
if (!wildcard)
|
|
804
|
+
placed.push({ occurrence: same, write, frame });
|
|
805
|
+
if (restore !== undefined)
|
|
806
|
+
unprovenRestores.push({ restore, occurrence: same });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
instantiated.set(key, occurrence);
|
|
810
|
+
}
|
|
811
|
+
if (!wildcard)
|
|
812
|
+
placed.push({ occurrence, write, frame });
|
|
813
|
+
if (restore !== undefined)
|
|
814
|
+
unprovenRestores.push({ restore, occurrence });
|
|
815
|
+
if (judged.tests.size > 0)
|
|
816
|
+
guarded.push({ occurrence, tests: judged.tests });
|
|
817
|
+
const tries = triesAround(ancestors, write.site);
|
|
818
|
+
if (tries.length > 0)
|
|
819
|
+
armPrunes.tried.push({ occurrence, tries, at: occurrence.trace.length - outcome.length });
|
|
820
|
+
argumentTraced(occurrence, judged.tests);
|
|
821
|
+
decidedTraced(occurrence, frame, outcome.length);
|
|
822
|
+
output.occurrences.push(occurrence);
|
|
823
|
+
};
|
|
824
|
+
for (const { write, frame } of writes) {
|
|
825
|
+
// A write of a native in an iteration the iteration cap cut raises `analysis-budget` (Principle 5).
|
|
826
|
+
const cut = cutIn(frame);
|
|
827
|
+
if (cut !== undefined)
|
|
828
|
+
loopCut(analysis, cut);
|
|
829
|
+
for (const decided of withWrapEvidence(write, outcomesOf(write, catalog, source.realm), evidence)) {
|
|
830
|
+
// A value restore replaces, and a descriptor restore may also redefine the attributes (§3 Restores).
|
|
831
|
+
const restores = !isIneffective(decided) &&
|
|
832
|
+
(decided.effect === 'replace' || (write.change.kind === 'define' && decided.effect === 'descriptor-change'));
|
|
833
|
+
giving(write, frame, decided);
|
|
834
|
+
emit(write, frame, decided, [], restores ? judgeRestore(write, frame, decided, ancestors) : undefined);
|
|
835
|
+
}
|
|
836
|
+
// A write-back through a heap field changes nothing, but its field may carry the native saved before an install,
|
|
837
|
+
// so a proven one is a restore and leaves its entry on that install (§3 Restores).
|
|
838
|
+
if (write.change.kind === 'set' && write.change.throughHeap === true) {
|
|
839
|
+
const fresh = { ...write, change: { ...write.change, same: false } };
|
|
840
|
+
for (const decided of outcomesOf(fresh, catalog, source.realm)) {
|
|
841
|
+
if (isIneffective(decided) || decided.effect !== 'replace')
|
|
842
|
+
continue;
|
|
843
|
+
const restore = judgeRestore(fresh, frame, decided, ancestors);
|
|
844
|
+
if (restore?.proven === true)
|
|
845
|
+
emit(fresh, frame, decided, [], restore);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const { write, frame } of wild) {
|
|
850
|
+
const cut = cutIn(frame);
|
|
851
|
+
if (cut !== undefined)
|
|
852
|
+
loopCut(analysis, cut);
|
|
853
|
+
for (const { decided, extra } of wildcardOutcomes(write, shapes, evidence, catalog, source.realm, starts)) {
|
|
854
|
+
giving(write, frame, decided);
|
|
855
|
+
emit(write, frame, decided, extra);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const write of open) {
|
|
859
|
+
if (found.has(write.site))
|
|
860
|
+
continue;
|
|
861
|
+
unresolvedHolder(analysis, write, shapes.patchShaped);
|
|
862
|
+
}
|
|
863
|
+
for (const site of wide) {
|
|
864
|
+
if (found.has(site.site))
|
|
865
|
+
continue;
|
|
866
|
+
wideHolders(analysis, site, shapes);
|
|
867
|
+
}
|
|
868
|
+
// The frames whose writes a call argument literal's field decided are found again without those decisions once
|
|
869
|
+
// the walk is done: what they write then and not now, the argument-literal rule dropped.
|
|
870
|
+
if (decidedIn.size > 0) {
|
|
871
|
+
const shown = new Map([...guards].map(([name, tests]) => [name, tests.at(-1)]));
|
|
872
|
+
const position = { guard: (name) => shown.get(name), topLevel: at.topLevel };
|
|
873
|
+
for (const [frame, decisions] of decidedIn) {
|
|
874
|
+
undecided.push({ node, frame, decisions, position: { ...position, frame }, ancestors: [...ancestors], given: given.get(frame) ?? new Set() });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
leave(node) {
|
|
879
|
+
if (ancestors.at(-1) === node)
|
|
880
|
+
ancestors.pop();
|
|
881
|
+
if (contexts.at(-1) === node)
|
|
882
|
+
contexts.pop();
|
|
883
|
+
if (evals.at(-1)?.node === node)
|
|
884
|
+
evals.pop();
|
|
885
|
+
if (frames.at(-1)?.node !== node)
|
|
886
|
+
return;
|
|
887
|
+
const frame = frames.pop();
|
|
888
|
+
for (const name of frame.guards.keys())
|
|
889
|
+
guards.get(name).pop();
|
|
890
|
+
if (frame.boundary)
|
|
891
|
+
boundaries--;
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
unlinkedModuleCalls(analysis, host ? new Map([...links.unlinkedCalls, ...importCalls(analysis, program, scope)]) : links.unlinkedCalls, resolution, calls);
|
|
895
|
+
armPruneSteps(analysis, armPrunes);
|
|
896
|
+
pruneSteps(analysis, prunedBy, guarded);
|
|
897
|
+
installRestores(placed, provenRestores, unprovenRestores, (read, install) => sameChain(calls.functionOf(read), calls.functionOf(install), (fn) => {
|
|
898
|
+
const callers = resolution.callersOf(fn);
|
|
899
|
+
return callers.length > 0 ? callers.map((binding) => binding.site.within) : [calls.functionOf(fn)];
|
|
900
|
+
}));
|
|
901
|
+
dropRewrittenDeletes(analysis, placed, {
|
|
902
|
+
program,
|
|
903
|
+
calls,
|
|
904
|
+
slots: slotSignatures({
|
|
905
|
+
definitions,
|
|
906
|
+
fold: foldKey,
|
|
907
|
+
aliasDepth: analysis.caps.aliasDepth,
|
|
908
|
+
aliasCut: (id) => capHit(analysis, 'aliasDepth', id),
|
|
909
|
+
}),
|
|
910
|
+
});
|
|
911
|
+
// Last, as nothing else then reads the resolution: the writes the argument-literal rule dropped through the values
|
|
912
|
+
// it decided, found by giving each decided frame's writes again without those decisions. That work leaves no trace
|
|
913
|
+
// of its own: its steps, cap hits and limitations are undone, and it is cut short, not the analysis, when it runs
|
|
914
|
+
// past a budget of its own as large as the group's.
|
|
915
|
+
const undone = sideEffects(analysis);
|
|
916
|
+
try {
|
|
917
|
+
for (const each of undecided) {
|
|
918
|
+
const outcomesWithout = (variant, unsettles) => {
|
|
919
|
+
const sites = resolution.counterfactual(variant, unsettles, () => sitesAt(each.node, each.position, writeContext));
|
|
920
|
+
const found = new Map();
|
|
921
|
+
const add = (write, decided) => {
|
|
922
|
+
const key = outcomeKey(write, decided);
|
|
923
|
+
if (each.given.has(key) || found.has(key))
|
|
924
|
+
return;
|
|
925
|
+
const { name, holder, shadows, effect } = decided;
|
|
926
|
+
const target = { name, ...(shadows === undefined ? {} : { shadows }) };
|
|
927
|
+
const judged = judgeChain(each.ancestors, { site: write.site, target, taints: [] }, each.position, functionAround(each.ancestors, write.site));
|
|
928
|
+
// A write another rule drops anyway is that rule's.
|
|
929
|
+
if (judged.dead !== undefined && !ruledOutTests.has(judged.dead))
|
|
930
|
+
return;
|
|
931
|
+
const { start, end } = write.site;
|
|
932
|
+
found.set(key, { start, end, holder, name, shadows, effect, mechanism: write.mechanism });
|
|
933
|
+
};
|
|
934
|
+
for (const write of sites.writes) {
|
|
935
|
+
for (const decided of withWrapEvidence(write, outcomesOf(write, catalog, source.realm), evidence))
|
|
936
|
+
add(write, decided);
|
|
937
|
+
}
|
|
938
|
+
for (const write of sites.wild) {
|
|
939
|
+
for (const { decided } of wildcardOutcomes(write, shapes, evidence, catalog, source.realm, starts))
|
|
940
|
+
add(write, decided);
|
|
941
|
+
}
|
|
942
|
+
return found;
|
|
943
|
+
};
|
|
944
|
+
const dropped = outcomesWithout('', () => true);
|
|
945
|
+
if (dropped.size === 0)
|
|
946
|
+
continue;
|
|
947
|
+
// Each dropped write is put with the test whose decision alone drops it, or, when none does alone, with each.
|
|
948
|
+
const alone = new Set();
|
|
949
|
+
if (each.decisions.length > 1) {
|
|
950
|
+
for (const decision of each.decisions) {
|
|
951
|
+
const key = decisionKey(decision.test, decision.literal);
|
|
952
|
+
const one = outcomesWithout(key, (other) => decisionKey(other.test, other.literal) === key);
|
|
953
|
+
for (const [outcome, record] of one) {
|
|
954
|
+
if (!dropped.has(outcome))
|
|
955
|
+
continue;
|
|
956
|
+
alone.add(outcome);
|
|
957
|
+
dropRuledOut(decision.test, decision.literal, record);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
for (const [outcome, record] of dropped) {
|
|
962
|
+
if (alone.has(outcome))
|
|
963
|
+
continue;
|
|
964
|
+
for (const { test, literal } of each.decisions)
|
|
965
|
+
dropRuledOut(test, literal, record);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
if (!(error instanceof BudgetExhausted))
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
undone();
|
|
975
|
+
}
|
|
976
|
+
argumentPruneSteps(analysis, argumentPrunes);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* A write's outcome, as the frames of one write are compared with and without the argument-literal rule's decisions.
|
|
980
|
+
*/
|
|
981
|
+
function outcomeKey(write, { holder, name, shadows, effect }) {
|
|
982
|
+
return JSON.stringify([write.site.start, write.site.end, holder, name, shadows ?? null, effect, write.mechanism]);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Saves what the analysis has counted and raised, and gives back what undoes whatever is counted or raised after it,
|
|
986
|
+
* with a fresh step budget as large as the group's meanwhile: for work whose results only find prunes.
|
|
987
|
+
*/
|
|
988
|
+
function sideEffects(analysis) {
|
|
989
|
+
const { output } = analysis;
|
|
990
|
+
const { steps, budget } = analysis;
|
|
991
|
+
const caps = { ...output.stats.caps };
|
|
992
|
+
const limitations = output.limitations.length;
|
|
993
|
+
const sets = [analysis.foldsTooLong, analysis.callsCut, analysis.holdersUnresolved, analysis.loopsCut].map((set) => [set, new Set(set)]);
|
|
994
|
+
const maps = [analysis.descriptorsCut, analysis.candidatesCut].map((map) => [map, new Map(map)]);
|
|
995
|
+
const hit = new Map([...analysis.capsHit].map(([name, nodes]) => [name, new Set(nodes)]));
|
|
996
|
+
analysis.budget = steps + budget;
|
|
997
|
+
return () => {
|
|
998
|
+
analysis.steps = steps;
|
|
999
|
+
analysis.budget = budget;
|
|
1000
|
+
for (const name of Object.keys(output.stats.caps))
|
|
1001
|
+
delete output.stats.caps[name];
|
|
1002
|
+
Object.assign(output.stats.caps, caps);
|
|
1003
|
+
output.limitations.length = limitations;
|
|
1004
|
+
for (const [set, saved] of sets) {
|
|
1005
|
+
set.clear();
|
|
1006
|
+
for (const each of saved)
|
|
1007
|
+
set.add(each);
|
|
1008
|
+
}
|
|
1009
|
+
for (const [map, saved] of maps) {
|
|
1010
|
+
map.clear();
|
|
1011
|
+
for (const [key, value] of saved)
|
|
1012
|
+
map.set(key, value);
|
|
1013
|
+
}
|
|
1014
|
+
analysis.capsHit.clear();
|
|
1015
|
+
for (const [name, nodes] of hit)
|
|
1016
|
+
analysis.capsHit.set(name, nodes);
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* A broken module link (§6 Degradation) raises `unlinked-module-call` only when a global, a holder or a target flows into
|
|
1021
|
+
* a call through what it returns: an argument can be the global object, a native holder or a native function, in any
|
|
1022
|
+
* frame of the code the call is in. Any other broken link is only counted, in `moduleLinking.unlinked`.
|
|
1023
|
+
*/
|
|
1024
|
+
function unlinkedModuleCalls(analysis, unlinkedCalls, resolution, calls) {
|
|
1025
|
+
const { source, starts, output } = analysis;
|
|
1026
|
+
const carries = (values) => values.some((value) => value.kind === 'holder' || value.kind === 'native');
|
|
1027
|
+
for (const [call, broken] of [...unlinkedCalls].sort(([a], [b]) => a.start - b.start)) {
|
|
1028
|
+
const info = calls.infoOf(call);
|
|
1029
|
+
const base = { guard: (name) => info?.guards.get(name), topLevel: info?.topLevel ?? false };
|
|
1030
|
+
const args = call['arguments'].map((arg) => (arg.type === 'SpreadElement' ? arg['argument'] : arg));
|
|
1031
|
+
const flows = args.some((arg) => {
|
|
1032
|
+
const { value, functions } = resolution.contextual(() => resolution.evaluate(arg, base));
|
|
1033
|
+
if (carries(value))
|
|
1034
|
+
return true;
|
|
1035
|
+
// In the frames of the innermost function whose bindings the argument reads.
|
|
1036
|
+
const around = info?.contexts.find((context) => functions.has(context));
|
|
1037
|
+
if (around === undefined)
|
|
1038
|
+
return false;
|
|
1039
|
+
return resolution.framesOf(around, true).some((frame) => carries(resolution.evaluate(arg, { ...base, frame })));
|
|
1040
|
+
});
|
|
1041
|
+
if (!flows)
|
|
1042
|
+
continue;
|
|
1043
|
+
const location = locate(starts, call.start, call.end);
|
|
1044
|
+
const link = locate(starts, broken.call.start, broken.call.end);
|
|
1045
|
+
const through = broken.why === 'import'
|
|
1046
|
+
? `the import of \`${String(broken.call['source']['value'])}\` at ${link.line}:${link.column}, whose ` +
|
|
1047
|
+
"module isn't in the link group"
|
|
1048
|
+
: `the module link \`${printed(broken.call)}\` at ${link.line}:${link.column}, whose ` +
|
|
1049
|
+
(broken.why === 'missing' ? 'module id names no factory in its table' : "module id isn't a constant");
|
|
1050
|
+
output.limitations.push({
|
|
1051
|
+
kind: 'unlinked-module-call',
|
|
1052
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1053
|
+
location,
|
|
1054
|
+
detail: `the call \`${printed(call['callee'])}(…)\` at ${location.line}:${location.column} passes a global, ` +
|
|
1055
|
+
`holder or target through ${through}`,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The calls through an import binding whose module isn't in the link group (§6 Degradation): `f(…)`, `ns.f(…)` or
|
|
1061
|
+
* `new F(…)` where `f`, `ns` or `F` is that binding, each with the broken link.
|
|
1062
|
+
*/
|
|
1063
|
+
function importCalls(analysis, program, scope) {
|
|
1064
|
+
const { unlinkedBindings } = analysis.group;
|
|
1065
|
+
const found = new Map();
|
|
1066
|
+
if (unlinkedBindings.size === 0)
|
|
1067
|
+
return found;
|
|
1068
|
+
walkTree(program, {
|
|
1069
|
+
enter(node) {
|
|
1070
|
+
if (node.type !== 'CallExpression' && node.type !== 'NewExpression')
|
|
1071
|
+
return;
|
|
1072
|
+
let base = calleeExpression(node['callee']);
|
|
1073
|
+
while (base.type === 'MemberExpression')
|
|
1074
|
+
base = base['object'];
|
|
1075
|
+
if (base.type !== 'Identifier')
|
|
1076
|
+
return;
|
|
1077
|
+
let declaration;
|
|
1078
|
+
try {
|
|
1079
|
+
declaration = scope.declaration(base);
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const broken = declaration === undefined ? undefined : unlinkedBindings.get(declaration)?.get(scope.nameOf(base));
|
|
1085
|
+
if (broken !== undefined)
|
|
1086
|
+
found.set(node, { call: broken, why: 'import' });
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
return found;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Counts the occurrences statically false catalog checks pruned (§7), and gives each surviving occurrence a check is on
|
|
1093
|
+
* a `prune` step after that check's `guard` step: the count it pruned, and the guard (§14).
|
|
1094
|
+
*/
|
|
1095
|
+
function pruneSteps({ output, starts }, prunedBy, guarded) {
|
|
1096
|
+
if (prunedBy.size === 0)
|
|
1097
|
+
return;
|
|
1098
|
+
let total = 0;
|
|
1099
|
+
for (const pruned of prunedBy.values())
|
|
1100
|
+
total += pruned.size;
|
|
1101
|
+
output.stats.pruned = (output.stats.pruned ?? 0) + total;
|
|
1102
|
+
for (const { occurrence, tests } of guarded) {
|
|
1103
|
+
for (const [test, guard] of tests) {
|
|
1104
|
+
const count = prunedBy.get(test)?.size;
|
|
1105
|
+
if (count === undefined)
|
|
1106
|
+
continue;
|
|
1107
|
+
const generated = locate(starts, test.start, test.end);
|
|
1108
|
+
const expression = printed(test);
|
|
1109
|
+
const prune = {
|
|
1110
|
+
kind: 'prune',
|
|
1111
|
+
count,
|
|
1112
|
+
expression,
|
|
1113
|
+
generated,
|
|
1114
|
+
text: `the guard \`${expression}\` at ${generated.line}:${generated.column} is statically false against the catalog, ` +
|
|
1115
|
+
`so ${count} finding ${count === 1 ? 'occurrence' : 'occurrences'} it alone reaches ${count === 1 ? 'was' : 'were'} pruned`,
|
|
1116
|
+
};
|
|
1117
|
+
occurrence.trace.splice(occurrence.trace.indexOf(guard) + 1, 0, prune);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* The `try` statements whose block a write is in, in the function it is in, outermost first: what runs a `catch` arm
|
|
1123
|
+
* that one of them judged unreachable would have run instead.
|
|
1124
|
+
*/
|
|
1125
|
+
function triesAround(ancestors, site) {
|
|
1126
|
+
const end = ancestors.indexOf(site) === -1 ? ancestors.length : ancestors.indexOf(site) + 1;
|
|
1127
|
+
let from = 0;
|
|
1128
|
+
for (let index = end - 2; index >= 0; index--) {
|
|
1129
|
+
if (isFunction(ancestors[index])) {
|
|
1130
|
+
from = index;
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const tries = [];
|
|
1135
|
+
for (let index = from + 1; index < end; index++) {
|
|
1136
|
+
const parent = ancestors[index - 1];
|
|
1137
|
+
if (parent.type === 'TryStatement' && ancestors[index] === parent['block'])
|
|
1138
|
+
tries.push(parent);
|
|
1139
|
+
}
|
|
1140
|
+
return tries;
|
|
1141
|
+
}
|
|
1142
|
+
/** A `try` statement as a `prune` step shows it: its block's statements, printed, cut at 120 characters. */
|
|
1143
|
+
function printedTry(statement) {
|
|
1144
|
+
const body = statement['block']['body'].map((each) => each.type === 'ExpressionStatement' ? printed(each['expression']) : '…');
|
|
1145
|
+
const text = body.length === 0 ? 'try {}' : `try { ${body.join('; ')} }`;
|
|
1146
|
+
return text.length > 120 ? `${text.slice(0, 119)}…` : text;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Counts the writes `catch` arms whose `try` can't throw dropped (§5 Resolver capabilities 9), where the
|
|
1150
|
+
* statically false catalog checks' prunes are counted (§7), and gives each surviving occurrence in such a `try` block a
|
|
1151
|
+
* `prune` step after its guards: the `try`, the count it dropped, and the judgement that dropped them (§14).
|
|
1152
|
+
*/
|
|
1153
|
+
function armPruneSteps({ output, starts }, { prunedBy, tried }) {
|
|
1154
|
+
if (prunedBy.size === 0)
|
|
1155
|
+
return;
|
|
1156
|
+
let total = 0;
|
|
1157
|
+
for (const pruned of prunedBy.values())
|
|
1158
|
+
total += pruned.size;
|
|
1159
|
+
output.stats.pruned = (output.stats.pruned ?? 0) + total;
|
|
1160
|
+
for (const { occurrence, tries, at } of tried) {
|
|
1161
|
+
const steps = tries.flatMap((statement) => {
|
|
1162
|
+
const count = prunedBy.get(statement)?.size;
|
|
1163
|
+
if (count === undefined)
|
|
1164
|
+
return [];
|
|
1165
|
+
const generated = locate(starts, statement.start, statement.end);
|
|
1166
|
+
return [
|
|
1167
|
+
{
|
|
1168
|
+
kind: 'prune',
|
|
1169
|
+
count,
|
|
1170
|
+
expression: printedTry(statement),
|
|
1171
|
+
generated,
|
|
1172
|
+
judgement: 'try-cannot-throw',
|
|
1173
|
+
text: `the \`try\` block at ${generated.line}:${generated.column} can't throw, so its ` +
|
|
1174
|
+
`\`catch\` arm never runs and the ${count} ${count === 1 ? 'write' : 'writes'} in it ` +
|
|
1175
|
+
`${count === 1 ? 'was' : 'were'} pruned`,
|
|
1176
|
+
},
|
|
1177
|
+
];
|
|
1178
|
+
});
|
|
1179
|
+
occurrence.trace.splice(at, 0, ...steps);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
/** The key of a test a call argument literal settled, with that literal. */
|
|
1183
|
+
const decisionKey = (test, literal) => `${test.start}:${test.end}@${literal === undefined ? '' : `${literal.start}:${literal.end}`}`;
|
|
1184
|
+
/**
|
|
1185
|
+
* A call argument literal as a `prune` step shows it: its properties printed one level deep, a method or accessor as
|
|
1186
|
+
* `…`, and cut at 120 characters.
|
|
1187
|
+
*/
|
|
1188
|
+
function printedLiteral(literal) {
|
|
1189
|
+
const properties = literal['properties'].map((property) => {
|
|
1190
|
+
if (property.type !== 'Property')
|
|
1191
|
+
return printed(property);
|
|
1192
|
+
const key = property['key'];
|
|
1193
|
+
const name = property['computed'] === true ? `[${printed(key)}]` : printed(key);
|
|
1194
|
+
if (property['kind'] !== 'init' || property['method'] === true)
|
|
1195
|
+
return `${name}…`;
|
|
1196
|
+
return property['shorthand'] === true ? name : `${name}: ${printed(property['value'])}`;
|
|
1197
|
+
});
|
|
1198
|
+
const text = properties.length === 0 ? '{}' : `{ ${properties.join(', ')} }`;
|
|
1199
|
+
return text.length > 120 ? `${text.slice(0, 119)}…` : text;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Counts the writes the argument-literal rule dropped (§5 Resolver capabilities 5), where the statically false catalog
|
|
1203
|
+
* checks' prunes are counted (§7), once per write site, target, effect and mechanism. Each surviving occurrence whose
|
|
1204
|
+
* write a test settled that way gets a `prune` step after its guards for each such test that dropped writes: the test,
|
|
1205
|
+
* the count of writes it dropped, and the literal that settled it (§14).
|
|
1206
|
+
*/
|
|
1207
|
+
function argumentPruneSteps({ output, starts }, { prunedBy, traced }) {
|
|
1208
|
+
if (prunedBy.size === 0)
|
|
1209
|
+
return;
|
|
1210
|
+
const all = new Set();
|
|
1211
|
+
for (const { records } of prunedBy.values())
|
|
1212
|
+
for (const record of records)
|
|
1213
|
+
all.add(record);
|
|
1214
|
+
output.stats.pruned = (output.stats.pruned ?? 0) + all.size;
|
|
1215
|
+
const added = new Map();
|
|
1216
|
+
// Steps that go at one place are put there last first, so that they end up in the order they were given.
|
|
1217
|
+
for (const { occurrence, key, after } of [...traced].reverse()) {
|
|
1218
|
+
const pruned = prunedBy.get(key);
|
|
1219
|
+
if (pruned === undefined)
|
|
1220
|
+
continue;
|
|
1221
|
+
// One step per test and literal on each occurrence, however many ways it was decided.
|
|
1222
|
+
let keys = added.get(occurrence);
|
|
1223
|
+
if (keys === undefined)
|
|
1224
|
+
added.set(occurrence, (keys = new Set()));
|
|
1225
|
+
if (keys.has(key))
|
|
1226
|
+
continue;
|
|
1227
|
+
keys.add(key);
|
|
1228
|
+
const { test, literal, records } = pruned;
|
|
1229
|
+
const count = records.size;
|
|
1230
|
+
const generated = locate(starts, test.start, test.end);
|
|
1231
|
+
const expression = printed(test);
|
|
1232
|
+
const settled = literal === undefined ? undefined : { expression: printedLiteral(literal), generated: locate(starts, literal.start, literal.end) };
|
|
1233
|
+
const step = {
|
|
1234
|
+
kind: 'prune',
|
|
1235
|
+
count,
|
|
1236
|
+
expression,
|
|
1237
|
+
generated,
|
|
1238
|
+
judgement: 'argument-literal',
|
|
1239
|
+
...(settled === undefined ? {} : { literal: settled }),
|
|
1240
|
+
text: `the test \`${expression}\` at ${generated.line}:${generated.column} is settled by ` +
|
|
1241
|
+
(settled === undefined
|
|
1242
|
+
? 'a call argument literal'
|
|
1243
|
+
: `the call argument literal \`${settled.expression}\` at ${settled.generated.line}:${settled.generated.column}`) +
|
|
1244
|
+
`, so the arm it rules out never runs and the ${count} ${count === 1 ? 'write' : 'writes'} only that arm reaches ` +
|
|
1245
|
+
`${count === 1 ? 'was' : 'were'} pruned`,
|
|
1246
|
+
};
|
|
1247
|
+
// A trace index is where the step goes, even the start; a step it goes after must still be on the trace.
|
|
1248
|
+
const at = typeof after === 'number' ? after : occurrence.trace.indexOf(after) + 1;
|
|
1249
|
+
if (typeof after === 'number' || at > 0)
|
|
1250
|
+
occurrence.trace.splice(at, 0, step);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/** The mechanisms that write a slot again after a delete (§3): a define or an assign. */
|
|
1254
|
+
const rewriting = new Set(['assign', 'bulk-assign', 'define-property', 'define-properties', 'reflect-define']);
|
|
1255
|
+
/** The effects of an install: a write that sets a value (§3 Restores). */
|
|
1256
|
+
const installEffects = new Set(['replace', 'wrap', 'descriptor-change']);
|
|
1257
|
+
/**
|
|
1258
|
+
* Puts each restore with the install occurrences in its chain (§3 Restores): an install is an occurrence of the same
|
|
1259
|
+
* target, other than a restore's, whose write is in one chain with a native read the restore's value came from. A
|
|
1260
|
+
* proven restore leaves a `restore` step on each install's trace, or nothing when it has none. An unproven restore with
|
|
1261
|
+
* an install joins the first install's finding, in document order, by taking its effect and its attribution
|
|
1262
|
+
* lookup.
|
|
1263
|
+
*/
|
|
1264
|
+
function installRestores(placed, proven, unproven, sameChain) {
|
|
1265
|
+
if (proven.length === 0 && unproven.length === 0)
|
|
1266
|
+
return;
|
|
1267
|
+
const restores = new Set(unproven.map(({ occurrence }) => occurrence));
|
|
1268
|
+
const installsOf = (target, restore) => {
|
|
1269
|
+
const found = new Set();
|
|
1270
|
+
for (const { occurrence, write } of placed) {
|
|
1271
|
+
if (restores.has(occurrence) || found.has(occurrence))
|
|
1272
|
+
continue;
|
|
1273
|
+
if (occurrence.target.realm !== target.realm || occurrence.target.name !== target.name)
|
|
1274
|
+
continue;
|
|
1275
|
+
// Only a write that sets a value is an install; a `delete` never is (§3).
|
|
1276
|
+
if (!installEffects.has(occurrence.effect))
|
|
1277
|
+
continue;
|
|
1278
|
+
if (restore.reads.some((read) => sameChain(read, write.site)))
|
|
1279
|
+
found.add(occurrence);
|
|
1280
|
+
}
|
|
1281
|
+
return [...found].sort((a, b) => a.generated.start - b.generated.start);
|
|
1282
|
+
};
|
|
1283
|
+
for (const { restore, target, generated } of proven) {
|
|
1284
|
+
const step = {
|
|
1285
|
+
kind: 'restore',
|
|
1286
|
+
generated,
|
|
1287
|
+
guard: restore.guard,
|
|
1288
|
+
leavesOwnShadow: restore.leavesOwnShadow,
|
|
1289
|
+
text: `the restore at ${generated.line}:${generated.column} writes the native back` +
|
|
1290
|
+
(restore.guard === 'identity' ? ' only while the slot still holds this code’s value' : '') +
|
|
1291
|
+
(restore.leavesOwnShadow ? ', and leaves the own property the install created' : ''),
|
|
1292
|
+
};
|
|
1293
|
+
for (const install of installsOf(target, restore)) {
|
|
1294
|
+
const has = install.trace.some((each) => each.kind === 'restore' && each.generated.start === generated.start);
|
|
1295
|
+
if (!has)
|
|
1296
|
+
install.trace.push(step);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
for (const { restore, occurrence } of unproven) {
|
|
1300
|
+
const [install] = installsOf(occurrence.target, restore);
|
|
1301
|
+
if (install === undefined)
|
|
1302
|
+
continue;
|
|
1303
|
+
// It takes the install's attribution lookup as well as its effect, so it never adds a fingerprint (§3).
|
|
1304
|
+
// A lookup point in the install's own artifact is named when that isn't the restore's.
|
|
1305
|
+
occurrence.effect = install.effect;
|
|
1306
|
+
const artifact = install.lookup.artifact ?? install.artifact;
|
|
1307
|
+
const point = { generated: install.lookup.generated, token: install.lookup.token };
|
|
1308
|
+
occurrence.lookup = artifact === occurrence.artifact ? point : { ...point, artifact };
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* A delete that the same function always follows with a define or assign of the same slot gives no finding of its own
|
|
1313
|
+
* (§3): its occurrences are dropped when every instantiation of it is rewritten, and each write that rewrites it gets
|
|
1314
|
+
* a `note` naming the delete. The same slot is the same holder expression (the same root through the same constant
|
|
1315
|
+
* chain, with no redefinition of the root between), the same target and the same instantiation of the function: the
|
|
1316
|
+
* write is in its own code, or in a call made there.
|
|
1317
|
+
*/
|
|
1318
|
+
function dropRewrittenDeletes(analysis, placed, { program, calls, slots }) {
|
|
1319
|
+
const deletes = placed.filter(({ write }) => write.mechanism === 'delete');
|
|
1320
|
+
if (deletes.length === 0)
|
|
1321
|
+
return;
|
|
1322
|
+
const parents = new Map();
|
|
1323
|
+
walkTree(program, {
|
|
1324
|
+
enter(node, parent) {
|
|
1325
|
+
if (parent !== null)
|
|
1326
|
+
parents.set(node, parent);
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
const parentOf = (node) => parents.get(node);
|
|
1330
|
+
const sameTarget = (a, b) => a.target.realm === b.target.realm && a.target.name === b.target.name && a.realmInstance === b.realmInstance;
|
|
1331
|
+
/**
|
|
1332
|
+
* The expression a write site gives its holder with, and the node that runs the write: `h` in `h[k] = v`,
|
|
1333
|
+
* `delete h[k]` or `define(h, k, d)`, and for a bulk mechanism's property, `h` in `Object.assign(h, { k: v })`.
|
|
1334
|
+
*/
|
|
1335
|
+
const holderOf = (site) => {
|
|
1336
|
+
if (site.type === 'AssignmentExpression' || site.type === 'UnaryExpression') {
|
|
1337
|
+
const member = (site.type === 'AssignmentExpression' ? site['left'] : site['argument']);
|
|
1338
|
+
return member.type === 'MemberExpression' ? { holder: member['object'], runs: site } : undefined;
|
|
1339
|
+
}
|
|
1340
|
+
if (site.type === 'Property') {
|
|
1341
|
+
const call = parentOf(parentOf(site) ?? site);
|
|
1342
|
+
const holder = call?.type === 'CallExpression' ? call['arguments'][0] : undefined;
|
|
1343
|
+
return holder === undefined ? undefined : { holder, runs: call };
|
|
1344
|
+
}
|
|
1345
|
+
const holder = site.type === 'CallExpression' ? site['arguments'][0] : undefined;
|
|
1346
|
+
return holder === undefined ? undefined : { holder, runs: site };
|
|
1347
|
+
};
|
|
1348
|
+
/** The calls, out from `frame`, that instantiated code through, each with the frame it was made in. */
|
|
1349
|
+
const callsOut = (frame, depth = 0) => {
|
|
1350
|
+
if (frame === undefined || depth > 8)
|
|
1351
|
+
return [];
|
|
1352
|
+
const out = frame.bindings.flatMap((binding) => [
|
|
1353
|
+
{ call: binding.site.call, within: binding.site.within, caller: binding.caller },
|
|
1354
|
+
...callsOut(binding.caller, depth + 1),
|
|
1355
|
+
]);
|
|
1356
|
+
return [...out, ...callsOut(frame.outer, depth + 1)];
|
|
1357
|
+
};
|
|
1358
|
+
// Judged by the delete's site: every instantiation of it, whichever occurrence each gave, must be rewritten. The notes
|
|
1359
|
+
// each rewritten instantiation leaves on its writes are added only then, so no write says a delete gives no finding
|
|
1360
|
+
// while another instantiation of it keeps one.
|
|
1361
|
+
//
|
|
1362
|
+
// A node of the delete's function that rewrites the slot in one instantiation rewrites it in each: the slot is the
|
|
1363
|
+
// same syntactic (holder, key), and the node the same code. So the nodes are gathered over every instantiation of the
|
|
1364
|
+
// site first, and each is judged with all of them. An instantiation whose rewrite gives no occurrence of its own (a
|
|
1365
|
+
// write-back of what the slot holds, which changes nothing, or a call the context cap merged) is still rewritten.
|
|
1366
|
+
const rewritten = new Set();
|
|
1367
|
+
const kept = new Set();
|
|
1368
|
+
const notes = [];
|
|
1369
|
+
const judged = [];
|
|
1370
|
+
const rewritesOf = new Map();
|
|
1371
|
+
for (const removal of deletes) {
|
|
1372
|
+
const site = removal.write.site;
|
|
1373
|
+
const fn = calls.functionOf(site);
|
|
1374
|
+
const code = fn ?? program;
|
|
1375
|
+
const holder = holderOf(site)?.holder;
|
|
1376
|
+
const roots = [];
|
|
1377
|
+
const signature = holder === undefined ? undefined : slots.signature(holder, roots);
|
|
1378
|
+
/** The writes that rewrite the slot, by the node in the delete's function that runs each. */
|
|
1379
|
+
const byNode = new Map();
|
|
1380
|
+
if (signature !== undefined) {
|
|
1381
|
+
for (const { occurrence, write, frame } of placed) {
|
|
1382
|
+
if (!rewriting.has(write.mechanism) || !sameTarget(occurrence, removal.occurrence))
|
|
1383
|
+
continue;
|
|
1384
|
+
const nodes = [];
|
|
1385
|
+
// A write in the delete's own code, in the same instantiation.
|
|
1386
|
+
if (calls.functionOf(write.site) === fn && frame?.id === removal.frame?.id) {
|
|
1387
|
+
const written = holderOf(write.site);
|
|
1388
|
+
if (written !== undefined && slots.signature(written.holder, []) === signature)
|
|
1389
|
+
nodes.push(written.runs);
|
|
1390
|
+
}
|
|
1391
|
+
// A write in a call made there, in the same instantiation, that passes the same holder.
|
|
1392
|
+
for (const { call, within, caller } of callsOut(frame)) {
|
|
1393
|
+
if (within !== fn || caller?.id !== removal.frame?.id)
|
|
1394
|
+
continue;
|
|
1395
|
+
const args = call['arguments'] ?? [];
|
|
1396
|
+
if (args.some((arg) => slots.signature(arg, []) === signature))
|
|
1397
|
+
nodes.push(call);
|
|
1398
|
+
}
|
|
1399
|
+
for (const node of nodes) {
|
|
1400
|
+
if (node.start < site.end || slots.redefined(roots, site.end, node.start))
|
|
1401
|
+
continue;
|
|
1402
|
+
const list = byNode.get(node);
|
|
1403
|
+
if (list === undefined)
|
|
1404
|
+
byNode.set(node, [occurrence]);
|
|
1405
|
+
else if (!list.includes(occurrence))
|
|
1406
|
+
list.push(occurrence);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
judged.push({ removal, code, byNode });
|
|
1411
|
+
const rewrites = rewritesOf.get(site) ?? new Set();
|
|
1412
|
+
for (const node of byNode.keys())
|
|
1413
|
+
rewrites.add(node);
|
|
1414
|
+
rewritesOf.set(site, rewrites);
|
|
1415
|
+
}
|
|
1416
|
+
for (const { removal, code, byNode } of judged) {
|
|
1417
|
+
const site = removal.write.site;
|
|
1418
|
+
// Another instantiation's rewrite counts only when it is the same code with the same callee: a write in the delete's
|
|
1419
|
+
// own code, or a call whose callee doesn't read a binding of the delete's function, which each call may bind to a
|
|
1420
|
+
// different function (`function d(O, k, r){ delete O[k]; r(O, k) }`).
|
|
1421
|
+
const shared = [...rewritesOf.get(site)].filter((node) => byNode.has(node) || !variesByCall(node, code));
|
|
1422
|
+
if (shared.length === 0 || !alwaysRewritten(site, code, (node) => shared.includes(node), parentOf)) {
|
|
1423
|
+
kept.add(site);
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
rewritten.add(site);
|
|
1427
|
+
const at = locate(analysis.starts, site.start, site.end);
|
|
1428
|
+
const note = {
|
|
1429
|
+
kind: 'note',
|
|
1430
|
+
about: 'rewritten-delete',
|
|
1431
|
+
delete: at,
|
|
1432
|
+
text: `the delete of the same slot at ${at.line}:${at.column}, which this write always follows, gives no finding of its own`,
|
|
1433
|
+
};
|
|
1434
|
+
notes.push({ site, note, on: new Set([...byNode.values()].flat()) });
|
|
1435
|
+
}
|
|
1436
|
+
for (const { site, note, on } of notes) {
|
|
1437
|
+
if (kept.has(site))
|
|
1438
|
+
continue;
|
|
1439
|
+
for (const occurrence of on) {
|
|
1440
|
+
const has = occurrence.trace.some((step) => step.kind === 'note' && step.about === 'rewritten-delete' && step.delete.start === note.delete.start);
|
|
1441
|
+
if (!has)
|
|
1442
|
+
occurrence.trace.push(note);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const output = analysis.output;
|
|
1446
|
+
const dropped = new Set(deletes.filter(({ write }) => rewritten.has(write.site) && !kept.has(write.site)).map(({ occurrence }) => occurrence));
|
|
1447
|
+
output.occurrences = output.occurrences.filter((occurrence) => !dropped.has(occurrence));
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Whether a call node's callee may be a different function in each instantiation of `fn`: its root is `this`, or a name
|
|
1451
|
+
* that `fn` binds itself, as a parameter, `arguments` or a declaration in its own code. A node that is no call never is.
|
|
1452
|
+
*/
|
|
1453
|
+
function variesByCall(node, fn) {
|
|
1454
|
+
if (node.type !== 'CallExpression' && node.type !== 'NewExpression')
|
|
1455
|
+
return false;
|
|
1456
|
+
let root = calleeExpression(node['callee']);
|
|
1457
|
+
while (root.type === 'MemberExpression')
|
|
1458
|
+
root = root['object'];
|
|
1459
|
+
if (root.type === 'ThisExpression')
|
|
1460
|
+
return true;
|
|
1461
|
+
if (root.type !== 'Identifier')
|
|
1462
|
+
return true;
|
|
1463
|
+
const name = root['name'];
|
|
1464
|
+
if (fn.type === 'Program')
|
|
1465
|
+
return false;
|
|
1466
|
+
if (bindsOwn(fn, name))
|
|
1467
|
+
return true;
|
|
1468
|
+
let declared = false;
|
|
1469
|
+
walkTree(fn['body'], {
|
|
1470
|
+
enter(child) {
|
|
1471
|
+
if (declared || isFunction(child)) {
|
|
1472
|
+
if (child.type === 'FunctionDeclaration' && child['id']?.['name'] === name)
|
|
1473
|
+
declared = true;
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
if (child.type === 'VariableDeclarator' && boundNames(child['id']).includes(name))
|
|
1477
|
+
declared = true;
|
|
1478
|
+
if (child.type === 'ClassDeclaration' && child['id']?.['name'] === name)
|
|
1479
|
+
declared = true;
|
|
1480
|
+
return undefined;
|
|
1481
|
+
},
|
|
1482
|
+
});
|
|
1483
|
+
return declared;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* A write's outcomes with wrap evidence weighed (§5 Resolver capabilities 10, §8): a `replace` whose new value is shown
|
|
1487
|
+
* to call the original is a `wrap`. When the value's alternatives differ in wrap evidence, the write gives one outcome
|
|
1488
|
+
* per effect, each as decided as the `replace` was.
|
|
1489
|
+
*/
|
|
1490
|
+
function withWrapEvidence(write, outcomes, evidence) {
|
|
1491
|
+
const sources = valueSources(write);
|
|
1492
|
+
if (sources.length === 0)
|
|
1493
|
+
return [...outcomes];
|
|
1494
|
+
return outcomes.flatMap((outcome) => {
|
|
1495
|
+
if (outcome.effect !== 'replace')
|
|
1496
|
+
return [outcome];
|
|
1497
|
+
const replaced = { holder: write.holder, original: outcome.shadows ?? outcome.name, leaves: write.leaves };
|
|
1498
|
+
const answers = evidence(sources, replaced);
|
|
1499
|
+
return [...(answers.has(true) ? [{ ...outcome, effect: 'wrap' }] : []), ...(answers.has(false) ? [outcome] : [])];
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* The outcomes of a wildcard write (§5 Partially resolved writes, ADR 0004): none unless the same key is reused. The
|
|
1504
|
+
* effect follows the value written, as for a named member: a delete is a `delete`, a define that gives `get` or
|
|
1505
|
+
* `set`, or no `value`, a `descriptor-change`, a value that equals the read when present an `add`, a value with wrap
|
|
1506
|
+
* evidence a `wrap`, and any other a `replace`, one outcome per effect when the value's alternatives differ. The
|
|
1507
|
+
* outcome is judged over the members the wildcard matches (§3): a finding, with `wildcard-outcome-mixed` when some are
|
|
1508
|
+
* ineffective (§9), or ineffective writes under the wildcard's name.
|
|
1509
|
+
*/
|
|
1510
|
+
function wildcardOutcomes(write, shapes, evidence, catalog, realm, starts) {
|
|
1511
|
+
if (write.pattern !== undefined)
|
|
1512
|
+
return patternOutcomes(write, catalog, realm);
|
|
1513
|
+
const reuse = shapes.reuse(write);
|
|
1514
|
+
if (reuse === undefined || reuse.writesBack)
|
|
1515
|
+
return [];
|
|
1516
|
+
const name = `${write.holder}.*`;
|
|
1517
|
+
const { change } = write;
|
|
1518
|
+
let effects;
|
|
1519
|
+
if (change.kind === 'delete')
|
|
1520
|
+
effects = ['delete'];
|
|
1521
|
+
else if (change.kind === 'define' && (change.descriptor.get || change.descriptor.set || change.descriptor.value === undefined)) {
|
|
1522
|
+
effects = ['descriptor-change'];
|
|
1523
|
+
}
|
|
1524
|
+
else if (reuse.keptWhenPresent)
|
|
1525
|
+
effects = ['add'];
|
|
1526
|
+
else {
|
|
1527
|
+
const sources = valueSources(write);
|
|
1528
|
+
const answers = sources.length === 0 ? new Set([false]) : evidence(sources, { holder: write.holder, original: name, leaves: write.leaves, isOriginal: reuse.isOriginal });
|
|
1529
|
+
effects = [...(answers.has(true) ? ['wrap'] : []), ...(answers.has(false) ? ['replace'] : [])];
|
|
1530
|
+
}
|
|
1531
|
+
return effects.flatMap((effect) => {
|
|
1532
|
+
const outcome = wildcardOutcome(write, effect, catalog, realm, write.members, write.prefixes);
|
|
1533
|
+
if (outcome === undefined)
|
|
1534
|
+
return [];
|
|
1535
|
+
if (outcome.kind === 'ineffective') {
|
|
1536
|
+
return outcome.refused.map((refused) => ({
|
|
1537
|
+
decided: { ...refused, name, holder: outcome.holder, effect },
|
|
1538
|
+
extra: [],
|
|
1539
|
+
}));
|
|
1540
|
+
}
|
|
1541
|
+
const { locks, mixed, undecidable } = outcome;
|
|
1542
|
+
const decided = { name, holder: outcome.holder, effect, locks, ...(undecidable === undefined ? {} : { undecidable }) };
|
|
1543
|
+
return [{ decided, extra: mixed ? [mixedStep(name, write, starts)] : [] }];
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* The outcome of a pattern wildcard (§5 Pattern wildcards): a non-native `add` of `<holder>.<prefix>*`, whether or not
|
|
1548
|
+
* the key is reused, on prototype, constructor and namespace holders only. A delete of a member no catalog member's key
|
|
1549
|
+
* starts with changes no native, as a named one doesn't (§3).
|
|
1550
|
+
*/
|
|
1551
|
+
function patternOutcomes(write, catalog, realm) {
|
|
1552
|
+
if (write.change.kind === 'delete' || write.change.kind === 'prototype')
|
|
1553
|
+
return [];
|
|
1554
|
+
const outcome = wildcardOutcome(write, 'add', catalog, realm);
|
|
1555
|
+
if (outcome?.kind !== 'finding')
|
|
1556
|
+
return [];
|
|
1557
|
+
const name = patternName(write.holder, write.pattern);
|
|
1558
|
+
return [{ decided: { name, holder: outcome.holder, effect: 'add', locks: outcome.locks }, extra: [] }];
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* The `confidence-reason` step (§14) for `wildcard-outcome-mixed` (§9): some members the wildcard matches would be
|
|
1562
|
+
* changed and others not.
|
|
1563
|
+
*/
|
|
1564
|
+
function mixedStep(name, write, starts) {
|
|
1565
|
+
return {
|
|
1566
|
+
kind: 'confidence-reason',
|
|
1567
|
+
dimension: 'mutation',
|
|
1568
|
+
reason: 'wildcard-outcome-mixed',
|
|
1569
|
+
level: mutationReasons['wildcard-outcome-mixed'],
|
|
1570
|
+
sites: [locate(starts, write.site.start, write.site.end)],
|
|
1571
|
+
text: `the members \`${name}\` matches are partly ones the write changes and partly ones it can't change`,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
/** The expressions a write's new value comes from: an assignment's value, or every `value` its descriptors give. */
|
|
1575
|
+
function valueSources(write) {
|
|
1576
|
+
const { change } = write;
|
|
1577
|
+
if (change.kind === 'set')
|
|
1578
|
+
return change.sources ?? [];
|
|
1579
|
+
if (change.kind !== 'define')
|
|
1580
|
+
return [];
|
|
1581
|
+
const descriptors = change.alternatives ?? [change.descriptor];
|
|
1582
|
+
return descriptors.flatMap((descriptor) => (descriptor === 'refused' ? [] : (descriptor.value?.sources ?? [])));
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* A call that compiles code from strings (§2): a literal code string is analyzed as nested code located in the host,
|
|
1586
|
+
* and a code string that may not be a literal, or literal code that doesn't parse, is a `dynamic-code` limitation
|
|
1587
|
+
* (§13). A direct eval's literal code was grafted in place of its literal, and the walk analyzes it where it stands. A
|
|
1588
|
+
* call that reaches such a native by any other route is a `dynamic-code` limitation when it passes code, and its code is
|
|
1589
|
+
* not analyzed.
|
|
1590
|
+
*/
|
|
1591
|
+
function codeSite(node, context, at, around) {
|
|
1592
|
+
// A conditional or logical callee, even one whose test leaves a single branch (`(true ? eval : f)(s)`), is each of its
|
|
1593
|
+
// alternatives.
|
|
1594
|
+
const branches = context.routes.alternatives(node['callee'], at);
|
|
1595
|
+
if (branches.length !== 1 || branches[0] !== node['callee']) {
|
|
1596
|
+
alternativeSites(node, branches, context, at, around);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const callee = nativeCallee(node, context, at);
|
|
1600
|
+
if (callee === undefined)
|
|
1601
|
+
indirectSite(node, context, at);
|
|
1602
|
+
else
|
|
1603
|
+
nativeSite(node, callee, context, at, around);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* A call whose callee has alternatives (`(c ? eval : f)(s)`), each counted (Principle 3): an alternative that
|
|
1607
|
+
* is a direct form's native raises what that native's single-value form raises, analyzed once for each native, and one
|
|
1608
|
+
* that reaches a native by another route raises `dynamic-code` for each native that no other alternative reached.
|
|
1609
|
+
*/
|
|
1610
|
+
function alternativeSites(node, branches, context, at, around) {
|
|
1611
|
+
const reached = new Set();
|
|
1612
|
+
const indirect = [];
|
|
1613
|
+
for (const branch of branches) {
|
|
1614
|
+
const call = { ...node, callee: branch };
|
|
1615
|
+
const callee = nativeCallee(call, context, at);
|
|
1616
|
+
if (callee === undefined)
|
|
1617
|
+
indirect.push(...context.routes.reached(call, at));
|
|
1618
|
+
else if (!reached.has(callee.name)) {
|
|
1619
|
+
reached.add(callee.name);
|
|
1620
|
+
// No alternative of several is a direct eval, whose callee is the plain name.
|
|
1621
|
+
nativeSite(node, { ...callee, direct: false }, context, at, around);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
for (const call of indirect) {
|
|
1625
|
+
if (reached.has(call.compiler))
|
|
1626
|
+
continue;
|
|
1627
|
+
reached.add(call.compiler);
|
|
1628
|
+
const code = indirectCodeAt(call, node, context.resolution, at);
|
|
1629
|
+
if (code !== undefined)
|
|
1630
|
+
indirectCode(context.analysis, call, node, code);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/** A call whose callee is a native that compiles code from strings, reached as a direct form. */
|
|
1634
|
+
function nativeSite(node, callee, context, at, around) {
|
|
1635
|
+
const { analysis, resolution, grafts } = context;
|
|
1636
|
+
const args = node['arguments'];
|
|
1637
|
+
const [first] = args;
|
|
1638
|
+
const code = (literal) => analysis.nested.code(literal);
|
|
1639
|
+
const nestedIn = (scope, literals) => joined(around, nestingOf(callee.name, scope, node, callee.leaves, analysis, literals, callee.folds));
|
|
1640
|
+
// Global code runs in the window of the native that compiles it.
|
|
1641
|
+
const compiledIn = instanceOf(callee.leaves);
|
|
1642
|
+
switch (callee.name) {
|
|
1643
|
+
case 'eval': {
|
|
1644
|
+
// `eval` returns anything but a string unchanged, so only a string can be code.
|
|
1645
|
+
if (first === undefined || grafts.has(first))
|
|
1646
|
+
return;
|
|
1647
|
+
if (isCodeLiteral(first)) {
|
|
1648
|
+
// A direct eval's literal that parses was grafted, so this one doesn't parse.
|
|
1649
|
+
const program = callee.direct ? undefined : analysis.nested.program(code(first));
|
|
1650
|
+
if (program === undefined)
|
|
1651
|
+
dynamicCode(analysis, callee.name, node, first, 'unparsed');
|
|
1652
|
+
else
|
|
1653
|
+
nestedGlobalCode(analysis, program, true, nestedIn('global', [first]), compiledIn, context.deferralAt(node, false));
|
|
1654
|
+
}
|
|
1655
|
+
else if (first.type === 'SpreadElement' || resolution.evaluate(first, at).some(maybeString)) {
|
|
1656
|
+
dynamicCode(analysis, callee.name, node, first, 'not-literal');
|
|
1657
|
+
}
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
case 'setTimeout':
|
|
1661
|
+
case 'setInterval': {
|
|
1662
|
+
// A handler that is not callable is converted to a string and compiled as a classic script (HTML timer
|
|
1663
|
+
// initialization steps). Only a handler that is plainly a string is taken for one: most are functions.
|
|
1664
|
+
if (first === undefined || first.type === 'SpreadElement')
|
|
1665
|
+
return;
|
|
1666
|
+
if (isCodeLiteral(first)) {
|
|
1667
|
+
const program = analysis.nested.program(code(first));
|
|
1668
|
+
if (program === undefined)
|
|
1669
|
+
dynamicCode(analysis, callee.name, node, first, 'unparsed');
|
|
1670
|
+
// A timer runs its code only later (§7 Install context).
|
|
1671
|
+
else
|
|
1672
|
+
nestedGlobalCode(analysis, program, false, nestedIn('global', [first]), compiledIn, context.deferralAt(node, true));
|
|
1673
|
+
}
|
|
1674
|
+
else if (stringValued(first) || resolution.evaluate(first, at).some(isString)) {
|
|
1675
|
+
dynamicCode(analysis, callee.name, node, first, 'not-literal');
|
|
1676
|
+
}
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
case 'Function': {
|
|
1680
|
+
// Every argument is converted to a string: the parameters, then the body.
|
|
1681
|
+
const dynamic = args.find((arg) => !isCodeLiteral(arg) && !isPrimitiveLiteral(arg) && !isBigIntLiteral(arg));
|
|
1682
|
+
if (dynamic !== undefined) {
|
|
1683
|
+
dynamicCode(analysis, callee.name, node, dynamic, 'not-literal');
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
// A number, bigint, boolean or null among them is literal code too, converted with ToString (Principle 2).
|
|
1687
|
+
if (args.length === 0)
|
|
1688
|
+
return;
|
|
1689
|
+
const parts = args.map((arg) => (isCodeLiteral(arg) ? code(arg) : analysis.nested.converted(arg)));
|
|
1690
|
+
const program = analysis.nested.functionProgram(parts);
|
|
1691
|
+
if (program === undefined)
|
|
1692
|
+
dynamicCode(analysis, callee.name, node, spanned(args), 'unparsed');
|
|
1693
|
+
else
|
|
1694
|
+
nestedGlobalCode(analysis, program, false, nestedIn('global', args), compiledIn, context.deferralAt(node, false));
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* A call that inserts a script element the code created with no `src`: the literal text it carries is global
|
|
1700
|
+
* nested code, compiled as a classic script where the call inserts it, and located in the host. Text that may not be a
|
|
1701
|
+
* literal, or literal text that doesn't parse, is a `dynamic-code` limitation (§13).
|
|
1702
|
+
*/
|
|
1703
|
+
function scriptTextSite(node, texts, context, around) {
|
|
1704
|
+
const { analysis } = context;
|
|
1705
|
+
const site = locate(analysis.starts, node.start, node.end);
|
|
1706
|
+
for (const text of texts) {
|
|
1707
|
+
if (!isCodeLiteral(text)) {
|
|
1708
|
+
dynamicCodeRecord(analysis, text, `the script element inserted at ${site.line}:${site.column} carries text that is not a string literal`);
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const program = analysis.nested.program(analysis.nested.code(text));
|
|
1712
|
+
if (program === undefined) {
|
|
1713
|
+
dynamicCodeRecord(analysis, text, `the script element inserted at ${site.line}:${site.column} carries literal code that does not parse`);
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const nesting = joined(around, nestingOf('script', 'global', node, [], analysis, [text]));
|
|
1717
|
+
nestedGlobalCode(analysis, program, false, nesting, context.instance, context.deferralAt(node, false));
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Analyzes nested global code: its own program, whose free names are the properties of the global object of the window
|
|
1722
|
+
* `instance` (§5).
|
|
1723
|
+
*/
|
|
1724
|
+
function nestedGlobalCode(analysis, program, evalCode, around, instance, deferral) {
|
|
1725
|
+
const grafts = new Map();
|
|
1726
|
+
analysis.nested.graftDirectEvals(program, grafts);
|
|
1727
|
+
analyzeCode(analysis, {
|
|
1728
|
+
program,
|
|
1729
|
+
sourceType: 'script',
|
|
1730
|
+
grafts,
|
|
1731
|
+
globalCode: true,
|
|
1732
|
+
evalCode,
|
|
1733
|
+
around,
|
|
1734
|
+
instance,
|
|
1735
|
+
deferral,
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* A `dynamic-code` limitation (§13), located at `code`: a native that compiles code, given a code string that may not be
|
|
1740
|
+
* a literal, or literal code that doesn't parse (§2).
|
|
1741
|
+
*/
|
|
1742
|
+
function dynamicCode(analysis, callee, call, code, why) {
|
|
1743
|
+
const site = locate(analysis.starts, call.start, call.end);
|
|
1744
|
+
const compiles = callee === 'eval' ? 'runs' : 'compiles';
|
|
1745
|
+
const what = why === 'not-literal' ? 'code that is not a string literal' : 'literal code that does not parse';
|
|
1746
|
+
dynamicCodeRecord(analysis, code, `\`${callee}\` at ${site.line}:${site.column} ${compiles} ${what}`);
|
|
1747
|
+
}
|
|
1748
|
+
/** A `dynamic-code` limitation record (§13) in the artifact being analyzed, located at `code`. */
|
|
1749
|
+
function dynamicCodeRecord(analysis, code, detail) {
|
|
1750
|
+
const { source, starts } = analysis;
|
|
1751
|
+
analysis.output.limitations.push({
|
|
1752
|
+
kind: 'dynamic-code',
|
|
1753
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1754
|
+
location: locate(starts, code.start, code.end),
|
|
1755
|
+
detail,
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* A call, `new` expression or tagged template that reaches a native that compiles code by a route other than a direct
|
|
1760
|
+
* form (§2): a `dynamic-code` limitation (§13), whose code is not analyzed.
|
|
1761
|
+
*/
|
|
1762
|
+
function indirectSite(node, { analysis, resolution, routes }, at) {
|
|
1763
|
+
for (const reached of routes.reached(node, at)) {
|
|
1764
|
+
const code = indirectCodeAt(reached, node, resolution, at);
|
|
1765
|
+
if (code !== undefined)
|
|
1766
|
+
indirectCode(analysis, reached, node, code);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Where an indirect call is dynamic code (§2), or undefined when it isn't. Every call that reaches `eval` or a function
|
|
1771
|
+
* constructor is, located at the code it passes: its first code argument, or the arguments the analyzer can't list, or
|
|
1772
|
+
* the call when it passes none. A timer is only when it is a string timer, whose handler is plainly a string (a handler
|
|
1773
|
+
* that isn't callable, such as a tagged template's strings array, is converted to one).
|
|
1774
|
+
*/
|
|
1775
|
+
function indirectCodeAt({ compiler, args }, call, resolution, at) {
|
|
1776
|
+
const [first] = args.items;
|
|
1777
|
+
switch (compiler) {
|
|
1778
|
+
case 'setTimeout':
|
|
1779
|
+
case 'setInterval':
|
|
1780
|
+
if (first === undefined || first === null)
|
|
1781
|
+
return undefined;
|
|
1782
|
+
if (first.type === templateStrings)
|
|
1783
|
+
return first;
|
|
1784
|
+
return stringValued(first) || resolution.evaluate(first, at).some(isString) ? first : undefined;
|
|
1785
|
+
case 'eval':
|
|
1786
|
+
return first ?? args.rest ?? call;
|
|
1787
|
+
default:
|
|
1788
|
+
return args.items.find((arg) => arg !== null && !isPrimitiveLiteral(arg)) ?? args.rest ?? first ?? call;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/** A `dynamic-code` limitation (§13) for an indirect call that passes code, located at `code`, whose code isn't read. */
|
|
1792
|
+
function indirectCode(analysis, { compiler, via }, call, code) {
|
|
1793
|
+
const site = locate(analysis.starts, call.start, call.end);
|
|
1794
|
+
const route = via.map((step) => (step.startsWith('a ') ? step : `\`${step}\``)).join(', ');
|
|
1795
|
+
const compiles = compiler === 'eval' ? 'runs' : 'compiles';
|
|
1796
|
+
dynamicCodeRecord(analysis, code, `the call at ${site.line}:${site.column} reaches the native \`${compiler}\` indirectly` +
|
|
1797
|
+
`${route === '' ? '' : ` through ${route}`}, so the code it ${compiles} is not analyzed`);
|
|
1798
|
+
}
|
|
1799
|
+
/** The range from the start of the first node to the end of the last. */
|
|
1800
|
+
const spanned = (nodes) => ({ start: nodes[0].start, end: nodes.at(-1).end });
|
|
1801
|
+
/**
|
|
1802
|
+
* An `analysis-budget` limitation (§13, §18) for a fold that passed the `fold-length` cap, so its value is unknown, in the
|
|
1803
|
+
* key, holder or descriptor of a write that reaches a native holder: one for each fold, however often it is evaluated.
|
|
1804
|
+
*/
|
|
1805
|
+
function foldTooLong(analysis, node) {
|
|
1806
|
+
const { source, starts, foldsTooLong } = analysis;
|
|
1807
|
+
if (foldsTooLong.has(node))
|
|
1808
|
+
return;
|
|
1809
|
+
foldsTooLong.add(node);
|
|
1810
|
+
const location = locate(starts, node.start, node.end);
|
|
1811
|
+
analysis.output.limitations.push({
|
|
1812
|
+
kind: 'analysis-budget',
|
|
1813
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1814
|
+
location,
|
|
1815
|
+
detail: `\`${printed(node)}\` at ${location.line}:${location.column} folds to a string, or a BigInt whose decimal ` +
|
|
1816
|
+
`string, longer than ${analysis.caps.foldLength} UTF-16 code units, the fold-length cap, so its value is unknown`,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* A descriptor the descriptor-alternatives cap cut (§18): counted once, and when its define reaches the global object or
|
|
1821
|
+
* a native holder, an `analysis-budget` limitation (§13, Principle 5): one for each descriptor.
|
|
1822
|
+
*/
|
|
1823
|
+
function descriptorCut(analysis, site, native) {
|
|
1824
|
+
const { source, starts, descriptorsCut, output } = analysis;
|
|
1825
|
+
const limited = descriptorsCut.get(site);
|
|
1826
|
+
if (limited === undefined)
|
|
1827
|
+
output.stats.caps['descriptors'] = (output.stats.caps['descriptors'] ?? 0) + 1;
|
|
1828
|
+
if (limited === true || !native) {
|
|
1829
|
+
if (limited === undefined)
|
|
1830
|
+
descriptorsCut.set(site, false);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
descriptorsCut.set(site, true);
|
|
1834
|
+
const location = locate(starts, site.start, site.end);
|
|
1835
|
+
output.limitations.push({
|
|
1836
|
+
kind: 'analysis-budget',
|
|
1837
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1838
|
+
location,
|
|
1839
|
+
detail: `the descriptor \`${printed(site)}\` at ${location.line}:${location.column} has more combinations of fields that ` +
|
|
1840
|
+
`may be absent than the descriptor-alternatives cap of ${analysis.caps.descriptorAlternatives}, so they are read ` +
|
|
1841
|
+
'as unseen',
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* A hit of a cap whose cut the value evaluation doesn't count itself (§18): `candidateRuns`, `testReads`,
|
|
1846
|
+
* `throwDepth`, `flagDepth`, `aliasDepth`, `heapDepth` (the heap's write-back depth) and `nesting`. Each is counted once
|
|
1847
|
+
* for each node where it cut, however often that node is judged, so the count doesn't depend on the order or the number
|
|
1848
|
+
* of times the analysis asks.
|
|
1849
|
+
*/
|
|
1850
|
+
function capHit(analysis, name, node) {
|
|
1851
|
+
let nodes = analysis.capsHit.get(name);
|
|
1852
|
+
if (nodes === undefined)
|
|
1853
|
+
analysis.capsHit.set(name, (nodes = new Set()));
|
|
1854
|
+
if (nodes.has(node))
|
|
1855
|
+
return;
|
|
1856
|
+
nodes.add(node);
|
|
1857
|
+
const { caps } = analysis.output.stats;
|
|
1858
|
+
caps[name] = (caps[name] ?? 0) + 1;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* A node whose candidate sites the candidate-runs cap cut, so they give nothing (§5 Candidate sets, §18): counted
|
|
1862
|
+
* once, and when a holder they write can be the global object or a native holder, an `analysis-budget` limitation
|
|
1863
|
+
* (§13, Principle 5): one for each node. The work done before the cut, every other write, stays.
|
|
1864
|
+
*/
|
|
1865
|
+
function candidatesCut(analysis, node, native) {
|
|
1866
|
+
const { source, starts, output } = analysis;
|
|
1867
|
+
const limited = analysis.candidatesCut.get(node);
|
|
1868
|
+
if (limited === undefined)
|
|
1869
|
+
capHit(analysis, 'candidateRuns', node);
|
|
1870
|
+
if (limited === true || !native) {
|
|
1871
|
+
if (limited === undefined)
|
|
1872
|
+
analysis.candidatesCut.set(node, false);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
analysis.candidatesCut.set(node, true);
|
|
1876
|
+
const location = locate(starts, node.start, node.end);
|
|
1877
|
+
output.limitations.push({
|
|
1878
|
+
kind: 'analysis-budget',
|
|
1879
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1880
|
+
location,
|
|
1881
|
+
detail: `the write \`${printed(node)}\` at ${location.line}:${location.column} has more combinations of candidate holders and ` +
|
|
1882
|
+
`keys than the candidate-runs cap of ${analysis.caps.candidateRuns} runs, so its candidate sites are not analyzed`,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
/** The natives that read a property's descriptor, which captures its original (§5 Partially resolved writes). */
|
|
1886
|
+
const descriptorReaders = new Set(['Object.getOwnPropertyDescriptor', 'Reflect.getOwnPropertyDescriptor']);
|
|
1887
|
+
/**
|
|
1888
|
+
* Whether a callee is a catalog native: a native function or holder, or a catalog member read from a native holder or
|
|
1889
|
+
* as a bare global. Passing the original to one doesn't keep it (§5 Partially resolved writes).
|
|
1890
|
+
*/
|
|
1891
|
+
function catalogNative(callee, resolution, at, catalog, realm) {
|
|
1892
|
+
const values = resolution.evaluate(callee, at);
|
|
1893
|
+
if (values.length > 0 && values.every((value) => value.kind === 'native' || value.kind === 'holder' || value.kind === 'member'))
|
|
1894
|
+
return true;
|
|
1895
|
+
const expression = calleeExpression(callee);
|
|
1896
|
+
if (expression.type !== 'MemberExpression')
|
|
1897
|
+
return false;
|
|
1898
|
+
const property = expression['property'];
|
|
1899
|
+
const key = expression['computed'] === true
|
|
1900
|
+
? resolution.evaluate(property, at)
|
|
1901
|
+
: property.type === 'Identifier'
|
|
1902
|
+
? [{ kind: 'constant', value: property['name'] }]
|
|
1903
|
+
: [];
|
|
1904
|
+
const [only] = key;
|
|
1905
|
+
if (key.length !== 1 || only?.kind !== 'constant')
|
|
1906
|
+
return false;
|
|
1907
|
+
const member = toPropertyKey(only.value);
|
|
1908
|
+
const holders = resolution.evaluate(expression['object'], at);
|
|
1909
|
+
const targets = catalog.targets[realm];
|
|
1910
|
+
const holdersChain = catalog.holders[realm];
|
|
1911
|
+
return (holders.length > 0 &&
|
|
1912
|
+
holders.every((value) => {
|
|
1913
|
+
if (value.kind !== 'holder')
|
|
1914
|
+
return false;
|
|
1915
|
+
for (let at = value.holder; at !== null; at = holdersChain[at]?.proto ?? null) {
|
|
1916
|
+
if (Object.hasOwn(targets, memberName(at, member)))
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
return false;
|
|
1920
|
+
}));
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* An `unresolved-holder` limitation (§5 Partially resolved writes, §13) for a write site whose member folds but whose
|
|
1924
|
+
* holder may be an object the analyzer can't resolve, when the site is patch-shaped and the member names a catalog
|
|
1925
|
+
* member it could mutate: one per site, however often it is instantiated. `detail` lists the candidate targets.
|
|
1926
|
+
*/
|
|
1927
|
+
function unresolvedHolder(analysis, write, patchShaped) {
|
|
1928
|
+
const { source, starts, catalog, holdersUnresolved } = analysis;
|
|
1929
|
+
if (holdersUnresolved.has(write.site))
|
|
1930
|
+
return;
|
|
1931
|
+
const candidates = candidateTargets(write.change, catalog, source.realm);
|
|
1932
|
+
if (candidates.length === 0 || !patchShaped(write))
|
|
1933
|
+
return;
|
|
1934
|
+
holdersUnresolved.add(write.site);
|
|
1935
|
+
const location = locate(starts, write.site.start, write.site.end);
|
|
1936
|
+
analysis.output.limitations.push({
|
|
1937
|
+
kind: 'unresolved-holder',
|
|
1938
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1939
|
+
location,
|
|
1940
|
+
detail: `\`${printed(write.holder)}\` at ${location.line}:${location.column} doesn't resolve, and the patch-shaped ` +
|
|
1941
|
+
`${write.mechanism} there could write ${candidates.join(', ')}`,
|
|
1942
|
+
candidates,
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* A write site whose candidate set passes the cap on holders alone (§5 Candidate sets): an `unresolved-holder`
|
|
1947
|
+
* limitation when it is patch-shaped, whose `detail` lists the candidates. With a member that folds, the site must keep
|
|
1948
|
+
* the original of its slot, and the candidates are the targets §3's outcome table could make it mutate. With one that
|
|
1949
|
+
* doesn't, the key must be reused, and the candidates are the `<holder>.*` wildcards, none on the global object.
|
|
1950
|
+
* Never a finding.
|
|
1951
|
+
*/
|
|
1952
|
+
function wideHolders(analysis, site, shapes) {
|
|
1953
|
+
const { source, starts, catalog, holdersUnresolved } = analysis;
|
|
1954
|
+
if (holdersUnresolved.has(site.site))
|
|
1955
|
+
return;
|
|
1956
|
+
let candidates;
|
|
1957
|
+
let holder;
|
|
1958
|
+
let mechanism;
|
|
1959
|
+
if ('open' in site) {
|
|
1960
|
+
if (!shapes.patchShaped(site.open))
|
|
1961
|
+
return;
|
|
1962
|
+
const names = new Set();
|
|
1963
|
+
for (const write of site.writes) {
|
|
1964
|
+
for (const outcome of outcomesOf(write, catalog, source.realm))
|
|
1965
|
+
if (!isIneffective(outcome))
|
|
1966
|
+
names.add(outcome.name);
|
|
1967
|
+
}
|
|
1968
|
+
candidates = [...names];
|
|
1969
|
+
({ holder, mechanism } = site.open);
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
const [first] = site.wild;
|
|
1973
|
+
const reuse = first === undefined ? undefined : shapes.reuse(first);
|
|
1974
|
+
if (first === undefined || reuse === undefined || reuse.writesBack)
|
|
1975
|
+
return;
|
|
1976
|
+
const holders = catalog.holders[source.realm];
|
|
1977
|
+
candidates = site.wild
|
|
1978
|
+
.filter((write) => Object.hasOwn(holders, write.holder) && holders[write.holder].kind !== 'global')
|
|
1979
|
+
.map((write) => `${write.holder}.*`);
|
|
1980
|
+
holder = first.object;
|
|
1981
|
+
mechanism = first.mechanism;
|
|
1982
|
+
}
|
|
1983
|
+
if (candidates.length === 0)
|
|
1984
|
+
return;
|
|
1985
|
+
holdersUnresolved.add(site.site);
|
|
1986
|
+
const location = locate(starts, site.site.start, site.site.end);
|
|
1987
|
+
analysis.output.limitations.push({
|
|
1988
|
+
kind: 'unresolved-holder',
|
|
1989
|
+
artifact: { url: source.url, hash: source.hash },
|
|
1990
|
+
location,
|
|
1991
|
+
detail: `\`${printed(holder)}\` at ${location.line}:${location.column} can be more holders than a candidate set takes, ` +
|
|
1992
|
+
`and the patch-shaped ${mechanism} there could write ${candidates.join(', ')}`,
|
|
1993
|
+
candidates,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* The `confidence-reason` steps (§14) of a write that is one candidate of its site's candidate set (§5 Candidate
|
|
1998
|
+
* sets, §9): `holder-alternatives` for several holders and `member-alternatives` for several members, each at the
|
|
1999
|
+
* branch that gives them, with the site's other candidates.
|
|
2000
|
+
*/
|
|
2001
|
+
function candidateSteps(write, starts) {
|
|
2002
|
+
const { alternatives } = write;
|
|
2003
|
+
if (alternatives === undefined)
|
|
2004
|
+
return none;
|
|
2005
|
+
const own = candidateName(write);
|
|
2006
|
+
const others = alternatives.targets.filter((name) => name !== own);
|
|
2007
|
+
const steps = [];
|
|
2008
|
+
const reasons = [];
|
|
2009
|
+
const step = (reason, node, what) => {
|
|
2010
|
+
const beside = others.length === 0 ? '' : `, beside ${others.map((name) => `\`${name}\``).join(', ')}`;
|
|
2011
|
+
reasons.push(reason);
|
|
2012
|
+
steps.push({
|
|
2013
|
+
kind: 'confidence-reason',
|
|
2014
|
+
dimension: 'target',
|
|
2015
|
+
reason,
|
|
2016
|
+
level: targetReasons[reason],
|
|
2017
|
+
sites: [locate(starts, node.start, node.end)],
|
|
2018
|
+
alternatives: others,
|
|
2019
|
+
text: `\`${printed(node)}\` can be ${what}, which neither folding nor the catalog settles, so \`${own}\` is one ` +
|
|
2020
|
+
`candidate${beside}`,
|
|
2021
|
+
});
|
|
2022
|
+
};
|
|
2023
|
+
if (alternatives.holders !== undefined)
|
|
2024
|
+
step('holder-alternatives', alternatives.holders.node, alternatives.holders.names.join(', '));
|
|
2025
|
+
if (alternatives.members !== undefined) {
|
|
2026
|
+
const keys = [
|
|
2027
|
+
...alternatives.members.keys.map((key) => (typeof key === 'symbol' ? symbolMember(key) : JSON.stringify(key))),
|
|
2028
|
+
// A prefix a pattern wildcard comes from (§5 Pattern wildcards).
|
|
2029
|
+
...(alternatives.members.prefixes ?? []).map((prefix) => `a string that starts with ${JSON.stringify(prefix)}`),
|
|
2030
|
+
];
|
|
2031
|
+
const unresolved = alternatives.members.unresolved === true ? ' or a value the analyzer can\'t resolve, which gives nothing' : '';
|
|
2032
|
+
const tested = alternatives.members.tested === true
|
|
2033
|
+
? ' or a heap-read or once-flag boolean, null or undefined that is never a key'
|
|
2034
|
+
: '';
|
|
2035
|
+
step('member-alternatives', alternatives.members.node, `${keys.join(', ')}${unresolved}${tested}`);
|
|
2036
|
+
}
|
|
2037
|
+
return { steps, reasons };
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* The native a call or `new` expression calls, when it compiles code from strings: the global object's `eval`,
|
|
2041
|
+
* `setTimeout` or `setInterval`, by name or read from the global object, or the `Function` constructor however it is
|
|
2042
|
+
* reached (§5 Global resolution). A name that resolves to a local, or to a top-level function declaration, is no native.
|
|
2043
|
+
*/
|
|
2044
|
+
function nativeCallee(node, { analysis, scope, resolution, instance }, at) {
|
|
2045
|
+
const { global } = analysis;
|
|
2046
|
+
const call = node.type === 'CallExpression';
|
|
2047
|
+
const callee = calleeExpression(node['callee']);
|
|
2048
|
+
// Only a callee spelt with one of the natives' names can denote one, so no other call is evaluated.
|
|
2049
|
+
const named = callee.type === 'Identifier' ? callee : callee.type === 'MemberExpression' ? callee['property'] : undefined;
|
|
2050
|
+
if (named === undefined || !codeCallees.includes(named['name'])) {
|
|
2051
|
+
return callee.type === 'Identifier' ? aliasedCallee(node, callee, resolution, at) : undefined;
|
|
2052
|
+
}
|
|
2053
|
+
const holders = resolution.evaluate(callee, at).filter((value) => value.kind === 'holder');
|
|
2054
|
+
if (holders.length === 1 && holders[0].holder === 'Function') {
|
|
2055
|
+
return { name: 'Function', leaves: inDocumentOrder(holders[0].leaves), direct: false };
|
|
2056
|
+
}
|
|
2057
|
+
if (!call)
|
|
2058
|
+
return undefined;
|
|
2059
|
+
if (callee.type === 'Identifier') {
|
|
2060
|
+
const name = callee['name'];
|
|
2061
|
+
if (!isGlobalCompiler(name) || !globalHas(analysis, name))
|
|
2062
|
+
return undefined;
|
|
2063
|
+
const binding = scope.resolve(callee);
|
|
2064
|
+
if (binding === 'local' || binding.declaration?.kind === 'function')
|
|
2065
|
+
return undefined;
|
|
2066
|
+
const leaf = { form: 'global-property', node: callee, name, binding, instance };
|
|
2067
|
+
return { name, leaves: [leaf], direct: isEvalCall(node) };
|
|
2068
|
+
}
|
|
2069
|
+
if (callee.type !== 'MemberExpression' || callee['computed'] === true)
|
|
2070
|
+
return undefined;
|
|
2071
|
+
const property = callee['property'];
|
|
2072
|
+
const name = property['name'];
|
|
2073
|
+
const object = callee['object'];
|
|
2074
|
+
if (property.type !== 'Identifier' || object.type === 'Super' || !isGlobalCompiler(name) || !globalHas(analysis, name)) {
|
|
2075
|
+
return undefined;
|
|
2076
|
+
}
|
|
2077
|
+
const bases = resolution.evaluate(object, at).filter((value) => value.kind === 'holder');
|
|
2078
|
+
if (bases.length !== 1 || bases[0].holder !== global)
|
|
2079
|
+
return undefined;
|
|
2080
|
+
return { name, leaves: inDocumentOrder(bases[0].leaves), direct: false };
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* The native that compiles code a name aliases (§5 Resolver capabilities 2): `var e = eval; e(s)` is an indirect eval,
|
|
2084
|
+
* whose literal code is global nested code, like `(0, eval)(s)`. So is an aliased string timer, and the `Function`
|
|
2085
|
+
* constructor. Each alternative that is one counts (Principle 3), the first in source order when there are several.
|
|
2086
|
+
*/
|
|
2087
|
+
function aliasedCallee(node, id, resolution, at) {
|
|
2088
|
+
const construct = node.type === 'NewExpression';
|
|
2089
|
+
for (const value of resolution.aliasCallee(id, at)) {
|
|
2090
|
+
if (value.kind !== 'holder' && value.kind !== 'native')
|
|
2091
|
+
continue;
|
|
2092
|
+
const leaves = inDocumentOrder(value.leaves);
|
|
2093
|
+
const folds = value.folds ?? [];
|
|
2094
|
+
if (value.kind === 'holder' && value.holder === 'Function')
|
|
2095
|
+
return { name: 'Function', leaves, folds, direct: false };
|
|
2096
|
+
// `eval` and the timers are no constructors, so `new` throws.
|
|
2097
|
+
if (value.kind === 'native' && !construct && isGlobalCompiler(value.name)) {
|
|
2098
|
+
return { name: value.name, leaves, folds, direct: false };
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return undefined;
|
|
2102
|
+
}
|
|
2103
|
+
/** Whether the realm's global object has the member `name`, its own or inherited, in the unmutated catalog (§4). */
|
|
2104
|
+
function globalHas({ catalog, targets, global, source }, name) {
|
|
2105
|
+
const holders = catalog.holders[source.realm];
|
|
2106
|
+
for (let holder = global; holder !== undefined; holder = holders[holder]?.proto ?? undefined) {
|
|
2107
|
+
if (Object.hasOwn(targets, `${holder}.${name}`))
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* The nesting a call that compiles code adds: its note, then how its native callee was reached (§14), and the code
|
|
2114
|
+
* `literals` it compiles.
|
|
2115
|
+
*/
|
|
2116
|
+
function nestingOf(callee, scope, call, leaves, analysis, literals, folds = []) {
|
|
2117
|
+
const site = locate(analysis.starts, call.start, call.end);
|
|
2118
|
+
const runs = scope === 'caller'
|
|
2119
|
+
? `runs in the scope of its caller`
|
|
2120
|
+
: callee === 'Function'
|
|
2121
|
+
? `compiles into a function in the global scope`
|
|
2122
|
+
: callee === 'eval'
|
|
2123
|
+
? `runs in the global scope`
|
|
2124
|
+
: callee === 'script'
|
|
2125
|
+
? `inserts as a script element's text, which runs as a classic script`
|
|
2126
|
+
: `compiles as a classic script`;
|
|
2127
|
+
const note = {
|
|
2128
|
+
kind: 'note',
|
|
2129
|
+
about: 'nested-code',
|
|
2130
|
+
callee,
|
|
2131
|
+
scope,
|
|
2132
|
+
call: site,
|
|
2133
|
+
text: callee === 'script'
|
|
2134
|
+
? `the write is in code from a string literal that the call at ${site.line}:${site.column} ${runs}`
|
|
2135
|
+
: `the write is in code from a string literal that \`${callee}\` at ${site.line}:${site.column} ${runs}`,
|
|
2136
|
+
};
|
|
2137
|
+
return joined({ steps: [note], reasons: [], literals }, stepsOf(leaves, analysis), foldSteps(folds, analysis));
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Each leaf's binding step, after those of the leaves it was reached from, and followed by the evidence for the taint on
|
|
2141
|
+
* its name's resolution path (§9, §14).
|
|
2142
|
+
*/
|
|
2143
|
+
function stepsOf(leaves, { global, source, starts }) {
|
|
2144
|
+
const steps = onePerNode(leaves.flatMap(withBase)).map((leaf) => ({
|
|
2145
|
+
step: isBindingLeaf(leaf) ? bindingStep(leaf, { global, realm: source.realm, starts }) : catalogHolderStep(leaf, starts),
|
|
2146
|
+
taints: leaf.binding === undefined ? [] : taintSteps(leaf.name, leaf.binding, starts),
|
|
2147
|
+
}));
|
|
2148
|
+
return {
|
|
2149
|
+
steps: steps.flatMap(({ step, taints }) => [step, ...taints]),
|
|
2150
|
+
reasons: steps.flatMap(({ taints }) => taints.map((step) => step.reason)),
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
const isBindingLeaf = (leaf) => leaf.form !== 'prototype-of';
|
|
2154
|
+
/** The `catalog-holder` step (§14) of `Object.getPrototypeOf(h)`: the prototype the unmutated catalog gives `h`. */
|
|
2155
|
+
function catalogHolderStep(leaf, starts) {
|
|
2156
|
+
const { node, prototypeOf, holds } = leaf;
|
|
2157
|
+
const generated = locate(starts, node.start, node.end);
|
|
2158
|
+
const expression = printed(node);
|
|
2159
|
+
return {
|
|
2160
|
+
kind: 'catalog-holder',
|
|
2161
|
+
expression,
|
|
2162
|
+
generated,
|
|
2163
|
+
prototypeOf: prototypeOf,
|
|
2164
|
+
holder: holds,
|
|
2165
|
+
text: `\`${expression}\` at ${generated.line}:${generated.column} is the prototype of \`${prototypeOf}\` in the ` +
|
|
2166
|
+
`unmutated catalog, \`${holds}\``,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
/** The `catalog-holder` step (§14) of an own-property test the unmutated catalog decided (§4). */
|
|
2170
|
+
function ownTestStep(node, { tested, member, owns }, starts) {
|
|
2171
|
+
const generated = locate(starts, node.start, node.end);
|
|
2172
|
+
const expression = printed(node);
|
|
2173
|
+
const at = `${generated.line}:${generated.column}`;
|
|
2174
|
+
return {
|
|
2175
|
+
kind: 'catalog-holder',
|
|
2176
|
+
expression,
|
|
2177
|
+
generated,
|
|
2178
|
+
tested,
|
|
2179
|
+
...(member === undefined ? {} : { member }),
|
|
2180
|
+
owns,
|
|
2181
|
+
text: member === undefined
|
|
2182
|
+
? `\`${expression}\` at ${at} is false in the unmutated catalog: \`${tested}\` owns only structural members`
|
|
2183
|
+
: `\`${expression}\` at ${at} is ${String(owns)} in the unmutated catalog: \`${tested}\` ` +
|
|
2184
|
+
`${owns ? 'owns' : "doesn't own"} \`${member}\``,
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
/** The `enumeration` step (§14) of a key a member sweep gave (§5 Resolver capabilities 9), with its filters. */
|
|
2188
|
+
function enumerationStep(node, { holder, form, member, filters }, starts) {
|
|
2189
|
+
const generated = locate(starts, node.start, node.end);
|
|
2190
|
+
const expression = printed(node);
|
|
2191
|
+
const shown = filters.map((filter) => {
|
|
2192
|
+
const where = locate(starts, filter.node.start, filter.node.end);
|
|
2193
|
+
return { expression: printed(filter.node), generated: where, result: filter.result };
|
|
2194
|
+
});
|
|
2195
|
+
const through = shown.map(({ expression, generated, result }) => `, ${result === 'kept' ? 'kept by' : 'perhaps kept by'} \`${expression}\` at ${generated.line}:${generated.column}`);
|
|
2196
|
+
return {
|
|
2197
|
+
kind: 'enumeration',
|
|
2198
|
+
expression,
|
|
2199
|
+
generated,
|
|
2200
|
+
holder,
|
|
2201
|
+
enumeration: form,
|
|
2202
|
+
member,
|
|
2203
|
+
filters: shown,
|
|
2204
|
+
text: `\`${expression}\` at ${generated.line}:${generated.column} sweeps \`${holder}\` (${form}), and the catalog ` +
|
|
2205
|
+
`gives its member \`${member}\`${through.join('')}`,
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* The `enumeration` step (§14) of a wildcard whose key starts with a configurable prefix (§5 Pattern wildcards):
|
|
2210
|
+
* the key's other alternatives are values the analyzer can't resolve, which give nothing, and the prefix is the
|
|
2211
|
+
* literal default the wildcard comes from. None for any other write.
|
|
2212
|
+
*/
|
|
2213
|
+
function prefixSteps(write, starts) {
|
|
2214
|
+
const { pattern, prefixes, configurable, key, holder } = write;
|
|
2215
|
+
const literal = pattern ?? prefixes?.[0];
|
|
2216
|
+
if (configurable !== true || literal === undefined || key === undefined)
|
|
2217
|
+
return none;
|
|
2218
|
+
const generated = locate(starts, key.start, key.end);
|
|
2219
|
+
const expression = printed(key);
|
|
2220
|
+
const step = {
|
|
2221
|
+
kind: 'enumeration',
|
|
2222
|
+
expression,
|
|
2223
|
+
generated,
|
|
2224
|
+
holder,
|
|
2225
|
+
prefix: literal,
|
|
2226
|
+
configurable: true,
|
|
2227
|
+
text: `\`${expression}\` at ${generated.line}:${generated.column} starts with a configurable prefix whose default is ` +
|
|
2228
|
+
`${JSON.stringify(literal)}: its alternatives the analyzer can't resolve give nothing`,
|
|
2229
|
+
};
|
|
2230
|
+
return { steps: [step], reasons: [] };
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* A write's steps with the filters on its path that test a swept key (§5 Resolver capabilities 9) added to each of its
|
|
2234
|
+
* `enumeration` steps, after the filters its keys' lists went through: `kept` for a filter the analyzer decided keeps
|
|
2235
|
+
* the key, `undecided` for one it can't decide.
|
|
2236
|
+
*/
|
|
2237
|
+
function withPathFilters(steps, tests, starts) {
|
|
2238
|
+
const filters = [...tests]
|
|
2239
|
+
.filter(([test]) => sweepTests.has(test))
|
|
2240
|
+
.map(([test, step]) => ({
|
|
2241
|
+
expression: printed(test),
|
|
2242
|
+
generated: locate(starts, test.start, test.end),
|
|
2243
|
+
result: step.class === 'transparent' ? 'kept' : 'undecided',
|
|
2244
|
+
}));
|
|
2245
|
+
if (filters.length === 0)
|
|
2246
|
+
return [...steps];
|
|
2247
|
+
return steps.map((step) => {
|
|
2248
|
+
if (step.kind !== 'enumeration' || !('filters' in step))
|
|
2249
|
+
return step;
|
|
2250
|
+
const added = filters.map(({ expression, generated, result }) => `, ${result === 'kept' ? 'kept by' : 'perhaps kept by'} \`${expression}\` at ${generated.line}:${generated.column}`);
|
|
2251
|
+
return { ...step, filters: [...step.filters, ...filters], text: `${step.text}${added.join('')}` };
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* The `guard` steps (§14) of the filters a write's swept keys went through that may have dropped them (§5 Resolver
|
|
2256
|
+
* capabilities 9): each `undecided`, so `conditional`, once per filter.
|
|
2257
|
+
*/
|
|
2258
|
+
function undecidedFilterSteps(folds, starts) {
|
|
2259
|
+
const nodes = new Set();
|
|
2260
|
+
for (const fold of folds) {
|
|
2261
|
+
for (const filter of fold.sweep?.filters ?? [])
|
|
2262
|
+
if (filter.result === 'undecided')
|
|
2263
|
+
nodes.add(filter.node);
|
|
2264
|
+
}
|
|
2265
|
+
return [...nodes].map((node) => {
|
|
2266
|
+
const generated = locate(starts, node.start, node.end);
|
|
2267
|
+
const expression = printed(node);
|
|
2268
|
+
return {
|
|
2269
|
+
kind: 'guard',
|
|
2270
|
+
expression,
|
|
2271
|
+
generated,
|
|
2272
|
+
class: 'undecided',
|
|
2273
|
+
result: 'conditional',
|
|
2274
|
+
text: `the sweep filter \`${expression}\` at ${generated.line}:${generated.column} can't be decided on the swept ` +
|
|
2275
|
+
'key, so it may keep it: conditional',
|
|
2276
|
+
};
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
/** The `catalog-holder` step (§14) of a prototype walk to the first owner the unmutated catalog simulated (§4). */
|
|
2280
|
+
function walkStep(node, { walk, holder, member }, starts) {
|
|
2281
|
+
const generated = locate(starts, node.start, node.end);
|
|
2282
|
+
const expression = printed(node);
|
|
2283
|
+
const at = `${generated.line}:${generated.column}`;
|
|
2284
|
+
const visits = walk.map((each) => `\`${each}\``).join(', ');
|
|
2285
|
+
return {
|
|
2286
|
+
kind: 'catalog-holder',
|
|
2287
|
+
expression,
|
|
2288
|
+
generated,
|
|
2289
|
+
walk,
|
|
2290
|
+
...(member === undefined ? {} : { member }),
|
|
2291
|
+
holder,
|
|
2292
|
+
text: `the prototype walk at ${at}, simulated on the unmutated catalog, visits ${visits} ` +
|
|
2293
|
+
(member === undefined
|
|
2294
|
+
? `and may stop at \`${holder}\`, which may own the member`
|
|
2295
|
+
: `and stops at \`${holder}\`, the first that owns \`${member}\``),
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Each fold's `fold` step (§14), once, in the order the write's folds were made, followed by the evidence for the taint
|
|
2300
|
+
* on its name's resolution path (§9). A well-known symbol's fold comes after the steps that reached `Symbol`. A fold
|
|
2301
|
+
* whose value JSON can't carry (`undefined`, `NaN`) has no step, but its taint still lowers target confidence.
|
|
2302
|
+
*/
|
|
2303
|
+
function foldSteps(folds, analysis) {
|
|
2304
|
+
const { starts } = analysis;
|
|
2305
|
+
// A call's folds stay apart by what came through it, and its step is one.
|
|
2306
|
+
const called = new Set();
|
|
2307
|
+
const steps = mergedFolds([], folds).map(({ node, name, value, taint, via, alias, call, catalog, walked, link, sweep }) => {
|
|
2308
|
+
if (sweep !== undefined)
|
|
2309
|
+
return { steps: [enumerationStep(node, sweep, starts)], reasons: [] };
|
|
2310
|
+
if (catalog !== undefined)
|
|
2311
|
+
return { steps: [ownTestStep(node, catalog, starts)], reasons: [] };
|
|
2312
|
+
if (walked !== undefined)
|
|
2313
|
+
return { steps: [walkStep(node, walked, starts)], reasons: [] };
|
|
2314
|
+
if (link !== undefined)
|
|
2315
|
+
return { steps: [moduleLinkStep(node, link, analysis)], reasons: [] };
|
|
2316
|
+
if (call !== undefined) {
|
|
2317
|
+
const key = `${call.site.call.start}:${call.site.fn.start}`;
|
|
2318
|
+
if (called.has(key))
|
|
2319
|
+
return { steps: [], reasons: [] };
|
|
2320
|
+
called.add(key);
|
|
2321
|
+
// A require call that links a module is a module-link hop, whose `module-link` fold stands for it (§6).
|
|
2322
|
+
if (analysis.requireLinks.has(call.site.call))
|
|
2323
|
+
return { steps: [], reasons: [] };
|
|
2324
|
+
// The module links the callee came through come before the call (§6).
|
|
2325
|
+
const callee = (call.site.links ?? []).map((each) => moduleLinkStep(each.node, each.link, analysis));
|
|
2326
|
+
return { steps: [...callee, callStep(call, starts)], reasons: [] };
|
|
2327
|
+
}
|
|
2328
|
+
const reached = via === undefined ? { steps: [], reasons: [] } : stepsOf(via, analysis);
|
|
2329
|
+
const taints = name === undefined || taint === undefined ? [] : taintSteps(name, taint, starts);
|
|
2330
|
+
const fold = alias === true
|
|
2331
|
+
? [aliasStep(node, name, starts)]
|
|
2332
|
+
: typeof value === 'symbol' || recordable(value)
|
|
2333
|
+
? [foldStep(node, name, value, starts)]
|
|
2334
|
+
: [];
|
|
2335
|
+
return {
|
|
2336
|
+
steps: [...reached.steps, ...fold, ...taints],
|
|
2337
|
+
reasons: [...reached.reasons, ...taints.map((step) => step.reason)],
|
|
2338
|
+
};
|
|
2339
|
+
});
|
|
2340
|
+
return joined(...steps);
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* The innermost function around a write in the walk's tree, out from its site: grafted nested code shares its literal's
|
|
2344
|
+
* position, so a position lookup could name a function inside it. The walk can be at a node inside the site, or at a
|
|
2345
|
+
* function declaration whose name is the site, which its enclosing scope declares.
|
|
2346
|
+
*/
|
|
2347
|
+
function functionAround(ancestors, site) {
|
|
2348
|
+
const at = ancestors.lastIndexOf(site);
|
|
2349
|
+
return (at < 0 ? ancestors : ancestors.slice(0, at)).findLast((each) => isFunction(each) && each['id'] !== site);
|
|
2350
|
+
}
|
|
2351
|
+
/** What identifies a `creation-site` step: the function, and where its object was made. */
|
|
2352
|
+
const creationKey = (step) => `${step.function.start}:${step.generated.start}`;
|
|
2353
|
+
/**
|
|
2354
|
+
* The `creation-site` steps (§14) of the function objects a write is instantiated in that no resolved call reaches
|
|
2355
|
+
* (§5 Resolver capabilities 3), outermost first: where each was made, and the `this` it was assumed to run with.
|
|
2356
|
+
*/
|
|
2357
|
+
function creationSteps(frame, starts) {
|
|
2358
|
+
const found = [];
|
|
2359
|
+
const seen = new Set();
|
|
2360
|
+
const pending = frame === undefined ? [] : [frame];
|
|
2361
|
+
while (pending.length > 0) {
|
|
2362
|
+
const next = pending.shift();
|
|
2363
|
+
if (seen.has(next))
|
|
2364
|
+
continue;
|
|
2365
|
+
seen.add(next);
|
|
2366
|
+
if (next.how === 'creation')
|
|
2367
|
+
found.push(next);
|
|
2368
|
+
if (next.outer !== undefined)
|
|
2369
|
+
pending.push(next.outer);
|
|
2370
|
+
for (const { caller } of next.bindings)
|
|
2371
|
+
if (caller !== undefined)
|
|
2372
|
+
pending.push(caller);
|
|
2373
|
+
}
|
|
2374
|
+
const steps = found.toReversed().map((each) => {
|
|
2375
|
+
const fn = locate(starts, each.fn.start, each.fn.end);
|
|
2376
|
+
const created = each.created ?? each.fn;
|
|
2377
|
+
const generated = locate(starts, created.start, created.end);
|
|
2378
|
+
const receiver = each.receiverNode === undefined ? undefined : printed(each.receiverNode);
|
|
2379
|
+
return {
|
|
2380
|
+
kind: 'creation-site',
|
|
2381
|
+
function: fn,
|
|
2382
|
+
generated,
|
|
2383
|
+
...(receiver === undefined ? {} : { this: receiver }),
|
|
2384
|
+
text: `no resolved call reaches the function at ${fn.line}:${fn.column} made at ${generated.line}:${generated.column}, ` +
|
|
2385
|
+
`so it is judged from where it was made` +
|
|
2386
|
+
(receiver === undefined ? '' : `, with \`this\` = \`${receiver}\`, the object it is held on`),
|
|
2387
|
+
};
|
|
2388
|
+
});
|
|
2389
|
+
return { steps, reasons: [] };
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* The `call` step (§14) of a summarized call: the call, the function it calls, its argument bindings, and the receiver
|
|
2393
|
+
* it dispatches through, if it passes one. A class with no constructor of its own takes no parameters.
|
|
2394
|
+
*/
|
|
2395
|
+
function callStep({ site, element }, starts) {
|
|
2396
|
+
const generated = locate(starts, site.call.start, site.call.end);
|
|
2397
|
+
const fn = site.fn;
|
|
2398
|
+
const callee = printed(site.call['callee']);
|
|
2399
|
+
const bindings = (fn['params'] ?? []).map((param, index) => {
|
|
2400
|
+
if (site.iterates !== undefined)
|
|
2401
|
+
return { parameter: printed(param), argument: iteratedArgument(site, element, index) };
|
|
2402
|
+
if (site.unlisted === undefined || index < site.unlisted) {
|
|
2403
|
+
const arg = site.args[index];
|
|
2404
|
+
return { parameter: printed(param), argument: arg === undefined ? null : printed(arg) };
|
|
2405
|
+
}
|
|
2406
|
+
return { parameter: printed(param), argument: unlistedArgument(site) };
|
|
2407
|
+
});
|
|
2408
|
+
const receiver = site.receiver === undefined || site.receiver === 'new' || site.receiver === 'none' ? undefined : printed(site.receiver);
|
|
2409
|
+
const at = `${generated.line}:${generated.column}`;
|
|
2410
|
+
const passes = bindings.map(({ parameter, argument }) => `\`${parameter}\` = ${argument === null ? 'nothing' : `\`${argument}\``}`);
|
|
2411
|
+
return {
|
|
2412
|
+
kind: 'call',
|
|
2413
|
+
callee,
|
|
2414
|
+
generated,
|
|
2415
|
+
function: locate(starts, fn.start, fn.end),
|
|
2416
|
+
bindings,
|
|
2417
|
+
...(receiver === undefined ? {} : { this: receiver }),
|
|
2418
|
+
text: `the call \`${callee}(…)\` at ${at} runs the function with ` +
|
|
2419
|
+
(receiver === undefined ? 'its arguments' : `\`this\` = \`${receiver}\`${passes.length === 0 ? '' : ', and its arguments'}`) +
|
|
2420
|
+
(passes.length === 0 ? '' : ` bound as ${passes.join(', ')}`),
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* What an iteration native passes its callback's parameter `index` in the call that visits `element` (§5 Resolver
|
|
2425
|
+
* capabilities 4): the element, printed as the collection indexed, its index, or the collection. `reduce` passes its
|
|
2426
|
+
* accumulator first, which is unknown.
|
|
2427
|
+
*/
|
|
2428
|
+
function iteratedArgument(site, element, index) {
|
|
2429
|
+
const { native, collection } = site.iterates;
|
|
2430
|
+
const reduces = native === 'reduce' || native === 'reduceRight';
|
|
2431
|
+
const position = reduces ? index - 1 : index;
|
|
2432
|
+
const list = printed(collection);
|
|
2433
|
+
if (position < 0)
|
|
2434
|
+
return 'accumulator';
|
|
2435
|
+
if (element === undefined)
|
|
2436
|
+
return '…';
|
|
2437
|
+
return [`${list}[${element}]`, String(element), list][position] ?? null;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* What a call passes to a parameter at or past the first argument it can't list: the spread there, in the call or in
|
|
2441
|
+
* `.apply`'s array, or else `.apply`'s argument list spread (`...args`). Never null: the call may pass something there.
|
|
2442
|
+
*/
|
|
2443
|
+
function unlistedArgument(site) {
|
|
2444
|
+
const at = site.args[site.unlisted];
|
|
2445
|
+
if (at !== undefined)
|
|
2446
|
+
return printed(at);
|
|
2447
|
+
const list = site.call['arguments'].at(-1);
|
|
2448
|
+
if (list === undefined)
|
|
2449
|
+
return '…';
|
|
2450
|
+
const element = list.type === 'ArrayExpression' ? list['elements'][site.unlisted] : undefined;
|
|
2451
|
+
if (element !== undefined && element !== null)
|
|
2452
|
+
return printed(element);
|
|
2453
|
+
return list.type === 'SpreadElement' ? printed(list) : `...${printed(list)}`;
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* The `module-link` step (§6, §14) of a module-link hop at `node`: a require call, a module's thunk, getter or interop
|
|
2457
|
+
* wrapper call, or an export's getter or value.
|
|
2458
|
+
*/
|
|
2459
|
+
function moduleLinkStep(node, link, { source, starts }) {
|
|
2460
|
+
const generated = locate(starts, node.start, node.end);
|
|
2461
|
+
const at = `${generated.line}:${generated.column}`;
|
|
2462
|
+
return {
|
|
2463
|
+
kind: 'module-link',
|
|
2464
|
+
idiom: link.idiom,
|
|
2465
|
+
module: link.module,
|
|
2466
|
+
...(link.export === undefined ? {} : { export: link.export }),
|
|
2467
|
+
artifact: source.url,
|
|
2468
|
+
generated,
|
|
2469
|
+
text: link.export === undefined
|
|
2470
|
+
? `via module ${link.module}, which the ${link.idiom === 'factory-table' ? 'require call' : 'call'} at ${at} runs (${link.idiom})`
|
|
2471
|
+
: `via module ${link.module} export \`${link.export}\` at ${at} (${link.idiom})`,
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
/** The `alias` step (§14) of a value read through the variable `name`, from its definition `site`. */
|
|
2475
|
+
function aliasStep(site, name, starts) {
|
|
2476
|
+
const generated = locate(starts, site.start, site.end);
|
|
2477
|
+
const expression = printed(site);
|
|
2478
|
+
return {
|
|
2479
|
+
kind: 'alias',
|
|
2480
|
+
name,
|
|
2481
|
+
expression,
|
|
2482
|
+
generated,
|
|
2483
|
+
text: `\`${name}\` is read from its definition \`${expression}\` at ${generated.line}:${generated.column}`,
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function foldStep(node, name, value, starts) {
|
|
2487
|
+
const generated = locate(starts, node.start, node.end);
|
|
2488
|
+
const expression = printed(node);
|
|
2489
|
+
const at = `${generated.line}:${generated.column}`;
|
|
2490
|
+
const constant = typeof value === 'symbol'
|
|
2491
|
+
? `the well-known symbol ${symbolMember(value)}`
|
|
2492
|
+
: typeof value === 'string'
|
|
2493
|
+
? JSON.stringify(value)
|
|
2494
|
+
: String(value);
|
|
2495
|
+
const text = name === undefined
|
|
2496
|
+
? `\`${expression}\` at ${at} folds to ${constant}`
|
|
2497
|
+
: `\`${name}\` is a \`const\` whose value \`${expression}\` at ${at} folds to ${constant}`;
|
|
2498
|
+
const folded = typeof value === 'symbol' ? { symbol: symbolMember(value) } : { value };
|
|
2499
|
+
return { kind: 'fold', ...(name === undefined ? {} : { name }), expression, generated, ...folded, text };
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* A leaf, after the leaves the value it steps from was reached from (`window` before `window.parent`), and for
|
|
2503
|
+
* `Object.getPrototypeOf(h)`, after the leaves of `Object` and then of `h`.
|
|
2504
|
+
*/
|
|
2505
|
+
function withBase(leaf) {
|
|
2506
|
+
return [...(leaf.callee ?? []).flatMap(withBase), ...(leaf.base ?? []).flatMap(withBase), leaf];
|
|
2507
|
+
}
|
|
2508
|
+
function joined(...nestings) {
|
|
2509
|
+
const literals = nestings.find((nesting) => nesting.literals !== undefined)?.literals;
|
|
2510
|
+
return {
|
|
2511
|
+
steps: nestings.flatMap((nesting) => nesting.steps),
|
|
2512
|
+
reasons: nestings.flatMap((nesting) => nesting.reasons),
|
|
2513
|
+
...(literals === undefined ? {} : { literals }),
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Where attribution looks a write up (§10 lookup point): its member token or holder argument, or for a write in nested
|
|
2518
|
+
* code, the start of the code literal in the host artifact's own code that it is in, with no token: the host's map
|
|
2519
|
+
* maps the literal as one token.
|
|
2520
|
+
*/
|
|
2521
|
+
function lookupOf(write, { literals }, starts, resolution, artifact) {
|
|
2522
|
+
if (literals === undefined) {
|
|
2523
|
+
const helper = helperLookup(write, starts, resolution, artifact);
|
|
2524
|
+
if (helper !== undefined)
|
|
2525
|
+
return helper;
|
|
2526
|
+
const { node, token } = write.lookup;
|
|
2527
|
+
return { generated: locate(starts, node.start, node.end), token };
|
|
2528
|
+
}
|
|
2529
|
+
const { start } = write.site;
|
|
2530
|
+
const literal = literals.find((each) => each.start <= start && start < each.end) ?? literals[0];
|
|
2531
|
+
return { generated: locate(starts, literal.start, literal.end), token: null };
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* For a write inside a helper, the outermost call on the chain whose arguments carry both the holder and the member
|
|
2535
|
+
* (§10): the holder written came through the call, and the member is the key one of its arguments is, which came
|
|
2536
|
+
* through it too, or the key folds part of it (`o['set' + n]`), looked up at the call with no token. A write with
|
|
2537
|
+
* no member needs only the holder. Its token is the member's key when the call writes it
|
|
2538
|
+
* as a string or a template with no substitutions. Undefined when no call carries both, so the write itself is looked
|
|
2539
|
+
* up. The calls a value came through are its `call` folds, outermost first.
|
|
2540
|
+
*/
|
|
2541
|
+
function helperLookup(write, starts, resolution, artifact) {
|
|
2542
|
+
const { holder, member } = write.parts;
|
|
2543
|
+
// The lookup crosses artifacts only within the write artifact's origin: a call in another member of the group
|
|
2544
|
+
// counts when that member has the write's origin, and the lookup then names it.
|
|
2545
|
+
const lookup = (node, token) => {
|
|
2546
|
+
const generated = locate(starts, node.start, node.end);
|
|
2547
|
+
const at = starts.artifactOf(generated);
|
|
2548
|
+
return at.url === artifact.url ? { generated, token } : { artifact: at.url, generated, token };
|
|
2549
|
+
};
|
|
2550
|
+
const sameOrigin = (node) => {
|
|
2551
|
+
const at = starts.memberAt(node.start);
|
|
2552
|
+
return at.url === artifact.url || (at.origin !== undefined && at.origin === artifact.origin);
|
|
2553
|
+
};
|
|
2554
|
+
// The argument each call passed the member in through.
|
|
2555
|
+
const through = new Map(member.flatMap((fold) => (fold.call === undefined ? [] : [[fold.node, fold.argument]])));
|
|
2556
|
+
const key = write.change.kind === 'prototype' ? undefined : write.change.member;
|
|
2557
|
+
for (const fold of holder) {
|
|
2558
|
+
if (fold.call === undefined)
|
|
2559
|
+
continue;
|
|
2560
|
+
const { site } = fold.call;
|
|
2561
|
+
if (!sameOrigin(site.call))
|
|
2562
|
+
continue;
|
|
2563
|
+
let token = null;
|
|
2564
|
+
if (key !== undefined) {
|
|
2565
|
+
const index = through.get(site.call);
|
|
2566
|
+
if (index === undefined || !resolution.argumentCarries(fold.call, index, key, member))
|
|
2567
|
+
continue;
|
|
2568
|
+
// A member read through heap fields is looked up at the string literal in the call's arguments that supplies it,
|
|
2569
|
+
// with that token, and at the call with no token when the call doesn't write it.
|
|
2570
|
+
const arg = site.args[index];
|
|
2571
|
+
const literal = member.find((each) => each.heap === true && arg.start <= each.node.start && each.node.end <= arg.end && writtenKey(each.node) === key);
|
|
2572
|
+
if (literal !== undefined)
|
|
2573
|
+
return lookup(literal.node, key);
|
|
2574
|
+
token = typeof key === 'string' && writtenIn(arg, key) ? key : null;
|
|
2575
|
+
}
|
|
2576
|
+
return lookup(site.call, token);
|
|
2577
|
+
}
|
|
2578
|
+
return undefined;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Whether the member's key is written in an argument as a string or a template with no substitutions: the argument
|
|
2582
|
+
* itself, or an element of an array literal it is.
|
|
2583
|
+
*/
|
|
2584
|
+
function writtenIn(arg, key) {
|
|
2585
|
+
if (writtenKey(arg) === key)
|
|
2586
|
+
return true;
|
|
2587
|
+
if (arg.type !== 'ArrayExpression')
|
|
2588
|
+
return false;
|
|
2589
|
+
return arg['elements'].some((element) => element !== null && writtenIn(element, key));
|
|
2590
|
+
}
|
|
2591
|
+
/** A key written as a string literal or a template literal with no substitutions. */
|
|
2592
|
+
function writtenKey(node) {
|
|
2593
|
+
if (node.type === 'Literal')
|
|
2594
|
+
return typeof node['value'] === 'string' ? node['value'] : undefined;
|
|
2595
|
+
if (node.type !== 'TemplateLiteral' || node['expressions'].length > 0)
|
|
2596
|
+
return undefined;
|
|
2597
|
+
return node['quasis'][0]['value'].cooked ?? undefined;
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* An `analysis-budget` limitation (§13, §18) for a call that a recursion or context cap widened or merged while it
|
|
2601
|
+
* carries the global object, a native holder or a native member (Principle 5): one for each call.
|
|
2602
|
+
*/
|
|
2603
|
+
function callCut(analysis, site, why) {
|
|
2604
|
+
const { source, starts, callsCut } = analysis;
|
|
2605
|
+
if (callsCut.has(site))
|
|
2606
|
+
return;
|
|
2607
|
+
callsCut.add(site);
|
|
2608
|
+
const location = locate(starts, site.start, site.end);
|
|
2609
|
+
const cap = why === 'recursion'
|
|
2610
|
+
? 'it recurses or lies past the call-depth cap, so its function ran in a widened frame'
|
|
2611
|
+
: 'the context cap merged it with other calls of its function';
|
|
2612
|
+
analysis.output.limitations.push({
|
|
2613
|
+
kind: 'analysis-budget',
|
|
2614
|
+
artifact: { url: source.url, hash: source.hash },
|
|
2615
|
+
location,
|
|
2616
|
+
detail: `the call \`${printed(site['callee'])}(…)\` at ${location.line}:${location.column} carries a native, and ${cap}`,
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* An `analysis-budget` limitation (§13, §18) for a loop, or an iteration native's call, that the iteration cap cut while
|
|
2621
|
+
* it writes the global object or a native holder (Principle 5): one for each loop. The iterations done keep their
|
|
2622
|
+
* findings.
|
|
2623
|
+
*/
|
|
2624
|
+
function loopCut(analysis, loop) {
|
|
2625
|
+
const { source, starts, loopsCut } = analysis;
|
|
2626
|
+
if (loopsCut.has(loop))
|
|
2627
|
+
return;
|
|
2628
|
+
loopsCut.add(loop);
|
|
2629
|
+
const location = locate(starts, loop.start, loop.end);
|
|
2630
|
+
const what = isLoop(loop) ? 'the loop' : `the call \`${printed(loop['callee'])}(…)\``;
|
|
2631
|
+
analysis.output.limitations.push({
|
|
2632
|
+
kind: 'analysis-budget',
|
|
2633
|
+
artifact: { url: source.url, hash: source.hash },
|
|
2634
|
+
location,
|
|
2635
|
+
detail: `${what} at ${location.line}:${location.column} writes a native, and runs past the iteration cap of ` +
|
|
2636
|
+
`${analysis.caps.iterations} iterations, so the iterations after it are not analyzed`,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
/** Whether a value could be a string: unknown, or a string constant. */
|
|
2640
|
+
const maybeString = (value) => opaque(value) || isString(value);
|
|
2641
|
+
const isString = (value) => value.kind === 'constant' && typeof value.value === 'string';
|
|
2642
|
+
/** Whether an expression plainly makes a string: a template literal, or a `+` with a string operand. */
|
|
2643
|
+
function stringValued(node) {
|
|
2644
|
+
if (node.type === 'TemplateLiteral')
|
|
2645
|
+
return true;
|
|
2646
|
+
if (node.type === 'Literal')
|
|
2647
|
+
return typeof node['value'] === 'string';
|
|
2648
|
+
if (node.type !== 'BinaryExpression' || node['operator'] !== '+')
|
|
2649
|
+
return false;
|
|
2650
|
+
return stringValued(node['left']) || stringValued(node['right']);
|
|
2651
|
+
}
|
|
2652
|
+
/** A bigint literal, whose value ToString converts to its decimal digits. */
|
|
2653
|
+
const isBigIntLiteral = (node) => node.type === 'Literal' && typeof node['value'] === 'bigint';
|
|
2654
|
+
/** A number, boolean or null literal. */
|
|
2655
|
+
function isPrimitiveLiteral(node) {
|
|
2656
|
+
if (node.type !== 'Literal')
|
|
2657
|
+
return false;
|
|
2658
|
+
const value = node['value'];
|
|
2659
|
+
return value === null || typeof value === 'number' || typeof value === 'boolean';
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Whether `node`, a child of `parent`, gets its own `this`: a non-arrow function, a class field's initializer or a
|
|
2663
|
+
* static block. A class's `extends` clause and computed keys, and arrow functions, see the `this` around them.
|
|
2664
|
+
*/
|
|
2665
|
+
function isThisBoundary(node, parent) {
|
|
2666
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'StaticBlock')
|
|
2667
|
+
return true;
|
|
2668
|
+
return (parent.type === 'PropertyDefinition' || parent.type === 'AccessorProperty') && parent['value'] === node;
|
|
2669
|
+
}
|
|
2670
|
+
/** The `binding` trace step (§14) for a leaf a write reached its holder from: how its name resolved, and its form. */
|
|
2671
|
+
function bindingStep(leaf, context) {
|
|
2672
|
+
const { global, realm, starts } = context;
|
|
2673
|
+
const { name, form, holds, path } = leaf;
|
|
2674
|
+
const alias = form !== 'global-property';
|
|
2675
|
+
// A singleton step is an alias of the singleton; the other alias forms are aliases of the global object.
|
|
2676
|
+
const aliasOf = form === 'singleton' ? holds : global;
|
|
2677
|
+
const declared = leaf.binding?.declaration;
|
|
2678
|
+
const declaration = declared && { kind: declared.kind, generated: locate(starts, declared.node.start, declared.node.end) };
|
|
2679
|
+
const chain = leaf.chain && locate(starts, leaf.chain.start, leaf.chain.end);
|
|
2680
|
+
const selected = chain === undefined ? '' : `, selected through the \`${chainOperator(leaf.chain)}\` chain at ${chain.line}:${chain.column}`;
|
|
2681
|
+
return {
|
|
2682
|
+
kind: 'binding',
|
|
2683
|
+
name,
|
|
2684
|
+
resolution: declaration !== undefined ? 'declaration' : alias ? 'alias' : 'global',
|
|
2685
|
+
form,
|
|
2686
|
+
...(alias ? { aliasOf } : {}),
|
|
2687
|
+
...(holds === undefined || alias ? {} : { holds }),
|
|
2688
|
+
...(path === undefined ? {} : { path }),
|
|
2689
|
+
...(declaration === undefined ? {} : { declaration }),
|
|
2690
|
+
...(chain === undefined ? {} : { chain }),
|
|
2691
|
+
realm,
|
|
2692
|
+
realmInstance: leaf.instance,
|
|
2693
|
+
text: `${recognized(leaf, global, declaration, starts)}${selected} (${realm} realm, ${leaf.instance} instance)`,
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
/** How a leaf's form reaches its holder, as a sentence without the realm. */
|
|
2697
|
+
function recognized(leaf, global, declaration, starts) {
|
|
2698
|
+
const { name } = leaf;
|
|
2699
|
+
const resolves = declaration === undefined
|
|
2700
|
+
? `\`${name}\` resolves to no declaration`
|
|
2701
|
+
: `\`${name}\` resolves to the ${declarationKinds[declaration.kind]} at ${declaration.generated.line}:` +
|
|
2702
|
+
`${declaration.generated.column}, which ${lexicalKinds.has(declaration.kind) ? "hides the global object's property from bare-name callers" : "binds the global object's property"}`;
|
|
2703
|
+
const globalObject = `the global object ${global}`;
|
|
2704
|
+
switch (leaf.form) {
|
|
2705
|
+
case 'global-property': {
|
|
2706
|
+
const holds = leaf.holds === undefined ? '' : `, which holds the native \`${leaf.holds}\``;
|
|
2707
|
+
return `${resolves}, so it is the global property ${global}.${name}${holds}`;
|
|
2708
|
+
}
|
|
2709
|
+
case 'global-name':
|
|
2710
|
+
return `${resolves}, so it is ${globalObject}`;
|
|
2711
|
+
case 'guarded-global': {
|
|
2712
|
+
const guard = locate(starts, leaf.guard.start, leaf.guard.end);
|
|
2713
|
+
const at = `${guard.line}:${guard.column}`;
|
|
2714
|
+
return `${resolves} and the \`typeof global\` test at ${at} shows it is defined, so it is ${globalObject}`;
|
|
2715
|
+
}
|
|
2716
|
+
case 'top-level-this':
|
|
2717
|
+
return `\`this\` at the top level of a classic script is ${globalObject}`;
|
|
2718
|
+
case 'sloppy-this':
|
|
2719
|
+
return `\`this\` in a sloppy function called without a \`this\`, or with \`null\` or \`undefined\`, is ${globalObject}`;
|
|
2720
|
+
case 'function-return-this':
|
|
2721
|
+
// Through another window's `Function`, after the steps that reached that window.
|
|
2722
|
+
if (leaf.base !== undefined) {
|
|
2723
|
+
return (`\`Function('return this')()\` through that window's \`Function\` calls a new sloppy function without a ` +
|
|
2724
|
+
`\`this\`, which returns that window's global object ${global}`);
|
|
2725
|
+
}
|
|
2726
|
+
return (`${resolves}, and \`Function('return this')()\` through it calls a new sloppy function without a \`this\`, ` +
|
|
2727
|
+
`which returns ${globalObject}`);
|
|
2728
|
+
case 'singleton': {
|
|
2729
|
+
const through = leaf.path === leaf.holds ? '' : ` through the alias \`${leaf.path}\``;
|
|
2730
|
+
return `\`.${name}\` of that object is the platform singleton \`${leaf.holds}\`${through}`;
|
|
2731
|
+
}
|
|
2732
|
+
case 'other-window': {
|
|
2733
|
+
const window = otherWindows[leaf.instance];
|
|
2734
|
+
if (leaf.binding !== undefined)
|
|
2735
|
+
return `${resolves}, so it is the global property ${global}.${name}, which is ${window}`;
|
|
2736
|
+
if (leaf.base === undefined)
|
|
2737
|
+
return `\`.${name}\` on any object is a window, and which window is unknown`;
|
|
2738
|
+
// The code's own `document.defaultView` is its own window.
|
|
2739
|
+
if (name === 'defaultView')
|
|
2740
|
+
return `\`.${name}\` of that document is the window it belongs to, ${window}`;
|
|
2741
|
+
// An index read (`[0]`) on a window is a child window.
|
|
2742
|
+
if (name.startsWith('['))
|
|
2743
|
+
return `the index read \`${name}\` on that window is a child window, ${window}`;
|
|
2744
|
+
return `\`.${name}\` of that window is ${window}`;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
/** The operator of a chain: `?:`, `||`, `&&` or `??`. */
|
|
2749
|
+
function chainOperator(chain) {
|
|
2750
|
+
return chain.type === 'ConditionalExpression' ? '?:' : chain['operator'];
|
|
2751
|
+
}
|
|
2752
|
+
/** The `with` statements and direct `eval` calls the taint steps of a trace locate, each once, in document order. */
|
|
2753
|
+
function taintSites(steps) {
|
|
2754
|
+
const sites = new Map();
|
|
2755
|
+
for (const step of steps) {
|
|
2756
|
+
if (step.kind !== 'confidence-reason' || (step.reason !== 'with-scope' && step.reason !== 'direct-eval-scope'))
|
|
2757
|
+
continue;
|
|
2758
|
+
for (const site of step.sites ?? [])
|
|
2759
|
+
sites.set(site.start, site);
|
|
2760
|
+
}
|
|
2761
|
+
return [...sites.values()].sort((a, b) => a.start - b.start);
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* The `confidence-reason` steps (§14) for the taint on a name's resolution path (§5 post-pass 3), in §9's order:
|
|
2765
|
+
* `direct-eval-scope` when a direct `eval` could declare a `var` that shadows the name, and `with-scope` when a `with`
|
|
2766
|
+
* block could redirect it to an object property.
|
|
2767
|
+
*/
|
|
2768
|
+
function taintSteps(name, taint, starts) {
|
|
2769
|
+
const taints = [
|
|
2770
|
+
{
|
|
2771
|
+
reason: 'direct-eval-scope',
|
|
2772
|
+
nodes: taint.evals,
|
|
2773
|
+
what: 'a direct `eval` in sloppy code',
|
|
2774
|
+
could: 'declare a `var` that shadows it',
|
|
2775
|
+
},
|
|
2776
|
+
{ reason: 'with-scope', nodes: taint.withs, what: 'a `with` block', could: 'redirect it to an object property' },
|
|
2777
|
+
];
|
|
2778
|
+
return taints
|
|
2779
|
+
.filter(({ nodes }) => nodes.length > 0)
|
|
2780
|
+
.map(({ reason, nodes, what, could }) => {
|
|
2781
|
+
const sites = nodes.map((site) => locate(starts, site.start, site.end));
|
|
2782
|
+
const at = sites.map(({ line, column }) => `${line}:${column}`).join(', ');
|
|
2783
|
+
return {
|
|
2784
|
+
kind: 'confidence-reason',
|
|
2785
|
+
dimension: 'target',
|
|
2786
|
+
reason,
|
|
2787
|
+
level: targetReasons[reason],
|
|
2788
|
+
name,
|
|
2789
|
+
sites,
|
|
2790
|
+
text: `${what} on the resolution path of \`${name}\` (at ${at}) could ${could}`,
|
|
2791
|
+
};
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
/** The `outcome` step (§14) of an ineffective write (§3): why it can't change its target, and whether it throws. */
|
|
2795
|
+
function outcomeStep({ name, reason, throws, mayChangeNothing }) {
|
|
2796
|
+
const why = ineffectiveWording[reason];
|
|
2797
|
+
const throwing = throwsWording[throws];
|
|
2798
|
+
const noOp = mayChangeNothing ? `, or it changes nothing, on a value the analyzer can't see` : '';
|
|
2799
|
+
return {
|
|
2800
|
+
kind: 'outcome',
|
|
2801
|
+
result: 'ineffective',
|
|
2802
|
+
reason,
|
|
2803
|
+
throws,
|
|
2804
|
+
mayChangeNothing,
|
|
2805
|
+
text: `the write can't change ${name}, ${why}${noOp}: an ineffective write, which ${throwing}`,
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
const ineffectiveWording = {
|
|
2809
|
+
readonly: 'an accessor without a setter',
|
|
2810
|
+
'lenient-setter': 'whose [LegacyLenientSetter] setter does nothing',
|
|
2811
|
+
'non-writable': 'a non-writable data property',
|
|
2812
|
+
'non-configurable': 'a non-configurable property',
|
|
2813
|
+
'immutable-prototype': 'whose object is an immutable prototype exotic object',
|
|
2814
|
+
};
|
|
2815
|
+
const throwsWording = {
|
|
2816
|
+
always: 'throws in sloppy and strict code alike',
|
|
2817
|
+
'strict-only': 'throws only in strict code',
|
|
2818
|
+
never: 'never throws',
|
|
2819
|
+
};
|
|
2820
|
+
/**
|
|
2821
|
+
* The `confidence-reason` step (§14) for `outcome-undecidable` (§9): what the outcome depends on and the analyzer can't
|
|
2822
|
+
* see or decide (§3 Undecidable outcomes), in source order. That is the parts of a define's descriptor, the object a
|
|
2823
|
+
* `Reflect.set` looks its member up on, or the value an assign of `Window.opener` writes, which may be `null`.
|
|
2824
|
+
*/
|
|
2825
|
+
function undecidableStep(name, { on, sites: parts }, starts) {
|
|
2826
|
+
const sites = parts.map((part) => locate(starts, part.start, part.end));
|
|
2827
|
+
const at = sites.map(({ line, column }) => `${line}:${column}`).join(', ');
|
|
2828
|
+
const why = {
|
|
2829
|
+
descriptor: `the define's descriptor has parts the analyzer can't see or decide (at ${at})`,
|
|
2830
|
+
lookup: `the object \`Reflect.set\` looks the member up on doesn't resolve (at ${at})`,
|
|
2831
|
+
value: `the value written may be \`null\`, which the setter doesn't replace the member with (at ${at})`,
|
|
2832
|
+
}[on];
|
|
2833
|
+
return {
|
|
2834
|
+
kind: 'confidence-reason',
|
|
2835
|
+
dimension: 'mutation',
|
|
2836
|
+
reason: 'outcome-undecidable',
|
|
2837
|
+
level: mutationReasons['outcome-undecidable'],
|
|
2838
|
+
sites,
|
|
2839
|
+
text: `${why}, so whether it changes \`${name}\` can't be decided`,
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
/** The window another window's step reaches, by its realm instance. */
|
|
2843
|
+
const otherWindows = {
|
|
2844
|
+
current: 'this window',
|
|
2845
|
+
other: 'another window',
|
|
2846
|
+
unknown: 'a window, and which window is unknown',
|
|
2847
|
+
};
|
|
2848
|
+
/** The declarations that bind no property of the global object, but hide it from bare-name callers (§3). */
|
|
2849
|
+
const lexicalKinds = new Set(['let', 'const', 'class']);
|
|
2850
|
+
const declarationKinds = {
|
|
2851
|
+
var: "classic script's top-level `var`",
|
|
2852
|
+
function: "classic script's top-level function declaration",
|
|
2853
|
+
'block-function': 'block-level function that Annex B hoists to the global object',
|
|
2854
|
+
let: "classic script's top-level `let`",
|
|
2855
|
+
const: "classic script's top-level `const`",
|
|
2856
|
+
class: "classic script's top-level class declaration",
|
|
2857
|
+
};
|
|
2858
|
+
/**
|
|
2859
|
+
* What a group's realm adds to every write in it (§9, §14): nothing when the realm comes from how the group's entry was
|
|
2860
|
+
* referenced, and `realm-assumed` with its evidence when only the sweep found the entry, so its realm is assumed (§2).
|
|
2861
|
+
*/
|
|
2862
|
+
function realmOf({ realm, realmBasis }) {
|
|
2863
|
+
if (realmBasis === 'reference')
|
|
2864
|
+
return none;
|
|
2865
|
+
const step = {
|
|
2866
|
+
kind: 'confidence-reason',
|
|
2867
|
+
dimension: 'target',
|
|
2868
|
+
reason: 'realm-assumed',
|
|
2869
|
+
level: targetReasons['realm-assumed'],
|
|
2870
|
+
text: `only the build-directory sweep found the artifact, so its realm is assumed to be ${realm}`,
|
|
2871
|
+
};
|
|
2872
|
+
return { steps: [step], reasons: ['realm-assumed'] };
|
|
2873
|
+
}
|
|
2874
|
+
/** The `guard` steps of `steps` that `shown` doesn't already show, each once: the same guard at the same place. */
|
|
2875
|
+
function unique(steps, shown) {
|
|
2876
|
+
const out = [];
|
|
2877
|
+
const key = (step) => `${step.generated.start}:${step.generated.end}:${step.class}`;
|
|
2878
|
+
const seen = new Set(shown.flatMap((step) => (step.kind === 'guard' ? [key(step)] : [])));
|
|
2879
|
+
for (const step of steps) {
|
|
2880
|
+
if (seen.has(key(step)))
|
|
2881
|
+
continue;
|
|
2882
|
+
seen.add(key(step));
|
|
2883
|
+
out.push(step);
|
|
2884
|
+
}
|
|
2885
|
+
return out;
|
|
2886
|
+
}
|