@cplieger/deadset-ts 3.2.0 → 3.3.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.
package/src/exempt.ts CHANGED
@@ -13,13 +13,16 @@ import {
13
13
  EXEMPTION_CLASSES,
14
14
  isExemptionClass,
15
15
  TS_EXEMPTION_CLASSES,
16
+ typescriptVisibilityOf,
16
17
  type ExemptionClass,
17
18
  type TSExemptionClass,
19
+ type TypeScriptVisibility,
18
20
  } from "./exempt-classes.ts";
19
- import type { Inventory, InventorySymbol } from "./inventory.ts";
21
+ import type { Inventory, InventorySymbol, Visibility } from "./inventory.ts";
20
22
  import type { Matrix } from "./matrix.ts";
21
23
  import { byPosition, positionKey, type Position } from "./position.ts";
22
24
  import type { ProjectView } from "./session.ts";
25
+ import type { Templates } from "./template-field.ts";
23
26
  import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
24
27
 
25
28
  /** One declaration a detector holds back, and where and why. */
@@ -36,6 +39,8 @@ export interface DetectorInput<Brand> {
36
39
  readonly project: ProjectView<Brand>;
37
40
  readonly held: Inventory;
38
41
  readonly targetRoot: string;
42
+ /** The configured templates, read once for the run. */
43
+ readonly templates: Templates;
39
44
  }
40
45
 
41
46
  /**
@@ -77,23 +82,44 @@ export function disabledClasses(config: Config): ReadonlySet<ExemptionClass> {
77
82
  return disabled;
78
83
  }
79
84
 
85
+ /**
86
+ * Whether a class whose row states `allowed` may retain a member of one visibility. A
87
+ * declaration the inventory does not hold has no visibility to refuse it by.
88
+ */
89
+ function retainable(allowed: TypeScriptVisibility, visibility: Visibility | undefined): boolean {
90
+ if (visibility === "private") {
91
+ return allowed.private;
92
+ }
93
+ if (visibility === "private-name") {
94
+ return allowed.privateName;
95
+ }
96
+ return true;
97
+ }
98
+
80
99
  /**
81
100
  * The records one project's detectors found, each class in the vocabulary's order and
82
- * none of a switched-off class. Under a production mode a record whose evidence is
83
- * written in a test file holds nothing, as a reference a test file makes is none there.
101
+ * none of a switched-off class. Evidence on a private member the class's row says it
102
+ * cannot retain is dropped first, whatever the detector found. Under a production mode
103
+ * a record whose evidence is written in a test file holds nothing, as a reference a
104
+ * test file makes is none there.
84
105
  */
85
106
  export function computeExemptions<Brand>(
86
107
  input: DetectorInput<Brand>,
87
108
  detectors: Detectors,
88
109
  holding: Holding,
89
110
  ): readonly Exemption[] {
111
+ const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
90
112
  const found: Exemption[] = [];
91
113
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
92
114
  const detect = detectors.get(exemptionClass);
93
115
  if (detect === undefined || holding.disabled.has(exemptionClass)) {
94
116
  continue;
95
117
  }
118
+ const allowed = typescriptVisibilityOf(exemptionClass);
96
119
  for (const evidence of detect(input)) {
120
+ if (!retainable(allowed, visibility.get(evidence.id))) {
121
+ continue;
122
+ }
97
123
  if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
98
124
  continue;
99
125
  }
@@ -0,0 +1,417 @@
1
+ /**
2
+ * The interface-satisfaction class: a class member is held back when a value of the
3
+ * class reaches a position the checker types as an interface requiring a member of
4
+ * that name, so a caller reaches the member through the interface while no name in
5
+ * the program resolves to it.
6
+ *
7
+ * A value reaches such a position as an `implements` entry, the initializer of a
8
+ * declaration whose type is written, an assignment, an argument, a returned value, an
9
+ * array element, an object property, or the operand of `as` or `satisfies`, read
10
+ * through parentheses, conditionals and the operators whose result is an operand.
11
+ * `this` in an instance member is a value of its class. Types are asked in batches;
12
+ * the contextual lookup has none, so it is asked only of a value whose type is a class
13
+ * of the target, and each distinct pair, interface and class is asked once.
14
+ */
15
+
16
+ import {
17
+ isArrayLiteralExpression,
18
+ isArrowFunction,
19
+ isAsExpression,
20
+ isBinaryExpression,
21
+ isCallExpression,
22
+ isClassDeclaration,
23
+ isClassExpression,
24
+ isClassStaticBlockDeclaration,
25
+ isConditionalExpression,
26
+ isFunctionDeclaration,
27
+ isFunctionExpression,
28
+ isGetAccessorDeclaration,
29
+ isHeritageClause,
30
+ isIdentifier,
31
+ isMethodDeclaration,
32
+ isNewExpression,
33
+ isObjectLiteralExpression,
34
+ isParameterDeclaration,
35
+ isParenthesizedExpression,
36
+ isPropertyAssignment,
37
+ isPropertyDeclaration,
38
+ isReturnStatement,
39
+ isSatisfiesExpression,
40
+ isSetAccessorDeclaration,
41
+ isShorthandPropertyAssignment,
42
+ isSpreadElement,
43
+ isVariableDeclaration,
44
+ ModifierFlags,
45
+ SyntaxKind,
46
+ type Expression,
47
+ type Node,
48
+ type SourceFile,
49
+ } from "@typescript/native/unstable/ast";
50
+ import { SymbolFlags, type Type } from "@typescript/native/unstable/sync";
51
+ import { aliasChains } from "./alias-chain.ts";
52
+ import type { DetectorInput, Evidence } from "./exempt.ts";
53
+ import { nodeKey } from "./inventory.ts";
54
+ import { renderPosition } from "./position.ts";
55
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
56
+
57
+ /** One class the inventory holds, by its declaration. */
58
+ interface ClassAt {
59
+ readonly id: string;
60
+ /** The name node, whose type is the class's instance type. */
61
+ readonly name: Node;
62
+ /** Whether the class declares type parameters, so its values carry instantiations of it. */
63
+ readonly generic: boolean;
64
+ }
65
+
66
+ /** One value that flows into a position the checker types from its context. */
67
+ interface Flow {
68
+ readonly value: Expression;
69
+ /** The class `this` stands for, where the value is `this`. */
70
+ readonly self: ClassAt | undefined;
71
+ }
72
+
73
+ /** One entry of a class's `implements` clause. */
74
+ interface Clause {
75
+ readonly owner: ClassAt;
76
+ readonly entry: Node;
77
+ }
78
+
79
+ /** One class type reaching one interface type, and the places it does. */
80
+ interface Pair {
81
+ readonly owner: ClassAt;
82
+ readonly source: Type;
83
+ readonly target: Type;
84
+ readonly name: string;
85
+ readonly sites: Node[];
86
+ }
87
+
88
+ /** The operators of an assignment whose right side is stored into the left side's type. */
89
+ const STORES: ReadonlySet<SyntaxKind> = new Set([
90
+ SyntaxKind.EqualsToken,
91
+ SyntaxKind.QuestionQuestionEqualsToken,
92
+ SyntaxKind.BarBarEqualsToken,
93
+ SyntaxKind.AmpersandAmpersandEqualsToken,
94
+ ]);
95
+
96
+ /** Whether one node declares a static member, whose `this` is the class itself. */
97
+ function isStatic(node: Node): boolean {
98
+ const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
99
+ return (flags & ModifierFlags.Static) !== 0;
100
+ }
101
+
102
+ /** Whether one node is a method or an accessor of an object literal, whose `this` is the literal. */
103
+ function isObjectLiteralMethod(node: Node): boolean {
104
+ return (
105
+ (isMethodDeclaration(node) ||
106
+ isGetAccessorDeclaration(node) ||
107
+ isSetAccessorDeclaration(node)) &&
108
+ isObjectLiteralExpression(node.parent)
109
+ );
110
+ }
111
+
112
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
113
+ function writesType(node: Node): boolean {
114
+ return (node as { readonly type?: Node }).type !== undefined;
115
+ }
116
+
117
+ /**
118
+ * The values one written expression hands on: the expression itself, or what it
119
+ * evaluates to through parentheses, a conditional and the operators whose result is
120
+ * one of their operands.
121
+ */
122
+ function valuesOf(expression: Expression): readonly Expression[] {
123
+ if (isParenthesizedExpression(expression)) {
124
+ return valuesOf(expression.expression);
125
+ }
126
+ if (isConditionalExpression(expression)) {
127
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
128
+ }
129
+ if (isBinaryExpression(expression)) {
130
+ const operator = expression.operatorToken.kind;
131
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
132
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
133
+ }
134
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
135
+ return valuesOf(expression.right);
136
+ }
137
+ }
138
+ return [expression];
139
+ }
140
+
141
+ /** The expressions one node hands to a position typed by its context. */
142
+ function handedOn(node: Node): readonly Expression[] {
143
+ if (
144
+ (isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
145
+ node.initializer !== undefined &&
146
+ writesType(node)
147
+ ) {
148
+ return [node.initializer];
149
+ }
150
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
151
+ return [node.right];
152
+ }
153
+ if (isCallExpression(node) || isNewExpression(node)) {
154
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
155
+ }
156
+ if (isReturnStatement(node)) {
157
+ return node.expression === undefined ? [] : [node.expression];
158
+ }
159
+ if (isArrowFunction(node)) {
160
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
161
+ }
162
+ if (isArrayLiteralExpression(node)) {
163
+ return node.elements.filter(
164
+ (element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression,
165
+ );
166
+ }
167
+ if (isPropertyAssignment(node)) {
168
+ return [node.initializer];
169
+ }
170
+ if (isShorthandPropertyAssignment(node)) {
171
+ return isIdentifier(node.name) ? [node.name] : [];
172
+ }
173
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
174
+ return [node.expression];
175
+ }
176
+ return [];
177
+ }
178
+
179
+ /** What one project's walk found to resolve. */
180
+ interface Walked {
181
+ readonly classes: readonly ClassAt[];
182
+ readonly clauses: readonly Clause[];
183
+ readonly flows: readonly Flow[];
184
+ }
185
+
186
+ /**
187
+ * The classes, the `implements` entries and the flowing values of one project's own
188
+ * files. `this` is the class an instance member belongs to, so a function that is not
189
+ * an arrow, a static member and a static block each stop it.
190
+ */
191
+ function walk(files: readonly SourceFile[], declarations: ReadonlyMap<string, string>): Walked {
192
+ const classes: ClassAt[] = [];
193
+ const clauses: Clause[] = [];
194
+ const flows: Flow[] = [];
195
+ for (const file of files) {
196
+ const visit = (node: Node, self: ClassAt | undefined): void => {
197
+ let inner = self;
198
+ if (isClassDeclaration(node) || isClassExpression(node)) {
199
+ const id = declarations.get(nodeKey(file, node));
200
+ const name = node.name;
201
+ const owner =
202
+ id === undefined || name === undefined
203
+ ? undefined
204
+ : { id, name, generic: node.typeParameters !== undefined };
205
+ if (owner !== undefined) {
206
+ classes.push(owner);
207
+ for (const clause of node.heritageClauses ?? []) {
208
+ if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
209
+ clauses.push(...clause.types.map((entry) => ({ owner, entry })));
210
+ }
211
+ }
212
+ }
213
+ inner = owner;
214
+ } else if (
215
+ isFunctionDeclaration(node) ||
216
+ isFunctionExpression(node) ||
217
+ isClassStaticBlockDeclaration(node) ||
218
+ isStatic(node) ||
219
+ isObjectLiteralMethod(node)
220
+ ) {
221
+ inner = undefined;
222
+ }
223
+ for (const written of handedOn(node)) {
224
+ for (const value of valuesOf(written)) {
225
+ flows.push({
226
+ value,
227
+ self: value.kind === SyntaxKind.ThisKeyword ? inner : undefined,
228
+ });
229
+ }
230
+ }
231
+ node.forEachChild((child) => {
232
+ visit(child, inner);
233
+ });
234
+ };
235
+ file.forEachChild((child) => {
236
+ visit(child, undefined);
237
+ });
238
+ }
239
+ return { classes, clauses, flows };
240
+ }
241
+
242
+ /**
243
+ * The interface-satisfaction detector: for each class type that reaches an interface
244
+ * type it is assignable to, the class's members whose names the interface's members
245
+ * carry, each recorded at the place the value flows.
246
+ */
247
+ export function interfaceSatisfaction<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
248
+ const { project, held, targetRoot } = input;
249
+ const checker = project.checker;
250
+ const files = project.ownSourceFiles();
251
+ const { classes, clauses, flows } = walk(files, held.declarations);
252
+ if (classes.length === 0) {
253
+ return [];
254
+ }
255
+
256
+ // One list, so the batches are capped runs of the whole project rather than of a
257
+ // file: a class's name, an `implements` entry and a value are each one question.
258
+ const asked = [
259
+ ...classes.map((owner) => owner.name),
260
+ ...clauses.map((clause) => clause.entry),
261
+ ...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
262
+ ];
263
+ const typeAt = new Map<Node, Type>();
264
+ for (let from = 0; from < asked.length; from += DEFAULT_BATCH_CAP) {
265
+ const run = asked.slice(from, from + DEFAULT_BATCH_CAP);
266
+ checker.getTypeAtLocation(run).forEach((type, index) => {
267
+ const node = run[index];
268
+ if (type !== undefined && node !== undefined) {
269
+ typeAt.set(node, type);
270
+ }
271
+ });
272
+ }
273
+
274
+ const classOfType = new Map<number, ClassAt>();
275
+ const instanceOf = new Map<string, Type>();
276
+ for (const owner of classes) {
277
+ const type = typeAt.get(owner.name);
278
+ if (type !== undefined) {
279
+ classOfType.set(type.id, owner);
280
+ instanceOf.set(owner.id, type);
281
+ }
282
+ }
283
+ const instantiates = classes.some((owner) => owner.generic);
284
+
285
+ const constituents = new Map<number, readonly Type[]>();
286
+ const partsOf = (type: Type): readonly Type[] => {
287
+ if (!type.isUnionType() && !type.isIntersectionType()) {
288
+ return [type];
289
+ }
290
+ let parts = constituents.get(type.id);
291
+ if (parts === undefined) {
292
+ parts = type.getTypes();
293
+ constituents.set(type.id, parts);
294
+ }
295
+ return parts;
296
+ };
297
+
298
+ const targets = new Map<number, ClassAt | undefined>();
299
+ /** The classes of the target one value's type is an instance of, with the instance type. */
300
+ const classesOf = (type: Type): readonly { owner: ClassAt; type: Type }[] =>
301
+ partsOf(type).flatMap((part) => {
302
+ const direct = classOfType.get(part.id);
303
+ if (direct !== undefined) {
304
+ return [{ owner: direct, type: part }];
305
+ }
306
+ if (!instantiates || !part.isTypeReference()) {
307
+ return [];
308
+ }
309
+ if (!targets.has(part.id)) {
310
+ targets.set(part.id, classOfType.get(part.getTarget().id));
311
+ }
312
+ const generic = targets.get(part.id);
313
+ return generic === undefined ? [] : [{ owner: generic, type: part }];
314
+ });
315
+
316
+ const interfaceNames = new Map<number, string | undefined>();
317
+ /** The interface types one position's type is or holds, each with the interface's name. */
318
+ const interfacesOf = (type: Type): readonly { type: Type; name: string }[] =>
319
+ partsOf(type).flatMap((part) => {
320
+ if (!interfaceNames.has(part.id)) {
321
+ const symbol = part.isObjectType() ? part.getSymbol() : undefined;
322
+ interfaceNames.set(
323
+ part.id,
324
+ symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
325
+ ? symbol.name
326
+ : undefined,
327
+ );
328
+ }
329
+ const name = interfaceNames.get(part.id);
330
+ return name === undefined ? [] : [{ type: part, name }];
331
+ });
332
+
333
+ const pairs = new Map<string, Pair>();
334
+ const reach = (owner: ClassAt, source: Type, target: Type, name: string, site: Node): void => {
335
+ const key = `${String(source.id)}:${String(target.id)}`;
336
+ const pair = pairs.get(key);
337
+ if (pair === undefined) {
338
+ pairs.set(key, { owner, source, target, name, sites: [site] });
339
+ } else {
340
+ pair.sites.push(site);
341
+ }
342
+ };
343
+
344
+ for (const clause of clauses) {
345
+ const source = instanceOf.get(clause.owner.id);
346
+ const target = typeAt.get(clause.entry);
347
+ if (source === undefined || target === undefined) {
348
+ continue;
349
+ }
350
+ for (const reached of interfacesOf(target)) {
351
+ reach(clause.owner, source, reached.type, reached.name, clause.entry);
352
+ }
353
+ }
354
+
355
+ for (const flow of flows) {
356
+ const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
357
+ const valueType = typeAt.get(flow.value);
358
+ const sources =
359
+ flow.self !== undefined && self !== undefined
360
+ ? [{ owner: flow.self, type: self }]
361
+ : valueType === undefined
362
+ ? []
363
+ : classesOf(valueType);
364
+ if (sources.length === 0) {
365
+ continue;
366
+ }
367
+ const position = checker.getContextualType(flow.value);
368
+ if (position === undefined) {
369
+ continue;
370
+ }
371
+ for (const reached of interfacesOf(position)) {
372
+ for (const source of sources) {
373
+ reach(source.owner, source.type, reached.type, reached.name, flow.value);
374
+ }
375
+ }
376
+ }
377
+
378
+ const chains = aliasChains(project, held);
379
+ const required = new Map<number, ReadonlySet<string>>();
380
+ const members = new Map<string, ReadonlyMap<string, readonly string[]>>();
381
+ const found: Evidence[] = [];
382
+ for (const pair of pairs.values()) {
383
+ if (!checker.isTypeAssignableTo(pair.source, pair.target)) {
384
+ continue;
385
+ }
386
+ let names = required.get(pair.target.id);
387
+ if (names === undefined) {
388
+ names = new Set(
389
+ checker.getPropertiesOfType(pair.target).map((property) => property.escapedName),
390
+ );
391
+ required.set(pair.target.id, names);
392
+ }
393
+ let answering = members.get(pair.owner.id);
394
+ if (answering === undefined) {
395
+ const instance = instanceOf.get(pair.owner.id) ?? pair.source;
396
+ answering = new Map(
397
+ checker
398
+ .getPropertiesOfType(instance)
399
+ .map((property) => [property.escapedName, chains.declarationsOf(property)]),
400
+ );
401
+ members.set(pair.owner.id, answering);
402
+ }
403
+ const detail = `satisfies ${pair.name}`;
404
+ for (const name of names) {
405
+ for (const id of answering.get(name) ?? []) {
406
+ for (const site of pair.sites) {
407
+ found.push({
408
+ id,
409
+ detail,
410
+ site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
411
+ });
412
+ }
413
+ }
414
+ }
415
+ }
416
+ return found;
417
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The members of one project's inventory by the name each is written with, for the
3
+ * classes whose evidence is a name rather than a type relation: a template and a
4
+ * string key both hold a name and nothing else, so each holds back every member of
5
+ * that name its own mechanism can reach.
6
+ */
7
+
8
+ import {
9
+ isIdentifier,
10
+ isNumericLiteral,
11
+ isPrivateIdentifier,
12
+ isStringLiteral,
13
+ type Node,
14
+ type SourceFile,
15
+ } from "@typescript/native/unstable/ast";
16
+ import { nodeKey, type Inventory, type InventorySymbol, type SymbolKind } from "./inventory.ts";
17
+
18
+ /** The kinds of declaration a member name can name. */
19
+ const MEMBER_KINDS: ReadonlySet<SymbolKind> = new Set([
20
+ "method",
21
+ "class-member",
22
+ "interface-method",
23
+ "type-member",
24
+ "enum-member",
25
+ ]);
26
+
27
+ /**
28
+ * The name a member declaration is written with: an identifier's text, a private
29
+ * name's text without its `#`, a string key's value and a numeric key's text. A
30
+ * private name is indexed under the name a template or a string would spell, so the
31
+ * rule that no such evidence holds it back is the framework's alone. A computed key
32
+ * names nothing a string can be equal to before the program runs.
33
+ */
34
+ function writtenName(name: Node | undefined): string | undefined {
35
+ if (name === undefined) {
36
+ return undefined;
37
+ }
38
+ if (isPrivateIdentifier(name)) {
39
+ return name.text.replace(/^#/u, "");
40
+ }
41
+ if (isIdentifier(name)) {
42
+ return name.text;
43
+ }
44
+ if (isStringLiteral(name) || isNumericLiteral(name)) {
45
+ return name.text;
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ /**
51
+ * Every member the inventory holds, keyed by the name it is written with, each
52
+ * member once and in inventory order. A getter and a setter of one name are one
53
+ * member.
54
+ */
55
+ export function memberNames(
56
+ files: readonly SourceFile[],
57
+ held: Inventory,
58
+ ): ReadonlyMap<string, readonly InventorySymbol[]> {
59
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
60
+ const named = new Map<string, string>();
61
+ for (const file of files) {
62
+ const visit = (node: Node): void => {
63
+ const id = held.declarations.get(nodeKey(file, node));
64
+ const symbol = id === undefined ? undefined : byId.get(id);
65
+ if (symbol !== undefined && MEMBER_KINDS.has(symbol.kind)) {
66
+ const name = writtenName((node as { readonly name?: Node }).name);
67
+ if (name !== undefined) {
68
+ named.set(symbol.id, name);
69
+ }
70
+ }
71
+ node.forEachChild(visit);
72
+ };
73
+ visit(file);
74
+ }
75
+ const index = new Map<string, InventorySymbol[]>();
76
+ for (const symbol of held.symbols) {
77
+ const name = named.get(symbol.id);
78
+ if (name === undefined) {
79
+ continue;
80
+ }
81
+ const members = index.get(name);
82
+ if (members === undefined) {
83
+ index.set(name, [symbol]);
84
+ } else {
85
+ members.push(symbol);
86
+ }
87
+ }
88
+ return index;
89
+ }
package/src/references.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  isArrayLiteralExpression,
3
3
  isBinaryExpression,
4
4
  isCallExpression,
5
+ isDecorator,
5
6
  isDeleteExpression,
6
7
  isElementAccessExpression,
7
8
  isExportDeclaration,
@@ -53,9 +54,11 @@ import type { ProjectView } from "./session.ts";
53
54
  /**
54
55
  * How one reference uses the declaration it names. An `evaluation` names a file: the
55
56
  * place it is written imports the module, which runs the module's top level and reads
56
- * none of what it exports. A `read` of a file reads the module's namespace.
57
+ * none of what it exports. A `read` of a file reads the module's namespace. A
58
+ * `decorator` names the declaration a decorator expression is attached to, which the
59
+ * decorator receives when the class is defined.
57
60
  */
58
- export type Use = "read" | "write" | "evaluation";
61
+ export type Use = "read" | "write" | "evaluation" | "decorator";
59
62
 
60
63
  /**
61
64
  * Which accessor answered for one reference, so that the cost of a run is attributable
@@ -67,8 +70,9 @@ export type Use = "read" | "write" | "evaluation";
67
70
  * - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
68
71
  * the literal declares rather than to the declaration that name reads.
69
72
  * - `alias` is a step along an import or export chain, from one link to what it names.
73
+ * - `syntax` is the tree alone, which says what a decorator is attached to.
70
74
  */
71
- export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias";
75
+ export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax";
72
76
 
73
77
  /** One use of one declaration by one declaration. */
74
78
  export interface Reference {
@@ -318,11 +322,18 @@ interface Evaluation {
318
322
  readonly from: string;
319
323
  }
320
324
 
321
- /** Everything one file's walk found to resolve. */
325
+ /** One decorator expression, and the declaration it is attached to. */
326
+ interface Decorated {
327
+ readonly decorator: Node;
328
+ readonly declaration: string;
329
+ }
330
+
331
+ /** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
322
332
  interface FileSites {
323
333
  readonly uses: readonly Site[];
324
334
  readonly links: readonly Link[];
325
335
  readonly evaluations: readonly Evaluation[];
336
+ readonly decorated: readonly Decorated[];
326
337
  }
327
338
 
328
339
  /** Whether one node is a string literal or a template literal with no substitution. */
@@ -418,6 +429,7 @@ function sitesOf(
418
429
  const found: Site[] = [];
419
430
  const links: Link[] = [];
420
431
  const evaluations: Evaluation[] = [];
432
+ const decorated: Decorated[] = [];
421
433
  const writes = new Set<number>();
422
434
  const declared = new Set<number>();
423
435
  const shorthands = new Map<number, Node>();
@@ -464,13 +476,18 @@ function sitesOf(
464
476
  const outer = enclosing;
465
477
  if (held !== undefined) {
466
478
  enclosing = held;
479
+ for (const modifier of (node as { readonly modifiers?: readonly Node[] }).modifiers ?? []) {
480
+ if (isDecorator(modifier)) {
481
+ decorated.push({ decorator: modifier, declaration: held });
482
+ }
483
+ }
467
484
  }
468
485
  node.forEachChild(visit);
469
486
  enclosing = outer;
470
487
  };
471
488
 
472
489
  file.forEachChild(visit);
473
- return { uses: found, links, evaluations };
490
+ return { uses: found, links, evaluations, decorated };
474
491
  }
475
492
 
476
493
  /**
@@ -567,7 +584,25 @@ export function references<Brand>(
567
584
  if (test) {
568
585
  tests.push(path);
569
586
  }
570
- const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
587
+ const { uses, links, evaluations, decorated } = sitesOf(
588
+ file,
589
+ declarations,
590
+ declaredAt(file) ?? "",
591
+ );
592
+
593
+ // A decorator is written inside the declaration it is attached to and receives it,
594
+ // so the declaration is used where the decorator is written, whatever the
595
+ // decorator does with it.
596
+ for (const { decorator, declaration } of decorated) {
597
+ found.push({
598
+ from: declaration,
599
+ to: declaration,
600
+ position: renderPosition(file, targetRoot, decorator.getStart()),
601
+ use: "decorator",
602
+ resolution: "syntax",
603
+ test,
604
+ });
605
+ }
571
606
 
572
607
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
573
608
  // the uses and the evaluated modules' specifiers last, each node once: a bare star