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

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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
@@ -8,7 +8,7 @@ import { jsonChunks } from "./json-chunks.js";
8
8
  import { joinPath } from "./paths.js";
9
9
  import { positionAt, walkDocument } from "./json-document.js";
10
10
  import { declarationKey } from "./ref.js";
11
- import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
11
+ import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isPartKind, isSymbolRef, siteText, } from "./suppress.js";
12
12
  /** The name and the location the grammar fixes for the ignore file. */
13
13
  export const IGNORE_FILE = "deadset-ignore.json";
14
14
  /** The name and the location the grammar fixes for the baseline. */
@@ -107,12 +107,20 @@ function readRecord(value, site, byReference, shape) {
107
107
  if (refusals.length > 0) {
108
108
  return { records: [], refusals };
109
109
  }
110
- const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
110
+ const named = byReference.get(declarationKey(symbol, written.path)) ??
111
+ (isPartKind(code)
112
+ ? byReference.get(declarationKey(parameterHolder(symbol), written.path))
113
+ : undefined) ??
114
+ [];
111
115
  if (named.length === 0) {
112
116
  return { records: [{ ...written, bound: "" }], refusals: [] };
113
117
  }
114
118
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
115
119
  }
120
+ /** The reference of the declaration a parameter's reference names it under, or `ref` itself. */
121
+ function parameterHolder(ref) {
122
+ return ref.endsWith(")") ? ref.slice(0, ref.lastIndexOf("(")) : ref;
123
+ }
116
124
  function byReferenceOf(symbols) {
117
125
  const byReference = new Map();
118
126
  for (const symbol of symbols) {
@@ -4,8 +4,10 @@
4
4
  * below it, or on its own line where it trails a token there.
5
5
  */
6
6
  import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
7
+ import { importBindings } from "./import-bindings.js";
7
8
  import { isParameterProperty, nodeKey } from "./inventory.js";
8
9
  import { byPosition, positionKey, renderPosition } from "./position.js";
10
+ import { importRef } from "./ref.js";
9
11
  import { signatureOf } from "./signatures.js";
10
12
  import { isPartKind, SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
11
13
  /**
@@ -144,7 +146,7 @@ function bindingKey(one) {
144
146
  * function also begins, for a directive, on the line of each of its parameters, and a
145
147
  * class, for a part kind, on the lines of its constructor and its constructor's parameters.
146
148
  */
147
- function declarationLines(file, held, symbols) {
149
+ function declarationLines(file, held, symbols, targetRoot) {
148
150
  const below = new Map();
149
151
  const add = (offset, one) => {
150
152
  const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
@@ -186,6 +188,20 @@ function declarationLines(file, held, symbols) {
186
188
  node.forEachChild(visit);
187
189
  };
188
190
  file.forEachChild(visit);
191
+ // An import binding is a subject of its own, bound on its line as a declaration is.
192
+ const fileId = held.declarations.get(nodeKey(file, file));
193
+ const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
194
+ for (const name of moduleRef === "" ? [] : importBindings(file)) {
195
+ const position = renderPosition(file, targetRoot, name.getStart());
196
+ const one = { id: positionKey(position), ref: importRef(moduleRef, name.text), position };
197
+ let statement = name;
198
+ while (statement.parent.kind !== SyntaxKind.SourceFile) {
199
+ statement = statement.parent;
200
+ }
201
+ for (const at of [statement.pos, name.getStart()]) {
202
+ add(at, { declared: one, part: one });
203
+ }
204
+ }
189
205
  return below;
190
206
  }
191
207
  /**
@@ -210,7 +226,7 @@ export function inlineDirectives(project, held, targetRoot) {
210
226
  if (comments.length === 0) {
211
227
  continue;
212
228
  }
213
- const below = declarationLines(file, held, symbols);
229
+ const below = declarationLines(file, held, symbols, targetRoot);
214
230
  for (const comment of comments) {
215
231
  const site = renderPosition(file, targetRoot, comment.offset);
216
232
  const target = trails(file, comment.offset) ? site.line : site.line + 1;
@@ -57,6 +57,7 @@ export const GO_REF_EXPRESSIONS = [
57
57
  `^go://${GO_PATH}#${GO_IDENT}$`,
58
58
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})+$`,
59
59
  `^go://${GO_PATH}#${GO_IDENT}(?:\\.${GO_IDENT})?\\[${GO_IDENT}\\]$`,
60
+ `^go://${GO_PATH}#(?:${GO_IDENT}(?:\\.${GO_IDENT})?)?\\(${GO_IDENT}\\)$`,
60
61
  `^go://${GO_PATH}#${GO_FILE}:file$`,
61
62
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:require$`,
62
63
  `^go://${GO_PATH}#${GO_PATH}(?:@${GO_VERSION})?:replace$`,
@@ -144,10 +145,11 @@ function claimKey(finding) {
144
145
  }
145
146
  /**
146
147
  * Whether one record names one finding. A record reaches a finding in two ways: it bound
147
- * the declaration at the finding's position, or it names the finding's reference and the
148
- * file the finding is reported in, each exactly. The second is how an entry copied from a
149
- * finding about a part of a declaration reaches the part, and the only way any record
150
- * reaches a dependency row.
148
+ * the declaration at the finding's position, or it names the finding's reference, or the
149
+ * reference of the declaration holding a part that has one of its own, and the file the
150
+ * finding is reported in, each exactly. The second is how an entry copied from a finding
151
+ * about a part of a declaration reaches the part, and the only way any record reaches a
152
+ * dependency row.
151
153
  */
152
154
  export function pairs(record, finding) {
153
155
  if (record.code !== finding.code) {
@@ -164,7 +166,8 @@ export function pairs(record, finding) {
164
166
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
165
167
  return true;
166
168
  }
167
- return record.symbol === finding.symbol.ref && record.path === finding.position.path;
169
+ return ((record.symbol === finding.symbol.ref || record.symbol === finding.symbol.holder) &&
170
+ record.path === finding.position.path);
168
171
  }
169
172
  /**
170
173
  * Decides every record against `would`, the findings the run would produce were no record
package/dist/src/sweep.js CHANGED
@@ -70,16 +70,30 @@ export function sweep(graph, input) {
70
70
  });
71
71
  return { liveUnder: live, candidates };
72
72
  }
73
+ /**
74
+ * The directory a test runner reads a manual mock from, whose exports it puts in place of
75
+ * the module's: https://jestjs.io/docs/manual-mocks
76
+ */
77
+ const MANUAL_MOCKS = "__mocks__";
78
+ /** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
79
+ export function runnerCalls(symbol) {
80
+ return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
81
+ }
73
82
  /**
74
83
  * Per declaration of the graph, whether the run that counts test references reaches it.
75
- * That run's roots are the graph's own and each test file, being the file's top level
76
- * and every declaration it exports, which a test runner may call; `held` seeds it as a
77
- * mark or an exemption seeds a sweep.
84
+ * That run's roots are the graph's own and, of each test file, its top level and every
85
+ * function it exports, which a test runner may call, and every export of a manual mock,
86
+ * which the runner reads; any other export of a test file, and test-support code, which
87
+ * runs where a test imports it, are judged as any file's are.
88
+ * `held` seeds it as a mark or an exemption seeds a sweep.
78
89
  */
79
90
  export function reachedWithTheTests(graph, held) {
80
91
  const tests = graph.symbols.flatMap((symbol, at) => {
81
- const top = symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
82
- return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
92
+ const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
93
+ const called = exported && runnerCalls(symbol);
94
+ return graph.test[at] === true &&
95
+ graph.support[at] !== true &&
96
+ (symbol.kind === "file" || called)
83
97
  ? [{ at, kind: "test-runner" }]
84
98
  : [];
85
99
  });
@@ -162,10 +176,11 @@ function reachable(graph, held, mode) {
162
176
  }
163
177
  /**
164
178
  * Admits every test declaration whose set of referenced production declarations is
165
- * non-empty and wholly dead, and returns which it admitted. The set is read from every
166
- * reference the test makes, which a production sweep does not count. A test-support
167
- * declaration that a test referencing a live target also references is a live target of
168
- * every test referencing it. A marked or exempt test is never admitted.
179
+ * non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
180
+ * code the target does not implement, so it is no production declaration here. The set is
181
+ * read from every reference the test makes, which a production sweep does not count. A
182
+ * test-support declaration a test referencing a live target also references is a live
183
+ * target of every test referencing it. A marked or exempt test is never admitted.
169
184
  */
170
185
  function admitTestsOfDeadCode(graph, dead, held) {
171
186
  const testCode = (at) => graph.test[at] === true || graph.support[at] === true;
@@ -174,7 +189,9 @@ function admitTestsOfDeadCode(graph, dead, held) {
174
189
  let live = 0;
175
190
  if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
176
191
  for (const edge of graph.out[at] ?? []) {
177
- if (testCode(edge.to) || graph.subject[edge.to] !== true) {
192
+ if (testCode(edge.to) ||
193
+ graph.subject[edge.to] !== true ||
194
+ graph.ambient[edge.to] === true) {
178
195
  continue;
179
196
  }
180
197
  targets += 1;
@@ -2,9 +2,12 @@
2
2
  * The members a unique symbol keys, used through element accesses. `value[key]`, where
3
3
  * `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
4
4
  * whose computed name is that same constant, and a store through the access writes it.
5
- * No name node resolves to such a member: the key's name resolves to the constant.
5
+ * No name node resolves to such a member: the key's name resolves to the constant. An
6
+ * iteration reads the member its well-known iteration symbol keys.
6
7
  */
7
- import { isComputedPropertyName, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
8
+ import { isComputedPropertyName, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPropertyAccessExpression, isSpreadElement, isStringLiteral, } from "@typescript/native/ast";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { renderPosition } from "./position.js";
8
11
  import { isAnswered } from "./query.js";
9
12
  /** The identifier an element access's argument names its key by, where it is a name. */
10
13
  export function keyNameOf(argument) {
@@ -112,3 +115,101 @@ export function keyedUses(project, chains, resolved, properties, cap) {
112
115
  return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
113
116
  });
114
117
  }
118
+ /** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
119
+ function wellKnownName(name) {
120
+ if (name === undefined || !isComputedPropertyName(name)) {
121
+ return undefined;
122
+ }
123
+ const key = name.expression;
124
+ return isPropertyAccessExpression(key) &&
125
+ isIdentifier(key.expression) &&
126
+ key.expression.text === "Symbol" &&
127
+ isIdentifier(key.name)
128
+ ? key.name.text
129
+ : undefined;
130
+ }
131
+ /**
132
+ * The references the iterations of one project's own files make to the iteration members
133
+ * of the values they iterate: a `for…of` statement and a spread element read
134
+ * `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
135
+ * which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
136
+ */
137
+ export function iterationReferences(project, held, chains, root, testFiles) {
138
+ const queries = project.queries;
139
+ const ownPaths = project.ownPaths();
140
+ const iterated = [];
141
+ for (const file of project.ownSourceFiles()) {
142
+ const fileId = held.declarations.get(nodeKey(file, file));
143
+ if (fileId === undefined) {
144
+ continue;
145
+ }
146
+ const visit = (node, from) => {
147
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
148
+ if (isForOfStatement(node)) {
149
+ const protocols = node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
150
+ iterated.push({ file, from: holder, operand: node.expression, protocols });
151
+ }
152
+ else if (isSpreadElement(node)) {
153
+ iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
154
+ }
155
+ node.forEachChild((child) => {
156
+ visit(child, holder);
157
+ });
158
+ };
159
+ file.forEachChild((child) => {
160
+ visit(child, fileId);
161
+ });
162
+ }
163
+ if (iterated.length === 0) {
164
+ return [];
165
+ }
166
+ const types = queries.typesAt(iterated.map((one) => one.operand));
167
+ const members = new Map();
168
+ const membersOf = (type) => {
169
+ const known = members.get(type.id);
170
+ if (known !== undefined) {
171
+ return known;
172
+ }
173
+ const keyed = [];
174
+ const properties = queries.propertiesOf(type);
175
+ for (const property of isAnswered(properties) ? properties : []) {
176
+ for (const handle of property.declarations) {
177
+ const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
178
+ const name = wellKnownName(node?.name);
179
+ if (name !== undefined) {
180
+ keyed.push({ name, ids: chains.declarationsOf(property) });
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ members.set(type.id, keyed);
186
+ return keyed;
187
+ };
188
+ const found = [];
189
+ iterated.forEach((one, index) => {
190
+ const type = types[index];
191
+ if (type === undefined || !isAnswered(type)) {
192
+ return;
193
+ }
194
+ const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
195
+ const position = renderPosition(one.file, root, one.operand.getStart());
196
+ for (const part of isAnswered(parts) ? parts : []) {
197
+ for (const member of membersOf(part)) {
198
+ if (!one.protocols.includes(member.name)) {
199
+ continue;
200
+ }
201
+ for (const to of member.ids) {
202
+ found.push({
203
+ from: one.from,
204
+ to,
205
+ position,
206
+ use: "read",
207
+ resolution: "keyed",
208
+ test: testFiles.has(position.path),
209
+ });
210
+ }
211
+ }
212
+ }
213
+ });
214
+ return found;
215
+ }
@@ -1,19 +1,113 @@
1
1
  /**
2
- * Test-support code: the files only tests reach. A test reaches each one through a chain
3
- * of references, and every reference to a declaration of one, an import for its effects
4
- * included, is written in a test file or in another such file, so it is judged with the
5
- * tests: what it references is referenced by test code.
2
+ * Test-support code: the files only tests reach that carry test evidence of their own. A
3
+ * test reaches each one through a chain of references, and every reference to a
4
+ * declaration of one, an import for its effects included, is written in a test file or in
5
+ * another such file, so it is judged with the tests: what it references is referenced by
6
+ * test code.
6
7
  */
8
+ import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.js";
9
+ import { packageOfSpecifier } from "./dependencies.js";
7
10
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
8
11
  const SKIP_ROOT = "type-error";
12
+ /**
13
+ * The test runners and test libraries an import of which is test evidence: each runner a
14
+ * convention row reads test files for, and the convention table's test libraries.
15
+ */
16
+ const TEST_PACKAGES = new Set([
17
+ ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
18
+ ...TEST_LIBRARIES,
19
+ ]);
20
+ /**
21
+ * The Node.js built-in modules a bare specifier names, which are no package. The set is
22
+ * closed: Node.js adds a new built-in under the `node:` scheme alone
23
+ * (https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
24
+ */
25
+ const NODE_BUILTINS = new Set([
26
+ "assert",
27
+ "async_hooks",
28
+ "buffer",
29
+ "child_process",
30
+ "cluster",
31
+ "console",
32
+ "constants",
33
+ "crypto",
34
+ "dgram",
35
+ "diagnostics_channel",
36
+ "dns",
37
+ "domain",
38
+ "events",
39
+ "fs",
40
+ "http",
41
+ "http2",
42
+ "https",
43
+ "inspector",
44
+ "module",
45
+ "net",
46
+ "os",
47
+ "path",
48
+ "perf_hooks",
49
+ "process",
50
+ "punycode",
51
+ "querystring",
52
+ "readline",
53
+ "repl",
54
+ "stream",
55
+ "string_decoder",
56
+ "sys",
57
+ "timers",
58
+ "tls",
59
+ "trace_events",
60
+ "tty",
61
+ "url",
62
+ "util",
63
+ "v8",
64
+ "vm",
65
+ "wasi",
66
+ "worker_threads",
67
+ "zlib",
68
+ ]);
69
+ /** Whether a specifier names a test runner or a test library. */
70
+ function namesTestPackage(specifier) {
71
+ const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
72
+ return (name !== undefined &&
73
+ (TEST_PACKAGES.has(name) || TEST_LIBRARY_SCOPES.some((scope) => name.startsWith(scope))));
74
+ }
75
+ /**
76
+ * Whether a file carries test evidence: an import of a test runner or a test library, or
77
+ * package imports, Node built-ins aside, that are all devDependencies no file imports
78
+ * outside the test files and `onlyTests`, the files only tests reach.
79
+ */
80
+ function evidenceReader(tests, evidence) {
81
+ const importers = new Map();
82
+ for (const [path, specifiers] of evidence.imports) {
83
+ for (const specifier of specifiers) {
84
+ const name = packageOfSpecifier(specifier);
85
+ if (name !== undefined) {
86
+ importers.set(name, [...(importers.get(name) ?? []), path]);
87
+ }
88
+ }
89
+ }
90
+ return (onlyTests, path) => {
91
+ const specifiers = evidence.imports.get(path) ?? [];
92
+ if (specifiers.some(namesTestPackage)) {
93
+ return true;
94
+ }
95
+ const names = specifiers.flatMap((one) => packageOfSpecifier(one) ?? []);
96
+ const packages = names.filter((name) => !NODE_BUILTINS.has(name));
97
+ return (packages.length > 0 &&
98
+ packages.every((name) => evidence.devDependency(path, name) &&
99
+ (importers.get(name) ?? []).every((one) => tests.has(one) || onlyTests.has(one))));
100
+ };
101
+ }
9
102
  /**
10
103
  * The files of one project that are test-support code, below the target root: the
11
104
  * largest set of files a test file reaches, none of them a test file or holding a root,
12
- * whose every user outside the set is a test file. A root has a caller outside the
13
- * reference graph: a runtime, a tool, a configuration, or a consumer of a library's
14
- * published API. `testFiles` are the files the test-file rules classified.
105
+ * each carrying test evidence of its own, whose every user outside the set is a test
106
+ * file; and the files only tests reach that are not among them. A root has a caller
107
+ * outside the reference graph: a runtime, a tool, a configuration, or a consumer of a
108
+ * library's published API. `testFiles` are the files the test-file rules classified.
15
109
  */
16
- export function testSupportFiles(symbols, references, roots, testFiles) {
110
+ export function testSupportFiles(symbols, references, roots, testFiles, evidence) {
17
111
  const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
18
112
  const users = new Map();
19
113
  const uses = new Map();
@@ -47,18 +141,25 @@ export function testSupportFiles(symbols, references, roots, testFiles) {
47
141
  }
48
142
  }
49
143
  }
50
- const support = new Set([...reached].filter((path) => !rooted.has(path)));
51
- for (let changed = true; changed;) {
52
- changed = false;
53
- for (const path of support) {
54
- const from = users.get(path) ?? new Set();
55
- if (![...from].every((one) => tests.has(one) || support.has(one))) {
56
- support.delete(path);
57
- changed = true;
144
+ const withoutOutsideUsers = (files) => {
145
+ for (let changed = true; changed;) {
146
+ changed = false;
147
+ for (const path of files) {
148
+ const from = users.get(path) ?? new Set();
149
+ if (![...from].every((one) => tests.has(one) || files.has(one))) {
150
+ files.delete(path);
151
+ changed = true;
152
+ }
58
153
  }
59
154
  }
60
- }
61
- return support;
155
+ return files;
156
+ };
157
+ // Evidence reads the files only tests reach, evidence or none, so it is fixed before the
158
+ // files without evidence leave the set.
159
+ const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
160
+ const carries = evidenceReader(tests, evidence);
161
+ const support = withoutOutsideUsers(new Set([...onlyTests].filter((path) => carries(onlyTests, path))));
162
+ return { support, testUsed: new Set([...onlyTests].filter((path) => !support.has(path))) };
62
163
  }
63
164
  /** The references with every one a test-support file makes counted as a test reference. */
64
165
  export function supportReferences(references, support) {
@@ -5,11 +5,12 @@
5
5
  * which only ever withholds a finding. `projectErrors` decides, before any of these, which
6
6
  * errors the compiler places in no file leave the project unloaded.
7
7
  */
8
- import { isClassDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
8
+ import { isClassDeclaration, isFunctionLikeDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/ast";
9
9
  import { packageOfSpecifier } from "./dependencies.js";
10
10
  import { nodeKey } from "./inventory.js";
11
11
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
12
12
  import { isAnswered } from "./query.js";
13
+ import { findWorkspace } from "./workspace.js";
13
14
  /** The compiler's codes for an import whose module it cannot find. */
14
15
  const MODULE_NOT_FOUND = new Set([2307, 2792]);
15
16
  const MANIFEST = "package.json";
@@ -163,20 +164,36 @@ function expectedModule(context, importer, specifier) {
163
164
  const manifest = name === undefined
164
165
  ? undefined
165
166
  : declaringManifest(host, dirnamePath(importer), targetRoot, name);
166
- if (manifest === undefined) {
167
+ // No run requires a nested package that is no workspace member to be installed.
168
+ if (manifest === undefined ||
169
+ (dirnamePath(manifest) !== targetRoot && !context.member(manifest))) {
167
170
  return undefined;
168
171
  }
169
172
  const where = relativePath(targetRoot, manifest) ?? manifest;
170
173
  return `a dependency ${where} declares that is not installed: run the package manager's install`;
171
174
  }
172
175
  /**
173
- * The unit a type error at one offset skips: the class member holding it where a
174
- * top-level class does, else the top-level statement.
176
+ * The unit a type error at one offset skips: the innermost function holding it, else the
177
+ * class member holding it where a top-level class does, else the top-level statement.
175
178
  */
176
179
  function unitAt(file, offset) {
177
180
  const statement = file.statements.find((one) => one.pos <= offset && offset < one.end);
178
- if (statement === undefined || !isClassDeclaration(statement)) {
179
- return statement;
181
+ if (statement === undefined) {
182
+ return undefined;
183
+ }
184
+ let innermost;
185
+ const visit = (node) => {
186
+ if (offset < node.getStart() || offset >= node.end) {
187
+ return;
188
+ }
189
+ if (isFunctionLikeDeclaration(node)) {
190
+ innermost = node;
191
+ }
192
+ node.forEachChild(visit);
193
+ };
194
+ visit(statement);
195
+ if (innermost !== undefined || !isClassDeclaration(statement)) {
196
+ return innermost ?? statement;
180
197
  }
181
198
  return statement.members.find((one) => one.pos <= offset && offset < one.end) ?? statement;
182
199
  }
@@ -190,7 +207,17 @@ function messageOf(text, targetRoot) {
190
207
  * is no error here, and one in a file that is not the project's own is that file's
191
208
  * package's to answer for.
192
209
  */
193
- export function readErrors(project, errors, context) {
210
+ export function readErrors(project, errors, where) {
211
+ let workspace = null;
212
+ const context = {
213
+ ...where,
214
+ member: (manifest) => {
215
+ if (workspace === null) {
216
+ workspace = findWorkspace(where.host, where.targetRoot, () => undefined);
217
+ }
218
+ return workspace?.memberOf(manifest) !== undefined;
219
+ },
220
+ };
194
221
  const files = new Map(project.ownSourceFiles().map((file) => [file.fileName, file]));
195
222
  const missing = [];
196
223
  const skips = [];