@cplieger/deadset-ts 3.8.0 → 3.9.1

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.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * The members a destructuring reads. An object pattern reads each property it names
3
+ * from the value it destructures, so the property's name in the pattern is a read of
4
+ * the member the value's type declares under that name, whether the pattern binds it
5
+ * to a local of the same name, renames it, gives it a default or nests a pattern under
6
+ * it. A rest element names no property: what it copies is read where the copy is.
7
+ */
8
+
9
+ import {
10
+ isArrayLiteralExpression,
11
+ isBinaryExpression,
12
+ isForOfStatement,
13
+ isIdentifier,
14
+ isNoSubstitutionTemplateLiteral,
15
+ isNumericLiteral,
16
+ isObjectBindingPattern,
17
+ isObjectLiteralExpression,
18
+ isParameterDeclaration,
19
+ isParenthesizedExpression,
20
+ isPropertyAssignment,
21
+ isShorthandPropertyAssignment,
22
+ isStringLiteral,
23
+ SyntaxKind,
24
+ type BinaryExpression,
25
+ type Node,
26
+ } from "@typescript/native/unstable/ast";
27
+ import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
28
+ import type { ProjectView } from "./session.ts";
29
+
30
+ /** One property one object pattern reads. */
31
+ export interface PropertyRead {
32
+ /** The pattern, whose destructured value declares the property. */
33
+ readonly pattern: Node;
34
+ /** The property's name as the pattern writes it, which is where the read is written. */
35
+ readonly key: Node;
36
+ /** The property's name. */
37
+ readonly name: string;
38
+ }
39
+
40
+ /** The text a property name spells, or `undefined` for a computed name. */
41
+ function nameText(node: Node | undefined): string | undefined {
42
+ if (node === undefined) {
43
+ return undefined;
44
+ }
45
+ if (
46
+ isIdentifier(node) ||
47
+ isStringLiteral(node) ||
48
+ isNoSubstitutionTemplateLiteral(node) ||
49
+ isNumericLiteral(node)
50
+ ) {
51
+ return node.text;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ /** The node one expression is written inside, through however many parentheses enclose it. */
57
+ function container(node: Node): { readonly child: Node; readonly parent: Node } {
58
+ let child = node;
59
+ while (isParenthesizedExpression(child.parent)) {
60
+ child = child.parent;
61
+ }
62
+ return { child, parent: child.parent };
63
+ }
64
+
65
+ /** The plain assignment one node is the left side of, if it is one. */
66
+ function assignmentOf(node: Node, parent: Node): BinaryExpression | undefined {
67
+ return isBinaryExpression(parent) &&
68
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
69
+ parent.left === node
70
+ ? parent
71
+ : undefined;
72
+ }
73
+
74
+ /**
75
+ * Whether an object or array literal is a destructuring target rather than a value: the
76
+ * left side of an assignment, the target of a `for…of`, or an element of a literal that
77
+ * is one, with or without a default.
78
+ */
79
+ function isDestructuringTarget(literal: Node): boolean {
80
+ const { child, parent } = container(literal);
81
+ if (assignmentOf(child, parent) !== undefined) {
82
+ return true;
83
+ }
84
+ if (isForOfStatement(parent)) {
85
+ return parent.initializer === child;
86
+ }
87
+ return isElement(child, parent);
88
+ }
89
+
90
+ /** Whether one node is an element of a destructuring target: a property's value, or an array element. */
91
+ function isElement(child: Node, parent: Node): boolean {
92
+ if (isPropertyAssignment(parent)) {
93
+ return parent.initializer === child && isDestructuringTarget(parent.parent);
94
+ }
95
+ return isArrayLiteralExpression(parent) && isDestructuringTarget(parent);
96
+ }
97
+
98
+ /**
99
+ * Whether an object binding pattern runs: a pattern in the parameter list of a signature
100
+ * with no body, a function type or an overload among them, binds nothing at runtime.
101
+ */
102
+ function binds(pattern: Node): boolean {
103
+ let at = pattern.parent;
104
+ while (at.kind === SyntaxKind.BindingElement) {
105
+ at = at.parent.parent;
106
+ }
107
+ if (!isParameterDeclaration(at)) {
108
+ return true;
109
+ }
110
+ return (at.parent as { readonly body?: Node }).body !== undefined;
111
+ }
112
+
113
+ /** The properties one node reads, where it is an object pattern; none otherwise. */
114
+ export function propertyReadsOf(node: Node): readonly PropertyRead[] {
115
+ const reads: PropertyRead[] = [];
116
+ if (isObjectBindingPattern(node)) {
117
+ if (!binds(node)) {
118
+ return reads;
119
+ }
120
+ for (const element of node.elements) {
121
+ const key = element.propertyName ?? element.name;
122
+ const name = nameText(key);
123
+ if (element.dotDotDotToken === undefined && key !== undefined && name !== undefined) {
124
+ reads.push({ pattern: node, key, name });
125
+ }
126
+ }
127
+ return reads;
128
+ }
129
+ if (isObjectLiteralExpression(node) && isDestructuringTarget(node)) {
130
+ for (const property of node.properties) {
131
+ if (isShorthandPropertyAssignment(property) || isPropertyAssignment(property)) {
132
+ const name = nameText(property.name);
133
+ if (name !== undefined) {
134
+ reads.push({ pattern: node, key: property.name, name });
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return reads;
140
+ }
141
+
142
+ /** One step from a destructured value to the value a nested target destructures. */
143
+ type Step =
144
+ | { readonly kind: "property"; readonly name: string }
145
+ | { readonly kind: "element"; readonly index: number }
146
+ | { readonly kind: "iterated" };
147
+
148
+ /** Where an assignment target's value comes from: an expression, and the steps into it. */
149
+ interface Source {
150
+ readonly expression: Node;
151
+ readonly steps: readonly Step[];
152
+ }
153
+
154
+ /**
155
+ * Where the value one destructuring target receives comes from. The checker answers for
156
+ * a binding pattern's type itself, so this is asked of an assignment target only, whose
157
+ * own type is the literal's rather than the assigned value's. A default does not move
158
+ * the source: `{ a: { b } = fallback }` reads `b` from `a`. A target inside a spread
159
+ * takes the rest of a value and has no source here.
160
+ */
161
+ function sourceOf(target: Node): Source | undefined {
162
+ const steps: Step[] = [];
163
+ let at = target;
164
+ for (;;) {
165
+ const { child, parent } = container(at);
166
+ const assignment = assignmentOf(child, parent);
167
+ if (assignment !== undefined) {
168
+ const outer = container(assignment);
169
+ if (!isElement(outer.child, outer.parent)) {
170
+ return { expression: assignment.right, steps: steps.reverse() };
171
+ }
172
+ at = assignment;
173
+ continue;
174
+ }
175
+ if (isForOfStatement(parent) && parent.initializer === child) {
176
+ steps.push({ kind: "iterated" });
177
+ return { expression: parent.expression, steps: steps.reverse() };
178
+ }
179
+ if (isPropertyAssignment(parent) && parent.initializer === child) {
180
+ const name = nameText(parent.name);
181
+ if (name === undefined) {
182
+ return undefined;
183
+ }
184
+ steps.push({ kind: "property", name });
185
+ at = parent.parent;
186
+ continue;
187
+ }
188
+ if (isArrayLiteralExpression(parent)) {
189
+ steps.push({
190
+ kind: "element",
191
+ index: parent.elements.findIndex((element) => element === child),
192
+ });
193
+ at = parent;
194
+ continue;
195
+ }
196
+ return undefined;
197
+ }
198
+ }
199
+
200
+ /** What resolving one project's property reads cost at the client boundary. */
201
+ interface DestructuringCost {
202
+ /** Batched type lookups, one per capped run of a file's patterns and assigned values. */
203
+ readonly patternBatches: number;
204
+ /**
205
+ * Per-type lookups: one property table per distinct destructured type, and each step
206
+ * an assignment target takes into its assigned value.
207
+ */
208
+ readonly patternLookups: number;
209
+ }
210
+
211
+ /** Resolves property reads to the symbols the destructured values' types declare. */
212
+ interface Destructuring {
213
+ /**
214
+ * The property symbol each read names, in the order given, `undefined` where the
215
+ * destructured value's type declares no property of that name or is not known.
216
+ */
217
+ resolve(reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[];
218
+ readonly cost: DestructuringCost;
219
+ }
220
+
221
+ /**
222
+ * The resolution over one project, its batches capped at `cap`. Property tables are
223
+ * read once per type for the whole project, since one type is destructured in many
224
+ * places and a table answers every name of it.
225
+ */
226
+ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number): Destructuring {
227
+ const checker = project.checker;
228
+ const tables = new Map<number, ReadonlyMap<string, TSSymbol>>();
229
+ let patternBatches = 0;
230
+ let patternLookups = 0;
231
+
232
+ const typesAt = (nodes: readonly Node[]): (Type | undefined)[] => {
233
+ const types: (Type | undefined)[] = [];
234
+ for (let from = 0; from < nodes.length; from += cap) {
235
+ types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
236
+ patternBatches += 1;
237
+ }
238
+ return types;
239
+ };
240
+
241
+ const tableOf = (type: Type): ReadonlyMap<string, TSSymbol> => {
242
+ const known = tables.get(type.id);
243
+ if (known !== undefined) {
244
+ return known;
245
+ }
246
+ const table = new Map<string, TSSymbol>();
247
+ if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
248
+ patternLookups += 1;
249
+ for (const property of checker.getPropertiesOfType(type)) {
250
+ table.set(property.name, property);
251
+ }
252
+ }
253
+ tables.set(type.id, table);
254
+ return table;
255
+ };
256
+
257
+ const definedOf = (type: Type): Type | undefined => {
258
+ if ((type.flags & TypeFlags.Union) === 0) {
259
+ return type;
260
+ }
261
+ patternLookups += 1;
262
+ return checker.getNonNullableType(type);
263
+ };
264
+
265
+ /**
266
+ * The type one element of an array or tuple value has: a tuple's element at `index`,
267
+ * an array's element type at any index, and `undefined` for a tuple iterated whole.
268
+ */
269
+ const elementOf = (type: Type, index: number | undefined): Type | undefined => {
270
+ if (!type.isTypeReference()) {
271
+ return undefined;
272
+ }
273
+ patternLookups += 1;
274
+ if (checker.isTupleType(type)) {
275
+ if (index === undefined) {
276
+ return undefined;
277
+ }
278
+ patternLookups += 1;
279
+ return checker.getTypeArguments(type)[index];
280
+ }
281
+ patternLookups += 1;
282
+ if (!checker.isArrayType(type)) {
283
+ return undefined;
284
+ }
285
+ patternLookups += 1;
286
+ return checker.getTypeArguments(type)[0];
287
+ };
288
+
289
+ const step = (type: Type, next: Step): Type | undefined => {
290
+ const defined = definedOf(type);
291
+ if (defined === undefined) {
292
+ return undefined;
293
+ }
294
+ if (next.kind === "property") {
295
+ const property = tableOf(defined).get(next.name);
296
+ if (property === undefined) {
297
+ return undefined;
298
+ }
299
+ patternLookups += 1;
300
+ return checker.getTypeOfSymbol(property);
301
+ }
302
+ return elementOf(defined, next.kind === "element" ? next.index : undefined);
303
+ };
304
+
305
+ const resolve = (reads: readonly PropertyRead[]): readonly (TSSymbol | undefined)[] => {
306
+ const patterns = [...new Set(reads.map((read) => read.pattern))];
307
+ const sources = new Map<Node, Source | undefined>();
308
+ const asking = new Set<Node>();
309
+ for (const pattern of patterns) {
310
+ if (isObjectBindingPattern(pattern)) {
311
+ asking.add(pattern);
312
+ continue;
313
+ }
314
+ const source = sourceOf(pattern);
315
+ sources.set(pattern, source);
316
+ if (source !== undefined) {
317
+ asking.add(source.expression);
318
+ }
319
+ }
320
+ const answered = new Map<Node, Type>();
321
+ const asked = [...asking];
322
+ typesAt(asked).forEach((type, index) => {
323
+ const node = asked[index];
324
+ if (type !== undefined && node !== undefined) {
325
+ answered.set(node, type);
326
+ }
327
+ });
328
+ const patternTypes = new Map<Node, Type | undefined>();
329
+ for (const pattern of patterns) {
330
+ const source = sources.get(pattern);
331
+ if (source === undefined) {
332
+ patternTypes.set(pattern, answered.get(pattern));
333
+ continue;
334
+ }
335
+ let type = answered.get(source.expression);
336
+ for (const next of source.steps) {
337
+ type = type === undefined ? undefined : step(type, next);
338
+ }
339
+ patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
340
+ }
341
+ return reads.map((read) => {
342
+ const type = patternTypes.get(read.pattern);
343
+ return type === undefined ? undefined : tableOf(type).get(read.name);
344
+ });
345
+ };
346
+
347
+ return {
348
+ resolve,
349
+ get cost() {
350
+ return { patternBatches, patternLookups };
351
+ },
352
+ };
353
+ }
package/src/finding.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Confidence, Severity } from "./config.ts";
2
2
  import type { Fixability } from "./kinds.ts";
3
3
  import type { DependencySection } from "./ref.ts";
4
+ import type { Mechanism } from "./suppress.ts";
4
5
  import type { Relation } from "./sweep.ts";
5
6
 
6
7
  /**
@@ -58,6 +59,18 @@ export interface FindingDetails {
58
59
  readonly writePositions?: readonly FindingPosition[];
59
60
  /** On `DS1601`: the manifest section that declares the dependency. */
60
61
  readonly dependencyClass?: DependencySection;
62
+ /** On `DS1701` and `DS1702`: the document that holds the refused suppression. */
63
+ readonly mechanism?: Mechanism;
64
+ /** On `DS1701` and `DS1702`: the refused suppression as written, a member absent where it lacks one. */
65
+ readonly entry?: SuppressionEntry;
66
+ }
67
+
68
+ /** One suppression in the four-member form the ignore file and the baseline share. */
69
+ export interface SuppressionEntry {
70
+ readonly code: string;
71
+ readonly symbol?: string;
72
+ readonly path?: string;
73
+ readonly reason?: string;
61
74
  }
62
75
 
63
76
  /** One finding as its emitter states it. */
@@ -11,7 +11,7 @@ import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts"
11
11
  import { OUTSIDE } from "../graph.ts";
12
12
  import { KINDS } from "../kinds.ts";
13
13
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
14
- import type { Dials } from "../suppress.ts";
14
+ import { isRowSubject, type Dials } from "../suppress.ts";
15
15
  import type { EmitterInput } from "./emitter.ts";
16
16
 
17
17
  /** The classes ranked by the claim each makes, the strongest highest. */
@@ -75,7 +75,8 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
75
75
  * candidate's relation, counts, configurations and component; a live one carries no
76
76
  * relation and holds where it is declared; a file or a manifest row is `certain` and
77
77
  * holds in every configuration. A subject in no dead component gets one of its own,
78
- * numbered past the computed ones in finding order, so no two families mint one.
78
+ * numbered past the computed ones in finding order, so no two families mint one; nothing
79
+ * falls with it, so its deletion removes no line.
79
80
  */
80
81
  export function completed(
81
82
  input: EmitterInput,
@@ -94,7 +95,11 @@ export function completed(
94
95
  if (row === undefined) {
95
96
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
96
97
  }
97
- const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
98
+ // A row's reference may spell a declaration's, as an ignore entry naming one does, and
99
+ // a row is still never the declaration.
100
+ const at = isRowSubject(finding.symbol.kind)
101
+ ? OUTSIDE
102
+ : (positions.get(finding.symbol.ref) ?? OUTSIDE);
98
103
  const symbol = union.symbols[at];
99
104
  const judged = union.subject[at] === true ? symbol : undefined;
100
105
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
@@ -109,7 +114,7 @@ export function completed(
109
114
  id: componentId(minted),
110
115
  root: true,
111
116
  symbolCount: 1,
112
- deletableLines: finding.symbol.sizeLines,
117
+ deletableLines: 0,
113
118
  };
114
119
  }
115
120
  const counts =
@@ -1,5 +1,5 @@
1
1
  import { REPOSITORY_DOCUMENT } from "../config.ts";
2
- import type { Finding } from "../finding.ts";
2
+ import type { Finding, FindingDetails, SuppressionEntry } from "../finding.ts";
3
3
  import type { InventorySymbol } from "../inventory.ts";
4
4
  import type { Position } from "../position.ts";
5
5
  import {
@@ -73,12 +73,32 @@ function spelled(codes: readonly string[]): string {
73
73
  }
74
74
 
75
75
  /** A finding about one suppression, at the suppression's own site. */
76
- function aboutSuppression(code: string, ref: string, site: Position, message: string): Finding {
76
+ function aboutSuppression(
77
+ code: string,
78
+ ref: string,
79
+ site: Position,
80
+ message: string,
81
+ details: FindingDetails,
82
+ ): Finding {
77
83
  return {
78
84
  code,
79
85
  position: { ...site, endLine: site.line },
80
86
  symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
81
87
  message,
88
+ details,
89
+ };
90
+ }
91
+
92
+ /** Whether a reason holds a character other than whitespace. */
93
+ const STATED = /[^ \t\r\n]/u;
94
+
95
+ /** A refused suppression as written: a member it lacks is absent. */
96
+ function writtenEntry(refused: Refusal): SuppressionEntry {
97
+ return {
98
+ code: refused.code,
99
+ ...(refused.symbol === "" ? {} : { symbol: refused.symbol }),
100
+ ...(refused.path === "" ? {} : { path: refused.path }),
101
+ ...(STATED.test(refused.reason) ? { reason: refused.reason } : {}),
82
102
  };
83
103
  }
84
104
 
@@ -108,6 +128,7 @@ export function refusedSuppressions(
108
128
  ? "carries no reason"
109
129
  : "names a symbol and no path"
110
130
  }`,
131
+ { mechanism: refused.mechanism, entry: writtenEntry(refused) },
111
132
  ),
112
133
  );
113
134
  }
package/src/host.ts CHANGED
@@ -34,4 +34,9 @@ export interface Host {
34
34
  * says so at the one moment the value is asked for.
35
35
  */
36
36
  analyzerVersion(): string;
37
+ /**
38
+ * Writes one document whole, or a throw naming why it cannot. The write is atomic: a
39
+ * reader of the path sees the previous document or this one and never a part of it.
40
+ */
41
+ writeFile(path: string, text: string): void;
37
42
  }
package/src/references.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  } from "@typescript/native/unstable/ast";
30
30
  import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
31
31
  import { aliasChains } from "./alias-chain.ts";
32
+ import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
32
33
  import { globExpression } from "./glob.ts";
33
34
  import { nodeKey, type Inventory } from "./inventory.ts";
34
35
  import { byPosition, renderPosition, type Position } from "./position.ts";
@@ -71,8 +72,11 @@ export type Use = "read" | "write" | "evaluation" | "decorator";
71
72
  * the literal declares rather than to the declaration that name reads.
72
73
  * - `alias` is a step along an import or export chain, from one link to what it names.
73
74
  * - `syntax` is the tree alone, which says what a decorator is attached to.
75
+ * - `destructured` is the property a pattern names, looked up on the type of the value
76
+ * the pattern destructures.
74
77
  */
75
- export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax";
78
+ export type Resolution =
79
+ "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured";
76
80
 
77
81
  /** One use of one declaration by one declaration. */
78
82
  export interface Reference {
@@ -119,6 +123,16 @@ export interface ReferenceCost {
119
123
  readonly shorthandLookups: number;
120
124
  /** Per-symbol lookups that step from one alias to what it names, one per alias. */
121
125
  readonly aliasSteps: number;
126
+ /**
127
+ * Batched type lookups for the object patterns: per file, the patterns and the values
128
+ * assigned to destructuring targets, divided by the batch cap, rounded up.
129
+ */
130
+ readonly patternBatches: number;
131
+ /**
132
+ * Per-type lookups for the object patterns: one property table per distinct type a
133
+ * pattern destructures, and each step a nested assignment target takes into its value.
134
+ */
135
+ readonly patternLookups: number;
122
136
  }
123
137
 
124
138
  /** One project's references, in position order, and what reading them cost. */
@@ -126,8 +140,6 @@ export interface References {
126
140
  /** The compiler configuration the project was opened from. */
127
141
  readonly configFile: string;
128
142
  readonly references: readonly Reference[];
129
- /** The rules that classified files as test files, ordered by rule. */
130
- readonly testFileRules: readonly TestFileRule[];
131
143
  /**
132
144
  * Every file the rules classified as a test file, by its path below the target root,
133
145
  * ascending. A declaration such a file holds is a test declaration, by the same
@@ -147,7 +159,7 @@ export interface ReferenceOptions {
147
159
  readonly batchCap?: number;
148
160
  /**
149
161
  * Glob patterns, relative to the target root, naming the files that are test files.
150
- * Each pattern is a rule of its own and reports the files it matched.
162
+ * Each pattern is a rule of its own, which {@link testFileRulesOf} counts.
151
163
  */
152
164
  readonly testFiles: readonly string[];
153
165
  }
@@ -328,9 +340,15 @@ interface Decorated {
328
340
  readonly declaration: string;
329
341
  }
330
342
 
343
+ /** One property a pattern reads, and the declaration the pattern is written inside. */
344
+ interface Read extends PropertyRead {
345
+ readonly from: string;
346
+ }
347
+
331
348
  /** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
332
349
  interface FileSites {
333
350
  readonly uses: readonly Site[];
351
+ readonly reads: readonly Read[];
334
352
  readonly links: readonly Link[];
335
353
  readonly evaluations: readonly Evaluation[];
336
354
  readonly decorated: readonly Decorated[];
@@ -427,6 +445,7 @@ function sitesOf(
427
445
  fileId: string,
428
446
  ): FileSites {
429
447
  const found: Site[] = [];
448
+ const reads: Read[] = [];
430
449
  const links: Link[] = [];
431
450
  const evaluations: Evaluation[] = [];
432
451
  const decorated: Decorated[] = [];
@@ -467,6 +486,9 @@ function sitesOf(
467
486
  }
468
487
  markNames(node);
469
488
  markStores(node, writes);
489
+ for (const read of propertyReadsOf(node)) {
490
+ reads.push({ ...read, from: enclosing });
491
+ }
470
492
  links.push(...linksOf(file, node, declarations, enclosing));
471
493
  const evaluated = evaluatedBy(node);
472
494
  if (evaluated !== undefined) {
@@ -487,7 +509,7 @@ function sitesOf(
487
509
  };
488
510
 
489
511
  file.forEachChild(visit);
490
- return { uses: found, links, evaluations, decorated };
512
+ return { uses: found, reads, links, evaluations, decorated };
491
513
  }
492
514
 
493
515
  /**
@@ -499,6 +521,26 @@ function patternRule(index: number): string {
499
521
  return `test-file-pattern-${String(index + 1)}`;
500
522
  }
501
523
 
524
+ /**
525
+ * The rules over the test files of several projects at once, ordered by rule: a file two
526
+ * projects hold is one file, so each rule counts the distinct paths it matches.
527
+ */
528
+ export function testFileRulesOf(
529
+ testFiles: readonly string[],
530
+ paths: readonly string[],
531
+ ): readonly TestFileRule[] {
532
+ const distinct = [...new Set(paths)];
533
+ return testFiles
534
+ .map((pattern, index) => {
535
+ const expression = globExpression(pattern);
536
+ return {
537
+ rule: patternRule(index),
538
+ matched: distinct.filter((path) => expression.test(path)).length,
539
+ };
540
+ })
541
+ .sort((a, b) => compare(a.rule, b.rule));
542
+ }
543
+
502
544
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
503
545
  function compare(a: string, b: string): number {
504
546
  if (a === b) {
@@ -509,7 +551,7 @@ function compare(a: string, b: string): number {
509
551
 
510
552
  /**
511
553
  * Every reference one project's own files make to the project's own declarations, in
512
- * position order, and the test-file rules that classified those files.
554
+ * position order, and the files the test-file rules classified.
513
555
  *
514
556
  * `held` is the inventory of the same project, whose declaration map is what a resolved
515
557
  * symbol is looked up in; `targetRoot` is the absolute path every position is rendered
@@ -530,10 +572,10 @@ export function references<Brand>(
530
572
  const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
531
573
  const declarations = held.declarations;
532
574
  const patterns = options.testFiles.map(globExpression);
533
- const matched = options.testFiles.map(() => 0);
534
575
  const found: Reference[] = [];
535
576
  const tests: string[] = [];
536
577
  const chains = aliasChains(project, held);
578
+ const destructured = destructuring(project, cap);
537
579
  let batched = 0;
538
580
  let fileBatches = 0;
539
581
  let residueFallbacks = 0;
@@ -574,17 +616,11 @@ export function references<Brand>(
574
616
 
575
617
  for (const file of files) {
576
618
  const path = renderPosition(file, targetRoot, 0).path;
577
- const hits = patterns.map((pattern) => pattern.test(path));
578
- hits.forEach((hit, index) => {
579
- if (hit) {
580
- matched[index] = (matched[index] ?? 0) + 1;
581
- }
582
- });
583
- const test = hits.includes(true);
619
+ const test = patterns.some((pattern) => pattern.test(path));
584
620
  if (test) {
585
621
  tests.push(path);
586
622
  }
587
- const { uses, links, evaluations, decorated } = sitesOf(
623
+ const { uses, reads, links, evaluations, decorated } = sitesOf(
588
624
  file,
589
625
  declarations,
590
626
  declaredAt(file) ?? "",
@@ -668,6 +704,26 @@ export function references<Brand>(
668
704
  }
669
705
  }
670
706
 
707
+ // A pattern names a property of the value it destructures, which no name node of
708
+ // the pattern resolves to: a shorthand's name is the local it binds.
709
+ destructured.resolve(reads).forEach((symbol, index) => {
710
+ const read = reads[index];
711
+ if (symbol === undefined || read === undefined) {
712
+ return;
713
+ }
714
+ const position = renderPosition(file, targetRoot, read.key.getStart());
715
+ for (const target of chains.chainOf(symbol)) {
716
+ found.push({
717
+ from: read.from,
718
+ to: target.id,
719
+ position,
720
+ use: "read",
721
+ resolution: "destructured",
722
+ test,
723
+ });
724
+ }
725
+ });
726
+
671
727
  for (const site of uses) {
672
728
  const direct: Resolution =
673
729
  site.shorthand !== undefined
@@ -706,10 +762,15 @@ export function references<Brand>(
706
762
  return {
707
763
  configFile: project.configFile,
708
764
  references: found,
709
- testFileRules: options.testFiles
710
- .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
711
- .sort((a, b) => compare(a.rule, b.rule)),
712
765
  testFilePaths: tests.sort(compare),
713
- cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
766
+ cost: {
767
+ batched,
768
+ fileBatches,
769
+ residueFallbacks,
770
+ shorthandLookups,
771
+ aliasSteps: chains.steps,
772
+ patternBatches: destructured.cost.patternBatches,
773
+ patternLookups: destructured.cost.patternLookups,
774
+ },
714
775
  };
715
776
  }