@cplieger/deadset-ts 7.2.0-dev.4 → 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 (288) 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 +245 -309
  5. package/dist/src/augmentations.js +124 -18
  6. package/dist/src/automatic-types.js +170 -0
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +9 -9
  10. package/dist/src/config.js +6 -81
  11. package/dist/src/configuration-files.js +78 -188
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +2 -2
  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 +146 -1
  20. package/dist/src/conventions.js +136 -70
  21. package/dist/src/decorator.js +21 -18
  22. package/dist/src/dependencies.js +280 -62
  23. package/dist/src/edges.js +38 -122
  24. package/dist/src/element-access.js +4 -6
  25. package/dist/src/entry-point-gaps.js +9 -18
  26. package/dist/src/entry-points.js +106 -21
  27. package/dist/src/enum-group.js +3 -10
  28. package/dist/src/erased-stores.js +4 -6
  29. package/dist/src/exempt-classes.js +1 -63
  30. package/dist/src/exempt.js +21 -40
  31. package/dist/src/failure.js +36 -0
  32. package/dist/src/file-facts.js +2 -25
  33. package/dist/src/findings/completion.js +225 -63
  34. package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
  35. package/dist/src/findings/emitters.js +6 -6
  36. package/dist/src/findings/interfaces.js +4 -5
  37. package/dist/src/findings/intra-function.js +53 -19
  38. package/dist/src/findings/non-code-artifacts.js +71 -39
  39. package/dist/src/findings/reads-and-writes.js +16 -13
  40. package/dist/src/findings/self-check.js +10 -12
  41. package/dist/src/findings/unused-declarations.js +57 -28
  42. package/dist/src/findings/visibility-narrowing.js +40 -18
  43. package/dist/src/findings-pass.js +8 -15
  44. package/dist/src/framework-lifecycle.js +7 -7
  45. package/dist/src/fs/document.js +52 -0
  46. package/dist/src/fs/manifest.js +72 -0
  47. package/dist/src/fs/tree.js +88 -0
  48. package/dist/src/generated-file.js +18 -4
  49. package/dist/src/git-ignore.js +99 -0
  50. package/dist/src/graph.js +9 -4
  51. package/dist/src/html-entries.js +6 -33
  52. package/dist/src/import-bindings.js +97 -0
  53. package/dist/src/in-operands.js +2 -1
  54. package/dist/src/index.js +1 -1
  55. package/dist/src/indexed-access.js +130 -28
  56. package/dist/src/injection-container.js +1 -1
  57. package/dist/src/interface-satisfaction.js +30 -12
  58. package/dist/src/intra-function-parts.js +341 -69
  59. package/dist/src/inventory.js +54 -29
  60. package/dist/src/javascript-file.js +33 -0
  61. package/dist/src/json-document.js +198 -67
  62. package/dist/src/keyof-comparisons.js +4 -6
  63. package/dist/src/kinds.js +3 -366
  64. package/dist/src/load/classify.js +270 -0
  65. package/dist/src/load/diagnostics.js +80 -0
  66. package/dist/src/load/discover.js +277 -0
  67. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  68. package/dist/src/load/expected-module.js +136 -0
  69. package/dist/src/load/load.js +27 -0
  70. package/dist/src/load/module-lookup.js +37 -0
  71. package/dist/src/load/plan.js +313 -0
  72. package/dist/src/load/specifiers.js +115 -0
  73. package/dist/src/load/synthesize.js +140 -0
  74. package/dist/src/load/test-globals.js +207 -0
  75. package/dist/src/load/test-joins.js +82 -0
  76. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  77. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
  78. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
  79. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  80. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  81. package/dist/src/manifest.js +49 -54
  82. package/dist/src/matrix.js +94 -13
  83. package/dist/src/mdx-entries.js +174 -0
  84. package/dist/src/module-declarations.js +9 -13
  85. package/dist/src/outside-tests.js +1 -1
  86. package/dist/src/overrides.js +41 -10
  87. package/dist/src/partial.js +2 -1
  88. package/dist/src/paths.js +26 -15
  89. package/dist/src/position.js +2 -1
  90. package/dist/src/print.js +11 -78
  91. package/dist/src/product.js +13 -0
  92. package/dist/src/query.js +1 -0
  93. package/dist/src/ref.js +11 -0
  94. package/dist/src/references.js +52 -31
  95. package/dist/src/report-schema.js +51 -0
  96. package/dist/src/report.js +59 -41
  97. package/dist/src/reporters/annotations.js +15 -8
  98. package/dist/src/reporters/sarif.js +16 -10
  99. package/dist/src/reporters/template.js +4 -19
  100. package/dist/src/resolve.js +245 -727
  101. package/dist/src/roots.js +374 -116
  102. package/dist/src/run.js +66 -77
  103. package/dist/src/schema.js +320 -342
  104. package/dist/src/scope.js +38 -115
  105. package/dist/src/serialization-contract.js +649 -29
  106. package/dist/src/session.js +41 -29
  107. package/dist/src/source-files.js +40 -15
  108. package/dist/src/suppress-file.js +34 -29
  109. package/dist/src/suppress-inline.js +21 -4
  110. package/dist/src/suppress.js +40 -56
  111. package/dist/src/sweep.js +32 -17
  112. package/dist/src/symbol-keys.js +105 -6
  113. package/dist/src/template-field.js +9 -19
  114. package/dist/src/test-support.js +119 -18
  115. package/dist/src/text/compare.js +26 -0
  116. package/dist/src/text/glob.js +194 -0
  117. package/dist/src/text/named-files.js +54 -0
  118. package/dist/src/text/shell.js +159 -0
  119. package/dist/src/text/yaml.js +393 -0
  120. package/dist/src/type-errors.js +27 -221
  121. package/dist/src/type-parameter-reads.js +172 -0
  122. package/dist/src/unseen-loads.js +100 -0
  123. package/dist/src/unwrap.js +23 -0
  124. package/dist/src/usage.js +10 -0
  125. package/dist/src/verbs/analyze.js +181 -125
  126. package/dist/src/verbs/describe.js +9 -16
  127. package/dist/src/verbs/explain.js +14 -9
  128. package/dist/src/verbs/print-config.js +2 -1
  129. package/dist/src/verbs/print-projects.js +6 -4
  130. package/dist/src/verbs/print-retained.js +4 -2
  131. package/dist/src/verbs/print-roots.js +4 -2
  132. package/dist/src/verbs/verb.js +1 -5
  133. package/dist/src/verbs/version.js +14 -3
  134. package/dist/src/workflow-steps.js +128 -33
  135. package/jsr.json +1 -1
  136. package/package.json +5 -4
  137. package/src/analysis.ts +333 -457
  138. package/src/augmentations.ts +152 -23
  139. package/src/automatic-types.ts +228 -0
  140. package/src/build-scripts.ts +543 -0
  141. package/src/component-layer.ts +4 -2
  142. package/src/components.ts +9 -10
  143. package/src/config.ts +31 -299
  144. package/src/configuration-files.ts +100 -191
  145. package/src/configured-declarations.ts +18 -23
  146. package/src/conformance-record.ts +2 -2
  147. package/src/consumers.ts +6 -57
  148. package/src/contextual-uses.ts +64 -12
  149. package/src/contract/config.generated.ts +818 -0
  150. package/src/contract/contract.generated.ts +1308 -0
  151. package/src/contract/report.generated.ts +1237 -0
  152. package/src/convention-rows.ts +186 -1
  153. package/src/conventions.ts +166 -90
  154. package/src/decorator.ts +28 -18
  155. package/src/dependencies.ts +356 -78
  156. package/src/edges.ts +41 -140
  157. package/src/element-access.ts +4 -7
  158. package/src/entry-point-gaps.ts +11 -20
  159. package/src/entry-points.ts +124 -28
  160. package/src/enum-group.ts +2 -11
  161. package/src/erased-stores.ts +3 -7
  162. package/src/exempt-classes.ts +1 -75
  163. package/src/exempt.ts +36 -50
  164. package/src/failure.ts +63 -0
  165. package/src/file-facts.ts +9 -24
  166. package/src/finding.ts +24 -9
  167. package/src/findings/boundary.ts +2 -0
  168. package/src/findings/completion.ts +297 -77
  169. package/src/findings/dependencies-and-module-machinery.ts +90 -31
  170. package/src/findings/emitter.ts +8 -9
  171. package/src/findings/emitters.ts +8 -8
  172. package/src/findings/interfaces.ts +4 -6
  173. package/src/findings/intra-function.ts +64 -20
  174. package/src/findings/non-code-artifacts.ts +88 -50
  175. package/src/findings/reads-and-writes.ts +20 -16
  176. package/src/findings/self-check.ts +11 -17
  177. package/src/findings/unused-declarations.ts +65 -28
  178. package/src/findings/visibility-narrowing.ts +51 -19
  179. package/src/findings-pass.ts +10 -19
  180. package/src/framework-lifecycle.ts +7 -7
  181. package/src/fs/document.ts +70 -0
  182. package/src/fs/manifest.ts +92 -0
  183. package/src/fs/tree.ts +110 -0
  184. package/src/generated-file.ts +23 -4
  185. package/src/git-ignore.ts +122 -0
  186. package/src/graph.ts +11 -3
  187. package/src/host.ts +17 -14
  188. package/src/html-entries.ts +6 -33
  189. package/src/import-bindings.ts +128 -0
  190. package/src/in-operands.ts +2 -1
  191. package/src/index.ts +1 -1
  192. package/src/indexed-access.ts +152 -30
  193. package/src/injection-container.ts +1 -1
  194. package/src/interface-satisfaction.ts +33 -13
  195. package/src/intra-function-parts.ts +449 -85
  196. package/src/inventory.ts +73 -27
  197. package/src/javascript-file.ts +37 -0
  198. package/src/json-document.ts +241 -76
  199. package/src/keyof-comparisons.ts +3 -7
  200. package/src/kinds.ts +5 -413
  201. package/src/load/classify.ts +397 -0
  202. package/src/load/diagnostics.ts +144 -0
  203. package/src/load/discover.ts +455 -0
  204. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  205. package/src/load/expected-module.ts +188 -0
  206. package/src/load/load.ts +73 -0
  207. package/src/load/module-lookup.ts +50 -0
  208. package/src/load/plan.ts +504 -0
  209. package/src/load/specifiers.ts +168 -0
  210. package/src/load/synthesize.ts +211 -0
  211. package/src/load/test-globals.ts +251 -0
  212. package/src/load/test-joins.ts +98 -0
  213. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  214. package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
  215. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
  216. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  217. package/src/{workspace.ts → load/workspace.ts} +90 -134
  218. package/src/manifest.ts +81 -75
  219. package/src/matrix.ts +154 -18
  220. package/src/mdx-entries.ts +218 -0
  221. package/src/module-declarations.ts +11 -13
  222. package/src/outside-tests.ts +1 -1
  223. package/src/overrides.ts +51 -12
  224. package/src/partial.ts +3 -2
  225. package/src/paths.ts +27 -15
  226. package/src/position.ts +2 -1
  227. package/src/print.ts +12 -91
  228. package/src/product.ts +18 -0
  229. package/src/query.ts +11 -0
  230. package/src/ref.ts +13 -0
  231. package/src/references.ts +62 -32
  232. package/src/report-schema.ts +68 -0
  233. package/src/report.ts +105 -215
  234. package/src/reporters/annotations.ts +22 -11
  235. package/src/reporters/json.ts +1 -1
  236. package/src/reporters/reporter.ts +2 -4
  237. package/src/reporters/reporters.ts +1 -1
  238. package/src/reporters/sarif.ts +29 -16
  239. package/src/reporters/template.ts +4 -20
  240. package/src/reporters/text.ts +3 -3
  241. package/src/resolve.ts +283 -1111
  242. package/src/roots.ts +490 -137
  243. package/src/run.ts +75 -91
  244. package/src/schema.ts +391 -387
  245. package/src/scope.ts +46 -138
  246. package/src/serialization-contract.ts +818 -26
  247. package/src/session.ts +77 -54
  248. package/src/source-files.ts +67 -17
  249. package/src/suppress-file.ts +36 -34
  250. package/src/suppress-inline.ts +20 -3
  251. package/src/suppress.ts +52 -63
  252. package/src/sweep.ts +53 -18
  253. package/src/symbol-keys.ts +131 -7
  254. package/src/template-field.ts +15 -25
  255. package/src/test-support.ts +155 -18
  256. package/src/text/compare.ts +27 -0
  257. package/src/text/glob.ts +210 -0
  258. package/src/text/named-files.inputs.json +1522 -0
  259. package/src/text/named-files.ts +68 -0
  260. package/src/text/shell.ts +161 -0
  261. package/src/text/yaml.ts +423 -0
  262. package/src/type-errors.ts +35 -273
  263. package/src/type-parameter-reads.ts +227 -0
  264. package/src/unseen-loads.ts +134 -0
  265. package/src/unwrap.ts +34 -0
  266. package/src/usage.ts +10 -0
  267. package/src/verbs/analyze.ts +245 -161
  268. package/src/verbs/describe.ts +14 -15
  269. package/src/verbs/explain.ts +15 -13
  270. package/src/verbs/print-config.ts +2 -1
  271. package/src/verbs/print-projects.ts +6 -4
  272. package/src/verbs/print-retained.ts +4 -2
  273. package/src/verbs/print-roots.ts +4 -2
  274. package/src/verbs/verb.ts +2 -8
  275. package/src/verbs/version.ts +15 -3
  276. package/src/workflow-steps.ts +149 -34
  277. package/dist/src/discover.js +0 -347
  278. package/dist/src/glob.js +0 -65
  279. package/dist/src/pnpm-workspace.js +0 -120
  280. package/dist/src/setup-failure.js +0 -23
  281. package/dist/src/test-joins.js +0 -101
  282. package/dist/src/version.js +0 -7
  283. package/src/discover.ts +0 -510
  284. package/src/glob.ts +0 -67
  285. package/src/pnpm-workspace.ts +0 -135
  286. package/src/setup-failure.ts +0 -39
  287. package/src/test-joins.ts +0 -120
  288. package/src/version.ts +0 -7
package/src/suppress.ts CHANGED
@@ -7,7 +7,13 @@
7
7
  * withholds every such finding anyway, and stale otherwise.
8
8
  */
9
9
 
10
- import { ConfigError } from "./config.ts";
10
+ import {
11
+ type CODE,
12
+ KIND_ROWS,
13
+ SYMBOL_SUBJECTS,
14
+ type Subject,
15
+ } from "./contract/contract.generated.ts";
16
+ import { UsageError } from "./usage.ts";
11
17
  import type { Finding } from "./finding.ts";
12
18
  import { positionKey, type Position } from "./position.ts";
13
19
  import { isRef } from "./ref.ts";
@@ -15,15 +21,6 @@ import { isRef } from "./ref.ts";
15
21
  /** Which document a suppression is written in, spelled as a finding carries it. */
16
22
  export type Mechanism = "inline" | "ignore" | "baseline";
17
23
 
18
- /** The code a suppression that carries no reason is reported under. */
19
- export const SUPPRESSION_WITHOUT_REASON = "DS1701";
20
-
21
- /** The code an ignore entry or a baseline row that names no path is reported under. */
22
- export const UNSCOPED_ENTRY = "DS1702";
23
-
24
- /** The code a suppression that withholds nothing is reported under. */
25
- export const STALE_SUPPRESSION = "DS1703";
26
-
27
24
  /**
28
25
  * One suppression record: one code bound to one site. A directive naming two codes is two
29
26
  * records with one reason, and a directive above a line that declares two symbols is one
@@ -49,7 +46,7 @@ export interface SuppressionRecord {
49
46
  * nothing, and its finding reports the suppression as written.
50
47
  */
51
48
  export interface Refusal {
52
- readonly reported: typeof SUPPRESSION_WITHOUT_REASON | typeof UNSCOPED_ENTRY;
49
+ readonly reported: typeof CODE.suppressionWithoutReason | typeof CODE.unscopedIgnoreEntry;
53
50
  readonly code: string;
54
51
  /** The reference the entry names, empty for a directive. */
55
52
  readonly symbol: string;
@@ -71,9 +68,9 @@ export interface Suppressions {
71
68
  * instruction the analysis cannot carry out. It is a malformed input, so the command line
72
69
  * answers it with the usage code.
73
70
  */
74
- export class SuppressionError extends ConfigError {
71
+ export class SuppressionError extends UsageError {
75
72
  constructor(mechanism: Mechanism, at: string, text: string, want: string) {
76
- super("malformed", at, `${mechanism} ${at}: ${text}: want ${want}`);
73
+ super(`${mechanism} ${at}: ${text}: want ${want}`);
77
74
  this.name = "SuppressionError";
78
75
  }
79
76
  }
@@ -115,6 +112,7 @@ export const GO_REF_EXPRESSIONS: readonly string[] = [
115
112
  `^go://${GO_PATH}#${GO_IDENT}$`,
116
113
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
117
114
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
115
+ `^go://${GO_PATH}#(?:${GO_IDENT}(?:\\.${GO_IDENT})?)?\\(${GO_IDENT}\\)$`,
118
116
  `^go://${GO_PATH}#${GO_FILE}:file$`,
119
117
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
120
118
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
@@ -157,48 +155,48 @@ export interface Ledger {
157
155
  readonly withheld: (finding: Finding) => boolean;
158
156
  }
159
157
 
160
- /**
161
- * The subject kinds that are a dependency row, each with the code that reports one. An
162
- * ignore entry or a baseline row naming the row's reference and file withholds its finding
163
- * and marks nothing, and an inline directive never binds one.
164
- */
165
- const DEPENDENCY_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
166
- ["dependency", ["DS1601"]],
167
- ["module-directive", ["DS1605"]],
168
- ]);
158
+ /** The subject of the kind each live code names. */
159
+ const SUBJECT_OF_CODE: ReadonlyMap<string, Subject> = new Map(
160
+ KIND_ROWS.map((row): [string, Subject] => [row.code, row.subject]),
161
+ );
162
+
163
+ /** The subject a finding about one symbol kind is about. */
164
+ function subjectOf(kind: string): Subject | undefined {
165
+ return Object.hasOwn(SYMBOL_SUBJECTS, kind)
166
+ ? SYMBOL_SUBJECTS[kind as keyof typeof SYMBOL_SUBJECTS]
167
+ : undefined;
168
+ }
169
+
170
+ /** Whether a subject is a row of a document rather than a declaration of the program. */
171
+ function rowOf(subject: Subject | undefined): boolean {
172
+ return subject === "file" || subject === "dependency" || subject === "record";
173
+ }
169
174
 
170
175
  /**
171
- * The subject kinds that are a row of a document rather than a declaration of the program,
172
- * each with the codes that report one. Apart from a dependency row, nothing withholds a
173
- * finding about one.
176
+ * Whether a finding's subject is a row of a document rather than a declaration. Apart
177
+ * from a dependency row, nothing withholds a finding about one.
174
178
  */
175
- const ROW_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
176
- ["file", ["DS1501", "DS1502"]],
177
- ...DEPENDENCY_SUBJECTS,
178
- ["root", ["DS1704"]],
179
- ["configured-declaration", ["DS1706"]],
180
- ["suppression", ["DS1701", "DS1702", "DS1703"]],
181
- ["edge", ["DS1705"]],
182
- ]);
183
-
184
- /** Whether a finding's subject is a row of a document rather than a declaration. */
185
179
  export function isRowSubject(kind: string): boolean {
186
- return ROW_SUBJECTS.has(kind);
180
+ return rowOf(subjectOf(kind));
187
181
  }
188
182
 
189
183
  /** Whether a code reports a row of a document. */
190
184
  export function isRowCode(code: string): boolean {
191
- return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
185
+ return rowOf(SUBJECT_OF_CODE.get(code));
192
186
  }
193
187
 
194
- /** Whether a finding's subject is a dependency row. */
188
+ /**
189
+ * Whether a finding's subject is a dependency row. An ignore entry or a baseline row
190
+ * naming the row's reference and file withholds its finding and marks nothing, and an
191
+ * inline directive never binds one.
192
+ */
195
193
  export function isDependencySubject(kind: string): boolean {
196
- return DEPENDENCY_SUBJECTS.has(kind);
194
+ return subjectOf(kind) === "dependency";
197
195
  }
198
196
 
199
197
  /** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
200
198
  export function isDependencyCode(code: string): boolean {
201
- return [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
199
+ return SUBJECT_OF_CODE.get(code) === "dependency";
202
200
  }
203
201
 
204
202
  /** Whether a code reports a row of a document that no record of any mechanism can name. */
@@ -207,33 +205,20 @@ export function isUnsuppressibleCode(code: string): boolean {
207
205
  }
208
206
 
209
207
  /**
210
- * The subject kinds that are a part of a declaration rather than a declaration: a part
208
+ * Whether a finding's subject is a part of the declaration its reference names: a part
211
209
  * has no reference of its own, so a finding about one names the declaration that holds
212
210
  * it, and the part is decided inside that declaration whatever uses it.
213
211
  */
214
- const PART_SUBJECTS: ReadonlySet<string> = new Set([
215
- "parameter",
216
- "receiver",
217
- "result",
218
- "statement",
219
- "case",
220
- "store",
221
- ]);
222
-
223
- /** Whether a finding's subject is a part of the declaration its reference names. */
224
212
  export function isPartSubject(kind: string): boolean {
225
- return PART_SUBJECTS.has(kind);
213
+ return subjectOf(kind) === "part";
226
214
  }
227
215
 
228
- /** The prefix of the codes of the part kinds, the intra-function range. */
229
- const PART_KIND_PREFIX = "DS18";
230
-
231
216
  /**
232
217
  * Whether a code is a part kind, whose record withholds its finding and marks nothing:
233
218
  * the record says the part is wanted, not that the declaration holding it is.
234
219
  */
235
220
  export function isPartKind(code: string): boolean {
236
- return code.startsWith(PART_KIND_PREFIX);
221
+ return SUBJECT_OF_CODE.get(code) === "part";
237
222
  }
238
223
 
239
224
  /** One finding's identity: its code and the position of the thing it names. */
@@ -243,29 +228,33 @@ function claimKey(finding: Finding): string {
243
228
 
244
229
  /**
245
230
  * Whether one record names one finding. A record reaches a finding in two ways: it bound
246
- * the declaration at the finding's position, or it names the finding's reference and the
247
- * file the finding is reported in, each exactly. The second is how an entry copied from a
248
- * finding about a part of a declaration reaches the part, and the only way any record
249
- * reaches a dependency row.
231
+ * the declaration at the finding's position, or it names the finding's reference, or the
232
+ * reference of the declaration holding a part that has one of its own, and the file the
233
+ * finding is reported in, each exactly. The second is how an entry copied from a finding
234
+ * about a part of a declaration reaches the part, and the only way any record reaches a
235
+ * dependency row.
250
236
  */
251
237
  export function pairs(record: SuppressionRecord, finding: Finding): boolean {
252
238
  if (record.code !== finding.code) {
253
239
  return false;
254
240
  }
255
- if (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
241
+ if (isDependencySubject(finding.symbol.kind)) {
256
242
  return (
257
243
  record.mechanism !== "inline" &&
258
244
  record.symbol === finding.symbol.ref &&
259
245
  record.path === finding.position.path
260
246
  );
261
247
  }
262
- if (ROW_SUBJECTS.has(finding.symbol.kind)) {
248
+ if (isRowSubject(finding.symbol.kind)) {
263
249
  return false;
264
250
  }
265
251
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
266
252
  return true;
267
253
  }
268
- return record.symbol === finding.symbol.ref && record.path === finding.position.path;
254
+ return (
255
+ (record.symbol === finding.symbol.ref || record.symbol === finding.symbol.holder) &&
256
+ record.path === finding.position.path
257
+ );
269
258
  }
270
259
 
271
260
  /**
package/src/sweep.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { TSExemptionClass } from "./exempt-classes.ts";
10
10
  import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
11
+ import type { InventorySymbol } from "./inventory.ts";
11
12
  import type { Position } from "./position.ts";
12
13
 
13
14
  /** The liveness relation that decided a declaration, as a finding spells it. */
@@ -32,6 +33,8 @@ export interface Exemption {
32
33
  * The record holds only while one of them is called through its interface.
33
34
  */
34
35
  readonly implementing?: Implementing;
36
+ /** Whether the record holds only where a write the mode counts stores into the declaration. */
37
+ readonly whileWritten?: true;
35
38
  }
36
39
 
37
40
  /** The interface methods one retained class member implements, and whether its body is empty. */
@@ -178,23 +181,45 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
178
181
  return { liveUnder: live, candidates };
179
182
  }
180
183
 
184
+ /**
185
+ * The directory a test runner reads a manual mock from, whose exports it puts in place of
186
+ * the module's: https://jestjs.io/docs/manual-mocks
187
+ */
188
+ const MANUAL_MOCKS = "__mocks__";
189
+
190
+ /** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
191
+ export function runnerCalls(symbol: InventorySymbol): boolean {
192
+ return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
193
+ }
194
+
181
195
  /**
182
196
  * Per declaration of the graph, whether the run that counts test references reaches it.
183
- * That run's roots are the graph's own and each test file, being the file's top level
184
- * and every declaration it exports, which a test runner may call; `held` seeds it as a
185
- * mark or an exemption seeds a sweep.
197
+ * That run's roots are the graph's own and, of each test file, its top level and every
198
+ * function it exports, which a test runner may call, and every export of a manual mock,
199
+ * which the runner reads; any other export of a test file, and test-support code, which
200
+ * runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
201
+ * seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
186
202
  */
187
- export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
203
+ export function reachedWithTheTests(
204
+ graph: Graph,
205
+ held: readonly string[],
206
+ namespaces = true,
207
+ ): readonly boolean[] {
188
208
  const tests = graph.symbols.flatMap((symbol, at) => {
189
- const top =
190
- symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
191
- return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
209
+ const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
210
+ const called = exported && runnerCalls(symbol);
211
+ return graph.test[at] === true &&
212
+ graph.support[at] !== true &&
213
+ (symbol.kind === "file" || called)
192
214
  ? [{ at, kind: "test-runner" as const }]
193
215
  : [];
194
216
  });
195
- return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
196
- production: false,
197
- });
217
+ return reachable(
218
+ { ...graph, rooted: [...graph.rooted, ...tests] },
219
+ flagged(graph, held),
220
+ { production: false },
221
+ namespaces,
222
+ );
198
223
  }
199
224
 
200
225
  /**
@@ -224,7 +249,12 @@ function flagged(graph: Graph, ids: readonly string[]): boolean[] {
224
249
  * included, while a root reaches through the references the mode counts. The held seed
225
250
  * runs first, so what it reaches is expanded under the wider rule.
226
251
  */
227
- function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly boolean[] {
252
+ function reachable(
253
+ graph: Graph,
254
+ held: readonly boolean[],
255
+ mode: Mode,
256
+ namespaces = true,
257
+ ): readonly boolean[] {
228
258
  const reached = graph.symbols.map(() => false);
229
259
  const expanded = graph.symbols.map(() => false);
230
260
  const queue: number[] = [];
@@ -253,7 +283,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
253
283
  if (!holding && mode.production && edge.test) {
254
284
  continue;
255
285
  }
256
- enter(edge.to, !edge.evaluation);
286
+ enter(edge.to, namespaces && !edge.evaluation);
257
287
  }
258
288
  }
259
289
  };
@@ -269,7 +299,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
269
299
  }
270
300
  // A consumer's reference reaches what it names as one of the target's own does.
271
301
  for (const edge of callsOf(graph, mode)) {
272
- enter(edge.to, !edge.evaluation);
302
+ enter(edge.to, namespaces && !edge.evaluation);
273
303
  }
274
304
  walk(false);
275
305
  return reached;
@@ -277,10 +307,11 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
277
307
 
278
308
  /**
279
309
  * Admits every test declaration whose set of referenced production declarations is
280
- * non-empty and wholly dead, and returns which it admitted. The set is read from every
281
- * reference the test makes, which a production sweep does not count. A test-support
282
- * declaration that a test referencing a live target also references is a live target of
283
- * every test referencing it. A marked or exempt test is never admitted.
310
+ * non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
311
+ * code the target does not implement, so it is no production declaration here. The set is
312
+ * read from every reference the test makes, which a production sweep does not count. A
313
+ * test-support declaration a test referencing a live target also references is a live
314
+ * target of every test referencing it. A marked or exempt test is never admitted.
284
315
  */
285
316
  function admitTestsOfDeadCode(
286
317
  graph: Graph,
@@ -293,7 +324,11 @@ function admitTestsOfDeadCode(
293
324
  let live = 0;
294
325
  if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
295
326
  for (const edge of graph.out[at] ?? []) {
296
- if (testCode(edge.to) || graph.subject[edge.to] !== true) {
327
+ if (
328
+ testCode(edge.to) ||
329
+ graph.subject[edge.to] !== true ||
330
+ graph.ambient[edge.to] === true
331
+ ) {
297
332
  continue;
298
333
  }
299
334
  targets += 1;
@@ -2,24 +2,31 @@
2
2
  * The members a unique symbol keys, used through element accesses. `value[key]`, where
3
3
  * `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
4
4
  * whose computed name is that same constant, and a store through the access writes it.
5
- * No name node resolves to such a member: the key's name resolves to the constant.
5
+ * No name node resolves to such a member: the key's name resolves to the constant. An
6
+ * iteration reads the member its well-known iteration symbol keys.
6
7
  */
7
8
 
8
9
  import {
9
10
  isComputedPropertyName,
11
+ isForOfStatement,
10
12
  isIdentifier,
11
13
  isNoSubstitutionTemplateLiteral,
12
14
  isNumericLiteral,
13
- isParenthesizedExpression,
14
15
  isPropertyAccessExpression,
16
+ isSpreadElement,
15
17
  isStringLiteral,
16
18
  type ElementAccessExpression,
19
+ type Expression,
17
20
  type Node,
21
+ type SourceFile,
18
22
  } from "@typescript/native/ast";
23
+ import { parenthesesRemoved } from "./unwrap.ts";
19
24
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
20
25
  import type { AliasChains } from "./alias-chain.ts";
21
- import type { InventorySymbol } from "./inventory.ts";
26
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
27
+ import { renderPosition } from "./position.ts";
22
28
  import { isAnswered, type PropertyTables } from "./query.ts";
29
+ import type { Reference } from "./references.ts";
23
30
  import type { ProjectView } from "./session.ts";
24
31
 
25
32
  /** One element access whose key is a name, and how the access uses the member it selects. */
@@ -45,10 +52,7 @@ interface KeyedUse {
45
52
 
46
53
  /** The identifier an element access's argument names its key by, where it is a name. */
47
54
  export function keyNameOf(argument: Node): Node | undefined {
48
- let at = argument;
49
- while (isParenthesizedExpression(at)) {
50
- at = at.expression;
51
- }
55
+ const at = parenthesesRemoved(argument);
52
56
  if (isIdentifier(at)) {
53
57
  return at;
54
58
  }
@@ -170,3 +174,123 @@ export function keyedUses<Brand>(
170
174
  return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
171
175
  });
172
176
  }
177
+
178
+ /** The well-known symbol a value's iteration calls, by the construct that iterates it. */
179
+ type Protocol = "iterator" | "asyncIterator";
180
+
181
+ /** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
182
+ function wellKnownName(name: Node | undefined): string | undefined {
183
+ if (name === undefined || !isComputedPropertyName(name)) {
184
+ return undefined;
185
+ }
186
+ const key = name.expression;
187
+ return isPropertyAccessExpression(key) &&
188
+ isIdentifier(key.expression) &&
189
+ key.expression.text === "Symbol" &&
190
+ isIdentifier(key.name)
191
+ ? key.name.text
192
+ : undefined;
193
+ }
194
+
195
+ /**
196
+ * The references the iterations of one project's own files make to the iteration members
197
+ * of the values they iterate: a `for…of` statement and a spread element read
198
+ * `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
199
+ * which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
200
+ */
201
+ export function iterationReferences<Brand>(
202
+ project: ProjectView<Brand>,
203
+ held: Inventory,
204
+ chains: AliasChains,
205
+ root: string,
206
+ testFiles: ReadonlySet<string>,
207
+ ): readonly Reference[] {
208
+ const queries = project.queries;
209
+ const ownPaths = project.ownPaths();
210
+ const iterated: {
211
+ readonly file: SourceFile;
212
+ readonly from: string;
213
+ readonly operand: Expression;
214
+ readonly protocols: readonly Protocol[];
215
+ }[] = [];
216
+ for (const file of project.ownSourceFiles()) {
217
+ const fileId = held.declarations.get(nodeKey(file, file));
218
+ if (fileId === undefined) {
219
+ continue;
220
+ }
221
+ const visit = (node: Node, from: string): void => {
222
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
223
+ if (isForOfStatement(node)) {
224
+ const protocols: readonly Protocol[] =
225
+ node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
226
+ iterated.push({ file, from: holder, operand: node.expression, protocols });
227
+ } else if (isSpreadElement(node)) {
228
+ iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
229
+ }
230
+ node.forEachChild((child) => {
231
+ visit(child, holder);
232
+ });
233
+ };
234
+ file.forEachChild((child) => {
235
+ visit(child, fileId);
236
+ });
237
+ }
238
+ if (iterated.length === 0) {
239
+ return [];
240
+ }
241
+ const types = queries.typesAt(iterated.map((one) => one.operand));
242
+ const members = new Map<
243
+ number,
244
+ readonly { readonly name: string; readonly ids: readonly string[] }[]
245
+ >();
246
+ const membersOf = (
247
+ type: Type,
248
+ ): readonly { readonly name: string; readonly ids: readonly string[] }[] => {
249
+ const known = members.get(type.id);
250
+ if (known !== undefined) {
251
+ return known;
252
+ }
253
+ const keyed: { name: string; ids: readonly string[] }[] = [];
254
+ const properties = queries.propertiesOf(type);
255
+ for (const property of isAnswered(properties) ? properties : []) {
256
+ for (const handle of property.declarations) {
257
+ const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
258
+ const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
259
+ if (name !== undefined) {
260
+ keyed.push({ name, ids: chains.declarationsOf(property) });
261
+ break;
262
+ }
263
+ }
264
+ }
265
+ members.set(type.id, keyed);
266
+ return keyed;
267
+ };
268
+ const found: Reference[] = [];
269
+ iterated.forEach((one, index) => {
270
+ const type = types[index];
271
+ if (type === undefined || !isAnswered(type)) {
272
+ return;
273
+ }
274
+ const parts =
275
+ type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
276
+ const position = renderPosition(one.file, root, one.operand.getStart());
277
+ for (const part of isAnswered(parts) ? parts : []) {
278
+ for (const member of membersOf(part)) {
279
+ if (!(one.protocols as readonly string[]).includes(member.name)) {
280
+ continue;
281
+ }
282
+ for (const to of member.ids) {
283
+ found.push({
284
+ from: one.from,
285
+ to,
286
+ position,
287
+ use: "read",
288
+ resolution: "keyed",
289
+ test: testFiles.has(position.path),
290
+ });
291
+ }
292
+ }
293
+ }
294
+ });
295
+ return found;
296
+ }
@@ -13,13 +13,16 @@
13
13
  */
14
14
 
15
15
  import { readComponent } from "./component-files.ts";
16
- import { ConfigError, type Analysis, type TemplateDelimiters } from "./config.ts";
16
+ import type { ResolvedConfig, TemplateDelimiters } from "./config.ts";
17
17
  import type { DetectorInput, Evidence } from "./exempt.ts";
18
18
  import type { Host } from "./host.ts";
19
19
  import type { SymbolKind } from "./inventory.ts";
20
20
  import { memberNames } from "./member-names.ts";
21
21
  import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
+ import { bytewise } from "./text/compare.ts";
24
+ import { filesBelow } from "./fs/tree.ts";
25
+ import { UsageError } from "./usage.ts";
23
26
 
24
27
  /** One template, by its path below the target root. */
25
28
  interface TemplateFile {
@@ -50,14 +53,6 @@ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE,
50
53
  /** One identifier, as the grammar of the analyzed language spells one. */
51
54
  const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
52
55
 
53
- /** Two strings ordered bytewise. */
54
- function compare(a: string, b: string): number {
55
- if (a === b) {
56
- return 0;
57
- }
58
- return a < b ? -1 : 1;
59
- }
60
-
61
56
  /**
62
57
  * Every template under the configured directories, read once for the run, in path
63
58
  * order and each file once. A directory is named relative to the target root, and
@@ -66,25 +61,22 @@ function compare(a: string, b: string): number {
66
61
  * exemption it was configured for off. A symbolic link to a directory is not
67
62
  * followed.
68
63
  */
69
- export function readTemplates(host: Host, targetRoot: string, analysis: Analysis): Templates {
64
+ export function readTemplates(
65
+ host: Host,
66
+ targetRoot: string,
67
+ analysis: ResolvedConfig["analysis"],
68
+ ): Templates {
70
69
  const files = new Map<string, string>();
71
70
  const read = (dir: string): void => {
72
- const entries = [...host.readDirectory(joinPath(targetRoot, dir))].sort((a, b) =>
73
- compare(a.name, b.name),
74
- );
75
- for (const entry of entries) {
76
- const path = joinPath(dir, entry.name);
77
- if (entry.directory) {
78
- read(path);
79
- continue;
80
- }
71
+ for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
72
+ const path = joinPath(dir, below);
81
73
  const absolute = joinPath(targetRoot, path);
82
74
  if (!files.has(path) && host.kindOf(absolute) === "file") {
83
75
  files.set(path, host.readFile(absolute));
84
76
  }
85
77
  }
86
78
  };
87
- for (const configured of analysis.templateDirs) {
79
+ for (const configured of analysis.template_dirs) {
88
80
  const dir = normalizePath(configured);
89
81
  if (
90
82
  isAbsolutePath(dir) ||
@@ -92,19 +84,17 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
92
84
  dir.startsWith("../") ||
93
85
  host.kindOf(joinPath(targetRoot, dir)) !== "directory"
94
86
  ) {
95
- throw new ConfigError(
96
- "malformed",
97
- "analysis.template_dirs",
87
+ throw new UsageError(
98
88
  `analysis.template_dirs: ${JSON.stringify(configured)} is not a directory below the target root`,
99
89
  );
100
90
  }
101
91
  read(dir);
102
92
  }
103
93
  return {
104
- delimiters: analysis.templateDelimiters,
94
+ delimiters: analysis.template_delimiters,
105
95
  files: [...files]
106
96
  .map(([path, text]) => ({ path, text }))
107
- .sort((a, b) => compare(a.path, b.path)),
97
+ .sort((a, b) => bytewise(a.path, b.path)),
108
98
  };
109
99
  }
110
100