@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
@@ -3,8 +3,9 @@
3
3
  * path from the target root. A workflow or hook file is read as text, line by line, with no
4
4
  * YAML parsed.
5
5
  */
6
- import { scriptTokens } from "./manifest.js";
7
6
  import { joinPath, relativePath, resolvePath } from "./paths.js";
7
+ import { filesIn } from "./fs/tree.js";
8
+ import { commandsOf, scriptTokens, unquoted, withoutComment } from "./text/shell.js";
8
9
  const WORKFLOWS = ".github/workflows";
9
10
  /** A line holding a run command: indentation and one sequence marker before `run:`. */
10
11
  const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
@@ -19,26 +20,13 @@ const HOOK_MANAGERS = [
19
20
  ];
20
21
  /** The names GNU make reads a makefile under, in the order it tries them. */
21
22
  const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"];
22
- /** The operators that end one shell command of a recipe line. */
23
- const COMMAND_SEPARATORS = /&&|\|\||;|\|/u;
24
23
  const BLOCK_SCALAR = /^[|>][+-]?$/u;
25
- const QUOTED = /^(["'])(.*)\1$/su;
26
24
  /** A token holding one of these is computed or a pattern, so it names no file. */
27
25
  const COMPUTED = /[${*?[]/u;
28
26
  const INDENT = /^\s*/u;
29
- /** The files directly in one directory whose name `accepts`, in name order. */
30
- function filesIn(host, dir, accepts) {
31
- let entries;
32
- try {
33
- entries = host.readDirectory(dir);
34
- }
35
- catch {
36
- return [];
37
- }
38
- return entries
39
- .filter((entry) => !entry.directory && accepts(entry.name))
40
- .map((entry) => joinPath(dir, entry.name))
41
- .sort();
27
+ /** The files directly in one directory whose name `accepts`, by path in name order. */
28
+ function pathsIn(host, dir, accepts) {
29
+ return filesIn(host, dir, accepts).map((name) => joinPath(dir, name));
42
30
  }
43
31
  function readText(host, path) {
44
32
  try {
@@ -58,7 +46,7 @@ export function runCommands(text, key = RUN_LINE) {
58
46
  continue;
59
47
  }
60
48
  const column = (match[1] ?? "").length;
61
- const value = (match[2] ?? "").trim().replace(QUOTED, "$2");
49
+ const value = unquoted((match[2] ?? "").trim());
62
50
  if (!BLOCK_SCALAR.test(value)) {
63
51
  found.push(value);
64
52
  continue;
@@ -88,7 +76,7 @@ function recipeLines(host, root) {
88
76
  .replace(/\\\r?\n/gu, " ")
89
77
  .split(/\r?\n/u)
90
78
  .filter((line) => line.startsWith("\t"))
91
- .map((line) => withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS));
79
+ .map((line) => commandsOf(withoutComment(line.slice(1).replace(/^[@+-]+/u, ""))));
92
80
  }
93
81
  /**
94
82
  * Every token the recipe lines of the target root's makefile hold, each with the directory
@@ -113,31 +101,6 @@ function recipeTokens(host, root) {
113
101
  }
114
102
  return found;
115
103
  }
116
- /** One shell line up to its comment: a `#` that starts a word outside quotes. */
117
- function withoutComment(line) {
118
- let quote = "";
119
- for (let at = 0; at < line.length; at += 1) {
120
- const char = line[at];
121
- if (quote !== "") {
122
- if (char === quote) {
123
- quote = "";
124
- }
125
- else if (char === "\\" && quote === '"') {
126
- at += 1;
127
- }
128
- }
129
- else if (char === "'" || char === '"') {
130
- quote = char;
131
- }
132
- else if (char === "\\") {
133
- at += 1;
134
- }
135
- else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
136
- return line.slice(0, at);
137
- }
138
- }
139
- return line;
140
- }
141
104
  /**
142
105
  * Every command the target root's workflow steps, git hooks and makefile run: the `run:`
143
106
  * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
@@ -159,11 +122,11 @@ export function workflowTokens(host, targetRoot) {
159
122
  /** The commands of the workflow steps, git hooks and hook managers at one root. */
160
123
  function stepCommands(host, root) {
161
124
  const commands = [];
162
- for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
125
+ for (const path of pathsIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
163
126
  commands.push(...runCommands(readText(host, path) ?? ""));
164
127
  }
165
128
  for (const dir of HOOK_DIRS) {
166
- for (const path of filesIn(host, joinPath(root, dir), () => true)) {
129
+ for (const path of pathsIn(host, joinPath(root, dir), () => true)) {
167
130
  commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
168
131
  }
169
132
  }
@@ -8,6 +8,7 @@
8
8
  import { emittedFrom } from "./emit-map.js";
9
9
  import { readManifest } from "./manifest.js";
10
10
  import { joinPath, relativePath } from "./paths.js";
11
+ import { MANIFEST } from "./fs/manifest.js";
11
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
12
13
  const SUBPATH_WILDCARD = "*";
13
14
  /** The paths one written target stands for in its member's sources, itself included. */
@@ -53,8 +54,7 @@ export function memberEntries(host, resolver, targetRoot) {
53
54
  return [];
54
55
  }
55
56
  const manifest = readManifest(host, member.dir);
56
- const where = relativePath(targetRoot, joinPath(member.dir, "package.json")) ??
57
- joinPath(member.dir, "package.json");
57
+ const where = relativePath(targetRoot, joinPath(member.dir, MANIFEST)) ?? joinPath(member.dir, MANIFEST);
58
58
  const kept = manifest.entries.flatMap((entry) => {
59
59
  const exported = entry.member.startsWith("exports");
60
60
  if (member.private && exported) {
@@ -9,6 +9,8 @@ import { ModuleKind, ModuleResolutionKind } from "@typescript/native/sync";
9
9
  import { isStringLiteralLikeNode } from "@typescript/native/ast";
10
10
  import { baseUrlPaths } from "./base-url.js";
11
11
  import { dirnamePath, resolvePath } from "./paths.js";
12
+ import { bytewise } from "./text/compare.js";
13
+ import { PRODUCT } from "./product.js";
12
14
  /** The most questions one round trip carries. */
13
15
  const BATCH = 2048;
14
16
  /**
@@ -55,7 +57,7 @@ function unstatable(reading) {
55
57
  export function writtenPath(host, configFile) {
56
58
  const stem = configFile.replace(/\.json$/u, "");
57
59
  for (let counter = 0;; counter += 1) {
58
- const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;
60
+ const path = `${stem}.${PRODUCT}${counter === 0 ? "" : `-${String(counter)}`}.json`;
59
61
  if (host.kindOf(path) === "absent") {
60
62
  return path;
61
63
  }
@@ -102,10 +104,10 @@ function configurationOf(engine, host, reading) {
102
104
  ...(exclude === undefined ? {} : { exclude }),
103
105
  });
104
106
  const file = writtenPath(host, reading.configFile);
105
- const expected = [...reading.parsed.fileNames].sort().join("\u0000");
107
+ const expected = [...reading.parsed.fileNames].sort(bytewise).join("\u0000");
106
108
  const listed = (value) => {
107
109
  try {
108
- return [...engine.parseConfigJson(value, file).fileNames].sort().join("\u0000");
110
+ return [...engine.parseConfigJson(value, file).fileNames].sort(bytewise).join("\u0000");
109
111
  }
110
112
  catch {
111
113
  return undefined;
@@ -7,8 +7,10 @@
7
7
  */
8
8
  import { emittedFrom } from "./emit-map.js";
9
9
  import { gitIgnored } from "./git-ignore.js";
10
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
10
+ import { ancestors, dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath, } from "./paths.js";
11
11
  import { satisfies } from "./version-range.js";
12
+ import { isRecord } from "./json-document.js";
13
+ import { INSTALLED_PACKAGES, MANIFEST, manifestAt } from "./fs/manifest.js";
12
14
  /** The manifest members that name an entry point for the subpath `.` without `exports`. */
13
15
  const LEGACY_MEMBERS = ["types", "typings", "module", "main"];
14
16
  /** The dependency members a manifest declares other packages under. */
@@ -20,9 +22,6 @@ const DEPENDENCY_MEMBERS = [
20
22
  ];
21
23
  const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
22
24
  const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
23
- function isRecord(value) {
24
- return typeof value === "object" && value !== null && !Array.isArray(value);
25
- }
26
25
  /** The package name a bare specifier names, or `undefined` for any other specifier. */
27
26
  export function packageNameOf(specifier) {
28
27
  if (specifier === "" ||
@@ -146,7 +145,7 @@ export function workspaceResolver(host, workspace) {
146
145
  if (links.has(key)) {
147
146
  return links.get(key);
148
147
  }
149
- const candidate = joinPath(dir, "node_modules", name);
148
+ const candidate = joinPath(dir, INSTALLED_PACKAGES, name);
150
149
  let found;
151
150
  if (host.kindOf(candidate) === "absent") {
152
151
  const up = dirnamePath(dir);
@@ -171,16 +170,13 @@ export function workspaceResolver(host, workspace) {
171
170
  if (manifests.has(dir)) {
172
171
  return manifests.get(dir);
173
172
  }
174
- let found;
175
- try {
176
- const value = JSON.parse(host.readFile(joinPath(dir, "package.json")));
177
- found = isRecord(value) ? { dir, manifest: value } : undefined;
178
- }
179
- catch {
180
- const up = dirnamePath(dir);
181
- found =
182
- up === dir || root === undefined || !inside(root, up) ? undefined : nearestManifest(up);
183
- }
173
+ const manifest = manifestAt(host, dir);
174
+ const up = dirnamePath(dir);
175
+ const found = manifest !== undefined
176
+ ? { dir, manifest }
177
+ : up === dir || root === undefined || !inside(root, up)
178
+ ? undefined
179
+ : nearestManifest(up);
184
180
  manifests.set(dir, found);
185
181
  return found;
186
182
  };
@@ -191,7 +187,7 @@ export function workspaceResolver(host, workspace) {
191
187
  const member = candidates.find((one) => one.dir === linked);
192
188
  return member === undefined ? { kind: "none" } : { kind: "member", member };
193
189
  }
194
- const scope = workspace.memberOf(joinPath(fromDir, "package.json"));
190
+ const scope = workspace.memberOf(joinPath(fromDir, MANIFEST));
195
191
  if (scope?.name === name) {
196
192
  return { kind: "member", member: scope };
197
193
  }
@@ -221,7 +217,7 @@ export function workspaceResolver(host, workspace) {
221
217
  * directory however its configuration writes it.
222
218
  */
223
219
  const mirrored = (member, path) => {
224
- for (let dir = dirnamePath(path); inside(member.dir, dir); dir = dirnamePath(dir)) {
220
+ for (const dir of ancestors(dirnamePath(path), member.dir).filter((at) => at !== member.dir)) {
225
221
  const found = emittedFrom(path, { outDir: dir, sourceDirs: [member.dir] }).find((candidate) => SOURCE.test(candidate) && !ignored(candidate) && host.kindOf(candidate) === "file");
226
222
  if (found !== undefined) {
227
223
  return found;
@@ -6,42 +6,30 @@
6
6
  * member is the workspace root or a directory the patterns name that holds a named
7
7
  * manifest, never one below `node_modules`.
8
8
  */
9
- import { DiscoveryError } from "./discover.js";
9
+ import { DiscoveryError, isConfigFile } from "./discover.js";
10
10
  import { commonDirectory } from "./emit-map.js";
11
- import { dirnamePath, joinPath, relativePath } from "./paths.js";
12
- import { pnpmPackages } from "./pnpm-workspace.js";
13
- const MANIFEST = "package.json";
11
+ import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.js";
12
+ import { bytewise } from "./text/compare.js";
13
+ import { isRecord } from "./json-document.js";
14
+ import { directoriesIn, filesIn, repositoryOf } from "./fs/tree.js";
15
+ import { holdsManifest, inInstalledPackages, INSTALLED_PACKAGES, MANIFEST, manifestAt, } from "./fs/manifest.js";
16
+ import { braceExpanded, pathMatches, segmentMatches } from "./text/glob.js";
17
+ import { yamlKey } from "./text/yaml.js";
14
18
  const PNPM_WORKSPACE = "pnpm-workspace.yaml";
15
- const CONFIG_FILE = /^tsconfig.*\.json$/u;
16
- function isRecord(value) {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
19
- /** One manifest, decoded, or `undefined` where there is none to read. */
20
- function manifestAt(host, dir) {
21
- let text;
22
- try {
23
- text = host.readFile(joinPath(dir, MANIFEST));
24
- }
25
- catch {
26
- return undefined;
27
- }
28
- try {
29
- const value = JSON.parse(text);
30
- return isRecord(value) ? value : undefined;
31
- }
32
- catch {
33
- return undefined;
34
- }
35
- }
36
19
  /** The patterns one directory declares a workspace with, or `undefined` where it declares none. */
37
20
  function declarationAt(host, dir) {
38
21
  const pnpm = joinPath(dir, PNPM_WORKSPACE);
39
22
  if (host.kindOf(pnpm) === "file") {
40
- const read = pnpmPackages(host.readFile(pnpm));
41
- if (read.kind === "refused") {
42
- throw new DiscoveryError(`${pnpm}: ${read.reason}; the workspace's packages are read from a sequence of strings`, []);
23
+ const read = yamlKey(host.readFile(pnpm), "packages");
24
+ if (read.kind === "absent") {
25
+ return { file: pnpm, patterns: [] };
26
+ }
27
+ const listed = read.kind === "value" ? read.value : undefined;
28
+ if (!Array.isArray(listed) || !listed.every((entry) => typeof entry === "string")) {
29
+ const reason = read.kind === "refused" ? read.reason : "packages is not a sequence of strings";
30
+ throw new DiscoveryError(`${pnpm}: ${reason}; the workspace's packages are read from a sequence of strings`, []);
43
31
  }
44
- return { file: pnpm, patterns: read.kind === "listed" ? read.patterns : [] };
32
+ return { file: pnpm, patterns: listed };
45
33
  }
46
34
  const manifest = manifestAt(host, dir);
47
35
  if (manifest === undefined || !Object.hasOwn(manifest, "workspaces")) {
@@ -62,20 +50,6 @@ function declaresWorkspace(host, dir) {
62
50
  const manifest = manifestAt(host, dir);
63
51
  return manifest !== undefined && Object.hasOwn(manifest, "workspaces");
64
52
  }
65
- /** A pattern with every brace list expanded into its alternatives. */
66
- function braceExpanded(pattern) {
67
- const open = pattern.indexOf("{");
68
- const close = open < 0 ? -1 : pattern.indexOf("}", open);
69
- if (close < 0) {
70
- return [pattern];
71
- }
72
- const before = pattern.slice(0, open);
73
- const after = pattern.slice(close + 1);
74
- return pattern
75
- .slice(open + 1, close)
76
- .split(",")
77
- .flatMap((member) => braceExpanded(before + member + after));
78
- }
79
53
  /**
80
54
  * The segments of one pattern below the workspace root, `.` and empty segments
81
55
  * dropped and `..` taken back, or `undefined` where it leaves the root.
@@ -96,52 +70,6 @@ function segmentsOf(pattern) {
96
70
  }
97
71
  return segments;
98
72
  }
99
- /**
100
- * Whether one directory name matches one pattern segment: `*` stands for any run of
101
- * characters and `?` for one, and neither matches a leading dot.
102
- */
103
- function segmentMatches(segment, name) {
104
- if (name.startsWith(".") && !segment.startsWith(".")) {
105
- return false;
106
- }
107
- const source = Array.from(segment)
108
- .map((character) => {
109
- if (character === "*") {
110
- return ".*";
111
- }
112
- if (character === "?") {
113
- return ".";
114
- }
115
- return character.replaceAll(/[.+^${}()|[\]\\/]/gu, (c) => `\\${c}`);
116
- })
117
- .join("");
118
- return new RegExp(`^${source}$`, "u").test(name);
119
- }
120
- /** Whether one path's segments match one pattern's, `**` standing for any run of segments. */
121
- function pathMatches(pattern, path) {
122
- const [head, ...rest] = pattern;
123
- if (head === undefined) {
124
- return path.length === 0;
125
- }
126
- if (head === "**") {
127
- return (pathMatches(rest, path) ||
128
- (path.length > 0 && !(path[0] ?? "").startsWith(".") && pathMatches(pattern, path.slice(1))));
129
- }
130
- const [name, ...below] = path;
131
- return name !== undefined && segmentMatches(head, name) && pathMatches(rest, below);
132
- }
133
- function subdirectories(host, dir) {
134
- try {
135
- return host
136
- .readDirectory(dir)
137
- .filter((entry) => entry.directory && entry.name !== "node_modules" && entry.name !== ".git")
138
- .map((entry) => entry.name)
139
- .sort();
140
- }
141
- catch {
142
- return [];
143
- }
144
- }
145
73
  /**
146
74
  * Every directory below `dir` one pattern names. `**` does not descend into a
147
75
  * directory that declares a workspace of its own, whose packages are that
@@ -155,7 +83,7 @@ function expand(host, dir, pattern, into) {
155
83
  }
156
84
  if (head === "**") {
157
85
  expand(host, dir, rest, into);
158
- for (const name of subdirectories(host, dir)) {
86
+ for (const name of directoriesIn(host, dir)) {
159
87
  if (name.startsWith(".")) {
160
88
  continue;
161
89
  }
@@ -166,12 +94,12 @@ function expand(host, dir, pattern, into) {
166
94
  }
167
95
  if (!head.includes("*") && !head.includes("?")) {
168
96
  const child = joinPath(dir, head);
169
- if (head !== "node_modules" && host.kindOf(child) === "directory") {
97
+ if (head !== INSTALLED_PACKAGES && host.kindOf(child) === "directory") {
170
98
  expand(host, child, rest, into);
171
99
  }
172
100
  return;
173
101
  }
174
- for (const name of subdirectories(host, dir)) {
102
+ for (const name of directoriesIn(host, dir)) {
175
103
  if (segmentMatches(head, name)) {
176
104
  expand(host, joinPath(dir, name), rest, into);
177
105
  }
@@ -208,10 +136,10 @@ function namesDirectory(root, patterns, dir) {
208
136
  if (below === undefined) {
209
137
  return false;
210
138
  }
211
- const path = below.split("/");
212
- if (path.includes("node_modules")) {
139
+ if (inInstalledPackages(below)) {
213
140
  return false;
214
141
  }
142
+ const path = below.split("/");
215
143
  let named = false;
216
144
  for (const written of patterns) {
217
145
  const negated = written.startsWith("!");
@@ -238,18 +166,7 @@ function layoutOf(configFile, parsed) {
238
166
  return { outDir: parsed.outDir, declarationDir: parsed.declarationDir, sourceDirs };
239
167
  }
240
168
  function configurationsOf(host, dir, parse) {
241
- let names;
242
- try {
243
- names = host
244
- .readDirectory(dir)
245
- .filter((entry) => !entry.directory && CONFIG_FILE.test(entry.name))
246
- .map((entry) => entry.name)
247
- .sort();
248
- }
249
- catch {
250
- return [];
251
- }
252
- return names.flatMap((name) => {
169
+ return filesIn(host, dir, isConfigFile).flatMap((name) => {
253
170
  const configFile = joinPath(dir, name);
254
171
  const parsed = parse(configFile);
255
172
  return parsed === undefined
@@ -267,22 +184,17 @@ function configurationsOf(host, dir, parse) {
267
184
  * highest directory below it, at or above the target root, that holds a manifest.
268
185
  */
269
186
  function nearestDeclaration(host, targetRoot) {
270
- let dir = targetRoot;
271
187
  let highestPackage;
272
- for (;;) {
188
+ for (const dir of ancestors(targetRoot, repositoryOf(host, targetRoot)?.root)) {
273
189
  const declared = declarationAt(host, dir);
274
190
  if (declared !== undefined) {
275
191
  return { root: dir, ...declared, highestPackage };
276
192
  }
277
- if (host.kindOf(joinPath(dir, MANIFEST)) === "file") {
193
+ if (holdsManifest(host, dir)) {
278
194
  highestPackage = dir;
279
195
  }
280
- const up = dirnamePath(dir);
281
- if (up === dir || host.kindOf(joinPath(dir, ".git")) !== "absent") {
282
- return undefined;
283
- }
284
- dir = up;
285
196
  }
197
+ return undefined;
286
198
  }
287
199
  /**
288
200
  * The workspace the target sits in, or `undefined` where none holds it: no directory
@@ -320,7 +232,7 @@ export function findWorkspace(host, targetRoot, parse) {
320
232
  if (members.length < 2) {
321
233
  return undefined;
322
234
  }
323
- members.sort((a, b) => a.dir === declared.root ? -1 : b.dir === declared.root ? 1 : a.dir < b.dir ? -1 : 1);
235
+ members.sort((a, b) => a.dir === declared.root ? -1 : b.dir === declared.root ? 1 : bytewise(a.dir, b.dir));
324
236
  const named = new Map();
325
237
  for (const member of members) {
326
238
  named.set(member.name, [...(named.get(member.name) ?? []), member]);
@@ -332,7 +244,7 @@ export function findWorkspace(host, targetRoot, parse) {
332
244
  return scopes.get(dir);
333
245
  }
334
246
  const up = dirnamePath(dir);
335
- const found = host.kindOf(joinPath(dir, MANIFEST)) === "file" ? dir : up === dir ? undefined : scopeOf(up);
247
+ const found = holdsManifest(host, dir) ? dir : up === dir ? undefined : scopeOf(up);
336
248
  scopes.set(dir, found);
337
249
  return found;
338
250
  };
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.6",
3
+ "version": "8.0.0-dev.7",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.6",
3
+ "version": "8.0.0-dev.7",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -62,7 +62,6 @@ import {
62
62
  } from "./exempt.ts";
63
63
  import type { TSExemptionClass } from "./exempt-classes.ts";
64
64
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.ts";
65
- import { globExpression } from "./glob.ts";
66
65
  import type { Finding } from "./finding.ts";
67
66
  import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
68
67
  import type { EmitterInput } from "./findings/emitter.ts";
@@ -85,6 +84,7 @@ import {
85
84
  type SweepResult,
86
85
  } from "./matrix.ts";
87
86
  import {
87
+ ancestors,
88
88
  dirnamePath,
89
89
  extensionOf,
90
90
  joinPath,
@@ -94,7 +94,7 @@ import {
94
94
  } from "./paths.ts";
95
95
  import type { UnansweredQuestion } from "./query.ts";
96
96
  import { byPosition, isComponentFile, type Position } from "./position.ts";
97
- import { LANGUAGE, type TypeErrorSkip, type UnansweredCount } from "./report.ts";
97
+ import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
98
98
  import { SetupError, setupLine, type SetupFailure } from "./setup-failure.ts";
99
99
  import {
100
100
  pathsOf,
@@ -136,6 +136,10 @@ import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./su
136
136
  import type { Exemption, Mode, SweepInput } from "./sweep.ts";
137
137
  import { readTemplates, templateField } from "./template-field.ts";
138
138
  import { ownErrors, workspaceRun } from "./workspace-run.ts";
139
+ import { bytewise } from "./text/compare.ts";
140
+ import { holdsManifest, inInstalledPackages, MANIFEST, manifestAt } from "./fs/manifest.ts";
141
+ import { globExpression } from "./text/glob.ts";
142
+ import { LANGUAGE } from "./product.ts";
139
143
 
140
144
  /** The setting whose source decides where a finding about a configured root sits. */
141
145
  const ROOTS_SETTING = "roots.patterns";
@@ -192,14 +196,6 @@ function documentOf(
192
196
  return relativePath(resolvePath(host.workingDirectory(), targetRoot), document) ?? "";
193
197
  }
194
198
 
195
- /** Two strings ordered bytewise. */
196
- function compare(a: string, b: string): number {
197
- if (a === b) {
198
- return 0;
199
- }
200
- return a < b ? -1 : 1;
201
- }
202
-
203
199
  /** What every stage after the root set reads of one project. */
204
200
  interface ProjectRead {
205
201
  readonly configuration: string;
@@ -272,8 +268,8 @@ function conventionManifests(
272
268
  nestedDirs: readonly string[],
273
269
  ): readonly ConventionManifest[] {
274
270
  const found = new Map<string, ConventionManifest>();
275
- if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
276
- found.set(targetRoot, { dir: targetRoot, path: "package.json" });
271
+ if (holdsManifest(host, targetRoot)) {
272
+ found.set(targetRoot, { dir: targetRoot, path: MANIFEST });
277
273
  }
278
274
  for (const [dirs, nested] of [
279
275
  [memberDirs, false],
@@ -303,23 +299,18 @@ function nestedPackageDirs(
303
299
  const seen = new Set<string>();
304
300
  const found: string[] = [];
305
301
  for (const file of files) {
306
- for (
307
- let dir = dirnamePath(file);
308
- dir !== targetRoot && !seen.has(dir) && relativePath(targetRoot, dir) !== undefined;
309
- dir = dirnamePath(dir)
310
- ) {
302
+ for (const dir of ancestors(dirnamePath(file), targetRoot)) {
303
+ if (seen.has(dir) || relativePath(targetRoot, dir) === undefined) {
304
+ break;
305
+ }
311
306
  seen.add(dir);
312
307
  const below = relativePath(targetRoot, dir) ?? "";
313
- if (
314
- !below.split("/").includes("node_modules") &&
315
- !known.has(dir) &&
316
- host.kindOf(joinPath(dir, "package.json")) === "file"
317
- ) {
308
+ if (!inInstalledPackages(below) && !known.has(dir) && holdsManifest(host, dir)) {
318
309
  found.push(dir);
319
310
  }
320
311
  }
321
312
  }
322
- return found.sort(compare);
313
+ return found.sort(bytewise);
323
314
  }
324
315
 
325
316
  /**
@@ -723,7 +714,9 @@ export function runRoots(
723
714
 
724
715
  const ordered = [...merged.values()].sort(
725
716
  (a, b) =>
726
- byPosition(a.position, b.position) || compare(a.kind, b.kind) || compare(a.source, b.source),
717
+ byPosition(a.position, b.position) ||
718
+ bytewise(a.kind, b.kind) ||
719
+ bytewise(a.source, b.source),
727
720
  );
728
721
  const unmatched = unmatchedEverywhere(
729
722
  config.rootPatterns,
@@ -872,20 +865,9 @@ function testEvidence(
872
865
  }
873
866
  }
874
867
  const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>();
875
- const manifestAt = (dir: string): Readonly<Record<string, unknown>> | undefined => {
868
+ const manifestIn = (dir: string): Readonly<Record<string, unknown>> | undefined => {
876
869
  if (!manifests.has(dir)) {
877
- let value: unknown;
878
- try {
879
- value = JSON.parse(host.readFile(joinPath(dir, "package.json")));
880
- } catch {
881
- value = undefined;
882
- }
883
- manifests.set(
884
- dir,
885
- typeof value === "object" && value !== null && !Array.isArray(value)
886
- ? (value as Record<string, unknown>)
887
- : undefined,
888
- );
870
+ manifests.set(dir, manifestAt(host, dir));
889
871
  }
890
872
  return manifests.get(dir);
891
873
  };
@@ -895,16 +877,14 @@ function testEvidence(
895
877
  return {
896
878
  imports,
897
879
  devDependency: (path, name) => {
898
- for (let dir = dirnamePath(joinPath(root, path)); ; dir = dirnamePath(dir)) {
899
- const manifest = manifestAt(dir);
880
+ for (const dir of ancestors(dirnamePath(joinPath(root, path)), root)) {
881
+ const manifest = manifestIn(dir);
900
882
  const other = OTHER_SECTIONS.some((one) => declares(manifest?.[one], name));
901
883
  if (declares(manifest?.[DEV_SECTION], name) || other) {
902
884
  return !other;
903
885
  }
904
- if (normalizePath(dir) === normalizePath(root) || dirnamePath(dir) === dir) {
905
- return false;
906
- }
907
886
  }
887
+ return false;
908
888
  },
909
889
  };
910
890
  }
@@ -1056,8 +1036,8 @@ function readRun<Extra>(
1056
1036
  const exempt = detected.records;
1057
1037
  const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
1058
1038
  const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
1059
- const supportFiles = [...support].filter((path) => held.has(path)).sort();
1060
- const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort();
1039
+ const supportFiles = [...support].filter((path) => held.has(path)).sort(bytewise);
1040
+ const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort(bytewise);
1061
1041
  const configured: Configured = {
1062
1042
  configuration: projectRead.configuration,
1063
1043
  symbols: projectRead.held.symbols,
@@ -1285,7 +1265,7 @@ function sweptOf<Extra>(
1285
1265
  unanswered: read.unanswered,
1286
1266
  heldByUnanswered: [...held],
1287
1267
  skipped: read.projects.flatMap((one) => one.skipped),
1288
- componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(compare),
1268
+ componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(bytewise),
1289
1269
  };
1290
1270
  }
1291
1271
 
@@ -1427,12 +1407,7 @@ function judgedMembers(
1427
1407
 
1428
1408
  /** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
1429
1409
  function nearestManifestDir(host: Host, root: string, file: string): string {
1430
- for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
1431
- if (host.kindOf(joinPath(dir, "package.json")) === "file") {
1432
- return dir;
1433
- }
1434
- }
1435
- return root;
1410
+ return ancestors(dirnamePath(file), root).find((dir) => holdsManifest(host, dir)) ?? root;
1436
1411
  }
1437
1412
 
1438
1413
  /**