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,107 @@
1
+ // What discovery reads from the code of a file only the sweep found (§2 Sweep, Realm): no reference says how it is
2
+ // loaded or where it runs, so its source type, and the worker markers that make its assumed `window` realm uncertain,
3
+ // come from its syntax. A file only a `<link rel=preload as=script>` names, or only preloads that disagree, and a
4
+ // worker's script whose options can't be read statically take their source type the same way, and any file whose
5
+ // realm is assumed has its worker markers read (§2 Discovery, Files that don't run as referenced, §13
6
+ // `realm-uncertain`). Syntax only.
7
+ import { visitorKeys } from 'oxc-parser';
8
+ import { isNode, parseCode } from "./syntax.js";
9
+ /** The events whose listeners on `self` mark a worker: a service worker's `install` and `fetch`, and `message`. */
10
+ const workerEvents = new Set(['install', 'fetch', 'message']);
11
+ /** The words every worker marker holds: code without them has none, and needs no parse to say so. */
12
+ const markerWords = /\b(?:importScripts|addEventListener)\b/;
13
+ /**
14
+ * The source type of a file no reference loads, and its worker markers in source order. Its source type comes from its
15
+ * syntax (`bySyntax`), and code that parses as neither has no markers.
16
+ */
17
+ export function sweptCode(text) {
18
+ const mayHaveMarkers = markerWords.test(text);
19
+ const { sourceType, program } = bySyntax(text, mayHaveMarkers);
20
+ return { sourceType, markers: program === undefined ? [] : workerMarkers(program) };
21
+ }
22
+ /**
23
+ * The worker markers in code of the source type `sourceType`, in source order: none when it doesn't parse as that type.
24
+ */
25
+ export function workerMarkersAs(text, sourceType) {
26
+ if (!markerWords.test(text))
27
+ return [];
28
+ const parsed = parseCode(text, sourceType);
29
+ return parsed === undefined ? [] : workerMarkers(parsed.program);
30
+ }
31
+ /**
32
+ * The source type of code no reference says how to load, as a sweep-only file's (§2 Sweep): a `module` when it parses
33
+ * only as a module, because it holds an `import` or `export` declaration, `import.meta` or a top-level `await`, and a
34
+ * `script` otherwise. Code that parses as neither is a `script`, which the resolver reports as a `parse-failure`.
35
+ */
36
+ export function sourceTypeBySyntax(text) {
37
+ return bySyntax(text, false).sourceType;
38
+ }
39
+ /** The source type of code by its syntax, and its program as that type when `withProgram` asks for it and it parses. */
40
+ function bySyntax(text, withProgram) {
41
+ // Only these words let a module parse accept what a script parse refuses; every other difference goes the other way.
42
+ const mayBeModule = /\b(?:import|export|await)\b/.test(text);
43
+ if (!mayBeModule && !withProgram)
44
+ return { sourceType: 'script' };
45
+ const script = parseCode(text, 'script');
46
+ const module = script === undefined && mayBeModule ? parseCode(text, 'module') : undefined;
47
+ const parsed = script?.program ?? module?.program;
48
+ const sourceType = module === undefined ? 'script' : 'module';
49
+ return withProgram && parsed !== undefined ? { sourceType, program: parsed } : { sourceType };
50
+ }
51
+ /**
52
+ * The worker markers in a program, in source order. `importScripts` counts only in top-level code, outside every
53
+ * function and class body; a listener on `self` counts anywhere, when its event is named by a literal.
54
+ */
55
+ function workerMarkers(program) {
56
+ const markers = [];
57
+ const stack = [{ node: program, topLevel: true }];
58
+ for (let item = stack.pop(); item !== undefined; item = stack.pop()) {
59
+ const { node, topLevel } = item;
60
+ if (node.type === 'CallExpression') {
61
+ const marker = markerOf(node, topLevel);
62
+ if (marker !== undefined)
63
+ markers.push({ marker, start: node.start, end: node.end });
64
+ }
65
+ const nested = topLevel && !scopes.has(node.type);
66
+ for (const key of visitorKeys[node.type] ?? []) {
67
+ const value = node[key];
68
+ for (const child of Array.isArray(value) ? value : [value]) {
69
+ if (isNode(child))
70
+ stack.push({ node: child, topLevel: nested });
71
+ }
72
+ }
73
+ }
74
+ return markers.sort((a, b) => a.start - b.start);
75
+ }
76
+ /** The nodes whose code does not run at the top level: functions, and class bodies. */
77
+ const scopes = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', 'ClassBody']);
78
+ /** The worker marker a call is, if any. */
79
+ function markerOf(call, topLevel) {
80
+ const callee = call['callee'];
81
+ if (callee.type === 'Identifier')
82
+ return topLevel && callee['name'] === 'importScripts' ? 'importScripts' : undefined;
83
+ if (callee.type !== 'MemberExpression')
84
+ return undefined;
85
+ const object = callee['object'];
86
+ if (object.type !== 'Identifier' || object['name'] !== 'self')
87
+ return undefined;
88
+ if (literal(callee['property'], callee['computed'] === true) !== 'addEventListener')
89
+ return undefined;
90
+ const [event] = call['arguments'];
91
+ const name = event === undefined ? undefined : literal(event, true);
92
+ return name !== undefined && workerEvents.has(name) ? name : undefined;
93
+ }
94
+ /**
95
+ * The string a property key or an argument spells: an identifier's name, when it is not computed, or a string literal
96
+ * or template literal without substitutions.
97
+ */
98
+ function literal(node, computed) {
99
+ if (!computed)
100
+ return node.type === 'Identifier' ? node['name'] : undefined;
101
+ if (node.type === 'Literal' && typeof node['value'] === 'string')
102
+ return node['value'];
103
+ if (node.type === 'TemplateLiteral' && node['expressions'].length === 0) {
104
+ return node['quasis'][0]['value'].cooked ?? undefined;
105
+ }
106
+ return undefined;
107
+ }
@@ -0,0 +1,17 @@
1
+ import { parseSync } from 'oxc-parser';
2
+ export interface Node {
3
+ type: string;
4
+ start: number;
5
+ end: number;
6
+ [key: string]: unknown;
7
+ }
8
+ /**
9
+ * Parses `text` with `sourceType`: its program and module record, or undefined when it doesn't parse, as then nothing
10
+ * of it runs. That includes a classic script with an `import` or `export` declaration, which oxc reports only among the
11
+ * semantic errors §5's options leave out.
12
+ */
13
+ export declare function parseCode(text: string, sourceType: 'script' | 'module'): {
14
+ program: Node;
15
+ module: ReturnType<typeof parseSync>['module'];
16
+ } | undefined;
17
+ export declare function isNode(value: unknown): value is Node;
@@ -0,0 +1,39 @@
1
+ // The oxc parse discovery reads syntax from (§5 Parsing and scope, §19 Discovery): the references in a script or module
2
+ // (`requests.ts`), and the source type and worker markers of a file only the sweep found (`swept-code.ts`). Syntax only.
3
+ import { parseSync, rawTransferSupported } from 'oxc-parser';
4
+ /** Raw transfer where the platform supports it (§5 Parsing and scope). The option is not in oxc's typings. */
5
+ const rawTransfer = rawTransferSupported();
6
+ /** The parser options §5 fixes, with the `sourceType` always explicit, as the resolver parses. */
7
+ const parserOptions = (sourceType) => ({
8
+ lang: 'js',
9
+ sourceType,
10
+ range: true,
11
+ preserveParens: false,
12
+ experimentalRawTransfer: rawTransfer,
13
+ });
14
+ /** The import and export declarations, which a classic script cannot hold. */
15
+ const moduleDeclarations = new Set([
16
+ 'ImportDeclaration',
17
+ 'ExportAllDeclaration',
18
+ 'ExportNamedDeclaration',
19
+ 'ExportDefaultDeclaration',
20
+ ]);
21
+ /**
22
+ * Parses `text` with `sourceType`: its program and module record, or undefined when it doesn't parse, as then nothing
23
+ * of it runs. That includes a classic script with an `import` or `export` declaration, which oxc reports only among the
24
+ * semantic errors §5's options leave out.
25
+ */
26
+ export function parseCode(text, sourceType) {
27
+ const parsed = parseSync('artifact.js', text, parserOptions(sourceType));
28
+ if (parsed.errors.length > 0)
29
+ return undefined;
30
+ const program = parsed.program;
31
+ // Import and export declarations can only be top-level statements.
32
+ if (sourceType === 'script' && program['body'].some(({ type }) => moduleDeclarations.has(type))) {
33
+ return undefined;
34
+ }
35
+ return { program, module: parsed.module };
36
+ }
37
+ export function isNode(value) {
38
+ return typeof value === 'object' && value !== null && typeof value.type === 'string';
39
+ }
@@ -0,0 +1,106 @@
1
+ import { type SerializedCatalog, type Surface } from '../../catalog.ts';
2
+ import type { Effect, IneffectiveReason, InstallContext, Limitation, Locks, Reachability, ReachabilityBasis, Target } from '../analysis/index.ts';
3
+ import { type AttributedIneffectiveWrite, type AttributedOccurrence, type Attribution, type PackageVersion } from '../attribution/index.ts';
4
+ import { type Confidence } from '../confidence/index.ts';
5
+ import type { SurfaceScope } from '../config/index.ts';
6
+ import { protectedSurface, type IsProtected, type ScopeTarget } from './lib/scope.ts';
7
+ export { protectedSurface, type IsProtected, type ScopeTarget };
8
+ /** The fingerprint scheme version (§11), recorded in the report's `tool` block and the baseline header. */
9
+ export declare const fingerprintScheme = 1;
10
+ /** An occurrence of a finding: everything but the finding-level fields. */
11
+ export type Occurrence = Omit<AttributedOccurrence, 'target' | 'effect' | 'locks' | 'attribution'>;
12
+ /** One platform mutation, identified by its fingerprint, with every site that makes it (§8). */
13
+ export interface Finding {
14
+ /** `(realm, target, effect, attribution key)` as a readable string: `window|Window.fetch|wrap|pkg:@sentry/core`. */
15
+ fingerprint: string;
16
+ /** `(realm, target, effect)`, written as the fingerprint is: `window|Window.fetch|wrap`. */
17
+ targetKey: string;
18
+ target: Target;
19
+ surface: Surface;
20
+ effect: Effect;
21
+ /** The most-locking occurrence's (§8): `true` over `unknown` over `false`. */
22
+ locks: Locks;
23
+ attribution: Attribution;
24
+ /**
25
+ * The union of the occurrences' introducing dependencies (§10 Record), sorted: absent when none of them has any.
26
+ * Evidence only: never in the fingerprint, the baseline or `changed`.
27
+ */
28
+ introducedBy?: string[];
29
+ /** The most-reaching occurrence's (§7, §8): `unconditional` over `conditional` over `unknown` over absence. */
30
+ reachability: Reachability;
31
+ /**
32
+ * The basis of the occurrences that give its reachability (§7): `call-path` when any of them is `call-path`, else
33
+ * `creation-site`.
34
+ */
35
+ reachabilityBasis: ReachabilityBasis;
36
+ /** `load` if any occurrence is `load`, else `deferred` (§7 Install context, §11). */
37
+ installContext: InstallContext;
38
+ /** The highest level per dimension across the occurrences (§9). */
39
+ confidence: Confidence;
40
+ occurrences: Occurrence[];
41
+ }
42
+ /**
43
+ * The `packages` map (§10 Version, §11 Baseline): each containing package of a finding, by name in code-unit order,
44
+ * with its strong version (store path or lockfile) and that version's source. Only the occurrences of findings keyed on
45
+ * the package count. A name with no strong version gets no entry, and nor does one with more than one strong version in
46
+ * the scan. One version from two strong sources takes the stronger.
47
+ */
48
+ export declare function packageVersions(findings: readonly Pick<Finding, 'attribution' | 'occurrences'>[]): Record<string, PackageVersion>;
49
+ /**
50
+ * Merges occurrences into findings by fingerprint, and rolls their confidence (§9), `locks` (§8), reachability and install context (§7)
51
+ * up. Findings are sorted by fingerprint, and each finding's occurrences by artifact URL, then position, then artifact
52
+ * hash: one key URL reached through two fetched URLs is an artifact of each (§11).
53
+ */
54
+ export declare function buildFindings(occurrences: readonly AttributedOccurrence[], catalog: SerializedCatalog): Finding[];
55
+ /** An occurrence of an ineffective write: everything but the record-level fields. */
56
+ export type IneffectiveOccurrence = Omit<AttributedIneffectiveWrite, 'target' | 'reason'>;
57
+ /**
58
+ * The ineffective writes to one target for one reason (§3 Record): never a finding, so no fingerprint, baseline state
59
+ * or effect of its own. Each occurrence carries its intended effect, mechanism, strictness and `throws`.
60
+ */
61
+ export interface IneffectiveWriteRecord {
62
+ target: Target;
63
+ /** The target's surface, as a finding's (§12): records follow `--surface` as findings do. */
64
+ surface: Surface;
65
+ reason: IneffectiveReason;
66
+ occurrences: IneffectiveOccurrence[];
67
+ }
68
+ /**
69
+ * Groups the occurrences of ineffective writes per `(realm, target, reason)` (§3 Record). Records are sorted by realm,
70
+ * target and reason, and each record's occurrences by artifact URL, then position, then artifact hash.
71
+ */
72
+ export declare function buildIneffectiveWrites(writes: readonly AttributedIneffectiveWrite[], catalog: SerializedCatalog): IneffectiveWriteRecord[];
73
+ /** The target key `(realm, target, effect)` (§11), written as the fingerprint is: `window|Window.fetch|wrap`. */
74
+ export declare function targetKey(target: Pick<Target, 'realm' | 'name'>, effect: Effect): string;
75
+ /** Orders strings by UTF-16 code unit, independent of locale. */
76
+ export declare function compareStrings(a: string, b: string): number;
77
+ /** How many findings a `protected` scan leaves out, per surface (§12). Every surface is listed, in `surfaces` order. */
78
+ export type OutOfScopeBySurface = Record<Surface, number>;
79
+ /**
80
+ * A record that may name the targets it could be about (§5 Partially resolved writes): an `unresolved-holder` record's
81
+ * candidate targets, which analysis returns as data for this stage to filter. Its `detail` ends with their names,
82
+ * joined by `, `.
83
+ */
84
+ export type ScopedLimitation<R extends Pick<Limitation, 'detail'>> = R & {
85
+ candidates?: readonly ScopeTarget[];
86
+ };
87
+ /**
88
+ * Applies `--surface` (§12) to what a scan reports. `all` keeps everything. `protected` keeps:
89
+ * - the findings the protected surface covers, and counts the others per surface
90
+ * - the ineffective-write records it covers, matched as findings are, through `shadows` (§3 Record): a record is kept
91
+ * when its target is covered with the intended effect of any of its occurrences
92
+ * - each record with candidate targets (`unresolved-holder`) when at least one candidate is covered, with its `detail`
93
+ * listing only the covered ones
94
+ *
95
+ * A record's `candidates` never cross into the output.
96
+ */
97
+ export declare function applySurface<R extends Pick<Limitation, 'detail'>>(input: {
98
+ findings: readonly Finding[];
99
+ ineffectiveWrites: readonly IneffectiveWriteRecord[];
100
+ limitations: readonly ScopedLimitation<R>[];
101
+ }, scope: SurfaceScope, catalog: SerializedCatalog): {
102
+ findings: Finding[];
103
+ outOfScopeBySurface: OutOfScopeBySurface;
104
+ ineffectiveWrites: IneffectiveWriteRecord[];
105
+ limitations: R[];
106
+ };
@@ -0,0 +1,254 @@
1
+ // Findings (§19, module 6): computes each occurrence's fingerprint and target key (§11), and merges occurrences into
2
+ // findings by fingerprint. It knows nothing of the baseline.
3
+ import { ecmascriptSpecs, surfaces } from "../../catalog.js";
4
+ import { strongVersionSources, versionSources, } from "../attribution/index.js";
5
+ import { rollUp } from "../confidence/index.js";
6
+ import { holderOf, protectedSurface } from "./lib/scope.js";
7
+ export { protectedSurface };
8
+ /** The fingerprint scheme version (§11), recorded in the report's `tool` block and the baseline header. */
9
+ export const fingerprintScheme = 1;
10
+ /**
11
+ * The `packages` map (§10 Version, §11 Baseline): each containing package of a finding, by name in code-unit order,
12
+ * with its strong version (store path or lockfile) and that version's source. Only the occurrences of findings keyed on
13
+ * the package count. A name with no strong version gets no entry, and nor does one with more than one strong version in
14
+ * the scan. One version from two strong sources takes the stronger.
15
+ */
16
+ export function packageVersions(findings) {
17
+ const seen = new Map();
18
+ for (const { attribution, occurrences } of findings) {
19
+ const name = attribution.package;
20
+ if (name === undefined)
21
+ continue;
22
+ let versions = seen.get(name);
23
+ if (versions === undefined)
24
+ seen.set(name, (versions = []));
25
+ for (const { evidence } of occurrences) {
26
+ if (evidence.version !== undefined && strongVersionSources.has(evidence.version.versionSource)) {
27
+ versions.push(evidence.version);
28
+ }
29
+ }
30
+ }
31
+ const packages = [];
32
+ for (const [name, versions] of seen) {
33
+ if (new Set(versions.map(({ version }) => version)).size !== 1)
34
+ continue;
35
+ const strongest = versions.reduce((a, b) => versionSources.indexOf(b.versionSource) < versionSources.indexOf(a.versionSource) ? b : a);
36
+ packages.push([name, { version: strongest.version, versionSource: strongest.versionSource }]);
37
+ }
38
+ return Object.fromEntries(packages.sort(([a], [b]) => compareStrings(a, b)));
39
+ }
40
+ /**
41
+ * Merges occurrences into findings by fingerprint, and rolls their confidence (§9), `locks` (§8), reachability and install context (§7)
42
+ * up. Findings are sorted by fingerprint, and each finding's occurrences by artifact URL, then position, then artifact
43
+ * hash: one key URL reached through two fetched URLs is an artifact of each (§11).
44
+ */
45
+ export function buildFindings(occurrences, catalog) {
46
+ const merged = new Map();
47
+ const surfaces = surfaceLookup(catalog);
48
+ for (const { target, effect, locks, attribution, ...occurrence } of occurrences) {
49
+ const key = targetKey(target, effect);
50
+ const fingerprint = `${key}|${attribution.key}`;
51
+ let finding = merged.get(fingerprint);
52
+ if (finding === undefined) {
53
+ const surface = surfaces.of(target);
54
+ finding = { fingerprint, targetKey: key, target, surface, effect, locks, attribution, occurrences: [] };
55
+ merged.set(fingerprint, finding);
56
+ }
57
+ finding.locks = mostLocking(finding.locks, locks);
58
+ finding.occurrences.push(occurrence);
59
+ }
60
+ const findings = [...merged.values()].map(({ occurrences, ...finding }) => {
61
+ occurrences.sort(byArtifactAndPosition);
62
+ const reachability = occurrences.map((occurrence) => occurrence.reachability).reduce(mostReaching);
63
+ const giving = occurrences.filter((occurrence) => occurrence.reachability === reachability);
64
+ const reachabilityBasis = giving.some((occurrence) => occurrence.reachabilityBasis === 'call-path')
65
+ ? 'call-path'
66
+ : 'creation-site';
67
+ const installContext = occurrences.some((occurrence) => occurrence.installContext === 'load') ? 'load' : 'deferred';
68
+ const confidence = rollUp(occurrences.map((occurrence) => occurrence.confidence));
69
+ const attribution = withWorkspace(withVendoredBy(finding.attribution, occurrences), occurrences);
70
+ const introducers = occurrences.flatMap(({ introducedBy }) => (introducedBy === undefined ? [] : [introducedBy]));
71
+ const introducedBy = introducers.length === 0 ? {} : { introducedBy: [...new Set(introducers.flat())].sort(compareStrings) };
72
+ return { ...finding, attribution, ...introducedBy, reachability, reachabilityBasis, installContext, confidence, occurrences };
73
+ });
74
+ return findings.sort((a, b) => compareStrings(a.fingerprint, b.fingerprint));
75
+ }
76
+ /**
77
+ * A finding's attribution, with `vendoredBy` when its code is vendored (§10 Provenance, §15): when every occurrence is
78
+ * vendored by one package. A copy that isn't vendored, or two vendoring packages, leave it to the occurrences' evidence.
79
+ * Evidence only: the fingerprint is computed before it, and neither the baseline nor `changed` reads it.
80
+ */
81
+ function withVendoredBy(attribution, occurrences) {
82
+ const by = new Set(occurrences.map(({ evidence }) => evidence.vendoredBy));
83
+ const [only] = by;
84
+ return by.size === 1 && only !== undefined && attribution.provenance === 'bundled-dependency'
85
+ ? { ...attribution, vendoredBy: only }
86
+ : attribution;
87
+ }
88
+ /**
89
+ * A finding's attribution, with `workspace` when its first-party code is in one lockfile workspace (§10 Provenance,
90
+ * §15): when every occurrence names that workspace. Evidence only, as `vendoredBy` is.
91
+ */
92
+ function withWorkspace(attribution, occurrences) {
93
+ const workspaces = new Set(occurrences.map(({ evidence }) => evidence.workspace));
94
+ const [only] = workspaces;
95
+ return workspaces.size === 1 && only !== undefined && attribution.provenance === 'first-party'
96
+ ? { ...attribution, workspace: only }
97
+ : attribution;
98
+ }
99
+ /**
100
+ * Groups the occurrences of ineffective writes per `(realm, target, reason)` (§3 Record). Records are sorted by realm,
101
+ * target and reason, and each record's occurrences by artifact URL, then position, then artifact hash.
102
+ */
103
+ export function buildIneffectiveWrites(writes, catalog) {
104
+ const grouped = new Map();
105
+ const surfaces = surfaceLookup(catalog);
106
+ for (const { target, reason, ...occurrence } of writes) {
107
+ const key = `${target.realm}|${target.name}|${reason}`;
108
+ let record = grouped.get(key);
109
+ if (record === undefined) {
110
+ record = { target, surface: surfaces.of(target), reason, occurrences: [] };
111
+ grouped.set(key, record);
112
+ }
113
+ record.occurrences.push(occurrence);
114
+ }
115
+ return [...grouped.entries()]
116
+ .sort(([a], [b]) => compareStrings(a, b))
117
+ .map(([, record]) => {
118
+ record.occurrences.sort(byArtifactAndPosition);
119
+ return record;
120
+ });
121
+ }
122
+ /** The `locks` values from the least locking to the most. */
123
+ const lockingOrder = [false, 'unknown', true];
124
+ /**
125
+ * The more locking of two occurrences' `locks` (§8): `true` over `unknown` over `false`. One site that locks the target
126
+ * locks it when it runs, and one the analyzer can't tell about leaves it unable to say the target stays unlocked.
127
+ */
128
+ function mostLocking(a, b) {
129
+ return lockingOrder.indexOf(a) >= lockingOrder.indexOf(b) ? a : b;
130
+ }
131
+ /** The reachability values from the least reaching to the most (§7 Combining guards, across alternatives). */
132
+ const reachingOrder = ['guarded-native-absence', 'unknown', 'conditional', 'unconditional'];
133
+ /** The more reaching of two occurrences' reachability (§7): a finding reports the most-reaching over its occurrences. */
134
+ function mostReaching(a, b) {
135
+ return reachingOrder.indexOf(a) >= reachingOrder.indexOf(b) ? a : b;
136
+ }
137
+ /** The target key `(realm, target, effect)` (§11), written as the fingerprint is: `window|Window.fetch|wrap`. */
138
+ export function targetKey(target, effect) {
139
+ return `${target.realm}|${target.name}|${effect}`;
140
+ }
141
+ /**
142
+ * A target's surface (§12 Surfaces and data): a catalog member's own, and for a shadowing target, which is derived and
143
+ * not catalogued (ADR 0002), the member's it shadows. For a target the catalog doesn't list (an `add`, or a holder's
144
+ * `prototype-change`), it is that of its holder's owner in the owner → surface map. An ECMAScript owner is always
145
+ * `ECMAScript`, and an owner the map doesn't name is `other`.
146
+ */
147
+ function surfaceLookup(catalog) {
148
+ // An ECMAScript owner is one whose every member ECMA-262, ECMA-402, an ES addition or an engine extension defines.
149
+ // `Window` is not: it has Web IDL members as well as ECMAScript globals.
150
+ const specs = ecmascriptSpecs(catalog);
151
+ const ecmascriptOwners = new Map();
152
+ const ecmascript = (realm) => {
153
+ let owners = ecmascriptOwners.get(realm);
154
+ if (owners === undefined) {
155
+ const idl = new Set();
156
+ const es = new Set();
157
+ for (const { owner, spec } of Object.values(catalog.targets[realm]))
158
+ (specs.has(spec) ? es : idl).add(owner);
159
+ owners = new Set([...es].filter((owner) => !idl.has(owner)));
160
+ ecmascriptOwners.set(realm, owners);
161
+ }
162
+ return owners;
163
+ };
164
+ return {
165
+ of({ realm, name, shadows }) {
166
+ const targets = catalog.targets[realm];
167
+ const catalogued = shadows ?? name;
168
+ if (Object.hasOwn(targets, catalogued))
169
+ return targets[catalogued].surface;
170
+ if (shadows !== undefined)
171
+ throw new Error(`an occurrence shadows ${shadows}, which the ${realm} catalog lacks`);
172
+ const holders = catalog.holders[realm];
173
+ const holderName = holderOf(name, holders);
174
+ if (holderName === undefined) {
175
+ throw new Error(`an occurrence names ${name}, which is on no holder of the ${realm} catalog`);
176
+ }
177
+ const { owner } = holders[holderName];
178
+ if (Object.hasOwn(catalog.surfaceMap.owners, owner))
179
+ return catalog.surfaceMap.owners[owner];
180
+ return ecmascript(realm).has(owner) ? 'ECMAScript' : 'other';
181
+ },
182
+ };
183
+ }
184
+ /** Orders strings by UTF-16 code unit, independent of locale. */
185
+ export function compareStrings(a, b) {
186
+ return a < b ? -1 : a > b ? 1 : 0;
187
+ }
188
+ /**
189
+ * Applies `--surface` (§12) to what a scan reports. `all` keeps everything. `protected` keeps:
190
+ * - the findings the protected surface covers, and counts the others per surface
191
+ * - the ineffective-write records it covers, matched as findings are, through `shadows` (§3 Record): a record is kept
192
+ * when its target is covered with the intended effect of any of its occurrences
193
+ * - each record with candidate targets (`unresolved-holder`) when at least one candidate is covered, with its `detail`
194
+ * listing only the covered ones
195
+ *
196
+ * A record's `candidates` never cross into the output.
197
+ */
198
+ export function applySurface(input, scope, catalog) {
199
+ const covers = scope === 'all' ? () => true : protectedSurface(catalog);
200
+ const outOfScopeBySurface = Object.fromEntries(surfaces.map((surface) => [surface, 0]));
201
+ const findings = input.findings.flatMap((finding) => {
202
+ const covered = covers(narrowed(finding.target, finding.occurrences), finding.effect);
203
+ if (!covered)
204
+ outOfScopeBySurface[finding.surface]++;
205
+ return covered ? [withoutMatches(finding)] : [];
206
+ });
207
+ const ineffectiveWrites = input.ineffectiveWrites.flatMap((record) => record.occurrences.some(({ effect }) => covers(narrowed(record.target, record.occurrences), effect))
208
+ ? [withoutMatches(record)]
209
+ : []);
210
+ const limitations = input.limitations.flatMap(({ candidates, ...rest }) => {
211
+ const record = rest;
212
+ if (candidates === undefined)
213
+ return [record];
214
+ const kept = candidates.filter((candidate) => covers(candidate, ''));
215
+ if (kept.length === 0)
216
+ return [];
217
+ if (kept.length === candidates.length)
218
+ return [record];
219
+ const listed = candidates.map(({ name }) => name).join(', ');
220
+ if (!record.detail.endsWith(listed))
221
+ throw new Error(`a record's detail doesn't end with its candidates: ${record.detail}`);
222
+ const detail = record.detail.slice(0, record.detail.length - listed.length) + kept.map(({ name }) => name).join(', ');
223
+ return [{ ...record, detail }];
224
+ });
225
+ return { findings, outOfScopeBySurface, ineffectiveWrites, limitations };
226
+ }
227
+ /**
228
+ * A target as the surface sees it (§12): a wildcard each of whose occurrences a candidate set narrows matches the
229
+ * members they match, and any other the members its name gives.
230
+ */
231
+ function narrowed(target, occurrences) {
232
+ if (!target.name.endsWith('.*') || occurrences.length === 0)
233
+ return target;
234
+ if (occurrences.some(({ matches }) => matches === undefined))
235
+ return target;
236
+ return { ...target, matches: [...new Set(occurrences.flatMap(({ matches }) => matches ?? []))] };
237
+ }
238
+ /** A finding or record whose occurrences carry no matched members, which never cross into the output. */
239
+ function withoutMatches(found) {
240
+ if (!found.target.name.endsWith('.*'))
241
+ return found;
242
+ const occurrences = found.occurrences.map((occurrence) => {
243
+ const copy = { ...occurrence };
244
+ delete copy.matches;
245
+ return copy;
246
+ });
247
+ return { ...found, occurrences };
248
+ }
249
+ /** Orders occurrences by artifact URL, then position, then artifact hash. */
250
+ function byArtifactAndPosition(a, b) {
251
+ return (compareStrings(a.artifact.url, b.artifact.url) ||
252
+ a.generated.start - b.generated.start ||
253
+ compareStrings(a.artifact.hash, b.artifact.hash));
254
+ }
@@ -0,0 +1,38 @@
1
+ import type { Realm, SerializedCatalog } from '../../../catalog.ts';
2
+ /** A target as the protected surface sees it: its realm, its name, and the member it shadows (ADR 0002). */
3
+ export interface ScopeTarget {
4
+ realm: Realm;
5
+ name: string;
6
+ shadows?: string;
7
+ /**
8
+ * For a wildcard narrowed to a candidate set (§5), the catalog targets it matches: it is covered when one of
9
+ * them is.
10
+ */
11
+ matches?: readonly string[];
12
+ }
13
+ /** Whether the protected surface covers a target with this effect. */
14
+ export type IsProtected = (target: ScopeTarget, effect: string) => boolean;
15
+ /**
16
+ * The protected surface of a catalog (§12 Matching). Entries are keyed on (interface, member), independent of realm
17
+ * and holder. A target is covered when:
18
+ * - it is a catalog target an entry names: a member entry's member, in every realm and on each interface that
19
+ * includes its mixin, and its worker-realm equivalent; a whole entry's global binding and the static, prototype and
20
+ * namespace members the catalog lists for it, without `onX`
21
+ * - it shadows one of those, on whatever holder it lands (`history.pushState`, `XMLHttpRequest.prototype.toString`)
22
+ * - it is an `add` of a named member, whatever its key, to a whole entry's constructor, prototype or namespace
23
+ * - it is a `prototype-change` of a whole entry's prototype or namespace (never of its constructor), or of a covered
24
+ * singleton
25
+ * - it is a wildcard over native members (`<holder>.*`) of a holder a covered target sits on, or of a covered singleton;
26
+ * one a candidate set narrows when a member it matches is a covered target
27
+ *
28
+ * A singleton is covered when its interface or namespace is a whole entry (`history`, `localStorage`, `crypto.subtle`,
29
+ * `navigator.serviceWorker`), or when an entry names a member of its interface (`navigator`, through
30
+ * `Navigator.sendBeacon`). A pattern wildcard of non-native adds (`<holder>.<prefix>*`) is never covered.
31
+ */
32
+ export declare function protectedSurface(catalog: SerializedCatalog): IsProtected;
33
+ /**
34
+ * The holder a target is on (§4 Property keys): the part before a member written in JSON-quoted brackets
35
+ * (`Array["prototype.map"]`), and otherwise the longest holder the name continues with a dot, since a canonical numeric
36
+ * member keeps the dot form (`Array.prototype.1.5`).
37
+ */
38
+ export declare function holderOf(name: string, holders: SerializedCatalog['holders'][Realm]): string | undefined;