@cplieger/deadset-ts 8.0.0-dev.15 → 8.0.0-dev.17

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 (207) hide show
  1. package/dist/src/alias-chain.js +0 -5
  2. package/dist/src/amd.js +81 -0
  3. package/dist/src/analysis.js +94 -54
  4. package/dist/src/augmentations.js +23 -24
  5. package/dist/src/bindings.js +382 -0
  6. package/dist/src/calls.js +25 -8
  7. package/dist/src/commands/read.js +6 -7
  8. package/dist/src/commands/tools.js +5 -9
  9. package/dist/src/component-files.js +3 -2
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/consumers.js +9 -10
  12. package/dist/src/contextual-uses.js +1 -17
  13. package/dist/src/contract/config.generated.js +5 -2
  14. package/dist/src/contract/contract.generated.js +119 -2
  15. package/dist/src/contract/report.generated.js +6 -2
  16. package/dist/src/dependencies.js +98 -71
  17. package/dist/src/destructuring.js +1 -19
  18. package/dist/src/element-access.js +179 -164
  19. package/dist/src/entries/build-scripts.js +222 -0
  20. package/dist/src/{configuration-files.js → entries/configuration-files.js} +22 -64
  21. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  22. package/dist/src/entries/conventions.js +739 -0
  23. package/dist/src/{html-entries.js → entries/html-entries.js} +9 -12
  24. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +3 -3
  25. package/dist/src/entries/run-directories.js +77 -0
  26. package/dist/src/entries/sources.js +144 -0
  27. package/dist/src/entries/static-module.js +670 -0
  28. package/dist/src/entries/test-runners.js +684 -0
  29. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  30. package/dist/src/extensions.js +37 -0
  31. package/dist/src/facts.js +38 -0
  32. package/dist/src/findings/completion.js +40 -4
  33. package/dist/src/findings/emitters.js +5 -8
  34. package/dist/src/{implementations.js → findings/implementations.js} +3 -3
  35. package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
  36. package/dist/src/findings/self-check.js +36 -20
  37. package/dist/src/findings-pass.js +5 -6
  38. package/dist/src/framework-lifecycle.js +42 -15
  39. package/dist/src/fs/manifest.js +15 -20
  40. package/dist/src/generated-file.js +172 -29
  41. package/dist/src/graph.js +14 -8
  42. package/dist/src/import-bindings.js +73 -28
  43. package/dist/src/index/sites.js +557 -0
  44. package/dist/src/index.js +1 -1
  45. package/dist/src/indexed-access.js +523 -478
  46. package/dist/src/injection-container.js +1 -1
  47. package/dist/src/interface-satisfaction.js +1 -1
  48. package/dist/src/inventory/commonjs.js +62 -17
  49. package/dist/src/inventory/enumerate.js +2 -1
  50. package/dist/src/javascript-file.js +26 -11
  51. package/dist/src/keyof-comparisons.js +124 -103
  52. package/dist/src/load/classify.js +28 -9
  53. package/dist/src/load/diagnostics.js +8 -1
  54. package/dist/src/load/discover.js +15 -7
  55. package/dist/src/load/expected-module.js +2 -2
  56. package/dist/src/load/plan.js +2 -2
  57. package/dist/src/load/specifiers.js +2 -4
  58. package/dist/src/load/synthesize.js +10 -5
  59. package/dist/src/load/workspace-manifests.js +1 -1
  60. package/dist/src/load/workspace-resolution.js +2 -2
  61. package/dist/src/matrix.js +12 -17
  62. package/dist/src/module-declarations.js +70 -85
  63. package/dist/src/overrides.js +134 -112
  64. package/dist/src/paths.js +56 -1
  65. package/dist/src/platform-calls.js +78 -0
  66. package/dist/src/platform-symbols.js +134 -0
  67. package/dist/src/references/read-rule.js +1 -0
  68. package/dist/src/references/read-rules.js +48 -0
  69. package/dist/src/references/sink.js +151 -0
  70. package/dist/src/references/sources.js +450 -0
  71. package/dist/src/references.js +111 -1116
  72. package/dist/src/resolve.js +1 -1
  73. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +7 -4
  74. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  75. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  76. package/dist/src/roots/published-reach.js +412 -0
  77. package/dist/src/roots/root-set.js +489 -0
  78. package/dist/src/roots/unseen-loads.js +236 -0
  79. package/dist/src/serialization-contract.js +9 -4
  80. package/dist/src/source-files.js +5 -9
  81. package/dist/src/stores.js +4 -2
  82. package/dist/src/suppress.js +30 -32
  83. package/dist/src/sweep.js +25 -13
  84. package/dist/src/symbol-keys.js +89 -83
  85. package/dist/src/tests/file-class.js +76 -0
  86. package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
  87. package/dist/src/{test-support.js → tests/support.js} +5 -12
  88. package/dist/src/type-parameter-reads.js +178 -139
  89. package/dist/src/unimplemented-callees.js +262 -242
  90. package/dist/src/verbs/analyze.js +15 -3
  91. package/dist/src/verbs/describe.js +5 -3
  92. package/dist/src/verbs/explain.js +50 -28
  93. package/dist/src/verbs/print-retained.js +2 -2
  94. package/jsr.json +1 -1
  95. package/package.json +3 -3
  96. package/src/alias-chain.ts +0 -7
  97. package/src/amd.ts +130 -0
  98. package/src/analysis.ts +156 -70
  99. package/src/augmentations.ts +41 -27
  100. package/src/automatic-types.ts +1 -1
  101. package/src/bindings.ts +460 -0
  102. package/src/calls.ts +25 -7
  103. package/src/commands/read.ts +6 -8
  104. package/src/commands/tools.ts +5 -11
  105. package/src/component-files.ts +4 -2
  106. package/src/conformance-record.ts +1 -1
  107. package/src/consumers.ts +12 -11
  108. package/src/contextual-uses.ts +2 -22
  109. package/src/contract/config.generated.ts +5 -2
  110. package/src/contract/contract.generated.ts +138 -2
  111. package/src/contract/report.generated.ts +6 -2
  112. package/src/dependencies.ts +117 -71
  113. package/src/destructuring.ts +1 -31
  114. package/src/element-access.ts +188 -182
  115. package/src/entries/build-scripts.ts +292 -0
  116. package/src/{configuration-files.ts → entries/configuration-files.ts} +33 -82
  117. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  118. package/src/{conventions.ts → entries/conventions.ts} +349 -523
  119. package/src/{html-entries.ts → entries/html-entries.ts} +19 -18
  120. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +5 -5
  121. package/src/entries/run-directories.ts +108 -0
  122. package/src/entries/sources.ts +266 -0
  123. package/src/entries/static-module.ts +858 -0
  124. package/src/entries/test-runners.ts +868 -0
  125. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  126. package/src/exempt.ts +3 -2
  127. package/src/extensions.ts +45 -0
  128. package/src/facts.ts +46 -0
  129. package/src/file-facts.ts +1 -1
  130. package/src/findings/boundary.ts +2 -0
  131. package/src/findings/completion.ts +43 -4
  132. package/src/findings/emitter.ts +1 -1
  133. package/src/findings/emitters.ts +14 -10
  134. package/src/{implementations.ts → findings/implementations.ts} +4 -4
  135. package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
  136. package/src/findings/self-check.ts +41 -22
  137. package/src/findings/visibility-narrowing.ts +1 -1
  138. package/src/findings-pass.ts +5 -12
  139. package/src/framework-lifecycle.ts +43 -15
  140. package/src/fs/manifest.ts +25 -21
  141. package/src/generated-file.ts +240 -36
  142. package/src/graph.ts +27 -11
  143. package/src/import-bindings.ts +98 -29
  144. package/src/index/sites.ts +781 -0
  145. package/src/index.ts +1 -1
  146. package/src/indexed-access.ts +570 -527
  147. package/src/injection-container.ts +1 -1
  148. package/src/interface-satisfaction.ts +1 -1
  149. package/src/inventory/commonjs.ts +102 -17
  150. package/src/inventory/enumerate.ts +2 -1
  151. package/src/javascript-file.ts +29 -12
  152. package/src/keyof-comparisons.ts +131 -116
  153. package/src/load/classify.ts +49 -13
  154. package/src/load/diagnostics.ts +11 -1
  155. package/src/load/discover.ts +27 -11
  156. package/src/load/expected-module.ts +8 -2
  157. package/src/load/plan.ts +11 -2
  158. package/src/load/specifiers.ts +8 -5
  159. package/src/load/synthesize.ts +12 -6
  160. package/src/load/test-globals.ts +1 -1
  161. package/src/load/workspace-manifests.ts +1 -1
  162. package/src/load/workspace-resolution.ts +9 -2
  163. package/src/load/workspace-run.ts +1 -1
  164. package/src/matrix.ts +13 -17
  165. package/src/module-declarations.ts +74 -98
  166. package/src/overrides.ts +162 -134
  167. package/src/paths.ts +65 -1
  168. package/src/platform-calls.ts +119 -0
  169. package/src/platform-symbols.ts +189 -0
  170. package/src/references/read-rule.ts +60 -0
  171. package/src/references/read-rules.ts +55 -0
  172. package/src/references/sink.ts +213 -0
  173. package/src/references/sources.ts +632 -0
  174. package/src/references.ts +174 -1492
  175. package/src/report.ts +2 -2
  176. package/src/resolve.ts +1 -1
  177. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +10 -7
  178. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  179. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  180. package/src/roots/published-reach.ts +481 -0
  181. package/src/{roots.ts → roots/root-set.ts} +203 -630
  182. package/src/roots/unseen-loads.ts +345 -0
  183. package/src/serialization-contract.ts +9 -4
  184. package/src/source-files.ts +5 -10
  185. package/src/stores.ts +13 -2
  186. package/src/suppress.ts +48 -33
  187. package/src/sweep.ts +37 -14
  188. package/src/symbol-keys.ts +102 -102
  189. package/src/tests/file-class.ts +118 -0
  190. package/src/{outside-tests.ts → tests/outside.ts} +16 -10
  191. package/src/{test-support.ts → tests/support.ts} +15 -21
  192. package/src/type-parameter-reads.ts +187 -158
  193. package/src/unimplemented-callees.ts +297 -280
  194. package/src/verbs/analyze.ts +19 -5
  195. package/src/verbs/describe.ts +5 -2
  196. package/src/verbs/explain.ts +75 -32
  197. package/src/verbs/print-retained.ts +2 -2
  198. package/dist/src/build-scripts.js +0 -418
  199. package/dist/src/conventions.js +0 -907
  200. package/dist/src/entry-points.js +0 -444
  201. package/dist/src/own-body.js +0 -28
  202. package/dist/src/roots.js +0 -865
  203. package/dist/src/unseen-loads.js +0 -189
  204. package/src/build-scripts.ts +0 -536
  205. package/src/entry-points.ts +0 -584
  206. package/src/own-body.ts +0 -42
  207. package/src/unseen-loads.ts +0 -282
@@ -4,11 +4,11 @@
4
4
  * it runs in. A workflow or hook manager file is read by `text/yaml.ts`, and one it does not
5
5
  * read gives no command; each command is read by `commands/read.ts`.
6
6
  */
7
- import { pathsNamed, readCommands } from "./commands/read.js";
8
- import { isRecord } from "./json-document.js";
9
- import { joinPath, resolvePath } from "./paths.js";
10
- import { filesIn } from "./fs/tree.js";
11
- import { yamlDocument } from "./text/yaml.js";
7
+ import { pathsNamed, readCommands } from "../commands/read.js";
8
+ import { isRecord } from "../json-document.js";
9
+ import { joinPath, resolvePath } from "../paths.js";
10
+ import { filesIn } from "../fs/tree.js";
11
+ import { yamlDocument } from "../text/yaml.js";
12
12
  const WORKFLOWS = ".github/workflows";
13
13
  /** The directories whose every file is a git hook script, run as the shell runs it. */
14
14
  const HOOK_DIRS = [".husky", ".githooks"];
@@ -144,14 +144,24 @@ export function commandFileTexts(host, targetRoot) {
144
144
  ...[makefileAt(host, root) ?? []].flat(),
145
145
  ].flatMap((path) => readText(host, path) ?? []);
146
146
  }
147
+ /** Reads the target root's workflow steps, git hooks, hook managers and makefile. */
148
+ export function readWorkflows(host, targetRoot) {
149
+ return {
150
+ readings: workflowCommands(host, targetRoot).map(({ text, dir, writtenDirectory }) => {
151
+ const reading = readCommands(text, dir);
152
+ return { ...reading, held: [reading.held, writtenDirectory ?? ""].join("\n") };
153
+ }),
154
+ texts: commandFileTexts(host, targetRoot),
155
+ };
156
+ }
147
157
  /**
148
158
  * The own files the target root's workflow steps, git hooks and recipes run or name, as
149
159
  * `pathsNamed` reads each command.
150
160
  */
151
- export function workflowEntries(host, targetRoot, files) {
161
+ export function workflowEntries(workflows, files) {
152
162
  const found = [];
153
- for (const { text, dir } of workflowCommands(host, targetRoot)) {
154
- for (const { path, written } of pathsNamed(readCommands(text, dir))) {
163
+ for (const reading of workflows.readings) {
164
+ for (const { path, written } of pathsNamed(reading)) {
155
165
  for (const file of files.named(path)) {
156
166
  found.push({ file, source: written });
157
167
  }
@@ -0,0 +1,37 @@
1
+ /** The extension lists the analysis reads files by, each written once. */
2
+ /**
3
+ * The extensions tried, in order, after a path written without one: a relative specifier
4
+ * of a configuration file (`roots.5`) and a path a command runs or loads (`roots.13`).
5
+ */
6
+ export const APPENDED_EXTENSIONS = [
7
+ ".ts",
8
+ ".tsx",
9
+ ".mts",
10
+ ".cts",
11
+ ".js",
12
+ ".jsx",
13
+ ".mjs",
14
+ ".cjs",
15
+ ];
16
+ /** The extensions of a module a tool runs or loads as its configuration (`roots.2`). */
17
+ export const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
18
+ /** The extensions of a file a build writes from a source file. */
19
+ export const OUTPUT_EXTENSIONS = [
20
+ ".js",
21
+ ".mjs",
22
+ ".cjs",
23
+ ".jsx",
24
+ ".d.ts",
25
+ ".d.mts",
26
+ ".d.cts",
27
+ ];
28
+ /** One list as a glob's brace list, its dots dropped: `{js,mjs}`. */
29
+ export function braced(extensions) {
30
+ return `{${extensions.map((one) => one.slice(1)).join(",")}}`;
31
+ }
32
+ /** The extension of one list a name ends with, the longest where several do. */
33
+ export function extensionOf(name, extensions) {
34
+ return extensions
35
+ .filter((one) => name.endsWith(one))
36
+ .reduce((longest, one) => (one.length > (longest?.length ?? 0) ? one : longest), undefined);
37
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The facts document `--facts` writes, `bindings.1` and `bindings.2`: the run's generated
3
+ * files under the one decision the generated-file class reads, and the binding identities
4
+ * they publish. No binding state is computed, so it states `evaluated: false` (`bindings.10`).
5
+ */
6
+ import { FACTS_VERSION } from "./contract/contract.generated.js";
7
+ import { LANGUAGE, PRODUCT } from "./product.js";
8
+ import { bytewise } from "./text/compare.js";
9
+ /** The facts document of one run, as the text written to the path `--facts` names. */
10
+ export function factsDocument(version, generated, bindings) {
11
+ const document = {
12
+ facts_version: FACTS_VERSION,
13
+ analyzer: { name: PRODUCT, version },
14
+ language: LANGUAGE,
15
+ evaluated: false,
16
+ generated: [...generated]
17
+ .sort(([a], [b]) => bytewise(a, b))
18
+ .map(([path, fact]) => ({
19
+ path,
20
+ line: fact.line,
21
+ evidence: fact.evidence,
22
+ detail: fact.detail,
23
+ ...(fact.generator === undefined
24
+ ? {}
25
+ : {
26
+ generator: {
27
+ row: fact.generator.row,
28
+ ...(fact.generator.version === undefined
29
+ ? {}
30
+ : { version: fact.generator.version }),
31
+ },
32
+ schema_files: fact.generator.schemaFiles,
33
+ }),
34
+ })),
35
+ bindings,
36
+ };
37
+ return `${JSON.stringify(document, null, 2)}\n`;
38
+ }
@@ -79,12 +79,13 @@ function testedByIn(input) {
79
79
  * published API, a public member of a published type included, is `certain` where the
80
80
  * run loaded every consumer the scope declared and declared one, `probable` where one did
81
81
  * not load, and `possible` where it declared none, whatever the configuration says of the
82
- * consumer set. A member an `in` expression tests by name, and one a function outside the
83
- * analysis fills, is `possible`.
82
+ * consumer set. A member an `in` expression tests by name, one a function outside the
83
+ * analysis fills, and one {@link possiblyLoaded} names, is `possible`.
84
84
  */
85
85
  function reachabilityClasses(input) {
86
86
  const tested = testedByIn(input);
87
87
  const filled = input.boundary.filledOutside ?? new Set();
88
+ const possible = possiblyLoaded(input);
88
89
  const published = publishedIds(input.swept.matrix.union);
89
90
  const { declared } = input.boundary.consumers;
90
91
  const outside = declared.length === 0
@@ -93,7 +94,7 @@ function reachabilityClasses(input) {
93
94
  ? "certain"
94
95
  : "probable";
95
96
  return (id) => {
96
- if (tested.has(id) || filled.has(id)) {
97
+ if (tested.has(id) || filled.has(id) || possible.has(id)) {
97
98
  return "possible";
98
99
  }
99
100
  return published.has(id) ? outside : "certain";
@@ -137,6 +138,35 @@ function testCeilings(input, classOf) {
137
138
  ? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
138
139
  : "certain";
139
140
  }
141
+ /**
142
+ * What only a bundler's retry of an import Node leaves unresolved names (program.38), and each
143
+ * dead declaration that reaches through dead declarations, as confidence.20 reads what only
144
+ * tests reach: each is dead only while no loader the analysis does not see loads the file.
145
+ */
146
+ function possiblyLoaded(input) {
147
+ const seeds = input.boundary.possiblyLoaded ?? new Set();
148
+ if (seeds.size === 0) {
149
+ return seeds;
150
+ }
151
+ const union = input.swept.matrix.union;
152
+ const dead = new Set(input.swept.sweep.candidates.map((one) => one.id));
153
+ const reached = new Set();
154
+ const pending = union.symbols.flatMap((symbol, at) => (seeds.has(symbol.id) ? [at] : []));
155
+ for (let at = pending.pop(); at !== undefined; at = pending.pop()) {
156
+ const symbol = union.symbols[at];
157
+ if (symbol === undefined || reached.has(symbol.id)) {
158
+ continue;
159
+ }
160
+ reached.add(symbol.id);
161
+ for (const edge of union.out[at] ?? []) {
162
+ const to = union.symbols[edge.to];
163
+ if (to !== undefined && dead.has(to.id)) {
164
+ pending.push(edge.to);
165
+ }
166
+ }
167
+ }
168
+ return reached;
169
+ }
140
170
  /**
141
171
  * The subjects dead only because production code only tests use is: each declaration the
142
172
  * sweep marks test-used, each `DS1602` file of such code, and each test of dead code every
@@ -281,6 +311,7 @@ export function completed(input, findings, kinds = KINDS) {
281
311
  const union = swept.matrix.union;
282
312
  const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
283
313
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
314
+ const fileIds = new Map(union.symbols.flatMap((one) => (one.kind === "file" ? [[one.position.path, one.id]] : [])));
284
315
  const classOf = reachabilityClasses(input);
285
316
  const ceilingOf = testCeilings(input, classOf);
286
317
  const testUsed = testUsedSubjects(input);
@@ -365,7 +396,12 @@ export function completed(input, findings, kinds = KINDS) {
365
396
  const counts = candidate === undefined
366
397
  ? union.made[at]
367
398
  : { production: candidate.productionRefs, test: candidate.testRefs };
368
- const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
399
+ const ownClass = judged !== undefined
400
+ ? classOf(judged.id)
401
+ : (finding.reachabilityClass ??
402
+ (finding.code === CODE.fileNeverImported
403
+ ? classOf(fileIds.get(finding.position.path) ?? "")
404
+ : "certain"));
369
405
  const used = judged !== undefined
370
406
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
371
407
  : finding.code === CODE.fileNeverImported && testUsed.files.has(finding.position.path);
@@ -96,16 +96,13 @@ function atPossible(input) {
96
96
  */
97
97
  function dialledOver(input, suppressed, found, unmarked) {
98
98
  const would = [...unmarked, ...found];
99
- const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
100
99
  const unanswered = heldKeys(input);
100
+ const heldIds = new Set(input.swept.heldByUnanswered);
101
+ const keyOf = (finding) => declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
102
+ const held = (finding) => unanswered.has(keyOf(finding)) || insideSkipped(input.swept.skipped, finding.position);
103
+ const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked), { withholds: held, holdsRecord: (record) => heldIds.has(record.bound) });
101
104
  const markup = markupKeys(input);
102
- const candidates = found.filter((finding) => {
103
- const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
104
- return (!ledger.withheld(finding) &&
105
- !unanswered.has(key) &&
106
- !markup.has(key) &&
107
- !insideSkipped(input.swept.skipped, finding.position));
108
- });
105
+ const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(keyOf(finding)));
109
106
  const kept = reportable(input, candidates);
110
107
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
111
108
  return {
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodDeclaration, SyntaxKind, } from "@typescript/native/ast";
9
9
  import { SymbolFlags } from "@typescript/native/sync";
10
- import { aliasChains } from "./alias-chain.js";
11
- import { nodeKey } from "./inventory.js";
12
- import { must, Unanswerable } from "./query.js";
10
+ import { aliasChains } from "../alias-chain.js";
11
+ import { nodeKey } from "../inventory.js";
12
+ import { must, Unanswerable } from "../query.js";
13
13
  /** The interfaces, the classes and the `implements` entries of one project's own files. */
14
14
  function walk(project, held) {
15
15
  const interfaces = [];
@@ -4,8 +4,8 @@
4
4
  * links the read to a declaration, so only a text match can tell which member it reads.
5
5
  */
6
6
  import { isBinaryExpression, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
7
- import { sourceFilesOf } from "./source-files.js";
8
- import { bytewise } from "./text/compare.js";
7
+ import { sourceFilesOf } from "../source-files.js";
8
+ import { bytewise } from "../text/compare.js";
9
9
  /**
10
10
  * The text of each string literal, or template literal with no substitution, written as
11
11
  * the left operand of an `in` expression in a file of the project that is no test file.
@@ -1,7 +1,7 @@
1
- import { CODE } from "../contract/contract.generated.js";
1
+ import { CODE, REMEDIES } from "../contract/contract.generated.js";
2
2
  import { REPOSITORY_DOCUMENT } from "../config.js";
3
3
  import { FIXED_SEVERITY_CODES } from "../kinds.js";
4
- import { isDependencyCode, isUnsuppressibleCode, } from "../suppress.js";
4
+ import { names, subjectOfCode, } from "../suppress.js";
5
5
  import { bytewise } from "../text/compare.js";
6
6
  /** The subject kind of a finding about a configured declaration. */
7
7
  const DECLARATION_SUBJECT = "configured-declaration";
@@ -24,6 +24,39 @@ export function fileRefsOf(symbols) {
24
24
  .map((symbol) => [symbol.position.path, symbol.ref]));
25
25
  return (path) => byPath.get(path);
26
26
  }
27
+ /**
28
+ * Why a record of `mechanism` naming `codes` can match nothing, one clause per subject whose
29
+ * findings kinds.json's remedies table lets no record of that mechanism name, in subject order.
30
+ */
31
+ function unnameable(codes, mechanism) {
32
+ return Object.keys(REMEDIES).flatMap((subject) => {
33
+ const named = codes.filter((code) => subjectOfCode(code) === subject);
34
+ if (named.length === 0 || names(subject, mechanism)) {
35
+ return [];
36
+ }
37
+ const { names: what, suppressions } = REMEDIES[subject];
38
+ const reports = `${spelled(named)} ${named.length === 1 ? "reports" : "each report"} ${what}`;
39
+ if (suppressions.length > 0) {
40
+ const records = suppressions.map((one) => articled(WRITTEN[one]));
41
+ return [`${reports}, which only ${spelledOr(records)} can name`];
42
+ }
43
+ const settable = named.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
44
+ const remedy = settable.length === 0
45
+ ? "fix what the finding names instead"
46
+ : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
47
+ return [`${reports}, which no suppression can name; ${remedy}`];
48
+ });
49
+ }
50
+ /** A record's written name with its indefinite article. */
51
+ function articled(name) {
52
+ return `${/^[aeiou]/u.test(name) ? "an" : "a"} ${name}`;
53
+ }
54
+ /** A list as a sentence names alternatives. */
55
+ function spelledOr(items) {
56
+ return items.length < 2
57
+ ? items.join("")
58
+ : `${items.slice(0, -1).join(", ")} or ${items.at(-1) ?? ""}`;
59
+ }
27
60
  /** A list of codes as a sentence names them. */
28
61
  function spelled(codes) {
29
62
  if (codes.length < 2) {
@@ -85,25 +118,8 @@ export function staleSuppressions(stale, files) {
85
118
  const reports = [...new Set(records.flatMap((one) => one.reports))]
86
119
  .filter((code) => !codes.includes(code))
87
120
  .sort(bytewise);
88
- const rows = codes.filter(isUnsuppressibleCode);
89
- const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
90
- const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
91
- const remedy = settable.length === 0
92
- ? "fix what the finding names instead"
93
- : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
94
121
  const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
95
- const because = [
96
- ...(rows.length > 0
97
- ? [
98
- `${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}`,
99
- ]
100
- : []),
101
- ...(dependencies.length > 0
102
- ? [
103
- `${spelled(dependencies)} ${dependencies.length === 1 ? "reports a dependency row" : "report dependency rows"}, which only an ignore entry or a baseline row can name`,
104
- ]
105
- : []),
106
- ];
122
+ const because = unnameable(codes, first.mechanism);
107
123
  const instead = because.length > 0
108
124
  ? `, because ${because.join("; ")}${declared === "" ? "" : `; ${declared}`}`
109
125
  : declared === ""
@@ -7,7 +7,7 @@ import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.gen
7
7
  import { fails } from "./config.js";
8
8
  import { decidedFindings } from "./findings/emitters.js";
9
9
  import { fileRefsOf, staleSuppressions, } from "./findings/self-check.js";
10
- import { isDependencySubject, isRowSubject, pairs, suppressionCounts, } from "./suppress.js";
10
+ import { namesKind, pairs, suppressionCounts } from "./suppress.js";
11
11
  import { bytewise } from "./text/compare.js";
12
12
  import { PRODUCT } from "./product.js";
13
13
  /** The canonical order: path, line, column, code, then the symbol reference. */
@@ -71,15 +71,14 @@ export function verdictOf(result, config) {
71
71
  }
72
72
  /**
73
73
  * The rows one round of a baseline write records from its findings, in their order. A
74
- * finding about a row of a document other than a dependency row is not recorded: no record
75
- * withholds one, so a row recording it would be stale on the first read back. Two findings
76
- * that would record the same row, two parts of one declaration under one code, record it
77
- * once.
74
+ * finding no baseline row can name is not recorded: a row recording it would be stale on
75
+ * the first read back. Two findings that would record the same row, two parts of one
76
+ * declaration under one code, record it once.
78
77
  */
79
78
  export function recordedFindings(findings) {
80
79
  const rows = new Map();
81
80
  for (const finding of findings) {
82
- if (isRowSubject(finding.symbol.kind) && !isDependencySubject(finding.symbol.kind)) {
81
+ if (!namesKind(finding.symbol.kind, "baseline")) {
83
82
  continue;
84
83
  }
85
84
  const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
@@ -1,18 +1,42 @@
1
1
  /**
2
- * The framework-lifecycle class: a member a `ts.lifecycle_contracts` entry lists is held
3
- * back on each class that is the entry's component, being a class a decorator or a call
4
- * the entry's `components` name is attached to or passes, or one extending, directly or
5
- * through a chain, a class its `bases` name. A component's members are its type's, so a
6
- * member a base class declares answers for every subclass.
2
+ * The framework-lifecycle class: a member a lifecycle contract lists is held back on each
3
+ * class that is the contract's component, being a class a decorator or a call the
4
+ * contract's `components` name is attached to or passes, or one extending, directly or
5
+ * through a chain, a class its `bases` name. The contracts are the platform's custom
6
+ * elements and each `ts.lifecycle_contracts` entry. A component's members are its type's,
7
+ * so a member a base class declares answers for every subclass.
7
8
  */
8
9
  import { isClassDeclaration, isClassExpression, isHeritageClause, SyntaxKind, } from "@typescript/native/ast";
9
10
  import { aliasChains } from "./alias-chain.js";
10
11
  import { calleeName, callsOf, classesPassed, decoratedClasses, decoratorCallee } from "./calls.js";
11
12
  import { classMembers, memberComponent, typesAt } from "./class-members.js";
12
- import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
13
+ import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } from "./roots/configured-declarations.js";
13
14
  import { nodeKey } from "./inventory.js";
14
15
  import { renderPosition } from "./position.js";
15
16
  import { must } from "./query.js";
17
+ /**
18
+ * The platform's own contract (references-ts.38): `CustomElementRegistry.define` reads
19
+ * these members off the class it registers, its constructor's and its prototype's
20
+ * (https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define),
21
+ * and calls each callback as a custom element reaction.
22
+ */
23
+ const CUSTOM_ELEMENTS = {
24
+ components: [{ global: "CustomElementRegistry.define" }],
25
+ members: [
26
+ "observedAttributes:static",
27
+ "disabledFeatures:static",
28
+ "formAssociated:static",
29
+ "connectedCallback",
30
+ "disconnectedCallback",
31
+ "connectedMoveCallback",
32
+ "adoptedCallback",
33
+ "attributeChangedCallback",
34
+ "formAssociatedCallback",
35
+ "formResetCallback",
36
+ "formDisabledCallback",
37
+ "formStateRestoreCallback",
38
+ ],
39
+ };
16
40
  /** Every contract's components and bases, resolved in one pass over the entries. */
17
41
  function resolveContracts(project, held, contracts) {
18
42
  const entries = contracts.flatMap((contract) => [
@@ -155,16 +179,17 @@ function byBases(project, held, chains, contracts) {
155
179
  }
156
180
  /**
157
181
  * The framework-lifecycle detector: each member a contract lists, on each class that
158
- * is a component of that contract's framework, recorded where the class was found to
159
- * be one.
182
+ * is a component of that contract's framework, recorded once per place where the class
183
+ * was found to be one.
160
184
  */
161
185
  export function frameworkLifecycle(input) {
162
186
  const { project, held, targetRoot } = input;
163
- if (input.ts.lifecycle_contracts.length === 0) {
187
+ const declared = [CUSTOM_ELEMENTS, ...input.ts.lifecycle_contracts];
188
+ const contracts = resolveContracts(project, held, declared);
189
+ if (contracts.every((one) => one.components.length === 0 && one.bases.length === 0)) {
164
190
  return [];
165
191
  }
166
192
  const chains = aliasChains(project, held);
167
- const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
168
193
  const components = [
169
194
  ...byComponents(project, held, chains, contracts),
170
195
  ...byBases(project, held, chains, contracts),
@@ -173,22 +198,24 @@ export function frameworkLifecycle(input) {
173
198
  return [];
174
199
  }
175
200
  const ids = new Set(components.map((one) => one.id));
176
- const sides = input.ts.lifecycle_contracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
201
+ const sides = declared.some((contract) => contract.members.some((name) => name.endsWith(":static")))
177
202
  ? ["instance", "static"]
178
203
  : ["instance"];
179
204
  const members = sides.map((side) => classMembers(project, held, ids, side));
180
205
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
181
- const found = [];
206
+ const found = new Map();
182
207
  for (const component of components) {
183
208
  const listed = new Set(component.contract.members);
184
209
  const site = renderPosition(component.site.getSourceFile(), targetRoot, component.site.getStart());
185
210
  for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
186
211
  const symbol = byId.get(member.id);
187
212
  const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
188
- if (spelled !== undefined && listed.has(spelled)) {
189
- found.push({ id: member.id, detail: component.detail, site });
213
+ // A configured contract may name the platform's registry too; one record answers both.
214
+ const key = `${member.id}\0${component.detail}\0${site.path}:${String(site.line)}:${String(site.column)}`;
215
+ if (spelled !== undefined && listed.has(spelled) && !found.has(key)) {
216
+ found.set(key, { id: member.id, detail: component.detail, site });
190
217
  }
191
218
  }
192
219
  }
193
- return found;
220
+ return [...found.values()];
194
221
  }
@@ -16,22 +16,27 @@ export const DEPENDENCY_SECTIONS = [
16
16
  export function inInstalledPackages(path) {
17
17
  return path.split("/").includes(INSTALLED_PACKAGES);
18
18
  }
19
- /** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
20
- export function readDocument(host, path) {
19
+ /** One JSON file, read and parsed, as {@link JSONRead} tells its states apart. */
20
+ export function readJSONFile(host, path) {
21
21
  let text;
22
22
  try {
23
23
  text = host.readFile(path);
24
24
  }
25
- catch {
26
- return undefined;
25
+ catch (error) {
26
+ return { kind: "unread", reason: message(error) };
27
27
  }
28
28
  try {
29
- return { text, value: JSON.parse(text) };
29
+ return { kind: "read", text, value: JSON.parse(text) };
30
30
  }
31
31
  catch {
32
- return undefined;
32
+ return { kind: "malformed" };
33
33
  }
34
34
  }
35
+ /** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
36
+ export function readDocument(host, path) {
37
+ const read = readJSONFile(host, path);
38
+ return read.kind === "read" ? { text: read.text, value: read.value } : undefined;
39
+ }
35
40
  /** One JSON file's decoded value, or undefined where it cannot be read or parsed. */
36
41
  export function readJSON(host, path) {
37
42
  return readDocument(host, path)?.value;
@@ -50,21 +55,11 @@ export function readManifestAt(host, dir) {
50
55
  if (!holdsManifest(host, dir)) {
51
56
  return { kind: "absent" };
52
57
  }
53
- let text;
54
- try {
55
- text = host.readFile(joinPath(dir, MANIFEST));
56
- }
57
- catch (error) {
58
- return { kind: "unread", reason: message(error) };
59
- }
60
- let value;
61
- try {
62
- value = JSON.parse(text);
63
- }
64
- catch {
65
- return { kind: "malformed" };
58
+ const read = readJSONFile(host, joinPath(dir, MANIFEST));
59
+ if (read.kind !== "read") {
60
+ return read;
66
61
  }
67
- return isRecord(value) ? { kind: "read", manifest: value } : { kind: "malformed" };
62
+ return isRecord(read.value) ? { kind: "read", manifest: read.value } : { kind: "malformed" };
68
63
  }
69
64
  /** Whether one directory holds a manifest file. */
70
65
  export function holdsManifest(host, dir) {