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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +61 -10
  3. package/dist/src/augmentations.js +63 -1
  4. package/dist/src/build-scripts.js +437 -0
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +56 -9
  7. package/dist/src/convention-rows.js +39 -2
  8. package/dist/src/conventions.js +15 -0
  9. package/dist/src/dependencies.js +98 -1
  10. package/dist/src/discover.js +14 -82
  11. package/dist/src/entry-points.js +9 -18
  12. package/dist/src/exempt.js +8 -0
  13. package/dist/src/findings/completion.js +66 -28
  14. package/dist/src/findings/intra-function.js +4 -2
  15. package/dist/src/findings/reads-and-writes.js +7 -6
  16. package/dist/src/findings/unused-declarations.js +4 -1
  17. package/dist/src/graph.js +5 -3
  18. package/dist/src/intra-function-parts.js +45 -17
  19. package/dist/src/kinds.js +8 -8
  20. package/dist/src/manifest.js +18 -0
  21. package/dist/src/matrix.js +38 -4
  22. package/dist/src/query.js +1 -0
  23. package/dist/src/report.js +23 -4
  24. package/dist/src/reporters/annotations.js +15 -8
  25. package/dist/src/reporters/sarif.js +12 -8
  26. package/dist/src/roots.js +197 -42
  27. package/dist/src/serialization-contract.js +590 -12
  28. package/dist/src/sweep.js +7 -9
  29. package/dist/src/test-globals.js +234 -0
  30. package/dist/src/type-errors.js +11 -3
  31. package/dist/src/unseen-loads.js +107 -0
  32. package/dist/src/verbs/analyze.js +24 -3
  33. package/dist/src/workflow-steps.js +32 -15
  34. package/dist/src/yaml-document.js +355 -0
  35. package/jsr.json +1 -1
  36. package/package.json +2 -2
  37. package/src/analysis.ts +109 -12
  38. package/src/augmentations.ts +77 -2
  39. package/src/build-scripts.ts +564 -0
  40. package/src/conformance-record.ts +1 -1
  41. package/src/contextual-uses.ts +64 -12
  42. package/src/convention-rows.ts +47 -2
  43. package/src/conventions.ts +16 -0
  44. package/src/dependencies.ts +110 -1
  45. package/src/discover.ts +14 -94
  46. package/src/entry-points.ts +10 -25
  47. package/src/exempt.ts +16 -0
  48. package/src/file-facts.ts +3 -0
  49. package/src/findings/boundary.ts +2 -0
  50. package/src/findings/completion.ts +76 -30
  51. package/src/findings/intra-function.ts +4 -2
  52. package/src/findings/reads-and-writes.ts +7 -6
  53. package/src/findings/unused-declarations.ts +6 -1
  54. package/src/graph.ts +5 -3
  55. package/src/intra-function-parts.ts +55 -16
  56. package/src/kinds.ts +8 -8
  57. package/src/manifest.ts +21 -3
  58. package/src/matrix.ts +51 -4
  59. package/src/query.ts +11 -0
  60. package/src/report.ts +56 -16
  61. package/src/reporters/annotations.ts +21 -10
  62. package/src/reporters/reporter.ts +1 -3
  63. package/src/reporters/sarif.ts +16 -8
  64. package/src/roots.ts +226 -46
  65. package/src/serialization-contract.ts +758 -12
  66. package/src/sweep.ts +23 -9
  67. package/src/test-globals.ts +275 -0
  68. package/src/type-errors.ts +14 -3
  69. package/src/unseen-loads.ts +145 -0
  70. package/src/verbs/analyze.ts +28 -3
  71. package/src/workflow-steps.ts +35 -17
  72. package/src/yaml-document.ts +377 -0
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * The uses contextual types make of the target's members, which no name node resolves to.
3
3
  * An object-literal property writes the member of its name on each constituent of the
4
- * literal's contextual type that declares one. A value reaching a position typed by another
5
- * object type reads each of its members that type declares, through array elements and
6
- * the parameters and results of functions and constructors too; a class instance there is
7
- * the interface-satisfaction class's evidence instead.
4
+ * literal's contextual type that declares one, and so does a property of a literal whose
5
+ * type is inferred, where a value of that type reaches such a position. A value reaching a
6
+ * position typed by another object type reads each of its members that type declares,
7
+ * through array elements and the parameters and results of functions and constructors
8
+ * too; a class instance there is the interface-satisfaction class's evidence instead.
8
9
  */
9
10
 
10
11
  import {
@@ -109,10 +110,12 @@ interface Checked {
109
110
  readonly source: Type;
110
111
  readonly targets: readonly Type[];
111
112
  /**
112
- * Whether the source is declared outside the target, so code the analysis does not
113
- * hold reads the members of the target's types it declares.
113
+ * What the source is: a type whose members the target declares, which the position
114
+ * reads; one declared outside the target, so code the analysis does not hold reads the
115
+ * members of the target's types it declares; or an object literal's own type, whose
116
+ * properties write the position's members of their names.
114
117
  */
115
- readonly outside: boolean;
118
+ readonly sourceKind: "members" | "outside" | "literal";
116
119
  }
117
120
 
118
121
  /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
@@ -287,6 +290,37 @@ export function contextualUses<Brand>(
287
290
  return members.length > 0 && members.every((member) => !member.own);
288
291
  };
289
292
 
293
+ const literalTypes = new Map<number, boolean>();
294
+ /**
295
+ * Whether an object type is an object literal's own, inferred where no contextual type
296
+ * names it: every member a property an object literal of the target writes.
297
+ */
298
+ const literalTyped = (part: Type): boolean => {
299
+ let known = literalTypes.get(part.id);
300
+ if (known === undefined) {
301
+ const members = [...membersOf(part).values()];
302
+ known =
303
+ members.length > 0 &&
304
+ members.every(
305
+ (member) =>
306
+ member.ids.length === 0 &&
307
+ member.declarations.length > 0 &&
308
+ member.declarations.every((handle) => literalProperty(handle) !== undefined),
309
+ );
310
+ literalTypes.set(part.id, known);
311
+ }
312
+ return known;
313
+ };
314
+ /** The name node of the object literal property one declaration handle names, if it is one. */
315
+ const literalProperty = (handle: NodeHandle): Node | undefined => {
316
+ const node = project.declarationAt(handle)?.node;
317
+ return node !== undefined &&
318
+ (isPropertyAssignment(node) || isShorthandPropertyAssignment(node)) &&
319
+ isObjectLiteralExpression(node.parent)
320
+ ? node.name
321
+ : undefined;
322
+ };
323
+
290
324
  const elements = new Map<number, Type | undefined>();
291
325
  /** The element type of an array type, read once per type. */
292
326
  const elementOf = (type: Type): Type | undefined => {
@@ -379,9 +413,11 @@ export function contextualUses<Brand>(
379
413
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
380
414
  for (const one of sources) {
381
415
  if (carries(one)) {
382
- into.push({ source: one, targets, outside: false });
416
+ into.push({ source: one, targets, sourceKind: "members" });
383
417
  } else if (outside(one)) {
384
- into.push({ source: one, targets, outside: true });
418
+ into.push({ source: one, targets, sourceKind: "outside" });
419
+ } else if (literalTyped(one)) {
420
+ into.push({ source: one, targets, sourceKind: "literal" });
385
421
  }
386
422
  const element = elementOf(one);
387
423
  const called = element === undefined ? callsOf(one) : [];
@@ -423,7 +459,7 @@ export function contextualUses<Brand>(
423
459
  }
424
460
  seen.add(type.id);
425
461
  return partsOf(type).some((part) => {
426
- if (carries(part) || outside(part)) {
462
+ if (carries(part) || outside(part) || literalTyped(part)) {
427
463
  return true;
428
464
  }
429
465
  const element = elementOf(part);
@@ -444,6 +480,8 @@ export function contextualUses<Brand>(
444
480
  cap,
445
481
  );
446
482
  const reads: ContextualUse[] = [];
483
+ // A literal property writes a position's members once, however many values carry it there.
484
+ const carriedWrites = new Set<string>();
447
485
  sourced.forEach(({ flow, type }, index) => {
448
486
  const position = positions[index];
449
487
  if (position === undefined || !isAnswered(position)) {
@@ -452,10 +490,24 @@ export function contextualUses<Brand>(
452
490
  const checked: Checked[] = [];
453
491
  compared(type, position, checked, new Set());
454
492
  const ids = new Set<string>();
455
- for (const { source, targets, outside: held } of checked) {
493
+ for (const { source, targets, sourceKind } of checked) {
456
494
  const tables = targets.map(membersOf);
457
495
  const sourceMembers = membersOf(source);
458
- if (held) {
496
+ if (sourceKind === "literal") {
497
+ for (const [name, member] of sourceMembers) {
498
+ const written = [...new Set(tables.flatMap((table) => table.get(name)?.ids ?? []))];
499
+ for (const at of written.length === 0 ? [] : member.declarations) {
500
+ const node = literalProperty(at);
501
+ const key = `${at.path}#${String(at.index)}>${written.join(",")}`;
502
+ if (node !== undefined && !carriedWrites.has(key)) {
503
+ carriedWrites.add(key);
504
+ writes.push({ at: node, from: flow.from, ids: written });
505
+ }
506
+ }
507
+ }
508
+ continue;
509
+ }
510
+ if (sourceKind === "outside") {
459
511
  for (const table of tables) {
460
512
  for (const [name, member] of table) {
461
513
  if (sourceMembers.has(name)) {
@@ -146,8 +146,8 @@ const SCRIPT = "{js,jsx,ts,tsx}";
146
146
  const MODULE = "{js,mjs,ts,mts}";
147
147
  const ANY_MODULE = "{js,mjs,cjs,ts,mts,cts}";
148
148
  const VITE_CONFIG = `vite.config.${ANY_MODULE}`;
149
- /** The legacy configuration files of ESLint that hold JSON or a CommonJS module. */
150
- const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{js,cjs}"];
149
+ /** The legacy configuration files of ESLint: JSON, YAML or a CommonJS module. */
150
+ const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{yml,yaml}", ".eslintrc.{js,cjs}"];
151
151
 
152
152
  const SVELTEKIT_ROUTE_FILES = "+{page,layout,server,page.server,layout.server,error}";
153
153
 
@@ -209,6 +209,35 @@ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
209
209
  "scripts",
210
210
  ].map((key) => ({ key, files: ["angular.json"] }));
211
211
 
212
+ /**
213
+ * The row of a code generator that writes its output below one directory its
214
+ * configuration's `property` sets, `fallback` where no configuration file sets it, so
215
+ * every file there is a generated file.
216
+ */
217
+ function outputDirectoryRow(row: {
218
+ readonly name: string;
219
+ readonly package: string;
220
+ readonly range: string;
221
+ readonly files: readonly string[];
222
+ readonly property: string;
223
+ readonly fallback: string;
224
+ }): ConventionRow {
225
+ return {
226
+ name: row.name,
227
+ package: row.package,
228
+ range: row.range,
229
+ entries: [],
230
+ generated: ["<output>"],
231
+ moves: [
232
+ {
233
+ id: "output",
234
+ defaults: [row.fallback],
235
+ readings: [{ property: row.property, files: row.files }],
236
+ },
237
+ ],
238
+ };
239
+ }
240
+
212
241
  /**
213
242
  * The test runners and the assertion, mock and runner libraries whose import is test
214
243
  * evidence beside each runner a row reads test files for. Each applies whether or not a
@@ -299,6 +328,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
299
328
  range: ">=4.0.0 <10.0.0",
300
329
  entries: [],
301
330
  moves: [],
331
+ packageKeys: [{ key: "parser", files: ESLINTRC }],
302
332
  prefixedNames: [
303
333
  { property: "plugins", files: ESLINTRC, prefix: "eslint-plugin" },
304
334
  { property: "extends", files: ESLINTRC, prefix: "eslint-plugin", marker: "plugin:" },
@@ -414,6 +444,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
414
444
  { key: "oclif.hooks", directory: false },
415
445
  ],
416
446
  },
447
+ outputDirectoryRow({
448
+ name: "panda",
449
+ package: "@pandacss/dev",
450
+ range: ">=0.1.0 <2.0.0",
451
+ files: [`panda.config.${ANY_MODULE}`],
452
+ property: "outdir",
453
+ fallback: "styled-system",
454
+ }),
417
455
  {
418
456
  name: "parcel",
419
457
  package: "parcel",
@@ -481,6 +519,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
481
519
  },
482
520
  ],
483
521
  },
522
+ {
523
+ name: "react-scripts",
524
+ package: "react-scripts",
525
+ range: ">=1.0.0 <6.0.0",
526
+ entries: [`src/index.${SCRIPT}`, "src/setupProxy.js", `src/setupTests.${SCRIPT}`],
527
+ moves: [],
528
+ },
484
529
  {
485
530
  name: "remix",
486
531
  package: "@remix-run/dev",
@@ -66,6 +66,7 @@ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from
66
66
  import type { SetupFailure } from "./setup-failure.ts";
67
67
  import type { SourceFiles } from "./source-files.ts";
68
68
  import { satisfies } from "./version-range.ts";
69
+ import { yamlDocument } from "./yaml-document.ts";
69
70
 
70
71
  /** One manifest the analysis reads. */
71
72
  export interface ConventionManifest {
@@ -151,6 +152,13 @@ type ParseSource = (fileName: string, text: string) => SourceFile;
151
152
 
152
153
  const JSON_EXTENSION = ".json";
153
154
 
155
+ /**
156
+ * A configuration file its tool reads as a YAML document, read as the JSON document it
157
+ * denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
158
+ * line would name the wrong line.
159
+ */
160
+ const YAML_NAME = /\.ya?ml$/u;
161
+
154
162
  /** A configuration file with no extension that its tool reads as a JSON document. */
155
163
  const JSON_RC = /^\.[\w-]+rc$/u;
156
164
 
@@ -883,6 +891,14 @@ function readingFiles(
883
891
  } catch {
884
892
  continue;
885
893
  }
894
+ if (YAML_NAME.test(name)) {
895
+ const value = yamlDocument(text);
896
+ if (value !== undefined) {
897
+ const json = JSON.stringify(value);
898
+ found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
899
+ }
900
+ continue;
901
+ }
886
902
  found.push({
887
903
  name,
888
904
  text,
@@ -48,7 +48,7 @@ import type { DependencySection, Module } from "./ref.ts";
48
48
  import { UNANSWERED } from "./query.ts";
49
49
  import type { Handle, ProjectView } from "./session.ts";
50
50
  import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
51
- import { workflowTokens } from "./workflow-steps.ts";
51
+ import { workflowCommands, workflowTokens } from "./workflow-steps.ts";
52
52
 
53
53
  const MANIFEST = "package.json";
54
54
  const MODULES_DIR = "node_modules";
@@ -871,6 +871,110 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
871
871
  return found;
872
872
  }
873
873
 
874
+ /**
875
+ * The runtime flags whose value is a module loaded before the entry, as `--flag value` or
876
+ * `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
877
+ * https://nodejs.org/api/cli.html#--experimental-loadermodule,
878
+ * https://nodejs.org/api/cli.html#-r---require-module
879
+ */
880
+ const PRELOAD_FLAGS: ReadonlySet<string> = new Set([
881
+ "--experimental-loader",
882
+ "--import",
883
+ "--loader",
884
+ "--require",
885
+ "-r",
886
+ ]);
887
+
888
+ /**
889
+ * The programs that pass a preload flag to the runtime they start: Node itself, and the
890
+ * runners that forward Node's flags or read the same `--require`:
891
+ * https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
892
+ * https://typestrong.org/ts-node/docs/options#require,
893
+ * https://github.com/remy/nodemon#running-non-node-scripts
894
+ */
895
+ const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
896
+ "mocha",
897
+ "node",
898
+ "nodemon",
899
+ "ts-node",
900
+ "tsx",
901
+ ]);
902
+
903
+ /** The programs that run the command after them, and an environment assignment before it. */
904
+ const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
905
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
906
+
907
+ /** The commands one script string runs, split at the shell's list and pipeline operators. */
908
+ const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
909
+
910
+ /**
911
+ * The command strings one manifest runs: each of its `scripts`, and the `exec` command of
912
+ * nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
913
+ * it, which nodemon runs the watched script with:
914
+ * https://github.com/remy/nodemon#config-files
915
+ */
916
+ function manifestCommands(
917
+ host: Host,
918
+ dir: string,
919
+ manifest: Record<string, unknown>,
920
+ ): readonly string[] {
921
+ const scripts = manifest["scripts"];
922
+ const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
923
+ return [
924
+ ...(isRecord(scripts) ? Object.values(scripts) : []),
925
+ ...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
926
+ ].filter((command): command is string => typeof command === "string");
927
+ }
928
+
929
+ /**
930
+ * The packages `scripts` preload by a runtime flag's bare module specifier, a flag
931
+ * counting only in a command whose program passes it to the runtime, and for Node only
932
+ * before the script it runs, whose own arguments come after it.
933
+ */
934
+ function preloadsOf(scripts: readonly string[]): ReadonlySet<string> {
935
+ const found = new Set<string>();
936
+ for (const script of scripts) {
937
+ for (const command of script.split(COMMAND_SEPARATORS)) {
938
+ const tokens = scriptTokens(command);
939
+ let at = 0;
940
+ while (
941
+ at < tokens.length &&
942
+ (ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))
943
+ ) {
944
+ at += 1;
945
+ }
946
+ const path = tokens[at] ?? "";
947
+ const program = path.slice(path.lastIndexOf("/") + 1);
948
+ if (!PRELOADING_PROGRAMS.has(program)) {
949
+ continue;
950
+ }
951
+ for (at += 1; at < tokens.length; at += 1) {
952
+ const token = tokens[at] ?? "";
953
+ if (!token.startsWith("-")) {
954
+ if (program === "node") {
955
+ break;
956
+ }
957
+ continue;
958
+ }
959
+ const equals = token.indexOf("=");
960
+ const flag = equals < 0 ? token : token.slice(0, equals);
961
+ if (!PRELOAD_FLAGS.has(flag)) {
962
+ continue;
963
+ }
964
+ if (equals < 0) {
965
+ at += 1;
966
+ }
967
+ const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
968
+ const name = value === undefined ? undefined : packageOfSpecifier(value);
969
+ if (name !== undefined) {
970
+ found.add(name);
971
+ }
972
+ }
973
+ }
974
+ }
975
+ return found;
976
+ }
977
+
874
978
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
875
979
  function isInside(path: string, dir: string): boolean {
876
980
  return dir === "" || path.startsWith(`${dir}/`);
@@ -966,6 +1070,7 @@ export function dependenciesOf(
966
1070
  }
967
1071
  }
968
1072
  const workflow = new Set(workflowTokens(host, targetRoot));
1073
+ const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
969
1074
  const declared: DeclaredDependency[] = [];
970
1075
  const ran = new Set<string>();
971
1076
  const peers = new Map<string, readonly string[]>();
@@ -976,12 +1081,16 @@ export function dependenciesOf(
976
1081
  continue;
977
1082
  }
978
1083
  const tokens = scriptTokensOf(read.value);
1084
+ const preloads = preloadsOf(manifestCommands(host, dir, read.value));
979
1085
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
980
1086
  const found = installedOf(host, dir, dependency.name);
981
1087
  if (found === undefined && uninstalledHere.has(dir)) {
982
1088
  continue;
983
1089
  }
984
1090
  declared.push(dependency);
1091
+ if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
1092
+ ran.add(declarationKey(dependency));
1093
+ }
985
1094
  if (found === undefined) {
986
1095
  continue;
987
1096
  }
package/src/discover.ts CHANGED
@@ -3,7 +3,7 @@ import type { Diagnostic } from "@typescript/native/sync";
3
3
  import { baseUrlPaths } from "./base-url.ts";
4
4
  import type { BuildConfiguration, ProjectConfiguration } from "./config.ts";
5
5
  import type { Host } from "./host.ts";
6
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
6
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
7
7
  import type { Scope } from "./scope.ts";
8
8
  import { SetupError } from "./setup-failure.ts";
9
9
  import type { Engine, ProjectDiagnostics } from "./session.ts";
@@ -347,55 +347,8 @@ function derivedId(targetRoot: string, configFile: string): string {
347
347
  return relativePath(targetRoot, configFile) ?? configFile;
348
348
  }
349
349
 
350
- /**
351
- * The code of a configuration whose `extends` chain returns to a configuration it already
352
- * passed. The parse reports it and returns, and a snapshot opened from it never answers:
353
- * https://github.com/microsoft/TypeScript/issues/64754
354
- */
355
- const CIRCULAR_EXTENDS = 18000;
356
-
357
350
  type Parsed = ReturnType<Engine["parseConfigFile"]>;
358
351
 
359
- /** The configuration files one parsed configuration's own `extends` names by path. */
360
- function extendedPaths(parsed: Parsed, configFile: string): readonly string[] {
361
- const { raw } = parsed as { readonly raw?: unknown };
362
- const named: unknown = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
363
- return (Array.isArray(named) ? named : [named])
364
- .filter((one): one is string => typeof one === "string")
365
- .filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
366
- .map((one) => resolvePath(dirnamePath(configFile), one));
367
- }
368
-
369
- /**
370
- * The `extends` chain from `configFile` to the first configuration it reaches twice, each
371
- * link by path, or undefined where no chain written by path returns.
372
- */
373
- function extendsCycle(
374
- engine: Engine,
375
- configFile: string,
376
- parsed: Parsed,
377
- ): readonly string[] | undefined {
378
- const walk = (chain: readonly string[], at: Parsed): readonly string[] | undefined => {
379
- for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
380
- if (chain.includes(next)) {
381
- return [...chain, next];
382
- }
383
- let read: Parsed;
384
- try {
385
- read = engine.parseConfigFile(next);
386
- } catch {
387
- continue;
388
- }
389
- const cycle = walk([...chain, next], read);
390
- if (cycle !== undefined) {
391
- return cycle;
392
- }
393
- }
394
- return undefined;
395
- };
396
- return walk([configFile], parsed);
397
- }
398
-
399
352
  /** The configuration files one parsed configuration's `references` name. */
400
353
  function referencesOf(parsed: Parsed): readonly string[] {
401
354
  // The parse answers a directory reference as the directory; the compiler reads a
@@ -406,51 +359,17 @@ function referencesOf(parsed: Parsed): readonly string[] {
406
359
  );
407
360
  }
408
361
 
409
- /** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
410
- function circularity(engine: Engine, configFile: string, parsed: Parsed): string | undefined {
411
- const chain = extendsCycle(engine, configFile, parsed);
412
- if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
413
- return undefined;
414
- }
415
- return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
416
- }
417
-
418
362
  /**
419
- * Why a configuration the `references` reach, transitively, cannot be opened, where one's
420
- * `extends` chain is circular: the compiler opens every referenced project with the one
421
- * that references it.
363
+ * The code of a configuration whose `extends` chain returns to a configuration it already
364
+ * passed. A snapshot that opens two configurations whose `extends` chains reach one such
365
+ * cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
366
+ * so the analysis refuses one before any snapshot is asked to open it.
422
367
  */
423
- function referencedCircularity(
424
- engine: Engine,
425
- references: readonly string[],
426
- visited: Set<string>,
427
- ): string | undefined {
428
- for (const reference of references) {
429
- if (visited.has(reference)) {
430
- continue;
431
- }
432
- visited.add(reference);
433
- let parsed: Parsed;
434
- try {
435
- parsed = engine.parseConfigFile(reference);
436
- } catch {
437
- continue;
438
- }
439
- const reason =
440
- circularity(engine, reference, parsed) ??
441
- referencedCircularity(engine, referencesOf(parsed), visited);
442
- if (reason !== undefined) {
443
- return reason;
444
- }
445
- }
446
- return undefined;
447
- }
368
+ const CIRCULAR_EXTENDS = 18000;
448
369
 
449
370
  /**
450
- * The configuration files one configuration's `references` name, or why the compiler
451
- * cannot read the configuration: it throws, or its `extends` chain or that of a
452
- * configuration it references is circular, which the analysis refuses before any
453
- * snapshot is asked to open it.
371
+ * The configurations one configuration file's `references` name, or why it cannot be read:
372
+ * the compiler throws, or its parse reports the `extends` chain circular.
454
373
  */
455
374
  function readConfiguration(
456
375
  engine: Engine,
@@ -458,11 +377,12 @@ function readConfiguration(
458
377
  ): { readonly references: readonly string[] } | { readonly reason: string } {
459
378
  try {
460
379
  const parsed = engine.parseConfigFile(configFile);
461
- const references = referencesOf(parsed);
462
- const reason =
463
- circularity(engine, configFile, parsed) ??
464
- referencedCircularity(engine, references, new Set([configFile]));
465
- return reason === undefined ? { references } : { reason };
380
+ const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
381
+ // The compiler leaves the message's `{0}` unfilled:
382
+ // https://github.com/microsoft/TypeScript/pull/64755
383
+ return circular === undefined
384
+ ? { references: referencesOf(parsed) }
385
+ : { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
466
386
  } catch (error: unknown) {
467
387
  return { reason: error instanceof Error ? error.message : String(error) };
468
388
  }
@@ -14,7 +14,6 @@
14
14
  import {
15
15
  isArrayLiteralExpression,
16
16
  isAsExpression,
17
- isBinaryExpression,
18
17
  isCallExpression,
19
18
  isExportAssignment,
20
19
  isIdentifier,
@@ -27,7 +26,6 @@ import {
27
26
  isPropertyAssignment,
28
27
  isSatisfiesExpression,
29
28
  isStringLiteral,
30
- isTemplateExpression,
31
29
  SyntaxKind,
32
30
  type Node,
33
31
  type SourceFile,
@@ -40,6 +38,7 @@ import {
40
38
  type ParseFile,
41
39
  } from "./configuration-files.ts";
42
40
  import type { Selected } from "./conventions.ts";
41
+ import { buildScriptEntries } from "./build-scripts.ts";
43
42
  import { globExpression } from "./glob.ts";
44
43
  import type { Host } from "./host.ts";
45
44
  import { htmlEntries } from "./html-entries.ts";
@@ -48,6 +47,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
48
47
  import { UNANSWERED } from "./query.ts";
49
48
  import type { ProjectView } from "./session.ts";
50
49
  import type { SourceFiles } from "./source-files.ts";
50
+ import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
51
51
  import { workflowEntries } from "./workflow-steps.ts";
52
52
 
53
53
  /** Which rule entered a file. */
@@ -56,6 +56,8 @@ export type EntryRule =
56
56
  | "configuration-file"
57
57
  /** A file a relative string of a configuration module or a JSON configuration file names. */
58
58
  | "configuration-string"
59
+ /** An entry a build script's bundler call names, whose exports the bundle exposes. */
60
+ | "bundler-entry"
59
61
  /** The test runner: its workspace configuration, its setup files and the test files. */
60
62
  | "test-runner"
61
63
  /** The test files below the browser-test runner's test directory. */
@@ -324,10 +326,7 @@ function configurationEntries(
324
326
  strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
325
327
  }
326
328
  for (const { file } of outside) {
327
- strings(
328
- dirnamePath(file.fileName),
329
- moduleStrings(file).map((one) => one.text),
330
- );
329
+ strings(dirnamePath(file.fileName), unselected(file));
331
330
  }
332
331
  return found;
333
332
  }
@@ -541,18 +540,6 @@ function workersOf<Brand>(
541
540
  return found;
542
541
  }
543
542
 
544
- /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
545
- function leadingLiteral(node: Node): string | undefined {
546
- const at = unwrapped(node);
547
- if (isTemplateExpression(at)) {
548
- return at.head.text;
549
- }
550
- if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
551
- return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
552
- }
553
- return undefined;
554
- }
555
-
556
543
  /** Whether one call is `import.meta.glob(...)`. */
557
544
  function isMetaGlob(node: Node): boolean {
558
545
  if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
@@ -579,13 +566,8 @@ function computedImportsOf(file: SourceFile, files: SourceFiles): readonly Entry
579
566
  const dir = dirnamePath(file.fileName);
580
567
  const found: EntryPoint[] = [];
581
568
  const below = (prefix: string): void => {
582
- const target = resolvePath(dir, prefix);
583
- if (
584
- !isModuleRelative(prefix) ||
585
- !prefix.endsWith("/") ||
586
- target === dir ||
587
- relativePath(target, dir) !== undefined
588
- ) {
569
+ const target = computedLoadDirectory(dir, prefix);
570
+ if (target === undefined) {
589
571
  return;
590
572
  }
591
573
  for (const candidate of files.byName.values()) {
@@ -680,6 +662,9 @@ export function entryPoints<Brand>(
680
662
  ...(names === undefined ? {} : { names }),
681
663
  });
682
664
  }
665
+ for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
666
+ found.push({ file, rule: "bundler-entry", source, exports: true });
667
+ }
683
668
  for (const { file, source } of workflowEntries(host, targetRoot, files)) {
684
669
  found.push({ file, rule: "workflow-step", source, exports: true });
685
670
  }
package/src/exempt.ts CHANGED
@@ -37,6 +37,8 @@ export interface Evidence {
37
37
  readonly whileLive?: string;
38
38
  /** Where the evidence is an implementation of interface methods, which and how. */
39
39
  readonly implementing?: Implementing;
40
+ /** Whether the evidence holds only where a write the mode counts stores into the declaration. */
41
+ readonly whileWritten?: true;
40
42
  }
41
43
 
42
44
  /** What a detector reads of one project, while the project's view is open. */
@@ -193,12 +195,26 @@ export function evidenceRecords<Brand>(
193
195
  site: evidence.site,
194
196
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
195
197
  ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
198
+ ...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
196
199
  });
197
200
  }
198
201
  }
199
202
  return found;
200
203
  }
201
204
 
205
+ /**
206
+ * The records that hold given the declarations a write the mode counts stores into: one
207
+ * that holds only while written holds where its declaration is among them.
208
+ */
209
+ export function writtenRecords(
210
+ records: readonly Exemption[],
211
+ written: ReadonlySet<string>,
212
+ ): readonly Exemption[] {
213
+ return records.flatMap(({ whileWritten, ...record }) =>
214
+ whileWritten === undefined || written.has(record.id) ? [record] : [],
215
+ );
216
+ }
217
+
202
218
  /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
203
219
  export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
204
220
  const found: Exemption[] = [];