@cplieger/deadset-ts 4.0.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -15,50 +15,40 @@ const RANK = { certain: 3, probable: 2, possible: 1 };
15
15
  function capped(found, ceiling) {
16
16
  return RANK[found] <= RANK[ceiling] ? found : ceiling;
17
17
  }
18
- /** The kind whose default turns on what the run knows about the target's consumers. */
19
- const UNUSED_EXPORTED = "DS1001";
20
18
  /** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
21
19
  function everyDeclaredLoaded(boundary) {
22
20
  const { declared, loaded } = boundary.consumers;
23
21
  return declared.every((id) => loaded.includes(id));
24
22
  }
25
- /**
26
- * Whether a library's published API may have a caller the run cannot see: the target is
27
- * a library, and the run either holds no consumer information or did not load a consumer
28
- * the scope declared. A configuration declaring the consumer set complete holds consumer
29
- * information whether or not it declares a consumer, because it says no other exists.
30
- */
31
- function openWorld(input) {
32
- const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
33
- return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
34
- }
35
23
  /**
36
24
  * The severity the configuration gives one code: the key naming the code, else the key
37
- * naming its family, else the code's default. The unused-exported kind defaults to
38
- * `allow` while a library's published API is open to callers the run cannot see.
25
+ * naming its family, else the code's default.
39
26
  */
40
27
  function severityOf(input, code, fallback) {
41
28
  const { config } = input;
42
- const named = config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH));
43
- if (named !== undefined) {
44
- return named;
45
- }
46
- return code === UNUSED_EXPORTED && openWorld(input) ? "allow" : fallback;
29
+ return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
30
+ }
31
+ /**
32
+ * The declarations the roots name as a library's published API, each public member of a
33
+ * published type included.
34
+ */
35
+ function publishedIds(union) {
36
+ return new Set(union.rooted
37
+ .filter((root) => root.kind === "published-api")
38
+ .flatMap((root) => union.symbols[root.at]?.id ?? []));
47
39
  }
48
40
  /**
49
41
  * The reachability class of one declaration of the run. A declaration of a library's
50
- * published API has callers outside the target: it is `certain` where the run loaded
51
- * every consumer the scope declared and declared at least one, `probable` where it
52
- * declared consumers and did not load them all, and `possible` where it declared none.
53
- * Whether the configuration declares the consumer set complete decides nothing here.
54
- * Every other declaration has every reference in the loaded program, a private member
55
- * and a `#private` name among them, and is `certain`.
42
+ * published API, a public member of a published type included, has callers outside the
43
+ * target: it is `certain` where the run loaded every consumer the scope declared and
44
+ * declared at least one, `probable` where it declared consumers and did not load them
45
+ * all, and `possible` where it declared none. Whether the configuration declares the
46
+ * consumer set complete decides nothing here. Every other declaration has every reference
47
+ * in the loaded program, a private member and a `#private` name among them, and is
48
+ * `certain`.
56
49
  */
57
50
  function reachabilityClasses(input) {
58
- const union = input.swept.matrix.union;
59
- const published = new Set(union.rooted
60
- .filter((root) => root.kind === "published-api")
61
- .map((root) => union.symbols[root.at]?.id));
51
+ const published = publishedIds(input.swept.matrix.union);
62
52
  const { declared } = input.boundary.consumers;
63
53
  const outside = declared.length === 0
64
54
  ? "possible"
@@ -126,7 +116,8 @@ export function completed(input, findings, kinds = KINDS) {
126
116
  const classOf = reachabilityClasses(input);
127
117
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
128
118
  let minted = swept.sweep.components.length;
129
- return findings.map((finding) => {
119
+ const dead = new Set(swept.sweep.components.map((component) => component.id));
120
+ const done = findings.map((finding) => {
130
121
  const row = kinds.get(finding.code);
131
122
  if (row === undefined) {
132
123
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
@@ -161,6 +152,7 @@ export function completed(input, findings, kinds = KINDS) {
161
152
  ? union.made[at]
162
153
  : { production: candidate.productionRefs, test: candidate.testRefs };
163
154
  const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
155
+ const generated = input.files.generated?.has(finding.position.path) === true;
164
156
  return {
165
157
  code: finding.code,
166
158
  kind: row.name,
@@ -171,7 +163,7 @@ export function completed(input, findings, kinds = KINDS) {
171
163
  confidence: capped(reachabilityClass, row.maxClass),
172
164
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
173
165
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
174
- generated: false,
166
+ generated,
175
167
  component,
176
168
  retainedBy: [],
177
169
  configurations: candidate?.configurations ??
@@ -179,12 +171,34 @@ export function completed(input, findings, kinds = KINDS) {
179
171
  ? swept.matrix.configurations
180
172
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
181
173
  consumersLoaded: input.boundary.consumers.loaded,
182
- fixability: row.fixability,
174
+ fixability: generated ? "none" : row.fixability,
183
175
  severity: severityOf(input, finding.code, row.defaultSeverity),
184
176
  message: finding.message,
185
177
  details: finding.details ?? {},
186
178
  };
187
179
  });
180
+ return componentCapped(done, dead);
181
+ }
182
+ /**
183
+ * The findings with every finding of a dead component capped by the lowest confidence
184
+ * among the component's root members, so the minimum confidence reports or withholds a
185
+ * component whole.
186
+ */
187
+ function componentCapped(findings, dead) {
188
+ const lowest = new Map();
189
+ for (const finding of findings) {
190
+ const { id, root } = finding.component;
191
+ if (root && dead.has(id)) {
192
+ const held = lowest.get(id);
193
+ lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
194
+ }
195
+ }
196
+ return findings.map((finding) => {
197
+ const cap = lowest.get(finding.component.id);
198
+ return cap === undefined || RANK[cap] >= RANK[finding.confidence]
199
+ ? finding
200
+ : { ...finding, confidence: cap };
201
+ });
188
202
  }
189
203
  /** Whether the dials withhold a finding by its own severity or confidence. */
190
204
  function dialed(finding, minConfidence) {
@@ -1,5 +1,7 @@
1
1
  import { evaluateEdges } from "../edges.js";
2
+ import { liveFilesAt } from "../matrix.js";
2
3
  import { ledgerOf } from "../suppress.js";
4
+ import { insideSkipped } from "../type-errors.js";
3
5
  import { completed, dialsOf, reportable } from "./completion.js";
4
6
  import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
5
7
  import { interfaces } from "./interfaces.js";
@@ -33,6 +35,21 @@ function heldRefs(input) {
33
35
  .filter((symbol) => held.has(symbol.id))
34
36
  .map((symbol) => symbol.ref));
35
37
  }
38
+ /**
39
+ * The references of every top-level declaration of a live component file. The file's
40
+ * markup may use each of them, which no analysis of the markup can rule out, so the run
41
+ * reports nothing about one while the file is live.
42
+ */
43
+ function markupRefs(input) {
44
+ const { swept } = input;
45
+ if (swept.componentFiles.length === 0) {
46
+ return new Set();
47
+ }
48
+ const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
49
+ return new Set(swept.matrix.union.symbols
50
+ .filter((symbol) => live.has(symbol.parent))
51
+ .map((symbol) => symbol.ref));
52
+ }
36
53
  /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
37
54
  function completedOver(input, ...more) {
38
55
  return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
@@ -64,7 +81,11 @@ export function decidedFindings(input, suppressed = { unmarked: input, records:
64
81
  const would = [...unmarked, ...found];
65
82
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
66
83
  const unanswered = heldRefs(input);
67
- const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)));
84
+ const markup = markupRefs(input);
85
+ const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) &&
86
+ !unanswered.has(finding.symbol.ref) &&
87
+ !markup.has(finding.symbol.ref) &&
88
+ !insideSkipped(input.swept.skipped, finding.position)));
68
89
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
69
90
  return {
70
91
  findings: evaluated.findings,
@@ -26,6 +26,7 @@ function indexed(input) {
26
26
  return {
27
27
  symbolOf,
28
28
  inTest: (id) => union.test[union.at(id)] === true,
29
+ inSupport: (id) => union.support[union.at(id)] === true,
29
30
  candidateOf: (id) => candidates.get(id),
30
31
  implementationsOf: (id) => [...(input.implementations.classes.get(id) ?? [])]
31
32
  .map(symbolOf)
@@ -55,14 +56,17 @@ function findingOf(code, symbol, message, implementations) {
55
56
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
56
57
  * interface no reference names, unless it is a marker, a method whose every
57
58
  * implementation has an empty body. A test file's declaration is neither kind's
58
- * subject, because a production sweep counts none of the references it can have.
59
+ * subject, because a production sweep counts none of the references it can have, and
60
+ * neither is a test-support declaration test code references, which that code keeps live.
59
61
  */
60
62
  export const interfaces = (input) => {
61
63
  const run = indexed(input);
62
64
  const found = [];
63
65
  for (const candidate of input.swept.sweep.candidates) {
64
66
  const symbol = run.symbolOf(candidate.id);
65
- if (symbol === undefined || run.inTest(candidate.id)) {
67
+ if (symbol === undefined ||
68
+ run.inTest(candidate.id) ||
69
+ (run.inSupport(candidate.id) && candidate.testRefs > 0)) {
66
70
  continue;
67
71
  }
68
72
  if (symbol.kind === "interface") {
@@ -36,18 +36,55 @@ function findingAbout(symbol, code, message, details) {
36
36
  }
37
37
  /**
38
38
  * Whether one declaration is a subject of the write-only kind: a module-level variable,
39
- * or a class member that is state rather than code. A getter or a setter is code, so a
40
- * store into one runs it.
39
+ * a member of an object type, or a class member that is state rather than code. A getter
40
+ * or a setter is code, so a store into one runs it.
41
41
  */
42
42
  function writable(run, symbol) {
43
- if (symbol.kind === "variable") {
43
+ if (symbol.kind === "variable" || symbol.kind === "type-member") {
44
44
  return true;
45
45
  }
46
46
  return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
47
47
  }
48
+ /** The reader's word for each kind of the write-only kind's subject. */
49
+ const SUBJECT_WORDS = {
50
+ variable: "variable",
51
+ "class-member": "member",
52
+ "type-member": "type member",
53
+ };
54
+ /**
55
+ * The type parameters of one member's declaring type that only the member names, which
56
+ * deleting the member leaves unused.
57
+ */
58
+ function parametersOnlyNamedBy(run, namedBy, member) {
59
+ return [...run.symbols.values()]
60
+ .filter((symbol) => {
61
+ const from = namedBy.get(symbol.id);
62
+ return (symbol.kind === "type-parameter" &&
63
+ symbol.parent === member.parent &&
64
+ from?.size === 1 &&
65
+ from.has(member.id));
66
+ })
67
+ .map((symbol) => symbol.name);
68
+ }
69
+ /** What one write-only finding says, the declarations its deletion takes with it included. */
70
+ function writeOnlyMessage(run, namedBy, symbol, held) {
71
+ const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
72
+ const read = run.input.stores.readInTests.has(symbol.id)
73
+ ? "read only from test files"
74
+ : "never read";
75
+ const cascade = [
76
+ ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
77
+ ...[...held.alone].sort().map((name) => `import ${name}`),
78
+ ];
79
+ const word = SUBJECT_WORDS[symbol.kind] ?? "member";
80
+ const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
81
+ return cascade.length === 0
82
+ ? message
83
+ : `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
84
+ }
48
85
  /**
49
- * `DS1301`: a module-level variable or a class member the counted references store into
50
- * and never read, named with every position written.
86
+ * `DS1301`: a module-level variable, a type member or a class member the counted
87
+ * references store into and never read, named with every position written.
51
88
  *
52
89
  * The subject is a live declaration: a store is a reference, so the sweep holds a
53
90
  * written one live and its candidates are never this kind's. Every reference that is
@@ -60,7 +97,7 @@ function writeOnlySymbols(run) {
60
97
  for (const reference of stores.references) {
61
98
  let held = uses.get(reference.to);
62
99
  if (held === undefined) {
63
- held = { writes: [], reads: 0 };
100
+ held = { writes: [], reads: 0, alone: new Set() };
64
101
  uses.set(reference.to, held);
65
102
  }
66
103
  // A consumer's store is written at a position no report of the target can name, so
@@ -69,12 +106,21 @@ function writeOnlySymbols(run) {
69
106
  held.reads += 1;
70
107
  continue;
71
108
  }
109
+ for (const name of reference.alone ?? []) {
110
+ held.alone.add(name);
111
+ }
72
112
  const at = reference.position;
73
113
  const key = positionKey(at);
74
114
  if (!held.writes.some((written) => positionKey(written) === key)) {
75
115
  held.writes.push({ path: at.path, line: at.line, column: at.column, endLine: at.line });
76
116
  }
77
117
  }
118
+ const namedBy = new Map();
119
+ for (const reference of stores.references) {
120
+ if (run.symbols.get(reference.to)?.kind === "type-parameter") {
121
+ namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
122
+ }
123
+ }
78
124
  const found = [];
79
125
  for (const symbol of matrix.union.symbols) {
80
126
  const held = uses.get(symbol.id);
@@ -86,9 +132,7 @@ function writeOnlySymbols(run) {
86
132
  run.candidates.has(symbol.id)) {
87
133
  continue;
88
134
  }
89
- const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
90
- const word = symbol.kind === "variable" ? "variable" : "member";
91
- found.push(findingAbout(symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
135
+ found.push(findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
92
136
  writePositions: held.writes,
93
137
  }));
94
138
  }
@@ -40,8 +40,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
40
40
  /**
41
41
  * The code one candidate is reported under, or none, each symbol once under the most
42
42
  * specific code: a test of dead code first; no other test-file declaration, member of a
43
- * dead container or enum member nothing names; then deprecation, a test reference, and
44
- * the unreferenced codes, an unimportable unused export being the unreachable kind's.
43
+ * dead container or enum member nothing names, nor a test-support declaration test code
44
+ * references; then deprecation, a test reference, and the unreferenced codes, an
45
+ * unimportable unused export being the unreachable kind's.
45
46
  */
46
47
  function codeOf(judged) {
47
48
  const { candidate, symbol } = judged;
@@ -55,6 +56,9 @@ function codeOf(judged) {
55
56
  if (symbol.kind === "enum-member" && !referenced) {
56
57
  return undefined;
57
58
  }
59
+ if (judged.support && candidate.testRefs > 0) {
60
+ return undefined;
61
+ }
58
62
  if (candidate.productionRefs === 0 && judged.deprecated) {
59
63
  return "DS1006";
60
64
  }
@@ -110,6 +114,43 @@ function messageOf(code, judged) {
110
114
  }
111
115
  return `${subject} has no reference in the target`;
112
116
  }
117
+ /**
118
+ * The members nothing writes or reads that are brands: each names a type parameter of
119
+ * its declaring type that no other part of the declaration names, other than another
120
+ * such member. They are what make two instantiations of the type distinct types, so
121
+ * deleting them would let one instantiation stand for another.
122
+ */
123
+ function brandMembers(input) {
124
+ const union = input.swept.matrix.union;
125
+ const unreferenced = new Map(input.swept.sweep.candidates
126
+ .filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
127
+ .map((candidate) => [candidate.id, candidate]));
128
+ const namedBy = new Map();
129
+ for (const reference of input.stores.references) {
130
+ const to = union.symbols[union.at(reference.to)];
131
+ if (to?.kind === "type-parameter") {
132
+ namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
133
+ }
134
+ }
135
+ const brands = new Set();
136
+ for (const [parameter, from] of namedBy) {
137
+ const owner = union.symbols[union.at(parameter)]?.parent;
138
+ const members = [...from].map((id) => union.symbols[union.at(id)]);
139
+ if (members.every((member) => member !== undefined &&
140
+ member.parent === owner &&
141
+ BRANDS.has(member.kind) &&
142
+ unreferenced.has(member.id))) {
143
+ for (const member of members) {
144
+ if (member !== undefined) {
145
+ brands.add(member.id);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return brands;
151
+ }
152
+ /** The kinds of member a brand is: a data member of a type or of a class. */
153
+ const BRANDS = new Set(["type-member", "class-member"]);
113
154
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
114
155
  export const unusedDeclarations = (input) => familyFindings(input, false);
115
156
  /**
@@ -123,6 +164,7 @@ function familyFindings(input, fallen) {
123
164
  const union = input.swept.matrix.union;
124
165
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
125
166
  const unreachable = unreachableExports(input);
167
+ let brands;
126
168
  const found = [];
127
169
  for (const candidate of input.swept.sweep.candidates) {
128
170
  const at = union.at(candidate.id);
@@ -138,12 +180,14 @@ function familyFindings(input, fallen) {
138
180
  candidate,
139
181
  symbol,
140
182
  test: union.test[at] === true,
183
+ support: union.support[at] === true,
141
184
  containerDead: containerDead && !fallen,
142
185
  deprecated: input.deprecated.has(symbol.id),
143
186
  unreachable: unreachable.has(symbol.id),
144
187
  };
145
188
  const code = codeOf(judged);
146
- if (code === undefined) {
189
+ if (code === undefined ||
190
+ (code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))) {
147
191
  continue;
148
192
  }
149
193
  const endLine = Math.max(symbol.position.line, symbol.endLine);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The generated-file class on TypeScript: every declaration in a file below a directory
3
+ * an applied convention row names as generated is retained, because the framework owns
4
+ * the file and a deletion there is undone at the next generation.
5
+ */
6
+ /** The generated-file detector, recording each file's first line. */
7
+ export function generatedFile(input) {
8
+ const generated = input.generated;
9
+ if (generated === undefined || generated.size === 0) {
10
+ return [];
11
+ }
12
+ return input.held.symbols.flatMap((symbol) => {
13
+ const row = generated.get(symbol.position.path);
14
+ return symbol.kind === "file" || row === undefined
15
+ ? []
16
+ : [
17
+ {
18
+ id: symbol.id,
19
+ detail: `generated below a directory the convention row ${row} names`,
20
+ site: { path: symbol.position.path, line: 1, column: 1 },
21
+ },
22
+ ];
23
+ });
24
+ }
package/dist/src/graph.js CHANGED
@@ -32,20 +32,26 @@ const SWEPT = {
32
32
  };
33
33
  /**
34
34
  * Whether a root of each kind names a caller, which holds it live under both relations: a
35
- * tool reads the configuration it loads, a runtime runs a test file, a setup file, a
36
- * worker and a command, the manifest names what enters the package, and an entry file, a
37
- * configured root and a pattern assert a caller. A library's published API only supposes
35
+ * tool reads the configuration it loads and the files its strings name, a runtime runs a
36
+ * test file, a setup file, a worker, a command and a script, the manifest names what
37
+ * enters the package, an alias global stands for the export it names, and an entry file,
38
+ * a configured root and a pattern assert a caller. A library's published API only supposes
38
39
  * a consumer the run may not see, so it is live under reachability alone.
39
40
  */
40
41
  const NAMES_A_CALLER = {
41
42
  "entry-file": true,
43
+ convention: true,
42
44
  "manifest-entry": true,
43
45
  "manifest-binary": true,
46
+ script: true,
44
47
  "published-api": false,
45
48
  configured: true,
46
49
  pattern: true,
50
+ "type-error": true,
51
+ "type-query-alias": true,
52
+ "configuration-file": true,
53
+ "configuration-string": true,
47
54
  "test-runner": true,
48
- "lint-configuration": true,
49
55
  "mutation-testing": true,
50
56
  "browser-tests": true,
51
57
  worker: true,
@@ -65,7 +71,8 @@ export function namesACaller(kind) {
65
71
  * from such a declaration still counts toward its target, because reference counting
66
72
  * asks whether any declaration names the target. A root naming nothing is dropped.
67
73
  */
68
- export function graphOf(symbols, references, roots, testFiles) {
74
+ export function graphOf(symbols, references, roots, testFiles, supportFiles = []) {
75
+ const supports = new Set(supportFiles);
69
76
  const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
70
77
  const at = (id) => index.get(id) ?? OUTSIDE;
71
78
  const tests = new Set(testFiles);
@@ -114,6 +121,7 @@ export function graphOf(symbols, references, roots, testFiles) {
114
121
  made,
115
122
  parent,
116
123
  test: symbols.map((symbol) => tests.has(symbol.position.path)),
124
+ support: symbols.map((symbol) => supports.has(symbol.position.path)),
117
125
  subject: symbols.map((symbol) => swept(symbol.kind)),
118
126
  exportsOf,
119
127
  rooted,
@@ -12,19 +12,13 @@
12
12
  * the contextual lookup has none, so it is asked only of a value whose type is a class
13
13
  * of the target, and each distinct pair, interface and class is asked once.
14
14
  */
15
- import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isConditionalExpression, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isIdentifier, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
15
+ import { isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
16
16
  import { SymbolFlags } from "@typescript/native/unstable/sync";
17
17
  import { aliasChains } from "./alias-chain.js";
18
18
  import { nodeKey } from "./inventory.js";
19
19
  import { renderPosition } from "./position.js";
20
20
  import { must } from "./query.js";
21
- /** The operators of an assignment whose right side is stored into the left side's type. */
22
- const STORES = new Set([
23
- SyntaxKind.EqualsToken,
24
- SyntaxKind.QuestionQuestionEqualsToken,
25
- SyntaxKind.BarBarEqualsToken,
26
- SyntaxKind.AmpersandAmpersandEqualsToken,
27
- ]);
21
+ import { handedOn, valuesOf } from "./value-flow.js";
28
22
  /** Whether one node declares a static member, whose `this` is the class itself. */
29
23
  function isStatic(node) {
30
24
  const flags = node.modifierFlags ?? 0;
@@ -37,66 +31,6 @@ function isObjectLiteralMethod(node) {
37
31
  isSetAccessorDeclaration(node)) &&
38
32
  isObjectLiteralExpression(node.parent));
39
33
  }
40
- /** Whether a declaration writes its type, so its initializer is read in that type's context. */
41
- function writesType(node) {
42
- return node.type !== undefined;
43
- }
44
- /**
45
- * The values one written expression hands on: the expression itself, or what it
46
- * evaluates to through parentheses, a conditional and the operators whose result is
47
- * one of their operands.
48
- */
49
- function valuesOf(expression) {
50
- if (isParenthesizedExpression(expression)) {
51
- return valuesOf(expression.expression);
52
- }
53
- if (isConditionalExpression(expression)) {
54
- return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
55
- }
56
- if (isBinaryExpression(expression)) {
57
- const operator = expression.operatorToken.kind;
58
- if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
59
- return [...valuesOf(expression.left), ...valuesOf(expression.right)];
60
- }
61
- if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
62
- return valuesOf(expression.right);
63
- }
64
- }
65
- return [expression];
66
- }
67
- /** The expressions one node hands to a position typed by its context. */
68
- function handedOn(node) {
69
- if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
70
- node.initializer !== undefined &&
71
- writesType(node)) {
72
- return [node.initializer];
73
- }
74
- if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
75
- return [node.right];
76
- }
77
- if (isCallExpression(node) || isNewExpression(node)) {
78
- return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
79
- }
80
- if (isReturnStatement(node)) {
81
- return node.expression === undefined ? [] : [node.expression];
82
- }
83
- if (isArrowFunction(node)) {
84
- return node.body.kind === SyntaxKind.Block ? [] : [node.body];
85
- }
86
- if (isArrayLiteralExpression(node)) {
87
- return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
88
- }
89
- if (isPropertyAssignment(node)) {
90
- return [node.initializer];
91
- }
92
- if (isShorthandPropertyAssignment(node)) {
93
- return isIdentifier(node.name) ? [node.name] : [];
94
- }
95
- if (isAsExpression(node) || isSatisfiesExpression(node)) {
96
- return [node.expression];
97
- }
98
- return [];
99
- }
100
34
  /**
101
35
  * The classes, the `implements` entries and the flowing values of one project's own
102
36
  * files. `this` is the class an instance member belongs to, so a function that is not
@@ -1,6 +1,6 @@
1
1
  import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/unstable/ast";
2
2
  import { dirnamePath, joinPath, relativePath } from "./paths.js";
3
- import { byPosition, positionKey, renderPosition } from "./position.js";
3
+ import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
4
4
  import { computedComponent, nameComponent, renderRef } from "./ref.js";
5
5
  import { isAnswered, UNANSWERED } from "./query.js";
6
6
  const MANIFEST = "package.json";
@@ -69,7 +69,10 @@ function modifiersOf(node) {
69
69
  function typeParametersOf(node) {
70
70
  if (isFunctionDeclaration(node) ||
71
71
  isMethodDeclaration(node) ||
72
- isMethodSignatureDeclaration(node)) {
72
+ isMethodSignatureDeclaration(node) ||
73
+ isClassDeclaration(node) ||
74
+ isInterfaceDeclaration(node) ||
75
+ isTypeAliasDeclaration(node)) {
73
76
  return node.typeParameters ?? [];
74
77
  }
75
78
  return [];
@@ -273,7 +276,9 @@ export function inventory(project, host, targetRoot) {
273
276
  typeParameter: undefined,
274
277
  });
275
278
  exportScopes.push({ owner: fileRecord, nameNode: file });
276
- walkStatements(file.statements, fileRecord);
279
+ // A statement a component file's module appends is not written in the file.
280
+ const written = writtenLength(file);
281
+ walkStatements(file.statements.filter((statement) => statement.end <= written), fileRecord);
277
282
  }
278
283
  /** One declaration of a module, a namespace or a container body. */
279
284
  function walkStatements(statements, owner) {