@cplieger/deadset-ts 3.12.0 → 3.13.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 (63) hide show
  1. package/README.md +2 -2
  2. package/dist/src/analysis.js +100 -27
  3. package/dist/src/components.js +4 -2
  4. package/dist/src/configured-declarations.js +36 -0
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/edges.js +4 -1
  7. package/dist/src/findings/completion.js +37 -8
  8. package/dist/src/findings/emitters.js +5 -5
  9. package/dist/src/findings/intra-function.js +91 -2
  10. package/dist/src/findings/self-check.js +30 -7
  11. package/dist/src/findings/unused-declarations.js +15 -3
  12. package/dist/src/findings-pass.js +18 -11
  13. package/dist/src/graph.js +7 -7
  14. package/dist/src/intra-function-parts.js +643 -0
  15. package/dist/src/kinds.js +337 -35
  16. package/dist/src/reflective-lookup.js +35 -2
  17. package/dist/src/report.js +11 -2
  18. package/dist/src/reporters/sarif.js +20 -15
  19. package/dist/src/reporters/template.js +64 -5
  20. package/dist/src/roots.js +13 -8
  21. package/dist/src/serialization-contract.js +41 -22
  22. package/dist/src/signatures.js +32 -0
  23. package/dist/src/suppress-file.js +27 -6
  24. package/dist/src/suppress-inline.js +8 -1
  25. package/dist/src/suppress.js +27 -0
  26. package/dist/src/value-reach.js +33 -9
  27. package/dist/src/verbs/analyze.js +19 -11
  28. package/dist/src/verbs/describe.js +12 -8
  29. package/dist/src/version.js +1 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/analysis.ts +167 -48
  33. package/src/components.ts +4 -2
  34. package/src/configured-declarations.ts +57 -1
  35. package/src/conformance-record.ts +2 -2
  36. package/src/edges.ts +4 -1
  37. package/src/finding.ts +5 -1
  38. package/src/findings/completion.ts +46 -10
  39. package/src/findings/emitter.ts +3 -0
  40. package/src/findings/emitters.ts +9 -5
  41. package/src/findings/intra-function.ts +106 -3
  42. package/src/findings/self-check.ts +48 -7
  43. package/src/findings/unused-declarations.ts +18 -4
  44. package/src/findings-pass.ts +26 -11
  45. package/src/graph.ts +7 -7
  46. package/src/intra-function-parts.ts +871 -0
  47. package/src/kinds.ts +355 -41
  48. package/src/reflective-lookup.ts +42 -2
  49. package/src/report.ts +18 -2
  50. package/src/reporters/sarif.ts +24 -16
  51. package/src/reporters/template.ts +69 -6
  52. package/src/roots.ts +14 -9
  53. package/src/serialization-contract.ts +47 -30
  54. package/src/signatures.ts +55 -0
  55. package/src/suppress-file.ts +34 -6
  56. package/src/suppress-inline.ts +8 -1
  57. package/src/suppress.ts +31 -0
  58. package/src/value-reach.ts +53 -12
  59. package/src/verbs/analyze.ts +26 -11
  60. package/src/verbs/describe.ts +13 -9
  61. package/src/version.ts +1 -1
  62. package/dist/src/reporters/rules.js +0 -178
  63. package/src/reporters/rules.ts +0 -195
@@ -1,4 +1,107 @@
1
- import type { Emitter } from "./emitter.ts";
1
+ import type { Finding } from "../finding.ts";
2
+ import { KINDS } from "../kinds.ts";
3
+ import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
4
+ import type { Emitter, EmitterInput } from "./emitter.ts";
2
5
 
3
- /** The findings of the intra-function family, `DS1800` to `DS1899`. */
4
- export const intraFunction: Emitter = () => [];
6
+ /** A list of rules as a sentence names them. */
7
+ function spelled(rules: readonly string[]): string {
8
+ if (rules.length < 2) {
9
+ return rules.join("");
10
+ }
11
+ return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
12
+ }
13
+
14
+ /** The clause a message ends with: the other rules that report the kind, or that none is known. */
15
+ function overlapClause(overlap: readonly string[]): string {
16
+ return overlap.length === 1 && overlap[0] === "none known"
17
+ ? "no other rule is known to report it"
18
+ : `also reported by ${spelled(overlap)}`;
19
+ }
20
+
21
+ /**
22
+ * Whether one part is reported. A parameter needs its function's signature free to
23
+ * change; a result needs that, every call in the loaded program to discard it, and every
24
+ * call site to be in the loaded program, so a function with a root or one a module
25
+ * exports to callers the run cannot see has no result to report. Every other part is
26
+ * reported wherever no project found it in use.
27
+ */
28
+ function reported(
29
+ input: EmitterInput,
30
+ part: PartFact,
31
+ callersUnknown: ReadonlySet<string>,
32
+ ): boolean {
33
+ if (part.used) {
34
+ return false;
35
+ }
36
+ const { free, discardedEverywhere } = input.intraFunction;
37
+ switch (part.code) {
38
+ case UNUSED_PARAMETER:
39
+ return free.has(part.declaration);
40
+ case UNUSED_RESULT:
41
+ return (
42
+ free.has(part.declaration) &&
43
+ discardedEverywhere.has(part.declaration) &&
44
+ !callersUnknown.has(part.declaration)
45
+ );
46
+ default:
47
+ return true;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
53
+ * the declaration its reference names, at the part's own position, whatever the sweep
54
+ * decided about that declaration. Each message names the other rules that report the
55
+ * kind, as the vocabulary lists them, and so does the finding's details.
56
+ */
57
+ export const intraFunction: Emitter = (input) => {
58
+ const union = input.swept.matrix.union;
59
+ const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
60
+ const unknown = new Set(
61
+ union.rooted.flatMap((root) => {
62
+ const symbol = union.symbols[root.at];
63
+ return symbol === undefined ? [] : [symbol.id];
64
+ }),
65
+ );
66
+ const { declared, loaded } = input.boundary.consumers;
67
+ const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
68
+ const published = (id: string): boolean => {
69
+ const symbol = byId.get(id);
70
+ return (
71
+ symbol !== undefined &&
72
+ symbol.kind !== "file" &&
73
+ (symbol.exported || published(symbol.parent))
74
+ );
75
+ };
76
+ if (!closed) {
77
+ for (const part of input.intraFunction.parts) {
78
+ if (published(part.declaration)) {
79
+ unknown.add(part.declaration);
80
+ }
81
+ }
82
+ }
83
+ const found: Finding[] = [];
84
+ for (const part of input.intraFunction.parts) {
85
+ const declaration = byId.get(part.declaration);
86
+ const overlap = KINDS.get(part.code)?.overlap ?? [];
87
+ if (declaration === undefined || !reported(input, part, unknown)) {
88
+ continue;
89
+ }
90
+ found.push({
91
+ code: part.code,
92
+ position: part.position,
93
+ symbol: {
94
+ ref: declaration.ref,
95
+ kind: part.kind,
96
+ name: part.name,
97
+ sizeLines: part.position.endLine - part.position.line + 1,
98
+ },
99
+ message: `${part.message}; ${overlapClause(overlap)}`,
100
+ details: {
101
+ ...(part.kind === "store" ? { writePositions: [part.position] } : {}),
102
+ overlap,
103
+ },
104
+ });
105
+ }
106
+ return found;
107
+ };
@@ -14,6 +14,12 @@ import type { Emitter } from "./emitter.ts";
14
14
  /** The code a configured root that matches nothing is reported under. */
15
15
  const UNMATCHED_ROOT = "DS1704";
16
16
 
17
+ /** The code a configured declaration that names nothing is reported under. */
18
+ const UNMATCHED_DECLARATION = "DS1706";
19
+
20
+ /** The subject kind of a finding about a configured declaration. */
21
+ const DECLARATION_SUBJECT = "configured-declaration";
22
+
17
23
  /** The subject kind of a finding about a suppression record. */
18
24
  const SUPPRESSION_SUBJECT = "suppression";
19
25
 
@@ -38,6 +44,18 @@ export interface SelfCheckFacts {
38
44
  readonly unmatchedRoots: readonly string[];
39
45
  /** The document below the target root that declared the roots, empty where none did. */
40
46
  readonly rootsDocument: string;
47
+ /** Every configured declaration that named no declaration in any project, in reading order. */
48
+ readonly unmatchedDeclarations: readonly UnmatchedDeclaration[];
49
+ }
50
+
51
+ /** One configured declaration that named nothing, and the document that configured it. */
52
+ interface UnmatchedDeclaration {
53
+ /** The entry as a finding's subject reference spells it. */
54
+ readonly ref: string;
55
+ /** The dotted path of the key that holds the entry. */
56
+ readonly key: string;
57
+ /** The document below the target root that declared the key, empty where none did. */
58
+ readonly document: string;
41
59
  }
42
60
 
43
61
  /** A run with nothing for the self-check family to report. */
@@ -45,6 +63,7 @@ export const NO_SELF_CHECK: SelfCheckFacts = {
45
63
  refusals: [],
46
64
  unmatchedRoots: [],
47
65
  rootsDocument: "",
66
+ unmatchedDeclarations: [],
48
67
  };
49
68
 
50
69
  /** The words a message names a mechanism by. */
@@ -188,6 +207,31 @@ export function staleSuppressions(
188
207
  });
189
208
  }
190
209
 
210
+ /** The first position of the document that configured a setting, or of the conventional one. */
211
+ function documentPosition(document: string): Finding["position"] {
212
+ return {
213
+ path: document === "" ? REPOSITORY_DOCUMENT : document,
214
+ line: DOCUMENT_POSITION,
215
+ column: DOCUMENT_POSITION,
216
+ endLine: DOCUMENT_POSITION,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * One finding per configured declaration that named no declaration in any project, at the
222
+ * first position of the document that configured its key, as a configured root's is.
223
+ */
224
+ function unmatchedDeclarationFindings(
225
+ unmatched: readonly UnmatchedDeclaration[],
226
+ ): readonly Finding[] {
227
+ return unmatched.map((one) => ({
228
+ code: UNMATCHED_DECLARATION,
229
+ position: documentPosition(one.document),
230
+ symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
231
+ message: `${one.key} entry names no declaration in any project`,
232
+ }));
233
+ }
234
+
191
235
  /**
192
236
  * One finding per configured root or pattern that named nothing in any project. A finding
193
237
  * names the document that declared the roots, or the conventional repository configuration
@@ -199,12 +243,7 @@ export function unmatchedRootFindings(
199
243
  sources: readonly string[],
200
244
  document: string,
201
245
  ): readonly Finding[] {
202
- const position = {
203
- path: document === "" ? REPOSITORY_DOCUMENT : document,
204
- line: DOCUMENT_POSITION,
205
- column: DOCUMENT_POSITION,
206
- endLine: DOCUMENT_POSITION,
207
- };
246
+ const position = documentPosition(document);
208
247
  return sources.map((source) => ({
209
248
  code: UNMATCHED_ROOT,
210
249
  position,
@@ -218,11 +257,13 @@ export function unmatchedRootFindings(
218
257
 
219
258
  /**
220
259
  * The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
221
- * carrying no reason or naming no path, and a configured root that named nothing. A
260
+ * carrying no reason or naming no path, and a configured root or declaration that named
261
+ * nothing. A
222
262
  * suppression that matched nothing is a record of the report's stale suppressions, which
223
263
  * {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
224
264
  */
225
265
  export const selfCheck: Emitter = ({ selfCheck: facts, swept }) => [
226
266
  ...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
227
267
  ...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
268
+ ...unmatchedDeclarationFindings(facts.unmatchedDeclarations),
228
269
  ];
@@ -1,7 +1,7 @@
1
1
  import type { Finding } from "../finding.ts";
2
2
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
3
3
  import type { MatrixCandidate } from "../matrix.ts";
4
- import type { Emitter } from "./emitter.ts";
4
+ import type { Emitter, EmitterInput } from "./emitter.ts";
5
5
  import { unreachableExports } from "./visibility-narrowing.ts";
6
6
 
7
7
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
@@ -137,7 +137,17 @@ function messageOf(code: string, judged: Judged): string {
137
137
  }
138
138
 
139
139
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
140
- export const unusedDeclarations: Emitter = (input) => {
140
+ export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
141
+
142
+ /**
143
+ * The finding each member of a dead container would carry were its container live: what
144
+ * a record on such a member is matched against, since the report carries the container's
145
+ * finding rather than one of the member's own.
146
+ */
147
+ export const fallenMembers: Emitter = (input) => familyFindings(input, true);
148
+
149
+ /** The family's findings, or, where `fallen` is set, its dead containers' members' alone. */
150
+ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
141
151
  const union = input.swept.matrix.union;
142
152
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
143
153
  const unreachable = unreachableExports(input);
@@ -149,11 +159,15 @@ export const unusedDeclarations: Emitter = (input) => {
149
159
  if (symbol === undefined) {
150
160
  continue;
151
161
  }
162
+ const containerDead = dead.has(symbol.parent);
163
+ if (fallen !== containerDead) {
164
+ continue;
165
+ }
152
166
  const judged: Judged = {
153
167
  candidate,
154
168
  symbol,
155
169
  test: union.test[at] === true,
156
- containerDead: dead.has(symbol.parent),
170
+ containerDead: containerDead && !fallen,
157
171
  deprecated: input.deprecated.has(symbol.id),
158
172
  unreachable: unreachable.has(symbol.id),
159
173
  };
@@ -175,4 +189,4 @@ export const unusedDeclarations: Emitter = (input) => {
175
189
  });
176
190
  }
177
191
  return found;
178
- };
192
+ }
@@ -122,19 +122,34 @@ export function verdictOf(result: PassResult, config: Config): number {
122
122
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
123
123
  }
124
124
 
125
+ /** What a baseline row is recorded from: a finding's code, subject and file. */
126
+ export interface RecordableFinding {
127
+ readonly code: string;
128
+ readonly symbol: { readonly ref: string; readonly kind: string };
129
+ readonly position: { readonly path: string };
130
+ }
131
+
125
132
  /**
126
- * The findings of a report as a baseline records them, in report order. A finding about a
127
- * row of a document is not recorded: no record withholds one, so a row recording it would
128
- * be stale on the first read back.
133
+ * The rows one round of a baseline write records from its findings, in their order. A
134
+ * finding about a row of a document is not recorded: no record withholds one, so a row
135
+ * recording it would be stale on the first read back. Two findings that would record the
136
+ * same row, two parts of one declaration under one code, record it once.
129
137
  */
130
- export function recordedFindings(result: PassResult): readonly Recorded[] {
131
- return result.findings
132
- .filter((finding) => !isRowSubject(finding.symbol.kind))
133
- .map((finding) => ({
134
- code: finding.code,
135
- symbol: finding.symbol.ref,
136
- path: finding.position.path,
137
- }));
138
+ export function recordedFindings(findings: readonly RecordableFinding[]): readonly Recorded[] {
139
+ const rows = new Map<string, Recorded>();
140
+ for (const finding of findings) {
141
+ if (isRowSubject(finding.symbol.kind)) {
142
+ continue;
143
+ }
144
+ const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
145
+ rows.set(rowKey(row), rows.get(rowKey(row)) ?? row);
146
+ }
147
+ return [...rows.values()];
148
+ }
149
+
150
+ /** One row's identity: its code, its symbol and its path. */
151
+ export function rowKey(row: Recorded): string {
152
+ return `${row.code}\u0000${row.symbol}\u0000${row.path}`;
138
153
  }
139
154
 
140
155
  /** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
package/src/graph.ts CHANGED
@@ -39,15 +39,15 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
39
39
 
40
40
  /**
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
- * tool reads the configuration it loads, a runtime runs a test file, a setup file and a
43
- * worker, and a configured root asserts a caller. The other kinds only suppose a consumer
44
- * that imports what a manifest entry, a binary or an entry file exports, or names a
45
- * library's published API, so such a root is live under reachability alone.
42
+ * tool reads the configuration it loads, a runtime runs a test file, a setup file, a
43
+ * worker and a command, the manifest names what enters the package, and an entry file, a
44
+ * configured root and a pattern assert a caller. A library's published API only supposes
45
+ * a consumer the run may not see, so it is live under reachability alone.
46
46
  */
47
47
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
48
- "entry-file": false,
49
- "manifest-entry": false,
50
- "manifest-binary": false,
48
+ "entry-file": true,
49
+ "manifest-entry": true,
50
+ "manifest-binary": true,
51
51
  "published-api": false,
52
52
  configured: true,
53
53
  pattern: true,