@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4

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/README.md +1 -1
  2. package/dist/src/analysis.js +61 -10
  3. package/dist/src/augmentations.js +63 -1
  4. package/dist/src/build-scripts.js +437 -0
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +56 -9
  7. package/dist/src/convention-rows.js +39 -2
  8. package/dist/src/conventions.js +15 -0
  9. package/dist/src/dependencies.js +98 -1
  10. package/dist/src/discover.js +14 -82
  11. package/dist/src/entry-points.js +9 -18
  12. package/dist/src/exempt.js +8 -0
  13. package/dist/src/findings/completion.js +66 -28
  14. package/dist/src/findings/intra-function.js +4 -2
  15. package/dist/src/findings/reads-and-writes.js +7 -6
  16. package/dist/src/findings/unused-declarations.js +4 -1
  17. package/dist/src/graph.js +5 -3
  18. package/dist/src/intra-function-parts.js +45 -17
  19. package/dist/src/kinds.js +8 -8
  20. package/dist/src/manifest.js +18 -0
  21. package/dist/src/matrix.js +38 -4
  22. package/dist/src/query.js +1 -0
  23. package/dist/src/report.js +23 -4
  24. package/dist/src/reporters/annotations.js +15 -8
  25. package/dist/src/reporters/sarif.js +12 -8
  26. package/dist/src/roots.js +197 -42
  27. package/dist/src/serialization-contract.js +590 -12
  28. package/dist/src/sweep.js +7 -9
  29. package/dist/src/test-globals.js +234 -0
  30. package/dist/src/type-errors.js +11 -3
  31. package/dist/src/unseen-loads.js +107 -0
  32. package/dist/src/verbs/analyze.js +24 -3
  33. package/dist/src/workflow-steps.js +32 -15
  34. package/dist/src/yaml-document.js +355 -0
  35. package/jsr.json +1 -1
  36. package/package.json +2 -2
  37. package/src/analysis.ts +109 -12
  38. package/src/augmentations.ts +77 -2
  39. package/src/build-scripts.ts +564 -0
  40. package/src/conformance-record.ts +1 -1
  41. package/src/contextual-uses.ts +64 -12
  42. package/src/convention-rows.ts +47 -2
  43. package/src/conventions.ts +16 -0
  44. package/src/dependencies.ts +110 -1
  45. package/src/discover.ts +14 -94
  46. package/src/entry-points.ts +10 -25
  47. package/src/exempt.ts +16 -0
  48. package/src/file-facts.ts +3 -0
  49. package/src/findings/boundary.ts +2 -0
  50. package/src/findings/completion.ts +76 -30
  51. package/src/findings/intra-function.ts +4 -2
  52. package/src/findings/reads-and-writes.ts +7 -6
  53. package/src/findings/unused-declarations.ts +6 -1
  54. package/src/graph.ts +5 -3
  55. package/src/intra-function-parts.ts +55 -16
  56. package/src/kinds.ts +8 -8
  57. package/src/manifest.ts +21 -3
  58. package/src/matrix.ts +51 -4
  59. package/src/query.ts +11 -0
  60. package/src/report.ts +56 -16
  61. package/src/reporters/annotations.ts +21 -10
  62. package/src/reporters/reporter.ts +1 -3
  63. package/src/reporters/sarif.ts +16 -8
  64. package/src/roots.ts +226 -46
  65. package/src/serialization-contract.ts +758 -12
  66. package/src/sweep.ts +23 -9
  67. package/src/test-globals.ts +275 -0
  68. package/src/type-errors.ts +14 -3
  69. package/src/unseen-loads.ts +145 -0
  70. package/src/verbs/analyze.ts +28 -3
  71. package/src/workflow-steps.ts +35 -17
  72. 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,10 +27,14 @@ 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";
37
+ import { isGlobalBlock } from "./augmentations.ts";
25
38
  import {
26
39
  filesNamedBy,
27
40
  outsideChain,
@@ -67,6 +80,8 @@ export type RootKind =
67
80
  | "pattern"
68
81
  /** A declaration of a function or statement a type error skipped, and what it may have selected. */
69
82
  | "type-error"
83
+ /** An ambient global interface merging into one declared outside the target, and its holders. */
84
+ | "global-augmentation"
70
85
  /** A file a tool or the platform enters by its own convention, and what it exports where the tool reads it. */
71
86
  | EntryRule;
72
87
 
@@ -446,6 +461,12 @@ export function roots<Brand>(
446
461
  published.push(...entryFiles(entry));
447
462
  }
448
463
  }
464
+ if (options.publishedAPI) {
465
+ // A library's bundle hands what its entry exports to every program that loads it.
466
+ published.push(
467
+ ...points.filter((point) => point.rule === "bundler-entry").map((point) => point.file),
468
+ );
469
+ }
449
470
  const publishedNames = new Set(published.map((file) => file.fileName));
450
471
  const patterned: { file: SourceFile; pattern: string }[] = [];
451
472
  for (const pattern of options.entryFiles) {
@@ -660,9 +681,11 @@ const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"])
660
681
  * Every declaration a consumer of the library can name: what the published files export,
661
682
  * and below each the members a consumer writes, a namespace's exported ones and any other
662
683
  * declaration's public and protected ones. A declaration a published one's type names is
663
- * published too, and so is an exported declaration a release tag marks. A type name the
664
- * checker leaves unanswered publishes every declaration spelled as it is, as `guessed`. A
665
- * published file is not itself published.
684
+ * published too, and so is an exported declaration a release tag marks, and every
685
+ * declaration a `declare global` block of a published file, or of a file holding a published
686
+ * declaration, holds: the consumer's compiler loads that file's typings and their global
687
+ * scope with it. A type name the checker leaves unanswered publishes every declaration
688
+ * spelled as it is, as `guessed`. A published file is not itself published.
666
689
  */
667
690
  function publishedAPI<Brand>(
668
691
  project: ProjectView<Brand>,
@@ -695,48 +718,84 @@ function publishedAPI<Brand>(
695
718
  reach(id);
696
719
  }
697
720
  }
698
- while (pending.length > 0) {
699
- const round: string[] = [];
700
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
701
- round.push(next);
702
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
703
- for (const member of children.get(next) ?? []) {
704
- const reachable = exporting
705
- ? member.exported
706
- : member.visibility !== "private" && member.visibility !== "private-name";
707
- if (member.kind !== "type-parameter" && reachable) {
721
+ const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
722
+ const blocks = held.symbols.filter((one) => isGlobalBlock(one, held, files));
723
+ const fileOf = (id: string): string | undefined => {
724
+ let at = byId.get(id);
725
+ while (at !== undefined && at.kind !== "file") {
726
+ at = byId.get(at.parent);
727
+ }
728
+ return at?.id;
729
+ };
730
+ const publishedFiles = published.flatMap(
731
+ (file) => held.declarations.get(nodeKey(file, file)) ?? [],
732
+ );
733
+ const globalsReached = (): boolean => {
734
+ const typed = new Set([...publishedFiles, ...[...reached].flatMap((id) => fileOf(id) ?? [])]);
735
+ const before = pending.length;
736
+ for (const block of blocks.filter((one) => typed.has(one.parent))) {
737
+ reach(block.id);
738
+ for (const member of children.get(block.id) ?? []) {
739
+ if (member.kind !== "type-parameter") {
708
740
  reach(member.id);
709
741
  }
710
742
  }
711
743
  }
712
- const named = round.flatMap((id) => {
713
- const symbol = byId.get(id);
714
- return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
715
- });
716
- for (const symbol of inferredTypeSymbols(
717
- project,
718
- round.flatMap((id) => {
719
- const symbol = byId.get(id);
720
- return symbol === undefined ? [] : nodes.nodesOf(symbol);
721
- }),
722
- )) {
723
- for (const target of chains.chainOf(symbol)) {
724
- reach(target.id);
725
- }
726
- }
727
- project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
728
- if (symbol === UNANSWERED) {
729
- for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
730
- guessed.add(id);
731
- reach(id);
744
+ return pending.length > before;
745
+ };
746
+ do {
747
+ while (pending.length > 0) {
748
+ const round: string[] = [];
749
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
750
+ round.push(next);
751
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
752
+ for (const member of children.get(next) ?? []) {
753
+ const reachable = exporting
754
+ ? member.exported
755
+ : member.visibility !== "private" && member.visibility !== "private-name";
756
+ if (member.kind !== "type-parameter" && reachable) {
757
+ reach(member.id);
758
+ }
732
759
  }
733
- } else if (symbol !== undefined) {
760
+ }
761
+ const named = round.flatMap((id) => {
762
+ const symbol = byId.get(id);
763
+ return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
764
+ });
765
+ const inferred = inferredTypeSymbols(
766
+ project,
767
+ round.flatMap((id) => {
768
+ const symbol = byId.get(id);
769
+ return symbol === undefined ? [] : nodes.nodesOf(symbol);
770
+ }),
771
+ );
772
+ for (const symbol of inferred.types) {
734
773
  for (const target of chains.chainOf(symbol)) {
735
774
  reach(target.id);
736
775
  }
737
776
  }
738
- });
739
- }
777
+ for (const symbol of inferred.members) {
778
+ for (const target of chains.chainOf(symbol)) {
779
+ const member = byId.get(target.id);
780
+ if (member?.visibility === "public" || member?.visibility === "protected") {
781
+ reach(target.id);
782
+ }
783
+ }
784
+ }
785
+ project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
786
+ if (symbol === UNANSWERED) {
787
+ for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
788
+ guessed.add(id);
789
+ reach(id);
790
+ }
791
+ } else if (symbol !== undefined) {
792
+ for (const target of chains.chainOf(symbol)) {
793
+ reach(target.id);
794
+ }
795
+ }
796
+ });
797
+ }
798
+ } while (globalsReached());
740
799
  return { ids: [...reached], guessed: [...guessed] };
741
800
  }
742
801
 
@@ -783,30 +842,139 @@ function declarationNodes(
783
842
  const INFERRED_DEPTH = 4;
784
843
 
785
844
  /**
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.
845
+ * The values one function body returns, each through the type assertions around it: the
846
+ * value a caller receives carries the operand's type whatever type the assertion or the
847
+ * signature names. A nested function's returns are its own.
848
+ */
849
+ function returnedValues(body: Node): readonly Expression[] {
850
+ const found: Expression[] = [];
851
+ const add = (value: Expression): void => {
852
+ for (let at: Expression | undefined = value; at !== undefined;) {
853
+ found.push(at);
854
+ at =
855
+ isAssertionExpression(at) || isSatisfiesExpression(at) || isParenthesizedExpression(at)
856
+ ? at.expression
857
+ : undefined;
858
+ }
859
+ };
860
+ if (!isFunctionLikeDeclaration(body.parent) || body.parent.body !== body) {
861
+ return found;
862
+ }
863
+ if (isArrowFunction(body.parent) && body.kind !== SyntaxKind.Block) {
864
+ add(body as Expression);
865
+ return found;
866
+ }
867
+ const visit = (node: Node): void => {
868
+ if (isFunctionLikeDeclaration(node)) {
869
+ return;
870
+ }
871
+ if (isReturnStatement(node) && node.expression !== undefined) {
872
+ add(node.expression);
873
+ }
874
+ node.forEachChild(visit);
875
+ };
876
+ body.forEachChild(visit);
877
+ return found;
878
+ }
879
+
880
+ /** The object literal one returned value is, written there or as the initializer of a variable `body` declares. */
881
+ function returnedLiteral(value: Expression, body: Node): ObjectLiteralExpression | undefined {
882
+ if (isObjectLiteralExpression(value)) {
883
+ return value;
884
+ }
885
+ if (!isIdentifier(value)) {
886
+ return undefined;
887
+ }
888
+ let found: ObjectLiteralExpression | undefined;
889
+ const visit = (node: Node): void => {
890
+ if (
891
+ isVariableDeclaration(node) &&
892
+ isIdentifier(node.name) &&
893
+ node.name.text === value.text &&
894
+ node.initializer !== undefined
895
+ ) {
896
+ let at: Expression = node.initializer;
897
+ while (
898
+ isAssertionExpression(at) ||
899
+ isSatisfiesExpression(at) ||
900
+ isParenthesizedExpression(at)
901
+ ) {
902
+ at = at.expression;
903
+ }
904
+ found = isObjectLiteralExpression(at) ? at : found;
905
+ }
906
+ if (!isFunctionLikeDeclaration(node)) {
907
+ node.forEachChild(visit);
908
+ }
909
+ };
910
+ body.forEachChild(visit);
911
+ return found;
912
+ }
913
+
914
+ /**
915
+ * The values one function body returns, and those each method of an object literal it
916
+ * returns returns, to a fixed depth: a caller holding the literal calls its methods, and
917
+ * receives what they return.
918
+ */
919
+ function returnedThroughLiterals(body: Node, depth = 0): readonly Expression[] {
920
+ const values = returnedValues(body);
921
+ if (depth >= INFERRED_DEPTH) {
922
+ return values;
923
+ }
924
+ const found = [...values];
925
+ for (const value of values) {
926
+ for (const property of returnedLiteral(value, body)?.properties ?? []) {
927
+ const fn = isMethodDeclaration(property)
928
+ ? property
929
+ : isPropertyAssignment(property) && isFunctionLikeDeclaration(property.initializer)
930
+ ? property.initializer
931
+ : undefined;
932
+ if (fn?.body !== undefined) {
933
+ found.push(...returnedThroughLiterals(fn.body, depth + 1));
934
+ }
935
+ }
936
+ }
937
+ return found;
938
+ }
939
+
940
+ /**
941
+ * The declarations of the types the compiler infers where a declaration writes none, and
942
+ * of the values a function returns: the type of a variable or a property with no
943
+ * annotation, the result of a function or a method with no result annotation, and the type
944
+ * of each value its body returns, each with the types its type arguments and its union or
945
+ * intersection constituents name, to a fixed depth; and as `members` the properties of each
946
+ * returned value's type. A question the checker leaves unanswered names nothing.
791
947
  */
792
948
  function inferredTypeSymbols<Brand>(
793
949
  project: ProjectView<Brand>,
794
950
  declarations: readonly Node[],
795
- ): readonly TSSymbol[] {
951
+ ): { readonly types: readonly TSSymbol[]; readonly members: readonly TSSymbol[] } {
796
952
  const { queries } = project;
797
- const valued = declarations.flatMap((node) =>
953
+ const valued: Node[] = declarations.flatMap((node) =>
798
954
  (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
799
955
  node.type === undefined &&
800
956
  !isFunctionLikeDeclaration(node.initializer ?? node)
801
957
  ? [node.name]
802
958
  : [],
803
959
  );
960
+ const returned = declarations.flatMap((node) => {
961
+ const signature = isFunctionLikeDeclaration(node)
962
+ ? node
963
+ : isVariableDeclaration(node) &&
964
+ node.initializer !== undefined &&
965
+ isFunctionLikeDeclaration(node.initializer)
966
+ ? node.initializer
967
+ : undefined;
968
+ return signature?.body === undefined ? [] : returnedThroughLiterals(signature.body);
969
+ });
804
970
  const types: Type[] = [];
805
- queries.typesAt(valued).forEach((type) => {
971
+ const returnedTypes: Type[] = [];
972
+ queries.typesAt([...valued, ...returned]).forEach((type, at) => {
806
973
  if (type !== UNANSWERED && type !== undefined) {
807
- types.push(type);
974
+ (at < valued.length ? types : returnedTypes).push(type);
808
975
  }
809
976
  });
977
+ types.push(...returnedTypes);
810
978
  for (const node of declarations) {
811
979
  const signature = isFunctionLikeDeclaration(node)
812
980
  ? node
@@ -849,7 +1017,19 @@ function inferredTypeSymbols<Brand>(
849
1017
  types.forEach((type) => {
850
1018
  visit(type, 0);
851
1019
  });
852
- return [...found.values()];
1020
+ // A returned value carries each property of its type, one an anonymous type declares or
1021
+ // an intersection merges included, which no type name reaches.
1022
+ const members = new Map<number, TSSymbol>();
1023
+ for (const type of returnedTypes) {
1024
+ const parts = queries.constituents(type);
1025
+ for (const one of [type, ...(parts === UNANSWERED ? [] : parts)]) {
1026
+ const properties = queries.propertiesOf(one);
1027
+ for (const property of properties === UNANSWERED ? [] : properties) {
1028
+ members.set(property.id, property);
1029
+ }
1030
+ }
1031
+ }
1032
+ return { types: [...found.values()], members: [...members.values()] };
853
1033
  }
854
1034
 
855
1035
  /**