@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.10

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 (276) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +181 -341
  5. package/dist/src/augmentations.js +106 -13
  6. package/dist/src/automatic-types.js +10 -29
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +2 -3
  10. package/dist/src/config.js +6 -82
  11. package/dist/src/configuration-files.js +28 -176
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +1 -1
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +73 -3
  20. package/dist/src/conventions.js +36 -66
  21. package/dist/src/dependencies.js +103 -40
  22. package/dist/src/edges.js +38 -122
  23. package/dist/src/element-access.js +4 -6
  24. package/dist/src/entry-point-gaps.js +2 -8
  25. package/dist/src/entry-points.js +11 -27
  26. package/dist/src/enum-group.js +3 -10
  27. package/dist/src/erased-stores.js +4 -6
  28. package/dist/src/exempt-classes.js +1 -63
  29. package/dist/src/exempt.js +21 -40
  30. package/dist/src/failure.js +36 -0
  31. package/dist/src/file-facts.js +2 -25
  32. package/dist/src/findings/completion.js +139 -72
  33. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  34. package/dist/src/findings/emitters.js +5 -5
  35. package/dist/src/findings/interfaces.js +4 -5
  36. package/dist/src/findings/intra-function.js +13 -10
  37. package/dist/src/findings/non-code-artifacts.js +7 -8
  38. package/dist/src/findings/reads-and-writes.js +16 -13
  39. package/dist/src/findings/self-check.js +10 -12
  40. package/dist/src/findings/unused-declarations.js +41 -32
  41. package/dist/src/findings/visibility-narrowing.js +19 -16
  42. package/dist/src/findings-pass.js +8 -15
  43. package/dist/src/framework-lifecycle.js +7 -7
  44. package/dist/src/fs/document.js +52 -0
  45. package/dist/src/fs/manifest.js +72 -0
  46. package/dist/src/fs/tree.js +88 -0
  47. package/dist/src/generated-file.js +7 -4
  48. package/dist/src/git-ignore.js +39 -26
  49. package/dist/src/graph.js +5 -3
  50. package/dist/src/html-entries.js +6 -33
  51. package/dist/src/in-operands.js +2 -1
  52. package/dist/src/index.js +1 -1
  53. package/dist/src/injection-container.js +1 -1
  54. package/dist/src/intra-function-parts.js +63 -43
  55. package/dist/src/inventory.js +2 -21
  56. package/dist/src/javascript-file.js +33 -0
  57. package/dist/src/json-document.js +198 -67
  58. package/dist/src/keyof-comparisons.js +4 -6
  59. package/dist/src/kinds.js +3 -366
  60. package/dist/src/load/classify.js +270 -0
  61. package/dist/src/load/diagnostics.js +80 -0
  62. package/dist/src/load/discover.js +277 -0
  63. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  64. package/dist/src/load/expected-module.js +136 -0
  65. package/dist/src/load/load.js +27 -0
  66. package/dist/src/load/module-lookup.js +37 -0
  67. package/dist/src/load/plan.js +313 -0
  68. package/dist/src/load/specifiers.js +115 -0
  69. package/dist/src/load/synthesize.js +140 -0
  70. package/dist/src/load/test-globals.js +207 -0
  71. package/dist/src/load/test-joins.js +82 -0
  72. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  73. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
  74. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
  75. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  76. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  77. package/dist/src/manifest.js +49 -54
  78. package/dist/src/matrix.js +76 -4
  79. package/dist/src/mdx-entries.js +3 -26
  80. package/dist/src/outside-tests.js +1 -1
  81. package/dist/src/partial.js +2 -1
  82. package/dist/src/paths.js +26 -15
  83. package/dist/src/position.js +2 -1
  84. package/dist/src/print.js +11 -79
  85. package/dist/src/product.js +13 -0
  86. package/dist/src/query.js +1 -0
  87. package/dist/src/ref.js +1 -0
  88. package/dist/src/references.js +48 -30
  89. package/dist/src/report-schema.js +51 -0
  90. package/dist/src/report.js +57 -41
  91. package/dist/src/reporters/annotations.js +15 -8
  92. package/dist/src/reporters/sarif.js +16 -10
  93. package/dist/src/reporters/template.js +4 -19
  94. package/dist/src/resolve.js +245 -729
  95. package/dist/src/roots.js +273 -87
  96. package/dist/src/run.js +66 -77
  97. package/dist/src/schema.js +320 -343
  98. package/dist/src/scope.js +38 -115
  99. package/dist/src/serialization-contract.js +595 -21
  100. package/dist/src/session.js +34 -29
  101. package/dist/src/source-files.js +1 -1
  102. package/dist/src/suppress-file.js +25 -28
  103. package/dist/src/suppress-inline.js +3 -2
  104. package/dist/src/suppress.js +33 -51
  105. package/dist/src/sweep.js +7 -9
  106. package/dist/src/symbol-keys.js +3 -5
  107. package/dist/src/template-field.js +9 -19
  108. package/dist/src/test-support.js +8 -11
  109. package/dist/src/text/compare.js +26 -0
  110. package/dist/src/text/glob.js +194 -0
  111. package/dist/src/text/named-files.js +54 -0
  112. package/dist/src/text/shell.js +159 -0
  113. package/dist/src/text/yaml.js +393 -0
  114. package/dist/src/type-errors.js +11 -232
  115. package/dist/src/unseen-loads.js +100 -0
  116. package/dist/src/unwrap.js +23 -0
  117. package/dist/src/usage.js +10 -0
  118. package/dist/src/verbs/analyze.js +166 -126
  119. package/dist/src/verbs/describe.js +9 -16
  120. package/dist/src/verbs/explain.js +10 -7
  121. package/dist/src/verbs/print-config.js +2 -1
  122. package/dist/src/verbs/print-projects.js +6 -4
  123. package/dist/src/verbs/print-retained.js +4 -2
  124. package/dist/src/verbs/print-roots.js +4 -2
  125. package/dist/src/verbs/verb.js +1 -5
  126. package/dist/src/verbs/version.js +14 -3
  127. package/dist/src/workflow-steps.js +62 -64
  128. package/jsr.json +1 -1
  129. package/package.json +4 -3
  130. package/src/analysis.ts +245 -502
  131. package/src/augmentations.ts +135 -17
  132. package/src/automatic-types.ts +11 -29
  133. package/src/build-scripts.ts +543 -0
  134. package/src/component-layer.ts +4 -2
  135. package/src/components.ts +2 -4
  136. package/src/config.ts +31 -302
  137. package/src/configuration-files.ts +34 -179
  138. package/src/configured-declarations.ts +18 -23
  139. package/src/conformance-record.ts +1 -1
  140. package/src/consumers.ts +6 -57
  141. package/src/contextual-uses.ts +64 -12
  142. package/src/contract/config.generated.ts +818 -0
  143. package/src/contract/contract.generated.ts +1308 -0
  144. package/src/contract/report.generated.ts +1237 -0
  145. package/src/convention-rows.ts +85 -3
  146. package/src/conventions.ts +46 -84
  147. package/src/dependencies.ts +118 -46
  148. package/src/edges.ts +41 -140
  149. package/src/element-access.ts +4 -7
  150. package/src/entry-point-gaps.ts +3 -9
  151. package/src/entry-points.ts +12 -38
  152. package/src/enum-group.ts +2 -11
  153. package/src/erased-stores.ts +3 -7
  154. package/src/exempt-classes.ts +1 -75
  155. package/src/exempt.ts +36 -50
  156. package/src/failure.ts +63 -0
  157. package/src/file-facts.ts +9 -24
  158. package/src/finding.ts +14 -9
  159. package/src/findings/boundary.ts +2 -0
  160. package/src/findings/completion.ts +181 -84
  161. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  162. package/src/findings/emitter.ts +8 -9
  163. package/src/findings/emitters.ts +7 -7
  164. package/src/findings/interfaces.ts +4 -6
  165. package/src/findings/intra-function.ts +13 -11
  166. package/src/findings/non-code-artifacts.ts +9 -11
  167. package/src/findings/reads-and-writes.ts +20 -16
  168. package/src/findings/self-check.ts +11 -17
  169. package/src/findings/unused-declarations.ts +45 -32
  170. package/src/findings/visibility-narrowing.ts +20 -17
  171. package/src/findings-pass.ts +10 -19
  172. package/src/framework-lifecycle.ts +7 -7
  173. package/src/fs/document.ts +70 -0
  174. package/src/fs/manifest.ts +92 -0
  175. package/src/fs/tree.ts +110 -0
  176. package/src/generated-file.ts +7 -4
  177. package/src/git-ignore.ts +50 -26
  178. package/src/graph.ts +5 -3
  179. package/src/host.ts +12 -5
  180. package/src/html-entries.ts +6 -33
  181. package/src/in-operands.ts +2 -1
  182. package/src/index.ts +1 -1
  183. package/src/injection-container.ts +1 -1
  184. package/src/intra-function-parts.ts +73 -44
  185. package/src/inventory.ts +2 -20
  186. package/src/javascript-file.ts +37 -0
  187. package/src/json-document.ts +241 -76
  188. package/src/keyof-comparisons.ts +3 -7
  189. package/src/kinds.ts +5 -413
  190. package/src/load/classify.ts +397 -0
  191. package/src/load/diagnostics.ts +144 -0
  192. package/src/load/discover.ts +455 -0
  193. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  194. package/src/load/expected-module.ts +188 -0
  195. package/src/load/load.ts +73 -0
  196. package/src/load/module-lookup.ts +50 -0
  197. package/src/load/plan.ts +504 -0
  198. package/src/load/specifiers.ts +168 -0
  199. package/src/load/synthesize.ts +211 -0
  200. package/src/load/test-globals.ts +251 -0
  201. package/src/load/test-joins.ts +98 -0
  202. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  203. package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
  204. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
  205. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  206. package/src/{workspace.ts → load/workspace.ts} +90 -134
  207. package/src/manifest.ts +81 -75
  208. package/src/matrix.ts +122 -4
  209. package/src/mdx-entries.ts +3 -26
  210. package/src/outside-tests.ts +1 -1
  211. package/src/partial.ts +3 -2
  212. package/src/paths.ts +27 -15
  213. package/src/position.ts +2 -1
  214. package/src/print.ts +12 -92
  215. package/src/product.ts +18 -0
  216. package/src/query.ts +11 -0
  217. package/src/ref.ts +1 -0
  218. package/src/references.ts +52 -31
  219. package/src/report-schema.ts +68 -0
  220. package/src/report.ts +101 -224
  221. package/src/reporters/annotations.ts +22 -11
  222. package/src/reporters/json.ts +1 -1
  223. package/src/reporters/reporter.ts +2 -4
  224. package/src/reporters/reporters.ts +1 -1
  225. package/src/reporters/sarif.ts +29 -16
  226. package/src/reporters/template.ts +4 -20
  227. package/src/reporters/text.ts +3 -3
  228. package/src/resolve.ts +283 -1114
  229. package/src/roots.ts +339 -104
  230. package/src/run.ts +75 -91
  231. package/src/schema.ts +391 -388
  232. package/src/scope.ts +46 -138
  233. package/src/serialization-contract.ts +763 -22
  234. package/src/session.ts +70 -54
  235. package/src/source-files.ts +1 -1
  236. package/src/suppress-file.ts +24 -33
  237. package/src/suppress-inline.ts +2 -2
  238. package/src/suppress.ts +43 -58
  239. package/src/sweep.ts +23 -9
  240. package/src/symbol-keys.ts +2 -5
  241. package/src/template-field.ts +15 -25
  242. package/src/test-support.ts +17 -13
  243. package/src/text/compare.ts +27 -0
  244. package/src/text/glob.ts +210 -0
  245. package/src/text/named-files.inputs.json +1522 -0
  246. package/src/text/named-files.ts +68 -0
  247. package/src/text/shell.ts +161 -0
  248. package/src/text/yaml.ts +423 -0
  249. package/src/type-errors.ts +18 -288
  250. package/src/unseen-loads.ts +134 -0
  251. package/src/unwrap.ts +34 -0
  252. package/src/usage.ts +10 -0
  253. package/src/verbs/analyze.ts +224 -162
  254. package/src/verbs/describe.ts +14 -15
  255. package/src/verbs/explain.ts +10 -11
  256. package/src/verbs/print-config.ts +2 -1
  257. package/src/verbs/print-projects.ts +6 -4
  258. package/src/verbs/print-retained.ts +4 -2
  259. package/src/verbs/print-roots.ts +4 -2
  260. package/src/verbs/verb.ts +2 -8
  261. package/src/verbs/version.ts +15 -3
  262. package/src/workflow-steps.ts +74 -64
  263. package/dist/src/base-url.js +0 -18
  264. package/dist/src/discover.js +0 -468
  265. package/dist/src/glob.js +0 -65
  266. package/dist/src/pnpm-workspace.js +0 -120
  267. package/dist/src/setup-failure.js +0 -23
  268. package/dist/src/test-joins.js +0 -101
  269. package/dist/src/version.js +0 -7
  270. package/src/base-url.ts +0 -24
  271. package/src/discover.ts +0 -653
  272. package/src/glob.ts +0 -67
  273. package/src/pnpm-workspace.ts +0 -135
  274. package/src/setup-failure.ts +0 -39
  275. package/src/test-joins.ts +0 -120
  276. package/src/version.ts +0 -7
package/dist/src/edges.js CHANGED
@@ -4,153 +4,69 @@
4
4
  * never resolves the paired symbol: the other language's analyzer decides whether the
5
5
  * pairing is a use, and the merge reads the two evaluations together.
6
6
  */
7
- import { ConfigError } from "./config.js";
8
- import { positionAt, walkDocument } from "./json-document.js";
9
- import { joinPath } from "./paths.js";
7
+ import { EDGES_SCHEMA } from "./contract/config.generated.js";
8
+ import { readTargetDocument } from "./fs/document.js";
9
+ import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.js";
10
10
  import { positionKey } from "./position.js";
11
+ import { firstRefusal } from "./schema.js";
11
12
  import { isPartSubject } from "./suppress.js";
13
+ import { bytewise } from "./text/compare.js";
14
+ import { UsageError } from "./usage.js";
12
15
  /** The name and the location the Contract fixes for the edges document. */
13
16
  export const EDGES_FILE = "deadset-edges.json";
14
17
  /** The language prefix of every reference this analyzer evaluates. */
15
18
  const OWN_PREFIX = "ts://";
16
- /** The closed key list of the document and of one edge. */
17
- const DOCUMENT_KEYS = new Set(["description", "edges"]);
18
- const EDGE_KEYS = new Set(["id", "because", "provides", "used_by"]);
19
- /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
20
- const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
21
- /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
22
- const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
23
- const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
24
- const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
25
- const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
26
- const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
27
19
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
28
- class EdgesError extends ConfigError {
29
- constructor(at, text, want) {
30
- super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
20
+ class EdgesError extends UsageError {
21
+ constructor(at, reason) {
22
+ super(`${at}: ${reason}`);
31
23
  this.name = "EdgesError";
32
24
  }
33
25
  }
34
- /**
35
- * Whether a reference carries a wildcard outside a quoted or a computed component, which
36
- * would name a set of symbols rather than one.
37
- */
38
- function carriesWildcard(ref) {
39
- let quoted = false;
40
- let computed = false;
41
- let escaped = false;
42
- for (const char of ref) {
43
- if (escaped) {
44
- escaped = false;
45
- }
46
- else if (quoted && char === "\\") {
47
- escaped = true;
48
- }
49
- else if (quoted) {
50
- quoted = char !== "'";
51
- }
52
- else if (computed) {
53
- computed = char !== "]";
54
- }
55
- else if (char === "'") {
56
- quoted = true;
57
- }
58
- else if (char === "[") {
59
- computed = true;
60
- }
61
- else if (char === "*") {
62
- return true;
63
- }
64
- }
65
- return false;
66
- }
67
- function isObject(value) {
68
- return typeof value === "object" && value !== null && !Array.isArray(value);
69
- }
70
- /** One edge, checked: the closed key list, the value types and the value forms. */
71
- function readEdge(value, at) {
72
- const refuse = (text, want) => {
73
- throw new EdgesError(at, text, want);
74
- };
75
- if (!isObject(value)) {
76
- return refuse(JSON.stringify(value), EDGE_WANT);
77
- }
78
- for (const [key, held] of Object.entries(value)) {
79
- if (!EDGE_KEYS.has(key) || typeof held !== "string") {
80
- refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
81
- }
82
- }
83
- const edge = value;
84
- const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
85
- const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
86
- const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
87
- if (!ID_FORM.test(id)) {
88
- refuse(JSON.stringify(id), ID_WANT);
89
- }
90
- for (const ref of [provides, usedBy]) {
91
- if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
92
- refuse(JSON.stringify(ref), REFERENCE_WANT);
93
- }
94
- }
95
- return { id, provides, usedBy };
96
- }
26
+ /** The sides of one edge, in the order a reader yields them. */
27
+ const SIDE_NAMES = ["provides", "used_by"];
97
28
  /**
98
29
  * Every side of a declared edge that names a symbol of this language, in document order,
99
30
  * `provides` before `used_by` within an edge. An absent document declares no edge. The
100
- * document is strict JSON with a closed key list at both levels, and a defect in it is an
101
- * {@link EdgesError}.
31
+ * document is strict JSON held to the Contract's edges schema, and a defect in it is an
32
+ * {@link EdgesError} at the line and column of the edge it is in, where it is in one.
102
33
  */
103
34
  export function readEdgeSides(host, targetRoot) {
104
- const file = joinPath(targetRoot, EDGES_FILE);
105
- if (host.kindOf(file) === "absent") {
35
+ const text = readTargetDocument(host, targetRoot, EDGES_FILE);
36
+ if (text === undefined) {
106
37
  return [];
107
38
  }
108
- const text = host.readFile(file);
109
- let parsed;
39
+ let read;
110
40
  try {
111
- parsed = JSON.parse(text);
41
+ read = readStrict(text, "edges");
112
42
  }
113
43
  catch (error) {
114
- throw new EdgesError(EDGES_FILE, error instanceof Error ? error.message : String(error), DOCUMENT_WANT);
115
- }
116
- if (!isObject(parsed)) {
117
- throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
118
- }
119
- const walk = walkDocument(text, "edges");
120
- if (walk.twice !== undefined) {
121
- throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)), `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`, DOCUMENT_WANT);
44
+ if (!(error instanceof StrictJSONError)) {
45
+ throw error;
46
+ }
47
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
122
48
  }
123
- const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
124
- const description = parsed["description"];
125
- const edges = parsed["edges"];
126
- if (unknown !== undefined ||
127
- (description !== undefined && typeof description !== "string") ||
128
- !Array.isArray(edges)) {
129
- throw new EdgesError(EDGES_FILE, unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`, DOCUMENT_WANT);
49
+ const value = read.value;
50
+ const refused = firstRefusal(EDGES_SCHEMA, value);
51
+ if (refused !== undefined) {
52
+ const place = placeIn(value, refused.pointer, refused.member);
53
+ const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
54
+ const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
55
+ const where = dotted(place);
56
+ throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
130
57
  }
58
+ const edges = value.edges;
131
59
  const sides = [];
132
- edges.forEach((value, index) => {
133
- const offset = walk.starts[index];
134
- const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
135
- const edge = readEdge(value, at);
136
- const named = [
137
- ["provides", edge.provides],
138
- ["used_by", edge.usedBy],
139
- ];
140
- for (const [side, symbol] of named) {
141
- if (symbol.startsWith(OWN_PREFIX)) {
142
- sides.push({ edge: edge.id, side, symbol });
60
+ for (const id of read.starts.keys()) {
61
+ const edge = String(id);
62
+ for (const side of SIDE_NAMES) {
63
+ const symbol = edges[edge]?.[side];
64
+ if (symbol?.startsWith(OWN_PREFIX) === true) {
65
+ sides.push({ edge, side, symbol });
143
66
  }
144
67
  }
145
- });
146
- return sides;
147
- }
148
- /** Two strings ordered bytewise. */
149
- function compare(a, b) {
150
- if (a === b) {
151
- return 0;
152
68
  }
153
- return a < b ? -1 : 1;
69
+ return sides;
154
70
  }
155
71
  /**
156
72
  * Evaluates every declared side of this language over the run's findings. A side naming a
@@ -191,6 +107,6 @@ export function evaluateEdges(findings, sides, symbols) {
191
107
  return { ...side, state: "dead", finding };
192
108
  });
193
109
  });
194
- evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
110
+ evaluations.sort((a, b) => bytewise(a.edge, b.edge) || bytewise(a.side, b.side));
195
111
  return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
196
112
  }
@@ -8,6 +8,7 @@ import { isArrayLiteralExpression, isArrowFunction, isBinaryExpression, isCallEx
8
8
  import { nodeKey } from "./inventory.js";
9
9
  import { renderPosition } from "./position.js";
10
10
  import { UNANSWERED } from "./query.js";
11
+ import { parenthesesRemoved } from "./unwrap.js";
11
12
  /** The assignment operators that read their target before they write it. */
12
13
  const COMPOUND = new Set([
13
14
  SyntaxKind.PlusEqualsToken,
@@ -47,12 +48,9 @@ function useOf(node) {
47
48
  }
48
49
  return "read";
49
50
  }
50
- function unwrapped(node) {
51
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
52
- }
53
51
  /** Whether an index is a literal the checker resolves to a member by its own text. */
54
52
  function isLiteralIndex(node) {
55
- const at = unwrapped(node);
53
+ const at = parenthesesRemoved(node);
56
54
  return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
57
55
  }
58
56
  /**
@@ -195,7 +193,7 @@ export function elementAccessReferences(project, held, chains, root, testFiles)
195
193
  const parameterOf = new Map();
196
194
  const byCallable = new Map();
197
195
  for (const candidate of candidates) {
198
- const index = unwrapped(candidate.access.argumentExpression);
196
+ const index = parenthesesRemoved(candidate.access.argumentExpression);
199
197
  const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
200
198
  const callable = fn === undefined ? undefined : callableOf(fn);
201
199
  if (callable === undefined || !isIdentifier(index)) {
@@ -252,7 +250,7 @@ export function elementAccessReferences(project, held, chains, root, testFiles)
252
250
  continue;
253
251
  }
254
252
  // An object that is a parameter too is read as the argument each call passes there.
255
- const object = unwrapped(candidate.access.expression);
253
+ const object = parenthesesRemoved(candidate.access.expression);
256
254
  const objectAt = isIdentifier(object)
257
255
  ? deferred.callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text)
258
256
  : -1;
@@ -7,6 +7,7 @@
7
7
  * Every row carries a reason, because the list is read where the rows are shown
8
8
  * one at a time: beside a finding, or in a report's declared gaps.
9
9
  */
10
+ import { bytewise } from "./text/compare.js";
10
11
  /** What is declined for a tool whose own conventions are entered only through its configuration files. */
11
12
  const EVERY_CONVENTION = "every file it loads by its own convention beyond its configuration files";
12
13
  /** Why a tool whose own conventions are not read declines them. */
@@ -291,15 +292,8 @@ const PARTLY_ENTERED = [
291
292
  reason: NOT_LITERAL,
292
293
  },
293
294
  ];
294
- /** Two strings ordered bytewise. */
295
- function compare(a, b) {
296
- if (a === b) {
297
- return 0;
298
- }
299
- return a < b ? -1 : 1;
300
- }
301
295
  /** Every declined entry-point convention, ordered by tool and then by convention. */
302
296
  export const DECLINED_CONVENTIONS = [
303
297
  ...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
304
298
  ...PARTLY_ENTERED,
305
- ].sort((a, b) => compare(a.tool, b.tool) || compare(a.convention, b.convention));
299
+ ].sort((a, b) => bytewise(a.tool, b.tool) || bytewise(a.convention, b.convention));
@@ -10,15 +10,18 @@
10
10
  * value that is not a literal is not evaluated and not guessed, so a file it would
11
11
  * have named is entered by nothing here.
12
12
  */
13
- import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
13
+ import { isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
14
14
  import { SymbolFlags } from "@typescript/native/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
- import { globExpression } from "./glob.js";
16
+ import { buildScriptEntries } from "./build-scripts.js";
17
17
  import { htmlEntries } from "./html-entries.js";
18
18
  import { documentEntries } from "./mdx-entries.js";
19
19
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
20
20
  import { UNANSWERED } from "./query.js";
21
+ import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.js";
21
22
  import { workflowEntries } from "./workflow-steps.js";
23
+ import { unwrapped } from "./unwrap.js";
24
+ import { globExpression } from "./text/glob.js";
22
25
  /** The extensions the test runner and the browser-test runner load a configuration from. */
23
26
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
24
27
  /** The tool file names the configuration form does not cover. */
@@ -67,14 +70,6 @@ function literalOf(node) {
67
70
  }
68
71
  return undefined;
69
72
  }
70
- /** The expression a wrapper that changes no value holds: parentheses and type assertions. */
71
- function unwrapped(node) {
72
- let at = node;
73
- while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
74
- at = at.expression;
75
- }
76
- return at;
77
- }
78
73
  /**
79
74
  * The object literals one configuration file's default export is written as: the
80
75
  * literal itself, or each literal argument of the call it is, at any depth of calls.
@@ -210,7 +205,7 @@ function configurationEntries(host, targetRoot, files, parse, selected) {
210
205
  strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
211
206
  }
212
207
  for (const { file } of outside) {
213
- strings(dirnamePath(file.fileName), moduleStrings(file).map((one) => one.text));
208
+ strings(dirnamePath(file.fileName), unselected(file));
214
209
  }
215
210
  return found;
216
211
  }
@@ -398,17 +393,6 @@ function workersOf(project, file, files) {
398
393
  }
399
394
  return found;
400
395
  }
401
- /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
402
- function leadingLiteral(node) {
403
- const at = unwrapped(node);
404
- if (isTemplateExpression(at)) {
405
- return at.head.text;
406
- }
407
- if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
408
- return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
409
- }
410
- return undefined;
411
- }
412
396
  /** Whether one call is `import.meta.glob(...)`. */
413
397
  function isMetaGlob(node) {
414
398
  if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
@@ -432,11 +416,8 @@ function computedImportsOf(file, files) {
432
416
  const dir = dirnamePath(file.fileName);
433
417
  const found = [];
434
418
  const below = (prefix) => {
435
- const target = resolvePath(dir, prefix);
436
- if (!isModuleRelative(prefix) ||
437
- !prefix.endsWith("/") ||
438
- target === dir ||
439
- relativePath(target, dir) !== undefined) {
419
+ const target = computedLoadDirectory(dir, prefix);
420
+ if (target === undefined) {
440
421
  return;
441
422
  }
442
423
  for (const candidate of files.byName.values()) {
@@ -522,6 +503,9 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
522
503
  ...(names === undefined ? {} : { names }),
523
504
  });
524
505
  }
506
+ for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
507
+ found.push({ file, rule: "bundler-entry", source, exports: true });
508
+ }
525
509
  for (const { file, source } of workflowEntries(host, targetRoot, files)) {
526
510
  found.push({ file, rule: "workflow-step", source, exports: true });
527
511
  }
@@ -6,7 +6,8 @@
6
6
  * or `unknown` expression asserted to it, or bound to a variable or returned from a
7
7
  * function it annotates, where a number counts too. An enum's own value converts nothing.
8
8
  */
9
- import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
9
+ import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
10
+ import { parenthesesRemoved } from "./unwrap.js";
10
11
  import { TypeFlags } from "@typescript/native/sync";
11
12
  import { aliasChains } from "./alias-chain.js";
12
13
  import { nodeKey } from "./inventory.js";
@@ -16,14 +17,6 @@ import { isAnswered, UNANSWERED } from "./query.js";
16
17
  const NUMBER = TypeFlags.NumberLike;
17
18
  const STRING = TypeFlags.StringLike;
18
19
  const DECODED = TypeFlags.Any | TypeFlags.Unknown;
19
- /** The expression one node is, through however many parentheses are written around it. */
20
- function unparenthesized(node) {
21
- let held = node;
22
- while (isParenthesizedExpression(held)) {
23
- held = held.expression;
24
- }
25
- return held;
26
- }
27
20
  /**
28
21
  * The name node a type node names its declaration by: the identifier of a type
29
22
  * reference, the right side of a qualified one, through parentheses. A type that is no
@@ -45,7 +38,7 @@ function typeName(type) {
45
38
  }
46
39
  /** The name node an expression names an object by: an identifier, or a property access's name. */
47
40
  function objectName(expression) {
48
- const held = unparenthesized(expression);
41
+ const held = parenthesesRemoved(expression);
49
42
  if (isIdentifier(held)) {
50
43
  return held;
51
44
  }
@@ -6,10 +6,11 @@
6
6
  * either stores. A store into an erased property of a type the analyzed program does not
7
7
  * declare hands the value to the code that declares it.
8
8
  */
9
- import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
9
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
10
10
  import { SymbolFlags, TypeFlags } from "@typescript/native/sync";
11
11
  import { must } from "./query.js";
12
12
  import { checkedTypes } from "./value-reach.js";
13
+ import { parenthesesRemoved } from "./unwrap.js";
13
14
  /** The types that keep nothing of the value they are given. */
14
15
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
15
16
  /** The array methods whose arguments become elements of the array they are called on. */
@@ -20,9 +21,6 @@ function literalKey(node) {
20
21
  ? node.text
21
22
  : undefined;
22
23
  }
23
- function unwrapped(node) {
24
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
25
- }
26
24
  /**
27
25
  * Whether a variable declaration's type can differ from what is stored in it: it carries an
28
26
  * annotation, or it has neither an annotation nor an initializer, so its type evolves.
@@ -76,8 +74,8 @@ function propertyAt(source, name) {
76
74
  * here, and a default value is stored in its target too.
77
75
  */
78
76
  function patternTargets(pattern, from, out) {
79
- const target = unwrapped(pattern);
80
- const source = from.path.length === 0 ? unwrapped(from.value) : from.value;
77
+ const target = parenthesesRemoved(pattern);
78
+ const source = from.path.length === 0 ? parenthesesRemoved(from.value) : from.value;
81
79
  const into = (element, step, literal) => {
82
80
  patternTargets(element, literal === undefined
83
81
  ? { value: from.value, path: [...from.path, step] }
@@ -1,62 +1,4 @@
1
- /**
2
- * The exemption classes of the Contract's vocabulary, in its order: each class's name, its
3
- * languages and, for a TypeScript class, which private members it may retain. The rows are
4
- * written out, as the fixed severity codes are, and a test holds them equal to the Contract.
5
- * A class name is a type, so a table or a record naming a class outside the vocabulary, or
6
- * one of another language only, does not compile.
7
- */
8
- /** Every class of the vocabulary, in the order the Contract lists them. */
9
- export const EXEMPTION_CLASSES = [
10
- {
11
- class: "interface-satisfaction",
12
- languages: ["go", "ts"],
13
- typescriptVisibility: { private: false, privateName: false },
14
- },
15
- { class: "encoding-reflection", languages: ["go"] },
16
- { class: "format-verb-contract", languages: ["go"] },
17
- { class: "errors-duck-typing", languages: ["go"] },
18
- {
19
- class: "enum-group",
20
- languages: ["go", "ts"],
21
- typescriptVisibility: { private: false, privateName: false },
22
- },
23
- {
24
- class: "generated-file",
25
- languages: ["go", "ts"],
26
- typescriptVisibility: { private: true, privateName: false },
27
- },
28
- { class: "linkname-cgo-asm-plugin", languages: ["go"] },
29
- {
30
- class: "template-field",
31
- languages: ["go", "ts"],
32
- typescriptVisibility: { private: true, privateName: false },
33
- },
34
- {
35
- class: "reflective-lookup",
36
- languages: ["go", "ts"],
37
- typescriptVisibility: { private: true, privateName: false },
38
- },
39
- {
40
- class: "decorator",
41
- languages: ["ts"],
42
- typescriptVisibility: { private: true, privateName: false },
43
- },
44
- {
45
- class: "injection-container",
46
- languages: ["ts"],
47
- typescriptVisibility: { private: true, privateName: false },
48
- },
49
- {
50
- class: "framework-lifecycle",
51
- languages: ["ts"],
52
- typescriptVisibility: { private: true, privateName: false },
53
- },
54
- {
55
- class: "serialization-contract",
56
- languages: ["ts"],
57
- typescriptVisibility: { private: true, privateName: false },
58
- },
59
- ];
1
+ import { EXEMPTION_CLASSES } from "./contract/contract.generated.js";
60
2
  function runsOnTS(row) {
61
3
  const languages = row.languages;
62
4
  return languages.includes("ts");
@@ -69,7 +11,3 @@ const VISIBILITY = new Map(TS_ROWS.map((row) => [row.class, row.typescriptVisibi
69
11
  export function typescriptVisibilityOf(exemptionClass) {
70
12
  return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
71
13
  }
72
- /** Whether one name is a class of the vocabulary, whichever language it runs on. */
73
- export function isExemptionClass(name) {
74
- return EXEMPTION_CLASSES.some((row) => row.class === name);
75
- }
@@ -6,26 +6,18 @@
6
6
  * which records held something back. It names no class's detector: the composition
7
7
  * hands it the table of the classes it has.
8
8
  */
9
- import { ConfigError } from "./config.js";
10
- import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
9
+ import { EXEMPTION_CLASSES } from "./contract/contract.generated.js";
10
+ import { TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
11
11
  import { byPosition, positionKey } from "./position.js";
12
12
  import { Unanswerable } from "./query.js";
13
13
  import { sweep } from "./sweep.js";
14
+ import { bytewise } from "./text/compare.js";
14
15
  /**
15
- * The classes `exemptions.disabled` switches off. A name is refused unless the
16
- * vocabulary holds it; a class that runs on another language only is admitted and
17
- * switches nothing off here, so one configuration serves every analyzer.
16
+ * The classes `exemptions.disabled` switches off. A class that runs on another language
17
+ * only switches nothing off here, so one configuration serves every analyzer.
18
18
  */
19
19
  export function disabledClasses(config) {
20
- const disabled = new Set();
21
- for (const name of config.exemptionsDisabled) {
22
- if (!isExemptionClass(name)) {
23
- const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
24
- throw new ConfigError("malformed", "exemptions.disabled", `exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`);
25
- }
26
- disabled.add(name);
27
- }
28
- return disabled;
20
+ return new Set(config.exemptions.disabled);
29
21
  }
30
22
  /**
31
23
  * Whether a class whose row states `allowed` may retain a member of one visibility. A
@@ -40,20 +32,8 @@ function retainable(allowed, visibility) {
40
32
  }
41
33
  return true;
42
34
  }
43
- /**
44
- * The kinds of declaration each class can hold back, which a detector that stops holds
45
- * back whole. A class with no row here holds back every declaration but a file.
46
- */
47
- const RETAINABLE = new Map([
48
- ["interface-satisfaction", new Set(["method", "class-member"])],
49
- ["enum-group", new Set(["enum-member"])],
50
- ["injection-container", new Set(["method", "class-member"])],
51
- ["framework-lifecycle", new Set(["method", "class-member"])],
52
- [
53
- "serialization-contract",
54
- new Set(["method", "class-member", "interface-method", "type-member"]),
55
- ],
56
- ]);
35
+ /** The kinds of declaration each class can hold back, which a detector that stops holds back whole. */
36
+ const RETAINABLE = new Map(EXEMPTION_CLASSES.flatMap((row) => "ts" in row.retainsKinds ? [[row.class, new Set(row.retainsKinds.ts)]] : []));
57
37
  /** The detail of a record a detector that could not finish holds. */
58
38
  const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
59
39
  /** Whether one record is held by a detector that could not finish. */
@@ -76,7 +56,7 @@ function detected(exemptionClass, detect, input) {
76
56
  }
77
57
  const kinds = RETAINABLE.get(exemptionClass);
78
58
  return input.held.symbols
79
- .filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
59
+ .filter((symbol) => kinds?.has(symbol.kind) === true)
80
60
  .map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
81
61
  }
82
62
  }
@@ -106,11 +86,19 @@ export function evidenceRecords(input, detectors, disabled) {
106
86
  site: evidence.site,
107
87
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
108
88
  ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
89
+ ...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
109
90
  });
110
91
  }
111
92
  }
112
93
  return found;
113
94
  }
95
+ /**
96
+ * The records that hold given the declarations a write the mode counts stores into: one
97
+ * that holds only while written holds where its declaration is among them.
98
+ */
99
+ export function writtenRecords(records, written) {
100
+ return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
101
+ }
114
102
  /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
115
103
  export function heldRecords(records, holding) {
116
104
  const found = [];
@@ -125,20 +113,13 @@ export function heldRecords(records, holding) {
125
113
  }
126
114
  return { records: exemptionsOf(found), inTestFiles };
127
115
  }
128
- /** Two strings ordered bytewise. */
129
- function compare(a, b) {
130
- if (a === b) {
131
- return 0;
132
- }
133
- return a < b ? -1 : 1;
134
- }
135
116
  /**
136
117
  * Records as a run holds them: ordered by site, then class, then declaration, and one
137
118
  * per declaration, class, detail and file it holds while live, the one at the first
138
119
  * site. A fact found at many sites, or by several projects, is one record.
139
120
  */
140
121
  export function exemptionsOf(found) {
141
- const ordered = [...found].sort((a, b) => byPosition(a.site, b.site) || compare(a.class, b.class) || compare(a.id, b.id));
122
+ const ordered = [...found].sort((a, b) => byPosition(a.site, b.site) || bytewise(a.class, b.class) || bytewise(a.id, b.id));
142
123
  const seen = new Set();
143
124
  return ordered.filter((record) => {
144
125
  const fact = [record.id, record.class, record.detail, record.whileLive ?? ""].join("\u0000");
@@ -189,8 +170,8 @@ export function retainedIn(matrix, input, writeOnly = new Set()) {
189
170
  return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
190
171
  }
191
172
  /**
192
- * One line per held-back declaration, in site order: its reference, then each record's
193
- * class, site and detail, all separated by tabs.
173
+ * One line per held-back declaration, in site order: its reference and position, then
174
+ * each record's class, site and detail, all separated by tabs.
194
175
  */
195
176
  export function retainedLines(symbols, retained) {
196
177
  const held = byDeclaration(retained);
@@ -204,6 +185,6 @@ export function retainedLines(symbols, retained) {
204
185
  positionKey(record.site),
205
186
  record.detail,
206
187
  ]);
207
- return [[symbol.ref, ...fields].join("\t")];
188
+ return [[symbol.ref, positionKey(symbol.position), ...fields].join("\t")];
208
189
  });
209
190
  }
@@ -0,0 +1,36 @@
1
+ import { FAILURE_CLASSES, FAILURE_PREFIXES } from "./contract/contract.generated.js";
2
+ import { PRODUCT } from "./product.js";
3
+ /** The failure as a class line of the error stream, opened by its group's prefix. */
4
+ export function failureLine(failure) {
5
+ return `${FAILURE_PREFIXES[FAILURE_CLASSES[failure.failureClass]]}${failure.failureClass}: ${failure.detail}`;
6
+ }
7
+ /**
8
+ * The lines of the error stream that name the failures: the first one's class line, the run's
9
+ * only one, then each further failure as its class and detail with no group prefix.
10
+ */
11
+ export function failureLines([first, ...further]) {
12
+ return [failureLine(first), ...further.map((one) => `${one.failureClass}: ${one.detail}`)];
13
+ }
14
+ /**
15
+ * A run the analysis cannot answer. It ends the run with the failure code and no finding
16
+ * list, printing the compiler's located errors and then the failures' lines.
17
+ */
18
+ export class FailureExit extends Error {
19
+ failures;
20
+ /** The compiler's errors behind the failures, in the order it reported them. */
21
+ diagnostics;
22
+ constructor(failures, diagnostics = []) {
23
+ super(failureLines(failures).join("\n"));
24
+ this.name = "FailureExit";
25
+ this.failures = failures;
26
+ this.diagnostics = diagnostics;
27
+ }
28
+ }
29
+ /** A failure exit of one failure. */
30
+ export function failureExit(failureClass, detail, diagnostics = []) {
31
+ return new FailureExit([{ failureClass, detail }], diagnostics);
32
+ }
33
+ /** A fault of this analyzer itself, `fault` saying what it cannot do. */
34
+ export function productFault(fault) {
35
+ return failureExit("product-fault", `${PRODUCT} ${fault}: reinstall ${PRODUCT}, and report the fault to its maintainers if a reinstalled ${PRODUCT} fails the same way`);
36
+ }