@cplieger/deadset-ts 8.0.0-dev.16 → 8.0.0-dev.17

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 (131) hide show
  1. package/dist/src/alias-chain.js +0 -5
  2. package/dist/src/amd.js +81 -0
  3. package/dist/src/analysis.js +58 -22
  4. package/dist/src/augmentations.js +23 -24
  5. package/dist/src/bindings.js +382 -0
  6. package/dist/src/calls.js +25 -8
  7. package/dist/src/commands/tools.js +5 -9
  8. package/dist/src/component-files.js +3 -2
  9. package/dist/src/conformance-record.js +1 -1
  10. package/dist/src/consumers.js +6 -9
  11. package/dist/src/contextual-uses.js +1 -17
  12. package/dist/src/contract/config.generated.js +4 -1
  13. package/dist/src/contract/report.generated.js +6 -2
  14. package/dist/src/dependencies.js +25 -29
  15. package/dist/src/destructuring.js +1 -19
  16. package/dist/src/element-access.js +179 -164
  17. package/dist/src/entries/build-scripts.js +2 -2
  18. package/dist/src/entries/configuration-files.js +3 -7
  19. package/dist/src/entries/conventions.js +2 -2
  20. package/dist/src/entries/html-entries.js +7 -10
  21. package/dist/src/entries/mdx-entries.js +2 -2
  22. package/dist/src/entries/static-module.js +2 -1
  23. package/dist/src/facts.js +4 -4
  24. package/dist/src/findings/completion.js +40 -4
  25. package/dist/src/{implementations.js → findings/implementations.js} +3 -3
  26. package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
  27. package/dist/src/framework-lifecycle.js +41 -14
  28. package/dist/src/graph.js +12 -4
  29. package/dist/src/import-bindings.js +73 -28
  30. package/dist/src/index/sites.js +557 -0
  31. package/dist/src/indexed-access.js +523 -478
  32. package/dist/src/interface-satisfaction.js +1 -1
  33. package/dist/src/keyof-comparisons.js +124 -103
  34. package/dist/src/load/expected-module.js +2 -2
  35. package/dist/src/load/specifiers.js +2 -4
  36. package/dist/src/load/synthesize.js +5 -0
  37. package/dist/src/load/workspace-resolution.js +2 -2
  38. package/dist/src/matrix.js +6 -4
  39. package/dist/src/module-declarations.js +70 -85
  40. package/dist/src/overrides.js +134 -112
  41. package/dist/src/paths.js +46 -7
  42. package/dist/src/platform-calls.js +2 -2
  43. package/dist/src/platform-symbols.js +13 -19
  44. package/dist/src/references/read-rule.js +1 -0
  45. package/dist/src/references/read-rules.js +48 -0
  46. package/dist/src/references/sink.js +151 -0
  47. package/dist/src/references/sources.js +450 -0
  48. package/dist/src/references.js +110 -1285
  49. package/dist/src/roots/configured-declarations.js +3 -0
  50. package/dist/src/roots/root-set.js +1 -1
  51. package/dist/src/roots/unseen-loads.js +38 -10
  52. package/dist/src/serialization-contract.js +8 -3
  53. package/dist/src/stores.js +4 -2
  54. package/dist/src/sweep.js +25 -13
  55. package/dist/src/symbol-keys.js +89 -83
  56. package/dist/src/tests/file-class.js +76 -0
  57. package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
  58. package/dist/src/{test-support.js → tests/support.js} +2 -11
  59. package/dist/src/type-parameter-reads.js +178 -139
  60. package/dist/src/unimplemented-callees.js +262 -242
  61. package/dist/src/verbs/analyze.js +3 -3
  62. package/dist/src/verbs/explain.js +50 -28
  63. package/dist/src/verbs/print-retained.js +2 -2
  64. package/jsr.json +1 -1
  65. package/package.json +2 -2
  66. package/src/alias-chain.ts +0 -7
  67. package/src/amd.ts +130 -0
  68. package/src/analysis.ts +86 -23
  69. package/src/augmentations.ts +41 -27
  70. package/src/bindings.ts +460 -0
  71. package/src/calls.ts +25 -7
  72. package/src/commands/tools.ts +5 -11
  73. package/src/component-files.ts +4 -2
  74. package/src/conformance-record.ts +1 -1
  75. package/src/consumers.ts +8 -11
  76. package/src/contextual-uses.ts +2 -22
  77. package/src/contract/config.generated.ts +4 -1
  78. package/src/contract/report.generated.ts +6 -2
  79. package/src/dependencies.ts +27 -28
  80. package/src/destructuring.ts +1 -31
  81. package/src/element-access.ts +188 -182
  82. package/src/entries/build-scripts.ts +2 -2
  83. package/src/entries/configuration-files.ts +11 -8
  84. package/src/entries/conventions.ts +2 -1
  85. package/src/entries/html-entries.ts +15 -14
  86. package/src/entries/mdx-entries.ts +2 -2
  87. package/src/entries/static-module.ts +2 -1
  88. package/src/facts.ts +5 -3
  89. package/src/findings/boundary.ts +2 -0
  90. package/src/findings/completion.ts +43 -4
  91. package/src/findings/emitter.ts +1 -1
  92. package/src/{implementations.ts → findings/implementations.ts} +4 -4
  93. package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
  94. package/src/framework-lifecycle.ts +42 -14
  95. package/src/graph.ts +24 -6
  96. package/src/import-bindings.ts +98 -29
  97. package/src/index/sites.ts +781 -0
  98. package/src/indexed-access.ts +570 -527
  99. package/src/interface-satisfaction.ts +1 -1
  100. package/src/keyof-comparisons.ts +131 -116
  101. package/src/load/expected-module.ts +8 -2
  102. package/src/load/specifiers.ts +8 -5
  103. package/src/load/synthesize.ts +6 -0
  104. package/src/load/workspace-resolution.ts +9 -2
  105. package/src/matrix.ts +6 -3
  106. package/src/module-declarations.ts +74 -98
  107. package/src/overrides.ts +162 -134
  108. package/src/paths.ts +54 -8
  109. package/src/platform-calls.ts +3 -3
  110. package/src/platform-symbols.ts +11 -27
  111. package/src/references/read-rule.ts +60 -0
  112. package/src/references/read-rules.ts +55 -0
  113. package/src/references/sink.ts +213 -0
  114. package/src/references/sources.ts +632 -0
  115. package/src/references.ts +168 -1733
  116. package/src/report.ts +1 -1
  117. package/src/roots/configured-declarations.ts +3 -0
  118. package/src/roots/root-set.ts +1 -1
  119. package/src/roots/unseen-loads.ts +49 -11
  120. package/src/serialization-contract.ts +8 -3
  121. package/src/stores.ts +13 -2
  122. package/src/sweep.ts +37 -14
  123. package/src/symbol-keys.ts +102 -102
  124. package/src/tests/file-class.ts +118 -0
  125. package/src/{outside-tests.ts → tests/outside.ts} +16 -10
  126. package/src/{test-support.ts → tests/support.ts} +9 -20
  127. package/src/type-parameter-reads.ts +187 -158
  128. package/src/unimplemented-callees.ts +297 -280
  129. package/src/verbs/analyze.ts +3 -2
  130. package/src/verbs/explain.ts +74 -31
  131. package/src/verbs/print-retained.ts +2 -2
@@ -140,6 +140,9 @@ function specifierOf(node) {
140
140
  * import resolves. A specifier no file writes resolves to no module.
141
141
  */
142
142
  function modulesNamed(project, specifiers) {
143
+ if (specifiers.size === 0) {
144
+ return new Map();
145
+ }
143
146
  const written = new Map();
144
147
  for (const file of project.ownSourceFiles()) {
145
148
  const visit = (node) => {
@@ -389,7 +389,7 @@ export function rootSet(project, held, targetRoot, options) {
389
389
  for (const id of skippedRoots(project, held, options.skips)) {
390
390
  add(id, "type-error", "");
391
391
  }
392
- for (const id of mergedGlobalInterfaces(project, held, files)) {
392
+ for (const id of mergedGlobalInterfaces(project, held, files).kept) {
393
393
  add(id, "global-augmentation", "");
394
394
  }
395
395
  return {
@@ -4,9 +4,10 @@
4
4
  * are never complete.
5
5
  */
6
6
  import { isBinaryExpression, isCallExpression, isIdentifier, isMetaProperty, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
7
+ import { amdLoadOf, amdUnresolved } from "../amd.js";
7
8
  import { membersNamed, textsOf, writtenValue } from "../entries/static-module.js";
8
9
  import { writtenName } from "../platform-calls.js";
9
- import { dirnamePath, relativePath, resolvePath } from "../paths.js";
10
+ import { cannotStartPackageName, dirnamePath, hasURLScheme, isAbsolutePath, relativePath, resolvePath, startsRelativePath, } from "../paths.js";
10
11
  import { unwrapped } from "../unwrap.js";
11
12
  import { globExpression } from "../text/glob.js";
12
13
  /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
@@ -29,7 +30,7 @@ function leadingLiteral(node) {
29
30
  * it, each of which would name every sibling.
30
31
  */
31
32
  function computedLoadDirectory(dir, prefix) {
32
- if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
33
+ if (!startsRelativePath(prefix) || !prefix.endsWith("/")) {
33
34
  return undefined;
34
35
  }
35
36
  const target = resolvePath(dir, prefix);
@@ -65,7 +66,7 @@ function globbed(file, first, options, others, rootOf) {
65
66
  const dir = dirnamePath(file.fileName);
66
67
  const root = () => rootOf(file.fileName);
67
68
  const against = (at) => {
68
- if (!at.startsWith("/")) {
69
+ if (!isAbsolutePath(at)) {
69
70
  return resolvePath(dir, at);
70
71
  }
71
72
  const top = root();
@@ -78,10 +79,10 @@ function globbed(file, first, options, others, rootOf) {
78
79
  const negated = written.startsWith("!");
79
80
  const pattern = negated ? written.slice(1) : written;
80
81
  let path;
81
- if (pattern.startsWith("/")) {
82
+ if (isAbsolutePath(pattern)) {
82
83
  path = against(pattern);
83
84
  }
84
- else if (pattern.startsWith("./") || pattern.startsWith("../")) {
85
+ else if (startsRelativePath(pattern)) {
85
86
  path = from === undefined ? undefined : resolvePath(from, pattern);
86
87
  }
87
88
  else if (pattern.startsWith("**")) {
@@ -100,7 +101,8 @@ function globbed(file, first, options, others, rootOf) {
100
101
  * file itself: an `import()` or `require()` whose path starts with a relative literal naming
101
102
  * a directory below the file's loads every candidate below it, as a bundler's context module
102
103
  * reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
103
- * call of a computed path that names no such directory and no package is an unresolved load.
104
+ * call of a computed path that names no such directory and no package is an unresolved load,
105
+ * and so is an AMD call one of whose dependencies names no path ({@link amdUnresolved}).
104
106
  */
105
107
  export function computedLoadsOf(file, calls, candidates, rootOf) {
106
108
  const dir = dirnamePath(file.fileName);
@@ -118,6 +120,7 @@ export function computedLoadsOf(file, calls, candidates, rootOf) {
118
120
  call,
119
121
  from,
120
122
  require: undefined,
123
+ amd: false,
121
124
  files: files ?? [],
122
125
  resolved: files !== undefined,
123
126
  asset,
@@ -125,6 +128,23 @@ export function computedLoadsOf(file, calls, candidates, rootOf) {
125
128
  },
126
129
  ];
127
130
  }
131
+ const amd = amdLoadOf(call);
132
+ if (amd !== undefined) {
133
+ return amdUnresolved(amd)
134
+ ? [
135
+ {
136
+ call,
137
+ from,
138
+ require: amd.callee,
139
+ amd: true,
140
+ files: [],
141
+ resolved: false,
142
+ asset: false,
143
+ eager: false,
144
+ },
145
+ ]
146
+ : [];
147
+ }
128
148
  const argument = unwrapped(first);
129
149
  const require = call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
130
150
  if (!mayNamePath(argument)) {
@@ -139,7 +159,16 @@ export function computedLoadsOf(file, calls, candidates, rootOf) {
139
159
  ? []
140
160
  : others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
141
161
  return [
142
- { call, from, require, files, resolved: target !== undefined, asset: false, eager: false },
162
+ {
163
+ call,
164
+ from,
165
+ require,
166
+ amd: false,
167
+ files,
168
+ resolved: target !== undefined,
169
+ asset: false,
170
+ eager: false,
171
+ },
143
172
  ];
144
173
  });
145
174
  }
@@ -202,7 +231,6 @@ function readsAsAsset(options) {
202
231
  function namesPackage(prefix) {
203
232
  return (prefix !== undefined &&
204
233
  prefix !== "" &&
205
- !prefix.startsWith(".") &&
206
- !prefix.startsWith("/") &&
207
- !/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix));
234
+ !cannotStartPackageName(prefix) &&
235
+ !hasURLScheme(prefix));
208
236
  }
@@ -898,7 +898,8 @@ export function serializationContract(input) {
898
898
  }
899
899
  /**
900
900
  * The data members the decodes and copies of `files` fill, as {@link Decoded} names them.
901
- * A generic declared outside the analysis, or only ambiently, fills the type that a type
901
+ * A generic declared outside the analysis, or only ambiently, or one that reads a value
902
+ * {@link readTypes} finds untyped as its own type parameter, fills the type that a type
902
903
  * parameter of its own no parameter names binds, where the awaited result carries that
903
904
  * type through its data members rather than only naming it, as a store's methods do: no
904
905
  * argument hands the value over, so it fills it where the analysis cannot see, and may
@@ -951,6 +952,10 @@ function decodedOf(project, held, files, consumers) {
951
952
  for (const file of files) {
952
953
  file.forEachChild(visit);
953
954
  }
955
+ const read = readTypes(project, untyped);
956
+ // A generic of the analysis that reads an untyped value as its own type parameter is a
957
+ // client of what made the value, and fills what that parameter binds as an outside one does.
958
+ const clients = new Set(read.made.flatMap((type) => (type.isTypeParameter() ? [type.id] : [])));
954
959
  const filled = [];
955
960
  const resolved = queries.resolvedSignatures(calls).map(must);
956
961
  for (const signature of resolved) {
@@ -963,7 +968,8 @@ function decodedOf(project, held, files, consumers) {
963
968
  const node = declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
964
969
  if (declaration === undefined ||
965
970
  (inAnalysis(declaration.path) &&
966
- (node === undefined || (node.flags & NodeFlags.Ambient) === 0))) {
971
+ (node === undefined || (node.flags & NodeFlags.Ambient) === 0) &&
972
+ !generic.some((id) => clients.has(id)))) {
967
973
  continue;
968
974
  }
969
975
  const result = must(queries.returnTypeOf(declared));
@@ -982,7 +988,6 @@ function decodedOf(project, held, files, consumers) {
982
988
  filled.push({ carried: awaited(project, given), types });
983
989
  }
984
990
  }
985
- const read = readTypes(project, untyped);
986
991
  const copies = copiedNames(project, spreads, calls);
987
992
  if (filled.length === 0 &&
988
993
  read.parsed.length === 0 &&
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { isGetAccessorDeclaration, isSetAccessorDeclaration, } from "@typescript/native/ast";
7
7
  import { nodeKey } from "./inventory.js";
8
+ import { classedAsTest, madeByTestCode, withheldBy } from "./tests/file-class.js";
8
9
  /** The class members one project's own files declare by a getter or a setter. */
9
10
  export function accessorsOf(project, held) {
10
11
  const found = [];
@@ -25,9 +26,10 @@ export function accessorsOf(project, held) {
25
26
  /** The run's stores, from what each configuration contributed, the records and the mode. */
26
27
  export function storesOf(per, exempt, mode) {
27
28
  return {
28
- references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
29
+ references: per.flatMap((one) => one.references.filter((reference) => !withheldBy(mode, reference, one.support))),
29
30
  readInTests: new Set(per.flatMap((one) => one.references
30
- .filter((reference) => reference.test && reference.use !== "write")
31
+ .filter((reference) => reference.use !== "write" &&
32
+ classedAsTest(madeByTestCode(reference, one.support), reference.consumer !== undefined, mode))
31
33
  .map((reference) => reference.to))),
32
34
  accessors: new Set(per.flatMap((one) => one.accessors)),
33
35
  exempt: new Set(exempt.map((record) => record.id)),
package/dist/src/sweep.js CHANGED
@@ -6,6 +6,22 @@
6
6
  * declaration only unreachable ones reference is live under the first, dead under the second.
7
7
  */
8
8
  import { namesACaller, OUTSIDE } from "./graph.js";
9
+ import { edgeWithheldBy } from "./tests/file-class.js";
10
+ /** The references to one declaration the mode counts: under `production` none test code made but those it classes as production. */
11
+ function countedRefs(made, mode) {
12
+ if (made === undefined) {
13
+ return 0;
14
+ }
15
+ if (!mode.production) {
16
+ return made.production + made.test;
17
+ }
18
+ return made.production + (mode.consumerTestsProduction === true ? made.consumerTest : 0);
19
+ }
20
+ /** The references to one declaration, split as the mode classes them, whatever it counts. */
21
+ export function splitRefs(made, mode) {
22
+ const moved = mode.consumerTestsProduction === true ? (made?.consumerTest ?? 0) : 0;
23
+ return { production: (made?.production ?? 0) + moved, test: (made?.test ?? 0) - moved };
24
+ }
9
25
  /**
10
26
  * Answers which declarations of one graph are dead under the input's mode.
11
27
  *
@@ -30,13 +46,7 @@ export function sweep(graph, input) {
30
46
  called[root.at] = true;
31
47
  }
32
48
  }
33
- const counted = (at) => {
34
- const made = graph.made[at];
35
- if (made === undefined) {
36
- return 0;
37
- }
38
- return input.mode.production ? made.production : made.production + made.test;
39
- };
49
+ const counted = (at) => countedRefs(graph.made[at], input.mode);
40
50
  const reached = reachable(graph, held, input.mode);
41
51
  const liveUnder = graph.symbols.map((_symbol, at) => {
42
52
  const relations = [];
@@ -60,10 +70,11 @@ export function sweep(graph, input) {
60
70
  if (dead[at] !== true) {
61
71
  return;
62
72
  }
73
+ const split = splitRefs(graph.made[at], input.mode);
63
74
  candidates.push({
64
75
  id: symbol.id,
65
- productionRefs: graph.made[at]?.production ?? 0,
66
- testRefs: graph.made[at]?.test ?? 0,
76
+ productionRefs: split.production,
77
+ testRefs: split.test,
67
78
  relation: counted(at) === 0 ? "reference-counting" : "reachability",
68
79
  testOfDeadCode: testOfDeadCode[at] === true,
69
80
  });
@@ -100,11 +111,12 @@ export function reachedWithTheTests(graph, held, namespaces = true) {
100
111
  return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), { production: false }, namespaces);
101
112
  }
102
113
  /**
103
- * The references a loaded consumer makes that the mode counts. A production mode counts
104
- * none a consumer's test file made, as it counts none of the target's.
114
+ * The references a loaded consumer or an unheld file makes that the mode counts. A
115
+ * production mode counts no test reference, but a loaded consumer's where the mode classes
116
+ * a consumer's tests as production.
105
117
  */
106
118
  function callsOf(graph, mode) {
107
- return graph.consumed.filter((edge) => !mode.production || !edge.test);
119
+ return graph.consumed.filter((edge) => !edgeWithheldBy(mode, edge));
108
120
  }
109
121
  /** Per declaration of the graph, whether one of the identifiers names it. */
110
122
  function flagged(graph, ids) {
@@ -149,7 +161,7 @@ function reachable(graph, held, mode, namespaces = true) {
149
161
  const walk = (holding) => {
150
162
  for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
151
163
  for (const edge of graph.out[at] ?? []) {
152
- if (!holding && mode.production && edge.test) {
164
+ if (!holding && edgeWithheldBy(mode, edge)) {
153
165
  continue;
154
166
  }
155
167
  enter(edge.to, namespaces && !edge.evaluation);
@@ -7,9 +7,7 @@
7
7
  */
8
8
  import { isComputedPropertyName, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isPropertyAccessExpression, isSpreadElement, isStringLiteral, } from "@typescript/native/ast";
9
9
  import { parenthesesRemoved } from "./unwrap.js";
10
- import { nodeKey } from "./inventory.js";
11
- import { renderPosition } from "./position.js";
12
- import { isAnswered } from "./query.js";
10
+ import { isAnswered, UNANSWERED } from "./query.js";
13
11
  /** The identifier an element access's argument names its key by, where it is a name. */
14
12
  export function keyNameOf(argument) {
15
13
  const at = parenthesesRemoved(argument);
@@ -127,87 +125,95 @@ function wellKnownName(name) {
127
125
  : undefined;
128
126
  }
129
127
  /**
130
- * The references the iterations of one project's own files make to the iteration members
131
- * of the values they iterate: a `for…of` statement and a spread element read
132
- * `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
133
- * which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
128
+ * The reads the iterations of a side's files make of the iteration members of the values they
129
+ * iterate: a `for…of` statement and a spread element read `[Symbol.iterator]`, and a
130
+ * `for await…of` statement reads `[Symbol.asyncIterator]` and, which it falls back to,
131
+ * `[Symbol.iterator]`. Each is made by the declaration holding it. A value whose type or
132
+ * members the checker leaves unanswered holds the members those names could spell.
134
133
  */
135
- export function iterationReferences(project, held, chains, root, testFiles) {
136
- const queries = project.queries;
137
- const ownPaths = project.ownPaths();
138
- const iterated = [];
139
- for (const file of project.ownSourceFiles()) {
140
- const fileId = held.declarations.get(nodeKey(file, file));
141
- if (fileId === undefined) {
142
- continue;
143
- }
144
- const visit = (node, from) => {
145
- const holder = held.declarations.get(nodeKey(file, node)) ?? from;
146
- if (isForOfStatement(node)) {
147
- const protocols = node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
148
- iterated.push({ file, from: holder, operand: node.expression, protocols });
149
- }
150
- else if (isSpreadElement(node)) {
151
- iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
152
- }
153
- node.forEachChild((child) => {
154
- visit(child, holder);
155
- });
156
- };
157
- file.forEachChild((child) => {
158
- visit(child, fileId);
159
- });
160
- }
161
- if (iterated.length === 0) {
162
- return [];
163
- }
164
- const types = queries.typesAt(iterated.map((one) => one.operand));
165
- const members = new Map();
166
- const membersOf = (type) => {
167
- const known = members.get(type.id);
168
- if (known !== undefined) {
169
- return known;
170
- }
171
- const keyed = [];
172
- const properties = queries.propertiesOf(type);
173
- for (const property of isAnswered(properties) ? properties : []) {
174
- for (const handle of property.declarations) {
175
- const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
176
- const name = wellKnownName(node?.name);
177
- if (name !== undefined) {
178
- keyed.push({ name, ids: chains.declarationsOf(property) });
179
- break;
134
+ export const iteration = {
135
+ start() {
136
+ const iterated = [];
137
+ return {
138
+ visit(node, { file, from }) {
139
+ if (isForOfStatement(node)) {
140
+ const protocols = node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
141
+ iterated.push({ file, from, operand: node.expression, protocols });
180
142
  }
181
- }
182
- }
183
- members.set(type.id, keyed);
184
- return keyed;
185
- };
186
- const found = [];
187
- iterated.forEach((one, index) => {
188
- const type = types[index];
189
- if (type === undefined || !isAnswered(type)) {
190
- return;
191
- }
192
- const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
193
- const position = renderPosition(one.file, root, one.operand.getStart());
194
- for (const part of isAnswered(parts) ? parts : []) {
195
- for (const member of membersOf(part)) {
196
- if (!one.protocols.includes(member.name)) {
197
- continue;
143
+ else if (isSpreadElement(node)) {
144
+ iterated.push({ file, from, operand: node.expression, protocols: ["iterator"] });
198
145
  }
199
- for (const to of member.ids) {
200
- found.push({
201
- from: one.from,
202
- to,
203
- position,
204
- use: "read",
205
- resolution: "keyed",
206
- test: testFiles.has(position.path),
207
- });
146
+ },
147
+ reads({ project, chains }) {
148
+ const found = [];
149
+ if (iterated.length === 0) {
150
+ return found;
208
151
  }
209
- }
210
- }
211
- });
212
- return found;
213
- }
152
+ const queries = project.queries;
153
+ const ownPaths = project.ownPaths();
154
+ const types = queries.typesAt(iterated.map((one) => one.operand));
155
+ const members = new Map();
156
+ const membersOf = (type) => {
157
+ const known = members.get(type.id);
158
+ if (known !== undefined) {
159
+ return known;
160
+ }
161
+ const properties = queries.propertiesOf(type);
162
+ const keyed = [];
163
+ for (const property of isAnswered(properties) ? properties : []) {
164
+ for (const handle of property.declarations) {
165
+ const node = ownPaths.has(handle.path)
166
+ ? project.declarationAt(handle)?.node
167
+ : undefined;
168
+ const name = wellKnownName(node?.name);
169
+ if (name !== undefined) {
170
+ keyed.push({ name, ids: chains.declarationsOf(property) });
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ const result = isAnswered(properties) ? keyed : UNANSWERED;
176
+ members.set(type.id, result);
177
+ return result;
178
+ };
179
+ // A member a well-known symbol keys is named by the symbol's computed name.
180
+ const spelled = (protocols) => protocols.map((protocol) => `Symbol.${protocol}`);
181
+ iterated.forEach((one, index) => {
182
+ const type = types[index];
183
+ if (type === UNANSWERED) {
184
+ found.push({ kind: "held", from: one.from, names: spelled(one.protocols) });
185
+ return;
186
+ }
187
+ if (type === undefined) {
188
+ return;
189
+ }
190
+ const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
191
+ for (const part of isAnswered(parts) ? parts : [type]) {
192
+ const keyed = membersOf(part);
193
+ if (keyed === UNANSWERED) {
194
+ found.push({ kind: "held", from: one.from, names: spelled(one.protocols) });
195
+ continue;
196
+ }
197
+ for (const member of keyed) {
198
+ if (!one.protocols.includes(member.name)) {
199
+ continue;
200
+ }
201
+ for (const to of member.ids) {
202
+ found.push({
203
+ kind: "named",
204
+ from: one.from,
205
+ to,
206
+ file: one.file,
207
+ at: one.operand,
208
+ use: "read",
209
+ via: "keyed",
210
+ });
211
+ }
212
+ }
213
+ }
214
+ });
215
+ return found;
216
+ },
217
+ };
218
+ },
219
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The file classes the run counts references by. A test file is one a `ts.test_files`
3
+ * pattern or a test runner's row names (the test-runner entry source decides which), a test
4
+ * file no project holds, or a consumer's file a pattern names below the consumer's root.
5
+ * A test-support file is test code as well, decided once the references are known
6
+ * (`support.ts`), so a reference carries only whether a test file made it and the counters
7
+ * read the support class beside it.
8
+ */
9
+ import { renderPosition } from "../position.js";
10
+ import { bytewise } from "../text/compare.js";
11
+ import { globExpression } from "../text/glob.js";
12
+ /** The test files of the target: each file the test-runner source names, by its file name. */
13
+ export function targetTestFile(named) {
14
+ return (file) => named.has(file.fileName);
15
+ }
16
+ /** The test files a pattern names, each by its path below `root`: a consumer's, below the consumer's root. */
17
+ export function patternTestFile(patterns, root) {
18
+ const expressions = patterns.map(globExpression);
19
+ return (file) => {
20
+ const path = renderPosition(file, root, 0).path;
21
+ return expressions.some((expression) => expression.test(path));
22
+ };
23
+ }
24
+ /** The own files of a project that are test files, by their path below `root`, ascending. */
25
+ export function testFilePaths(project, root, testFile) {
26
+ return project
27
+ .ownSourceFiles()
28
+ .filter(testFile)
29
+ .map((file) => renderPosition(file, root, 0).path)
30
+ .sort(bytewise);
31
+ }
32
+ /** Whether test code made one reference: a test file, or a test-support file of the target. */
33
+ export function madeByTestCode(reference, support) {
34
+ return (reference.test || (reference.consumer === undefined && support.has(reference.position.path)));
35
+ }
36
+ /** Whether the mode classes a reference test code made as a test one: any but a loaded consumer's it classes as production. */
37
+ export function classedAsTest(test, consumer, mode) {
38
+ return test && !(consumer && mode.consumerTestsProduction === true);
39
+ }
40
+ /** Whether a production mode counts no reference of this one: test code made it, as the mode classes it. */
41
+ export function withheldBy(mode, reference, support) {
42
+ return (mode.production &&
43
+ classedAsTest(madeByTestCode(reference, support), reference.consumer !== undefined, mode));
44
+ }
45
+ /** Whether a production mode follows no edge like this one: {@link withheldBy} for a graph edge. */
46
+ export function edgeWithheldBy(mode, edge) {
47
+ return mode.production && classedAsTest(edge.test, edge.consumer, mode);
48
+ }
49
+ /** The rule a file the analysis classified as test-support code is counted under. */
50
+ const TEST_SUPPORT_RULE = "test-support";
51
+ /**
52
+ * The name one test-file rule takes. A rule's name is a word list the report's own
53
+ * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
54
+ * its pattern in the configured list.
55
+ */
56
+ function patternRule(index) {
57
+ return `test-file-pattern-${String(index + 1)}`;
58
+ }
59
+ /**
60
+ * The rules over the test files of several projects at once, ordered by rule: a file two
61
+ * projects hold is one file, so each rule counts the distinct paths it matches.
62
+ */
63
+ export function testFileRulesOf(testFiles, paths, supportFiles = []) {
64
+ const distinct = [...new Set(paths)];
65
+ const support = new Set(supportFiles).size;
66
+ return [
67
+ ...testFiles.map((pattern, index) => {
68
+ const expression = globExpression(pattern);
69
+ return {
70
+ rule: patternRule(index),
71
+ matched: distinct.filter((path) => expression.test(path)).length,
72
+ };
73
+ }),
74
+ ...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
75
+ ].sort((a, b) => bytewise(a.rule, b.rule));
76
+ }
@@ -7,15 +7,15 @@
7
7
  * it names is not known.
8
8
  */
9
9
  import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
10
- import { RESOLVED_EXTENSIONS } from "./entries/configuration-files.js";
11
- import { emittedFrom } from "./load/emit-map.js";
12
- import { namesImported } from "./module-declarations.js";
13
- import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
14
- import { renderPosition } from "./position.js";
15
- import { nameComponent } from "./ref.js";
10
+ import { RESOLVED_EXTENSIONS } from "../entries/configuration-files.js";
11
+ import { emittedFrom } from "../load/emit-map.js";
12
+ import { namesImported } from "../import-bindings.js";
13
+ import { dirnamePath, joinPath, normalizePath, queriedModulePath, startsRelativePath, } from "../paths.js";
14
+ import { renderPosition } from "../position.js";
15
+ import { nameComponent } from "../ref.js";
16
16
  /** The paths below the target root one relative specifier may name, written path first. */
17
17
  function candidates(dir, specifier) {
18
- if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
18
+ if (!startsRelativePath(specifier)) {
19
19
  return [];
20
20
  }
21
21
  const path = normalizePath(dir === "" ? specifier : `${dir}/${specifier}`);
@@ -5,8 +5,8 @@
5
5
  * another such file, so it is judged with the tests: what it references is referenced by
6
6
  * test code.
7
7
  */
8
- import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./entries/convention-rows.js";
9
- import { packageOfSpecifier } from "./dependencies.js";
8
+ import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES, } from "../entries/convention-rows.js";
9
+ import { packageOfSpecifier } from "../dependencies.js";
10
10
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
11
11
  const SKIP_ROOT = "type-error";
12
12
  /**
@@ -163,12 +163,3 @@ export function testSupportFiles(symbols, references, roots, testFiles, evidence
163
163
  const support = withoutOutsideUsers(new Set([...onlyTests].filter((path) => carries(onlyTests, path))));
164
164
  return { support, testUsed: new Set([...onlyTests].filter((path) => !support.has(path))) };
165
165
  }
166
- /** The references with every one a test-support file makes counted as a test reference. */
167
- export function supportReferences(references, support) {
168
- if (support.size === 0) {
169
- return references;
170
- }
171
- return references.map((reference) => !reference.test && support.has(reference.position.path)
172
- ? { ...reference, test: true }
173
- : reference);
174
- }