@cplieger/deadset-ts 3.4.0 → 3.6.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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +70 -14
  3. package/dist/src/components.js +66 -107
  4. package/dist/src/config.js +6 -0
  5. package/dist/src/dependencies.js +493 -0
  6. package/dist/src/deprecation.js +83 -0
  7. package/dist/src/discover.js +1 -1
  8. package/dist/src/enum-group.js +217 -0
  9. package/dist/src/file-facts.js +101 -0
  10. package/dist/src/findings/boundary.js +1 -0
  11. package/dist/src/findings/completion.js +99 -0
  12. package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
  13. package/dist/src/findings/emitters.js +8 -0
  14. package/dist/src/findings/interfaces.js +95 -2
  15. package/dist/src/findings/non-code-artifacts.js +97 -1
  16. package/dist/src/findings/reads-and-writes.js +199 -2
  17. package/dist/src/findings/unused-declarations.js +182 -1
  18. package/dist/src/findings/visibility-narrowing.js +239 -2
  19. package/dist/src/implementations.js +210 -0
  20. package/dist/src/inventory.js +2 -2
  21. package/dist/src/kinds.js +40 -0
  22. package/dist/src/print.js +10 -1
  23. package/dist/src/resolve.js +91 -10
  24. package/dist/src/run.js +9 -3
  25. package/dist/src/schema.js +37 -1
  26. package/dist/src/scope.js +55 -21
  27. package/dist/src/serialization-contract.js +109 -19
  28. package/dist/src/stores.js +32 -0
  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 +113 -16
  33. package/src/components.ts +73 -131
  34. package/src/config.ts +37 -2
  35. package/src/dependencies.ts +617 -0
  36. package/src/deprecation.ts +101 -0
  37. package/src/discover.ts +1 -1
  38. package/src/enum-group.ts +265 -0
  39. package/src/file-facts.ts +143 -0
  40. package/src/findings/boundary.ts +39 -0
  41. package/src/findings/completion.ts +151 -0
  42. package/src/findings/dependencies-and-module-machinery.ts +79 -1
  43. package/src/findings/emitter.ts +21 -1
  44. package/src/findings/emitters.ts +13 -1
  45. package/src/findings/interfaces.ts +149 -3
  46. package/src/findings/non-code-artifacts.ts +124 -2
  47. package/src/findings/reads-and-writes.ts +310 -3
  48. package/src/findings/unused-declarations.ts +223 -2
  49. package/src/findings/visibility-narrowing.ts +324 -3
  50. package/src/implementations.ts +286 -0
  51. package/src/inventory.ts +2 -2
  52. package/src/kinds.ts +41 -0
  53. package/src/print.ts +17 -1
  54. package/src/resolve.ts +124 -10
  55. package/src/run.ts +10 -3
  56. package/src/schema.ts +47 -2
  57. package/src/scope.ts +63 -22
  58. package/src/serialization-contract.ts +138 -20
  59. package/src/stores.ts +77 -0
  60. package/src/version.ts +1 -1
@@ -1,2 +1,98 @@
1
+ const FILE_NEVER_BUILT = "DS1501";
2
+ const FILE_NEVER_IMPORTED = "DS1502";
3
+ /** The subject kind of a finding about a source file, from the finding schema's vocabulary. */
4
+ const FILE_SUBJECT = "file";
5
+ /** A file's subject starts at its first character. */
6
+ const FILE_START = 1;
7
+ /**
8
+ * Whether the configuration declares the run's matrix complete. The declaration is
9
+ * about the projects the configuration lists, so a configuration declaring
10
+ * completeness and listing no project declares it of a matrix the run derived, and a
11
+ * derived matrix is never complete.
12
+ */
13
+ function completeMatrix(config) {
14
+ return (config.analysis.matrixComplete &&
15
+ config.analysis.configurations.some((entry) => entry.shape === "project"));
16
+ }
17
+ /** One finding about a whole file. */
18
+ function fileFinding(code, file, message) {
19
+ return {
20
+ code,
21
+ position: { path: file.path, line: FILE_START, column: FILE_START, endLine: file.endLine },
22
+ symbol: { ref: file.ref, kind: FILE_SUBJECT, name: file.path, sizeLines: file.endLine },
23
+ message,
24
+ };
25
+ }
26
+ /**
27
+ * Every source file below the target root that no project of the matrix includes,
28
+ * under a matrix the configuration declares complete: that declaration says the listed
29
+ * projects are every one the target builds, so a file none of them includes is built
30
+ * by nothing. Under a derived matrix nothing is reported, because a project the
31
+ * derivation did not find is no evidence that nothing builds the file.
32
+ */
33
+ function fileNeverBuilt({ config, swept, files }) {
34
+ if (!completeMatrix(config)) {
35
+ return [];
36
+ }
37
+ const built = new Set(swept.matrix.union.symbols
38
+ .filter((symbol) => symbol.kind === FILE_SUBJECT)
39
+ .map((symbol) => symbol.position.path));
40
+ const message = `no project of the build matrix includes this file (${swept.matrix.configurations.join(", ")})`;
41
+ return files.tree
42
+ .filter((file) => !built.has(file.path))
43
+ .map((file) => fileFinding(FILE_NEVER_BUILT, file, message));
44
+ }
45
+ /**
46
+ * The paths of every file a reference written in another file reaches, naming the
47
+ * file or a declaration in it. An import is such a reference, so an import from any
48
+ * file reaches what it names, whatever reaches the importing file.
49
+ */
50
+ function reachedFromOutside(union) {
51
+ const reached = new Set();
52
+ union.out.forEach((edges, from) => {
53
+ const fromPath = union.symbols[from]?.position.path;
54
+ for (const edge of edges) {
55
+ const to = union.symbols[edge.to];
56
+ if (to !== undefined && to.position.path !== fromPath) {
57
+ reached.add(to.position.path);
58
+ }
59
+ }
60
+ });
61
+ return reached;
62
+ }
63
+ /**
64
+ * The paths of the files an import reaches, a root names, or a declared edge names a
65
+ * declaration in. A file holding a declaration the sweep judged live is kept whatever
66
+ * held it live, and so is a file a program holds by inclusion, which acts on every
67
+ * file of the program without an import naming it.
68
+ */
69
+ function keptFiles({ swept, files, boundary }) {
70
+ const union = swept.matrix.union;
71
+ const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
72
+ const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
73
+ const kept = new Set([...reachedFromOutside(union), ...files.heldByInclusion]);
74
+ for (const root of union.rooted) {
75
+ const named = union.symbols[root.at];
76
+ if (named !== undefined) {
77
+ kept.add(named.position.path);
78
+ }
79
+ }
80
+ for (const symbol of union.symbols) {
81
+ if ((symbol.kind !== FILE_SUBJECT && !dead.has(symbol.id)) || edgeNamed.has(symbol.ref)) {
82
+ kept.add(symbol.position.path);
83
+ }
84
+ }
85
+ return kept;
86
+ }
87
+ /** Every file of the run that no import reaches and no root or edge names. */
88
+ function fileNeverImported(input) {
89
+ const kept = keptFiles(input);
90
+ return input.swept.matrix.union.symbols
91
+ .filter((symbol) => symbol.kind === FILE_SUBJECT && !kept.has(symbol.position.path))
92
+ .map((symbol) => fileFinding(FILE_NEVER_IMPORTED, { path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine }, "no import reaches this file and no root names it or a declaration in it"));
93
+ }
1
94
  /** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
2
- export const nonCodeArtifacts = () => [];
95
+ export const nonCodeArtifacts = (input) => [
96
+ ...fileNeverBuilt(input),
97
+ ...fileNeverImported(input),
98
+ ];
@@ -1,2 +1,199 @@
1
- /** The findings of the reads-and-writes family, `DS1300` to `DS1399`. */
2
- export const readsAndWrites = () => [];
1
+ import { positionKey } from "../position.js";
2
+ import { findingComponents, RANK, reachabilityClasses, severityOf, } from "./completion.js";
3
+ /**
4
+ * Each code's severity where the configuration sets none, as the issue-kind vocabulary
5
+ * states it.
6
+ */
7
+ export const DEFAULT_SEVERITY = {
8
+ DS1301: "deny",
9
+ DS1302: "deny",
10
+ DS1303: "deny",
11
+ };
12
+ /** The containers whose type parameters `DS1303` reports: a function and a class method. */
13
+ const SIGNATURES = new Set(["function", "method"]);
14
+ /** The severity the configuration gives one code of the family. */
15
+ function severityIn(input, code) {
16
+ return severityOf(input.config, code, DEFAULT_SEVERITY[code]);
17
+ }
18
+ function indexed(input) {
19
+ const { matrix, sweep } = input.swept;
20
+ return {
21
+ input,
22
+ symbols: new Map(matrix.union.symbols.map((symbol) => [symbol.id, symbol])),
23
+ candidates: new Map(sweep.candidates.map((candidate) => [candidate.id, candidate])),
24
+ componentOf: findingComponents(input.swept),
25
+ classOf: reachabilityClasses(input.swept),
26
+ };
27
+ }
28
+ /** Where one declaration is, as a finding states it. */
29
+ function positionOf(symbol) {
30
+ return {
31
+ path: symbol.position.path,
32
+ line: symbol.position.line,
33
+ column: symbol.position.column,
34
+ endLine: Math.max(symbol.position.line, symbol.endLine),
35
+ };
36
+ }
37
+ /** The finding about one declaration, every member the family decides filled in. */
38
+ function findingAbout(run, symbol, code, message, facts) {
39
+ const position = positionOf(symbol);
40
+ const reachabilityClass = run.classOf(symbol.id);
41
+ const component = run.componentOf(symbol.id);
42
+ return {
43
+ code,
44
+ position,
45
+ symbol: {
46
+ ref: symbol.ref,
47
+ kind: symbol.kind,
48
+ name: symbol.name,
49
+ sizeLines: position.endLine - position.line + 1,
50
+ },
51
+ message,
52
+ reachabilityClass,
53
+ confidence: reachabilityClass,
54
+ ...(facts.livenessRelation === undefined ? {} : { livenessRelation: facts.livenessRelation }),
55
+ testOnly: facts.testOnly,
56
+ ...(component === undefined ? {} : { component }),
57
+ configurations: facts.configurations,
58
+ severity: severityIn(run.input, code),
59
+ details: facts.details,
60
+ };
61
+ }
62
+ /**
63
+ * Whether one declaration is a subject of the write-only kind: a module-level variable,
64
+ * or a class member that is state rather than code. A getter or a setter is code, so a
65
+ * store into one runs it.
66
+ */
67
+ function writable(run, symbol) {
68
+ if (symbol.kind === "variable") {
69
+ return true;
70
+ }
71
+ return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
72
+ }
73
+ /**
74
+ * `DS1301`: a module-level variable or a class member the counted references store into
75
+ * and never read, named with every position written.
76
+ *
77
+ * The subject is a live declaration: a store is a reference, so the sweep holds a
78
+ * written one live and its candidates are never this kind's. Every reference that is
79
+ * not a store reads, and so does an exemption record naming the declaration.
80
+ */
81
+ function writeOnlySymbols(run) {
82
+ const { stores } = run.input;
83
+ const { matrix } = run.input.swept;
84
+ const uses = new Map();
85
+ for (const reference of stores.references) {
86
+ let held = uses.get(reference.to);
87
+ if (held === undefined) {
88
+ held = { writes: [], reads: 0, production: 0, test: 0 };
89
+ uses.set(reference.to, held);
90
+ }
91
+ if (reference.test) {
92
+ held.test += 1;
93
+ }
94
+ else {
95
+ held.production += 1;
96
+ }
97
+ if (reference.use !== "write") {
98
+ held.reads += 1;
99
+ continue;
100
+ }
101
+ const at = reference.position;
102
+ const key = positionKey(at);
103
+ if (!held.writes.some((written) => positionKey(written) === key)) {
104
+ held.writes.push({ path: at.path, line: at.line, column: at.column, endLine: at.line });
105
+ }
106
+ }
107
+ const found = [];
108
+ matrix.union.symbols.forEach((symbol, at) => {
109
+ const held = uses.get(symbol.id);
110
+ if (held === undefined ||
111
+ held.writes.length === 0 ||
112
+ held.reads > 0 ||
113
+ !writable(run, symbol) ||
114
+ stores.exempt.has(symbol.id) ||
115
+ run.candidates.has(symbol.id)) {
116
+ return;
117
+ }
118
+ const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
119
+ const word = symbol.kind === "variable" ? "variable" : "member";
120
+ found.push(findingAbout(run, symbol, "DS1301", `${word} ${symbol.name} is written ${writes} and never read`, {
121
+ testOnly: held.production === 0 && held.test > 0,
122
+ configurations: matrix.heldIn[at] ?? [],
123
+ details: { writePositions: held.writes },
124
+ }));
125
+ });
126
+ return found;
127
+ }
128
+ /**
129
+ * The candidates no reference of any file names whose container is not itself dead, of
130
+ * one kind. A reference from a test file alone leaves a candidate to the test-only kind,
131
+ * and a member of a dead container falls with it and is reported inside its component.
132
+ */
133
+ function unnamed(run, kind) {
134
+ return run.input.swept.sweep.candidates.flatMap((candidate) => {
135
+ const symbol = run.symbols.get(candidate.id);
136
+ if (symbol?.kind !== kind ||
137
+ candidate.productionRefs > 0 ||
138
+ candidate.testRefs > 0 ||
139
+ run.candidates.has(symbol.parent)) {
140
+ return [];
141
+ }
142
+ return [[candidate, symbol]];
143
+ });
144
+ }
145
+ /**
146
+ * `DS1302`: a member of an enum declaration that nothing names. A member an enum-group
147
+ * record holds back is no candidate, so a member a conversion can produce never reaches
148
+ * this kind.
149
+ */
150
+ function unusedEnumMembers(run) {
151
+ return unnamed(run, "enum-member").map(([candidate, symbol]) => findingAbout(run, symbol, "DS1302", `enum member ${symbol.name} is named nowhere`, {
152
+ livenessRelation: candidate.relation,
153
+ testOnly: false,
154
+ configurations: candidate.configurations,
155
+ details: {},
156
+ }));
157
+ }
158
+ /**
159
+ * `DS1303`: a type parameter of a function or a class method that neither the
160
+ * declaration's signature nor its body names. A type parameter of a class, an interface,
161
+ * a type alias or a method signature belongs to a type declaration and is never
162
+ * reported, because a phantom parameter makes two instantiations distinct types.
163
+ */
164
+ function unusedTypeParameters(run) {
165
+ return unnamed(run, "type-parameter")
166
+ .filter(([, symbol]) => SIGNATURES.has(run.symbols.get(symbol.parent)?.kind ?? ""))
167
+ .map(([candidate, symbol]) => findingAbout(run, symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`, {
168
+ livenessRelation: candidate.relation,
169
+ testOnly: false,
170
+ configurations: candidate.configurations,
171
+ details: {},
172
+ }));
173
+ }
174
+ /**
175
+ * The references of the declarations the write-only kind reports, whatever the
176
+ * severity: a narrowing kind yields each to it, since deleting the declaration
177
+ * supersedes narrowing it.
178
+ */
179
+ export function writeOnlyDeclarations(input) {
180
+ return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
181
+ }
182
+ /** Each code's rule, in code order. */
183
+ const RULES = [
184
+ ["DS1301", writeOnlySymbols],
185
+ ["DS1302", unusedEnumMembers],
186
+ ["DS1303", unusedTypeParameters],
187
+ ];
188
+ /**
189
+ * The findings of the reads-and-writes family, `DS1300` to `DS1399`, each code's in site
190
+ * order. A code the configuration sets to `allow` reports nothing, and a finding whose
191
+ * confidence is below the configured minimum is withheld.
192
+ */
193
+ export const readsAndWrites = (input) => {
194
+ const run = indexed(input);
195
+ const least = RANK[input.config.analysis.minConfidence];
196
+ return RULES.flatMap(([code, rule]) => severityIn(input, code) === "allow"
197
+ ? []
198
+ : rule(run).filter((finding) => RANK[finding.confidence] >= least));
199
+ };
@@ -1,2 +1,183 @@
1
+ import { capped, findingComponents, reachabilityClasses, reportable, severityOf, } from "./completion.js";
2
+ import { unreachableExports } from "./visibility-narrowing.js";
3
+ /** The codes of the unused-declarations family, each by its row of the vocabulary. */
4
+ export const UNUSED_DECLARATION_KINDS = new Map([
5
+ ["DS1001", { defaultSeverity: "deny", maxClass: "certain" }],
6
+ ["DS1002", { defaultSeverity: "deny", maxClass: "certain" }],
7
+ ["DS1003", { defaultSeverity: "deny", maxClass: "certain" }],
8
+ ["DS1004", { defaultSeverity: "deny", maxClass: "certain" }],
9
+ ["DS1005", { defaultSeverity: "deny", maxClass: "certain" }],
10
+ ["DS1006", { defaultSeverity: "deny", maxClass: "certain" }],
11
+ ]);
12
+ /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
13
+ const MEMBERS = new Set([
14
+ "method",
15
+ "class-member",
16
+ "type-member",
17
+ "enum-member",
18
+ ]);
19
+ /**
20
+ * The kinds of declaration no code of this family reports: an interface and a method an
21
+ * interface declares are the interface kinds' subjects, and a type parameter is the
22
+ * subject of the unused-type-parameter kind or of none.
23
+ */
24
+ const NOT_THIS_FAMILY = new Set([
25
+ "file",
26
+ "interface",
27
+ "interface-method",
28
+ "type-parameter",
29
+ ]);
30
+ /** The reader's word for each kind of declaration a finding of the family names. */
31
+ const WORDS = {
32
+ file: "file",
33
+ function: "function",
34
+ class: "class",
35
+ interface: "interface",
36
+ type: "type alias",
37
+ enum: "enum",
38
+ namespace: "namespace",
39
+ variable: "variable",
40
+ "export-alias": "re-export",
41
+ method: "method",
42
+ "class-member": "class member",
43
+ "interface-method": "interface method",
44
+ "type-member": "type member",
45
+ "enum-member": "enum member",
46
+ "type-parameter": "type parameter",
47
+ };
48
+ /** What the test-of-dead-code kind says, which states the rule it applies. */
49
+ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
50
+ /**
51
+ * The code one candidate is reported under, or none, each symbol once under the most
52
+ * specific code: a test of dead code first; no other test-file declaration, member of a
53
+ * dead container or enum member nothing names; then deprecation, a test reference, and
54
+ * the unreferenced codes, an unimportable unused export being the unreachable kind's.
55
+ */
56
+ function codeOf(judged) {
57
+ const { candidate, symbol } = judged;
58
+ if (candidate.testOfDeadCode) {
59
+ return "DS1005";
60
+ }
61
+ if (judged.test || judged.containerDead || NOT_THIS_FAMILY.has(symbol.kind)) {
62
+ return undefined;
63
+ }
64
+ const referenced = candidate.productionRefs + candidate.testRefs > 0;
65
+ if (symbol.kind === "enum-member" && !referenced) {
66
+ return undefined;
67
+ }
68
+ if (candidate.productionRefs === 0 && judged.deprecated) {
69
+ return "DS1006";
70
+ }
71
+ if (candidate.productionRefs === 0 && candidate.testRefs > 0) {
72
+ return "DS1004";
73
+ }
74
+ if (MEMBERS.has(symbol.kind)) {
75
+ return "DS1003";
76
+ }
77
+ if (!symbol.exported) {
78
+ return "DS1002";
79
+ }
80
+ return judged.unreachable ? undefined : "DS1001";
81
+ }
82
+ /** The visibility a member's message names, where it narrows the member's reach. */
83
+ function visibilityWord(symbol) {
84
+ switch (symbol.visibility) {
85
+ case "private":
86
+ case "private-name":
87
+ return "private ";
88
+ case "protected":
89
+ return "protected ";
90
+ case "public":
91
+ return "";
92
+ }
93
+ }
94
+ /**
95
+ * What one finding says: what the subject is, and what the analysis found about its
96
+ * references. A candidate reachability found has references, each from a declaration
97
+ * that is itself dead, so saying it has none would be false.
98
+ */
99
+ function messageOf(code, judged) {
100
+ const { candidate, symbol } = judged;
101
+ const word = WORDS[symbol.kind];
102
+ switch (code) {
103
+ case "DS1005":
104
+ return TEST_OF_DEAD_CODE;
105
+ case "DS1006":
106
+ return `deprecated ${word} has no production reference`;
107
+ case "DS1004":
108
+ return `${word} is referenced only from test files and never from production code`;
109
+ }
110
+ const subject = code === "DS1003"
111
+ ? `${visibilityWord(symbol)}${word}`
112
+ : symbol.kind === "export-alias"
113
+ ? word
114
+ : `${symbol.exported ? "exported" : "unexported"} ${word}`;
115
+ if (candidate.relation === "reachability") {
116
+ return `${subject} is referenced only from declarations that are themselves dead`;
117
+ }
118
+ if (code === "DS1001") {
119
+ return `${subject} has no reference in the target and none from any loaded consumer`;
120
+ }
121
+ return `${subject} has no reference in the target`;
122
+ }
123
+ /**
124
+ * The findings of the unused-declarations family, `DS1000` to `DS1099`, completed and
125
+ * with what the dials withhold left out.
126
+ */
127
+ export function unusedDeclarationFindings(input) {
128
+ const { config, swept } = input;
129
+ const union = swept.matrix.union;
130
+ const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
131
+ const classOf = reachabilityClasses(swept);
132
+ const componentOf = findingComponents(swept);
133
+ const unreachable = unreachableExports(input);
134
+ const found = [];
135
+ for (const candidate of swept.sweep.candidates) {
136
+ const at = union.at(candidate.id);
137
+ const symbol = union.symbols[at];
138
+ if (symbol === undefined) {
139
+ continue;
140
+ }
141
+ const judged = {
142
+ candidate,
143
+ symbol,
144
+ test: union.test[at] === true,
145
+ containerDead: dead.has(symbol.parent),
146
+ deprecated: input.deprecated.has(symbol.id),
147
+ unreachable: unreachable.has(symbol.id),
148
+ };
149
+ const code = codeOf(judged);
150
+ const row = code === undefined ? undefined : UNUSED_DECLARATION_KINDS.get(code);
151
+ if (code === undefined || row === undefined) {
152
+ continue;
153
+ }
154
+ const component = componentOf(symbol.id);
155
+ if (component === undefined) {
156
+ throw new Error(`${symbol.id} is reported dead and falls in no dead component`);
157
+ }
158
+ const endLine = Math.max(symbol.position.line, symbol.endLine);
159
+ const reachabilityClass = classOf(symbol.id);
160
+ found.push({
161
+ code,
162
+ position: { ...symbol.position, endLine },
163
+ symbol: {
164
+ ref: symbol.ref,
165
+ kind: symbol.kind,
166
+ name: symbol.name,
167
+ sizeLines: endLine - symbol.position.line + 1,
168
+ },
169
+ message: messageOf(code, judged),
170
+ reachabilityClass,
171
+ confidence: capped(reachabilityClass, row.maxClass),
172
+ livenessRelation: candidate.relation,
173
+ testOnly: candidate.productionRefs === 0 && candidate.testRefs > 0,
174
+ component,
175
+ configurations: candidate.configurations,
176
+ severity: severityOf(config, code, row.defaultSeverity),
177
+ details: {},
178
+ });
179
+ }
180
+ return reportable(found, config.analysis.minConfidence);
181
+ }
1
182
  /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
2
- export const unusedDeclarations = () => [];
183
+ export const unusedDeclarations = unusedDeclarationFindings;