@cplieger/deadset-ts 5.2.3 → 6.0.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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
@@ -50,9 +50,11 @@ import {
50
50
  CONVENTION_ROWS,
51
51
  type ConventionRow,
52
52
  type DirectoryMove,
53
+ type MoveReading,
53
54
  type OptionsCall,
54
55
  type ShortNameReading,
55
56
  } from "./convention-rows.ts";
57
+ import { documentStrings } from "./configuration-files.ts";
56
58
  import { globExpression } from "./glob.ts";
57
59
  import type { Host } from "./host.ts";
58
60
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
@@ -108,8 +110,18 @@ export interface Conventions {
108
110
  readonly globs: readonly ConventionGlob[];
109
111
  readonly generated: readonly GeneratedDirectory[];
110
112
  readonly failures: readonly ConventionFailure[];
111
- /** The dependencies the applied rows' short names use, by name, each once. */
113
+ /** The dependencies the applied rows' short names and manifest keys use, by name, each once. */
112
114
  readonly uses: readonly string[];
115
+ /** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
116
+ readonly selected: ReadonlyMap<string, Selected>;
117
+ }
118
+
119
+ /** The strings one configuration file holds at selection keys, which root no file. */
120
+ export interface Selected {
121
+ /** Where each string starts in the file's text. */
122
+ readonly starts: ReadonlySet<number>;
123
+ /** Each string's text, once per occurrence. */
124
+ readonly texts: readonly string[];
113
125
  }
114
126
 
115
127
  /** One file a row roots. */
@@ -520,6 +532,86 @@ function readPath(
520
532
  return undefined;
521
533
  }
522
534
 
535
+ /**
536
+ * Every string literal one file writes at one property's path inside the objects it is
537
+ * read from, as {@link readProperty} reads them: the value itself, or each element of an
538
+ * array value, whatever else the path holds.
539
+ */
540
+ function stringsAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
541
+ const declared = declaredValues(file);
542
+ const objects =
543
+ call === undefined
544
+ ? defaultExports(file).map((one) => objectOf(one, declared, true))
545
+ : optionsArguments(file, call).map((one) =>
546
+ one === undefined ? undefined : objectOf(one, declared, false),
547
+ );
548
+ const found: Node[] = [];
549
+ const visit = (object: Node, keys: readonly string[]): void => {
550
+ if (!isObjectLiteralExpression(object)) {
551
+ return;
552
+ }
553
+ const [key, ...rest] = keys;
554
+ for (const member of object.properties) {
555
+ if (!isPropertyAssignment(member) || keyOf(member) !== key) {
556
+ continue;
557
+ }
558
+ const held = unwrapped(member.initializer);
559
+ if (rest.length > 0) {
560
+ visit(held, rest);
561
+ } else if (literalText(held) !== undefined) {
562
+ found.push(held);
563
+ } else if (isArrayLiteralExpression(held)) {
564
+ found.push(...held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined));
565
+ }
566
+ }
567
+ };
568
+ for (const object of objects) {
569
+ if (object !== undefined) {
570
+ visit(object, property.split("."));
571
+ }
572
+ }
573
+ return found;
574
+ }
575
+
576
+ /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
577
+ function selectedStrings(
578
+ host: Host,
579
+ parse: ParseSource,
580
+ dir: string,
581
+ readings: readonly MoveReading[],
582
+ into: Map<string, { starts: Set<number>; texts: string[] }>,
583
+ ): void {
584
+ for (const reading of readings) {
585
+ for (const { name, file } of readingFiles(host, parse, dir, reading.files)) {
586
+ const path = joinPath(dir, name);
587
+ const held = into.get(path) ?? { starts: new Set<number>(), texts: [] };
588
+ for (const node of stringsAt(file, reading.property, reading.call)) {
589
+ held.starts.add(node.getStart());
590
+ held.texts.push(literalText(node) ?? "");
591
+ }
592
+ into.set(path, held);
593
+ }
594
+ }
595
+ }
596
+
597
+ /**
598
+ * The dependencies one manifest declares that a string at any depth of one of its members
599
+ * names, alone or followed by `/` and a subpath, a member's name aside.
600
+ */
601
+ function manifestKeyUses(manifest: unknown, keys: readonly string[]): readonly string[] {
602
+ if (!isRecord(manifest)) {
603
+ return [];
604
+ }
605
+ const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
606
+ const held = manifest[section];
607
+ return isRecord(held) ? Object.keys(held) : [];
608
+ });
609
+ const strings = keys.flatMap((key) => documentStrings(manifest[key]));
610
+ return declared.filter((name) =>
611
+ strings.some((text) => text === name || text.startsWith(`${name}/`)),
612
+ );
613
+ }
614
+
523
615
  /** What one configuration file states about one property. */
524
616
  interface PropertyReading {
525
617
  /** The literal values the file sets the property to. */
@@ -740,6 +832,7 @@ export function readConventions(
740
832
  const generated: GeneratedDirectory[] = [];
741
833
  const failures: ConventionFailure[] = [];
742
834
  const uses = new Set<string>();
835
+ const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
743
836
  for (const manifest of manifests) {
744
837
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
745
838
  const versions = new Map<string, string | undefined>();
@@ -801,6 +894,10 @@ export function readConventions(
801
894
  for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
802
895
  uses.add(name);
803
896
  }
897
+ for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
898
+ uses.add(name);
899
+ }
900
+ selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
804
901
  const excludes = [
805
902
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
806
903
  ].map(globExpression);
@@ -824,7 +921,14 @@ export function readConventions(
824
921
  compare(a.manifest, b.manifest) ||
825
922
  compare(JSON.stringify(a), JSON.stringify(b)),
826
923
  );
827
- return { applied: unique, globs, generated, failures, uses: [...uses].sort(compare) };
924
+ return {
925
+ applied: unique,
926
+ globs,
927
+ generated,
928
+ failures,
929
+ uses: [...uses].sort(compare),
930
+ selected,
931
+ };
828
932
  }
829
933
 
830
934
  /**
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The dependencies the target's manifest declares, and what the projects' own files
3
- * need from outside the target. A file needs a package it imports, re-exports from,
4
- * augments or references the types of, by name or through the file the specifier
5
- * resolves to; a compiler configuration needs one it names, and a configuration file a
6
- * tool loads needs each dependency its strings spell. A package one dependency pulls in
7
- * for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and
3
+ * what the projects' own files need from outside the target. A file needs a package it
4
+ * imports, re-exports from, augments or references the types of, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a
6
+ * configuration file a tool loads needs each dependency its strings spell. A package one
7
+ * dependency pulls in for itself is that dependency's need, not the target's.
8
8
  */
9
9
 
10
10
  import {
@@ -32,7 +32,7 @@ import {
32
32
  import type { FindingPosition } from "./finding.ts";
33
33
  import type { Host } from "./host.ts";
34
34
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
35
- import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
35
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
36
36
  import type { DependencySection, Module } from "./ref.ts";
37
37
  import { UNANSWERED } from "./query.ts";
38
38
  import type { Handle, ProjectView } from "./session.ts";
@@ -53,12 +53,14 @@ const SECTIONS: readonly (readonly [string, DependencySection])[] = [
53
53
  /** The owner of a use no declaration of the inventory holds: a configuration's. */
54
54
  const PROJECT_OWNER = "";
55
55
 
56
- /** One dependency the target's manifest declares. */
56
+ /** One dependency a manifest the run reads declares. */
57
57
  export interface DeclaredDependency {
58
58
  readonly name: string;
59
59
  readonly section: DependencySection;
60
60
  /** Where the manifest writes the dependency's key, below the target root. */
61
61
  readonly position: FindingPosition;
62
+ /** The scope the dependency's reference is written in: its manifest's. */
63
+ readonly manifest: Module;
62
64
  }
63
65
 
64
66
  /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
@@ -97,9 +99,7 @@ export interface ProjectNeeds {
97
99
 
98
100
  /** The run's dependency evidence. */
99
101
  export interface Dependencies {
100
- /** The scope every dependency's reference is written in: the target's manifest. */
101
- readonly manifest: Module;
102
- /** Every declared dependency, in the order the manifest writes them. */
102
+ /** Every declared dependency, manifest by manifest, in the order each writes them. */
103
103
  readonly declared: readonly DeclaredDependency[];
104
104
  /** The installed copy of each declared dependency the run found, by name. */
105
105
  readonly installed: ReadonlyMap<string, InstalledDependency>;
@@ -608,8 +608,13 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
608
608
  }
609
609
  }
610
610
 
611
- /** Every dependency the manifest text declares, in the order the text writes them. */
612
- function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDependency[] {
611
+ /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
612
+ function declaredIn(
613
+ text: string,
614
+ manifest: Record<string, unknown>,
615
+ path: string,
616
+ scope: Module,
617
+ ): DeclaredDependency[] {
613
618
  const sections = membersAt(text, skipWhitespace(text, 0));
614
619
  const declared: { readonly dependency: DeclaredDependency; readonly at: number }[] = [];
615
620
  for (const [member, section] of SECTIONS) {
@@ -621,7 +626,12 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
621
626
  for (const [name, { key }] of membersAt(text, held.value)) {
622
627
  const { line, column } = lineAndColumn(text, key);
623
628
  declared.push({
624
- dependency: { name, section, position: { path: MANIFEST, line, column, endLine: line } },
629
+ dependency: {
630
+ name,
631
+ section,
632
+ position: { path, line, column, endLine: line },
633
+ manifest: scope,
634
+ },
625
635
  at: key,
626
636
  });
627
637
  }
@@ -630,11 +640,10 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
630
640
  }
631
641
 
632
642
  /**
633
- * The run's dependency evidence: the target's manifest read with the position of every
634
- * dependency it declares, the installed copy of each, what the projects need, and the
635
- * last uses each deletion candidate holds.
636
- *
637
- * Only the manifest at the target root is read, as for entry points. A manifest the
643
+ * The run's dependency evidence: the target's manifest and each workspace member's in
644
+ * `members` below the target root, read with the position of every dependency each
645
+ * declares, the installed copy of each found from its manifest's directory, what the
646
+ * projects need, and the last uses each deletion candidate holds. A manifest the
638
647
  * program cannot read declares nothing.
639
648
  */
640
649
  export function dependenciesOf(
@@ -642,20 +651,31 @@ export function dependenciesOf(
642
651
  targetRoot: string,
643
652
  perProject: readonly ProjectNeeds[],
644
653
  candidates: readonly string[],
654
+ members: readonly string[] = [],
645
655
  ): Dependencies {
646
- const read = readJSON(host, joinPath(targetRoot, MANIFEST));
647
- const manifest = isRecord(read?.value) ? read.value : undefined;
648
- const scope = packageScope(host, targetRoot)(MANIFEST);
649
- const declared =
650
- read !== undefined && manifest !== undefined ? declaredIn(read.text, manifest) : [];
651
-
652
656
  const root = resolvePath(host.workingDirectory(), targetRoot);
657
+ const scopeOf = packageScope(host, targetRoot);
658
+ const manifests = new Map<string, string>([[root, MANIFEST]]);
659
+ for (const dir of members) {
660
+ const below = relativePath(root, dir);
661
+ if (below !== undefined && !manifests.has(dir)) {
662
+ manifests.set(dir, `${below}/${MANIFEST}`);
663
+ }
664
+ }
665
+ const declared: DeclaredDependency[] = [];
653
666
  const installed = new Map<string, InstalledDependency>();
654
- for (const dependency of declared) {
655
- if (!installed.has(dependency.name)) {
656
- const found = installedOf(host, root, dependency.name);
657
- if (found !== undefined) {
658
- installed.set(dependency.name, found);
667
+ for (const [dir, path] of manifests) {
668
+ const read = readJSON(host, joinPath(dir, MANIFEST));
669
+ if (read === undefined || !isRecord(read.value)) {
670
+ continue;
671
+ }
672
+ for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
673
+ declared.push(dependency);
674
+ if (!installed.has(dependency.name)) {
675
+ const found = installedOf(host, dir, dependency.name);
676
+ if (found !== undefined) {
677
+ installed.set(dependency.name, found);
678
+ }
659
679
  }
660
680
  }
661
681
  }
@@ -691,5 +711,5 @@ export function dependenciesOf(
691
711
  );
692
712
  }
693
713
  }
694
- return { manifest: scope, declared, installed, needed, lastUses };
714
+ return { declared, installed, needed, lastUses };
695
715
  }
@@ -36,6 +36,7 @@ import {
36
36
  moduleStrings,
37
37
  type ParseFile,
38
38
  } from "./configuration-files.ts";
39
+ import type { Selected } from "./conventions.ts";
39
40
  import { globExpression } from "./glob.ts";
40
41
  import type { Host } from "./host.ts";
41
42
  import { htmlEntries } from "./html-entries.ts";
@@ -43,6 +44,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
43
44
  import { UNANSWERED } from "./query.ts";
44
45
  import type { ProjectView } from "./session.ts";
45
46
  import type { SourceFiles } from "./source-files.ts";
47
+ import { workflowEntries } from "./workflow-steps.ts";
46
48
 
47
49
  /** Which rule entered a file. */
48
50
  export type EntryRule =
@@ -52,14 +54,14 @@ export type EntryRule =
52
54
  | "configuration-string"
53
55
  /** The test runner: its workspace configuration, its setup files and the test files. */
54
56
  | "test-runner"
55
- /** The mutation tester's configuration under a name the configuration form does not cover. */
56
- | "mutation-testing"
57
57
  /** The test files below the browser-test runner's test directory. */
58
58
  | "browser-tests"
59
59
  /** A worker or service worker a call addresses by a string literal. */
60
60
  | "worker"
61
61
  /** A file a module script of an HTML file below the target root loads. */
62
- | "html-entry";
62
+ | "html-entry"
63
+ /** A file a `run:` step of a workflow in the target root's `.github/workflows` names. */
64
+ | "workflow-step";
63
65
 
64
66
  /** One file a rule enters. */
65
67
  export interface EntryPoint {
@@ -90,9 +92,6 @@ interface Convention {
90
92
  /** The extensions the test runner and the browser-test runner load a configuration from. */
91
93
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"] as const;
92
94
 
93
- /** The extensions the mutation tester's configuration is loaded from. */
94
- const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"] as const;
95
-
96
95
  /** The tool file names the configuration form does not cover. */
97
96
  const CONVENTIONS: readonly Convention[] = [
98
97
  {
@@ -101,12 +100,6 @@ const CONVENTIONS: readonly Convention[] = [
101
100
  extensions: MODULE_EXTENSIONS,
102
101
  testRunner: true,
103
102
  },
104
- {
105
- rule: "mutation-testing",
106
- stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
107
- extensions: SCRIPT_EXTENSIONS,
108
- testRunner: false,
109
- },
110
103
  ];
111
104
 
112
105
  /** The stem of the browser-test runner's configuration, whose test directory is read. */
@@ -262,16 +255,35 @@ function isBrowserTestConfig(name: string): boolean {
262
255
  /**
263
256
  * The files the configuration files of one project enter: each configuration module,
264
257
  * with what it exports, and each own file a relative string of a configuration module
265
- * or a JSON configuration file names, read against that file's directory.
258
+ * or a JSON configuration file names, read against that file's directory. A string at a
259
+ * selection key of an applied convention row names no file.
266
260
  */
267
261
  function configurationEntries(
268
262
  host: Host,
269
263
  targetRoot: string,
270
264
  files: SourceFiles,
271
265
  parse: ParseFile | undefined,
266
+ selected: ReadonlyMap<string, Selected>,
272
267
  ): readonly EntryPoint[] {
273
268
  const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
274
269
  const found: EntryPoint[] = [];
270
+ const unselected = (file: SourceFile): readonly string[] => {
271
+ const starts = selected.get(file.fileName)?.starts;
272
+ return moduleStrings(file)
273
+ .filter((one) => starts?.has(one.node.getStart()) !== true)
274
+ .map((one) => one.text);
275
+ };
276
+ const unselectedIn = (path: string, texts: readonly string[]): readonly string[] => {
277
+ const left = [...(selected.get(path)?.texts ?? [])];
278
+ return texts.filter((text) => {
279
+ const at = left.indexOf(text);
280
+ if (at < 0) {
281
+ return true;
282
+ }
283
+ left.splice(at, 1);
284
+ return false;
285
+ });
286
+ };
275
287
  const strings = (dir: string, texts: readonly string[]): void => {
276
288
  for (const text of texts) {
277
289
  for (const file of filesNamedBy(files, dir, text)) {
@@ -281,13 +293,10 @@ function configurationEntries(
281
293
  };
282
294
  for (const { file, form } of modules) {
283
295
  found.push({ file, rule: "configuration-file", source: form, exports: true });
284
- strings(
285
- dirnamePath(file.fileName),
286
- moduleStrings(file).map((one) => one.text),
287
- );
296
+ strings(dirnamePath(file.fileName), unselected(file));
288
297
  }
289
298
  for (const document of documents) {
290
- strings(dirnamePath(document.path), document.strings);
299
+ strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
291
300
  }
292
301
  for (const { file } of outside) {
293
302
  strings(
@@ -523,8 +532,9 @@ export function entryPoints<Brand>(
523
532
  host: Host,
524
533
  targetRoot: string,
525
534
  parse?: ParseFile,
535
+ selected: ReadonlyMap<string, Selected> = new Map(),
526
536
  ): readonly EntryPoint[] {
527
- const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
537
+ const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse, selected)];
528
538
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
529
539
  for (const [path, file] of files.byPath) {
530
540
  const name = basename(path);
@@ -548,5 +558,8 @@ export function entryPoints<Brand>(
548
558
  for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
549
559
  found.push({ file, rule: "html-entry", source, exports: true });
550
560
  }
561
+ for (const { file, source } of workflowEntries(host, targetRoot, files)) {
562
+ found.push({ file, rule: "workflow-step", source, exports: true });
563
+ }
551
564
  return found;
552
565
  }
package/src/exempt.ts CHANGED
@@ -24,7 +24,7 @@ import { byPosition, positionKey, type Position } from "./position.ts";
24
24
  import { Unanswerable } from "./query.ts";
25
25
  import type { ProjectView } from "./session.ts";
26
26
  import type { Templates } from "./template-field.ts";
27
- import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
27
+ import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
28
28
 
29
29
  /** One declaration a detector holds back, and where and why. */
30
30
  export interface Evidence {
@@ -35,6 +35,8 @@ export interface Evidence {
35
35
  readonly site: Position;
36
36
  /** The component file, below the target root, whose markup holds the evidence. */
37
37
  readonly whileLive?: string;
38
+ /** Where the evidence is an implementation of interface methods, which and how. */
39
+ readonly implementing?: Implementing;
38
40
  }
39
41
 
40
42
  /** What a detector reads of one project, while the project's view is open. */
@@ -214,6 +216,7 @@ export function evidenceRecords<Brand>(
214
216
  detail: evidence.detail,
215
217
  site: evidence.site,
216
218
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
219
+ ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
217
220
  });
218
221
  }
219
222
  }
@@ -10,7 +10,8 @@ import type { Cascade, Confidence, Severity } from "../config.ts";
10
10
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
11
  import { OUTSIDE, type Graph } from "../graph.ts";
12
12
  import { KINDS, type KindRow } from "../kinds.ts";
13
- import { byPosition } from "../position.ts";
13
+ import { byPosition, positionKey } from "../position.ts";
14
+ import { declarationKey } from "../ref.ts";
14
15
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
15
16
  import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
16
17
  import type { Boundary } from "./boundary.ts";
@@ -149,7 +150,9 @@ export function completed(
149
150
  ): readonly CompletedFinding[] {
150
151
  const { swept } = input;
151
152
  const union = swept.matrix.union;
152
- const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
153
+ const positions = new Map(
154
+ union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]),
155
+ );
153
156
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
154
157
  const classOf = reachabilityClasses(input);
155
158
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
@@ -168,10 +171,14 @@ export function completed(
168
171
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
169
172
  }
170
173
  // A row's reference may spell a declaration's, as an ignore entry naming one does, and
171
- // a row is still never the declaration.
174
+ // a row is still never the declaration. Two declarations of one file can share a
175
+ // reference, so the one the finding is positioned at is the subject where there is one.
176
+ const declared = union.at(positionKey(finding.position));
172
177
  const at = isRowSubject(finding.symbol.kind)
173
178
  ? OUTSIDE
174
- : (positions.get(finding.symbol.ref) ?? OUTSIDE);
179
+ : union.symbols[declared]?.ref === finding.symbol.ref
180
+ ? declared
181
+ : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
175
182
  // A part names the declaration that holds it, and is decided inside it rather than by
176
183
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
177
184
  const part = isPartSubject(finding.symbol.kind);
@@ -37,7 +37,7 @@ function neededBy(dependencies: Dependencies): ReadonlySet<string> {
37
37
  }
38
38
 
39
39
  /**
40
- * A dependency, development dependency or peer dependency of the target's manifest
40
+ * A dependency, development dependency or peer dependency of a manifest the run reads
41
41
  * that the target does not need, at the position of its key.
42
42
  */
43
43
  function unusedDependencies(dependencies: Dependencies): Finding[] {
@@ -48,7 +48,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
48
48
  code: UNUSED_DEPENDENCY,
49
49
  position: dependency.position,
50
50
  symbol: {
51
- ref: renderRef(dependencies.manifest, {
51
+ ref: renderRef(dependency.manifest, {
52
52
  of: "dependency",
53
53
  name: dependency.name,
54
54
  section: dependency.section,