@cplieger/deadset-ts 3.5.0 → 3.7.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 (47) hide show
  1. package/dist/src/analysis.js +70 -14
  2. package/dist/src/components.js +5 -1
  3. package/dist/src/dependencies.js +493 -0
  4. package/dist/src/deprecation.js +83 -0
  5. package/dist/src/discover.js +1 -1
  6. package/dist/src/enum-group.js +217 -0
  7. package/dist/src/file-facts.js +101 -0
  8. package/dist/src/finding.js +0 -11
  9. package/dist/src/findings/boundary.js +1 -0
  10. package/dist/src/findings/completion.js +135 -0
  11. package/dist/src/findings/dependencies-and-module-machinery.js +57 -1
  12. package/dist/src/findings/emitters.js +10 -0
  13. package/dist/src/findings/interfaces.js +95 -2
  14. package/dist/src/findings/non-code-artifacts.js +97 -1
  15. package/dist/src/findings/reads-and-writes.js +146 -2
  16. package/dist/src/findings/unused-declarations.js +150 -1
  17. package/dist/src/findings/visibility-narrowing.js +239 -2
  18. package/dist/src/implementations.js +210 -0
  19. package/dist/src/inventory.js +2 -2
  20. package/dist/src/kinds.js +47 -45
  21. package/dist/src/resolve.js +1 -1
  22. package/dist/src/stores.js +32 -0
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +113 -16
  26. package/src/components.ts +6 -1
  27. package/src/dependencies.ts +617 -0
  28. package/src/deprecation.ts +101 -0
  29. package/src/discover.ts +1 -1
  30. package/src/enum-group.ts +265 -0
  31. package/src/file-facts.ts +143 -0
  32. package/src/finding.ts +76 -10
  33. package/src/findings/boundary.ts +39 -0
  34. package/src/findings/completion.ts +163 -0
  35. package/src/findings/dependencies-and-module-machinery.ts +64 -1
  36. package/src/findings/emitter.ts +24 -3
  37. package/src/findings/emitters.ts +13 -1
  38. package/src/findings/interfaces.ts +137 -3
  39. package/src/findings/non-code-artifacts.ts +124 -2
  40. package/src/findings/reads-and-writes.ts +196 -3
  41. package/src/findings/unused-declarations.ts +175 -1
  42. package/src/findings/visibility-narrowing.ts +319 -3
  43. package/src/implementations.ts +286 -0
  44. package/src/inventory.ts +2 -2
  45. package/src/kinds.ts +79 -45
  46. package/src/resolve.ts +1 -1
  47. package/src/stores.ts +77 -0
@@ -1,2 +1,239 @@
1
- /** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
2
- export const visibilityNarrowing = () => [];
1
+ import { OUTSIDE } from "../graph.js";
2
+ import { writeOnlyDeclarations } from "./reads-and-writes.js";
3
+ const UNNECESSARY_EXPORT = "DS1101";
4
+ const UNREACHABLE_EXPORT = "DS1103";
5
+ const REDUNDANT_EXPORT_KEYWORD = "DS1104";
6
+ /**
7
+ * The root kinds by which a consumer importing the package reaches a declaration. A
8
+ * declaration or a file rooted by any other kind is entered by something that names it
9
+ * from outside the reference graph: a runtime, a tool, a framework or the configuration.
10
+ */
11
+ const PUBLISHING = new Set(["manifest-entry", "published-api"]);
12
+ /** The reader's word for each kind of declaration this family reports. */
13
+ const WORDS = {
14
+ function: "function",
15
+ class: "class",
16
+ interface: "interface",
17
+ type: "type alias",
18
+ enum: "enum",
19
+ namespace: "namespace",
20
+ variable: "variable",
21
+ "export-alias": "re-export",
22
+ };
23
+ function publishes(kinds) {
24
+ return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
25
+ }
26
+ function entersByName(kinds) {
27
+ return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
28
+ }
29
+ /**
30
+ * Folds the run's references into the reach of each declaration.
31
+ *
32
+ * A reference reaches the file that holds the declaration that makes it. A reference
33
+ * from a declaration the inventory does not hold is made by a consumer, and one from a
34
+ * re-export a manifest export reaches stands for every consumer importing the name it
35
+ * publishes; either reaches outside the package.
36
+ */
37
+ function factsOf(graph) {
38
+ const { symbols, parent } = graph;
39
+ const fileOf = symbols.map((_symbol, at) => {
40
+ let file = at;
41
+ while (file !== OUTSIDE && symbols[file]?.kind !== "file") {
42
+ file = parent[file] ?? OUTSIDE;
43
+ }
44
+ return file;
45
+ });
46
+ const rootedBy = symbols.map(() => new Set());
47
+ for (const root of graph.rooted) {
48
+ rootedBy[root.at]?.add(root.kind);
49
+ }
50
+ const published = symbols.map(() => false);
51
+ symbols.forEach((_symbol, at) => {
52
+ const file = fileOf[at] ?? OUTSIDE;
53
+ if (file !== OUTSIDE && publishes(rootedBy[at])) {
54
+ published[file] = true;
55
+ }
56
+ });
57
+ const order = ["file", "package", "outside"];
58
+ const wider = (a, b) => (order.indexOf(a) >= order.indexOf(b) ? a : b);
59
+ const inGraph = symbols.map(() => 0);
60
+ const reach = symbols.map(() => "file");
61
+ graph.out.forEach((edges, from) => {
62
+ const republished = symbols[from]?.kind === "export-alias" && publishes(rootedBy[from]);
63
+ for (const edge of edges) {
64
+ inGraph[edge.to] = (inGraph[edge.to] ?? 0) + 1;
65
+ const scope = republished
66
+ ? "outside"
67
+ : fileOf[from] === fileOf[edge.to]
68
+ ? "file"
69
+ : "package";
70
+ reach[edge.to] = wider(reach[edge.to] ?? "file", scope);
71
+ }
72
+ });
73
+ const named = graph.made.map((counts) => counts.production + counts.test);
74
+ named.forEach((count, at) => {
75
+ if (count > (inGraph[at] ?? 0)) {
76
+ reach[at] = "outside";
77
+ }
78
+ });
79
+ return { graph, fileOf, rootedBy, published, named, reach };
80
+ }
81
+ /** Whether one declaration is a declaration its file's export table names. */
82
+ function topLevelExport(facts, at, symbol) {
83
+ const parent = facts.graph.parent[at] ?? OUTSIDE;
84
+ return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
85
+ }
86
+ function wordOf(symbol) {
87
+ return WORDS[symbol.kind] ?? symbol.kind;
88
+ }
89
+ /** The visibility each narrowing code names, which only the narrowing codes carry. */
90
+ const NARROWER = {
91
+ [REDUNDANT_EXPORT_KEYWORD]: "file",
92
+ [UNNECESSARY_EXPORT]: "package",
93
+ };
94
+ function findingOf(symbol, code, message) {
95
+ const { path, line, column } = symbol.position;
96
+ const narrower = Object.hasOwn(NARROWER, code) ? NARROWER[code] : undefined;
97
+ const endLine = Math.max(line, symbol.endLine);
98
+ return {
99
+ code,
100
+ position: { path, line, column, endLine },
101
+ symbol: {
102
+ ref: symbol.ref,
103
+ kind: symbol.kind,
104
+ name: symbol.name,
105
+ sizeLines: endLine - line + 1,
106
+ },
107
+ message,
108
+ details: narrower === undefined ? {} : { narrowerVisibility: narrower },
109
+ };
110
+ }
111
+ /**
112
+ * The narrowing finding about one live declaration, if one holds: an export of a file,
113
+ * no re-export and none the write-only kind reports, that a reference names and nothing
114
+ * enters by name. Every reference inside its own file makes it a redundant export
115
+ * keyword, where no manifest export reaches the file or the world is closed; references
116
+ * from other files of the package alone make a published one unnecessary when closed.
117
+ */
118
+ function narrowing(facts, at, symbol, world) {
119
+ const { closed } = world;
120
+ const file = facts.fileOf[at] ?? OUTSIDE;
121
+ if (!topLevelExport(facts, at, symbol) ||
122
+ world.writeOnly.has(symbol.ref) ||
123
+ symbol.kind === "export-alias" ||
124
+ entersByName(facts.rootedBy[at]) ||
125
+ entersByName(facts.rootedBy[file]) ||
126
+ (facts.named[at] ?? 0) === 0) {
127
+ return undefined;
128
+ }
129
+ const word = wordOf(symbol);
130
+ switch (facts.reach[at]) {
131
+ case "file":
132
+ if (facts.published[file] === true && !closed) {
133
+ return undefined;
134
+ }
135
+ return findingOf(symbol, REDUNDANT_EXPORT_KEYWORD, `exported ${word} is referenced only inside the file that declares it`);
136
+ case "package":
137
+ if (!closed || !publishes(facts.rootedBy[at])) {
138
+ return undefined;
139
+ }
140
+ return findingOf(symbol, UNNECESSARY_EXPORT, `exported ${word} is referenced only inside the package that declares it`);
141
+ default:
142
+ return undefined;
143
+ }
144
+ }
145
+ /**
146
+ * The unreachable-export finding about one dead declaration, if one holds: an unused
147
+ * export no code outside the target can import, because the manifest declares
148
+ * `exports` and no root reaches it. Each arm yields to a more specific kind, so one
149
+ * declaration is reported once: a test whose every subject is dead, an interface, a
150
+ * declaration only a test file references, and a deprecated one.
151
+ */
152
+ function unreachable(facts, at, symbol, candidate, input) {
153
+ if (!input.boundary.encapsulated ||
154
+ !topLevelExport(facts, at, symbol) ||
155
+ (facts.rootedBy[at]?.size ?? 0) > 0 ||
156
+ candidate.testOfDeadCode ||
157
+ symbol.kind === "interface" ||
158
+ (candidate.productionRefs === 0 && candidate.testRefs > 0) ||
159
+ (candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
160
+ return undefined;
161
+ }
162
+ const found = candidate.relation === "reachability"
163
+ ? "is referenced only from declarations that are themselves dead"
164
+ : "has no reference in the target";
165
+ return findingOf(symbol, UNREACHABLE_EXPORT, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
166
+ }
167
+ /** Two strings ordered bytewise. */
168
+ function compare(a, b) {
169
+ if (a === b) {
170
+ return 0;
171
+ }
172
+ return a < b ? -1 : 1;
173
+ }
174
+ /**
175
+ * The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
176
+ *
177
+ * A finding about a symbol a declared cross-language edge names is pending instead of
178
+ * reported, once per side naming it: the edge counts as a reference from outside the
179
+ * file and the package, and whether it is a use is decided on the side in the other
180
+ * language.
181
+ */
182
+ export function narrowings(input) {
183
+ const { boundary } = input;
184
+ const facts = factsOf(input.swept.matrix.union);
185
+ const { declared, loaded } = boundary.consumers;
186
+ const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
187
+ const world = { closed, writeOnly: writeOnlyDeclarations(input) };
188
+ const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
189
+ const findings = [];
190
+ const pending = [];
191
+ facts.graph.symbols.forEach((symbol, at) => {
192
+ const candidate = candidates.get(symbol.id);
193
+ const found = candidate === undefined
194
+ ? narrowing(facts, at, symbol, world)
195
+ : unreachable(facts, at, symbol, candidate, input);
196
+ if (found === undefined) {
197
+ return;
198
+ }
199
+ const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
200
+ if (named.length === 0) {
201
+ findings.push(found);
202
+ return;
203
+ }
204
+ for (const side of named) {
205
+ pending.push({ ...side, finding: found });
206
+ }
207
+ });
208
+ pending.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
209
+ return { findings, pending };
210
+ }
211
+ /**
212
+ * The declarations the unreachable-export kind reports, whether or not a declared edge
213
+ * holds the finding pending: the unused-declaration kinds yield each to it.
214
+ */
215
+ export function unreachableExports(input) {
216
+ const facts = factsOf(input.swept.matrix.union);
217
+ const found = new Set();
218
+ for (const candidate of input.swept.sweep.candidates) {
219
+ const at = facts.graph.at(candidate.id);
220
+ const symbol = facts.graph.symbols[at];
221
+ if (symbol !== undefined && unreachable(facts, at, symbol, candidate, input) !== undefined) {
222
+ found.add(symbol.id);
223
+ }
224
+ }
225
+ return found;
226
+ }
227
+ /**
228
+ * The findings of the visibility-narrowing family, `DS1100` to `DS1199`. A finding a
229
+ * declared edge holds pending belongs in that edge's evaluation, which this analyzer
230
+ * does not publish yet, so a run that holds one is refused rather than reported
231
+ * without it.
232
+ */
233
+ export const visibilityNarrowing = (input) => {
234
+ const { findings, pending } = narrowings(input);
235
+ if (pending.length > 0) {
236
+ throw new Error(`${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`);
237
+ }
238
+ return findings;
239
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Which classes of the target implement which of its interfaces, and how each writes
3
+ * every method such an interface declares. A class implements an interface its
4
+ * `implements` clause names, in any instantiation, or one with a member its instance
5
+ * type is assignable to, asked only of a class carrying every member the interface
6
+ * requires and one it declares, and never of a generic side.
7
+ */
8
+ import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
10
+ import { aliasChains } from "./alias-chain.js";
11
+ import { nodeKey } from "./inventory.js";
12
+ import { DEFAULT_BATCH_CAP } from "./references.js";
13
+ /** The interfaces, the classes and the `implements` entries of one project's own files. */
14
+ function walk(project, held) {
15
+ const interfaces = [];
16
+ const classes = [];
17
+ const clauses = [];
18
+ for (const file of project.ownSourceFiles()) {
19
+ const visit = (node) => {
20
+ if (isInterfaceDeclaration(node) || isClassDeclaration(node)) {
21
+ const id = held.declarations.get(nodeKey(file, node));
22
+ const name = node.name;
23
+ if (id !== undefined && name !== undefined) {
24
+ const declared = { id, nameNode: name, generic: node.typeParameters !== undefined };
25
+ if (isInterfaceDeclaration(node)) {
26
+ interfaces.push(declared);
27
+ }
28
+ else {
29
+ classes.push(declared);
30
+ for (const clause of node.heritageClauses ?? []) {
31
+ if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
32
+ clauses.push(...clause.types.map((entry) => ({ owner: declared, entry })));
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ node.forEachChild(visit);
39
+ };
40
+ file.forEachChild(visit);
41
+ }
42
+ return { interfaces, classes, clauses };
43
+ }
44
+ /** The type at each node, asked in capped batches. */
45
+ function typesAt(project, nodes) {
46
+ const typeAt = new Map();
47
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
48
+ const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
49
+ project.checker.getTypeAtLocation(run).forEach((type, index) => {
50
+ const node = run[index];
51
+ if (type !== undefined && node !== undefined) {
52
+ typeAt.set(node, type);
53
+ }
54
+ });
55
+ }
56
+ return typeAt;
57
+ }
58
+ /** Whether one class member is a method whose every written body holds no statement. */
59
+ function emptyImplementation(project, member) {
60
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
61
+ let bodies = 0;
62
+ for (const handle of member.declarations) {
63
+ // A body outside the target is one this analysis cannot read, so it is not empty.
64
+ if (!own.has(handle.path)) {
65
+ return false;
66
+ }
67
+ const node = project.declarationAt(handle)?.node;
68
+ if (node === undefined || !isMethodDeclaration(node)) {
69
+ return false;
70
+ }
71
+ if (node.body !== undefined) {
72
+ if (node.body.statements.length > 0) {
73
+ return false;
74
+ }
75
+ bodies += 1;
76
+ }
77
+ }
78
+ return bodies > 0;
79
+ }
80
+ /** How one project's interfaces are implemented by its classes. */
81
+ export function implementations(project, held) {
82
+ const classes = new Map();
83
+ const bodies = new Map();
84
+ const walked = walk(project, held);
85
+ if (walked.interfaces.length === 0 || walked.classes.length === 0) {
86
+ return { classes, bodies };
87
+ }
88
+ const checker = project.checker;
89
+ const typeAt = typesAt(project, [
90
+ ...walked.interfaces.map((one) => one.nameNode),
91
+ ...walked.classes.map((one) => one.nameNode),
92
+ ...walked.clauses.map((clause) => clause.entry),
93
+ ]);
94
+ // Two declarations of one interface merge into one type.
95
+ const byType = new Map();
96
+ for (const declared of walked.interfaces) {
97
+ const type = typeAt.get(declared.nameNode);
98
+ if (type === undefined) {
99
+ continue;
100
+ }
101
+ const merged = byType.get(type.id);
102
+ if (merged === undefined) {
103
+ byType.set(type.id, { ids: [declared.id], type, generic: declared.generic });
104
+ }
105
+ else {
106
+ merged.ids.push(declared.id);
107
+ merged.generic ||= declared.generic;
108
+ }
109
+ }
110
+ const classAt = new Map();
111
+ for (const owner of walked.classes) {
112
+ const type = typeAt.get(owner.nameNode);
113
+ if (type !== undefined) {
114
+ classAt.set(owner.id, { owner, type });
115
+ }
116
+ }
117
+ const membersOf = (at) => (at.members ??= checker.getPropertiesOfType(at.type));
118
+ const classMembers = (at) => (at.members ??= new Map(checker.getPropertiesOfType(at.type).map((member) => [member.escapedName, member])));
119
+ const methodIds = new Set(held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id));
120
+ const chains = aliasChains(project, held);
121
+ const record = (at, implementer) => {
122
+ for (const id of at.ids) {
123
+ let set = classes.get(id);
124
+ if (set === undefined) {
125
+ set = new Set();
126
+ classes.set(id, set);
127
+ }
128
+ set.add(implementer.owner.id);
129
+ }
130
+ const written = classMembers(implementer);
131
+ for (const member of membersOf(at)) {
132
+ const answering = written.get(member.escapedName);
133
+ if (answering === undefined) {
134
+ continue;
135
+ }
136
+ const empty = emptyImplementation(project, answering);
137
+ for (const id of chains.declarationsOf(member)) {
138
+ if (!methodIds.has(id)) {
139
+ continue;
140
+ }
141
+ let byClass = bodies.get(id);
142
+ if (byClass === undefined) {
143
+ byClass = new Map();
144
+ bodies.set(id, byClass);
145
+ }
146
+ byClass.set(implementer.owner.id, empty);
147
+ }
148
+ }
149
+ };
150
+ const recorded = new Set();
151
+ const pair = (at, implementer) => {
152
+ const key = `${String(at.type.id)}:${implementer.owner.id}`;
153
+ if (!recorded.has(key)) {
154
+ recorded.add(key);
155
+ record(at, implementer);
156
+ }
157
+ };
158
+ for (const clause of walked.clauses) {
159
+ const entry = typeAt.get(clause.entry);
160
+ const implementer = classAt.get(clause.owner.id);
161
+ if (entry === undefined || implementer === undefined) {
162
+ continue;
163
+ }
164
+ const at = byType.get(entry.isTypeReference() ? entry.getTarget().id : entry.id);
165
+ if (at !== undefined) {
166
+ pair(at, implementer);
167
+ }
168
+ }
169
+ for (const at of byType.values()) {
170
+ const members = at.generic ? [] : membersOf(at);
171
+ if (members.length === 0) {
172
+ continue;
173
+ }
174
+ const required = members.filter((member) => (member.flags & SymbolFlags.Optional) === 0);
175
+ for (const implementer of classAt.values()) {
176
+ if (implementer.owner.generic) {
177
+ continue;
178
+ }
179
+ const written = classMembers(implementer);
180
+ if (required.every((member) => written.has(member.escapedName)) &&
181
+ members.some((member) => written.has(member.escapedName)) &&
182
+ checker.isTypeAssignableTo(implementer.type, at.type)) {
183
+ pair(at, implementer);
184
+ }
185
+ }
186
+ }
187
+ return { classes, bodies };
188
+ }
189
+ /** Every project's answers as one run's, each implementation once. */
190
+ export function mergeImplementations(per) {
191
+ const classes = new Map();
192
+ const bodies = new Map();
193
+ for (const one of per) {
194
+ for (const [id, held] of one.classes) {
195
+ const set = classes.get(id) ?? new Set();
196
+ classes.set(id, set);
197
+ for (const owner of held) {
198
+ set.add(owner);
199
+ }
200
+ }
201
+ for (const [id, held] of one.bodies) {
202
+ const byClass = bodies.get(id) ?? new Map();
203
+ bodies.set(id, byClass);
204
+ for (const [owner, empty] of held) {
205
+ byClass.set(owner, (byClass.get(owner) ?? true) && empty);
206
+ }
207
+ }
208
+ }
209
+ return { classes, bodies };
210
+ }
@@ -13,7 +13,7 @@ const MANIFEST = "package.json";
13
13
  * One directory is read at most once, because a package holds many files and a
14
14
  * manifest read is a filesystem call.
15
15
  */
16
- function packages(host, root) {
16
+ export function packageScope(host, root) {
17
17
  const named = new Map();
18
18
  const manifestName = (dir) => {
19
19
  let text;
@@ -233,7 +233,7 @@ function typeMemberKind(node, inInterface) {
233
233
  export function inventory(project, host, targetRoot) {
234
234
  const files = project.ownSourceFiles();
235
235
  const ownFiles = new Set(files.map((file) => file.fileName));
236
- const moduleOf = packages(host, targetRoot);
236
+ const moduleOf = packageScope(host, targetRoot);
237
237
  const building = [];
238
238
  const byNode = new Map();
239
239
  const exportScopes = [];
package/dist/src/kinds.js CHANGED
@@ -1,52 +1,54 @@
1
1
  /**
2
- * The issue-kind codes whose severity the Contract fixes. A severity key naming
3
- * one, or a family prefix whose range holds one, is an unimplemented key rather
4
- * than a setting.
5
- *
6
- * The codes are written out rather than read from the Contract at run time: the
7
- * Contract is data a reader outside this program consults, and a product that
8
- * read it while running would need it installed beside itself. A test compares
9
- * this list against the Contract release the analyzer is written against, so a
10
- * code the Contract fixes arrives here as a failing test.
2
+ * Every live row of the issue-kind vocabulary, in ascending code order, written out
3
+ * rather than read at run time, because a product that read the Contract while running
4
+ * would need it installed beside itself. A test compares this table against the
5
+ * Contract release the analyzer is written against.
11
6
  */
12
- export const FIXED_SEVERITY_CODES = ["DS1703", "DS1704"];
7
+ const ROWS = [
8
+ row("DS1001", "unused-exported", "deny", "deletable"),
9
+ row("DS1002", "unused-unexported", "deny", "deletable"),
10
+ row("DS1003", "unused-member", "deny", "deletable"),
11
+ row("DS1004", "test-only-use", "deny", "deletable"),
12
+ row("DS1005", "test-of-dead-code", "deny", "deletable"),
13
+ row("DS1006", "deprecated-unused", "deny", "deletable"),
14
+ row("DS1101", "unnecessary-export", "warn", "narrowable"),
15
+ row("DS1102", "unnecessary-exposure", "warn", "narrowable"),
16
+ row("DS1103", "unreachable-export", "deny", "deletable"),
17
+ row("DS1104", "redundant-export-keyword", "warn", "narrowable"),
18
+ row("DS1201", "unused-interface", "deny", "deletable"),
19
+ row("DS1203", "uncalled-interface-method", "warn", "manual"),
20
+ row("DS1204", "unused-satisfaction-assertion", "deny", "deletable"),
21
+ row("DS1301", "write-only-symbol", "deny", "deletable"),
22
+ row("DS1302", "unused-enum-member", "deny", "deletable"),
23
+ row("DS1303", "unused-type-parameter", "deny", "deletable"),
24
+ row("DS1501", "file-never-built", "deny", "deletable"),
25
+ row("DS1502", "file-never-imported", "deny", "deletable"),
26
+ row("DS1601", "unused-dependency", "deny", "manual"),
27
+ row("DS1605", "unused-module-directive", "warn", "deletable"),
28
+ row("DS1701", "suppression-without-reason", "deny", "none"),
29
+ row("DS1702", "unscoped-ignore-entry", "deny", "none"),
30
+ { ...row("DS1703", "stale-suppression", "deny", "none"), fixed: true },
31
+ { ...row("DS1704", "unmatched-root", "deny", "none"), fixed: true },
32
+ row("DS1705", "stale-cross-language-edge", "deny", "none"),
33
+ row("DS1801", "unused-parameter", "warn", "manual"),
34
+ row("DS1802", "unused-receiver", "warn", "deletable"),
35
+ row("DS1803", "unused-result", "warn", "manual"),
36
+ row("DS1805", "unreachable-statement", "deny", "deletable"),
37
+ row("DS1807", "dead-store", "deny", "deletable"),
38
+ row("DS1809", "unreachable-case", "deny", "deletable"),
39
+ ];
40
+ /** A row at the `certain` ceiling whose severity a configuration may set. */
41
+ function row(code, name, defaultSeverity, fixability) {
42
+ return { code, name, defaultSeverity, maxClass: "certain", fixability, fixed: false };
43
+ }
44
+ /** Every live row of the issue-kind vocabulary, by its code, in ascending code order. */
45
+ export const KINDS = new Map(ROWS.map((one) => [one.code, one]));
13
46
  /**
14
47
  * The code of every live issue kind, in ascending order. A severity key names one of
15
48
  * them or a family prefix whose range holds one; a key naming a retired code, an
16
49
  * unassigned code or a range holding no live kind is an unimplemented key, so every
17
- * severity a configuration sets is one some analyzer reports under. A test compares
18
- * this list against the Contract release the analyzer is written against.
50
+ * severity a configuration sets is one some analyzer reports under.
19
51
  */
20
- export const LIVE_CODES = [
21
- "DS1001",
22
- "DS1002",
23
- "DS1003",
24
- "DS1004",
25
- "DS1005",
26
- "DS1006",
27
- "DS1101",
28
- "DS1102",
29
- "DS1103",
30
- "DS1104",
31
- "DS1201",
32
- "DS1203",
33
- "DS1204",
34
- "DS1301",
35
- "DS1302",
36
- "DS1303",
37
- "DS1501",
38
- "DS1502",
39
- "DS1601",
40
- "DS1605",
41
- "DS1701",
42
- "DS1702",
43
- "DS1703",
44
- "DS1704",
45
- "DS1705",
46
- "DS1801",
47
- "DS1802",
48
- "DS1803",
49
- "DS1805",
50
- "DS1807",
51
- "DS1809",
52
- ];
52
+ export const LIVE_CODES = ROWS.map((one) => one.code);
53
+ /** The issue-kind codes whose severity the Contract fixes, in ascending order. */
54
+ export const FIXED_SEVERITY_CODES = ROWS.filter((one) => one.fixed).map((one) => one.code);
@@ -4,7 +4,7 @@ import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } f
4
4
  /** Names the command-line flags in a refusal, where a document is named by its path. */
5
5
  const FLAGS_LABEL = "the command-line flags";
6
6
  /** The length of a severity key naming a whole family: the prefix and two digits. */
7
- const FAMILY_KEY_LENGTH = 4;
7
+ export const FAMILY_KEY_LENGTH = 4;
8
8
  const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
9
9
  const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
10
10
  const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What a run knows about the stores into its declarations and the reads of them, which
3
+ * a sweep does not keep: a sweep asks whether anything names a declaration, and a store
4
+ * names it as surely as a read does.
5
+ */
6
+ import { isGetAccessorDeclaration, isSetAccessorDeclaration, } from "@typescript/native/unstable/ast";
7
+ import { nodeKey } from "./inventory.js";
8
+ /** The class members one project's own files declare by a getter or a setter. */
9
+ export function accessorsOf(project, held) {
10
+ const found = [];
11
+ for (const file of project.ownSourceFiles()) {
12
+ const visit = (node) => {
13
+ if (isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) {
14
+ const id = held.declarations.get(nodeKey(file, node));
15
+ if (id !== undefined) {
16
+ found.push(id);
17
+ }
18
+ }
19
+ node.forEachChild(visit);
20
+ };
21
+ file.forEachChild(visit);
22
+ }
23
+ return found;
24
+ }
25
+ /** The run's stores, from what each configuration contributed, the records and the mode. */
26
+ export function storesOf(per, exempt, mode) {
27
+ return {
28
+ references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
29
+ accessors: new Set(per.flatMap((one) => one.accessors)),
30
+ exempt: new Set(exempt.map((record) => record.id)),
31
+ };
32
+ }
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",