@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.22

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 (72) hide show
  1. package/dist/src/alias-chain.js +33 -2
  2. package/dist/src/analysis.js +18 -77
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/contract/contract.generated.js +5 -4
  5. package/dist/src/dependencies.js +1 -1
  6. package/dist/src/exemptions/argument-places.js +131 -0
  7. package/dist/src/{calls.js → exemptions/calls.js} +126 -179
  8. package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
  9. package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
  10. package/dist/src/exemptions/detectors.js +48 -0
  11. package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
  12. package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
  13. package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
  14. package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
  15. package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
  16. package/dist/src/exemptions/installed-globals.js +173 -0
  17. package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
  18. package/dist/src/exemptions/lifecycle.js +150 -0
  19. package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
  20. package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
  21. package/dist/src/exemptions/sites.js +182 -0
  22. package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
  23. package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
  24. package/dist/src/findings/intra-function.js +50 -37
  25. package/dist/src/index/sites.js +1 -1
  26. package/dist/src/index/values.js +124 -0
  27. package/dist/src/intra-function-parts.js +1149 -222
  28. package/dist/src/liveness/sweep.js +2 -2
  29. package/dist/src/query.js +48 -0
  30. package/dist/src/roots/configured-declarations.js +1 -1
  31. package/dist/src/verbs/print-retained.js +23 -1
  32. package/jsr.json +1 -1
  33. package/package.json +2 -2
  34. package/src/alias-chain.ts +38 -2
  35. package/src/analysis.ts +19 -100
  36. package/src/bindings.ts +1 -1
  37. package/src/conformance-record.ts +2 -2
  38. package/src/contract/contract.generated.ts +5 -4
  39. package/src/dependencies.ts +1 -1
  40. package/src/exemptions/argument-places.ts +198 -0
  41. package/src/{calls.ts → exemptions/calls.ts} +190 -224
  42. package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
  43. package/src/exemptions/decorator.ts +56 -0
  44. package/src/exemptions/detectors.ts +75 -0
  45. package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
  46. package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
  47. package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
  48. package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
  49. package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
  50. package/src/exemptions/installed-globals.ts +255 -0
  51. package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
  52. package/src/exemptions/lifecycle.ts +286 -0
  53. package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
  54. package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
  55. package/src/exemptions/sites.ts +293 -0
  56. package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
  57. package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
  58. package/src/facts.ts +1 -1
  59. package/src/findings/intra-function.ts +59 -37
  60. package/src/index/sites.ts +1 -1
  61. package/src/index/values.ts +170 -0
  62. package/src/intra-function-parts.ts +1435 -247
  63. package/src/liveness/sweep.ts +2 -2
  64. package/src/query.ts +109 -10
  65. package/src/roots/configured-declarations.ts +1 -2
  66. package/src/stores.ts +1 -1
  67. package/src/verbs/print-retained.ts +29 -1
  68. package/dist/src/exempt.js +0 -164
  69. package/dist/src/value-flow.js +0 -74
  70. package/src/decorator.ts +0 -78
  71. package/src/exempt.ts +0 -295
  72. package/src/value-flow.ts +0 -104
@@ -1,33 +1,26 @@
1
1
  /**
2
2
  * The calls and decorators of one project that the configured classes read: what each
3
- * callee resolves to, and which of the target's classes a call passes or a decorator
4
- * is attached to. A class is passed by each value an argument can be, at any depth inside
5
- * the array and object literals it writes, named directly or through variables, and a
6
- * decorator written as a call is a call.
3
+ * call invokes, a call through a method of `Function.prototype` included, and which of the
4
+ * target's classes a call passes or a decorator is attached to. A class is passed by each
5
+ * value an argument can carry ({@link carriedValues}), named directly or through
6
+ * variables, and a decorator written as a call is a call.
7
7
  */
8
8
 
9
9
  import {
10
10
  isArrayLiteralExpression,
11
11
  isArrowFunction,
12
- isBinaryExpression,
13
12
  isBindingElement,
14
13
  isCallExpression,
15
- isClassDeclaration,
16
- isConditionalExpression,
17
14
  isDecorator,
18
15
  isFunctionDeclaration,
19
16
  isFunctionExpression,
20
17
  isIdentifier,
21
18
  isMethodDeclaration,
22
- isObjectLiteralExpression,
23
19
  isParameterDeclaration,
24
- isParenthesizedExpression,
25
20
  isPropertyAccessExpression,
26
21
  isPropertyAssignment,
27
22
  isPropertyDeclaration,
28
23
  isShorthandPropertyAssignment,
29
- isOmittedExpression,
30
- isSpreadAssignment,
31
24
  isSpreadElement,
32
25
  isVariableDeclaration,
33
26
  type CallExpression,
@@ -35,7 +28,6 @@ import {
35
28
  type Expression,
36
29
  type Node,
37
30
  type ParameterDeclaration,
38
- SyntaxKind,
39
31
  } from "@typescript/native/ast";
40
32
  import {
41
33
  ObjectFlags,
@@ -43,23 +35,13 @@ import {
43
35
  type Symbol as TSSymbol,
44
36
  type Type,
45
37
  } from "@typescript/native/sync";
46
- import type { AliasChains } from "./alias-chain.ts";
47
- import { nodeKey, type Inventory } from "./inventory.ts";
48
- import { must } from "./query.ts";
49
- import type { ProjectView } from "./session.ts";
50
- import { unwrapped } from "./unwrap.ts";
51
-
52
- /** One value written where a call passes it, and the node its declaration is resolved from. */
53
- interface PassedValue {
54
- /** Where the value is written, which is where a record of it sits. */
55
- readonly site: Node;
56
- /** The name the value is resolved through, the shorthand assignment that holds it, or none. */
57
- readonly name: Node | undefined;
58
- readonly shorthand: boolean;
59
- }
60
-
61
- /** A passed value that names its declaration. */
62
- type NamedValue = PassedValue & { readonly name: Node };
38
+ import { resolvedTargets, type AliasChains } from "../alias-chain.ts";
39
+ import { carriedValues, nameOf, type CarriedValue } from "../index/values.ts";
40
+ import type { Inventory } from "../inventory.ts";
41
+ import { must } from "../query.ts";
42
+ import type { ProjectView } from "../session.ts";
43
+ import { parenthesesRemoved, unwrapped } from "../unwrap.ts";
44
+ import type { ProjectSites } from "./sites.ts";
63
45
 
64
46
  /** One class of the target a call passes or a decorator is attached to, and where. */
65
47
  interface ClassUse {
@@ -68,21 +50,10 @@ interface ClassUse {
68
50
  readonly site: Node;
69
51
  }
70
52
 
71
- /** The name node a callee or a decorator expression resolves through, where it has one. */
72
- export function calleeName(expression: Expression): Node | undefined {
73
- if (isParenthesizedExpression(expression)) {
74
- return calleeName(expression.expression);
75
- }
76
- if (isPropertyAccessExpression(expression)) {
77
- return expression.name;
78
- }
79
- return isIdentifier(expression) ? expression : undefined;
80
- }
81
-
82
53
  /** The callee one decorator names: its expression, or the callee of the call it writes. */
83
54
  export function decoratorCallee(decorator: Decorator): Node | undefined {
84
55
  const expression = decorator.expression;
85
- return calleeName(isCallExpression(expression) ? expression.expression : expression);
56
+ return nameOf(isCallExpression(expression) ? expression.expression : expression);
86
57
  }
87
58
 
88
59
  /** How one decorator is spelled in a record: its expression, without a call's arguments. */
@@ -97,127 +68,146 @@ export function decoratorsOf(node: Node): readonly Decorator[] {
97
68
  return ((node as { readonly modifiers?: readonly Node[] }).modifiers ?? []).filter(isDecorator);
98
69
  }
99
70
 
71
+ /** The methods of `Function.prototype` that invoke a function with arguments of their own. */
72
+ const FUNCTION_METHODS: ReadonlySet<string> = new Set(["call", "apply", "bind"]);
73
+
74
+ /** The interfaces of the standard library that declare those methods for a function value. */
75
+ const FUNCTION_INTERFACES: ReadonlySet<string> = new Set([
76
+ "Function",
77
+ "CallableFunction",
78
+ "NewableFunction",
79
+ ]);
80
+
100
81
  /**
101
- * What each of a batch of nodes resolves to, every alias followed to the declaration
102
- * it stands for. A node the checker resolves to nothing, and an alias that resolves
103
- * to nothing, are absent from the answer. Each alias is followed once. A question the
104
- * checker does not answer stops the pass, as {@link must} stops.
82
+ * One call as the function it invokes sees it: the expression naming that function,
83
+ * and the argument each of its parameters binds, in order.
105
84
  */
106
- export function resolvedTargets<Brand>(
107
- project: ProjectView<Brand>,
108
- nodes: readonly Node[],
109
- ): ReadonlyMap<Node, TSSymbol> {
110
- const followed = new Map<number, TSSymbol | undefined>();
111
- const targets = new Map<Node, TSSymbol>();
112
- project
113
- .symbolsAt(nodes.map((node) => project.handle(node)))
114
- .map(must)
115
- .forEach((symbol, index) => {
116
- const node = nodes[index];
117
- if (symbol === undefined || node === undefined) {
118
- return;
119
- }
120
- let target: TSSymbol | undefined = symbol;
121
- if ((symbol.flags & SymbolFlags.Alias) !== 0) {
122
- if (!followed.has(symbol.id)) {
123
- const aliased = must(project.queries.aliased(symbol));
124
- followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
125
- }
126
- target = followed.get(symbol.id);
127
- }
128
- if (target !== undefined) {
129
- targets.set(node, target);
130
- }
131
- });
132
- return targets;
85
+ export interface Invocation {
86
+ readonly callee: Expression;
87
+ readonly arguments: readonly Expression[];
88
+ /** Whether the call reaches the callee through a method of `Function.prototype`. */
89
+ readonly indirect: boolean;
90
+ /** Whether the last argument is a list the parameters bind the elements of, as `apply` takes. */
91
+ readonly listed: boolean;
133
92
  }
134
93
 
135
- /** The declaration a shorthand property's value resolves to, an import followed to what it names. */
136
- function shorthandTarget<Brand>(project: ProjectView<Brand>, node: Node): TSSymbol | undefined {
137
- const value = must(project.shorthandValueAt(project.handle(node)));
138
- if (value === undefined || (value.flags & SymbolFlags.Alias) === 0) {
139
- return value;
140
- }
141
- const aliased = must(project.queries.aliased(value));
142
- return aliased.declarations.length === 0 ? undefined : aliased;
94
+ /** A call through a method of `Function.prototype`: the method's name and the invocation. */
95
+ interface MethodForm {
96
+ readonly method: Node;
97
+ readonly invocation: Invocation;
143
98
  }
144
99
 
145
100
  /**
146
- * The values one expression passes: each value it can be ({@link leavesOf}), or what an
147
- * array or object literal it can be holds, at any depth.
101
+ * The invocation a call writes through a method of `Function.prototype`, read from
102
+ * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
103
+ * passes `a` though nothing calls the bound function there, because the bound function
104
+ * carries `a` to wherever it is called and that call is not followed, and
105
+ * `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
106
+ * that is no array literal is the value of the first parameter, whose elements it
107
+ * carries. Whether the method is the prototype's is resolution's to say.
148
108
  */
149
- function passedIn(written: Expression, found: PassedValue[]): void {
150
- for (const expression of leavesOf(written)) {
151
- if (isArrayLiteralExpression(expression)) {
152
- for (const element of expression.elements) {
153
- passedIn(element, found);
154
- }
155
- } else if (isObjectLiteralExpression(expression)) {
156
- for (const property of expression.properties) {
157
- if (isPropertyAssignment(property)) {
158
- passedIn(property.initializer, found);
159
- } else if (isSpreadAssignment(property)) {
160
- passedIn(property.expression, found);
161
- } else if (isShorthandPropertyAssignment(property)) {
162
- found.push({ site: property.name, name: property, shorthand: true });
163
- }
164
- }
165
- } else if (!isOmittedExpression(expression)) {
166
- found.push({ site: expression, name: calleeName(expression), shorthand: false });
109
+ function throughMethod(call: CallExpression): MethodForm | undefined {
110
+ const expression = parenthesesRemoved(call.expression);
111
+ if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
112
+ const [, ...rest] = call.arguments;
113
+ let passed: readonly Expression[] = rest;
114
+ let listed = false;
115
+ if (expression.name.text === "apply") {
116
+ const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
117
+ listed = list !== undefined && !isArrayLiteralExpression(list);
118
+ passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
167
119
  }
120
+ return {
121
+ method: expression.name,
122
+ invocation: { callee: expression.expression, arguments: passed, indirect: true, listed },
123
+ };
168
124
  }
125
+ if (isCallExpression(expression)) {
126
+ const bound = parenthesesRemoved(expression.expression);
127
+ if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
128
+ return {
129
+ method: bound.name,
130
+ invocation: {
131
+ callee: bound.expression,
132
+ arguments: [...expression.arguments.slice(1), ...call.arguments],
133
+ indirect: true,
134
+ listed: false,
135
+ },
136
+ };
137
+ }
138
+ }
139
+ return undefined;
169
140
  }
170
141
 
171
- /** The operators whose value is one of their operands. */
172
- const CHOOSING: ReadonlySet<SyntaxKind> = new Set([
173
- SyntaxKind.QuestionQuestionToken,
174
- SyntaxKind.BarBarToken,
175
- SyntaxKind.AmpersandAmpersandToken,
176
- ]);
142
+ /** What each call of one project invokes. */
143
+ export interface Invocations {
144
+ /** The function one call invokes and the arguments its parameters bind. */
145
+ readonly of: (call: CallExpression) => Invocation;
146
+ /** The declaration the invoked function's name resolves to, every alias followed. */
147
+ readonly callee: (call: CallExpression) => TSSymbol | undefined;
148
+ }
177
149
 
178
150
  /**
179
- * The expressions one value can be: itself, or each branch of a conditional, each operand
180
- * of `??`, `||` and `&&` and the last of a comma, inside any wrapper that changes no value.
181
- * Each is read alone because the checker reduces a union by subtype, so the type at the
182
- * whole drops a class structurally equal to, or a subtype of, another branch's.
151
+ * What each of a project's calls invokes. A call through `call`, `apply` or `bind`
152
+ * invokes the function the method is read from only where the method is the standard
153
+ * library's: the program's own `call` and `Reflect.apply` pass nothing on.
183
154
  */
184
- function leavesOf(written: Expression): readonly Expression[] {
185
- const expression = unwrapped(isSpreadElement(written) ? written.expression : written);
186
- if (isConditionalExpression(expression)) {
187
- return [...leavesOf(expression.whenTrue), ...leavesOf(expression.whenFalse)];
188
- }
189
- if (isBinaryExpression(expression)) {
190
- const operator = expression.operatorToken.kind;
191
- if (CHOOSING.has(operator)) {
192
- return [...leavesOf(expression.left), ...leavesOf(expression.right)];
193
- }
194
- if (operator === SyntaxKind.CommaToken) {
195
- return leavesOf(expression.right);
155
+ export function invocations<Brand>(
156
+ project: ProjectView<Brand>,
157
+ calls: readonly CallExpression[],
158
+ ): Invocations {
159
+ const forms = new Map<CallExpression, MethodForm>();
160
+ for (const call of calls) {
161
+ const form = throughMethod(call);
162
+ if (form !== undefined) {
163
+ forms.set(call, form);
196
164
  }
197
165
  }
198
- return [expression];
199
- }
200
-
201
- /** The values one argument passes, a spread argument's included. */
202
- function leavesPassed(argument: Expression): readonly PassedValue[] {
203
- const found: PassedValue[] = [];
204
- passedIn(argument, found);
205
- return found;
166
+ const callees = resolvedTargets(project, [
167
+ ...calls.flatMap((call) => nameOf(call.expression) ?? []),
168
+ ...[...forms.values()].flatMap((form) => {
169
+ const name = nameOf(form.invocation.callee);
170
+ return name === undefined ? [form.method] : [form.method, name];
171
+ }),
172
+ ]);
173
+ const standard = (method: TSSymbol): boolean =>
174
+ method.declarations.some(
175
+ (handle) => project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true,
176
+ ) && FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "");
177
+ const of = (call: CallExpression): Invocation => {
178
+ const form = forms.get(call);
179
+ const method = form === undefined ? undefined : callees.get(form.method);
180
+ return form !== undefined && method !== undefined && standard(method)
181
+ ? form.invocation
182
+ : { callee: call.expression, arguments: call.arguments, indirect: false, listed: false };
183
+ };
184
+ return {
185
+ of,
186
+ callee: (call) => {
187
+ const name = nameOf(of(call).callee);
188
+ return name === undefined ? undefined : callees.get(name);
189
+ },
190
+ };
206
191
  }
207
192
 
208
- /** The values one argument passes that name their declaration. */
209
- export function valuesPassed(argument: Expression): readonly NamedValue[] {
210
- return leavesPassed(argument).filter((value): value is NamedValue => value.name !== undefined);
193
+ /** The declaration a shorthand property's value resolves to, an import followed to what it names. */
194
+ function shorthandTarget<Brand>(project: ProjectView<Brand>, node: Node): TSSymbol | undefined {
195
+ const value = must(project.shorthandValueAt(project.handle(node)));
196
+ if (value === undefined || (value.flags & SymbolFlags.Alias) === 0) {
197
+ return value;
198
+ }
199
+ const aliased = must(project.queries.aliased(value));
200
+ return aliased.declarations.length === 0 ? undefined : aliased;
211
201
  }
212
202
 
213
203
  /**
214
- * What one passed value holds: the declaration its name resolves to, and the classes and
204
+ * What one carried value holds: the declaration its name resolves to, and the classes and
215
205
  * functions the checker's type at it holds ({@link constructedAt}). `known` holds names
216
206
  * already resolved in a batch.
217
207
  */
218
208
  function heldBy<Brand>(
219
209
  project: ProjectView<Brand>,
220
- value: PassedValue,
210
+ value: CarriedValue,
221
211
  known?: ReadonlyMap<Node, TSSymbol>,
222
212
  ): { readonly named: TSSymbol | undefined; readonly typed: readonly TSSymbol[] } {
223
213
  const { name } = value;
@@ -233,34 +223,58 @@ function heldBy<Brand>(
233
223
  };
234
224
  }
235
225
 
236
- /**
237
- * The classes of the target each call passes, by the call: each class a value an argument
238
- * can be holds as a constructor, as the checker types it. A name whose type erases the
239
- * class is followed through what its declaration binds ({@link boundTo}) and, for a
240
- * parameter, through what each call of its function passes at its place, so a class handed
241
- * to a wrapper is passed. A class is used at each value that passes it. A class of another
242
- * program passes nothing.
243
- */
226
+ /** The classes of the target each call passes, by the call, as {@link classesCarried} reads its invocation's arguments. */
244
227
  export function classesPassed<Brand>(
245
228
  project: ProjectView<Brand>,
246
229
  held: Inventory,
247
230
  chains: AliasChains,
231
+ sites: Pick<ProjectSites, "calls" | "invocations">,
248
232
  calls: readonly CallExpression[],
249
233
  ): ReadonlyMap<CallExpression, readonly ClassUse[]> {
250
- const passed = new Map(calls.map((call) => [call, call.arguments.flatMap(leavesPassed)]));
234
+ const carried = classesCarried(
235
+ project,
236
+ held,
237
+ chains,
238
+ sites,
239
+ calls.map((call) => sites.invocations.of(call).arguments),
240
+ );
241
+ return new Map(calls.map((call, at) => [call, carried[at] ?? []]));
242
+ }
243
+
244
+ /**
245
+ * The classes of the target each group of written values carries: each class a value
246
+ * can carry holds as a constructor, as the checker types it. A name whose type erases
247
+ * the class is followed through what its declaration binds ({@link boundTo}) and, for a
248
+ * parameter, through what each call of its function passes at its place, so a class
249
+ * handed to a wrapper is passed. A class is used at each value that carries it. A class
250
+ * of another program is carried by nothing.
251
+ */
252
+ export function classesCarried<Brand>(
253
+ project: ProjectView<Brand>,
254
+ held: Inventory,
255
+ chains: AliasChains,
256
+ sites: Pick<ProjectSites, "calls" | "invocations">,
257
+ groups: readonly (readonly Expression[])[],
258
+ ): readonly (readonly ClassUse[])[] {
259
+ const { invocations: invoked } = sites;
260
+ const passed = groups.map((written) => written.flatMap(carriedValues));
251
261
  const targets = resolvedTargets(
252
262
  project,
253
- [...passed.values()].flat().flatMap((value) => (value.shorthand ? [] : (value.name ?? []))),
263
+ passed.flat().flatMap((value) => (value.shorthand ? [] : (value.name ?? []))),
254
264
  );
255
265
  const classes = new Set(
256
266
  held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id),
257
267
  );
258
268
  let calling: CallsOf | undefined;
259
269
  const callsOfFunction = (symbol: TSSymbol): readonly CallExpression[] => {
260
- calling ??= callsByDeclaration(project);
270
+ calling ??= callsByDeclaration(
271
+ project,
272
+ sites.calls.map((one) => one.call),
273
+ invoked,
274
+ );
261
275
  return symbol.declarations.flatMap((handle) => calling?.get(keyOf(handle)) ?? []);
262
276
  };
263
- const classesOf = (value: PassedValue, seen: Set<number>): readonly string[] => {
277
+ const classesOf = (value: CarriedValue, seen: Set<number>): readonly string[] => {
264
278
  const { named, typed } = heldBy(project, value, targets);
265
279
  return [
266
280
  ...typed.flatMap((symbol) => chains.declarationsOf(symbol).filter((id) => classes.has(id))),
@@ -274,14 +288,14 @@ export function classesPassed<Brand>(
274
288
  }
275
289
  seen.add(symbol.id);
276
290
  const through = (written: Expression): readonly string[] =>
277
- leavesPassed(written).flatMap((value) => classesOf(value, seen));
291
+ carriedValues(written).flatMap((value) => classesOf(value, seen));
278
292
  return symbol.declarations.flatMap((handle) => {
279
293
  const node = project.declarationAt(handle)?.node;
280
294
  if (node === undefined) {
281
295
  return [];
282
296
  }
283
297
  if (isParameterDeclaration(node)) {
284
- return argumentsAt(project, node, callsOfFunction).flatMap(through);
298
+ return argumentsAt(project, node, callsOfFunction, invoked).flatMap(through);
285
299
  }
286
300
  const bound = boundTo(project, node);
287
301
  return [
@@ -290,17 +304,15 @@ export function classesPassed<Brand>(
290
304
  ];
291
305
  });
292
306
  };
293
- return new Map(
294
- [...passed].map(([call, values]) => {
295
- const uses = new Map<string, ClassUse>();
296
- for (const value of values) {
297
- for (const id of classesOf(value, new Set())) {
298
- uses.set(`${id}\u0000${String(value.site.pos)}`, { id, site: value.site });
299
- }
307
+ return passed.map((values) => {
308
+ const uses = new Map<string, ClassUse>();
309
+ for (const value of values) {
310
+ for (const id of classesOf(value, new Set())) {
311
+ uses.set(`${id}\u0000${String(value.site.pos)}`, { id, site: value.site });
300
312
  }
301
- return [call, [...uses.values()]];
302
- }),
303
- );
313
+ }
314
+ return [...uses.values()];
315
+ });
304
316
  }
305
317
 
306
318
  /**
@@ -371,19 +383,18 @@ function keyOf(handle: { readonly path: string; readonly index: number }): strin
371
383
  }
372
384
 
373
385
  /**
374
- * Every call of the project by the declarations its callee can be: the function or the
375
- * method it names, or else each function the checker's type at it holds, wherever that
376
- * value came from. A name is also followed through what its binding copies
386
+ * Every call of the project by the declarations its invoked function can be: the function
387
+ * or the method it names, or else each function the checker's type at it holds, wherever
388
+ * that value came from. A name is also followed through what its binding copies
377
389
  * ({@link boundTo}), which is how a call reaches a function an annotation erases.
378
390
  * Only the target's own declarations are keys, as only its own functions are asked for.
379
391
  */
380
- function callsByDeclaration<Brand>(project: ProjectView<Brand>): CallsOf {
381
- const calls = callsOf(project);
392
+ function callsByDeclaration<Brand>(
393
+ project: ProjectView<Brand>,
394
+ calls: readonly CallExpression[],
395
+ invoked: Invocations,
396
+ ): CallsOf {
382
397
  const own = project.ownPaths();
383
- const callees = resolvedTargets(
384
- project,
385
- calls.flatMap((call) => calleeName(call.expression) ?? []),
386
- );
387
398
  const named = new Map<number, readonly string[]>();
388
399
  const keysOf = (symbol: TSSymbol, seen: Set<number>): readonly string[] => {
389
400
  const known = named.get(symbol.id);
@@ -398,7 +409,7 @@ function callsByDeclaration<Brand>(project: ProjectView<Brand>): CallsOf {
398
409
  const bound = node === undefined ? { symbols: [] } : boundTo(project, node);
399
410
  const copied = [
400
411
  ...bound.symbols,
401
- ...(bound.written === undefined ? [] : leavesPassed(bound.written)).flatMap((value) => {
412
+ ...(bound.written === undefined ? [] : carriedValues(bound.written)).flatMap((value) => {
402
413
  const { named: next, typed } = heldBy(project, value);
403
414
  return [...typed, ...(next === undefined ? [] : [next])];
404
415
  }),
@@ -408,21 +419,17 @@ function callsByDeclaration<Brand>(project: ProjectView<Brand>): CallsOf {
408
419
  named.set(symbol.id, keys);
409
420
  return keys;
410
421
  };
411
- const calleeOf = (call: CallExpression): TSSymbol | undefined => {
412
- const name = calleeName(call.expression);
413
- return name === undefined ? undefined : callees.get(name);
414
- };
415
422
  const declared = (call: CallExpression): boolean =>
416
- ((calleeOf(call)?.flags ?? 0) & (SymbolFlags.Function | SymbolFlags.Method)) !== 0;
423
+ ((invoked.callee(call)?.flags ?? 0) & (SymbolFlags.Function | SymbolFlags.Method)) !== 0;
417
424
  const typed = calls.filter((call) => !declared(call));
418
425
  const types = new Map(
419
426
  project.queries
420
- .typesAt(typed.map((call) => unwrapped(call.expression)))
427
+ .typesAt(typed.map((call) => unwrapped(invoked.of(call).callee)))
421
428
  .map((type, index) => [typed[index], must(type)]),
422
429
  );
423
430
  const found = new Map<string, CallExpression[]>();
424
431
  for (const call of calls) {
425
- const callee = calleeOf(call);
432
+ const callee = invoked.callee(call);
426
433
  const held = [
427
434
  ...(callee === undefined ? [] : [callee]),
428
435
  ...(types.has(call) ? heldIn(project, types.get(call)) : []),
@@ -436,15 +443,17 @@ function callsByDeclaration<Brand>(project: ProjectView<Brand>): CallsOf {
436
443
 
437
444
  /**
438
445
  * What each call of the function a parameter belongs to passes at the parameter's place,
439
- * every argument from there on for a rest parameter. The function is the one the checker's
440
- * type at its own declaration names, so a function with no name of its own is called too.
441
- * A `this` parameter takes no place, and an argument at or after a spread argument passes
442
- * at every place the spread's length can move it to.
446
+ * every argument from there on for a rest parameter, as the call's invocation binds them.
447
+ * The function is the one the checker's type at its own declaration names, so a function
448
+ * with no name of its own is called too. A `this` parameter takes no place, and an
449
+ * argument at or after a spread argument passes at every place the spread's length can
450
+ * move it to.
443
451
  */
444
452
  function argumentsAt<Brand>(
445
453
  project: ProjectView<Brand>,
446
454
  parameter: ParameterDeclaration,
447
455
  callsOfFunction: (symbol: TSSymbol) => readonly CallExpression[],
456
+ invoked: Invocations,
448
457
  ): readonly Expression[] {
449
458
  const owner = parameter.parent;
450
459
  if (
@@ -462,7 +471,7 @@ function argumentsAt<Brand>(
462
471
  const rest = parameter.dotDotDotToken !== undefined;
463
472
  return [...calls].flatMap((call) => {
464
473
  let spreads = 0;
465
- return call.arguments.flatMap((argument, index) => {
474
+ return invoked.of(call).arguments.flatMap((argument, index) => {
466
475
  const spread = isSpreadElement(argument);
467
476
  const earliest = index - spreads;
468
477
  spreads += spread ? 1 : 0;
@@ -472,46 +481,3 @@ function argumentsAt<Brand>(
472
481
  });
473
482
  });
474
483
  }
475
-
476
- /** One class declaration of the target and the decorators written on it. */
477
- interface DecoratedClass {
478
- readonly id: string;
479
- readonly decorators: readonly Decorator[];
480
- }
481
-
482
- /** Every class declaration of the target that carries a decorator. */
483
- export function decoratedClasses<Brand>(
484
- project: ProjectView<Brand>,
485
- held: Inventory,
486
- ): readonly DecoratedClass[] {
487
- const found: DecoratedClass[] = [];
488
- for (const file of project.ownSourceFiles()) {
489
- const visit = (node: Node): void => {
490
- if (isClassDeclaration(node)) {
491
- const id = held.declarations.get(nodeKey(file, node));
492
- const decorators = decoratorsOf(node);
493
- if (id !== undefined && decorators.length > 0) {
494
- found.push({ id, decorators });
495
- }
496
- }
497
- node.forEachChild(visit);
498
- };
499
- file.forEachChild(visit);
500
- }
501
- return found;
502
- }
503
-
504
- /** Every call expression of the project's own files, decorators written as calls included. */
505
- export function callsOf<Brand>(project: ProjectView<Brand>): readonly CallExpression[] {
506
- const found: CallExpression[] = [];
507
- for (const file of project.ownSourceFiles()) {
508
- const visit = (node: Node): void => {
509
- if (isCallExpression(node)) {
510
- found.push(node);
511
- }
512
- node.forEachChild(visit);
513
- };
514
- file.forEachChild(visit);
515
- }
516
- return found;
517
- }
@@ -1,4 +1,4 @@
1
- import { EXEMPTION_CLASSES, type Language } from "./contract/contract.generated.ts";
1
+ import { EXEMPTION_CLASSES, type Language } from "../contract/contract.generated.ts";
2
2
 
3
3
  /**
4
4
  * Whether a class may retain a member declared with the `private` modifier, and one
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The decorator class: a member a decorator expression is attached to, and every member
3
+ * of a class one is attached to but the member a parameter property declares, is held
4
+ * back, because the decorator may reach the member by its name. The rule is read from
5
+ * the tree alone. The decorator's own use of what it decorates is the reference pass's
6
+ * `decorator` reference; this class records only that the declaration is held back,
7
+ * static members of a decorated class included.
8
+ */
9
+
10
+ import { decoratorsOf, spelledDecorator } from "./calls.ts";
11
+ import type { DetectorInput, Evidence } from "./lifecycle.ts";
12
+ import { isParameterProperty, type InventorySymbol, type SymbolKind } from "../inventory.ts";
13
+ import { renderPosition } from "../position.ts";
14
+
15
+ /** The kinds of declaration a decorator is attached to and holds back. */
16
+ const DECORATED: ReadonlySet<SymbolKind> = new Set(["class", "method", "class-member"]);
17
+
18
+ /**
19
+ * The decorator detector: each decorated member, and each member of a decorated
20
+ * class, recorded at the decorator.
21
+ */
22
+ export function decorator<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
23
+ const { held, sites, targetRoot } = input;
24
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
25
+ const membersOf = new Map<string, InventorySymbol[]>();
26
+ for (const symbol of held.symbols) {
27
+ if (symbol.kind === "method" || symbol.kind === "class-member") {
28
+ const members = membersOf.get(symbol.parent);
29
+ if (members === undefined) {
30
+ membersOf.set(symbol.parent, [symbol]);
31
+ } else {
32
+ members.push(symbol);
33
+ }
34
+ }
35
+ }
36
+ // A decorated class's container passes each constructor argument, which keeps the
37
+ // parameter, not the member a parameter property stores it in. A parameter property's
38
+ // decorator is attached to its parameter, and to no member.
39
+ return sites.decorated.flatMap(({ id, node }) => {
40
+ const symbol = byId.get(id);
41
+ if (symbol === undefined || !DECORATED.has(symbol.kind) || isParameterProperty(node)) {
42
+ return [];
43
+ }
44
+ const members =
45
+ symbol.kind === "class"
46
+ ? (membersOf.get(symbol.id) ?? []).filter(
47
+ (member) => !sites.parameterProperties.has(member.id),
48
+ )
49
+ : [symbol];
50
+ return decoratorsOf(node).flatMap((attached) => {
51
+ const detail = `decorated by ${spelledDecorator(attached)}`;
52
+ const site = renderPosition(node.getSourceFile(), targetRoot, attached.getStart());
53
+ return members.map((member) => ({ id: member.id, detail, site }));
54
+ });
55
+ });
56
+ }