@cplieger/deadset-ts 5.0.0 → 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 (55) hide show
  1. package/dist/src/analysis.js +77 -10
  2. package/dist/src/configuration-files.js +107 -3
  3. package/dist/src/conformance-record.js +3 -9
  4. package/dist/src/contextual-uses.js +240 -0
  5. package/dist/src/convention-rows.js +6 -0
  6. package/dist/src/conventions.js +123 -14
  7. package/dist/src/dependencies.js +8 -2
  8. package/dist/src/destructuring.js +6 -6
  9. package/dist/src/entry-points.js +8 -5
  10. package/dist/src/exempt-classes.js +6 -1
  11. package/dist/src/exempt.js +21 -17
  12. package/dist/src/findings/completion.js +27 -3
  13. package/dist/src/findings/interfaces.js +6 -2
  14. package/dist/src/findings/reads-and-writes.js +53 -9
  15. package/dist/src/findings/unused-declarations.js +40 -1
  16. package/dist/src/generated-file.js +24 -0
  17. package/dist/src/interface-satisfaction.js +2 -68
  18. package/dist/src/inventory.js +4 -1
  19. package/dist/src/kinds.js +4 -3
  20. package/dist/src/query.js +15 -0
  21. package/dist/src/references.js +199 -10
  22. package/dist/src/resolve.js +12 -11
  23. package/dist/src/roots.js +1 -1
  24. package/dist/src/stores.js +3 -0
  25. package/dist/src/value-flow.js +74 -0
  26. package/dist/src/version.js +1 -1
  27. package/jsr.json +1 -1
  28. package/package.json +2 -2
  29. package/src/analysis.ts +92 -13
  30. package/src/configuration-files.ts +136 -1
  31. package/src/conformance-record.ts +3 -10
  32. package/src/contextual-uses.ts +354 -0
  33. package/src/convention-rows.ts +8 -0
  34. package/src/conventions.ts +161 -13
  35. package/src/dependencies.ts +14 -1
  36. package/src/destructuring.ts +16 -6
  37. package/src/entry-points.ts +16 -3
  38. package/src/exempt-classes.ts +6 -1
  39. package/src/exempt.ts +48 -17
  40. package/src/file-facts.ts +2 -0
  41. package/src/findings/completion.ts +31 -3
  42. package/src/findings/interfaces.ts +9 -2
  43. package/src/findings/reads-and-writes.ts +70 -9
  44. package/src/findings/unused-declarations.ts +51 -1
  45. package/src/generated-file.ts +27 -0
  46. package/src/interface-satisfaction.ts +1 -92
  47. package/src/inventory.ts +4 -1
  48. package/src/kinds.ts +5 -3
  49. package/src/query.ts +26 -0
  50. package/src/references.ts +254 -21
  51. package/src/resolve.ts +14 -13
  52. package/src/roots.ts +11 -1
  53. package/src/stores.ts +9 -0
  54. package/src/value-flow.ts +104 -0
  55. package/src/version.ts +1 -1
@@ -5,15 +5,16 @@
5
5
  * directory a configuration property moves read from the property's literal value.
6
6
  * No configuration file is run.
7
7
  */
8
- import { isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { lineOf } from "./component-files.js";
10
10
  import { CONVENTION_ROWS, } from "./convention-rows.js";
11
11
  import { globExpression } from "./glob.js";
12
12
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
13
13
  import { satisfies } from "./version-range.js";
14
+ /** The sections an installed package is required by, so its absence fails the run. */
15
+ const REQUIRED_SECTIONS = ["dependencies", "devDependencies"];
14
16
  const DEPENDENCY_SECTIONS = [
15
- "dependencies",
16
- "devDependencies",
17
+ ...REQUIRED_SECTIONS,
17
18
  "peerDependencies",
18
19
  "optionalDependencies",
19
20
  ];
@@ -28,11 +29,11 @@ function readJSON(host, path) {
28
29
  return undefined;
29
30
  }
30
31
  }
31
- function declares(manifest, name) {
32
+ function declares(manifest, name, sections = DEPENDENCY_SECTIONS) {
32
33
  if (!isRecord(manifest)) {
33
34
  return false;
34
35
  }
35
- return DEPENDENCY_SECTIONS.some((section) => {
36
+ return sections.some((section) => {
36
37
  const held = manifest[section];
37
38
  return isRecord(held) && Object.hasOwn(held, name);
38
39
  });
@@ -70,20 +71,29 @@ function unwrapped(node) {
70
71
  }
71
72
  return at;
72
73
  }
73
- /** The key one property is written with, where it is written as a name or a literal. */
74
+ /** The key one property is written with, where it is written as a name or as literal text. */
74
75
  function keyOf(property) {
75
76
  if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
76
77
  return undefined;
77
78
  }
78
79
  const { name } = property;
79
- if (isComputedPropertyName(name)) {
80
- return undefined;
81
- }
82
- if (isIdentifier(name) || isStringLiteral(name) || isNoSubstitutionTemplateLiteral(name)) {
83
- return name.text;
80
+ const key = isComputedPropertyName(name) ? name.expression : name;
81
+ if ((isIdentifier(key) && key === name) ||
82
+ isStringLiteral(key) ||
83
+ isNoSubstitutionTemplateLiteral(key)) {
84
+ return key.text;
84
85
  }
85
86
  return undefined;
86
87
  }
88
+ /** Whether one member's computed name is no literal, so it may set any key. */
89
+ function computesKey(member) {
90
+ const name = member.name;
91
+ if (name === undefined || !isComputedPropertyName(name)) {
92
+ return false;
93
+ }
94
+ const key = name.expression;
95
+ return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
96
+ }
87
97
  /** The values one file's top-level variable declarations bind, by name. */
88
98
  function declaredValues(file) {
89
99
  const values = new Map();
@@ -134,6 +144,67 @@ function defaultExports(file) {
134
144
  }
135
145
  return found;
136
146
  }
147
+ /** The declarations whose name is a binding of the scope they are written in. */
148
+ const BINDING_KINDS = new Set([
149
+ SyntaxKind.VariableDeclaration,
150
+ SyntaxKind.Parameter,
151
+ SyntaxKind.BindingElement,
152
+ SyntaxKind.FunctionDeclaration,
153
+ SyntaxKind.ClassDeclaration,
154
+ SyntaxKind.ImportClause,
155
+ SyntaxKind.ImportSpecifier,
156
+ SyntaxKind.NamespaceImport,
157
+ SyntaxKind.ImportEqualsDeclaration,
158
+ ]);
159
+ /** Whether one file declares a binding named `require` at any depth, which shadows the global. */
160
+ function declaresRequire(file) {
161
+ let found = false;
162
+ const visit = (node) => {
163
+ const name = node.name;
164
+ if (name !== undefined &&
165
+ isIdentifier(name) &&
166
+ name.text === "require" &&
167
+ BINDING_KINDS.has(node.kind)) {
168
+ found = true;
169
+ }
170
+ if (!found) {
171
+ node.forEachChild(visit);
172
+ }
173
+ };
174
+ file.forEachChild(visit);
175
+ return found;
176
+ }
177
+ /**
178
+ * The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
179
+ * written with the module's name as its one string-literal argument, in a file that
180
+ * declares no binding named `require`: each binds as an import of the module does.
181
+ */
182
+ function requireBindings(file, module) {
183
+ const found = [];
184
+ for (const statement of file.statements) {
185
+ if (!isVariableStatement(statement)) {
186
+ continue;
187
+ }
188
+ for (const declaration of statement.declarationList.declarations) {
189
+ const value = declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
190
+ const [argument, ...rest] = value !== undefined && isCallExpression(value) ? value.arguments : [];
191
+ if (value === undefined ||
192
+ !isCallExpression(value) ||
193
+ !isIdentifier(value.expression) ||
194
+ value.expression.text !== "require" ||
195
+ argument === undefined ||
196
+ rest.length > 0 ||
197
+ !isStringLiteral(argument) ||
198
+ argument.text !== module) {
199
+ continue;
200
+ }
201
+ if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
202
+ found.push(declaration.name);
203
+ }
204
+ }
205
+ }
206
+ return found.length > 0 && declaresRequire(file) ? [] : found;
207
+ }
137
208
  /**
138
209
  * The first argument of every call one file makes to one module's export, through a
139
210
  * named or a namespace import; `undefined` for a call with no argument.
@@ -141,6 +212,24 @@ function defaultExports(file) {
141
212
  function optionsArguments(file, call) {
142
213
  const names = new Set();
143
214
  const namespaces = new Set();
215
+ for (const name of requireBindings(file, call.module)) {
216
+ if (isIdentifier(name)) {
217
+ namespaces.add(name.text);
218
+ continue;
219
+ }
220
+ for (const element of name.elements) {
221
+ const local = element.name;
222
+ const exported = element.propertyName ?? local;
223
+ if (element.dotDotDotToken === undefined &&
224
+ local !== undefined &&
225
+ isIdentifier(local) &&
226
+ exported !== undefined &&
227
+ (isIdentifier(exported) || isStringLiteral(exported)) &&
228
+ exported.text === call.export) {
229
+ names.add(local.text);
230
+ }
231
+ }
232
+ }
144
233
  for (const statement of file.statements) {
145
234
  const bindings = isImportDeclaration(statement)
146
235
  ? statement.importClause?.namedBindings
@@ -207,7 +296,7 @@ function objectOf(node, declared, throughCalls, seen = new Set()) {
207
296
  function readPath(object, keys, values) {
208
297
  const [key, ...rest] = keys;
209
298
  for (const member of object.properties) {
210
- if (isSpreadAssignment(member)) {
299
+ if (isSpreadAssignment(member) || computesKey(member)) {
211
300
  return member;
212
301
  }
213
302
  if (keyOf(member) !== key) {
@@ -352,6 +441,7 @@ function compare(a, b) {
352
441
  export function readConventions(host, parse, manifests, disabled, rows = CONVENTION_ROWS) {
353
442
  const applied = [];
354
443
  const globs = [];
444
+ const generated = [];
355
445
  const failures = [];
356
446
  for (const manifest of manifests) {
357
447
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
@@ -366,7 +456,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
366
456
  }
367
457
  const version = versions.get(row.package);
368
458
  if (version === undefined) {
369
- if (missing.has(row.package)) {
459
+ if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
370
460
  continue;
371
461
  }
372
462
  missing.add(row.package);
@@ -413,6 +503,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
413
503
  const excludes = [
414
504
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
415
505
  ].map(globExpression);
506
+ for (const below of new Set((row.generated ?? []).flatMap((one) => expand(one, resolved)))) {
507
+ generated.push({ dir: manifest.dir, row: row.name, below });
508
+ }
416
509
  for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
417
510
  globs.push({
418
511
  dir: manifest.dir,
@@ -427,7 +520,23 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
427
520
  unique.sort((a, b) => compare(a.name, b.name) ||
428
521
  compare(a.manifest, b.manifest) ||
429
522
  compare(JSON.stringify(a), JSON.stringify(b)));
430
- return { applied: unique, globs, failures };
523
+ return { applied: unique, globs, generated, failures };
524
+ }
525
+ /**
526
+ * Every own file of one project below a directory an applied row names as generated, by
527
+ * its path below the target root, with the name of the first such row.
528
+ */
529
+ export function generatedFiles(conventions, files) {
530
+ const found = new Map();
531
+ for (const { dir, row, below } of conventions.generated) {
532
+ const root = joinPath(dir, below);
533
+ for (const [path, file] of files.byPath) {
534
+ if (!found.has(path) && relativePath(root, file.fileName) !== undefined) {
535
+ found.set(path, row);
536
+ }
537
+ }
538
+ }
539
+ return found;
431
540
  }
432
541
  /** Every own file of one project an applied row's globs match, once per row. */
433
542
  export function conventionEntries(conventions, files) {
@@ -187,7 +187,7 @@ function bindingsOf(node) {
187
187
  * shadows the name is not one. A string of a configuration file uses each dependency
188
188
  * the manifest beside the file declares that it spells.
189
189
  */
190
- export function projectNeeds(project, held, host, targetRoot) {
190
+ export function projectNeeds(project, held, host, targetRoot, parse) {
191
191
  const programPackages = new Set();
192
192
  for (const name of project.program.getSourceFileNames()) {
193
193
  const named = packageOfPath(name);
@@ -225,7 +225,7 @@ export function projectNeeds(project, held, host, targetRoot) {
225
225
  }
226
226
  return names;
227
227
  };
228
- const configuration = configurationFiles(host, targetRoot, sourceFilesOf(project, targetRoot));
228
+ const configuration = configurationFiles(host, targetRoot, sourceFilesOf(project, targetRoot), parse);
229
229
  for (const { file } of configuration.modules) {
230
230
  const names = declaredAt(file.fileName);
231
231
  for (const { node, text } of moduleStrings(file)) {
@@ -238,6 +238,12 @@ export function projectNeeds(project, held, host, targetRoot) {
238
238
  use([PROJECT_OWNER], dependenciesNamed(text, names));
239
239
  }
240
240
  }
241
+ for (const { file, manifestDir } of configuration.outside) {
242
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
243
+ for (const { text } of moduleStrings(file)) {
244
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
245
+ }
246
+ }
241
247
  return { packages, uses, unanswered };
242
248
  }
243
249
  /** The declarations of the inventory a use written at one node belongs to. */
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { isArrayLiteralExpression, isBinaryExpression, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isShorthandPropertyAssignment, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { TypeFlags } from "@typescript/native/unstable/sync";
10
- import { isAnswered, UNANSWERED } from "./query.js";
10
+ import { isAnswered, propertyTables, UNANSWERED, } from "./query.js";
11
11
  /** The text a property name spells, or `undefined` for a computed name. */
12
12
  function nameText(node) {
13
13
  if (node === undefined) {
@@ -151,7 +151,7 @@ function sourceOf(target) {
151
151
  * read once per type for the whole project, since one type is destructured in many
152
152
  * places and a table answers every name of it.
153
153
  */
154
- export function destructuring(project, cap) {
154
+ export function destructuring(project, cap, properties = propertyTables(project.queries)) {
155
155
  const queries = project.queries;
156
156
  const tables = new Map();
157
157
  let patternBatches = 0;
@@ -163,10 +163,10 @@ export function destructuring(project, cap) {
163
163
  }
164
164
  let table = new Map();
165
165
  if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
166
- patternLookups += 1;
167
- const properties = queries.propertiesOf(type);
168
- table = isAnswered(properties)
169
- ? new Map(properties.map((property) => [property.name, property]))
166
+ const read = properties.of(type);
167
+ patternLookups += read.asked ? 1 : 0;
168
+ table = isAnswered(read.properties)
169
+ ? new Map(read.properties.map((property) => [property.name, property]))
170
170
  : UNANSWERED;
171
171
  }
172
172
  tables.set(type.id, table);
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
14
14
  import { SymbolFlags } from "@typescript/native/unstable/sync";
15
- import { configurationFiles, filesNamedBy, moduleStrings } from "./configuration-files.js";
15
+ import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
16
  import { globExpression } from "./glob.js";
17
17
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
18
18
  import { UNANSWERED } from "./query.js";
@@ -169,8 +169,8 @@ function isBrowserTestConfig(name) {
169
169
  * with what it exports, and each own file a relative string of a configuration module
170
170
  * or a JSON configuration file names, read against that file's directory.
171
171
  */
172
- function configurationEntries(host, targetRoot, files) {
173
- const { modules, documents } = configurationFiles(host, targetRoot, files);
172
+ function configurationEntries(host, targetRoot, files, parse) {
173
+ const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
174
174
  const found = [];
175
175
  const strings = (dir, texts) => {
176
176
  for (const text of texts) {
@@ -186,6 +186,9 @@ function configurationEntries(host, targetRoot, files) {
186
186
  for (const document of documents) {
187
187
  strings(dirnamePath(document.path), document.strings);
188
188
  }
189
+ for (const { file } of outside) {
190
+ strings(dirnamePath(file.fileName), moduleStrings(file).map((one) => one.text));
191
+ }
189
192
  return found;
190
193
  }
191
194
  /**
@@ -381,8 +384,8 @@ function workersOf(project, file, files) {
381
384
  * file's path below the target root; a file one of them matches is a test file the
382
385
  * runner executes.
383
386
  */
384
- export function entryPoints(project, files, testFiles, host, targetRoot) {
385
- const found = [...configurationEntries(host, targetRoot, files)];
387
+ export function entryPoints(project, files, testFiles, host, targetRoot, parse) {
388
+ const found = [...configurationEntries(host, targetRoot, files, parse)];
386
389
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
387
390
  for (const [path, file] of files.byPath) {
388
391
  const name = basename(path);
@@ -26,7 +26,12 @@ export const EXEMPTION_CLASSES = [
26
26
  confidence: "certain",
27
27
  typescriptVisibility: { private: false, privateName: false },
28
28
  },
29
- { class: "generated-file", languages: ["go"], confidence: "certain" },
29
+ {
30
+ class: "generated-file",
31
+ languages: ["go", "ts"],
32
+ confidence: "certain",
33
+ typescriptVisibility: { private: true, privateName: false },
34
+ },
30
35
  { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
31
36
  {
32
37
  class: "template-field",
@@ -88,8 +88,13 @@ function detected(exemptionClass, detect, input) {
88
88
  * test file makes is none there.
89
89
  */
90
90
  export function computeExemptions(input, detectors, holding) {
91
+ return detectExemptions(input, detectors, holding).records;
92
+ }
93
+ /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
94
+ export function detectExemptions(input, detectors, holding) {
91
95
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
92
96
  const found = [];
97
+ const inTestFiles = [];
93
98
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
94
99
  const detect = detectors.get(exemptionClass);
95
100
  if (detect === undefined || holding.disabled.has(exemptionClass)) {
@@ -100,19 +105,21 @@ export function computeExemptions(input, detectors, holding) {
100
105
  if (!retainable(allowed, visibility.get(evidence.id))) {
101
106
  continue;
102
107
  }
103
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
104
- continue;
105
- }
106
- found.push({
108
+ const record = {
107
109
  id: evidence.id,
108
110
  class: exemptionClass,
109
111
  detail: evidence.detail,
110
112
  site: evidence.site,
111
113
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
112
- });
114
+ };
115
+ if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
116
+ inTestFiles.push(record);
117
+ continue;
118
+ }
119
+ found.push(record);
113
120
  }
114
121
  }
115
- return exemptionsOf(found);
122
+ return { records: exemptionsOf(found), inTestFiles };
116
123
  }
117
124
  /** Two strings ordered bytewise. */
118
125
  function compare(a, b) {
@@ -160,24 +167,21 @@ function byDeclaration(records) {
160
167
  return grouped;
161
168
  }
162
169
  /**
163
- * The records that held back a declaration some configuration of the run would
164
- * otherwise judge dead: each configuration's graph is swept once more with no
165
- * exemption and the marks it was given, and a record is retained when one of those
166
- * sweeps judged its declaration a candidate. A record that holds a declaration back in
167
- * one configuration is retained whatever another configuration answers about it, as a
168
- * run of that configuration alone would report the declaration without it. A record
169
- * on a declaration that a relation or a mark holds live in every configuration held
170
- * nothing back. The order is the declarations' site order, and for one declaration
171
- * the input's.
170
+ * The records that held back a declaration: one some configuration's graph, swept
171
+ * again with no exemption and its marks, judges a candidate, whatever another
172
+ * configuration answers, or one in `writeOnly`, which the run would report as written
173
+ * and never read without the record's read. A declaration a relation or a mark holds
174
+ * live in every configuration is held back by no record. The order is the
175
+ * declarations' site order, and for one declaration the input's.
172
176
  */
173
- export function retainedIn(matrix, input) {
177
+ export function retainedIn(matrix, input, writeOnly = new Set()) {
174
178
  const exempt = input.exempt ?? [];
175
179
  if (exempt.length === 0) {
176
180
  return [];
177
181
  }
178
182
  const bare = { marked: input.marked, mode: input.mode };
179
183
  const dead = new Set(matrix.graphs.flatMap((graph) => sweep(graph, bare).candidates.map((candidate) => candidate.id)));
180
- const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
184
+ const held = byDeclaration(exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)));
181
185
  return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
182
186
  }
183
187
  /**
@@ -116,7 +116,8 @@ export function completed(input, findings, kinds = KINDS) {
116
116
  const classOf = reachabilityClasses(input);
117
117
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
118
118
  let minted = swept.sweep.components.length;
119
- return findings.map((finding) => {
119
+ const dead = new Set(swept.sweep.components.map((component) => component.id));
120
+ const done = findings.map((finding) => {
120
121
  const row = kinds.get(finding.code);
121
122
  if (row === undefined) {
122
123
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
@@ -151,6 +152,7 @@ export function completed(input, findings, kinds = KINDS) {
151
152
  ? union.made[at]
152
153
  : { production: candidate.productionRefs, test: candidate.testRefs };
153
154
  const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
155
+ const generated = input.files.generated?.has(finding.position.path) === true;
154
156
  return {
155
157
  code: finding.code,
156
158
  kind: row.name,
@@ -161,7 +163,7 @@ export function completed(input, findings, kinds = KINDS) {
161
163
  confidence: capped(reachabilityClass, row.maxClass),
162
164
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
163
165
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
164
- generated: false,
166
+ generated,
165
167
  component,
166
168
  retainedBy: [],
167
169
  configurations: candidate?.configurations ??
@@ -169,12 +171,34 @@ export function completed(input, findings, kinds = KINDS) {
169
171
  ? swept.matrix.configurations
170
172
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
171
173
  consumersLoaded: input.boundary.consumers.loaded,
172
- fixability: row.fixability,
174
+ fixability: generated ? "none" : row.fixability,
173
175
  severity: severityOf(input, finding.code, row.defaultSeverity),
174
176
  message: finding.message,
175
177
  details: finding.details ?? {},
176
178
  };
177
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
+ });
178
202
  }
179
203
  /** Whether the dials withhold a finding by its own severity or confidence. */
180
204
  function dialed(finding, minConfidence) {
@@ -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
  }