@cplieger/deadset-ts 5.2.3 → 6.0.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 (73) hide show
  1. package/dist/src/analysis.js +28 -5
  2. package/dist/src/augmentations.js +50 -0
  3. package/dist/src/components.js +15 -15
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/convention-rows.js +17 -2
  6. package/dist/src/conventions.js +82 -1
  7. package/dist/src/dependencies.js +42 -26
  8. package/dist/src/entry-points.js +28 -14
  9. package/dist/src/exempt.js +1 -0
  10. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  11. package/dist/src/findings/emitters.js +47 -12
  12. package/dist/src/findings/interfaces.js +4 -3
  13. package/dist/src/findings/intra-function.js +15 -2
  14. package/dist/src/findings/unused-declarations.js +13 -1
  15. package/dist/src/findings-pass.js +1 -0
  16. package/dist/src/graph.js +6 -6
  17. package/dist/src/implementations.js +1 -1
  18. package/dist/src/indexed-access.js +215 -0
  19. package/dist/src/interface-satisfaction.js +85 -23
  20. package/dist/src/intra-function-parts.js +139 -30
  21. package/dist/src/inventory.js +6 -1
  22. package/dist/src/kinds.js +13 -12
  23. package/dist/src/matrix.js +34 -9
  24. package/dist/src/module-declarations.js +180 -0
  25. package/dist/src/outside-tests.js +17 -15
  26. package/dist/src/overrides.js +70 -9
  27. package/dist/src/query.js +7 -0
  28. package/dist/src/ref.js +1 -0
  29. package/dist/src/references.js +6 -0
  30. package/dist/src/report.js +6 -1
  31. package/dist/src/reporters/sarif.js +3 -1
  32. package/dist/src/reporters/text.js +23 -3
  33. package/dist/src/roots.js +50 -10
  34. package/dist/src/version.js +1 -1
  35. package/dist/src/workflow-steps.js +82 -0
  36. package/jsr.json +1 -1
  37. package/package.json +7 -1
  38. package/src/analysis.ts +40 -4
  39. package/src/augmentations.ts +72 -0
  40. package/src/components.ts +15 -13
  41. package/src/conformance-record.ts +2 -2
  42. package/src/convention-rows.ts +25 -3
  43. package/src/conventions.ts +106 -2
  44. package/src/dependencies.ts +51 -31
  45. package/src/entry-points.ts +32 -19
  46. package/src/exempt.ts +4 -1
  47. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  48. package/src/findings/emitters.ts +71 -12
  49. package/src/findings/interfaces.ts +7 -6
  50. package/src/findings/intra-function.ts +20 -2
  51. package/src/findings/unused-declarations.ts +16 -1
  52. package/src/findings-pass.ts +4 -1
  53. package/src/graph.ts +6 -6
  54. package/src/implementations.ts +1 -1
  55. package/src/indexed-access.ts +295 -0
  56. package/src/interface-satisfaction.ts +118 -27
  57. package/src/intra-function-parts.ts +196 -35
  58. package/src/inventory.ts +8 -0
  59. package/src/kinds.ts +14 -12
  60. package/src/matrix.ts +51 -8
  61. package/src/module-declarations.ts +241 -0
  62. package/src/outside-tests.ts +18 -16
  63. package/src/overrides.ts +80 -14
  64. package/src/query.ts +28 -0
  65. package/src/ref.ts +1 -0
  66. package/src/references.ts +6 -0
  67. package/src/report.ts +11 -1
  68. package/src/reporters/sarif.ts +4 -1
  69. package/src/reporters/text.ts +25 -3
  70. package/src/roots.ts +56 -7
  71. package/src/sweep.ts +12 -0
  72. package/src/version.ts +1 -1
  73. package/src/workflow-steps.ts +104 -0
@@ -4,7 +4,7 @@
4
4
  * call sites that use or discard it, each statement control flow cannot reach, each
5
5
  * write to a local that no read reaches, and each case an earlier case covers.
6
6
  */
7
- import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
7
+ import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
8
8
  import { nodeKey } from "./inventory.js";
9
9
  import { positionKey, renderPosition } from "./position.js";
10
10
  import { isAnswered, UNANSWERED } from "./query.js";
@@ -24,8 +24,6 @@ const NO_VALUE = new Set([
24
24
  "Promise<undefined>",
25
25
  "Promise<never>",
26
26
  ]);
27
- /** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
28
- const UNUSED_PREFIX = "_";
29
27
  /** The rendered position of one node, from its first token to its last line. */
30
28
  function spanOf(file, root, node) {
31
29
  const start = renderPosition(file, root, node.getStart());
@@ -284,6 +282,8 @@ export function projectParts(project, held, references, targetRoot) {
284
282
  const id = held.declarations.get(nodeKey(file, node));
285
283
  return id === undefined || kinds.get(id) === "file" ? undefined : id;
286
284
  };
285
+ // A part no declaration holds belongs to the file, which is judged while the file is live.
286
+ const fileId = held.declarations.get(nodeKey(file, file));
287
287
  const holderOf = (node) => {
288
288
  for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
289
289
  const id = declarationAt(at);
@@ -291,9 +291,17 @@ export function projectParts(project, held, references, targetRoot) {
291
291
  return id;
292
292
  }
293
293
  }
294
- return undefined;
294
+ return fileId;
295
295
  };
296
- const walk = { declared: [], blocks: [], unreachable: [], cases: [] };
296
+ const walk = {
297
+ declared: [],
298
+ values: [],
299
+ catches: [],
300
+ blocks: [],
301
+ unreachable: [],
302
+ cases: [],
303
+ };
304
+ const covered = new Set();
297
305
  const position = (node) => positionKey(renderPosition(file, targetRoot, node.getStart()));
298
306
  // Code below an unreachable statement is still walked for its calls and specifiers,
299
307
  // and reports no part of its own: the unreachable statement is the one finding.
@@ -322,6 +330,19 @@ export function projectParts(project, held, references, targetRoot) {
322
330
  const signature = id === undefined ? undefined : signatureOf(node);
323
331
  if (id !== undefined && signature !== undefined) {
324
332
  walk.declared.push({ id, node, signature });
333
+ covered.add(signature);
334
+ }
335
+ const value = covered.has(node) ? undefined : valueSignature(node);
336
+ const valueHolder = id ?? holderOf(node);
337
+ if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
338
+ walk.values.push({ holder: valueHolder, signature: value });
339
+ }
340
+ if (isCatchClause(node) && node.variableDeclaration !== undefined) {
341
+ const name = node.variableDeclaration.name;
342
+ const holder = holderOf(node);
343
+ if (isIdentifier(name) && holder !== undefined && silenced === 0) {
344
+ walk.catches.push({ clause: node, name, holder });
345
+ }
325
346
  }
326
347
  if (isCallExpression(node)) {
327
348
  const name = calleeName(node);
@@ -371,15 +392,21 @@ export function projectParts(project, held, references, targetRoot) {
371
392
  // One batch per file resolves every name a parameter or a local is read through.
372
393
  const asked = [];
373
394
  const readers = new Map();
374
- for (const one of walk.declared) {
375
- const names = one.signature.parameters
376
- .map((parameter) => parameter.name)
377
- .filter(isIdentifier);
378
- const spelled = new Set(names.map((name) => name.text));
379
- const uses = identifiersNaming(one.signature, spelled).filter((use) => !names.includes(use));
380
- readers.set(one, { names, uses });
381
- asked.push(...names, ...uses);
395
+ for (const signature of [
396
+ ...walk.declared.map((one) => one.signature),
397
+ ...walk.values.map((one) => one.signature),
398
+ ]) {
399
+ const names = parameterNames(signature);
400
+ const declaredNames = new Set(names.map((one) => one.name));
401
+ const spelled = new Set(names.map((one) => one.name.text));
402
+ const uses = identifiersNaming(signature, spelled).filter((use) => !declaredNames.has(use));
403
+ readers.set(signature, { names, uses });
404
+ asked.push(...names.map((one) => one.name), ...uses);
382
405
  }
406
+ const caught = walk.catches.map(({ clause, name }) => identifiersNaming(clause.block, new Set([name.text])));
407
+ walk.catches.forEach(({ name }, at) => {
408
+ asked.push(name, ...(caught[at] ?? []));
409
+ });
383
410
  const stores = walk.blocks.map((block) => localsOf(block));
384
411
  for (const store of stores) {
385
412
  asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
@@ -392,9 +419,28 @@ export function projectParts(project, held, references, targetRoot) {
392
419
  stub: isStub(one.signature),
393
420
  fixed: hierarchyFixes(one.node, extended),
394
421
  });
395
- const { uses } = readers.get(one) ?? { uses: [] };
396
- parts.push(...parameterParts(file, targetRoot, one, uses, ids));
422
+ const reader = readers.get(one.signature);
423
+ if (reader !== undefined) {
424
+ parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
425
+ }
426
+ }
427
+ for (const one of walk.values) {
428
+ const reader = readers.get(one.signature);
429
+ if (reader !== undefined) {
430
+ parts.push(...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true));
431
+ }
397
432
  }
433
+ walk.catches.forEach(({ name, holder }, at) => {
434
+ parts.push({
435
+ code: DEAD_STORE,
436
+ declaration: holder,
437
+ kind: "store",
438
+ name: name.text,
439
+ position: spanOf(file, targetRoot, name),
440
+ message: `value written to ${name.text} is never read`,
441
+ used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
442
+ });
443
+ });
398
444
  walk.blocks.forEach((block, at) => {
399
445
  const holder = holderOf(block);
400
446
  if (holder !== undefined) {
@@ -427,31 +473,91 @@ export function projectParts(project, held, references, targetRoot) {
427
473
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
428
474
  return { parts, signatures, calls, specifiers };
429
475
  }
430
- /** Each named parameter of one function and whether the function reads it. */
431
- function parameterParts(file, root, one, uses, ids) {
432
- const signature = one.signature;
476
+ /**
477
+ * A function written in place that no declaration declares: an arrow, a function
478
+ * expression, a nested function declaration, or a method of an object literal.
479
+ */
480
+ function valueSignature(node) {
481
+ if (isArrowFunction(node) || isFunctionExpression(node)) {
482
+ return node;
483
+ }
484
+ if (isFunctionDeclaration(node)) {
485
+ return node.body === undefined ? undefined : node;
486
+ }
487
+ return isMethodDeclaration(node) &&
488
+ node.body !== undefined &&
489
+ isObjectLiteralExpression(node.parent)
490
+ ? node
491
+ : undefined;
492
+ }
493
+ /**
494
+ * Every name one function's parameters bind, a binding pattern's names one by one. A
495
+ * `this` parameter receives no argument and a decorated one is read by its decorator, so
496
+ * neither binds a subject.
497
+ */
498
+ function parameterNames(signature) {
499
+ const found = [];
500
+ const patternNames = (pattern, at) => {
501
+ if (isIdentifier(pattern)) {
502
+ found.push({ name: pattern, at, pattern: true, exempt: false });
503
+ return;
504
+ }
505
+ const rest = isObjectBindingPattern(pattern) &&
506
+ pattern.elements.some((element) => element.dotDotDotToken !== undefined);
507
+ for (const element of pattern.elements) {
508
+ if (!isBindingElement(element) || element.name === undefined) {
509
+ continue;
510
+ }
511
+ if (isIdentifier(element.name)) {
512
+ const exempt = rest && element.dotDotDotToken === undefined;
513
+ found.push({ name: element.name, at, pattern: true, exempt });
514
+ }
515
+ else {
516
+ patternNames(element.name, at);
517
+ }
518
+ }
519
+ };
520
+ signature.parameters.forEach((parameter, at) => {
521
+ const name = parameter.name;
522
+ if ((parameter.modifiers ?? []).some(isDecorator)) {
523
+ return;
524
+ }
525
+ if (isIdentifier(name)) {
526
+ if (name.text !== "this") {
527
+ found.push({ name, at, pattern: false, exempt: false });
528
+ }
529
+ return;
530
+ }
531
+ patternNames(name, at);
532
+ });
533
+ return found;
534
+ }
535
+ /**
536
+ * Each name one function's parameters bind and whether the function reads it. A binding
537
+ * pattern counts as read where its function reads any name it binds.
538
+ */
539
+ function parameterParts(file, root, declaration, signature, reader, ids, inValue) {
433
540
  // A function that reads `arguments` reads every parameter by position.
434
541
  const readsArguments = !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
542
+ const read = reader.names.map(({ name }) => readsArguments ||
543
+ reader.uses.some((use) => use.text === name.text && readsBinding(use, name, ids)));
544
+ const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
435
545
  const found = [];
436
- for (const parameter of signature.parameters) {
437
- const name = parameter.name;
438
- if (!isIdentifier(name) ||
439
- name.text === "this" ||
440
- name.text.startsWith(UNUSED_PREFIX) ||
441
- (parameter.modifiers ?? []).some(isDecorator)) {
442
- continue;
546
+ reader.names.forEach(({ name, at, pattern, exempt }, index) => {
547
+ if (exempt) {
548
+ return;
443
549
  }
444
550
  found.push({
445
551
  code: UNUSED_PARAMETER,
446
- declaration: one.id,
552
+ declaration,
447
553
  kind: "parameter",
448
554
  name: name.text,
449
555
  position: spanOf(file, root, name),
450
556
  message: `parameter ${name.text} is never read in the body`,
451
- used: readsArguments ||
452
- uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
557
+ used: read[index] === true,
558
+ parameter: { beforeARead: !pattern && at < lastRead, inValue },
453
559
  });
454
- }
560
+ });
455
561
  return found;
456
562
  }
457
563
  /** The locals one block declares with `let` or `const`, each with the names that spell it. */
@@ -655,6 +761,9 @@ export function intraFunctionFacts(projects, references, exempted) {
655
761
  const free = new Set(signatures
656
762
  .map((one) => one.declaration)
657
763
  .filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)));
764
+ const freeButValued = new Set(signatures
765
+ .map((one) => one.declaration)
766
+ .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)));
658
767
  const discardedEverywhere = new Set([...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id));
659
- return { parts: [...parts.values()], free, discardedEverywhere };
768
+ return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
660
769
  }
@@ -1,4 +1,4 @@
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";
1
+ import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/unstable/ast";
2
2
  import { dirnamePath, joinPath, relativePath } from "./paths.js";
3
3
  import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
4
4
  import { computedComponent, nameComponent, renderRef } from "./ref.js";
@@ -260,6 +260,7 @@ export function inventory(project, host, targetRoot) {
260
260
  const exportScopes = [];
261
261
  const containers = [];
262
262
  const starReExports = [];
263
+ const modules = [];
263
264
  let outsideOwnFiles = 0;
264
265
  const keep = (record) => {
265
266
  building.push(record);
@@ -450,6 +451,9 @@ export function inventory(project, host, targetRoot) {
450
451
  localName: undefined,
451
452
  });
452
453
  exportScopes.push({ owner: record, nameNode: node.name });
454
+ if (isStringLiteral(node.name)) {
455
+ modules.push([record, node.name.text]);
456
+ }
453
457
  const body = node.body;
454
458
  if (body === undefined) {
455
459
  return;
@@ -710,6 +714,7 @@ export function inventory(project, host, targetRoot) {
710
714
  .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
711
715
  .map((symbol) => symbol.id),
712
716
  byName: namesOf(building),
717
+ modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
713
718
  };
714
719
  }
715
720
  /** Whether one declaration is, or is declared under, a scope or container left unanswered. */
package/dist/src/kinds.js CHANGED
@@ -70,8 +70,8 @@ const ROWS = [
70
70
  code: "DS1101",
71
71
  name: "unnecessary-export",
72
72
  languages: ["go", "ts"],
73
- 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.",
74
- precondition: "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.",
73
+ 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. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it 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.",
74
+ precondition: "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world 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.",
75
75
  defaultSeverity: "warn",
76
76
  maxClass: "certain",
77
77
  fixability: "narrowable",
@@ -81,7 +81,7 @@ const ROWS = [
81
81
  code: "DS1102",
82
82
  name: "unnecessary-exposure",
83
83
  languages: ["go"],
84
- 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.",
84
+ 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. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it 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.",
85
85
  precondition: "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.",
86
86
  defaultSeverity: "warn",
87
87
  maxClass: "certain",
@@ -103,7 +103,7 @@ const ROWS = [
103
103
  name: "redundant-export-keyword",
104
104
  languages: ["ts"],
105
105
  rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
106
- precondition: "Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file; where the edge's other side is unknown to the analyzer the finding is emitted pending. 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
+ precondition: "Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. 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.",
107
107
  defaultSeverity: "warn",
108
108
  maxClass: "certain",
109
109
  fixability: "narrowable",
@@ -123,8 +123,8 @@ const ROWS = [
123
123
  code: "DS1203",
124
124
  name: "uncalled-interface-method",
125
125
  languages: ["go", "ts"],
126
- rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions.",
127
- precondition: "Exempt: in Go, every method of an interface that declares an unexported method, the sum-type shape whose method set exists to restrict the implementors, a shape a TypeScript interface cannot take because it declares no member less visible than itself; and, in both languages, every marker method, an interface method whose every implementation carries an empty body.",
126
+ rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions. Each method that implements it and that nothing else keeps is reported under the code its own references select. Such a method counts as referenced by the interface method alone, so it belongs to this finding's dead component, whose root member is the interface method.",
127
+ precondition: "In Go, every method of an interface that declares an unexported method is exempt. That is the sum-type shape, whose method set exists to restrict the implementors, and a TypeScript interface cannot take it because it declares no member less visible than itself. In both languages, every marker method is exempt, an interface method whose every implementation carries an empty body.",
128
128
  defaultSeverity: "warn",
129
129
  maxClass: "certain",
130
130
  fixability: "manual",
@@ -176,7 +176,7 @@ const ROWS = [
176
176
  name: "file-never-built",
177
177
  languages: ["go", "ts"],
178
178
  rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint is the ignore tag is never reported: the toolchain applies no build constraint to a file named on its own command line, so that tag is the toolchain's convention for a file built by hand. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
179
- precondition: 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled; such a file is recorded as excluded by cgo rather than as never built.',
179
+ precondition: 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
180
180
  defaultSeverity: "deny",
181
181
  maxClass: "certain",
182
182
  fixability: "deletable",
@@ -197,7 +197,7 @@ const ROWS = [
197
197
  name: "unused-dependency",
198
198
  languages: ["go", "ts"],
199
199
  rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
200
- precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and three more dependencies are needed: one whose installed manifest declares a command, because a command is run by name rather than imported; one that the installed manifest of a needed dependency declares as a peer and does not mark optional; and, to a fixpoint, the required peers of every dependency so held. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
200
+ precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
201
201
  defaultSeverity: "deny",
202
202
  maxClass: "certain",
203
203
  fixability: "manual",
@@ -208,7 +208,7 @@ const ROWS = [
208
208
  name: "unused-module-directive",
209
209
  languages: ["go"],
210
210
  rule: "A replace directive whose target module is absent from the build list, the one case in which the directive is exactly a no-op.",
211
- precondition: "Only a replace whose target is absent from the build list. No other module-file directive is reported: not an exclude directive, because not selected is a counterfactual about a resolution that did not happen; not a workspace use entry and not a tool directive, because each is an external entry point whose invocation lives outside anything the analysis reads.",
211
+ precondition: "Only a replace whose target is absent from the build list. No other module-file directive is reported. An exclude directive is not, because not selected is a counterfactual about a resolution that did not happen. A workspace use entry and a tool directive are not, because each is an external entry point whose invocation lives outside anything the analysis reads.",
212
212
  defaultSeverity: "warn",
213
213
  maxClass: "certain",
214
214
  fixability: "deletable",
@@ -228,7 +228,7 @@ const ROWS = [
228
228
  code: "DS1702",
229
229
  name: "unscoped-ignore-entry",
230
230
  languages: ["go", "ts"],
231
- rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project; the inline directive is scoped by its position and cannot be unscoped.",
231
+ rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project. The inline directive is scoped by its position and cannot be unscoped.",
232
232
  defaultSeverity: "deny",
233
233
  maxClass: "certain",
234
234
  fixability: "none",
@@ -279,7 +279,7 @@ const ROWS = [
279
279
  name: "unused-parameter",
280
280
  languages: ["go", "ts"],
281
281
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
282
- precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
282
+ precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported when its body reads no parameter after it, and an unread parameter before the last read one is not. A TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
283
283
  defaultSeverity: "warn",
284
284
  maxClass: "certain",
285
285
  fixability: "manual",
@@ -326,11 +326,12 @@ const ROWS = [
326
326
  name: "dead-store",
327
327
  languages: ["go", "ts"],
328
328
  rule: "A write to a local variable with no read before the next write to it or the end of its scope, computed on the same read-and-write classification the write-only-symbol kind uses. The finding names the write position.",
329
+ precondition: "In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same.",
329
330
  defaultSeverity: "deny",
330
331
  maxClass: "certain",
331
332
  fixability: "deletable",
332
333
  fixed: false,
333
- overlap: ["eslint no-useless-assignment"],
334
+ overlap: ["eslint no-useless-assignment", "@typescript-eslint/no-unused-vars"],
334
335
  },
335
336
  {
336
337
  code: "DS1809",
@@ -6,7 +6,7 @@
6
6
  import { componentsOf } from "./components.js";
7
7
  import { graphOf, OUTSIDE } from "./graph.js";
8
8
  import { byPosition } from "./position.js";
9
- import { sweep } from "./sweep.js";
9
+ import { reachedWithTheTests, sweep, } from "./sweep.js";
10
10
  /** Two strings ordered bytewise. */
11
11
  function compare(a, b) {
12
12
  if (a === b) {
@@ -103,37 +103,62 @@ export function sweepMatrix(matrix, input) {
103
103
  const dead = union.symbols.map(() => false);
104
104
  const testOfDeadCode = union.symbols.map(() => false);
105
105
  const heldBy = union.symbols.map(() => undefined);
106
- const candidates = [];
107
- union.symbols.forEach((symbol, at) => {
106
+ const unreferencedTest = union.symbols.map(() => false);
107
+ const testReached = reachedWithTheTests(union, [
108
+ ...input.marked,
109
+ ...(input.exempt ?? []).map((record) => record.id),
110
+ ...(input.unanswered ?? []),
111
+ ]);
112
+ const judged = union.symbols.map((symbol, at) => {
108
113
  const held = matrix.heldIn[at] ?? [];
109
114
  const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
110
- if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
115
+ return held.length === 0 || found.some((candidate) => candidate === undefined)
116
+ ? undefined
117
+ : found.filter((candidate) => candidate !== undefined);
118
+ });
119
+ // A configuration that does not hold a test's subject's caller finds the subject dead,
120
+ // so a test is of dead code only where every production subject it names is dead in
121
+ // every configuration that holds it.
122
+ const subjectsDead = (at) => (union.out[at] ?? []).every((edge) => union.test[edge.to] === true ||
123
+ union.support[edge.to] === true ||
124
+ union.subject[edge.to] !== true ||
125
+ judged[edge.to] !== undefined);
126
+ const candidates = [];
127
+ union.symbols.forEach((symbol, at) => {
128
+ const all = judged[at];
129
+ if (all === undefined) {
111
130
  return;
112
131
  }
113
- const all = found.filter((candidate) => candidate !== undefined);
114
- const admitted = all.every((candidate) => candidate.testOfDeadCode);
132
+ const held = matrix.heldIn[at] ?? [];
133
+ const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
115
134
  const testRefs = union.made[at]?.test ?? 0;
116
135
  const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
117
136
  (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
137
+ const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
118
138
  dead[at] = true;
119
139
  testOfDeadCode[at] = admitted;
120
140
  heldBy[at] = heldByTests;
141
+ unreferencedTest[at] = unreferenced;
142
+ const productionRefs = union.made[at]?.production ?? 0;
143
+ const counted = all.every((candidate) => candidate.relation === "reference-counting");
121
144
  candidates.push({
122
145
  id: symbol.id,
123
- productionRefs: union.made[at]?.production ?? 0,
146
+ productionRefs,
124
147
  testRefs,
125
- relation: all.every((candidate) => candidate.relation === "reference-counting")
148
+ // The run that judges a test-file declaration counts its test references.
149
+ relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
126
150
  ? "reference-counting"
127
151
  : "reachability",
128
152
  testOfDeadCode: admitted,
129
153
  configurations: held,
130
154
  heldByTests,
155
+ unreferencedTest: unreferenced,
131
156
  });
132
157
  });
133
158
  return {
134
159
  liveUnder: liveAnywhere(union.symbols, per),
135
160
  candidates,
136
- components: componentsOf(union, dead.map((one, at) => one && heldBy[at] === undefined), testOfDeadCode),
161
+ components: componentsOf(union, dead.map((one, at) => one && heldBy[at] === undefined), testOfDeadCode, unreferencedTest),
137
162
  };
138
163
  }
139
164
  /**