@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
@@ -6,7 +6,7 @@
6
6
  * the project handing such a parameter to a destination is one, to a fixpoint, and a
7
7
  * function invoked through `call`, `apply` or `bind` is the callee.
8
8
  */
9
- import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isTypeLiteralNode, isVariableDeclaration, ModifierFlags, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
9
+ import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isFunctionLikeDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isForInStatement, isPropertyAccessExpression, isPropertyDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isTypeLiteralNode, isVariableDeclaration, ModifierFlags, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
10
10
  import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/sync";
11
11
  import { aliasChains } from "./alias-chain.js";
12
12
  import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
@@ -173,9 +173,31 @@ function walkProject(project, held) {
173
173
  }
174
174
  return { sites, parameters };
175
175
  }
176
+ /** The calls of `Object` that read every own property of their argument by name. */
177
+ const ENUMERATIONS = new Set([
178
+ "keys",
179
+ "entries",
180
+ "values",
181
+ "getOwnPropertyNames",
182
+ "assign",
183
+ ]);
176
184
  /**
177
- * The parameters that keep nothing of the value they are given: one typed `unknown` or
178
- * `any`, and a rest parameter whose elements are, each by its symbol.
185
+ * Whether a parameter type keeps no member name of the value it is given: `unknown`, `any`,
186
+ * or an object type of a string index signature and no property.
187
+ */
188
+ function keepsNoName(project, type) {
189
+ if ((type.flags & ERASED) !== 0) {
190
+ return true;
191
+ }
192
+ if ((type.flags & TypeFlags.Object) === 0 ||
193
+ must(project.queries.propertiesOf(type)).length > 0) {
194
+ return false;
195
+ }
196
+ return must(project.queries.indexInfosOf(type)).some((info) => (info.keyType.flags & TypeFlags.String) !== 0);
197
+ }
198
+ /**
199
+ * The parameters that keep no member name of the value they are given, as `keepsNoName`
200
+ * reads it, and a rest parameter whose elements keep none, each by its symbol.
179
201
  */
180
202
  function erasedParameters(project, parameters) {
181
203
  const types = typesAt(project, parameters.map((parameter) => parameter.name));
@@ -185,10 +207,10 @@ function erasedParameters(project, parameters) {
185
207
  return false;
186
208
  }
187
209
  if (!parameter.rest) {
188
- return (type.flags & ERASED) !== 0;
210
+ return keepsNoName(project, type);
189
211
  }
190
212
  const [element] = must(project.queries.typeArguments(type));
191
- return element !== undefined && (element.flags & ERASED) !== 0;
213
+ return element !== undefined && keepsNoName(project, element);
192
214
  });
193
215
  const bySymbol = new Map();
194
216
  project
@@ -202,6 +224,103 @@ function erasedParameters(project, parameters) {
202
224
  });
203
225
  return bySymbol;
204
226
  }
227
+ /** Whether one node is a function, whose body runs only where something calls it. */
228
+ function isFunctionNode(node) {
229
+ return (isFunctionDeclaration(node) ||
230
+ isFunctionExpression(node) ||
231
+ isArrowFunction(node) ||
232
+ isMethodDeclaration(node) ||
233
+ isGetAccessorDeclaration(node) ||
234
+ isSetAccessorDeclaration(node));
235
+ }
236
+ /** The name a function nested in another is called by: its own, or the variable it initializes. */
237
+ function nestedName(fn) {
238
+ if (isFunctionDeclaration(fn)) {
239
+ return fn.name?.text;
240
+ }
241
+ let at = fn.parent;
242
+ while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
243
+ at = at.parent;
244
+ }
245
+ return isVariableDeclaration(at) && isIdentifier(at.name) ? at.name.text : undefined;
246
+ }
247
+ /** The names one function's body writes as identifiers, other than the names its declarations bind. */
248
+ function namesWritten(fn) {
249
+ const found = new Set();
250
+ const visit = (node) => {
251
+ const declares = (isFunctionDeclaration(node.parent) || isVariableDeclaration(node.parent)) &&
252
+ node.parent.name === node;
253
+ if (isIdentifier(node) && !declares) {
254
+ found.add(node.text);
255
+ }
256
+ node.forEachChild(visit);
257
+ };
258
+ fn.forEachChild(visit);
259
+ return found;
260
+ }
261
+ /**
262
+ * The erased parameters their own function enumerates: one an `Object.keys`, `entries`,
263
+ * `values`, `getOwnPropertyNames` or `assign` call takes, `Object` the standard library's,
264
+ * or a `for…in` statement walks, which reads every property of the value by its name. A
265
+ * nested function is read as the function's own only where its name is written in it, or
266
+ * it has none, a callback or a function called where it is written.
267
+ */
268
+ function enumeratedParameters(project, erased) {
269
+ const asked = [];
270
+ for (const parameter of erased.values()) {
271
+ const fn = parameter.name.parent.parent;
272
+ const spelled = parameter.name.getText();
273
+ const written = namesWritten(fn);
274
+ const visit = (node) => {
275
+ if (isFunctionNode(node)) {
276
+ const name = nestedName(node);
277
+ if (name !== undefined && !written.has(name)) {
278
+ return;
279
+ }
280
+ }
281
+ const read = [];
282
+ let object;
283
+ if (isCallExpression(node)) {
284
+ const callee = node.expression;
285
+ if (isPropertyAccessExpression(callee) &&
286
+ isIdentifier(callee.expression) &&
287
+ callee.expression.text === "Object" &&
288
+ ENUMERATIONS.has(callee.name.text)) {
289
+ object = callee.expression;
290
+ // `Object.assign` reads its sources, every argument after the target.
291
+ read.push(...node.arguments.slice(...(callee.name.text === "assign" ? [1] : [0, 1])));
292
+ }
293
+ }
294
+ else if (isForInStatement(node)) {
295
+ read.push(node.expression);
296
+ }
297
+ for (const value of read.map(evaluated)) {
298
+ if (isIdentifier(value) && value.text === spelled) {
299
+ asked.push({ node: value, parameter, object });
300
+ }
301
+ }
302
+ node.forEachChild(visit);
303
+ };
304
+ fn.forEachChild(visit);
305
+ }
306
+ const symbols = project.symbolsAt(asked.map((one) => project.handle(one.node))).map(must);
307
+ const objects = asked.flatMap((one) => (one.object === undefined ? [] : [one.object]));
308
+ const own = project.ownPaths();
309
+ const standard = new Set(project
310
+ .symbolsAt(objects.map((node) => project.handle(node)))
311
+ .map(must)
312
+ .flatMap((symbol, at) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [objects[at]] : []));
313
+ return [
314
+ ...new Set(asked.flatMap((one, at) => {
315
+ const symbol = symbols[at];
316
+ return symbol !== undefined &&
317
+ erased.get(symbol.id) === one.parameter &&
318
+ (one.object === undefined || standard.has(one.object))
319
+ ? [one.parameter]
320
+ : [];
321
+ })),
322
+ ];
323
+ }
205
324
  /** Each place a call's argument passes one of the erased parameters it can see. */
206
325
  function handedOn(project, sites, erased, invocationOf) {
207
326
  const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
@@ -285,8 +404,8 @@ function implementedOwners(project, inAnalysis) {
285
404
  * callee is a serializer an entry names; at the arguments its resolved signature types
286
405
  * `unknown` or `any` where its callee is declared outside the analysis or only ambiently
287
406
  * with no implementation in it; and, where its callee is a function of the project, at each
288
- * parameter the function hands on to a destination, holding back the widest of what those
289
- * destinations hold back, which is settled by widening until no parameter moves.
407
+ * parameter the function enumerates or hands on to a destination, holding back the widest
408
+ * of what those hold back, settled by widening until no parameter moves.
290
409
  */
291
410
  function destinations(input, chains, walked) {
292
411
  const { project, held } = input;
@@ -478,6 +597,9 @@ function destinations(input, chains, walked) {
478
597
  }
479
598
  return undefined;
480
599
  };
600
+ for (const parameter of enumeratedParameters(project, erased)) {
601
+ forwards.set(keyOf(parameter), "properties");
602
+ }
481
603
  const forwarded = handedOn(project, walked.sites, erased, invocation);
482
604
  for (let moved = true; moved;) {
483
605
  moved = false;
@@ -551,6 +673,14 @@ function flowsOf(project, held, sites, to, stores) {
551
673
  },
552
674
  ];
553
675
  });
676
+ const generic = typeParameterFlows(project, reach, sites, arguments_.flatMap((one) => {
677
+ const type = typeOf.get(one.value);
678
+ const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
679
+ const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
680
+ return destination === undefined
681
+ ? []
682
+ : parameters.map((parameter) => ({ parameter, to: destination }));
683
+ }));
554
684
  const left = stores.outside.flatMap((store) => {
555
685
  const reached = reach.reachedFrom(store.type);
556
686
  return reached.classes.size === 0 && reached.members.length === 0
@@ -565,7 +695,127 @@ function flowsOf(project, held, sites, to, stores) {
565
695
  },
566
696
  ];
567
697
  });
568
- return [...passed, ...left];
698
+ return [...passed, ...generic, ...left];
699
+ }
700
+ /** The type parameters one type names, through constituents and type arguments. */
701
+ function typeParametersOf(project, type, depth) {
702
+ if (type.isTypeParameter()) {
703
+ return [type];
704
+ }
705
+ if (depth >= MENTION_DEPTH) {
706
+ return [];
707
+ }
708
+ const { queries } = project;
709
+ return [
710
+ ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
711
+ ...must(queries.typeArguments(type)),
712
+ ].flatMap((one) => typeParametersOf(project, one, depth + 1));
713
+ }
714
+ /**
715
+ * The types an instantiated type puts where a declared type names `parameter`: matched
716
+ * through type arguments, and in a union every constituent the declared union does not
717
+ * itself name.
718
+ */
719
+ function bound(project, declared, instantiated, parameter, depth) {
720
+ if (declared.id === parameter.id) {
721
+ return [instantiated];
722
+ }
723
+ if (depth >= MENTION_DEPTH) {
724
+ return [];
725
+ }
726
+ const { queries } = project;
727
+ if (declared.isUnionType()) {
728
+ const parts = must(queries.constituents(declared));
729
+ if (parts.some((part) => part.id === parameter.id)) {
730
+ const kept = new Set(parts.map((part) => part.id));
731
+ const given = instantiated.isUnionType()
732
+ ? must(queries.constituents(instantiated))
733
+ : [instantiated];
734
+ return given.filter((part) => !kept.has(part.id));
735
+ }
736
+ }
737
+ const declaredArguments = must(queries.typeArguments(declared));
738
+ const givenArguments = must(queries.typeArguments(instantiated));
739
+ return declaredArguments.length !== givenArguments.length
740
+ ? []
741
+ : declaredArguments.flatMap((one, at) => {
742
+ const given = givenArguments[at];
743
+ return given === undefined ? [] : bound(project, one, given, parameter, depth + 1);
744
+ });
745
+ }
746
+ /**
747
+ * The flows a type parameter of a function of the project carries where a value its type
748
+ * names reaches a destination: each call of the function passes its type argument there,
749
+ * written or inferred from the arguments, and a type argument that is a type parameter
750
+ * of the calling function is followed on to that function's calls, to a fixpoint.
751
+ */
752
+ function typeParameterFlows(project, reach, sites, pending) {
753
+ const { queries } = project;
754
+ const byName = new Map();
755
+ for (const site of sites) {
756
+ const name = calleeName(site.call.expression)?.getText();
757
+ if (name !== undefined) {
758
+ byName.set(name, [...(byName.get(name) ?? []), site]);
759
+ }
760
+ }
761
+ const found = [];
762
+ const seen = new Set();
763
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
764
+ const { parameter, to } = next;
765
+ if (seen.has(parameter.id)) {
766
+ continue;
767
+ }
768
+ seen.add(parameter.id);
769
+ const symbol = must(queries.symbolOfType(parameter));
770
+ const [handle] = symbol?.declarations ?? [];
771
+ const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
772
+ const fn = declaration?.parent;
773
+ const index = fn === undefined || !isFunctionLikeDeclaration(fn)
774
+ ? -1
775
+ : (fn.typeParameters ?? []).findIndex((one) => one === declaration);
776
+ const name = fn === undefined
777
+ ? undefined
778
+ : isMethodDeclaration(fn) && isIdentifier(fn.name)
779
+ ? fn.name.text
780
+ : nestedName(fn);
781
+ if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
782
+ continue;
783
+ }
784
+ for (const site of byName.get(name) ?? []) {
785
+ const resolved = must(queries.resolvedSignature(site.call));
786
+ const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
787
+ const called = declared?.declaration === undefined
788
+ ? undefined
789
+ : project.declarationAt(declared.declaration)?.node;
790
+ if (resolved === undefined || declared === undefined || called !== fn) {
791
+ continue;
792
+ }
793
+ const written = site.call.typeArguments?.[index];
794
+ const given = written === undefined
795
+ ? site.call.arguments.flatMap((_argument, at) => {
796
+ const from = must(queries.parameterType(declared, at));
797
+ const into = must(queries.parameterType(resolved, at));
798
+ return from === undefined || into === undefined
799
+ ? []
800
+ : bound(project, from, into, parameter, 0);
801
+ })
802
+ : typesAt(project, [written]).filter((type) => type !== undefined);
803
+ for (const type of given) {
804
+ const reached = reach.reachedFrom(type);
805
+ if (reached.classes.size > 0 || reached.members.length > 0) {
806
+ found.push({
807
+ site: site.call,
808
+ classes: reached.classes,
809
+ members: reached.members,
810
+ to,
811
+ detail: `passed to ${to.callee} through the type parameter ${symbol.name}`,
812
+ });
813
+ }
814
+ pending.push(...typeParametersOf(project, type, 0).map((one) => ({ parameter: one, to })));
815
+ }
816
+ }
817
+ }
818
+ return found;
569
819
  }
570
820
  /**
571
821
  * The serialization-contract detector: each data member and `toJSON` of each class,
package/dist/src/sweep.js CHANGED
@@ -84,10 +84,10 @@ export function runnerCalls(symbol) {
84
84
  * That run's roots are the graph's own and, of each test file, its top level and every
85
85
  * function it exports, which a test runner may call, and every export of a manual mock,
86
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.
87
+ * runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
88
+ * seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
89
89
  */
90
- export function reachedWithTheTests(graph, held) {
90
+ export function reachedWithTheTests(graph, held, namespaces = true) {
91
91
  const tests = graph.symbols.flatMap((symbol, at) => {
92
92
  const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
93
93
  const called = exported && runnerCalls(symbol);
@@ -97,9 +97,7 @@ export function reachedWithTheTests(graph, held) {
97
97
  ? [{ at, kind: "test-runner" }]
98
98
  : [];
99
99
  });
100
- return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
101
- production: false,
102
- });
100
+ return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), { production: false }, namespaces);
103
101
  }
104
102
  /**
105
103
  * The references a loaded consumer makes that the mode counts. A production mode counts
@@ -126,7 +124,7 @@ function flagged(graph, ids) {
126
124
  * included, while a root reaches through the references the mode counts. The held seed
127
125
  * runs first, so what it reaches is expanded under the wider rule.
128
126
  */
129
- function reachable(graph, held, mode) {
127
+ function reachable(graph, held, mode, namespaces = true) {
130
128
  const reached = graph.symbols.map(() => false);
131
129
  const expanded = graph.symbols.map(() => false);
132
130
  const queue = [];
@@ -154,7 +152,7 @@ function reachable(graph, held, mode) {
154
152
  if (!holding && mode.production && edge.test) {
155
153
  continue;
156
154
  }
157
- enter(edge.to, !edge.evaluation);
155
+ enter(edge.to, namespaces && !edge.evaluation);
158
156
  }
159
157
  }
160
158
  };
@@ -169,7 +167,7 @@ function reachable(graph, held, mode) {
169
167
  }
170
168
  // A consumer's reference reaches what it names as one of the target's own does.
171
169
  for (const edge of callsOf(graph, mode)) {
172
- enter(edge.to, !edge.evaluation);
170
+ enter(edge.to, namespaces && !edge.evaluation);
173
171
  }
174
172
  walk(false);
175
173
  return reached;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The globals a test runner injects into every file it runs, which no import names: a
3
+ * Vitest configuration whose `test.globals` is `true` gives each test and each file it
4
+ * loads `describe`, `it`, `expect` and `vi` (https://vitest.dev/config/globals). A program
5
+ * analyzed without the runner's declarations of them reads every use as an error, so a
6
+ * configuration under such a runner is read with them; and a file that calls one is
7
+ * written for the runner, as one that imports it is.
8
+ */
9
+ import { isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isExpressionStatement, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, SyntaxKind, } from "@typescript/native/ast";
10
+ import { dirnamePath, joinPath } from "./paths.js";
11
+ import { UNANSWERED } from "./query.js";
12
+ import { writtenPath } from "./workspace-programs.js";
13
+ /** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
14
+ export const GLOBALS_RUNNER = "vitest";
15
+ const GLOBALS_TYPES = "vitest/globals";
16
+ /** The configuration files the runner reads its settings from, by name. */
17
+ const RUNNER_CONFIG = /^(?:vitest|vite)\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u;
18
+ /** The globals `vitest/globals` declares that a test or a helper calls or reads a member of. */
19
+ const GLOBAL_NAMES = new Set([
20
+ "afterAll",
21
+ "afterEach",
22
+ "assert",
23
+ "assertType",
24
+ "beforeAll",
25
+ "beforeEach",
26
+ "bench",
27
+ "describe",
28
+ "expect",
29
+ "expectTypeOf",
30
+ "it",
31
+ "onTestFailed",
32
+ "onTestFinished",
33
+ "suite",
34
+ "test",
35
+ "vi",
36
+ "vitest",
37
+ ]);
38
+ /** The expression one wrapper that changes no value holds, and a call's first argument. */
39
+ function configObject(node) {
40
+ let at = node;
41
+ while (at !== undefined) {
42
+ if (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
43
+ at = at.expression;
44
+ }
45
+ else if (isCallExpression(at)) {
46
+ at = at.arguments[0];
47
+ }
48
+ else {
49
+ return at;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /** The value one object literal writes for one member as `name: value`. */
55
+ function member(object, name) {
56
+ if (object === undefined || !isObjectLiteralExpression(object)) {
57
+ return undefined;
58
+ }
59
+ for (const property of object.properties) {
60
+ if (isPropertyAssignment(property) &&
61
+ isIdentifier(property.name) &&
62
+ property.name.text === name) {
63
+ return configObject(property.initializer);
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ /** Whether one runner configuration's default export sets `test.globals` to `true`. */
69
+ function setsGlobals(file) {
70
+ for (const statement of file.statements) {
71
+ let exported;
72
+ if (isExportAssignment(statement)) {
73
+ exported = statement.expression;
74
+ }
75
+ else if (isExpressionStatement(statement) && isBinaryExpression(statement.expression)) {
76
+ const { left, operatorToken, right } = statement.expression;
77
+ if (operatorToken.kind === SyntaxKind.EqualsToken &&
78
+ isPropertyAccessExpression(left) &&
79
+ isIdentifier(left.expression) &&
80
+ left.expression.text === "module" &&
81
+ isIdentifier(left.name) &&
82
+ left.name.text === "exports") {
83
+ exported = right;
84
+ }
85
+ }
86
+ const object = exported === undefined ? undefined : configObject(exported);
87
+ const globals = member(member(object, "test"), "globals");
88
+ if (globals?.kind === SyntaxKind.TrueKeyword) {
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+ /** Whether the runner's configuration in one manifest's directory injects the globals. */
95
+ function injectsGlobals(host, parse, dir) {
96
+ let names;
97
+ try {
98
+ names = host
99
+ .readDirectory(dir)
100
+ .filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
101
+ .map((entry) => entry.name);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ return names.some((name) => {
107
+ const path = joinPath(dir, name);
108
+ try {
109
+ return setsGlobals(parse(path, host.readFile(path)));
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ });
115
+ }
116
+ /** Whether a file declaring the globals is installed where the configuration resolves packages from. */
117
+ function globalsInstalled(host, configFile) {
118
+ for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
119
+ if (host.kindOf(joinPath(dir, "node_modules", GLOBALS_RUNNER, "globals.d.ts")) === "file") {
120
+ return true;
121
+ }
122
+ if (dirnamePath(dir) === dir) {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ /** The directory of the manifest nearest one configuration file, at or above its own. */
128
+ function manifestDir(host, configFile) {
129
+ for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
130
+ if (host.kindOf(joinPath(dir, "package.json")) === "file") {
131
+ return dir;
132
+ }
133
+ if (dirnamePath(dir) === dir) {
134
+ return undefined;
135
+ }
136
+ }
137
+ }
138
+ function typesOf(value) {
139
+ return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : undefined;
140
+ }
141
+ /**
142
+ * How the snapshot reads each configuration of `configFiles` whose nearest manifest's
143
+ * runner configuration injects the globals and that can resolve their declarations: as
144
+ * `programs` reads it, else as itself, with the globals' declarations added to its `types`.
145
+ * A configuration the compiler cannot read is left for its project to refuse.
146
+ */
147
+ export function withTestGlobals(engine, host, parse, configFiles, programs) {
148
+ const found = new Map(programs);
149
+ const injects = new Map();
150
+ for (const configFile of configFiles) {
151
+ const dir = manifestDir(host, configFile);
152
+ if (dir === undefined || !globalsInstalled(host, configFile)) {
153
+ continue;
154
+ }
155
+ if (!injects.has(dir)) {
156
+ injects.set(dir, injectsGlobals(host, parse, dir));
157
+ }
158
+ if (injects.get(dir) !== true) {
159
+ continue;
160
+ }
161
+ let base;
162
+ try {
163
+ base = typesOf(engine.parseConfigFile(configFile).options.types) ?? [];
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ const held = found.get(configFile);
169
+ if (held?.kind === "program") {
170
+ const listed = typesOf(held.params.compilerOptions.types) ?? base;
171
+ if (!listed.includes(GLOBALS_TYPES)) {
172
+ found.set(configFile, {
173
+ kind: "program",
174
+ params: {
175
+ ...held.params,
176
+ compilerOptions: { ...held.params.compilerOptions, types: [...listed, GLOBALS_TYPES] },
177
+ },
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ let written = { extends: configFile };
183
+ if (held !== undefined) {
184
+ try {
185
+ written = JSON.parse(held.text);
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ }
191
+ const options = (written["compilerOptions"] ?? {});
192
+ const listed = typesOf(options["types"]) ?? base;
193
+ if (listed.includes(GLOBALS_TYPES)) {
194
+ continue;
195
+ }
196
+ found.set(configFile, {
197
+ kind: "configuration",
198
+ file: held?.file ?? writtenPath(host, configFile),
199
+ text: `${JSON.stringify({ ...written, compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] } }, null, 2)}\n`,
200
+ });
201
+ }
202
+ return found;
203
+ }
204
+ /**
205
+ * Whether one file names a global the runner injects, which is how a file written for the
206
+ * runner's globals reads: a call of one of the names or a read of a member of it that
207
+ * resolves to a declaration outside the project's own files, the runner's declaration
208
+ * file. A name the file binds, imported, declared or a parameter, resolves to its own.
209
+ */
210
+ export function namesTestGlobals(project, file) {
211
+ const named = [];
212
+ const visit = (node) => {
213
+ const callee = isCallExpression(node)
214
+ ? node.expression
215
+ : isPropertyAccessExpression(node)
216
+ ? node.expression
217
+ : undefined;
218
+ if (callee !== undefined && isIdentifier(callee) && GLOBAL_NAMES.has(callee.text)) {
219
+ named.push(callee);
220
+ }
221
+ node.forEachChild(visit);
222
+ };
223
+ file.forEachChild(visit);
224
+ if (named.length === 0) {
225
+ return false;
226
+ }
227
+ const own = project.ownPaths();
228
+ return project
229
+ .symbolsAt(named.map((node) => project.handle(node)))
230
+ .some((symbol) => symbol === UNANSWERED ||
231
+ (symbol !== undefined &&
232
+ symbol.declarations.length > 0 &&
233
+ symbol.declarations.every((handle) => !own.has(handle.path))));
234
+ }
@@ -13,6 +13,12 @@ import { isAnswered } from "./query.js";
13
13
  import { findWorkspace } from "./workspace.js";
14
14
  /** The compiler's codes for an import whose module it cannot find. */
15
15
  const MODULE_NOT_FOUND = new Set([2307, 2792]);
16
+ /**
17
+ * The compiler's codes for where the configuration places a file, which leave every type
18
+ * the file's statements carry as it is: an output that would overwrite an input (5055,
19
+ * 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
20
+ */
21
+ const CONFIGURATION_LEVEL = new Set([5055, 5056, 6059, 6307]);
16
22
  const MANIFEST = "package.json";
17
23
  const MANIFEST_SECTIONS = [
18
24
  "dependencies",
@@ -204,8 +210,8 @@ function messageOf(text, targetRoot) {
204
210
  }
205
211
  /**
206
212
  * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
207
- * is no error here, and one in a file that is not the project's own is that file's
208
- * package's to answer for.
213
+ * or about where the configuration places a file is no error here, and one in a file that
214
+ * is not the project's own is that file's package's to answer for.
209
215
  */
210
216
  export function readErrors(project, errors, where) {
211
217
  let workspace = null;
@@ -223,7 +229,9 @@ export function readErrors(project, errors, where) {
223
229
  const skips = [];
224
230
  for (const { diagnostic, at } of errors) {
225
231
  const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
226
- if (file === undefined || diagnostic.reportsUnnecessary === true) {
232
+ if (file === undefined ||
233
+ diagnostic.reportsUnnecessary === true ||
234
+ CONFIGURATION_LEVEL.has(diagnostic.code)) {
227
235
  continue;
228
236
  }
229
237
  const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;