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.
Files changed (221) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +61 -0
  3. package/README.md +183 -2
  4. package/catalog/catalog.json +552211 -0
  5. package/catalog/catalog.schema.json +787 -0
  6. package/catalog/changelog.json +3 -0
  7. package/dist/bin/monkeypatchlist.d.ts +2 -0
  8. package/dist/bin/monkeypatchlist.js +3 -0
  9. package/dist/catalog.d.ts +561 -0
  10. package/dist/catalog.js +58 -0
  11. package/dist/cli.d.ts +84 -0
  12. package/dist/cli.js +470 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +1 -0
  15. package/dist/lockfile-input.d.ts +9 -0
  16. package/dist/lockfile-input.js +91 -0
  17. package/dist/packages/analysis/binding.d.ts +2 -0
  18. package/dist/packages/analysis/binding.js +5 -0
  19. package/dist/packages/analysis/index.d.ts +10 -0
  20. package/dist/packages/analysis/index.js +13 -0
  21. package/dist/packages/analysis/job.d.ts +33 -0
  22. package/dist/packages/analysis/job.js +85 -0
  23. package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
  24. package/dist/packages/analysis/lib/absence-tags.js +340 -0
  25. package/dist/packages/analysis/lib/ast.d.ts +28 -0
  26. package/dist/packages/analysis/lib/ast.js +64 -0
  27. package/dist/packages/analysis/lib/binding.d.ts +36 -0
  28. package/dist/packages/analysis/lib/binding.js +66 -0
  29. package/dist/packages/analysis/lib/budget.d.ts +80 -0
  30. package/dist/packages/analysis/lib/budget.js +31 -0
  31. package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
  32. package/dist/packages/analysis/lib/catch-arms.js +351 -0
  33. package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
  34. package/dist/packages/analysis/lib/chunk-loads.js +882 -0
  35. package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
  36. package/dist/packages/analysis/lib/chunk-registry.js +334 -0
  37. package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
  38. package/dist/packages/analysis/lib/code-routes.js +921 -0
  39. package/dist/packages/analysis/lib/collisions.d.ts +22 -0
  40. package/dist/packages/analysis/lib/collisions.js +103 -0
  41. package/dist/packages/analysis/lib/definitions.d.ts +76 -0
  42. package/dist/packages/analysis/lib/definitions.js +782 -0
  43. package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
  44. package/dist/packages/analysis/lib/early-exits.js +161 -0
  45. package/dist/packages/analysis/lib/effect.d.ts +138 -0
  46. package/dist/packages/analysis/lib/effect.js +800 -0
  47. package/dist/packages/analysis/lib/global-object.d.ts +581 -0
  48. package/dist/packages/analysis/lib/global-object.js +8240 -0
  49. package/dist/packages/analysis/lib/keys.d.ts +33 -0
  50. package/dist/packages/analysis/lib/keys.js +82 -0
  51. package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
  52. package/dist/packages/analysis/lib/line-starts.js +79 -0
  53. package/dist/packages/analysis/lib/link-group.d.ts +51 -0
  54. package/dist/packages/analysis/lib/link-group.js +487 -0
  55. package/dist/packages/analysis/lib/module-links.d.ts +117 -0
  56. package/dist/packages/analysis/lib/module-links.js +1060 -0
  57. package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
  58. package/dist/packages/analysis/lib/nested-code.js +200 -0
  59. package/dist/packages/analysis/lib/numeric.d.ts +16 -0
  60. package/dist/packages/analysis/lib/numeric.js +216 -0
  61. package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
  62. package/dist/packages/analysis/lib/once-flags.js +190 -0
  63. package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
  64. package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
  65. package/dist/packages/analysis/lib/parse.d.ts +10 -0
  66. package/dist/packages/analysis/lib/parse.js +39 -0
  67. package/dist/packages/analysis/lib/partial.d.ts +60 -0
  68. package/dist/packages/analysis/lib/partial.js +518 -0
  69. package/dist/packages/analysis/lib/print.d.ts +3 -0
  70. package/dist/packages/analysis/lib/print.js +150 -0
  71. package/dist/packages/analysis/lib/reachability.d.ts +137 -0
  72. package/dist/packages/analysis/lib/reachability.js +1295 -0
  73. package/dist/packages/analysis/lib/resolver.d.ts +13 -0
  74. package/dist/packages/analysis/lib/resolver.js +2886 -0
  75. package/dist/packages/analysis/lib/restores.d.ts +42 -0
  76. package/dist/packages/analysis/lib/restores.js +482 -0
  77. package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
  78. package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
  79. package/dist/packages/analysis/lib/scope.d.ts +110 -0
  80. package/dist/packages/analysis/lib/scope.js +443 -0
  81. package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
  82. package/dist/packages/analysis/lib/script-loads.js +359 -0
  83. package/dist/packages/analysis/lib/summaries.d.ts +177 -0
  84. package/dist/packages/analysis/lib/summaries.js +498 -0
  85. package/dist/packages/analysis/lib/types.d.ts +632 -0
  86. package/dist/packages/analysis/lib/types.js +128 -0
  87. package/dist/packages/analysis/lib/wrap.d.ts +34 -0
  88. package/dist/packages/analysis/lib/wrap.js +407 -0
  89. package/dist/packages/analysis/lib/writes.d.ts +358 -0
  90. package/dist/packages/analysis/lib/writes.js +1612 -0
  91. package/dist/packages/analysis/visit-order.d.ts +3 -0
  92. package/dist/packages/analysis/visit-order.js +23 -0
  93. package/dist/packages/attribution/index.d.ts +233 -0
  94. package/dist/packages/attribution/index.js +444 -0
  95. package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
  96. package/dist/packages/attribution/lib/containing-package.js +304 -0
  97. package/dist/packages/attribution/lib/first-party.d.ts +37 -0
  98. package/dist/packages/attribution/lib/first-party.js +153 -0
  99. package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
  100. package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
  101. package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
  102. package/dist/packages/attribution/lib/lockfile.js +330 -0
  103. package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
  104. package/dist/packages/attribution/lib/path-rules.js +84 -0
  105. package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
  106. package/dist/packages/attribution/lib/pnpm.js +133 -0
  107. package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
  108. package/dist/packages/attribution/lib/remote-url.js +24 -0
  109. package/dist/packages/attribution/lib/source-map.d.ts +51 -0
  110. package/dist/packages/attribution/lib/source-map.js +125 -0
  111. package/dist/packages/attribution/lib/yaml.d.ts +9 -0
  112. package/dist/packages/attribution/lib/yaml.js +277 -0
  113. package/dist/packages/attribution/lib/yarn.d.ts +7 -0
  114. package/dist/packages/attribution/lib/yarn.js +227 -0
  115. package/dist/packages/baseline/index.d.ts +45 -0
  116. package/dist/packages/baseline/index.js +41 -0
  117. package/dist/packages/baseline/lib/error.d.ts +15 -0
  118. package/dist/packages/baseline/lib/error.js +25 -0
  119. package/dist/packages/baseline/lib/format.d.ts +49 -0
  120. package/dist/packages/baseline/lib/format.js +22 -0
  121. package/dist/packages/baseline/lib/migrations.d.ts +19 -0
  122. package/dist/packages/baseline/lib/migrations.js +7 -0
  123. package/dist/packages/baseline/lib/previous.d.ts +23 -0
  124. package/dist/packages/baseline/lib/previous.js +64 -0
  125. package/dist/packages/baseline/lib/read.d.ts +39 -0
  126. package/dist/packages/baseline/lib/read.js +219 -0
  127. package/dist/packages/baseline/lib/report.d.ts +25 -0
  128. package/dist/packages/baseline/lib/report.js +104 -0
  129. package/dist/packages/baseline/lib/skew.d.ts +51 -0
  130. package/dist/packages/baseline/lib/skew.js +75 -0
  131. package/dist/packages/compare/index.d.ts +120 -0
  132. package/dist/packages/compare/index.js +274 -0
  133. package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
  134. package/dist/packages/compare/lib/catalog-notes.js +224 -0
  135. package/dist/packages/completeness/index.d.ts +246 -0
  136. package/dist/packages/completeness/index.js +249 -0
  137. package/dist/packages/confidence/index.d.ts +54 -0
  138. package/dist/packages/confidence/index.js +71 -0
  139. package/dist/packages/config/index.d.ts +107 -0
  140. package/dist/packages/config/index.js +95 -0
  141. package/dist/packages/config/lib/command-line.d.ts +23 -0
  142. package/dist/packages/config/lib/command-line.js +239 -0
  143. package/dist/packages/config/lib/config-file.d.ts +11 -0
  144. package/dist/packages/config/lib/config-file.js +119 -0
  145. package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
  146. package/dist/packages/config/lib/scan-config-id.js +54 -0
  147. package/dist/packages/config/lib/schema.d.ts +2 -0
  148. package/dist/packages/config/lib/schema.js +31 -0
  149. package/dist/packages/config/lib/types.d.ts +64 -0
  150. package/dist/packages/config/lib/types.js +6 -0
  151. package/dist/packages/config/lib/urls.d.ts +6 -0
  152. package/dist/packages/config/lib/urls.js +9 -0
  153. package/dist/packages/discovery/index.d.ts +277 -0
  154. package/dist/packages/discovery/index.js +1950 -0
  155. package/dist/packages/discovery/lib/import-map.d.ts +68 -0
  156. package/dist/packages/discovery/lib/import-map.js +180 -0
  157. package/dist/packages/discovery/lib/requests.d.ts +58 -0
  158. package/dist/packages/discovery/lib/requests.js +591 -0
  159. package/dist/packages/discovery/lib/scopes.d.ts +6 -0
  160. package/dist/packages/discovery/lib/scopes.js +286 -0
  161. package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
  162. package/dist/packages/discovery/lib/source-maps.js +209 -0
  163. package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
  164. package/dist/packages/discovery/lib/swept-code.js +107 -0
  165. package/dist/packages/discovery/lib/syntax.d.ts +17 -0
  166. package/dist/packages/discovery/lib/syntax.js +39 -0
  167. package/dist/packages/findings/index.d.ts +106 -0
  168. package/dist/packages/findings/index.js +254 -0
  169. package/dist/packages/findings/lib/scope.d.ts +38 -0
  170. package/dist/packages/findings/lib/scope.js +110 -0
  171. package/dist/packages/parallel/index.d.ts +2 -0
  172. package/dist/packages/parallel/index.js +5 -0
  173. package/dist/packages/parallel/lib/protocol.d.ts +15 -0
  174. package/dist/packages/parallel/lib/protocol.js +2 -0
  175. package/dist/packages/parallel/lib/runner.d.ts +36 -0
  176. package/dist/packages/parallel/lib/runner.js +96 -0
  177. package/dist/packages/parallel/lib/schedule.d.ts +16 -0
  178. package/dist/packages/parallel/lib/schedule.js +52 -0
  179. package/dist/packages/parallel/lib/worker.d.ts +1 -0
  180. package/dist/packages/parallel/lib/worker.js +18 -0
  181. package/dist/packages/policy/index.d.ts +36 -0
  182. package/dist/packages/policy/index.js +22 -0
  183. package/dist/packages/report/explain.d.ts +26 -0
  184. package/dist/packages/report/explain.js +99 -0
  185. package/dist/packages/report/index.d.ts +210 -0
  186. package/dist/packages/report/index.js +388 -0
  187. package/dist/packages/report/terminal.d.ts +11 -0
  188. package/dist/packages/report/terminal.js +331 -0
  189. package/dist/packages/source-store/fixture.d.ts +22 -0
  190. package/dist/packages/source-store/fixture.js +58 -0
  191. package/dist/packages/source-store/index.d.ts +278 -0
  192. package/dist/packages/source-store/index.js +322 -0
  193. package/dist/packages/source-store/lib/addresses.d.ts +6 -0
  194. package/dist/packages/source-store/lib/addresses.js +91 -0
  195. package/dist/packages/source-store/lib/connection.d.ts +15 -0
  196. package/dist/packages/source-store/lib/connection.js +135 -0
  197. package/dist/packages/source-store/lib/content-types.d.ts +16 -0
  198. package/dist/packages/source-store/lib/content-types.js +75 -0
  199. package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
  200. package/dist/packages/source-store/lib/data-urls.js +85 -0
  201. package/dist/packages/source-store/lib/deadline.d.ts +18 -0
  202. package/dist/packages/source-store/lib/deadline.js +44 -0
  203. package/dist/packages/source-store/lib/exchange.d.ts +29 -0
  204. package/dist/packages/source-store/lib/exchange.js +112 -0
  205. package/dist/packages/source-store/lib/integrity.d.ts +19 -0
  206. package/dist/packages/source-store/lib/integrity.js +86 -0
  207. package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
  208. package/dist/packages/source-store/lib/outcomes.js +22 -0
  209. package/dist/packages/source-store/lib/proxy.d.ts +11 -0
  210. package/dist/packages/source-store/lib/proxy.js +53 -0
  211. package/dist/packages/source-store/lib/session.d.ts +12 -0
  212. package/dist/packages/source-store/lib/session.js +139 -0
  213. package/dist/packages/source-store/lib/urls.d.ts +6 -0
  214. package/dist/packages/source-store/lib/urls.js +15 -0
  215. package/dist/packages/source-store/resolve.d.ts +59 -0
  216. package/dist/packages/source-store/resolve.js +45 -0
  217. package/dist/scan.d.ts +65 -0
  218. package/dist/scan.js +592 -0
  219. package/package.json +95 -4
  220. package/schema/config.schema.json +133 -0
  221. package/schema/report.schema.json +1905 -0
@@ -0,0 +1,882 @@
1
+ // Chunk loads (§2 Discovery, §6 idiom 6, §19 References only the resolver can see): the chunk URLs a chunk loader
2
+ // builds, folded from constants where they fold.
3
+ //
4
+ // - A **URL function** is a function of one parameter that builds a script URL by string concatenation (webpack's
5
+ // `n.u`): its id table gives the chunk ids it maps.
6
+ // - A **load site** hands a URL expression that calls a URL function to the browser: a created script element's `src`
7
+ // (`a.src = s`, a JSONP loader), `importScripts(…)` (a worker's loader) or `import(…)` (an ESM chunk loader).
8
+ // - Its URL is folded once per chunk id the URL function maps, under ECMA-262's `+` for strings and numbers (Principle
9
+ // 2). A name folds to the values its reaching definitions give, and a member `o.p` to the values every plain
10
+ // assignment `o.p = v` to the same variable `o` gives. A plain parameter folds to the arguments every call of its
11
+ // function the code shows passes it, so a URL handed to webpack's script-loading helper (`n.l(url, done, key, id)`)
12
+ // folds at its callers. Anything else is unknown. Each constant alternative is a chunk URL, and an unknown alternative
13
+ // leaves only the URL's folded constant prefix, which decides whether a URL that doesn't fold is local (§2).
14
+ // - A created script element's **integrity** (`o.integrity = u[a]`, `u` a constant table) is folded for each chunk id
15
+ // as its URL is, the variable the URL function was given standing for the id: a value that folds to one constant is
16
+ // the chunk reference's integrity, and any other is none (§17 Integrity).
17
+ //
18
+ // Chunk ids are build-local: they decide which URLs are references, never identity (§6 Reporting).
19
+ import { walkTree } from "./ast.js";
20
+ import { definitionsOf, shadowedNameOf } from "./definitions.js";
21
+ import { literalKey } from "./keys.js";
22
+ import { lineStarts, locate } from "./line-starts.js";
23
+ import { parse } from "./parse.js";
24
+ import { scopeRules } from "./scope.js";
25
+ /** How many alternatives a URL may fold to before it counts as unknown. */
26
+ const maxAlternatives = 64;
27
+ /** The chunk load sites of an artifact's code (above). None when it doesn't parse, or has no URL function. */
28
+ export function chunkLoadSites(text, sourceType, bindings) {
29
+ if (!mayLoadChunks(text))
30
+ return [];
31
+ const parsed = parse(text, sourceType);
32
+ if ('error' in parsed)
33
+ return [];
34
+ const { program } = parsed;
35
+ const candidates = chunkCandidates(program);
36
+ if (candidates.functions.size === 0 || candidates.sites.length === 0)
37
+ return [];
38
+ const definitions = definitionsOf(program, scopeRules(program, sourceType, bindings));
39
+ return foldChunkSites(candidates, new Folder(program, definitions, candidates.functions), lineStarts(text));
40
+ }
41
+ /** A cheap test first: a URL function concatenates a script URL, and a load site names one of these. */
42
+ export const mayLoadChunks = (text) => /\.[cm]?js/i.test(text) && /\.src\b|importScripts|import\s*\(/.test(text);
43
+ /** A program's URL functions, and the load sites whose URL expressions may call one. */
44
+ export function chunkCandidates(program) {
45
+ const functions = new Map();
46
+ const sites = [];
47
+ walkTree(program, {
48
+ enter(node) {
49
+ if (functionTypes.has(node.type)) {
50
+ const table = urlFunctionIds(node);
51
+ if (table !== undefined)
52
+ functions.set(node, table);
53
+ }
54
+ else if (node.type === 'ImportExpression') {
55
+ const source = node['source'];
56
+ if (!isLiteral(source))
57
+ sites.push({ load: 'import', url: source });
58
+ }
59
+ else if (node.type === 'CallExpression') {
60
+ const callee = node['callee'];
61
+ const [first] = node['arguments'];
62
+ const named = (callee.type === 'Identifier' && callee['name'] === 'importScripts') ||
63
+ (callee.type === 'MemberExpression' && propertyName(callee) === 'importScripts');
64
+ if (named && first !== undefined && first.type !== 'SpreadElement' && !isLiteral(first)) {
65
+ sites.push({ load: 'import-scripts', url: first });
66
+ }
67
+ }
68
+ else if (node.type === 'AssignmentExpression' && node['operator'] === '=') {
69
+ const left = node['left'];
70
+ if (left.type === 'MemberExpression' && propertyName(left) === 'src') {
71
+ sites.push({ load: 'element', url: node['right'], object: left['object'] });
72
+ }
73
+ }
74
+ },
75
+ });
76
+ return { functions, sites };
77
+ }
78
+ /** The chunk load sites among a program's candidates, folded with `folder`, located through `starts`. */
79
+ export function foldChunkSites({ functions, sites }, folder, starts) {
80
+ if (functions.size === 0)
81
+ return [];
82
+ const found = [];
83
+ for (const { load, url, object } of sites) {
84
+ if (object !== undefined && !folder.isScriptElement(object))
85
+ continue;
86
+ const folded = folder.chunkUrls(url, object);
87
+ if (folded === undefined)
88
+ continue;
89
+ found.push({
90
+ load,
91
+ location: locate(starts, url.start, url.end),
92
+ urls: folded.urls,
93
+ ...(folded.prefix === undefined ? {} : { prefix: folded.prefix }),
94
+ ...(folded.integrity === undefined ? {} : { integrity: folded.integrity }),
95
+ });
96
+ }
97
+ return found;
98
+ }
99
+ const functionTypes = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
100
+ /** Whether a call creates a script element: `X.createElement("script")`, the tag name in any ASCII case. */
101
+ export function isScriptCreation(call) {
102
+ const callee = call['callee'];
103
+ const [tag] = call['arguments'];
104
+ return (callee.type === 'MemberExpression' &&
105
+ propertyName(callee) === 'createElement' &&
106
+ tag?.type === 'Literal' &&
107
+ typeof tag['value'] === 'string' &&
108
+ tag['value'].toLowerCase() === 'script');
109
+ }
110
+ const isLiteral = (node) => (node.type === 'Literal' && typeof node['value'] === 'string') ||
111
+ (node.type === 'TemplateLiteral' && node['expressions'].length === 0);
112
+ /** A member expression's constant name: `o.x` or `o["x"]`. */
113
+ export function propertyName(node) {
114
+ const property = node['property'];
115
+ if (node['computed'] !== true)
116
+ return property.type === 'Identifier' ? property['name'] : undefined;
117
+ return property.type === 'Literal' && typeof property['value'] === 'string' ? property['value'] : undefined;
118
+ }
119
+ /** The names code calls the global object by. */
120
+ const globalNames = new Set(['window', 'self', 'globalThis']);
121
+ const scriptUrl = /\.[cm]?js(?:$|[?#])/i;
122
+ /**
123
+ * The ids a URL function maps, 'any' when it maps every id, or undefined when the function is none: it has one plain
124
+ * parameter, and its body concatenates a string literal naming a `.js`, `.mjs` or `.cjs` file and a use of that
125
+ * parameter. Each object literal it indexes with the parameter and no fallback maps only its constant keys.
126
+ */
127
+ function urlFunctionIds(fn) {
128
+ const params = fn['params'];
129
+ if (params.length !== 1 || params[0].type !== 'Identifier')
130
+ return undefined;
131
+ const param = params[0]['name'];
132
+ let script = false;
133
+ let used = false;
134
+ const constraints = [];
135
+ walkTree(fn['body'], {
136
+ enter(node, parent) {
137
+ if (node !== fn['body'] && functionTypes.has(node.type))
138
+ return false;
139
+ if (node.type === 'Literal' && typeof node['value'] === 'string' && scriptUrl.test(node['value']))
140
+ script = true;
141
+ if (node.type === 'Identifier' && node['name'] === param && !(parent?.type === 'MemberExpression' && parent['property'] === node && parent['computed'] !== true)) {
142
+ used = true;
143
+ }
144
+ if (node.type === 'MemberExpression' && node['computed'] === true) {
145
+ const property = node['property'];
146
+ const object = node['object'];
147
+ const fallback = parent?.type === 'LogicalExpression' && parent['left'] === node;
148
+ if (property.type === 'Identifier' && property['name'] === param && object.type === 'ObjectExpression' && !fallback) {
149
+ const keys = objectKeys(object);
150
+ if (keys !== undefined)
151
+ constraints.push(keys);
152
+ }
153
+ }
154
+ return undefined;
155
+ },
156
+ });
157
+ if (!script || !used)
158
+ return undefined;
159
+ if (constraints.length === 0)
160
+ return 'any';
161
+ const [first, ...rest] = constraints;
162
+ return new Set(first.filter((id) => rest.every((keys) => keys.includes(id))));
163
+ }
164
+ /** An object literal's constant keys, or undefined when one isn't constant. */
165
+ function objectKeys(object) {
166
+ const keys = [];
167
+ for (const property of object['properties']) {
168
+ if (property.type !== 'Property' || property['computed'] === true)
169
+ return undefined;
170
+ const key = property['key'];
171
+ const id = key.type === 'Identifier' ? key['name'] : literalKey(key);
172
+ if (id === undefined)
173
+ return undefined;
174
+ keys.push(id);
175
+ }
176
+ return keys;
177
+ }
178
+ const unknown = { values: [], unknown: true };
179
+ /** How deep a fold follows names, members and URL functions. */
180
+ const maxDepth = 12;
181
+ /**
182
+ * Folds URL expressions in one artifact's program (above), and the URLs code builds for the browser to load (§2 Computed
183
+ * URLs): besides string concatenation, templates, `new URL(x, base)` for a folded `x` and a base the scan knows,
184
+ * with its `.href` and `toString()`, and `import.meta.url`, the artifact's own URL. A base of `location` or
185
+ * `document.baseURI` is the environment's base URL, which the browser resolves a relative URL against anyway, so `x`
186
+ * folds to itself there. A parameter folds to the arguments every call the program shows passes it, when the
187
+ * function is called by a name or member it is bound to, or where it is written.
188
+ */
189
+ export class Folder {
190
+ /** The plain assignments to each member name, by the object's variable. */
191
+ members;
192
+ /** While a URL is folded, the variables a URL function is given, which hold the chunk id. */
193
+ idKeys;
194
+ /** While an integrity value is folded, the variables that hold the chunk id. */
195
+ ids;
196
+ /** Each node's parent. */
197
+ parents;
198
+ /** The calls of the program, by the name or member name their callee names. */
199
+ calls;
200
+ /** The function declarations of the program, by name. */
201
+ declarations;
202
+ program;
203
+ definitions;
204
+ functions;
205
+ url;
206
+ constructor(program, definitions, functions, url = '') {
207
+ this.program = program;
208
+ this.definitions = definitions;
209
+ this.functions = functions;
210
+ this.url = url;
211
+ }
212
+ /**
213
+ * What a URL expression folds to outside any chunk loader: its constant string alternatives, and whether some
214
+ * alternative is unknown. A chunk id never takes a value here.
215
+ */
216
+ strings(node) {
217
+ const called = new Set();
218
+ const folded = this.fold(node, null, 0, called);
219
+ const values = [...new Set(folded.values.map(String))].sort();
220
+ return { values, unknown: folded.unknown, chunk: called.size > 0 };
221
+ }
222
+ /** The variable an identifier names, when it is a free name of the global object that no code assigns. */
223
+ freeGlobal(node) {
224
+ if (node.type !== 'Identifier')
225
+ return undefined;
226
+ const key = this.definitions.keyOf(node);
227
+ if (key === undefined || key.scope !== 'global' || this.definitions.of(key).length > 0)
228
+ return undefined;
229
+ return key.name;
230
+ }
231
+ /**
232
+ * Whether an expression is the environment's base URL as a base of `new URL` (§2 Computed URLs): `location`,
233
+ * its `href`, or `document.baseURI`, through the global object's names.
234
+ */
235
+ isEnvironmentBase(node) {
236
+ const path = [];
237
+ let at = node;
238
+ while (at.type === 'MemberExpression') {
239
+ const name = propertyName(at);
240
+ if (name === undefined)
241
+ return false;
242
+ path.unshift(name);
243
+ at = at['object'];
244
+ }
245
+ const root = this.freeGlobal(at);
246
+ if (root === undefined)
247
+ return false;
248
+ const names = [root, ...path];
249
+ while (names.length > 1 && globalNames.has(names[0]))
250
+ names.shift();
251
+ const spelt = names.join('.');
252
+ return (spelt === 'location' ||
253
+ spelt === 'location.href' ||
254
+ spelt === 'document.baseURI' ||
255
+ spelt === 'document.location' ||
256
+ spelt === 'document.location.href');
257
+ }
258
+ /**
259
+ * Whether a base is the environment's base URL (`isEnvironmentBase`), also through the one value a name or member is
260
+ * given, or as the left side of `||`, which is never empty there (webpack's `n.b = document.baseURI || …`).
261
+ */
262
+ isBase(node, depth = 0) {
263
+ if (this.isEnvironmentBase(node))
264
+ return true;
265
+ if (depth > maxDepth)
266
+ return false;
267
+ if (node.type === 'LogicalExpression' && node['operator'] === '||')
268
+ return this.isBase(node['left'], depth + 1);
269
+ const one = this.resolveMember(this.resolveName(node));
270
+ return one !== node && this.isBase(one, depth + 1);
271
+ }
272
+ /** Whether an expression is a URL object: `new URL(…)`, or a name whose one definition is one. */
273
+ isUrlObject(node) {
274
+ const resolved = this.resolveName(node);
275
+ return resolved.type === 'NewExpression' && this.freeGlobal(resolved['callee']) === 'URL';
276
+ }
277
+ /** `new URL(x, base)` (URL Standard § URL constructor): each alternative of `x` against each of `base`. */
278
+ newUrl(node, id, depth, called, param) {
279
+ const [input, base] = node['arguments'];
280
+ if (input === undefined || input.type === 'SpreadElement' || base?.type === 'SpreadElement')
281
+ return unknown;
282
+ const x = this.fold(input, id, depth + 1, called, param);
283
+ const strings = x.values.map(String);
284
+ if (base === undefined || (base.type === 'Identifier' && base['name'] === 'undefined' && this.freeGlobal(base) !== undefined)) {
285
+ // With no base, a relative URL throws, which gives nothing.
286
+ return { values: strings.flatMap((value) => URL.parse(value)?.href ?? []), unknown: x.unknown };
287
+ }
288
+ if (this.isBase(base)) {
289
+ return { values: strings.map((value) => URL.parse(value)?.href ?? value), unknown: x.unknown };
290
+ }
291
+ const bases = this.fold(base, id, depth + 1, called, param);
292
+ const values = new Set();
293
+ for (const b of bases.values)
294
+ for (const value of strings) {
295
+ const href = URL.parse(value, String(b))?.href;
296
+ if (href !== undefined)
297
+ values.add(href);
298
+ }
299
+ if (values.size > maxAlternatives)
300
+ return unknown;
301
+ return { values: [...values], unknown: x.unknown || bases.unknown };
302
+ }
303
+ /**
304
+ * The arguments the calls of a parameter's function pass it, or undefined when the parameter is none, or no call is
305
+ * shown, or one spreads before it or passes none (`callsOf`).
306
+ */
307
+ parameterArguments(key) {
308
+ const fn = key.scope;
309
+ if (fn === 'global' || !functionTypes.has(fn.type))
310
+ return undefined;
311
+ const index = fn['params'].findIndex((param) => param.type === 'Identifier' && param['name'] === key.name);
312
+ if (index < 0)
313
+ return undefined;
314
+ const calls = this.callsOf(fn);
315
+ if (calls.length === 0)
316
+ return undefined;
317
+ const args = [];
318
+ for (const call of calls) {
319
+ const passed = call['arguments'].slice(0, index + 1);
320
+ if (passed.some((arg) => arg.type === 'SpreadElement'))
321
+ return undefined;
322
+ const arg = passed[index];
323
+ if (arg === undefined)
324
+ return undefined;
325
+ args.push(arg);
326
+ }
327
+ return args;
328
+ }
329
+ /**
330
+ * The chunk URLs a load site's URL expression folds to, and the folded prefix of its unknown alternatives, or
331
+ * undefined when it calls no URL function, so that it is no chunk load.
332
+ */
333
+ chunkUrls(url, element) {
334
+ const called = new Set();
335
+ this.fold(url, null, 0, called);
336
+ if (called.size === 0)
337
+ return undefined;
338
+ const ids = new Set();
339
+ let every = false;
340
+ for (const fn of called) {
341
+ const table = this.functions.get(fn);
342
+ if (table === 'any')
343
+ every = true;
344
+ else
345
+ for (const id of table)
346
+ ids.add(id);
347
+ }
348
+ const urls = new Set();
349
+ let open = every;
350
+ // Each URL's integrity value, or null when two chunk ids give it different ones, or one doesn't fold.
351
+ const integrity = new Map();
352
+ const integrities = element === undefined ? [] : this.integrityWrites(element);
353
+ for (const id of [...ids].sort()) {
354
+ const idKeys = [];
355
+ this.idKeys = idKeys;
356
+ const folded = this.fold(url, id, 0, new Set());
357
+ this.idKeys = undefined;
358
+ for (const value of folded.values)
359
+ if (typeof value === 'string')
360
+ urls.add(value);
361
+ if (folded.unknown || folded.values.some((value) => typeof value !== 'string'))
362
+ open = true;
363
+ if (integrities.length === 0)
364
+ continue;
365
+ // The chunk id the URL function was given is the id here: `u[a]` reads the table at it.
366
+ this.ids = idKeys;
367
+ const value = union(integrities.map((right) => this.fold(right, id, 0, new Set())));
368
+ this.ids = undefined;
369
+ const one = !value.unknown && value.values.length === 1 && typeof value.values[0] === 'string' ? value.values[0] : null;
370
+ for (const each of folded.values) {
371
+ if (typeof each !== 'string' || (value.values.length === 0 && !value.unknown))
372
+ continue;
373
+ const before = integrity.get(each);
374
+ integrity.set(each, before === undefined || before === one ? one : null);
375
+ }
376
+ }
377
+ const result = { urls: [...urls].sort() };
378
+ if (open || ids.size === 0)
379
+ result.prefix = this.prefix(url);
380
+ const set = [...integrity].filter((entry) => entry[1] !== null).sort(([a], [b]) => (a < b ? -1 : 1));
381
+ if (set.length > 0)
382
+ result.integrity = Object.fromEntries(set);
383
+ return result;
384
+ }
385
+ /** What each plain `element.integrity = v` assignment to the same variable as `element` assigns. */
386
+ integrityWrites(element) {
387
+ return (this.assigned(element, 'integrity') ?? []).filter((right) => right !== undefined);
388
+ }
389
+ /** Whether `object` is a script element the code creates: `document.createElement("script")`. */
390
+ isScriptElement(object) {
391
+ return this.elementsOf(object).size > 0;
392
+ }
393
+ /**
394
+ * The script elements the code creates (`X.createElement("script")`, each by its call) that an expression may be:
395
+ * the call itself, or a name whose reaching definitions are, through other names, parentheses, comma sequences and
396
+ * assignments.
397
+ */
398
+ elementsOf(node, depth = 0) {
399
+ const found = new Set();
400
+ if (depth > maxDepth)
401
+ return found;
402
+ switch (node.type) {
403
+ case 'ParenthesizedExpression':
404
+ return this.elementsOf(node['expression'], depth + 1);
405
+ case 'SequenceExpression':
406
+ return this.elementsOf(node['expressions'].at(-1), depth + 1);
407
+ case 'AssignmentExpression':
408
+ return node['operator'] === '=' ? this.elementsOf(node['right'], depth + 1) : found;
409
+ case 'CallExpression':
410
+ if (isScriptCreation(node))
411
+ found.add(node);
412
+ return found;
413
+ case 'Identifier': {
414
+ const key = this.definitions.keyOf(node);
415
+ if (key === undefined)
416
+ return found;
417
+ for (const { source, pattern } of this.definitions.reaching(node, key).definitions) {
418
+ if (source === undefined || pattern.type !== 'Identifier')
419
+ continue;
420
+ for (const element of this.elementsOf(source, depth + 1))
421
+ found.add(element);
422
+ }
423
+ return found;
424
+ }
425
+ default:
426
+ return found;
427
+ }
428
+ }
429
+ /** The constant prefix of a URL expression: its leading `+` operands that fold to one string, joined. */
430
+ prefix(url, depth = 0) {
431
+ let prefix = '';
432
+ for (const operand of operands(this.resolveName(url))) {
433
+ const folded = this.fold(operand, null, 0, new Set());
434
+ if (folded.unknown || folded.values.length !== 1 || typeof folded.values[0] !== 'string') {
435
+ // The operand's own constant prefix, through the one expression a name or member is given.
436
+ const one = this.resolveMember(this.resolveName(operand));
437
+ if (depth < maxDepth && one !== operand)
438
+ prefix += this.prefix(one, depth + 1);
439
+ break;
440
+ }
441
+ prefix += folded.values[0];
442
+ }
443
+ return prefix;
444
+ }
445
+ /** A member `o.p` that one plain assignment gives a value, followed to it. */
446
+ resolveMember(node) {
447
+ if (node.type !== 'MemberExpression')
448
+ return node;
449
+ const name = propertyName(node);
450
+ const assigned = name === undefined ? undefined : this.assigned(node['object'], name);
451
+ return assigned?.length === 1 && assigned[0] !== undefined ? assigned[0] : node;
452
+ }
453
+ /** A name whose one reaching definition is an expression, followed to it: `s` in `var s = n.p + n.u(t)`. */
454
+ resolveName(node, depth = 0) {
455
+ if (node.type !== 'Identifier' || depth > maxDepth)
456
+ return node;
457
+ const key = this.definitions.keyOf(node);
458
+ if (key === undefined)
459
+ return node;
460
+ const reaching = this.definitions.reaching(node, key);
461
+ const [only] = reaching.definitions;
462
+ if (reaching.start && reaching.definitions.length === 0) {
463
+ const args = this.parameterArguments(key);
464
+ return args?.length === 1 ? this.resolveName(args[0], depth + 1) : node;
465
+ }
466
+ if (reaching.start || reaching.definitions.length !== 1 || only.source === undefined || only.pattern.type !== 'Identifier') {
467
+ return node;
468
+ }
469
+ return this.resolveName(only.source, depth + 1);
470
+ }
471
+ /**
472
+ * What `node` folds to with the URL functions it calls given the chunk id `id` (none: unknown), adding every URL
473
+ * function it calls to `called`.
474
+ */
475
+ fold(node, id, depth, called, param) {
476
+ if (depth > maxDepth)
477
+ return unknown;
478
+ switch (node.type) {
479
+ case 'Literal':
480
+ return typeof node['value'] === 'string' || typeof node['value'] === 'number'
481
+ ? { values: [node['value']], unknown: false }
482
+ : unknown;
483
+ case 'TemplateLiteral': {
484
+ // Each substitution is converted with ToString, so a template is the concatenation of its parts.
485
+ const quasis = node['quasis'];
486
+ const expressions = node['expressions'];
487
+ let folded = { values: [''], unknown: false };
488
+ for (const [index, quasi] of quasis.entries()) {
489
+ const cooked = quasi['value'].cooked;
490
+ if (cooked === null)
491
+ return unknown;
492
+ folded = { values: folded.values.map((value) => String(value) + cooked), unknown: folded.unknown };
493
+ const expression = expressions[index];
494
+ if (expression === undefined)
495
+ continue;
496
+ const part = this.fold(expression, id, depth + 1, called, param);
497
+ const values = new Set();
498
+ for (const a of folded.values)
499
+ for (const b of part.values)
500
+ values.add(String(a) + String(b));
501
+ if (values.size > maxAlternatives)
502
+ return unknown;
503
+ folded = { values: [...values], unknown: folded.unknown || part.unknown };
504
+ }
505
+ return folded;
506
+ }
507
+ case 'NewExpression':
508
+ return this.freeGlobal(node['callee']) === 'URL' ? this.newUrl(node, id, depth, called, param) : unknown;
509
+ case 'ParenthesizedExpression':
510
+ return this.fold(node['expression'], id, depth + 1, called, param);
511
+ case 'BinaryExpression': {
512
+ if (node['operator'] !== '+')
513
+ return unknown;
514
+ const left = this.fold(node['left'], id, depth + 1, called, param);
515
+ const right = this.fold(node['right'], id, depth + 1, called, param);
516
+ const values = new Set();
517
+ for (const a of left.values)
518
+ for (const b of right.values)
519
+ values.add(add(a, b));
520
+ if (values.size > maxAlternatives)
521
+ return unknown;
522
+ return { values: [...values], unknown: left.unknown || right.unknown };
523
+ }
524
+ case 'ConditionalExpression': {
525
+ const consequent = this.fold(node['consequent'], id, depth + 1, called, param);
526
+ const alternate = this.fold(node['alternate'], id, depth + 1, called, param);
527
+ return union([consequent, alternate]);
528
+ }
529
+ case 'LogicalExpression': {
530
+ // Inside a URL function, a table lookup with a fallback for the id: the left value when it is truthy.
531
+ if (node['operator'] !== '||' || param === undefined)
532
+ return unknown;
533
+ const left = this.fold(node['left'], id, depth + 1, called, param);
534
+ if (left.unknown)
535
+ return unknown;
536
+ if (left.values.length > 0 && left.values.every(Boolean))
537
+ return left;
538
+ const right = this.fold(node['right'], id, depth + 1, called, param);
539
+ return union([{ values: left.values.filter(Boolean), unknown: false }, right]);
540
+ }
541
+ case 'Identifier': {
542
+ if (param !== undefined && node['name'] === param.name) {
543
+ return param.id === null ? unknown : { values: [param.id], unknown: false };
544
+ }
545
+ const key = this.definitions.keyOf(node);
546
+ if (key === undefined)
547
+ return unknown;
548
+ if (id !== null && this.ids?.some((each) => each.scope === key.scope && each.name === key.name)) {
549
+ return { values: [id], unknown: false };
550
+ }
551
+ const reaching = this.definitions.reaching(node, key);
552
+ const parts = reaching.definitions.map(({ source, pattern, logical }) => source === undefined || pattern.type !== 'Identifier' || logical
553
+ ? unknown
554
+ : this.fold(source, id, depth + 1, called, param));
555
+ // A parameter starts with the argument each call of its function the code shows passes it.
556
+ return union(reaching.start ? [...parts, this.argument(key, id, depth, called)] : parts);
557
+ }
558
+ case 'MemberExpression': {
559
+ const object = node['object'];
560
+ if (param !== undefined && object.type === 'ObjectExpression' && node['computed'] === true) {
561
+ return this.lookup(object, node['property'], param);
562
+ }
563
+ if (node['computed'] === true && this.ids !== undefined)
564
+ return this.tableRead(object, node['property'], id, depth, called);
565
+ const name = propertyName(node);
566
+ if (name === undefined)
567
+ return unknown;
568
+ if (object.type === 'MetaProperty' && name === 'url' && this.url !== '')
569
+ return { values: [this.url], unknown: false };
570
+ if (name === 'href' && this.isUrlObject(object))
571
+ return this.fold(object, id, depth + 1, called, param);
572
+ const assigned = this.assigned(object, name);
573
+ if (assigned === undefined || assigned.length === 0)
574
+ return unknown;
575
+ return union(assigned.map((right) => (right === undefined ? unknown : this.fold(right, id, depth + 1, called, param))));
576
+ }
577
+ case 'CallExpression': {
578
+ const callee = node['callee'];
579
+ if (callee.type === 'MemberExpression' && propertyName(callee) === 'toString' && node['arguments'].length === 0) {
580
+ const object = this.fold(callee['object'], id, depth + 1, called, param);
581
+ return { values: object.values.map(String), unknown: object.unknown };
582
+ }
583
+ const fn = this.urlFunction(callee);
584
+ const [argument] = node['arguments'];
585
+ if (fn === undefined || argument === undefined)
586
+ return unknown;
587
+ called.add(fn);
588
+ if (argument.type === 'Identifier') {
589
+ const key = this.definitions.keyOf(argument);
590
+ if (key !== undefined)
591
+ this.idKeys?.push(key);
592
+ }
593
+ // Outside a chunk id's fold, a call whose argument folds to one constant folds at it (`n.u(5)`).
594
+ let given = id;
595
+ if (given === null && this.ids === undefined && this.idKeys === undefined && argument.type !== 'SpreadElement') {
596
+ const folded = this.fold(argument, null, depth + 1, called, param);
597
+ if (!folded.unknown && folded.values.length === 1)
598
+ given = String(folded.values[0]);
599
+ }
600
+ if (given === null)
601
+ return unknown;
602
+ const name = (fn['params'][0])['name'];
603
+ const body = fn['body'];
604
+ const returned = body.type === 'BlockStatement' ? onlyReturn(body) : body;
605
+ if (returned === undefined)
606
+ return unknown;
607
+ return this.fold(returned, given, depth + 1, called, { name, id: given });
608
+ }
609
+ default:
610
+ return unknown;
611
+ }
612
+ }
613
+ /**
614
+ * `t[k]`, where `t` is a variable whose only definition is an object literal of constant keys and `k` folds to one
615
+ * key: the value the literal has there, or `undefined` (no value) when it has none.
616
+ */
617
+ tableRead(object, property, id, depth, called) {
618
+ if (object.type !== 'Identifier')
619
+ return unknown;
620
+ const key = this.definitions.keyOf(object);
621
+ if (key === undefined)
622
+ return unknown;
623
+ const reaching = this.definitions.reaching(object, key);
624
+ const [only] = reaching.definitions;
625
+ if (reaching.start || reaching.definitions.length !== 1 || only.source?.type !== 'ObjectExpression')
626
+ return unknown;
627
+ const folded = this.fold(property, id, depth + 1, called);
628
+ if (folded.unknown || folded.values.length !== 1)
629
+ return unknown;
630
+ const name = String(folded.values[0]);
631
+ for (const entry of only.source['properties']) {
632
+ if (entry.type !== 'Property' || entry['computed'] === true || entry['kind'] !== 'init')
633
+ return unknown;
634
+ const at = entry['key'];
635
+ if ((at.type === 'Identifier' ? at['name'] : literalKey(at)) !== name)
636
+ continue;
637
+ const value = entry['value'];
638
+ return value.type === 'Literal' && typeof value['value'] === 'string' ? { values: [value['value']], unknown: false } : unknown;
639
+ }
640
+ return { values: [], unknown: false };
641
+ }
642
+ /** `{k: v, …}[param]` in a URL function, for the chunk id the parameter holds. */
643
+ lookup(object, property, param) {
644
+ if (property.type !== 'Identifier' || property['name'] !== param.name || param.id === null)
645
+ return unknown;
646
+ for (const entry of object['properties']) {
647
+ if (entry.type !== 'Property' || entry['computed'] === true)
648
+ return unknown;
649
+ const key = entry['key'];
650
+ const id = key.type === 'Identifier' ? key['name'] : literalKey(key);
651
+ if (id !== param.id)
652
+ continue;
653
+ const value = entry['value'];
654
+ return value.type === 'Literal' && (typeof value['value'] === 'string' || typeof value['value'] === 'number')
655
+ ? { values: [value['value']], unknown: false }
656
+ : unknown;
657
+ }
658
+ // A missing key reads `undefined`, which only a fallback can turn into a URL.
659
+ return { values: [], unknown: false };
660
+ }
661
+ /**
662
+ * The start value of the variable `key` when it is a plain parameter of its function: the union of the arguments the
663
+ * function's calls pass it (`n.l(s, …, t)`, webpack's script-loading helper). Unknown for any other variable, a
664
+ * function with no call the code shows, or a call that spreads before it.
665
+ */
666
+ argument(key, id, depth, called) {
667
+ const fn = key.scope;
668
+ if (fn === 'global' || !functionTypes.has(fn.type))
669
+ return unknown;
670
+ const index = fn['params'].findIndex((param) => param.type === 'Identifier' && param['name'] === key.name);
671
+ if (index < 0)
672
+ return unknown;
673
+ const calls = this.callsOf(fn);
674
+ if (calls.length === 0)
675
+ return unknown;
676
+ return union(calls.map((call) => {
677
+ const args = call['arguments'];
678
+ if (args.slice(0, index + 1).some((arg) => arg.type === 'SpreadElement'))
679
+ return unknown;
680
+ const arg = args[index];
681
+ return arg === undefined ? unknown : this.fold(arg, id, depth + 1, called);
682
+ }));
683
+ }
684
+ /**
685
+ * The calls of a function the code shows: an immediate call of it, or a call of the name or member it is bound to
686
+ * (`n.l = (…) => {…}`, `var f = …`, `function f(…) {…}`) that names it alone.
687
+ */
688
+ callsOf(fn) {
689
+ this.parents ??= parentsOf(this.program);
690
+ let at = fn;
691
+ let parent = this.parents.get(at);
692
+ while (parent?.type === 'ParenthesizedExpression')
693
+ parent = this.parents.get((at = parent));
694
+ if (parent === undefined)
695
+ return [];
696
+ if (parent.type === 'CallExpression' && parent['callee'] === at)
697
+ return [parent];
698
+ let name;
699
+ if (fn.type === 'FunctionDeclaration')
700
+ name = fn['id']?.['name'];
701
+ else if (parent.type === 'VariableDeclarator' && parent['init'] === at) {
702
+ const id = parent['id'];
703
+ if (id.type === 'Identifier')
704
+ name = id['name'];
705
+ }
706
+ else if (parent.type === 'AssignmentExpression' && parent['right'] === at) {
707
+ const left = parent['left'];
708
+ name = left.type === 'Identifier' ? left['name'] : left.type === 'MemberExpression' ? propertyName(left) : undefined;
709
+ }
710
+ if (name === undefined)
711
+ return [];
712
+ this.calls ??= callsByName(this.program);
713
+ return (this.calls.get(name) ?? []).filter((call) => this.bound(call['callee']) === fn);
714
+ }
715
+ /** The URL function a callee names: a function bound to a name, or plainly assigned to a member, and nothing else. */
716
+ urlFunction(callee) {
717
+ const fn = this.bound(callee);
718
+ return fn !== undefined && this.functions.has(fn) ? fn : undefined;
719
+ }
720
+ /** The function a callee names: a function bound to a name, or plainly assigned to a member, and nothing else. */
721
+ bound(callee) {
722
+ let sources;
723
+ if (callee.type === 'Identifier') {
724
+ const key = this.definitions.keyOf(callee);
725
+ if (key === undefined)
726
+ return undefined;
727
+ const reaching = this.definitions.reaching(callee, key);
728
+ if (reaching.start && reaching.definitions.length === 0) {
729
+ // A function declaration's binding starts with the function.
730
+ return this.declared(callee['name'], key);
731
+ }
732
+ sources = reaching.definitions.map(({ source }) => source);
733
+ }
734
+ else if (callee.type === 'MemberExpression') {
735
+ const name = propertyName(callee);
736
+ sources = (name === undefined ? undefined : this.assigned(callee['object'], name)) ?? [];
737
+ }
738
+ else
739
+ return undefined;
740
+ if (sources.length !== 1)
741
+ return undefined;
742
+ const [only] = sources;
743
+ return only !== undefined && functionTypes.has(only.type) ? only : undefined;
744
+ }
745
+ /** The function declaration that declares the variable `key`, named `name`, if one does. */
746
+ declared(name, key) {
747
+ const shadowed = shadowedNameOf(key.scope);
748
+ if (shadowed !== undefined)
749
+ return this.functions.has(shadowed) ? shadowed : undefined;
750
+ if (key.scope !== 'global' && this.functions.has(key.scope))
751
+ return key.scope;
752
+ this.declarations ??= functionDeclarations(this.program);
753
+ return (this.declarations.get(name) ?? []).find((fn) => {
754
+ const declared = this.definitions.keyOf(fn['id']);
755
+ return declared !== undefined && declared.scope === key.scope && declared.name === key.name;
756
+ });
757
+ }
758
+ /**
759
+ * What each assignment to `object.name` assigns, when `object` is a variable: undefined for a write that isn't a plain
760
+ * `=`, or undefined for the whole when the object is no variable.
761
+ */
762
+ assigned(object, name) {
763
+ if (object.type !== 'Identifier')
764
+ return undefined;
765
+ const key = this.definitions.keyOf(object);
766
+ if (key === undefined)
767
+ return undefined;
768
+ this.members ??= this.indexMembers();
769
+ return (this.members.get(name) ?? [])
770
+ .filter((write) => write.key.scope === key.scope && write.key.name === key.name)
771
+ .map(({ right }) => right);
772
+ }
773
+ indexMembers() {
774
+ const members = new Map();
775
+ const record = (target, right) => {
776
+ if (target.type !== 'MemberExpression')
777
+ return;
778
+ const object = target['object'];
779
+ const name = propertyName(target);
780
+ if (object.type !== 'Identifier' || name === undefined)
781
+ return;
782
+ const key = this.definitions.keyOf(object);
783
+ if (key === undefined)
784
+ return;
785
+ const list = members.get(name);
786
+ if (list === undefined)
787
+ members.set(name, [{ key, right }]);
788
+ else
789
+ list.push({ key, right });
790
+ };
791
+ walkTree(this.program, {
792
+ enter(node) {
793
+ if (node.type === 'AssignmentExpression') {
794
+ record(node['left'], node['operator'] === '=' ? node['right'] : undefined);
795
+ }
796
+ else if (node.type === 'UpdateExpression')
797
+ record(node['argument'], undefined);
798
+ },
799
+ });
800
+ return members;
801
+ }
802
+ }
803
+ /** ECMA-262's `+` on two primitives that are strings or numbers. */
804
+ const add = (a, b) => typeof a === 'number' && typeof b === 'number' ? a + b : String(a) + String(b);
805
+ function union(parts) {
806
+ const values = new Set();
807
+ let open = false;
808
+ for (const part of parts) {
809
+ open ||= part.unknown;
810
+ for (const value of part.values)
811
+ values.add(value);
812
+ }
813
+ if (values.size > maxAlternatives)
814
+ return unknown;
815
+ return { values: [...values], unknown: open };
816
+ }
817
+ /** The operands of a `+` chain, left to right. */
818
+ function operands(node) {
819
+ if (node.type === 'ParenthesizedExpression')
820
+ return operands(node['expression']);
821
+ if (node.type !== 'BinaryExpression' || node['operator'] !== '+')
822
+ return [node];
823
+ return [...operands(node['left']), ...operands(node['right'])];
824
+ }
825
+ /** The expression a block's only statement returns. */
826
+ function onlyReturn(body) {
827
+ const statements = body['body'];
828
+ if (statements.length !== 1 || statements[0].type !== 'ReturnStatement')
829
+ return undefined;
830
+ return statements[0]['argument'] ?? undefined;
831
+ }
832
+ /** Each node's parent in `program`. */
833
+ function parentsOf(program) {
834
+ const parents = new Map();
835
+ walkTree(program, {
836
+ enter(node, parent) {
837
+ if (parent !== null)
838
+ parents.set(node, parent);
839
+ },
840
+ });
841
+ return parents;
842
+ }
843
+ /** The calls of `program` whose callee is a name or a member with a constant name, by that name. */
844
+ function callsByName(program) {
845
+ const calls = new Map();
846
+ walkTree(program, {
847
+ enter(node) {
848
+ if (node.type !== 'CallExpression')
849
+ return;
850
+ const callee = node['callee'];
851
+ const name = callee.type === 'Identifier' ? callee['name'] : callee.type === 'MemberExpression' ? propertyName(callee) : undefined;
852
+ if (name === undefined)
853
+ return;
854
+ const list = calls.get(name);
855
+ if (list === undefined)
856
+ calls.set(name, [node]);
857
+ else
858
+ list.push(node);
859
+ },
860
+ });
861
+ return calls;
862
+ }
863
+ /** The function declarations of `program`, by name. */
864
+ function functionDeclarations(program) {
865
+ const found = new Map();
866
+ walkTree(program, {
867
+ enter(node) {
868
+ if (node.type !== 'FunctionDeclaration')
869
+ return;
870
+ const id = node['id'];
871
+ if (id === null)
872
+ return;
873
+ const name = id['name'];
874
+ const list = found.get(name);
875
+ if (list === undefined)
876
+ found.set(name, [node]);
877
+ else
878
+ list.push(node);
879
+ },
880
+ });
881
+ return found;
882
+ }