@cplieger/deadset-ts 5.0.0 → 5.1.1

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. package/src/version.ts +1 -1
@@ -146,8 +146,9 @@ export function completed(
146
146
  const classOf = reachabilityClasses(input);
147
147
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
148
148
  let minted = swept.sweep.components.length;
149
+ const dead = new Set(swept.sweep.components.map((component) => component.id));
149
150
 
150
- return findings.map((finding): CompletedFinding => {
151
+ const done = findings.map((finding): CompletedFinding => {
151
152
  const row = kinds.get(finding.code);
152
153
  if (row === undefined) {
153
154
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
@@ -183,6 +184,7 @@ export function completed(
183
184
  ? union.made[at]
184
185
  : { production: candidate.productionRefs, test: candidate.testRefs };
185
186
  const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
187
+ const generated = input.files.generated?.has(finding.position.path) === true;
186
188
  return {
187
189
  code: finding.code,
188
190
  kind: row.name,
@@ -193,7 +195,7 @@ export function completed(
193
195
  confidence: capped(reachabilityClass, row.maxClass),
194
196
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
195
197
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
196
- generated: false,
198
+ generated,
197
199
  component,
198
200
  retainedBy: [],
199
201
  configurations:
@@ -202,12 +204,38 @@ export function completed(
202
204
  ? swept.matrix.configurations
203
205
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
204
206
  consumersLoaded: input.boundary.consumers.loaded,
205
- fixability: row.fixability,
207
+ fixability: generated ? "none" : row.fixability,
206
208
  severity: severityOf(input, finding.code, row.defaultSeverity),
207
209
  message: finding.message,
208
210
  details: finding.details ?? {},
209
211
  };
210
212
  });
213
+ return componentCapped(done, dead);
214
+ }
215
+
216
+ /**
217
+ * The findings with every finding of a dead component capped by the lowest confidence
218
+ * among the component's root members, so the minimum confidence reports or withholds a
219
+ * component whole.
220
+ */
221
+ function componentCapped(
222
+ findings: readonly CompletedFinding[],
223
+ dead: ReadonlySet<string>,
224
+ ): readonly CompletedFinding[] {
225
+ const lowest = new Map<string, Confidence>();
226
+ for (const finding of findings) {
227
+ const { id, root } = finding.component;
228
+ if (root && dead.has(id)) {
229
+ const held = lowest.get(id);
230
+ lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
231
+ }
232
+ }
233
+ return findings.map((finding) => {
234
+ const cap = lowest.get(finding.component.id);
235
+ return cap === undefined || RANK[cap] >= RANK[finding.confidence]
236
+ ? finding
237
+ : { ...finding, confidence: cap };
238
+ });
211
239
  }
212
240
 
213
241
  /** Whether the dials withhold a finding by its own severity or confidence. */
@@ -31,6 +31,7 @@ function compare(a: string, b: string): number {
31
31
  function indexed(input: EmitterInput): {
32
32
  readonly symbolOf: (id: string) => InventorySymbol | undefined;
33
33
  readonly inTest: (id: string) => boolean;
34
+ readonly inSupport: (id: string) => boolean;
34
35
  readonly candidateOf: (id: string) => Candidate | undefined;
35
36
  readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
36
37
  } {
@@ -43,6 +44,7 @@ function indexed(input: EmitterInput): {
43
44
  return {
44
45
  symbolOf,
45
46
  inTest: (id) => union.test[union.at(id)] === true,
47
+ inSupport: (id) => union.support[union.at(id)] === true,
46
48
  candidateOf: (id) => candidates.get(id),
47
49
  implementationsOf: (id) =>
48
50
  [...(input.implementations.classes.get(id) ?? [])]
@@ -80,14 +82,19 @@ function findingOf(
80
82
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
81
83
  * interface no reference names, unless it is a marker, a method whose every
82
84
  * implementation has an empty body. A test file's declaration is neither kind's
83
- * subject, because a production sweep counts none of the references it can have.
85
+ * subject, because a production sweep counts none of the references it can have, and
86
+ * neither is a test-support declaration test code references, which that code keeps live.
84
87
  */
85
88
  export const interfaces: Emitter = (input) => {
86
89
  const run = indexed(input);
87
90
  const found: Finding[] = [];
88
91
  for (const candidate of input.swept.sweep.candidates) {
89
92
  const symbol = run.symbolOf(candidate.id);
90
- if (symbol === undefined || run.inTest(candidate.id)) {
93
+ if (
94
+ symbol === undefined ||
95
+ run.inTest(candidate.id) ||
96
+ (run.inSupport(candidate.id) && candidate.testRefs > 0)
97
+ ) {
91
98
  continue;
92
99
  }
93
100
  if (symbol.kind === "interface") {
@@ -62,23 +62,76 @@ function findingAbout(
62
62
  interface Uses {
63
63
  readonly writes: FindingPosition[];
64
64
  reads: number;
65
+ /** The import bindings only the writes use. */
66
+ readonly alone: Set<string>;
65
67
  }
66
68
 
67
69
  /**
68
70
  * Whether one declaration is a subject of the write-only kind: a module-level variable,
69
- * or a class member that is state rather than code. A getter or a setter is code, so a
70
- * store into one runs it.
71
+ * a member of an object type, or a class member that is state rather than code. A getter
72
+ * or a setter is code, so a store into one runs it.
71
73
  */
72
74
  function writable(run: Run, symbol: InventorySymbol): boolean {
73
- if (symbol.kind === "variable") {
75
+ if (symbol.kind === "variable" || symbol.kind === "type-member") {
74
76
  return true;
75
77
  }
76
78
  return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
77
79
  }
78
80
 
81
+ /** The reader's word for each kind of the write-only kind's subject. */
82
+ const SUBJECT_WORDS: Readonly<Partial<Record<InventorySymbol["kind"], string>>> = {
83
+ variable: "variable",
84
+ "class-member": "member",
85
+ "type-member": "type member",
86
+ };
87
+
88
+ /**
89
+ * The type parameters of one member's declaring type that only the member names, which
90
+ * deleting the member leaves unused.
91
+ */
92
+ function parametersOnlyNamedBy(
93
+ run: Run,
94
+ namedBy: ReadonlyMap<string, ReadonlySet<string>>,
95
+ member: InventorySymbol,
96
+ ): readonly string[] {
97
+ return [...run.symbols.values()]
98
+ .filter((symbol) => {
99
+ const from = namedBy.get(symbol.id);
100
+ return (
101
+ symbol.kind === "type-parameter" &&
102
+ symbol.parent === member.parent &&
103
+ from?.size === 1 &&
104
+ from.has(member.id)
105
+ );
106
+ })
107
+ .map((symbol) => symbol.name);
108
+ }
109
+
110
+ /** What one write-only finding says, the declarations its deletion takes with it included. */
111
+ function writeOnlyMessage(
112
+ run: Run,
113
+ namedBy: ReadonlyMap<string, ReadonlySet<string>>,
114
+ symbol: InventorySymbol,
115
+ held: Uses,
116
+ ): string {
117
+ const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
118
+ const read = run.input.stores.readInTests.has(symbol.id)
119
+ ? "read only from test files"
120
+ : "never read";
121
+ const cascade = [
122
+ ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
123
+ ...[...held.alone].sort().map((name) => `import ${name}`),
124
+ ];
125
+ const word = SUBJECT_WORDS[symbol.kind] ?? "member";
126
+ const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
127
+ return cascade.length === 0
128
+ ? message
129
+ : `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
130
+ }
131
+
79
132
  /**
80
- * `DS1301`: a module-level variable or a class member the counted references store into
81
- * and never read, named with every position written.
133
+ * `DS1301`: a module-level variable, a type member or a class member the counted
134
+ * references store into and never read, named with every position written.
82
135
  *
83
136
  * The subject is a live declaration: a store is a reference, so the sweep holds a
84
137
  * written one live and its candidates are never this kind's. Every reference that is
@@ -91,7 +144,7 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
91
144
  for (const reference of stores.references) {
92
145
  let held = uses.get(reference.to);
93
146
  if (held === undefined) {
94
- held = { writes: [], reads: 0 };
147
+ held = { writes: [], reads: 0, alone: new Set() };
95
148
  uses.set(reference.to, held);
96
149
  }
97
150
  // A consumer's store is written at a position no report of the target can name, so
@@ -100,6 +153,9 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
100
153
  held.reads += 1;
101
154
  continue;
102
155
  }
156
+ for (const name of reference.alone ?? []) {
157
+ held.alone.add(name);
158
+ }
103
159
  const at = reference.position;
104
160
  const key = positionKey(at);
105
161
  if (!held.writes.some((written) => positionKey(written) === key)) {
@@ -107,6 +163,13 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
107
163
  }
108
164
  }
109
165
 
166
+ const namedBy = new Map<string, Set<string>>();
167
+ for (const reference of stores.references) {
168
+ if (run.symbols.get(reference.to)?.kind === "type-parameter") {
169
+ namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
170
+ }
171
+ }
172
+
110
173
  const found: Finding[] = [];
111
174
  for (const symbol of matrix.union.symbols) {
112
175
  const held = uses.get(symbol.id);
@@ -120,10 +183,8 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
120
183
  ) {
121
184
  continue;
122
185
  }
123
- const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
124
- const word = symbol.kind === "variable" ? "variable" : "member";
125
186
  found.push(
126
- findingAbout(symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
187
+ findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
127
188
  writePositions: held.writes,
128
189
  }),
129
190
  );
@@ -142,6 +142,52 @@ function messageOf(code: string, judged: Judged): string {
142
142
  return `${subject} has no reference in the target`;
143
143
  }
144
144
 
145
+ /**
146
+ * The members nothing writes or reads that are brands: each names a type parameter of
147
+ * its declaring type that no other part of the declaration names, other than another
148
+ * such member. They are what make two instantiations of the type distinct types, so
149
+ * deleting them would let one instantiation stand for another.
150
+ */
151
+ function brandMembers(input: EmitterInput): ReadonlySet<string> {
152
+ const union = input.swept.matrix.union;
153
+ const unreferenced = new Map(
154
+ input.swept.sweep.candidates
155
+ .filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
156
+ .map((candidate) => [candidate.id, candidate]),
157
+ );
158
+ const namedBy = new Map<string, Set<string>>();
159
+ for (const reference of input.stores.references) {
160
+ const to = union.symbols[union.at(reference.to)];
161
+ if (to?.kind === "type-parameter") {
162
+ namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
163
+ }
164
+ }
165
+ const brands = new Set<string>();
166
+ for (const [parameter, from] of namedBy) {
167
+ const owner = union.symbols[union.at(parameter)]?.parent;
168
+ const members = [...from].map((id) => union.symbols[union.at(id)]);
169
+ if (
170
+ members.every(
171
+ (member) =>
172
+ member !== undefined &&
173
+ member.parent === owner &&
174
+ BRANDS.has(member.kind) &&
175
+ unreferenced.has(member.id),
176
+ )
177
+ ) {
178
+ for (const member of members) {
179
+ if (member !== undefined) {
180
+ brands.add(member.id);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return brands;
186
+ }
187
+
188
+ /** The kinds of member a brand is: a data member of a type or of a class. */
189
+ const BRANDS: ReadonlySet<SymbolKind> = new Set(["type-member", "class-member"]);
190
+
145
191
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
146
192
  export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
147
193
 
@@ -157,6 +203,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
157
203
  const union = input.swept.matrix.union;
158
204
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
159
205
  const unreachable = unreachableExports(input);
206
+ let brands: ReadonlySet<string> | undefined;
160
207
 
161
208
  const found: Finding[] = [];
162
209
  for (const candidate of input.swept.sweep.candidates) {
@@ -179,7 +226,10 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
179
226
  unreachable: unreachable.has(symbol.id),
180
227
  };
181
228
  const code = codeOf(judged);
182
- if (code === undefined) {
229
+ if (
230
+ code === undefined ||
231
+ (code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))
232
+ ) {
183
233
  continue;
184
234
  }
185
235
  const endLine = Math.max(symbol.position.line, symbol.endLine);
@@ -0,0 +1,27 @@
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
+
7
+ import type { DetectorInput, Evidence } from "./exempt.ts";
8
+
9
+ /** The generated-file detector, recording each file's first line. */
10
+ export function generatedFile<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
11
+ const generated = input.generated;
12
+ if (generated === undefined || generated.size === 0) {
13
+ return [];
14
+ }
15
+ return input.held.symbols.flatMap((symbol): Evidence[] => {
16
+ const row = generated.get(symbol.position.path);
17
+ return symbol.kind === "file" || row === undefined
18
+ ? []
19
+ : [
20
+ {
21
+ id: symbol.id,
22
+ detail: `generated below a directory the convention row ${row} names`,
23
+ site: { path: symbol.position.path, line: 1, column: 1 },
24
+ },
25
+ ];
26
+ });
27
+ }
package/src/graph.ts CHANGED
@@ -122,9 +122,9 @@ export interface Graph {
122
122
  /** Every root naming a declaration the inventory holds, in the order the root set gives them. */
123
123
  readonly rooted: readonly Rooted[];
124
124
  /**
125
- * Every reference a loaded consumer makes to a declaration the inventory holds, in the
126
- * order given. A consumer is a caller the analysis read, so the sweep holds what it
127
- * names live under both relations and reaches on from there.
125
+ * Every reference a loaded consumer or an unheld file makes to a declaration the
126
+ * inventory holds, in the order given. Such a file is a caller the analysis read, so the
127
+ * sweep holds what it names live under both relations and reaches on from there.
128
128
  */
129
129
  readonly consumed: readonly Edge[];
130
130
  /** The position of the declaration one identifier names, or {@link OUTSIDE}. */
@@ -167,7 +167,7 @@ export function graphOf(
167
167
  }
168
168
  }
169
169
  const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
170
- if (reference.consumer !== undefined) {
170
+ if (reference.consumer !== undefined || reference.unheld === true) {
171
171
  consumed.push(edge);
172
172
  continue;
173
173
  }
@@ -14,33 +14,16 @@
14
14
  */
15
15
 
16
16
  import {
17
- isArrayLiteralExpression,
18
- isArrowFunction,
19
- isAsExpression,
20
- isBinaryExpression,
21
- isCallExpression,
22
17
  isClassDeclaration,
23
18
  isClassExpression,
24
19
  isClassStaticBlockDeclaration,
25
- isConditionalExpression,
26
20
  isFunctionDeclaration,
27
21
  isFunctionExpression,
28
22
  isGetAccessorDeclaration,
29
23
  isHeritageClause,
30
- isIdentifier,
31
24
  isMethodDeclaration,
32
- isNewExpression,
33
25
  isObjectLiteralExpression,
34
- isParameterDeclaration,
35
- isParenthesizedExpression,
36
- isPropertyAssignment,
37
- isPropertyDeclaration,
38
- isReturnStatement,
39
- isSatisfiesExpression,
40
26
  isSetAccessorDeclaration,
41
- isShorthandPropertyAssignment,
42
- isSpreadElement,
43
- isVariableDeclaration,
44
27
  ModifierFlags,
45
28
  SyntaxKind,
46
29
  type Expression,
@@ -53,6 +36,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
53
36
  import { nodeKey } from "./inventory.ts";
54
37
  import { renderPosition } from "./position.ts";
55
38
  import { must } from "./query.ts";
39
+ import { handedOn, valuesOf } from "./value-flow.ts";
56
40
 
57
41
  /** One class the inventory holds, by its declaration. */
58
42
  interface ClassAt {
@@ -85,14 +69,6 @@ interface Pair {
85
69
  readonly sites: Node[];
86
70
  }
87
71
 
88
- /** The operators of an assignment whose right side is stored into the left side's type. */
89
- const STORES: ReadonlySet<SyntaxKind> = new Set([
90
- SyntaxKind.EqualsToken,
91
- SyntaxKind.QuestionQuestionEqualsToken,
92
- SyntaxKind.BarBarEqualsToken,
93
- SyntaxKind.AmpersandAmpersandEqualsToken,
94
- ]);
95
-
96
72
  /** Whether one node declares a static member, whose `this` is the class itself. */
97
73
  function isStatic(node: Node): boolean {
98
74
  const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
@@ -109,73 +85,6 @@ function isObjectLiteralMethod(node: Node): boolean {
109
85
  );
110
86
  }
111
87
 
112
- /** Whether a declaration writes its type, so its initializer is read in that type's context. */
113
- function writesType(node: Node): boolean {
114
- return (node as { readonly type?: Node }).type !== undefined;
115
- }
116
-
117
- /**
118
- * The values one written expression hands on: the expression itself, or what it
119
- * evaluates to through parentheses, a conditional and the operators whose result is
120
- * one of their operands.
121
- */
122
- function valuesOf(expression: Expression): readonly Expression[] {
123
- if (isParenthesizedExpression(expression)) {
124
- return valuesOf(expression.expression);
125
- }
126
- if (isConditionalExpression(expression)) {
127
- return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
128
- }
129
- if (isBinaryExpression(expression)) {
130
- const operator = expression.operatorToken.kind;
131
- if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
132
- return [...valuesOf(expression.left), ...valuesOf(expression.right)];
133
- }
134
- if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
135
- return valuesOf(expression.right);
136
- }
137
- }
138
- return [expression];
139
- }
140
-
141
- /** The expressions one node hands to a position typed by its context. */
142
- function handedOn(node: Node): readonly Expression[] {
143
- if (
144
- (isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
145
- node.initializer !== undefined &&
146
- writesType(node)
147
- ) {
148
- return [node.initializer];
149
- }
150
- if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
151
- return [node.right];
152
- }
153
- if (isCallExpression(node) || isNewExpression(node)) {
154
- return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
155
- }
156
- if (isReturnStatement(node)) {
157
- return node.expression === undefined ? [] : [node.expression];
158
- }
159
- if (isArrowFunction(node)) {
160
- return node.body.kind === SyntaxKind.Block ? [] : [node.body];
161
- }
162
- if (isArrayLiteralExpression(node)) {
163
- return node.elements.filter(
164
- (element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
165
- );
166
- }
167
- if (isPropertyAssignment(node)) {
168
- return [node.initializer];
169
- }
170
- if (isShorthandPropertyAssignment(node)) {
171
- return isIdentifier(node.name) ? [node.name] : [];
172
- }
173
- if (isAsExpression(node) || isSatisfiesExpression(node)) {
174
- return [node.expression];
175
- }
176
- return [];
177
- }
178
-
179
88
  /** What one project's walk found to resolve. */
180
89
  interface Walked {
181
90
  readonly classes: readonly ClassAt[];
package/src/inventory.ts CHANGED
@@ -303,7 +303,10 @@ function typeParametersOf(node: Node): readonly Node[] {
303
303
  if (
304
304
  isFunctionDeclaration(node) ||
305
305
  isMethodDeclaration(node) ||
306
- isMethodSignatureDeclaration(node)
306
+ isMethodSignatureDeclaration(node) ||
307
+ isClassDeclaration(node) ||
308
+ isInterfaceDeclaration(node) ||
309
+ isTypeAliasDeclaration(node)
307
310
  ) {
308
311
  return node.typeParameters ?? [];
309
312
  }
@@ -467,7 +470,20 @@ function nameNodeOf(node: Node): Node | undefined {
467
470
  * needs the declaration this enumeration made for it.
468
471
  */
469
472
  export function nodeKey(file: SourceFile, node: Node): string {
470
- return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
473
+ return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
474
+ }
475
+
476
+ /** `export default`, up to the keyword that stands where an unnamed default export's name would. */
477
+ const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
478
+
479
+ /**
480
+ * Where an unnamed declaration is placed: at `default` when it is the default export, so it
481
+ * never shares the file's own position, else at its first token.
482
+ */
483
+ function unnamedStart(node: Node): number {
484
+ const start = node.getStart();
485
+ const prefix = DEFAULT_EXPORT.exec(node.getText());
486
+ return prefix === null ? start : start + prefix[0].length;
471
487
  }
472
488
 
473
489
  /** The kind a class element takes: a method, or a property or accessor. */
@@ -562,12 +578,11 @@ export function inventory<Brand>(
562
578
  name: Node | undefined,
563
579
  named: { readonly component: Component; readonly localName: string | undefined },
564
580
  ): Building {
565
- const at = name ?? node;
566
581
  const record = keep({
567
582
  file: owner.file,
568
583
  node,
569
584
  module: owner.module,
570
- position: renderPosition(owner.file, targetRoot, at.getStart()),
585
+ position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
571
586
  endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
572
587
  kind,
573
588
  parentChain: owner.kind === "file" ? [] : chainOf(owner),
package/src/kinds.ts CHANGED
@@ -60,6 +60,8 @@ const ROWS: readonly KindRow[] = [
60
60
  name: "unused-member",
61
61
  languages: ["go", "ts"],
62
62
  rule: "A struct field, class member or type member with no reference, a private member included. A member referenced only from its own declaration site counts as unreferenced.",
63
+ precondition:
64
+ "A TypeScript member that no value writes and nothing reads, whose type names a type parameter of its declaring interface, class or type alias, is not reported when every part of that declaration that names the type parameter is a member no value writes and nothing reads. TypeScript compares object types by their members, so such members are what make two instantiations of their type distinct types, and deleting them leaves the type parameter phantom and lets one instantiation stand for another. A member a value writes is decided by the rule of DS1301 instead, whatever its type names.",
63
65
  defaultSeverity: "deny",
64
66
  maxClass: "certain",
65
67
  fixability: "deletable",
@@ -101,7 +103,7 @@ const ROWS: readonly KindRow[] = [
101
103
  languages: ["go", "ts"],
102
104
  rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
103
105
  precondition:
104
- "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
106
+ "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
105
107
  defaultSeverity: "warn",
106
108
  maxClass: "certain",
107
109
  fixability: "narrowable",
@@ -113,7 +115,7 @@ const ROWS: readonly KindRow[] = [
113
115
  languages: ["go"],
114
116
  rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method, and three subjects are excluded: an interface method, whose exportedness is the contract of the interface that declares it; a struct field, which an encoder reads by name; and a method that satisfies an interface some symbol uses as a type, which cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference; where the edge's other side is unknown to the analyzer the finding is emitted pending.",
115
117
  precondition:
116
- "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
118
+ "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
117
119
  defaultSeverity: "warn",
118
120
  maxClass: "certain",
119
121
  fixability: "narrowable",
@@ -177,7 +179,7 @@ const ROWS: readonly KindRow[] = [
177
179
  code: "DS1301",
178
180
  name: "write-only-symbol",
179
181
  languages: ["go", "ts"],
180
- rule: "A package-level or module-level variable, struct field, class member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. The finding names each write position, so the deletion set is visible.",
182
+ rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, or an import binding the writes alone use. The finding's message names each such declaration.",
181
183
  defaultSeverity: "deny",
182
184
  maxClass: "certain",
183
185
  fixability: "deletable",
package/src/manifest.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * The entry points a package manifest names, which are roots because something
3
3
  * outside the analyzed program reaches them: a consumer importing the package, or a
4
- * shell running its command or one of its scripts.
5
- *
6
- * One call reads one manifest, the one in the directory it is given.
4
+ * shell running its command or one of its scripts. {@link readManifest} reads one
5
+ * directory's manifest, {@link packageEntries} those of the packages below the target root.
7
6
  */
8
7
 
9
8
  import type { Host } from "./host.ts";
10
- import { isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
9
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
11
10
 
12
11
  /** The manifest's own name. */
13
12
  const MANIFEST = "package.json";
@@ -205,3 +204,40 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
205
204
  declaresExports: manifest["exports"] !== undefined,
206
205
  };
207
206
  }
207
+
208
+ /**
209
+ * The entry points every package manifest between the target root and each compiler
210
+ * configuration names: each directory holding a `package.json` that a configuration sits
211
+ * in or below, the target root and `known` excluded, whose manifests the run reads
212
+ * otherwise. Each entry is spelled under its manifest's path below the target root, and
213
+ * is published by its own manifest's rule, as the target's own entries are.
214
+ */
215
+ export function packageEntries(
216
+ host: Host,
217
+ targetRoot: string,
218
+ configFiles: readonly string[],
219
+ known: ReadonlySet<string>,
220
+ ): readonly ManifestEntry[] {
221
+ const dirs = new Set<string>();
222
+ for (const configFile of configFiles) {
223
+ for (
224
+ let dir = dirnamePath(resolvePath(targetRoot, configFile));
225
+ dir !== targetRoot && relativePath(targetRoot, dir) !== undefined;
226
+ dir = dirnamePath(dir)
227
+ ) {
228
+ if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
229
+ dirs.add(dir);
230
+ }
231
+ }
232
+ }
233
+ return [...dirs].sort().flatMap((dir) => {
234
+ const manifest = readManifest(host, dir);
235
+ const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
236
+ return manifest.entries.map((entry) => ({
237
+ ...entry,
238
+ member: `${where} ${entry.member}`,
239
+ published:
240
+ entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
241
+ }));
242
+ });
243
+ }