@cplieger/deadset-ts 8.0.0-dev.2 → 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 (57) hide show
  1. package/dist/src/analysis.js +27 -5
  2. package/dist/src/augmentations.js +62 -0
  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 +39 -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 +62 -26
  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 +4 -1
  13. package/dist/src/graph.js +5 -3
  14. package/dist/src/intra-function-parts.js +45 -17
  15. package/dist/src/kinds.js +7 -7
  16. package/dist/src/manifest.js +18 -0
  17. package/dist/src/matrix.js +38 -4
  18. package/dist/src/report.js +23 -4
  19. package/dist/src/reporters/sarif.js +12 -8
  20. package/dist/src/roots.js +138 -11
  21. package/dist/src/serialization-contract.js +258 -8
  22. package/dist/src/sweep.js +7 -9
  23. package/dist/src/test-globals.js +234 -0
  24. package/dist/src/type-errors.js +11 -3
  25. package/dist/src/unseen-loads.js +107 -0
  26. package/dist/src/verbs/analyze.js +24 -2
  27. package/dist/src/yaml-document.js +355 -0
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +45 -7
  31. package/src/augmentations.ts +76 -1
  32. package/src/build-scripts.ts +564 -0
  33. package/src/conformance-record.ts +1 -1
  34. package/src/convention-rows.ts +47 -2
  35. package/src/conventions.ts +16 -0
  36. package/src/dependencies.ts +96 -0
  37. package/src/entry-points.ts +10 -25
  38. package/src/file-facts.ts +3 -0
  39. package/src/findings/completion.ts +72 -28
  40. package/src/findings/intra-function.ts +4 -2
  41. package/src/findings/reads-and-writes.ts +7 -6
  42. package/src/findings/unused-declarations.ts +6 -1
  43. package/src/graph.ts +5 -3
  44. package/src/intra-function-parts.ts +55 -16
  45. package/src/kinds.ts +7 -7
  46. package/src/manifest.ts +21 -3
  47. package/src/matrix.ts +51 -4
  48. package/src/report.ts +56 -16
  49. package/src/reporters/sarif.ts +16 -8
  50. package/src/roots.ts +162 -12
  51. package/src/serialization-contract.ts +309 -7
  52. package/src/sweep.ts +21 -9
  53. package/src/test-globals.ts +275 -0
  54. package/src/type-errors.ts +14 -3
  55. package/src/unseen-loads.ts +145 -0
  56. package/src/verbs/analyze.ts +28 -2
  57. package/src/yaml-document.ts +377 -0
package/src/roots.ts CHANGED
@@ -9,8 +9,17 @@
9
9
  */
10
10
 
11
11
  import {
12
+ isArrowFunction,
13
+ isAssertionExpression,
12
14
  isExpressionWithTypeArguments,
13
15
  isFunctionLikeDeclaration,
16
+ isIdentifier,
17
+ isMethodDeclaration,
18
+ isObjectLiteralExpression,
19
+ isParenthesizedExpression,
20
+ isPropertyAssignment,
21
+ isReturnStatement,
22
+ isSatisfiesExpression,
14
23
  isPropertyAccessExpression,
15
24
  isQualifiedName,
16
25
  isTypeQueryNode,
@@ -18,7 +27,10 @@ import {
18
27
  isPropertyDeclaration,
19
28
  isVariableDeclaration,
20
29
  isVariableStatement,
30
+ SyntaxKind,
31
+ type Expression,
21
32
  type Node,
33
+ type ObjectLiteralExpression,
22
34
  type SourceFile,
23
35
  } from "@typescript/native/ast";
24
36
  import { aliasChains } from "./alias-chain.ts";
@@ -67,6 +79,8 @@ export type RootKind =
67
79
  | "pattern"
68
80
  /** A declaration of a function or statement a type error skipped, and what it may have selected. */
69
81
  | "type-error"
82
+ /** An ambient global interface merging into one declared outside the target, and its holders. */
83
+ | "global-augmentation"
70
84
  /** A file a tool or the platform enters by its own convention, and what it exports where the tool reads it. */
71
85
  | EntryRule;
72
86
 
@@ -446,6 +460,12 @@ export function roots<Brand>(
446
460
  published.push(...entryFiles(entry));
447
461
  }
448
462
  }
463
+ if (options.publishedAPI) {
464
+ // A library's bundle hands what its entry exports to every program that loads it.
465
+ published.push(
466
+ ...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file),
467
+ );
468
+ }
449
469
  const publishedNames = new Set(published.map((file) => file.fileName));
450
470
  const patterned: { file: SourceFile; pattern: string }[] = [];
451
471
  for (const pattern of options.entryFiles) {
@@ -713,17 +733,26 @@ function publishedAPI<Brand>(
713
733
  const symbol = byId.get(id);
714
734
  return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
715
735
  });
716
- for (const symbol of inferredTypeSymbols(
736
+ const inferred = inferredTypeSymbols(
717
737
  project,
718
738
  round.flatMap((id) => {
719
739
  const symbol = byId.get(id);
720
740
  return symbol === undefined ? [] : nodes.nodesOf(symbol);
721
741
  }),
722
- )) {
742
+ );
743
+ for (const symbol of inferred.types) {
723
744
  for (const target of chains.chainOf(symbol)) {
724
745
  reach(target.id);
725
746
  }
726
747
  }
748
+ for (const symbol of inferred.members) {
749
+ for (const target of chains.chainOf(symbol)) {
750
+ const member = byId.get(target.id);
751
+ if (member?.visibility === "public" || member?.visibility === "protected") {
752
+ reach(target.id);
753
+ }
754
+ }
755
+ }
727
756
  project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
728
757
  if (symbol === UNANSWERED) {
729
758
  for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
@@ -783,30 +812,139 @@ function declarationNodes(
783
812
  const INFERRED_DEPTH = 4;
784
813
 
785
814
  /**
786
- * The declarations of the types the compiler infers where a declaration writes none: the
787
- * type of a variable or a property with no annotation, and the result of a function or a
788
- * method with no result annotation, each with the types its type arguments and its union
789
- * or intersection constituents name, to a fixed depth. A question the checker leaves
790
- * unanswered names nothing.
815
+ * The values one function body returns, each through the type assertions around it: the
816
+ * value a caller receives carries the operand's type whatever type the assertion or the
817
+ * signature names. A nested function's returns are its own.
818
+ */
819
+ function returnedValues(body: Node): readonly Expression[] {
820
+ const found: Expression[] = [];
821
+ const add = (value: Expression): void => {
822
+ for (let at: Expression | undefined = value; at !== undefined;) {
823
+ found.push(at);
824
+ at =
825
+ isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
826
+ ? at.expression
827
+ : undefined;
828
+ }
829
+ };
830
+ if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
831
+ return found;
832
+ }
833
+ if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
834
+ add(body as Expression);
835
+ return found;
836
+ }
837
+ const visit = (node: Node): void => {
838
+ if (isFunctionLikeDeclaration(node)) {
839
+ return;
840
+ }
841
+ if (isReturnStatement(node) && node.expression !== undefined) {
842
+ add(node.expression);
843
+ }
844
+ node.forEachChild(visit);
845
+ };
846
+ body.forEachChild(visit);
847
+ return found;
848
+ }
849
+
850
+ /** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
851
+ function returnedLiteral(value: Expression, body: Node): ObjectLiteralExpression | undefined {
852
+ if (isObjectLiteralExpression(value)) {
853
+ return value;
854
+ }
855
+ if (!isIdentifier(value)) {
856
+ return undefined;
857
+ }
858
+ let found: ObjectLiteralExpression | undefined;
859
+ const visit = (node: Node): void => {
860
+ if (
861
+ isVariableDeclaration(node) &&
862
+ isIdentifier(node.name) &&
863
+ node.name.text === value.text &&
864
+ node.initializer !== undefined
865
+ ) {
866
+ let at: Expression = node.initializer;
867
+ while (
868
+ isAssertionExpression(at) ||
869
+ isSatisfiesExpression(at) ||
870
+ isParenthesizedExpression(at)
871
+ ) {
872
+ at = at.expression;
873
+ }
874
+ found = isObjectLiteralExpression(at) ? at : found;
875
+ }
876
+ if (!isFunctionLikeDeclaration(node)) {
877
+ node.forEachChild(visit);
878
+ }
879
+ };
880
+ body.forEachChild(visit);
881
+ return found;
882
+ }
883
+
884
+ /**
885
+ * The values one function body returns, and those each method of an object literal it
886
+ * returns returns, to a fixed depth: a caller holding the literal calls its methods, and
887
+ * receives what they return.
888
+ */
889
+ function returnedThroughLiterals(body: Node, depth = 0): readonly Expression[] {
890
+ const values = returnedValues(body);
891
+ if (depth >= INFERRED_DEPTH) {
892
+ return values;
893
+ }
894
+ const found = [...values];
895
+ for (const value of values) {
896
+ for (const property of returnedLiteral(value, body)?.properties ?? []) {
897
+ const fn = isMethodDeclaration(property)
898
+ ? property
899
+ : isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
900
+ ? property.initializer
901
+ : undefined;
902
+ if (fn?.body !== undefined) {
903
+ found.push(...returnedThroughLiterals(fn.body, depth + 1));
904
+ }
905
+ }
906
+ }
907
+ return found;
908
+ }
909
+
910
+ /**
911
+ * The declarations of the types the compiler infers where a declaration writes none, and
912
+ * of the values a function returns: the type of a variable or a property with no
913
+ * annotation, the result of a function or a method with no result annotation, and the type
914
+ * of each value its body returns, each with the types its type arguments and its union or
915
+ * intersection constituents name, to a fixed depth; and as `members` the properties of each
916
+ * returned value's type. A question the checker leaves unanswered names nothing.
791
917
  */
792
918
  function inferredTypeSymbols<Brand>(
793
919
  project: ProjectView<Brand>,
794
920
  declarations: readonly Node[],
795
- ): readonly TSSymbol[] {
921
+ ): { readonly types: readonly TSSymbol[]; readonly members: readonly TSSymbol[] } {
796
922
  const { queries } = project;
797
- const valued = declarations.flatMap((node) =>
923
+ const valued: Node[] = declarations.flatMap((node) =>
798
924
  (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
799
925
  node.type === undefined &&
800
926
  !isFunctionLikeDeclaration(node.initializer ?? node)
801
927
  ? [node.name]
802
928
  : [],
803
929
  );
930
+ const returned = declarations.flatMap((node) => {
931
+ const signature = isFunctionLikeDeclaration(node)
932
+ ? node
933
+ : isVariableDeclaration(node) &&
934
+ node.initializer !== undefined &&
935
+ isFunctionLikeDeclaration(node.initializer)
936
+ ? node.initializer
937
+ : undefined;
938
+ return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
939
+ });
804
940
  const types: Type[] = [];
805
- queries.typesAt(valued).forEach((type) => {
941
+ const returnedTypes: Type[] = [];
942
+ queries.typesAt([...valued, ...returned]).forEach((type, at) => {
806
943
  if (type !== UNANSWERED && type !== undefined) {
807
- types.push(type);
944
+ (at < valued.length ? types : returnedTypes).push(type);
808
945
  }
809
946
  });
947
+ types.push(...returnedTypes);
810
948
  for (const node of declarations) {
811
949
  const signature = isFunctionLikeDeclaration(node)
812
950
  ? node
@@ -849,7 +987,19 @@ function inferredTypeSymbols<Brand>(
849
987
  types.forEach((type) => {
850
988
  visit(type, 0);
851
989
  });
852
- return [...found.values()];
990
+ // A returned value carries each property of its type, one an anonymous type declares or
991
+ // an intersection merges included, which no type name reaches.
992
+ const members = new Map<number, TSSymbol>();
993
+ for (const type of returnedTypes) {
994
+ const parts = queries.constituents(type);
995
+ for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
996
+ const properties = queries.propertiesOf(one);
997
+ for (const property of properties === UNANSWERED ? [] : properties) {
998
+ members.set(property.id, property);
999
+ }
1000
+ }
1001
+ }
1002
+ return { types: [...found.values()], members: [...members.values()] };
853
1003
  }
854
1004
 
855
1005
  /**
@@ -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/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;