@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
@@ -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/kinds.ts CHANGED
@@ -1,53 +1,87 @@
1
+ import type { Confidence, Severity } from "./config.ts";
2
+
3
+ /** What a mechanical edit may do with a finding of one kind. */
4
+ export type Fixability = "deletable" | "narrowable" | "manual" | "none";
5
+
6
+ /** One live row of the issue-kind vocabulary, as far as an analyzer reads it. */
7
+ export interface KindRow {
8
+ readonly code: string;
9
+ /** The kind's name, which a finding carries as its `kind`. */
10
+ readonly name: string;
11
+ /** The severity a finding carries where no severity key names its code or its family. */
12
+ readonly defaultSeverity: Severity;
13
+ /** The highest confidence a finding of the kind carries. */
14
+ readonly maxClass: Confidence;
15
+ readonly fixability: Fixability;
16
+ /**
17
+ * Whether the Contract fixes the kind on at its default severity, so a severity key
18
+ * naming the code, or a family prefix whose range holds it, is an unimplemented key.
19
+ */
20
+ readonly fixed: boolean;
21
+ }
22
+
1
23
  /**
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.
24
+ * Every live row of the issue-kind vocabulary, in ascending code order, written out
25
+ * rather than read at run time, because a product that read the Contract while running
26
+ * would need it installed beside itself. A test compares this table against the
27
+ * Contract release the analyzer is written against.
11
28
  */
12
- export const FIXED_SEVERITY_CODES: readonly string[] = ["DS1703", "DS1704"];
29
+ const ROWS: readonly KindRow[] = [
30
+ row("DS1001", "unused-exported", "deny", "deletable"),
31
+ row("DS1002", "unused-unexported", "deny", "deletable"),
32
+ row("DS1003", "unused-member", "deny", "deletable"),
33
+ row("DS1004", "test-only-use", "deny", "deletable"),
34
+ row("DS1005", "test-of-dead-code", "deny", "deletable"),
35
+ row("DS1006", "deprecated-unused", "deny", "deletable"),
36
+ row("DS1101", "unnecessary-export", "warn", "narrowable"),
37
+ row("DS1102", "unnecessary-exposure", "warn", "narrowable"),
38
+ row("DS1103", "unreachable-export", "deny", "deletable"),
39
+ row("DS1104", "redundant-export-keyword", "warn", "narrowable"),
40
+ row("DS1201", "unused-interface", "deny", "deletable"),
41
+ row("DS1203", "uncalled-interface-method", "warn", "manual"),
42
+ row("DS1204", "unused-satisfaction-assertion", "deny", "deletable"),
43
+ row("DS1301", "write-only-symbol", "deny", "deletable"),
44
+ row("DS1302", "unused-enum-member", "deny", "deletable"),
45
+ row("DS1303", "unused-type-parameter", "deny", "deletable"),
46
+ row("DS1501", "file-never-built", "deny", "deletable"),
47
+ row("DS1502", "file-never-imported", "deny", "deletable"),
48
+ row("DS1601", "unused-dependency", "deny", "manual"),
49
+ row("DS1605", "unused-module-directive", "warn", "deletable"),
50
+ row("DS1701", "suppression-without-reason", "deny", "none"),
51
+ row("DS1702", "unscoped-ignore-entry", "deny", "none"),
52
+ { ...row("DS1703", "stale-suppression", "deny", "none"), fixed: true },
53
+ { ...row("DS1704", "unmatched-root", "deny", "none"), fixed: true },
54
+ row("DS1705", "stale-cross-language-edge", "deny", "none"),
55
+ row("DS1801", "unused-parameter", "warn", "manual"),
56
+ row("DS1802", "unused-receiver", "warn", "deletable"),
57
+ row("DS1803", "unused-result", "warn", "manual"),
58
+ row("DS1805", "unreachable-statement", "deny", "deletable"),
59
+ row("DS1807", "dead-store", "deny", "deletable"),
60
+ row("DS1809", "unreachable-case", "deny", "deletable"),
61
+ ];
62
+
63
+ /** A row at the `certain` ceiling whose severity a configuration may set. */
64
+ function row(
65
+ code: string,
66
+ name: string,
67
+ defaultSeverity: Severity,
68
+ fixability: Fixability,
69
+ ): KindRow {
70
+ return { code, name, defaultSeverity, maxClass: "certain", fixability, fixed: false };
71
+ }
72
+
73
+ /** Every live row of the issue-kind vocabulary, by its code, in ascending code order. */
74
+ export const KINDS: ReadonlyMap<string, KindRow> = new Map(ROWS.map((one) => [one.code, one]));
13
75
 
14
76
  /**
15
77
  * The code of every live issue kind, in ascending order. A severity key names one of
16
78
  * them or a family prefix whose range holds one; a key naming a retired code, an
17
79
  * unassigned code or a range holding no live kind is an unimplemented key, so every
18
- * severity a configuration sets is one some analyzer reports under. A test compares
19
- * this list against the Contract release the analyzer is written against.
80
+ * severity a configuration sets is one some analyzer reports under.
20
81
  */
21
- export const LIVE_CODES: readonly string[] = [
22
- "DS1001",
23
- "DS1002",
24
- "DS1003",
25
- "DS1004",
26
- "DS1005",
27
- "DS1006",
28
- "DS1101",
29
- "DS1102",
30
- "DS1103",
31
- "DS1104",
32
- "DS1201",
33
- "DS1203",
34
- "DS1204",
35
- "DS1301",
36
- "DS1302",
37
- "DS1303",
38
- "DS1501",
39
- "DS1502",
40
- "DS1601",
41
- "DS1605",
42
- "DS1701",
43
- "DS1702",
44
- "DS1703",
45
- "DS1704",
46
- "DS1705",
47
- "DS1801",
48
- "DS1802",
49
- "DS1803",
50
- "DS1805",
51
- "DS1807",
52
- "DS1809",
53
- ];
82
+ export const LIVE_CODES: readonly string[] = ROWS.map((one) => one.code);
83
+
84
+ /** The issue-kind codes whose severity the Contract fixes, in ascending order. */
85
+ export const FIXED_SEVERITY_CODES: readonly string[] = ROWS.filter((one) => one.fixed).map(
86
+ (one) => one.code,
87
+ );
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;
package/src/stores.ts ADDED
@@ -0,0 +1,77 @@
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
+
7
+ import {
8
+ isGetAccessorDeclaration,
9
+ isSetAccessorDeclaration,
10
+ type Node,
11
+ } from "@typescript/native/unstable/ast";
12
+ import { nodeKey, type Inventory } from "./inventory.ts";
13
+ import type { Reference } from "./references.ts";
14
+ import type { ProjectView } from "./session.ts";
15
+ import type { Exemption, Mode } from "./sweep.ts";
16
+
17
+ /** One run's stores and reads, over every configuration. */
18
+ export interface Stores {
19
+ /**
20
+ * Every reference the run's mode counts, each configuration's in the run's order, so
21
+ * one written at a position several configurations compile appears once per
22
+ * configuration. A production mode counts none a test file made.
23
+ */
24
+ readonly references: readonly Reference[];
25
+ /**
26
+ * The class members declared by a getter or a setter. A store into one runs the
27
+ * setter, which is code rather than state.
28
+ */
29
+ readonly accessors: ReadonlySet<string>;
30
+ /**
31
+ * The declarations an exemption record names, whether or not the record held back
32
+ * anything dead: a mechanism the analysis cannot see uses each of them.
33
+ */
34
+ readonly exempt: ReadonlySet<string>;
35
+ }
36
+
37
+ /** What one configuration contributes to the run's stores. */
38
+ export interface ConfigurationStores {
39
+ readonly references: readonly Reference[];
40
+ readonly accessors: readonly string[];
41
+ }
42
+
43
+ /** The class members one project's own files declare by a getter or a setter. */
44
+ export function accessorsOf<Brand>(
45
+ project: ProjectView<Brand>,
46
+ held: Inventory,
47
+ ): readonly string[] {
48
+ const found: string[] = [];
49
+ for (const file of project.ownSourceFiles()) {
50
+ const visit = (node: Node): void => {
51
+ if (isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) {
52
+ const id = held.declarations.get(nodeKey(file, node));
53
+ if (id !== undefined) {
54
+ found.push(id);
55
+ }
56
+ }
57
+ node.forEachChild(visit);
58
+ };
59
+ file.forEachChild(visit);
60
+ }
61
+ return found;
62
+ }
63
+
64
+ /** The run's stores, from what each configuration contributed, the records and the mode. */
65
+ export function storesOf(
66
+ per: readonly ConfigurationStores[],
67
+ exempt: readonly Exemption[],
68
+ mode: Mode,
69
+ ): Stores {
70
+ return {
71
+ references: per.flatMap((one) =>
72
+ one.references.filter((reference) => !mode.production || !reference.test),
73
+ ),
74
+ accessors: new Set(per.flatMap((one) => one.accessors)),
75
+ exempt: new Set(exempt.map((record) => record.id)),
76
+ };
77
+ }