@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.7

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 (158) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +3 -2
  3. package/dist/src/analysis.js +25 -43
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +2 -1
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/contract/contract.generated.js +13 -0
  11. package/dist/src/conventions.js +20 -60
  12. package/dist/src/dependencies.js +25 -43
  13. package/dist/src/discover.js +13 -39
  14. package/dist/src/edges.js +7 -15
  15. package/dist/src/element-access.js +4 -6
  16. package/dist/src/entry-point-gaps.js +2 -8
  17. package/dist/src/entry-points.js +3 -10
  18. package/dist/src/enum-group.js +3 -10
  19. package/dist/src/erased-stores.js +4 -6
  20. package/dist/src/exempt-classes.js +2 -4
  21. package/dist/src/exempt.js +6 -22
  22. package/dist/src/file-facts.js +2 -25
  23. package/dist/src/findings/reads-and-writes.js +2 -1
  24. package/dist/src/findings/self-check.js +3 -2
  25. package/dist/src/findings-pass.js +6 -11
  26. package/dist/src/fs/manifest.js +43 -0
  27. package/dist/src/fs/tree.js +88 -0
  28. package/dist/src/git-ignore.js +34 -25
  29. package/dist/src/html-entries.js +6 -33
  30. package/dist/src/in-operands.js +2 -1
  31. package/dist/src/intra-function-parts.js +10 -13
  32. package/dist/src/inventory.js +2 -21
  33. package/dist/src/json-document.js +4 -0
  34. package/dist/src/keyof-comparisons.js +4 -6
  35. package/dist/src/manifest.js +5 -43
  36. package/dist/src/mdx-entries.js +3 -26
  37. package/dist/src/partial.js +2 -1
  38. package/dist/src/paths.js +26 -15
  39. package/dist/src/position.js +2 -1
  40. package/dist/src/print.js +3 -2
  41. package/dist/src/product.js +13 -0
  42. package/dist/src/references.js +14 -27
  43. package/dist/src/report-schema.js +15 -16
  44. package/dist/src/report.js +22 -31
  45. package/dist/src/reporters/sarif.js +4 -2
  46. package/dist/src/reporters/template.js +4 -19
  47. package/dist/src/resolve.js +11 -20
  48. package/dist/src/roots.js +7 -17
  49. package/dist/src/run.js +8 -7
  50. package/dist/src/schema.js +4 -3
  51. package/dist/src/scope.js +4 -5
  52. package/dist/src/serialization-contract.js +4 -8
  53. package/dist/src/session.js +2 -1
  54. package/dist/src/suppress-file.js +8 -10
  55. package/dist/src/symbol-keys.js +3 -5
  56. package/dist/src/template-field.js +5 -15
  57. package/dist/src/test-globals.js +6 -28
  58. package/dist/src/test-joins.js +6 -6
  59. package/dist/src/text/compare.js +26 -0
  60. package/dist/src/text/glob.js +194 -0
  61. package/dist/src/text/shell.js +159 -0
  62. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  63. package/dist/src/type-errors.js +14 -38
  64. package/dist/src/unseen-loads.js +2 -9
  65. package/dist/src/unwrap.js +23 -0
  66. package/dist/src/verbs/analyze.js +15 -13
  67. package/dist/src/verbs/describe.js +4 -4
  68. package/dist/src/verbs/explain.js +5 -3
  69. package/dist/src/verbs/print-projects.js +2 -1
  70. package/dist/src/verbs/print-retained.js +2 -1
  71. package/dist/src/verbs/print-roots.js +2 -1
  72. package/dist/src/verbs/version.js +2 -1
  73. package/dist/src/workflow-steps.js +9 -46
  74. package/dist/src/workspace-manifests.js +2 -2
  75. package/dist/src/workspace-programs.js +5 -3
  76. package/dist/src/workspace-resolution.js +13 -17
  77. package/dist/src/workspace.js +28 -116
  78. package/jsr.json +1 -1
  79. package/package.json +1 -1
  80. package/src/analysis.ts +26 -51
  81. package/src/automatic-types.ts +11 -29
  82. package/src/build-scripts.ts +17 -38
  83. package/src/component-layer.ts +4 -2
  84. package/src/components.ts +2 -4
  85. package/src/config.ts +4 -2
  86. package/src/configuration-files.ts +27 -170
  87. package/src/contract/contract.generated.ts +25 -0
  88. package/src/conventions.ts +26 -74
  89. package/src/dependencies.ts +32 -50
  90. package/src/discover.ts +14 -39
  91. package/src/edges.ts +7 -21
  92. package/src/element-access.ts +4 -7
  93. package/src/entry-point-gaps.ts +3 -9
  94. package/src/entry-points.ts +2 -13
  95. package/src/enum-group.ts +2 -11
  96. package/src/erased-stores.ts +3 -7
  97. package/src/exempt-classes.ts +4 -4
  98. package/src/exempt.ts +5 -27
  99. package/src/file-facts.ts +6 -24
  100. package/src/findings/reads-and-writes.ts +2 -1
  101. package/src/findings/self-check.ts +3 -2
  102. package/src/findings-pass.ts +6 -12
  103. package/src/fs/manifest.ts +52 -0
  104. package/src/fs/tree.ts +110 -0
  105. package/src/git-ignore.ts +38 -25
  106. package/src/html-entries.ts +6 -33
  107. package/src/in-operands.ts +2 -1
  108. package/src/intra-function-parts.ts +10 -14
  109. package/src/inventory.ts +2 -20
  110. package/src/json-document.ts +5 -0
  111. package/src/keyof-comparisons.ts +3 -7
  112. package/src/manifest.ts +5 -46
  113. package/src/mdx-entries.ts +3 -26
  114. package/src/partial.ts +2 -1
  115. package/src/paths.ts +27 -15
  116. package/src/position.ts +2 -1
  117. package/src/print.ts +3 -2
  118. package/src/product.ts +18 -0
  119. package/src/references.ts +14 -29
  120. package/src/report-schema.ts +15 -17
  121. package/src/report.ts +24 -34
  122. package/src/reporters/sarif.ts +4 -4
  123. package/src/reporters/template.ts +4 -20
  124. package/src/resolve.ts +20 -27
  125. package/src/roots.ts +7 -20
  126. package/src/run.ts +8 -13
  127. package/src/schema.ts +4 -3
  128. package/src/scope.ts +4 -10
  129. package/src/serialization-contract.ts +4 -9
  130. package/src/session.ts +2 -1
  131. package/src/suppress-file.ts +8 -11
  132. package/src/symbol-keys.ts +2 -5
  133. package/src/template-field.ts +5 -18
  134. package/src/test-globals.ts +9 -27
  135. package/src/test-joins.ts +6 -8
  136. package/src/text/compare.ts +27 -0
  137. package/src/text/glob.ts +210 -0
  138. package/src/text/shell.ts +161 -0
  139. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  140. package/src/type-errors.ts +25 -36
  141. package/src/unseen-loads.ts +1 -12
  142. package/src/unwrap.ts +34 -0
  143. package/src/verbs/analyze.ts +22 -21
  144. package/src/verbs/describe.ts +4 -4
  145. package/src/verbs/explain.ts +5 -3
  146. package/src/verbs/print-projects.ts +2 -1
  147. package/src/verbs/print-retained.ts +2 -1
  148. package/src/verbs/print-roots.ts +2 -1
  149. package/src/verbs/version.ts +2 -1
  150. package/src/workflow-steps.ts +9 -44
  151. package/src/workspace-manifests.ts +2 -2
  152. package/src/workspace-programs.ts +5 -3
  153. package/src/workspace-resolution.ts +21 -17
  154. package/src/workspace.ts +35 -121
  155. package/dist/src/glob.js +0 -65
  156. package/dist/src/pnpm-workspace.js +0 -120
  157. package/src/glob.ts +0 -67
  158. package/src/pnpm-workspace.ts +0 -135
@@ -8,7 +8,6 @@
8
8
 
9
9
  import {
10
10
  isArrayLiteralExpression,
11
- isAsExpression,
12
11
  isBinaryExpression,
13
12
  isCallExpression,
14
13
  isClassDeclaration,
@@ -22,27 +21,20 @@ import {
22
21
  isNamedExports,
23
22
  isNamespaceImport,
24
23
  isNoSubstitutionTemplateLiteral,
25
- isNonNullExpression,
26
24
  isNumericLiteral,
27
25
  isObjectBindingPattern,
28
26
  isObjectLiteralExpression,
29
- isParenthesizedExpression,
30
27
  isPropertyAccessExpression,
31
28
  isPropertyAssignment,
32
- isSatisfiesExpression,
33
29
  isShorthandPropertyAssignment,
34
30
  isSpreadAssignment,
35
31
  isStringLiteral,
36
32
  isVariableStatement,
37
33
  SyntaxKind,
38
- type AsExpression,
39
34
  type Identifier,
40
35
  type Node,
41
36
  type ObjectBindingPattern,
42
- type NonNullExpression,
43
37
  type ObjectLiteralExpression,
44
- type ParenthesizedExpression,
45
- type SatisfiesExpression,
46
38
  type SourceFile,
47
39
  } from "@typescript/native/ast";
48
40
  import { lineOf } from "./component-files.ts";
@@ -60,13 +52,25 @@ import {
60
52
  type TestFileReading,
61
53
  } from "./convention-rows.ts";
62
54
  import { documentStrings } from "./configuration-files.ts";
63
- import { globExpression } from "./glob.ts";
64
55
  import type { Host } from "./host.ts";
65
- import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
56
+ import {
57
+ ancestors,
58
+ dirnamePath,
59
+ joinPath,
60
+ normalizePath,
61
+ relativePath,
62
+ resolvePath,
63
+ } from "./paths.ts";
66
64
  import type { SetupFailure } from "./setup-failure.ts";
67
65
  import type { SourceFiles } from "./source-files.ts";
68
66
  import { satisfies } from "./version-range.ts";
69
- import { yamlDocument } from "./yaml-document.ts";
67
+ import { bytewise } from "./text/compare.ts";
68
+ import { isRecord } from "./json-document.ts";
69
+ import { unwrapped } from "./unwrap.ts";
70
+ import { filesIn } from "./fs/tree.ts";
71
+ import { INSTALLED_PACKAGES, MANIFEST, readJSON } from "./fs/manifest.ts";
72
+ import { globExpression } from "./text/glob.ts";
73
+ import { yamlDocument } from "./text/yaml.ts";
70
74
 
71
75
  /** One manifest the analysis reads. */
72
76
  export interface ConventionManifest {
@@ -171,18 +175,6 @@ const DEPENDENCY_SECTIONS = [
171
175
  "optionalDependencies",
172
176
  ] as const;
173
177
 
174
- function isRecord(value: unknown): value is Record<string, unknown> {
175
- return typeof value === "object" && value !== null && !Array.isArray(value);
176
- }
177
-
178
- function readJSON(host: Host, path: string): unknown {
179
- try {
180
- return JSON.parse(host.readFile(path));
181
- } catch {
182
- return undefined;
183
- }
184
- }
185
-
186
178
  function declares(
187
179
  manifest: unknown,
188
180
  name: string,
@@ -203,38 +195,15 @@ function declares(
203
195
  * none, or `undefined` where no `node_modules` holds it.
204
196
  */
205
197
  function installedVersion(host: Host, dir: string, name: string): string | undefined {
206
- for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
207
- const path = joinPath(at, "node_modules", name, "package.json");
198
+ for (const at of ancestors(dir)) {
199
+ const path = joinPath(at, INSTALLED_PACKAGES, name, MANIFEST);
208
200
  if (host.kindOf(path) === "file") {
209
201
  const manifest = readJSON(host, path);
210
202
  const version = isRecord(manifest) ? manifest["version"] : undefined;
211
203
  return typeof version === "string" ? version : "";
212
204
  }
213
- if (dirnamePath(at) === at) {
214
- return undefined;
215
- }
216
- }
217
- }
218
-
219
- /** Whether one node wraps an expression without changing its value. */
220
- function isWrapper(
221
- node: Node,
222
- ): node is ParenthesizedExpression | AsExpression | SatisfiesExpression | NonNullExpression {
223
- return (
224
- isParenthesizedExpression(node) ||
225
- isAsExpression(node) ||
226
- isSatisfiesExpression(node) ||
227
- isNonNullExpression(node)
228
- );
229
- }
230
-
231
- /** The expression a wrapper that changes no value holds. */
232
- function unwrapped(node: Node): Node {
233
- let at = node;
234
- while (isWrapper(at)) {
235
- at = at.expression;
236
205
  }
237
- return at;
206
+ return undefined;
238
207
  }
239
208
 
240
209
  /** The key one property is written with, where it is written as a name or as literal text. */
@@ -823,16 +792,7 @@ function expand(
823
792
  /** The configuration files in one directory that one glob names, by name. */
824
793
  function configurationFiles(host: Host, dir: string, glob: string): readonly string[] {
825
794
  const expression = globExpression(glob);
826
- let entries;
827
- try {
828
- entries = host.readDirectory(dir);
829
- } catch {
830
- return [];
831
- }
832
- return entries
833
- .filter((entry) => !entry.directory && expression.test(entry.name))
834
- .map((entry) => entry.name)
835
- .sort();
795
+ return filesIn(host, dir, (name) => expression.test(name));
836
796
  }
837
797
 
838
798
  /** What one move resolved to: its directories, or the failure its property met. */
@@ -948,7 +908,7 @@ function shortNameUses(
948
908
  }
949
909
  }
950
910
  }
951
- return [...used].filter((name) => declared.has(name)).sort(compare);
911
+ return [...used].filter((name) => declared.has(name)).sort(bytewise);
952
912
  }
953
913
 
954
914
  /**
@@ -1008,7 +968,7 @@ function prefixedNameUses(
1008
968
  }
1009
969
  }
1010
970
  }
1011
- return [...used].sort(compare);
971
+ return [...used].sort(bytewise);
1012
972
  }
1013
973
 
1014
974
  /** The declared dependencies an applied row's helper settings use. */
@@ -1092,14 +1052,6 @@ function packageKeyUses(
1092
1052
  );
1093
1053
  }
1094
1054
 
1095
- /** Two strings ordered bytewise. */
1096
- function compare(a: string, b: string): number {
1097
- if (a === b) {
1098
- return 0;
1099
- }
1100
- return a < b ? -1 : 1;
1101
- }
1102
-
1103
1055
  /**
1104
1056
  * What the rows decide over the manifests the analysis reads. A row `disabled` names
1105
1057
  * is skipped before anything is read for it.
@@ -1119,7 +1071,7 @@ export function readConventions(
1119
1071
  const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
1120
1072
  const testFiles = new Set<string>();
1121
1073
  for (const manifest of manifests) {
1122
- const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
1074
+ const declared = readJSON(host, joinPath(manifest.dir, MANIFEST));
1123
1075
  const versions = new Map<string, string | undefined>();
1124
1076
  const missing = new Set<string>();
1125
1077
  for (const row of rows) {
@@ -1222,16 +1174,16 @@ export function readConventions(
1222
1174
  const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
1223
1175
  unique.sort(
1224
1176
  (a, b) =>
1225
- compare(a.name, b.name) ||
1226
- compare(a.manifest, b.manifest) ||
1227
- compare(JSON.stringify(a), JSON.stringify(b)),
1177
+ bytewise(a.name, b.name) ||
1178
+ bytewise(a.manifest, b.manifest) ||
1179
+ bytewise(JSON.stringify(a), JSON.stringify(b)),
1228
1180
  );
1229
1181
  return {
1230
1182
  applied: unique,
1231
1183
  globs,
1232
1184
  generated,
1233
1185
  failures,
1234
- uses: [...uses].sort(compare),
1186
+ uses: [...uses].sort(bytewise),
1235
1187
  selected,
1236
1188
  testFiles,
1237
1189
  };
@@ -42,17 +42,25 @@ import {
42
42
  import type { FindingPosition } from "./finding.ts";
43
43
  import type { Host } from "./host.ts";
44
44
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
45
- import { scriptTokens } from "./manifest.ts";
46
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
45
+ import {
46
+ ancestors,
47
+ dirnamePath,
48
+ isAbsolutePath,
49
+ joinPath,
50
+ relativePath,
51
+ resolvePath,
52
+ } from "./paths.ts";
47
53
  import type { DependencySection, Module } from "./ref.ts";
48
54
  import { UNANSWERED } from "./query.ts";
49
55
  import type { Handle, ProjectView } from "./session.ts";
50
56
  import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
51
57
  import { workflowCommands, workflowTokens } from "./workflow-steps.ts";
58
+ import { bytewise } from "./text/compare.ts";
59
+ import { isRecord } from "./json-document.ts";
60
+ import { inInstalledPackages, INSTALLED_PACKAGES, MANIFEST, readDocument } from "./fs/manifest.ts";
61
+ import { commandsOf, scriptTokens } from "./text/shell.ts";
52
62
 
53
- const MANIFEST = "package.json";
54
- const MODULES_DIR = "node_modules";
55
- const MODULES_SEGMENT = `/${MODULES_DIR}/`;
63
+ const MODULES_SEGMENT = `/${INSTALLED_PACKAGES}/`;
56
64
  const TYPES_SCOPE = "@types/";
57
65
 
58
66
  /** A script token naming a JavaScript file by a path no expansion changes. */
@@ -160,25 +168,6 @@ export interface Dependencies {
160
168
  readonly heldBy: ReadonlyMap<string, readonly string[]>;
161
169
  }
162
170
 
163
- function isRecord(value: unknown): value is Record<string, unknown> {
164
- return typeof value === "object" && value !== null && !Array.isArray(value);
165
- }
166
-
167
- /** One JSON document's decoded value, or undefined where it cannot be read or parsed. */
168
- function readJSON(host: Host, path: string): { text: string; value: unknown } | undefined {
169
- let text: string;
170
- try {
171
- text = host.readFile(path);
172
- } catch {
173
- return undefined;
174
- }
175
- try {
176
- return { text, value: JSON.parse(text) };
177
- } catch {
178
- return undefined;
179
- }
180
- }
181
-
182
171
  /**
183
172
  * The package a bare module specifier names: its first segment, or its first two
184
173
  * where it starts with a scope. A relative, absolute or scheme-prefixed specifier and
@@ -429,7 +418,7 @@ export function projectNeeds<Brand>(
429
418
  }
430
419
  for (const { strings, manifestDirs } of configuration.documents) {
431
420
  for (const manifestDir of manifestDirs) {
432
- const names = declaredAt(joinPath(manifestDir, "package.json"));
421
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
433
422
  for (const text of strings) {
434
423
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
435
424
  }
@@ -444,7 +433,7 @@ export function projectNeeds<Brand>(
444
433
  );
445
434
  for (const { file, manifestDirs } of [...configuration.outside, ...ranFiles]) {
446
435
  for (const manifestDir of manifestDirs) {
447
- const names = declaredAt(joinPath(manifestDir, "package.json"));
436
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
448
437
  for (const { text } of moduleStrings(file)) {
449
438
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
450
439
  }
@@ -467,8 +456,8 @@ function scriptRunFiles(
467
456
  const root = resolvePath(host.workingDirectory(), targetRoot);
468
457
  const dirs = new Set<string>([root]);
469
458
  for (const name of files.byName.keys()) {
470
- for (let dir = dirnamePath(name); !dirs.has(dir); dir = dirnamePath(dir)) {
471
- if (relativePath(root, dir) === undefined) {
459
+ for (const dir of ancestors(dirnamePath(name), root)) {
460
+ if (dirs.has(dir)) {
472
461
  break;
473
462
  }
474
463
  dirs.add(dir);
@@ -478,7 +467,7 @@ function scriptRunFiles(
478
467
  [root, new Set(workflowTokens(host, root))],
479
468
  ]);
480
469
  for (const dir of dirs) {
481
- const read = readJSON(host, joinPath(dir, MANIFEST));
470
+ const read = readDocument(host, joinPath(dir, MANIFEST));
482
471
  if (read !== undefined && isRecord(read.value)) {
483
472
  const own = scriptTokensOf(read.value);
484
473
  tokens.set(dir, dir === root ? new Set([...own, ...(tokens.get(root) ?? [])]) : own);
@@ -491,7 +480,8 @@ function scriptRunFiles(
491
480
  const below = path === undefined ? undefined : relativePath(root, path);
492
481
  if (
493
482
  path !== undefined &&
494
- below?.split("/").includes(MODULES_DIR) === false &&
483
+ below !== undefined &&
484
+ !inInstalledPackages(below) &&
495
485
  !files.byName.has(path) &&
496
486
  host.kindOf(path) === "file"
497
487
  ) {
@@ -797,8 +787,8 @@ function lineAndColumn(text: string, offset: number): { line: number; column: nu
797
787
 
798
788
  /** The installed copy of one dependency, read where the package manager puts it for the target. */
799
789
  function installedOf(host: Host, root: string, name: string): InstalledDependency | undefined {
800
- for (let dir = root; ; dir = dirnamePath(dir)) {
801
- const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
790
+ for (const dir of ancestors(root)) {
791
+ const read = readDocument(host, joinPath(dir, INSTALLED_PACKAGES, name, MANIFEST));
802
792
  if (read !== undefined && isRecord(read.value)) {
803
793
  const bin = read.value["bin"];
804
794
  const peers = read.value["peerDependencies"];
@@ -815,10 +805,8 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
815
805
  : [],
816
806
  };
817
807
  }
818
- if (dirnamePath(dir) === dir) {
819
- return undefined;
820
- }
821
808
  }
809
+ return undefined;
822
810
  }
823
811
 
824
812
  /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
@@ -904,9 +892,6 @@ const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
904
892
  const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
905
893
  const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
906
894
 
907
- /** The commands one script string runs, split at the shell's list and pipeline operators. */
908
- const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
909
-
910
895
  /**
911
896
  * The command strings one manifest runs: each of its `scripts`, and the `exec` command of
912
897
  * nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
@@ -919,7 +904,10 @@ function manifestCommands(
919
904
  manifest: Record<string, unknown>,
920
905
  ): readonly string[] {
921
906
  const scripts = manifest["scripts"];
922
- const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
907
+ const nodemon = [
908
+ manifest["nodemonConfig"],
909
+ readDocument(host, joinPath(dir, "nodemon.json"))?.value,
910
+ ];
923
911
  return [
924
912
  ...(isRecord(scripts) ? Object.values(scripts) : []),
925
913
  ...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
@@ -934,7 +922,7 @@ function manifestCommands(
934
922
  function preloadsOf(scripts: readonly string[]): ReadonlySet<string> {
935
923
  const found = new Set<string>();
936
924
  for (const script of scripts) {
937
- for (const command of script.split(COMMAND_SEPARATORS)) {
925
+ for (const command of commandsOf(script)) {
938
926
  const tokens = scriptTokens(command);
939
927
  let at = 0;
940
928
  while (
@@ -1076,7 +1064,7 @@ export function dependenciesOf(
1076
1064
  const peers = new Map<string, readonly string[]>();
1077
1065
  const uninstalledHere = new Set(nested);
1078
1066
  for (const [dir, path] of manifests) {
1079
- const read = readJSON(host, joinPath(dir, MANIFEST));
1067
+ const read = readDocument(host, joinPath(dir, MANIFEST));
1080
1068
  if (read === undefined || !isRecord(read.value)) {
1081
1069
  continue;
1082
1070
  }
@@ -1154,10 +1142,7 @@ export function dependenciesOf(
1154
1142
  const heldBy = new Map<string, readonly string[]>();
1155
1143
  for (const [key, held] of holders) {
1156
1144
  if (!live.has(key) && !ran.has(key)) {
1157
- heldBy.set(
1158
- key,
1159
- [...held].sort((a, b) => (a < b ? -1 : 1)),
1160
- );
1145
+ heldBy.set(key, [...held].sort(bytewise));
1161
1146
  }
1162
1147
  }
1163
1148
  const users = new Map<string, number>();
@@ -1169,10 +1154,7 @@ export function dependenciesOf(
1169
1154
  for (const id of candidates) {
1170
1155
  const sole = [...(uses.get(id) ?? [])].filter((one) => users.get(one) === 1 && names.has(one));
1171
1156
  if (sole.length > 0) {
1172
- lastUses.set(
1173
- id,
1174
- sole.sort((a, b) => (a < b ? -1 : 1)),
1175
- );
1157
+ lastUses.set(id, sole.sort(bytewise));
1176
1158
  }
1177
1159
  }
1178
1160
  const owners = new Map<string, Set<string>>();
@@ -1184,7 +1166,7 @@ export function dependenciesOf(
1184
1166
  }
1185
1167
  }
1186
1168
  const fileLastUses = new Map<string, string[]>();
1187
- for (const [one, held] of [...owners].sort(([a], [b]) => (a < b ? -1 : 1))) {
1169
+ for (const [one, held] of [...owners].sort(([a], [b]) => bytewise(a, b))) {
1188
1170
  const paths = new Set([...held].map((owner) => owner.replace(/:\d+:\d+$/u, "")));
1189
1171
  const [path] = paths;
1190
1172
  if (paths.size === 1 && path !== undefined && path !== PROJECT_OWNER) {
package/src/discover.ts CHANGED
@@ -8,12 +8,17 @@ import type { Scope } from "./scope.ts";
8
8
  import { SetupError } from "./setup-failure.ts";
9
9
  import type { Engine, ProjectDiagnostics } from "./session.ts";
10
10
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
11
+ import { filesBelow } from "./fs/tree.ts";
12
+ import { LANGUAGE, message } from "./product.ts";
13
+ import { LANGUAGE_ROWS } from "./contract/contract.generated.ts";
14
+ import { globExpression } from "./text/glob.ts";
11
15
 
12
- /** Directory names discovery does not descend into. */
13
- export const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set(["node_modules", ".git"]);
16
+ const MARKERS = LANGUAGE_ROWS[LANGUAGE].markers.map(globExpression);
14
17
 
15
- /** A compiler configuration file discovery recognises under a root. */
16
- const CONFIG_FILE = /^tsconfig.*\.json$/u;
18
+ /** Whether one file name is a compiler configuration's: one of the language row's markers. */
19
+ export function isConfigFile(name: string): boolean {
20
+ return MARKERS.some((marker) => marker.test(name));
21
+ }
17
22
 
18
23
  /**
19
24
  * A run that cannot read what it was asked to analyze. It ends the run with the
@@ -218,39 +223,9 @@ interface Discovery {
218
223
  readonly fromScope: readonly string[];
219
224
  }
220
225
 
221
- /**
222
- * The configuration files below `root`, in ascending path order, skipping an
223
- * ignored directory. A directory that cannot be read is skipped: a tree the
224
- * analysis may not enter names no project.
225
- */
226
- function configFilesUnder(host: Host, root: string): string[] {
227
- const found: string[] = [];
228
- const pending = [root];
229
- while (pending.length > 0) {
230
- const dir = pending.pop();
231
- if (dir === undefined) {
232
- break;
233
- }
234
- let entries: readonly { readonly name: string; readonly directory: boolean }[];
235
- try {
236
- entries = host.readDirectory(dir);
237
- } catch {
238
- continue;
239
- }
240
- for (const entry of [...entries].sort((a, b) => (a.name < b.name ? -1 : 1))) {
241
- const path = joinPath(dir, entry.name);
242
- if (entry.directory) {
243
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
244
- pending.push(path);
245
- }
246
- continue;
247
- }
248
- if (CONFIG_FILE.test(entry.name)) {
249
- found.push(path);
250
- }
251
- }
252
- }
253
- return found.sort();
226
+ /** The configuration files below `root`, by absolute path in ascending order. */
227
+ function configFilesUnder(host: Host, root: string): readonly string[] {
228
+ return filesBelow(host, root, isConfigFile).map((path) => joinPath(root, path));
254
229
  }
255
230
 
256
231
  /**
@@ -329,7 +304,7 @@ export function consumerProjects(engine: Engine, host: Host, root: string): read
329
304
  if (kind !== "directory") {
330
305
  throw new DiscoveryError(`the consumer ${root} is not a directory`, []);
331
306
  }
332
- const found = derivedFrom(engine, host, configFilesUnder(host, root), () => true);
307
+ const found = derivedFrom(engine, host, [...configFilesUnder(host, root)], () => true);
333
308
  if (found.length === 0) {
334
309
  throw new DiscoveryError(
335
310
  `the consumer ${root} holds no compiler configuration, so none of its references can be read`,
@@ -384,7 +359,7 @@ function readConfiguration(
384
359
  ? { references: referencesOf(parsed) }
385
360
  : { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
386
361
  } catch (error: unknown) {
387
- return { reason: error instanceof Error ? error.message : String(error) };
362
+ return { reason: message(error) };
388
363
  }
389
364
  }
390
365
 
package/src/edges.ts CHANGED
@@ -10,10 +10,12 @@ import type { Finding } from "./finding.ts";
10
10
  import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
11
11
  import type { Host } from "./host.ts";
12
12
  import type { InventorySymbol } from "./inventory.ts";
13
- import { positionAt, walkDocument } from "./json-document.ts";
13
+ import { isRecord, positionAt, walkDocument } from "./json-document.ts";
14
14
  import { joinPath } from "./paths.ts";
15
15
  import { positionKey } from "./position.ts";
16
16
  import { isPartSubject } from "./suppress.ts";
17
+ import { bytewise } from "./text/compare.ts";
18
+ import { message } from "./product.ts";
17
19
 
18
20
  /** The name and the location the Contract fixes for the edges document. */
19
21
  export const EDGES_FILE = "deadset-edges.json";
@@ -74,16 +76,12 @@ function carriesWildcard(ref: string): boolean {
74
76
  return false;
75
77
  }
76
78
 
77
- function isObject(value: unknown): value is Record<string, unknown> {
78
- return typeof value === "object" && value !== null && !Array.isArray(value);
79
- }
80
-
81
79
  /** One edge, checked: the closed key list, the value types and the value forms. */
82
80
  function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
83
81
  const refuse = (text: string, want: string): never => {
84
82
  throw new EdgesError(at, text, want);
85
83
  };
86
- if (!isObject(value)) {
84
+ if (!isRecord(value)) {
87
85
  return refuse(JSON.stringify(value), EDGE_WANT);
88
86
  }
89
87
  for (const [key, held] of Object.entries(value)) {
@@ -122,13 +120,9 @@ export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide
122
120
  try {
123
121
  parsed = JSON.parse(text);
124
122
  } catch (error: unknown) {
125
- throw new EdgesError(
126
- EDGES_FILE,
127
- error instanceof Error ? error.message : String(error),
128
- DOCUMENT_WANT,
129
- );
123
+ throw new EdgesError(EDGES_FILE, message(error), DOCUMENT_WANT);
130
124
  }
131
- if (!isObject(parsed)) {
125
+ if (!isRecord(parsed)) {
132
126
  throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
133
127
  }
134
128
  const walk = walkDocument(text, "edges");
@@ -190,14 +184,6 @@ interface Evaluated<F extends Finding> {
190
184
  readonly evaluations: readonly EdgeEvaluation<F>[];
191
185
  }
192
186
 
193
- /** Two strings ordered bytewise. */
194
- function compare(a: string, b: string): number {
195
- if (a === b) {
196
- return 0;
197
- }
198
- return a < b ? -1 : 1;
199
- }
200
-
201
187
  /**
202
188
  * Evaluates every declared side of this language over the run's findings. A side naming a
203
189
  * symbol the run enumerates nothing under is `absent`; otherwise it is evaluated once per
@@ -242,6 +228,6 @@ export function evaluateEdges<F extends Finding>(
242
228
  return { ...side, state: "dead", finding };
243
229
  });
244
230
  });
245
- evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
231
+ evaluations.sort((a, b) => bytewise(a.edge, b.edge) || bytewise(a.side, b.side));
246
232
  return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
247
233
  }
@@ -47,6 +47,7 @@ import { renderPosition } from "./position.ts";
47
47
  import { UNANSWERED } from "./query.ts";
48
48
  import type { Reference } from "./references.ts";
49
49
  import type { ProjectView } from "./session.ts";
50
+ import { parenthesesRemoved } from "./unwrap.ts";
50
51
 
51
52
  type Use = "read" | "write" | "both";
52
53
 
@@ -93,13 +94,9 @@ function useOf(node: Node): Use {
93
94
  return "read";
94
95
  }
95
96
 
96
- function unwrapped(node: Expression): Expression {
97
- return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
98
- }
99
-
100
97
  /** Whether an index is a literal the checker resolves to a member by its own text. */
101
98
  function isLiteralIndex(node: Expression): boolean {
102
- const at = unwrapped(node);
99
+ const at = parenthesesRemoved(node);
103
100
  return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
104
101
  }
105
102
 
@@ -293,7 +290,7 @@ export function elementAccessReferences<Brand>(
293
290
  const parameterOf = new Map<Candidate, { callable: Callable; at: number }>();
294
291
  const byCallable = new Map<Identifier, Candidate[]>();
295
292
  for (const candidate of candidates) {
296
- const index = unwrapped(candidate.access.argumentExpression);
293
+ const index = parenthesesRemoved(candidate.access.argumentExpression);
297
294
  const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
298
295
  const callable = fn === undefined ? undefined : callableOf(fn);
299
296
  if (callable === undefined || !isIdentifier(index)) {
@@ -353,7 +350,7 @@ export function elementAccessReferences<Brand>(
353
350
  continue;
354
351
  }
355
352
  // An object that is a parameter too is read as the argument each call passes there.
356
- const object = unwrapped(candidate.access.expression);
353
+ const object = parenthesesRemoved(candidate.access.expression);
357
354
  const objectAt = isIdentifier(object)
358
355
  ? deferred.callable.parameters.findIndex(
359
356
  (parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text,
@@ -8,6 +8,8 @@
8
8
  * one at a time: beside a finding, or in a report's declared gaps.
9
9
  */
10
10
 
11
+ import { bytewise } from "./text/compare.ts";
12
+
11
13
  /** One declined entry-point convention. */
12
14
  export interface DeclinedConvention {
13
15
  /** The tool the convention belongs to, by the name of the package or service. */
@@ -323,16 +325,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
323
325
  },
324
326
  ];
325
327
 
326
- /** Two strings ordered bytewise. */
327
- function compare(a: string, b: string): number {
328
- if (a === b) {
329
- return 0;
330
- }
331
- return a < b ? -1 : 1;
332
- }
333
-
334
328
  /** Every declined entry-point convention, ordered by tool and then by convention. */
335
329
  export const DECLINED_CONVENTIONS: readonly DeclinedConvention[] = [
336
330
  ...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
337
331
  ...PARTLY_ENTERED,
338
- ].sort((a, b) => compare(a.tool, b.tool) || compare(a.convention, b.convention));
332
+ ].sort((a, b) => bytewise(a.tool, b.tool) || bytewise(a.convention, b.convention));
@@ -13,7 +13,6 @@
13
13
 
14
14
  import {
15
15
  isArrayLiteralExpression,
16
- isAsExpression,
17
16
  isCallExpression,
18
17
  isExportAssignment,
19
18
  isIdentifier,
@@ -21,10 +20,8 @@ import {
21
20
  isNewExpression,
22
21
  isNoSubstitutionTemplateLiteral,
23
22
  isObjectLiteralExpression,
24
- isParenthesizedExpression,
25
23
  isPropertyAccessExpression,
26
24
  isPropertyAssignment,
27
- isSatisfiesExpression,
28
25
  isStringLiteral,
29
26
  SyntaxKind,
30
27
  type Node,
@@ -39,7 +36,6 @@ import {
39
36
  } from "./configuration-files.ts";
40
37
  import type { Selected } from "./conventions.ts";
41
38
  import { buildScriptEntries } from "./build-scripts.ts";
42
- import { globExpression } from "./glob.ts";
43
39
  import type { Host } from "./host.ts";
44
40
  import { htmlEntries } from "./html-entries.ts";
45
41
  import { documentEntries } from "./mdx-entries.ts";
@@ -49,6 +45,8 @@ import type { ProjectView } from "./session.ts";
49
45
  import type { SourceFiles } from "./source-files.ts";
50
46
  import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
51
47
  import { workflowEntries } from "./workflow-steps.ts";
48
+ import { unwrapped } from "./unwrap.ts";
49
+ import { globExpression } from "./text/glob.ts";
52
50
 
53
51
  /** Which rule entered a file. */
54
52
  export type EntryRule =
@@ -166,15 +164,6 @@ function literalOf(node: Node | undefined): string | undefined {
166
164
  return undefined;
167
165
  }
168
166
 
169
- /** The expression a wrapper that changes no value holds: parentheses and type assertions. */
170
- function unwrapped(node: Node): Node {
171
- let at = node;
172
- while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
173
- at = at.expression;
174
- }
175
- return at;
176
- }
177
-
178
167
  /**
179
168
  * The object literals one configuration file's default export is written as: the
180
169
  * literal itself, or each literal argument of the call it is, at any depth of calls.