@cplieger/deadset-ts 3.5.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 (43) 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/findings/boundary.js +1 -0
  9. package/dist/src/findings/completion.js +99 -0
  10. package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
  11. package/dist/src/findings/emitters.js +8 -0
  12. package/dist/src/findings/interfaces.js +95 -2
  13. package/dist/src/findings/non-code-artifacts.js +97 -1
  14. package/dist/src/findings/reads-and-writes.js +199 -2
  15. package/dist/src/findings/unused-declarations.js +182 -1
  16. package/dist/src/findings/visibility-narrowing.js +239 -2
  17. package/dist/src/implementations.js +210 -0
  18. package/dist/src/inventory.js +2 -2
  19. package/dist/src/resolve.js +1 -1
  20. package/dist/src/stores.js +32 -0
  21. package/jsr.json +1 -1
  22. package/package.json +1 -1
  23. package/src/analysis.ts +113 -16
  24. package/src/components.ts +6 -1
  25. package/src/dependencies.ts +617 -0
  26. package/src/deprecation.ts +101 -0
  27. package/src/discover.ts +1 -1
  28. package/src/enum-group.ts +265 -0
  29. package/src/file-facts.ts +143 -0
  30. package/src/findings/boundary.ts +39 -0
  31. package/src/findings/completion.ts +151 -0
  32. package/src/findings/dependencies-and-module-machinery.ts +79 -1
  33. package/src/findings/emitter.ts +21 -1
  34. package/src/findings/emitters.ts +13 -1
  35. package/src/findings/interfaces.ts +149 -3
  36. package/src/findings/non-code-artifacts.ts +124 -2
  37. package/src/findings/reads-and-writes.ts +310 -3
  38. package/src/findings/unused-declarations.ts +223 -2
  39. package/src/findings/visibility-narrowing.ts +324 -3
  40. package/src/implementations.ts +286 -0
  41. package/src/inventory.ts +2 -2
  42. package/src/resolve.ts +1 -1
  43. package/src/stores.ts +77 -0
@@ -1,4 +1,325 @@
1
- import type { Emitter } from "./emitter.ts";
1
+ import type { Finding } from "../finding.ts";
2
+ import { OUTSIDE, type Graph } from "../graph.ts";
3
+ import type { InventorySymbol, SymbolKind } from "../inventory.ts";
4
+ import type { MatrixCandidate } from "../matrix.ts";
5
+ import type { RootKind } from "../roots.ts";
6
+ import type { PendingFinding } from "./boundary.ts";
7
+ import type { Emitter, EmitterInput } from "./emitter.ts";
8
+ import { writeOnlyDeclarations } from "./reads-and-writes.ts";
2
9
 
3
- /** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
4
- export const visibilityNarrowing: Emitter = () => [];
10
+ const UNNECESSARY_EXPORT = "DS1101";
11
+ const UNREACHABLE_EXPORT = "DS1103";
12
+ const REDUNDANT_EXPORT_KEYWORD = "DS1104";
13
+
14
+ /** How far the references to one declaration reach, from the narrowest. */
15
+ type Reach = "file" | "package" | "outside";
16
+
17
+ /**
18
+ * The root kinds by which a consumer importing the package reaches a declaration. A
19
+ * declaration or a file rooted by any other kind is entered by something that names it
20
+ * from outside the reference graph: a runtime, a tool, a framework or the configuration.
21
+ */
22
+ const PUBLISHING: ReadonlySet<RootKind> = new Set<RootKind>(["manifest-entry", "published-api"]);
23
+
24
+ /** The reader's word for each kind of declaration this family reports. */
25
+ const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
26
+ function: "function",
27
+ class: "class",
28
+ interface: "interface",
29
+ type: "type alias",
30
+ enum: "enum",
31
+ namespace: "namespace",
32
+ variable: "variable",
33
+ "export-alias": "re-export",
34
+ };
35
+
36
+ /** What the family found: the findings it reports, and those a declared edge holds pending. */
37
+ export interface Narrowings {
38
+ readonly findings: readonly Finding[];
39
+ /** Ordered by edge, then by side. */
40
+ readonly pending: readonly PendingFinding[];
41
+ }
42
+
43
+ /** Per declaration of the run, the facts both rules read. */
44
+ interface Facts {
45
+ readonly graph: Graph;
46
+ /** The position of the file that holds each declaration. */
47
+ readonly fileOf: readonly number[];
48
+ /** The root kinds naming each declaration. */
49
+ readonly rootedBy: readonly ReadonlySet<RootKind>[];
50
+ /** Per file, whether a manifest export reaches a declaration it holds. */
51
+ readonly published: readonly boolean[];
52
+ /** Per declaration, how many references name it, and how far they reach. */
53
+ readonly named: readonly number[];
54
+ readonly reach: readonly Reach[];
55
+ }
56
+
57
+ function publishes(kinds: ReadonlySet<RootKind> | undefined): boolean {
58
+ return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
59
+ }
60
+
61
+ function entersByName(kinds: ReadonlySet<RootKind> | undefined): boolean {
62
+ return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
63
+ }
64
+
65
+ /**
66
+ * Folds the run's references into the reach of each declaration.
67
+ *
68
+ * A reference reaches the file that holds the declaration that makes it. A reference
69
+ * from a declaration the inventory does not hold is made by a consumer, and one from a
70
+ * re-export a manifest export reaches stands for every consumer importing the name it
71
+ * publishes; either reaches outside the package.
72
+ */
73
+ function factsOf(graph: Graph): Facts {
74
+ const { symbols, parent } = graph;
75
+ const fileOf = symbols.map((_symbol, at) => {
76
+ let file = at;
77
+ while (file !== OUTSIDE && symbols[file]?.kind !== "file") {
78
+ file = parent[file] ?? OUTSIDE;
79
+ }
80
+ return file;
81
+ });
82
+ const rootedBy = symbols.map(() => new Set<RootKind>());
83
+ for (const root of graph.rooted) {
84
+ rootedBy[root.at]?.add(root.kind);
85
+ }
86
+ const published = symbols.map(() => false);
87
+ symbols.forEach((_symbol, at) => {
88
+ const file = fileOf[at] ?? OUTSIDE;
89
+ if (file !== OUTSIDE && publishes(rootedBy[at])) {
90
+ published[file] = true;
91
+ }
92
+ });
93
+
94
+ const order: readonly Reach[] = ["file", "package", "outside"];
95
+ const wider = (a: Reach, b: Reach): Reach => (order.indexOf(a) >= order.indexOf(b) ? a : b);
96
+ const inGraph = symbols.map(() => 0);
97
+ const reach = symbols.map((): Reach => "file");
98
+ graph.out.forEach((edges, from) => {
99
+ const republished = symbols[from]?.kind === "export-alias" && publishes(rootedBy[from]);
100
+ for (const edge of edges) {
101
+ inGraph[edge.to] = (inGraph[edge.to] ?? 0) + 1;
102
+ const scope: Reach = republished
103
+ ? "outside"
104
+ : fileOf[from] === fileOf[edge.to]
105
+ ? "file"
106
+ : "package";
107
+ reach[edge.to] = wider(reach[edge.to] ?? "file", scope);
108
+ }
109
+ });
110
+ const named = graph.made.map((counts) => counts.production + counts.test);
111
+ named.forEach((count, at) => {
112
+ if (count > (inGraph[at] ?? 0)) {
113
+ reach[at] = "outside";
114
+ }
115
+ });
116
+ return { graph, fileOf, rootedBy, published, named, reach };
117
+ }
118
+
119
+ /** Whether one declaration is a declaration its file's export table names. */
120
+ function topLevelExport(facts: Facts, at: number, symbol: InventorySymbol): boolean {
121
+ const parent = facts.graph.parent[at] ?? OUTSIDE;
122
+ return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
123
+ }
124
+
125
+ function wordOf(symbol: InventorySymbol): string {
126
+ return WORDS[symbol.kind] ?? symbol.kind;
127
+ }
128
+
129
+ /** A finding of this family, with the visibility a narrowing would give its subject. */
130
+ interface NarrowingFinding extends Finding {
131
+ readonly details: { readonly narrowerVisibility?: "file" | "package" };
132
+ }
133
+
134
+ /** The visibility each narrowing code names, which only the narrowing codes carry. */
135
+ const NARROWER: Readonly<Record<string, "file" | "package">> = {
136
+ [REDUNDANT_EXPORT_KEYWORD]: "file",
137
+ [UNNECESSARY_EXPORT]: "package",
138
+ };
139
+
140
+ function findingOf(symbol: InventorySymbol, code: string, message: string): NarrowingFinding {
141
+ const { path, line, column } = symbol.position;
142
+ const narrower = Object.hasOwn(NARROWER, code) ? NARROWER[code] : undefined;
143
+ const endLine = Math.max(line, symbol.endLine);
144
+ return {
145
+ code,
146
+ position: { path, line, column, endLine },
147
+ symbol: {
148
+ ref: symbol.ref,
149
+ kind: symbol.kind,
150
+ name: symbol.name,
151
+ sizeLines: endLine - line + 1,
152
+ },
153
+ message,
154
+ details: narrower === undefined ? {} : { narrowerVisibility: narrower },
155
+ };
156
+ }
157
+
158
+ /**
159
+ * The narrowing finding about one live declaration, if one holds: an export of a file,
160
+ * no re-export and none the write-only kind reports, that a reference names and nothing
161
+ * enters by name. Every reference inside its own file makes it a redundant export
162
+ * keyword, where no manifest export reaches the file or the world is closed; references
163
+ * from other files of the package alone make a published one unnecessary when closed.
164
+ */
165
+ function narrowing(
166
+ facts: Facts,
167
+ at: number,
168
+ symbol: InventorySymbol,
169
+ world: { readonly closed: boolean; readonly writeOnly: ReadonlySet<string> },
170
+ ): Finding | undefined {
171
+ const { closed } = world;
172
+ const file = facts.fileOf[at] ?? OUTSIDE;
173
+ if (
174
+ !topLevelExport(facts, at, symbol) ||
175
+ world.writeOnly.has(symbol.ref) ||
176
+ symbol.kind === "export-alias" ||
177
+ entersByName(facts.rootedBy[at]) ||
178
+ entersByName(facts.rootedBy[file]) ||
179
+ (facts.named[at] ?? 0) === 0
180
+ ) {
181
+ return undefined;
182
+ }
183
+ const word = wordOf(symbol);
184
+ switch (facts.reach[at]) {
185
+ case "file":
186
+ if (facts.published[file] === true && !closed) {
187
+ return undefined;
188
+ }
189
+ return findingOf(
190
+ symbol,
191
+ REDUNDANT_EXPORT_KEYWORD,
192
+ `exported ${word} is referenced only inside the file that declares it`,
193
+ );
194
+ case "package":
195
+ if (!closed || !publishes(facts.rootedBy[at])) {
196
+ return undefined;
197
+ }
198
+ return findingOf(
199
+ symbol,
200
+ UNNECESSARY_EXPORT,
201
+ `exported ${word} is referenced only inside the package that declares it`,
202
+ );
203
+ default:
204
+ return undefined;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * The unreachable-export finding about one dead declaration, if one holds: an unused
210
+ * export no code outside the target can import, because the manifest declares
211
+ * `exports` and no root reaches it. Each arm yields to a more specific kind, so one
212
+ * declaration is reported once: a test whose every subject is dead, an interface, a
213
+ * declaration only a test file references, and a deprecated one.
214
+ */
215
+ function unreachable(
216
+ facts: Facts,
217
+ at: number,
218
+ symbol: InventorySymbol,
219
+ candidate: MatrixCandidate,
220
+ input: EmitterInput,
221
+ ): Finding | undefined {
222
+ if (
223
+ !input.boundary.encapsulated ||
224
+ !topLevelExport(facts, at, symbol) ||
225
+ (facts.rootedBy[at]?.size ?? 0) > 0 ||
226
+ candidate.testOfDeadCode ||
227
+ symbol.kind === "interface" ||
228
+ (candidate.productionRefs === 0 && candidate.testRefs > 0) ||
229
+ (candidate.productionRefs === 0 && input.deprecated.has(symbol.id))
230
+ ) {
231
+ return undefined;
232
+ }
233
+ const found =
234
+ candidate.relation === "reachability"
235
+ ? "is referenced only from declarations that are themselves dead"
236
+ : "has no reference in the target";
237
+ return findingOf(
238
+ symbol,
239
+ UNREACHABLE_EXPORT,
240
+ `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`,
241
+ );
242
+ }
243
+
244
+ /** Two strings ordered bytewise. */
245
+ function compare(a: string, b: string): number {
246
+ if (a === b) {
247
+ return 0;
248
+ }
249
+ return a < b ? -1 : 1;
250
+ }
251
+
252
+ /**
253
+ * The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
254
+ *
255
+ * A finding about a symbol a declared cross-language edge names is pending instead of
256
+ * reported, once per side naming it: the edge counts as a reference from outside the
257
+ * file and the package, and whether it is a use is decided on the side in the other
258
+ * language.
259
+ */
260
+ export function narrowings(input: EmitterInput): Narrowings {
261
+ const { boundary } = input;
262
+ const facts = factsOf(input.swept.matrix.union);
263
+ const { declared, loaded } = boundary.consumers;
264
+ const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
265
+ const world = { closed, writeOnly: writeOnlyDeclarations(input) };
266
+ const candidates = new Map(
267
+ input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
268
+ );
269
+
270
+ const findings: Finding[] = [];
271
+ const pending: PendingFinding[] = [];
272
+ facts.graph.symbols.forEach((symbol, at) => {
273
+ const candidate = candidates.get(symbol.id);
274
+ const found =
275
+ candidate === undefined
276
+ ? narrowing(facts, at, symbol, world)
277
+ : unreachable(facts, at, symbol, candidate, input);
278
+ if (found === undefined) {
279
+ return;
280
+ }
281
+ const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
282
+ if (named.length === 0) {
283
+ findings.push(found);
284
+ return;
285
+ }
286
+ for (const side of named) {
287
+ pending.push({ ...side, finding: found });
288
+ }
289
+ });
290
+ pending.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
291
+ return { findings, pending };
292
+ }
293
+
294
+ /**
295
+ * The declarations the unreachable-export kind reports, whether or not a declared edge
296
+ * holds the finding pending: the unused-declaration kinds yield each to it.
297
+ */
298
+ export function unreachableExports(input: EmitterInput): ReadonlySet<string> {
299
+ const facts = factsOf(input.swept.matrix.union);
300
+ const found = new Set<string>();
301
+ for (const candidate of input.swept.sweep.candidates) {
302
+ const at = facts.graph.at(candidate.id);
303
+ const symbol = facts.graph.symbols[at];
304
+ if (symbol !== undefined && unreachable(facts, at, symbol, candidate, input) !== undefined) {
305
+ found.add(symbol.id);
306
+ }
307
+ }
308
+ return found;
309
+ }
310
+
311
+ /**
312
+ * The findings of the visibility-narrowing family, `DS1100` to `DS1199`. A finding a
313
+ * declared edge holds pending belongs in that edge's evaluation, which this analyzer
314
+ * does not publish yet, so a run that holds one is refused rather than reported
315
+ * without it.
316
+ */
317
+ export const visibilityNarrowing: Emitter = (input) => {
318
+ const { findings, pending } = narrowings(input);
319
+ if (pending.length > 0) {
320
+ throw new Error(
321
+ `${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`,
322
+ );
323
+ }
324
+ return findings;
325
+ };
@@ -0,0 +1,286 @@
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
+
9
+ import {
10
+ isClassDeclaration,
11
+ isHeritageClause,
12
+ isInterfaceDeclaration,
13
+ isMethodDeclaration,
14
+ SyntaxKind,
15
+ type Node,
16
+ } from "@typescript/native/unstable/ast";
17
+ import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
18
+ import { aliasChains } from "./alias-chain.ts";
19
+ import { nodeKey, type Inventory } from "./inventory.ts";
20
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
21
+ import type { ProjectView } from "./session.ts";
22
+
23
+ /** What one run knows about how its interfaces are implemented. */
24
+ export interface Implementations {
25
+ /** Per interface declaration, the classes that implement it, each by its identifier. */
26
+ readonly classes: ReadonlyMap<string, ReadonlySet<string>>;
27
+ /**
28
+ * Per method an interface declares, each class that implements it and whether that
29
+ * class's implementation is a method whose body holds no statement.
30
+ */
31
+ readonly bodies: ReadonlyMap<string, ReadonlyMap<string, boolean>>;
32
+ }
33
+
34
+ /** One interface or class the inventory holds, by its declaration. */
35
+ interface Declared {
36
+ readonly id: string;
37
+ readonly nameNode: Node;
38
+ readonly generic: boolean;
39
+ }
40
+
41
+ /** One entry of a class's `implements` clause. */
42
+ interface Clause {
43
+ readonly owner: Declared;
44
+ readonly entry: Node;
45
+ }
46
+
47
+ /** One interface type, the declarations it has, and the members it carries. */
48
+ interface InterfaceAt {
49
+ readonly ids: string[];
50
+ readonly type: Type;
51
+ generic: boolean;
52
+ members?: readonly TSSymbol[];
53
+ }
54
+
55
+ /** One class, its instance type, and the members that type carries by name. */
56
+ interface ClassAt {
57
+ readonly owner: Declared;
58
+ readonly type: Type;
59
+ members?: ReadonlyMap<string, TSSymbol>;
60
+ }
61
+
62
+ /** The interfaces, the classes and the `implements` entries of one project's own files. */
63
+ function walk<Brand>(
64
+ project: ProjectView<Brand>,
65
+ held: Inventory,
66
+ ): {
67
+ readonly interfaces: readonly Declared[];
68
+ readonly classes: readonly Declared[];
69
+ readonly clauses: readonly Clause[];
70
+ } {
71
+ const interfaces: Declared[] = [];
72
+ const classes: Declared[] = [];
73
+ const clauses: Clause[] = [];
74
+ for (const file of project.ownSourceFiles()) {
75
+ const visit = (node: Node): void => {
76
+ if (isInterfaceDeclaration(node) || isClassDeclaration(node)) {
77
+ const id = held.declarations.get(nodeKey(file, node));
78
+ const name = node.name;
79
+ if (id !== undefined && name !== undefined) {
80
+ const declared = { id, nameNode: name, generic: node.typeParameters !== undefined };
81
+ if (isInterfaceDeclaration(node)) {
82
+ interfaces.push(declared);
83
+ } else {
84
+ classes.push(declared);
85
+ for (const clause of node.heritageClauses ?? []) {
86
+ if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
87
+ clauses.push(...clause.types.map((entry) => ({ owner: declared, entry })));
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ node.forEachChild(visit);
94
+ };
95
+ file.forEachChild(visit);
96
+ }
97
+ return { interfaces, classes, clauses };
98
+ }
99
+
100
+ /** The type at each node, asked in capped batches. */
101
+ function typesAt<Brand>(project: ProjectView<Brand>, nodes: readonly Node[]): Map<Node, Type> {
102
+ const typeAt = new Map<Node, Type>();
103
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
104
+ const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
105
+ project.checker.getTypeAtLocation(run).forEach((type, index) => {
106
+ const node = run[index];
107
+ if (type !== undefined && node !== undefined) {
108
+ typeAt.set(node, type);
109
+ }
110
+ });
111
+ }
112
+ return typeAt;
113
+ }
114
+
115
+ /** Whether one class member is a method whose every written body holds no statement. */
116
+ function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
117
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
118
+ let bodies = 0;
119
+ for (const handle of member.declarations) {
120
+ // A body outside the target is one this analysis cannot read, so it is not empty.
121
+ if (!own.has(handle.path)) {
122
+ return false;
123
+ }
124
+ const node = project.declarationAt(handle)?.node;
125
+ if (node === undefined || !isMethodDeclaration(node)) {
126
+ return false;
127
+ }
128
+ if (node.body !== undefined) {
129
+ if (node.body.statements.length > 0) {
130
+ return false;
131
+ }
132
+ bodies += 1;
133
+ }
134
+ }
135
+ return bodies > 0;
136
+ }
137
+
138
+ /** How one project's interfaces are implemented by its classes. */
139
+ export function implementations<Brand>(
140
+ project: ProjectView<Brand>,
141
+ held: Inventory,
142
+ ): Implementations {
143
+ const classes = new Map<string, Set<string>>();
144
+ const bodies = new Map<string, Map<string, boolean>>();
145
+ const walked = walk(project, held);
146
+ if (walked.interfaces.length === 0 || walked.classes.length === 0) {
147
+ return { classes, bodies };
148
+ }
149
+
150
+ const checker = project.checker;
151
+ const typeAt = typesAt(project, [
152
+ ...walked.interfaces.map((one) => one.nameNode),
153
+ ...walked.classes.map((one) => one.nameNode),
154
+ ...walked.clauses.map((clause) => clause.entry),
155
+ ]);
156
+
157
+ // Two declarations of one interface merge into one type.
158
+ const byType = new Map<number, InterfaceAt>();
159
+ for (const declared of walked.interfaces) {
160
+ const type = typeAt.get(declared.nameNode);
161
+ if (type === undefined) {
162
+ continue;
163
+ }
164
+ const merged = byType.get(type.id);
165
+ if (merged === undefined) {
166
+ byType.set(type.id, { ids: [declared.id], type, generic: declared.generic });
167
+ } else {
168
+ merged.ids.push(declared.id);
169
+ merged.generic ||= declared.generic;
170
+ }
171
+ }
172
+ const classAt = new Map<string, ClassAt>();
173
+ for (const owner of walked.classes) {
174
+ const type = typeAt.get(owner.nameNode);
175
+ if (type !== undefined) {
176
+ classAt.set(owner.id, { owner, type });
177
+ }
178
+ }
179
+
180
+ const membersOf = (at: InterfaceAt): readonly TSSymbol[] =>
181
+ (at.members ??= checker.getPropertiesOfType(at.type));
182
+ const classMembers = (at: ClassAt): ReadonlyMap<string, TSSymbol> =>
183
+ (at.members ??= new Map(
184
+ checker.getPropertiesOfType(at.type).map((member) => [member.escapedName, member]),
185
+ ));
186
+
187
+ const methodIds = new Set(
188
+ held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id),
189
+ );
190
+ const chains = aliasChains(project, held);
191
+ const record = (at: InterfaceAt, implementer: ClassAt): void => {
192
+ for (const id of at.ids) {
193
+ let set = classes.get(id);
194
+ if (set === undefined) {
195
+ set = new Set();
196
+ classes.set(id, set);
197
+ }
198
+ set.add(implementer.owner.id);
199
+ }
200
+ const written = classMembers(implementer);
201
+ for (const member of membersOf(at)) {
202
+ const answering = written.get(member.escapedName);
203
+ if (answering === undefined) {
204
+ continue;
205
+ }
206
+ const empty = emptyImplementation(project, answering);
207
+ for (const id of chains.declarationsOf(member)) {
208
+ if (!methodIds.has(id)) {
209
+ continue;
210
+ }
211
+ let byClass = bodies.get(id);
212
+ if (byClass === undefined) {
213
+ byClass = new Map();
214
+ bodies.set(id, byClass);
215
+ }
216
+ byClass.set(implementer.owner.id, empty);
217
+ }
218
+ }
219
+ };
220
+
221
+ const recorded = new Set<string>();
222
+ const pair = (at: InterfaceAt, implementer: ClassAt): void => {
223
+ const key = `${String(at.type.id)}:${implementer.owner.id}`;
224
+ if (!recorded.has(key)) {
225
+ recorded.add(key);
226
+ record(at, implementer);
227
+ }
228
+ };
229
+
230
+ for (const clause of walked.clauses) {
231
+ const entry = typeAt.get(clause.entry);
232
+ const implementer = classAt.get(clause.owner.id);
233
+ if (entry === undefined || implementer === undefined) {
234
+ continue;
235
+ }
236
+ const at = byType.get(entry.isTypeReference() ? entry.getTarget().id : entry.id);
237
+ if (at !== undefined) {
238
+ pair(at, implementer);
239
+ }
240
+ }
241
+
242
+ for (const at of byType.values()) {
243
+ const members = at.generic ? [] : membersOf(at);
244
+ if (members.length === 0) {
245
+ continue;
246
+ }
247
+ const required = members.filter((member) => (member.flags & SymbolFlags.Optional) === 0);
248
+ for (const implementer of classAt.values()) {
249
+ if (implementer.owner.generic) {
250
+ continue;
251
+ }
252
+ const written = classMembers(implementer);
253
+ if (
254
+ required.every((member) => written.has(member.escapedName)) &&
255
+ members.some((member) => written.has(member.escapedName)) &&
256
+ checker.isTypeAssignableTo(implementer.type, at.type)
257
+ ) {
258
+ pair(at, implementer);
259
+ }
260
+ }
261
+ }
262
+ return { classes, bodies };
263
+ }
264
+
265
+ /** Every project's answers as one run's, each implementation once. */
266
+ export function mergeImplementations(per: readonly Implementations[]): Implementations {
267
+ const classes = new Map<string, Set<string>>();
268
+ const bodies = new Map<string, Map<string, boolean>>();
269
+ for (const one of per) {
270
+ for (const [id, held] of one.classes) {
271
+ const set = classes.get(id) ?? new Set();
272
+ classes.set(id, set);
273
+ for (const owner of held) {
274
+ set.add(owner);
275
+ }
276
+ }
277
+ for (const [id, held] of one.bodies) {
278
+ const byClass = bodies.get(id) ?? new Map<string, boolean>();
279
+ bodies.set(id, byClass);
280
+ for (const [owner, empty] of held) {
281
+ byClass.set(owner, (byClass.get(owner) ?? true) && empty);
282
+ }
283
+ }
284
+ }
285
+ return { classes, bodies };
286
+ }
package/src/inventory.ts CHANGED
@@ -224,7 +224,7 @@ interface Named {
224
224
  * One directory is read at most once, because a package holds many files and a
225
225
  * manifest read is a filesystem call.
226
226
  */
227
- function packages(host: Host, root: string): (path: string) => Module {
227
+ export function packageScope(host: Host, root: string): (path: string) => Module {
228
228
  const named = new Map<string, Named | undefined>();
229
229
 
230
230
  const manifestName = (dir: string): string | undefined => {
@@ -480,7 +480,7 @@ export function inventory<Brand>(
480
480
  ): Inventory {
481
481
  const files = project.ownSourceFiles();
482
482
  const ownFiles = new Set(files.map((file) => file.fileName));
483
- const moduleOf = packages(host, targetRoot);
483
+ const moduleOf = packageScope(host, targetRoot);
484
484
 
485
485
  const building: Building[] = [];
486
486
  const byNode = new Map<string, Building>();
package/src/resolve.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  const FLAGS_LABEL = "the command-line flags";
36
36
 
37
37
  /** The length of a severity key naming a whole family: the prefix and two digits. */
38
- const FAMILY_KEY_LENGTH = 4;
38
+ export const FAMILY_KEY_LENGTH = 4;
39
39
 
40
40
  const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
41
41
  const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;