@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3

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 (66) hide show
  1. package/dist/src/analysis.js +30 -6
  2. package/dist/src/augmentations.js +105 -12
  3. package/dist/src/build-scripts.js +437 -0
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/convention-rows.js +62 -2
  6. package/dist/src/conventions.js +15 -0
  7. package/dist/src/dependencies.js +89 -0
  8. package/dist/src/entry-points.js +9 -18
  9. package/dist/src/findings/completion.js +122 -49
  10. package/dist/src/findings/intra-function.js +4 -2
  11. package/dist/src/findings/reads-and-writes.js +7 -6
  12. package/dist/src/findings/unused-declarations.js +13 -5
  13. package/dist/src/findings/visibility-narrowing.js +8 -3
  14. package/dist/src/graph.js +5 -3
  15. package/dist/src/intra-function-parts.js +45 -17
  16. package/dist/src/kinds.js +8 -8
  17. package/dist/src/manifest.js +18 -0
  18. package/dist/src/matrix.js +76 -4
  19. package/dist/src/references.js +34 -3
  20. package/dist/src/report.js +23 -4
  21. package/dist/src/reporters/sarif.js +12 -8
  22. package/dist/src/roots.js +138 -11
  23. package/dist/src/serialization-contract.js +258 -8
  24. package/dist/src/suppress.js +1 -0
  25. package/dist/src/sweep.js +7 -9
  26. package/dist/src/test-globals.js +234 -0
  27. package/dist/src/test-support.js +8 -11
  28. package/dist/src/type-errors.js +11 -3
  29. package/dist/src/unseen-loads.js +107 -0
  30. package/dist/src/verbs/analyze.js +24 -2
  31. package/dist/src/yaml-document.js +355 -0
  32. package/jsr.json +1 -1
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -8
  35. package/src/augmentations.ts +134 -16
  36. package/src/build-scripts.ts +564 -0
  37. package/src/conformance-record.ts +1 -1
  38. package/src/convention-rows.ts +72 -2
  39. package/src/conventions.ts +16 -0
  40. package/src/dependencies.ts +96 -0
  41. package/src/entry-points.ts +10 -25
  42. package/src/file-facts.ts +3 -0
  43. package/src/finding.ts +5 -0
  44. package/src/findings/completion.ts +153 -56
  45. package/src/findings/intra-function.ts +4 -2
  46. package/src/findings/reads-and-writes.ts +7 -6
  47. package/src/findings/unused-declarations.ts +17 -5
  48. package/src/findings/visibility-narrowing.ts +9 -3
  49. package/src/graph.ts +5 -3
  50. package/src/intra-function-parts.ts +55 -16
  51. package/src/kinds.ts +8 -8
  52. package/src/manifest.ts +21 -3
  53. package/src/matrix.ts +122 -4
  54. package/src/references.ts +38 -2
  55. package/src/report.ts +56 -16
  56. package/src/reporters/sarif.ts +16 -8
  57. package/src/roots.ts +162 -12
  58. package/src/serialization-contract.ts +309 -7
  59. package/src/suppress.ts +1 -0
  60. package/src/sweep.ts +21 -9
  61. package/src/test-globals.ts +275 -0
  62. package/src/test-support.ts +17 -13
  63. package/src/type-errors.ts +14 -3
  64. package/src/unseen-loads.ts +145 -0
  65. package/src/verbs/analyze.ts +28 -2
  66. package/src/yaml-document.ts +377 -0
@@ -17,6 +17,7 @@ import {
17
17
  isClassStaticBlockDeclaration,
18
18
  isFunctionDeclaration,
19
19
  isFunctionExpression,
20
+ isFunctionLikeDeclaration,
20
21
  isGetAccessorDeclaration,
21
22
  isIdentifier,
22
23
  isInterfaceDeclaration,
@@ -25,6 +26,7 @@ import {
25
26
  isNumericLiteral,
26
27
  isObjectLiteralExpression,
27
28
  isParenthesizedExpression,
29
+ isForInStatement,
28
30
  isPropertyAccessExpression,
29
31
  isPropertyDeclaration,
30
32
  isSatisfiesExpression,
@@ -307,9 +309,37 @@ function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walke
307
309
  return { sites, parameters };
308
310
  }
309
311
 
312
+ /** The calls of `Object` that read every own property of their argument by name. */
313
+ const ENUMERATIONS: ReadonlySet<string> = new Set([
314
+ "keys",
315
+ "entries",
316
+ "values",
317
+ "getOwnPropertyNames",
318
+ "assign",
319
+ ]);
320
+
321
+ /**
322
+ * Whether a parameter type keeps no member name of the value it is given: `unknown`, `any`,
323
+ * or an object type of a string index signature and no property.
324
+ */
325
+ function keepsNoName<Brand>(project: ProjectView<Brand>, type: Type): boolean {
326
+ if ((type.flags & ERASED) !== 0) {
327
+ return true;
328
+ }
329
+ if (
330
+ (type.flags & TypeFlags.Object) === 0 ||
331
+ must(project.queries.propertiesOf(type)).length > 0
332
+ ) {
333
+ return false;
334
+ }
335
+ return must(project.queries.indexInfosOf(type)).some(
336
+ (info) => (info.keyType.flags & TypeFlags.String) !== 0,
337
+ );
338
+ }
339
+
310
340
  /**
311
- * The parameters that keep nothing of the value they are given: one typed `unknown` or
312
- * `any`, and a rest parameter whose elements are, each by its symbol.
341
+ * The parameters that keep no member name of the value they are given, as `keepsNoName`
342
+ * reads it, and a rest parameter whose elements keep none, each by its symbol.
313
343
  */
314
344
  function erasedParameters<Brand>(
315
345
  project: ProjectView<Brand>,
@@ -325,10 +355,10 @@ function erasedParameters<Brand>(
325
355
  return false;
326
356
  }
327
357
  if (!parameter.rest) {
328
- return (type.flags & ERASED) !== 0;
358
+ return keepsNoName(project, type);
329
359
  }
330
360
  const [element] = must(project.queries.typeArguments(type));
331
- return element !== undefined && (element.flags & ERASED) !== 0;
361
+ return element !== undefined && keepsNoName(project, element);
332
362
  });
333
363
  const bySymbol = new Map<number, Parameter>();
334
364
  project
@@ -343,6 +373,124 @@ function erasedParameters<Brand>(
343
373
  return bySymbol;
344
374
  }
345
375
 
376
+ /** Whether one node is a function, whose body runs only where something calls it. */
377
+ function isFunctionNode(node: Node): boolean {
378
+ return (
379
+ isFunctionDeclaration(node) ||
380
+ isFunctionExpression(node) ||
381
+ isArrowFunction(node) ||
382
+ isMethodDeclaration(node) ||
383
+ isGetAccessorDeclaration(node) ||
384
+ isSetAccessorDeclaration(node)
385
+ );
386
+ }
387
+
388
+ /** The name a function nested in another is called by: its own, or the variable it initializes. */
389
+ function nestedName(fn: Node): string | undefined {
390
+ if (isFunctionDeclaration(fn)) {
391
+ return fn.name?.text;
392
+ }
393
+ let at = fn.parent;
394
+ while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
395
+ at = at.parent;
396
+ }
397
+ return isVariableDeclaration(at) && isIdentifier(at.name) ? at.name.text : undefined;
398
+ }
399
+
400
+ /** The names one function's body writes as identifiers, other than the names its declarations bind. */
401
+ function namesWritten(fn: Node): ReadonlySet<string> {
402
+ const found = new Set<string>();
403
+ const visit = (node: Node): void => {
404
+ const declares =
405
+ (isFunctionDeclaration(node.parent) || isVariableDeclaration(node.parent)) &&
406
+ node.parent.name === node;
407
+ if (isIdentifier(node) && !declares) {
408
+ found.add(node.text);
409
+ }
410
+ node.forEachChild(visit);
411
+ };
412
+ fn.forEachChild(visit);
413
+ return found;
414
+ }
415
+
416
+ /**
417
+ * The erased parameters their own function enumerates: one an `Object.keys`, `entries`,
418
+ * `values`, `getOwnPropertyNames` or `assign` call takes, `Object` the standard library's,
419
+ * or a `for…in` statement walks, which reads every property of the value by its name. A
420
+ * nested function is read as the function's own only where its name is written in it, or
421
+ * it has none, a callback or a function called where it is written.
422
+ */
423
+ function enumeratedParameters<Brand>(
424
+ project: ProjectView<Brand>,
425
+ erased: ReadonlyMap<number, Parameter>,
426
+ ): readonly Parameter[] {
427
+ const asked: {
428
+ readonly node: Node;
429
+ readonly parameter: Parameter;
430
+ readonly object: Node | undefined;
431
+ }[] = [];
432
+ for (const parameter of erased.values()) {
433
+ const fn = parameter.name.parent.parent;
434
+ const spelled = parameter.name.getText();
435
+ const written = namesWritten(fn);
436
+ const visit = (node: Node): void => {
437
+ if (isFunctionNode(node)) {
438
+ const name = nestedName(node);
439
+ if (name !== undefined && !written.has(name)) {
440
+ return;
441
+ }
442
+ }
443
+ const read: Expression[] = [];
444
+ let object: Node | undefined;
445
+ if (isCallExpression(node)) {
446
+ const callee = node.expression;
447
+ if (
448
+ isPropertyAccessExpression(callee) &&
449
+ isIdentifier(callee.expression) &&
450
+ callee.expression.text === "Object" &&
451
+ ENUMERATIONS.has(callee.name.text)
452
+ ) {
453
+ object = callee.expression;
454
+ // `Object.assign` reads its sources, every argument after the target.
455
+ read.push(...node.arguments.slice(...(callee.name.text === "assign" ? [1] : [0, 1])));
456
+ }
457
+ } else if (isForInStatement(node)) {
458
+ read.push(node.expression);
459
+ }
460
+ for (const value of read.map(evaluated)) {
461
+ if (isIdentifier(value) && value.text === spelled) {
462
+ asked.push({ node: value, parameter, object });
463
+ }
464
+ }
465
+ node.forEachChild(visit);
466
+ };
467
+ fn.forEachChild(visit);
468
+ }
469
+ const symbols = project.symbolsAt(asked.map((one) => project.handle(one.node))).map(must);
470
+ const objects = asked.flatMap((one) => (one.object === undefined ? [] : [one.object]));
471
+ const own = project.ownPaths();
472
+ const standard = new Set(
473
+ project
474
+ .symbolsAt(objects.map((node) => project.handle(node)))
475
+ .map(must)
476
+ .flatMap((symbol, at) =>
477
+ symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [objects[at]] : [],
478
+ ),
479
+ );
480
+ return [
481
+ ...new Set(
482
+ asked.flatMap((one, at) => {
483
+ const symbol = symbols[at];
484
+ return symbol !== undefined &&
485
+ erased.get(symbol.id) === one.parameter &&
486
+ (one.object === undefined || standard.has(one.object))
487
+ ? [one.parameter]
488
+ : [];
489
+ }),
490
+ ),
491
+ ];
492
+ }
493
+
346
494
  /** Each place a call's argument passes one of the erased parameters it can see. */
347
495
  function handedOn<Brand>(
348
496
  project: ProjectView<Brand>,
@@ -452,8 +600,8 @@ interface Destinations {
452
600
  * callee is a serializer an entry names; at the arguments its resolved signature types
453
601
  * `unknown` or `any` where its callee is declared outside the analysis or only ambiently
454
602
  * with no implementation in it; and, where its callee is a function of the project, at each
455
- * parameter the function hands on to a destination, holding back the widest of what those
456
- * destinations hold back, which is settled by widening until no parameter moves.
603
+ * parameter the function enumerates or hands on to a destination, holding back the widest
604
+ * of what those hold back, settled by widening until no parameter moves.
457
605
  */
458
606
  function destinations<Brand>(
459
607
  input: DetectorInput<Brand>,
@@ -674,6 +822,9 @@ function destinations<Brand>(
674
822
  return undefined;
675
823
  };
676
824
 
825
+ for (const parameter of enumeratedParameters(project, erased)) {
826
+ forwards.set(keyOf(parameter), "properties");
827
+ }
677
828
  const forwarded = handedOn(project, walked.sites, erased, invocation);
678
829
  for (let moved = true; moved;) {
679
830
  moved = false;
@@ -776,6 +927,19 @@ function flowsOf<Brand>(
776
927
  },
777
928
  ];
778
929
  });
930
+ const generic = typeParameterFlows(
931
+ project,
932
+ reach,
933
+ sites,
934
+ arguments_.flatMap((one) => {
935
+ const type = typeOf.get(one.value);
936
+ const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
937
+ const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
938
+ return destination === undefined
939
+ ? []
940
+ : parameters.map((parameter) => ({ parameter, to: destination }));
941
+ }),
942
+ );
779
943
  const left = stores.outside.flatMap((store) => {
780
944
  const reached = reach.reachedFrom(store.type);
781
945
  return reached.classes.size === 0 && reached.members.length === 0
@@ -790,7 +954,145 @@ function flowsOf<Brand>(
790
954
  },
791
955
  ];
792
956
  });
793
- return [...passed, ...left];
957
+ return [...passed, ...generic, ...left];
958
+ }
959
+
960
+ /** The type parameters one type names, through constituents and type arguments. */
961
+ function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth: number): Type[] {
962
+ if (type.isTypeParameter()) {
963
+ return [type];
964
+ }
965
+ if (depth >= MENTION_DEPTH) {
966
+ return [];
967
+ }
968
+ const { queries } = project;
969
+ return [
970
+ ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
971
+ ...must(queries.typeArguments(type)),
972
+ ].flatMap((one) => typeParametersOf(project, one, depth + 1));
973
+ }
974
+
975
+ /**
976
+ * The types an instantiated type puts where a declared type names `parameter`: matched
977
+ * through type arguments, and in a union every constituent the declared union does not
978
+ * itself name.
979
+ */
980
+ function bound<Brand>(
981
+ project: ProjectView<Brand>,
982
+ declared: Type,
983
+ instantiated: Type,
984
+ parameter: Type,
985
+ depth: number,
986
+ ): Type[] {
987
+ if (declared.id === parameter.id) {
988
+ return [instantiated];
989
+ }
990
+ if (depth >= MENTION_DEPTH) {
991
+ return [];
992
+ }
993
+ const { queries } = project;
994
+ if (declared.isUnionType()) {
995
+ const parts = must(queries.constituents(declared));
996
+ if (parts.some((part) => part.id === parameter.id)) {
997
+ const kept = new Set(parts.map((part) => part.id));
998
+ const given = instantiated.isUnionType()
999
+ ? must(queries.constituents(instantiated))
1000
+ : [instantiated];
1001
+ return given.filter((part) => !kept.has(part.id));
1002
+ }
1003
+ }
1004
+ const declaredArguments = must(queries.typeArguments(declared));
1005
+ const givenArguments = must(queries.typeArguments(instantiated));
1006
+ return declaredArguments.length !== givenArguments.length
1007
+ ? []
1008
+ : declaredArguments.flatMap((one, at) => {
1009
+ const given = givenArguments[at];
1010
+ return given === undefined ? [] : bound(project, one, given, parameter, depth + 1);
1011
+ });
1012
+ }
1013
+
1014
+ /**
1015
+ * The flows a type parameter of a function of the project carries where a value its type
1016
+ * names reaches a destination: each call of the function passes its type argument there,
1017
+ * written or inferred from the arguments, and a type argument that is a type parameter
1018
+ * of the calling function is followed on to that function's calls, to a fixpoint.
1019
+ */
1020
+ function typeParameterFlows<Brand>(
1021
+ project: ProjectView<Brand>,
1022
+ reach: ReturnType<typeof valueReach>,
1023
+ sites: readonly Site[],
1024
+ pending: { readonly parameter: Type; readonly to: Destination }[],
1025
+ ): readonly Flow[] {
1026
+ const { queries } = project;
1027
+ const byName = new Map<string, Site[]>();
1028
+ for (const site of sites) {
1029
+ const name = calleeName(site.call.expression)?.getText();
1030
+ if (name !== undefined) {
1031
+ byName.set(name, [...(byName.get(name) ?? []), site]);
1032
+ }
1033
+ }
1034
+ const found: Flow[] = [];
1035
+ const seen = new Set<number>();
1036
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
1037
+ const { parameter, to } = next;
1038
+ if (seen.has(parameter.id)) {
1039
+ continue;
1040
+ }
1041
+ seen.add(parameter.id);
1042
+ const symbol = must(queries.symbolOfType(parameter));
1043
+ const [handle] = symbol?.declarations ?? [];
1044
+ const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
1045
+ const fn = declaration?.parent;
1046
+ const index =
1047
+ fn === undefined || !isFunctionLikeDeclaration(fn)
1048
+ ? -1
1049
+ : (fn.typeParameters ?? []).findIndex((one) => one === declaration);
1050
+ const name =
1051
+ fn === undefined
1052
+ ? undefined
1053
+ : isMethodDeclaration(fn) && isIdentifier(fn.name)
1054
+ ? fn.name.text
1055
+ : nestedName(fn);
1056
+ if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
1057
+ continue;
1058
+ }
1059
+ for (const site of byName.get(name) ?? []) {
1060
+ const resolved = must(queries.resolvedSignature(site.call));
1061
+ const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
1062
+ const called =
1063
+ declared?.declaration === undefined
1064
+ ? undefined
1065
+ : project.declarationAt(declared.declaration)?.node;
1066
+ if (resolved === undefined || declared === undefined || called !== fn) {
1067
+ continue;
1068
+ }
1069
+ const written = site.call.typeArguments?.[index];
1070
+ const given =
1071
+ written === undefined
1072
+ ? site.call.arguments.flatMap((_argument, at) => {
1073
+ const from = must(queries.parameterType(declared, at));
1074
+ const into = must(queries.parameterType(resolved, at));
1075
+ return from === undefined || into === undefined
1076
+ ? []
1077
+ : bound(project, from, into, parameter, 0);
1078
+ })
1079
+ : typesAt(project, [written]).filter((type) => type !== undefined);
1080
+ for (const type of given) {
1081
+ const reached = reach.reachedFrom(type);
1082
+ if (reached.classes.size > 0 || reached.members.length > 0) {
1083
+ found.push({
1084
+ site: site.call,
1085
+ classes: reached.classes,
1086
+ members: reached.members,
1087
+ to,
1088
+ detail: `passed to ${to.callee} through the type parameter ${symbol.name}`,
1089
+ });
1090
+ }
1091
+ pending.push(...typeParametersOf(project, type, 0).map((one) => ({ parameter: one, to })));
1092
+ }
1093
+ }
1094
+ }
1095
+ return found;
794
1096
  }
795
1097
 
796
1098
  /**
package/src/suppress.ts CHANGED
@@ -115,6 +115,7 @@ export const GO_REF_EXPRESSIONS: readonly string[] = [
115
115
  `^go://${GO_PATH}#${GO_IDENT}$`,
116
116
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
117
117
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
118
+ `^go://${GO_PATH}#(?:${GO_IDENT}(?:\\.${GO_IDENT})?)?\\(${GO_IDENT}\\)$`,
118
119
  `^go://${GO_PATH}#${GO_FILE}:file$`,
119
120
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
120
121
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
package/src/sweep.ts CHANGED
@@ -195,10 +195,14 @@ export function runnerCalls(symbol: InventorySymbol): boolean {
195
195
  * That run's roots are the graph's own and, of each test file, its top level and every
196
196
  * function it exports, which a test runner may call, and every export of a manual mock,
197
197
  * which the runner reads; any other export of a test file, and test-support code, which
198
- * runs where a test imports it, are judged as any file's are.
199
- * `held` seeds it as a mark or an exemption seeds a sweep.
198
+ * runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
199
+ * seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
200
200
  */
201
- export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
201
+ export function reachedWithTheTests(
202
+ graph: Graph,
203
+ held: readonly string[],
204
+ namespaces = true,
205
+ ): readonly boolean[] {
202
206
  const tests = graph.symbols.flatMap((symbol, at) => {
203
207
  const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
204
208
  const called = exported && runnerCalls(symbol);
@@ -208,9 +212,12 @@ export function reachedWithTheTests(graph: Graph, held: readonly string[]): read
208
212
  ? [{ at, kind: "test-runner" as const }]
209
213
  : [];
210
214
  });
211
- return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
212
- production: false,
213
- });
215
+ return reachable(
216
+ { ...graph, rooted: [...graph.rooted, ...tests] },
217
+ flagged(graph, held),
218
+ { production: false },
219
+ namespaces,
220
+ );
214
221
  }
215
222
 
216
223
  /**
@@ -240,7 +247,12 @@ function flagged(graph: Graph, ids: readonly string[]): boolean[] {
240
247
  * included, while a root reaches through the references the mode counts. The held seed
241
248
  * runs first, so what it reaches is expanded under the wider rule.
242
249
  */
243
- function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly boolean[] {
250
+ function reachable(
251
+ graph: Graph,
252
+ held: readonly boolean[],
253
+ mode: Mode,
254
+ namespaces = true,
255
+ ): readonly boolean[] {
244
256
  const reached = graph.symbols.map(() => false);
245
257
  const expanded = graph.symbols.map(() => false);
246
258
  const queue: number[] = [];
@@ -269,7 +281,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
269
281
  if (!holding && mode.production && edge.test) {
270
282
  continue;
271
283
  }
272
- enter(edge.to, !edge.evaluation);
284
+ enter(edge.to, namespaces && !edge.evaluation);
273
285
  }
274
286
  }
275
287
  };
@@ -285,7 +297,7 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
285
297
  }
286
298
  // A consumer's reference reaches what it names as one of the target's own does.
287
299
  for (const edge of callsOf(graph, mode)) {
288
- enter(edge.to, !edge.evaluation);
300
+ enter(edge.to, namespaces && !edge.evaluation);
289
301
  }
290
302
  walk(false);
291
303
  return reached;