@cplieger/deadset-ts 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +70 -14
  3. package/dist/src/components.js +66 -107
  4. package/dist/src/config.js +6 -0
  5. package/dist/src/dependencies.js +493 -0
  6. package/dist/src/deprecation.js +83 -0
  7. package/dist/src/discover.js +1 -1
  8. package/dist/src/enum-group.js +217 -0
  9. package/dist/src/file-facts.js +101 -0
  10. package/dist/src/findings/boundary.js +1 -0
  11. package/dist/src/findings/completion.js +99 -0
  12. package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
  13. package/dist/src/findings/emitters.js +8 -0
  14. package/dist/src/findings/interfaces.js +95 -2
  15. package/dist/src/findings/non-code-artifacts.js +97 -1
  16. package/dist/src/findings/reads-and-writes.js +199 -2
  17. package/dist/src/findings/unused-declarations.js +182 -1
  18. package/dist/src/findings/visibility-narrowing.js +239 -2
  19. package/dist/src/implementations.js +210 -0
  20. package/dist/src/inventory.js +2 -2
  21. package/dist/src/kinds.js +40 -0
  22. package/dist/src/print.js +10 -1
  23. package/dist/src/resolve.js +91 -10
  24. package/dist/src/run.js +9 -3
  25. package/dist/src/schema.js +37 -1
  26. package/dist/src/scope.js +55 -21
  27. package/dist/src/serialization-contract.js +109 -19
  28. package/dist/src/stores.js +32 -0
  29. package/dist/src/version.js +1 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/analysis.ts +113 -16
  33. package/src/components.ts +73 -131
  34. package/src/config.ts +37 -2
  35. package/src/dependencies.ts +617 -0
  36. package/src/deprecation.ts +101 -0
  37. package/src/discover.ts +1 -1
  38. package/src/enum-group.ts +265 -0
  39. package/src/file-facts.ts +143 -0
  40. package/src/findings/boundary.ts +39 -0
  41. package/src/findings/completion.ts +151 -0
  42. package/src/findings/dependencies-and-module-machinery.ts +79 -1
  43. package/src/findings/emitter.ts +21 -1
  44. package/src/findings/emitters.ts +13 -1
  45. package/src/findings/interfaces.ts +149 -3
  46. package/src/findings/non-code-artifacts.ts +124 -2
  47. package/src/findings/reads-and-writes.ts +310 -3
  48. package/src/findings/unused-declarations.ts +223 -2
  49. package/src/findings/visibility-narrowing.ts +324 -3
  50. package/src/implementations.ts +286 -0
  51. package/src/inventory.ts +2 -2
  52. package/src/kinds.ts +41 -0
  53. package/src/print.ts +17 -1
  54. package/src/resolve.ts +124 -10
  55. package/src/run.ts +10 -3
  56. package/src/schema.ts +47 -2
  57. package/src/scope.ts +63 -22
  58. package/src/serialization-contract.ts +138 -20
  59. package/src/stores.ts +77 -0
  60. package/src/version.ts +1 -1
@@ -3,11 +3,15 @@
3
3
  * `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
4
4
  * `toString` where a value is passed to an `unknown` or `any` parameter of a function
5
5
  * outside the analysis. A function of the project handing such a parameter of its own
6
- * to a destination is one, holding back what that destination does, to a fixpoint.
6
+ * to a destination is one, holding back what that destination does, to a fixpoint. A
7
+ * function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
8
+ * given the arguments those methods pass it.
7
9
  */
8
10
 
9
11
  import {
12
+ isArrayLiteralExpression,
10
13
  isArrowFunction,
14
+ isCallExpression,
11
15
  isClassDeclaration,
12
16
  isClassExpression,
13
17
  isClassStaticBlockDeclaration,
@@ -20,6 +24,7 @@ import {
20
24
  isNumericLiteral,
21
25
  isObjectLiteralExpression,
22
26
  isParenthesizedExpression,
27
+ isPropertyAccessExpression,
23
28
  isSetAccessorDeclaration,
24
29
  isSpreadElement,
25
30
  isStringLiteral,
@@ -32,6 +37,7 @@ import {
32
37
  type ParameterDeclaration,
33
38
  } from "@typescript/native/unstable/ast";
34
39
  import {
40
+ SignatureKind,
35
41
  SymbolFlags,
36
42
  TypeFlags,
37
43
  type Symbol as TSSymbol,
@@ -164,6 +170,76 @@ function evaluated(argument: Expression): Expression {
164
170
  return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
165
171
  }
166
172
 
173
+ /** The methods of `Function.prototype` that invoke a function with arguments of their own. */
174
+ const FUNCTION_METHODS: ReadonlySet<string> = new Set(["call", "apply", "bind"]);
175
+
176
+ /** The interfaces of the standard library that declare those methods for a function value. */
177
+ const FUNCTION_INTERFACES: ReadonlySet<string> = new Set([
178
+ "Function",
179
+ "CallableFunction",
180
+ "NewableFunction",
181
+ ]);
182
+
183
+ /**
184
+ * One call as the function it invokes sees it: the expression naming that function,
185
+ * and the argument each of its parameters binds, in order.
186
+ */
187
+ interface Invocation {
188
+ readonly callee: Expression;
189
+ readonly arguments: readonly Expression[];
190
+ /** Whether the call reaches the callee through a method of `Function.prototype`. */
191
+ readonly indirect: boolean;
192
+ }
193
+
194
+ /** A call through a method of `Function.prototype`: the method's name and the invocation. */
195
+ interface MethodForm {
196
+ readonly method: Node;
197
+ readonly invocation: Invocation;
198
+ }
199
+
200
+ function unparenthesized(expression: Expression): Expression {
201
+ return isParenthesizedExpression(expression)
202
+ ? unparenthesized(expression.expression)
203
+ : expression;
204
+ }
205
+
206
+ /**
207
+ * The invocation a call writes through a method of `Function.prototype`, read from
208
+ * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
209
+ * binds `a`, and `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
210
+ * that is no array literal is the value of the first parameter, whose elements it
211
+ * carries. Whether the method is the prototype's is resolution's to say.
212
+ */
213
+ function throughMethod(call: CallExpression): MethodForm | undefined {
214
+ const expression = unparenthesized(call.expression);
215
+ if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
216
+ const [, ...rest] = call.arguments;
217
+ let passed: readonly Expression[] = rest;
218
+ if (expression.name.text === "apply") {
219
+ const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
220
+ passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
221
+ }
222
+ return {
223
+ method: expression.name,
224
+ invocation: { callee: expression.expression, arguments: passed, indirect: true },
225
+ };
226
+ }
227
+ if (isCallExpression(expression)) {
228
+ const bound = unparenthesized(expression.expression);
229
+ if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
230
+ return {
231
+ method: bound.name,
232
+ invocation: {
233
+ callee: bound.expression,
234
+ arguments: [...expression.arguments.slice(1), ...call.arguments],
235
+ indirect: true,
236
+ },
237
+ };
238
+ }
239
+ }
240
+ return undefined;
241
+ }
242
+
167
243
  /** What one walk of the project's own files found: its calls and its functions' parameters. */
168
244
  interface Walked {
169
245
  readonly sites: readonly Site[];
@@ -256,10 +332,11 @@ function handedOn<Brand>(
256
332
  project: ProjectView<Brand>,
257
333
  sites: readonly Site[],
258
334
  erased: ReadonlyMap<number, Parameter>,
335
+ invocationOf: (call: CallExpression) => Invocation,
259
336
  ): readonly Forwarded[] {
260
337
  const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
261
338
  const candidates = sites.flatMap((site) =>
262
- site.call.arguments.flatMap((argument, index) =>
339
+ invocationOf(site.call).arguments.flatMap((argument, index) =>
263
340
  valuesPassed(argument)
264
341
  .filter((value) => spellings.has(value.name.getText()))
265
342
  .map((value) => ({ site, argument: index, value })),
@@ -279,6 +356,8 @@ function handedOn<Brand>(
279
356
 
280
357
  /** The destination of each call's arguments, with the project's forwarding functions settled. */
281
358
  interface Destinations {
359
+ /** The function one call invokes and the arguments its parameters bind. */
360
+ invocation(call: CallExpression): Invocation;
282
361
  /** The destination one argument of one call is, if it is one. */
283
362
  at(call: CallExpression, argument: number): Destination | undefined;
284
363
  /** Whether a call can be a destination at any argument, read without a round trip. */
@@ -304,14 +383,20 @@ function destinations<Brand>(
304
383
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
305
384
  namesADeclaration,
306
385
  );
307
- const callees = resolvedTargets(
308
- project,
309
- walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
310
- );
311
- const calleeOf = (call: CallExpression): TSSymbol | undefined => {
312
- const name = calleeName(call.expression);
313
- return name === undefined ? undefined : callees.get(name);
314
- };
386
+ const forms = new Map<CallExpression, MethodForm>();
387
+ for (const site of walked.sites) {
388
+ const form = throughMethod(site.call);
389
+ if (form !== undefined) {
390
+ forms.set(site.call, form);
391
+ }
392
+ }
393
+ const callees = resolvedTargets(project, [
394
+ ...walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
395
+ ...[...forms.values()].flatMap((form) => {
396
+ const name = calleeName(form.invocation.callee);
397
+ return name === undefined ? [form.method] : [form.method, name];
398
+ }),
399
+ ]);
315
400
  const erased = erasedParameters(project, walked.parameters);
316
401
  const byFunction = new Map<string, Parameter[]>();
317
402
  for (const parameter of erased.values()) {
@@ -324,17 +409,47 @@ function destinations<Brand>(
324
409
  !own.has(handle.path) &&
325
410
  !input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)),
326
411
  );
412
+ const invocation = (call: CallExpression): Invocation => {
413
+ const form = forms.get(call);
414
+ const method = form === undefined ? undefined : callees.get(form.method);
415
+ // Only the standard library's function methods pass their arguments on: the
416
+ // program's own `call` and `Reflect.apply` invoke nothing here.
417
+ if (
418
+ form !== undefined &&
419
+ method !== undefined &&
420
+ outside(method) &&
421
+ FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")
422
+ ) {
423
+ return form.invocation;
424
+ }
425
+ return { callee: call.expression, arguments: call.arguments, indirect: false };
426
+ };
427
+ const calleeOf = (call: CallExpression): TSSymbol | undefined => {
428
+ const name = calleeName(invocation(call).callee);
429
+ return name === undefined ? undefined : callees.get(name);
430
+ };
327
431
  const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
328
432
  const erasedAt = (call: CallExpression, argument: number): boolean => {
329
433
  let indexes = erasedArguments.get(call);
330
434
  if (indexes === undefined) {
331
- const signature = checker.getResolvedSignature(call);
435
+ const invoked = invocation(call);
436
+ // Through a method of the prototype the resolved signature is the method's, so
437
+ // the parameters are read off the callee's own signatures, any one erasing.
438
+ const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
439
+ const signatures = invoked.indirect
440
+ ? type === undefined
441
+ ? []
442
+ : checker.getSignaturesOfType(type, SignatureKind.Call)
443
+ : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
332
444
  indexes = new Set(
333
- call.arguments.flatMap((_unused, index) => {
334
- const type =
335
- signature === undefined ? undefined : checker.getParameterType(signature, index);
336
- return type !== undefined && (type.flags & ERASED) !== 0 ? [index] : [];
337
- }),
445
+ invoked.arguments.flatMap((_unused, index) =>
446
+ signatures.some((signature) => {
447
+ const parameter = checker.getParameterType(signature, index);
448
+ return parameter !== undefined && (parameter.flags & ERASED) !== 0;
449
+ })
450
+ ? [index]
451
+ : [],
452
+ ),
338
453
  );
339
454
  erasedArguments.set(call, indexes);
340
455
  }
@@ -355,7 +470,10 @@ function destinations<Brand>(
355
470
  const declared = chains.declarationsOf(target);
356
471
  if (declared.length === 0) {
357
472
  return outside(target) && erasedAt(call, argument)
358
- ? { reach: "conversions", callee: call.expression.getText().replace(/\s+/gu, "") }
473
+ ? {
474
+ reach: "conversions",
475
+ callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
476
+ }
359
477
  : undefined;
360
478
  }
361
479
  for (const fn of declared) {
@@ -370,7 +488,7 @@ function destinations<Brand>(
370
488
  return undefined;
371
489
  };
372
490
 
373
- const forwarded = handedOn(project, walked.sites, erased);
491
+ const forwarded = handedOn(project, walked.sites, erased, invocation);
374
492
  for (let moved = true; moved;) {
375
493
  moved = false;
376
494
  for (const one of forwarded) {
@@ -400,7 +518,7 @@ function destinations<Brand>(
400
518
  (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))),
401
519
  );
402
520
  };
403
- return { at, mayReceive };
521
+ return { invocation, at, mayReceive };
404
522
  }
405
523
 
406
524
  /** One argument whose value carries classes of the target into a destination. */
@@ -420,7 +538,7 @@ function flowsOf<Brand>(
420
538
  const reach = valueReach(project, held);
421
539
  const arguments_ = sites.flatMap((site) =>
422
540
  to.mayReceive(site.call)
423
- ? site.call.arguments.flatMap((argument, index) => {
541
+ ? to.invocation(site.call).arguments.flatMap((argument, index) => {
424
542
  const value = evaluated(argument);
425
543
  return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
426
544
  })
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
+ }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "3.1.0";
7
+ export const CONTRACT_VERSION = "3.2.0";