@cplieger/deadset-ts 8.0.0-dev.5 → 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 (201) 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 +28 -46
  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 +3 -2
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/contract/contract.generated.js +1084 -0
  12. package/dist/src/contract/report.generated.js +796 -0
  13. package/dist/src/conventions.js +20 -60
  14. package/dist/src/dependencies.js +25 -43
  15. package/dist/src/discover.js +13 -39
  16. package/dist/src/edges.js +7 -15
  17. package/dist/src/element-access.js +4 -6
  18. package/dist/src/entry-point-gaps.js +2 -8
  19. package/dist/src/entry-points.js +3 -10
  20. package/dist/src/enum-group.js +3 -10
  21. package/dist/src/erased-stores.js +4 -6
  22. package/dist/src/exempt-classes.js +3 -63
  23. package/dist/src/exempt.js +13 -40
  24. package/dist/src/file-facts.js +2 -25
  25. package/dist/src/findings/completion.js +11 -20
  26. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  27. package/dist/src/findings/emitters.js +4 -4
  28. package/dist/src/findings/interfaces.js +4 -5
  29. package/dist/src/findings/intra-function.js +8 -7
  30. package/dist/src/findings/non-code-artifacts.js +5 -6
  31. package/dist/src/findings/reads-and-writes.js +11 -9
  32. package/dist/src/findings/self-check.js +10 -12
  33. package/dist/src/findings/unused-declarations.js +28 -27
  34. package/dist/src/findings/visibility-narrowing.js +9 -11
  35. package/dist/src/findings-pass.js +7 -14
  36. package/dist/src/fs/manifest.js +43 -0
  37. package/dist/src/fs/tree.js +88 -0
  38. package/dist/src/git-ignore.js +34 -25
  39. package/dist/src/html-entries.js +6 -33
  40. package/dist/src/in-operands.js +2 -1
  41. package/dist/src/index.js +1 -1
  42. package/dist/src/intra-function-parts.js +18 -26
  43. package/dist/src/inventory.js +2 -21
  44. package/dist/src/json-document.js +4 -0
  45. package/dist/src/keyof-comparisons.js +4 -6
  46. package/dist/src/kinds.js +4 -360
  47. package/dist/src/manifest.js +5 -43
  48. package/dist/src/mdx-entries.js +3 -26
  49. package/dist/src/partial.js +2 -1
  50. package/dist/src/paths.js +26 -15
  51. package/dist/src/position.js +2 -1
  52. package/dist/src/print.js +3 -2
  53. package/dist/src/product.js +13 -0
  54. package/dist/src/references.js +14 -27
  55. package/dist/src/report-schema.js +272 -0
  56. package/dist/src/report.js +39 -42
  57. package/dist/src/reporters/sarif.js +4 -2
  58. package/dist/src/reporters/template.js +4 -19
  59. package/dist/src/resolve.js +12 -26
  60. package/dist/src/roots.js +7 -17
  61. package/dist/src/run.js +10 -8
  62. package/dist/src/schema.js +4 -3
  63. package/dist/src/scope.js +4 -5
  64. package/dist/src/serialization-contract.js +4 -8
  65. package/dist/src/session.js +2 -1
  66. package/dist/src/setup-failure.js +2 -6
  67. package/dist/src/suppress-file.js +12 -13
  68. package/dist/src/suppress-inline.js +3 -2
  69. package/dist/src/suppress.js +29 -48
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +5 -15
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/verbs/analyze.js +22 -15
  82. package/dist/src/verbs/describe.js +7 -7
  83. package/dist/src/verbs/explain.js +7 -4
  84. package/dist/src/verbs/print-config.js +2 -1
  85. package/dist/src/verbs/print-projects.js +4 -2
  86. package/dist/src/verbs/print-retained.js +4 -2
  87. package/dist/src/verbs/print-roots.js +4 -2
  88. package/dist/src/verbs/verb.js +1 -5
  89. package/dist/src/verbs/version.js +4 -3
  90. package/dist/src/workflow-steps.js +9 -46
  91. package/dist/src/workspace-manifests.js +2 -2
  92. package/dist/src/workspace-programs.js +5 -3
  93. package/dist/src/workspace-resolution.js +13 -17
  94. package/dist/src/workspace.js +28 -116
  95. package/jsr.json +1 -1
  96. package/package.json +2 -1
  97. package/src/analysis.ts +29 -54
  98. package/src/automatic-types.ts +11 -29
  99. package/src/build-scripts.ts +17 -38
  100. package/src/component-layer.ts +4 -2
  101. package/src/components.ts +2 -4
  102. package/src/config.ts +11 -15
  103. package/src/configuration-files.ts +27 -170
  104. package/src/conformance-record.ts +1 -1
  105. package/src/contract/contract.generated.ts +1148 -0
  106. package/src/contract/report.generated.ts +1157 -0
  107. package/src/conventions.ts +26 -74
  108. package/src/dependencies.ts +32 -50
  109. package/src/discover.ts +14 -39
  110. package/src/edges.ts +7 -21
  111. package/src/element-access.ts +4 -7
  112. package/src/entry-point-gaps.ts +3 -9
  113. package/src/entry-points.ts +2 -13
  114. package/src/enum-group.ts +2 -11
  115. package/src/erased-stores.ts +3 -7
  116. package/src/exempt-classes.ts +5 -74
  117. package/src/exempt.ts +17 -47
  118. package/src/file-facts.ts +6 -24
  119. package/src/finding.ts +9 -9
  120. package/src/findings/completion.ts +22 -23
  121. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  122. package/src/findings/emitter.ts +6 -7
  123. package/src/findings/emitters.ts +6 -6
  124. package/src/findings/interfaces.ts +4 -6
  125. package/src/findings/intra-function.ts +8 -8
  126. package/src/findings/non-code-artifacts.ts +5 -7
  127. package/src/findings/reads-and-writes.ts +15 -12
  128. package/src/findings/self-check.ts +11 -17
  129. package/src/findings/unused-declarations.ts +28 -27
  130. package/src/findings/visibility-narrowing.ts +9 -12
  131. package/src/findings-pass.ts +7 -16
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/html-entries.ts +6 -33
  136. package/src/in-operands.ts +2 -1
  137. package/src/index.ts +1 -1
  138. package/src/intra-function-parts.ts +18 -28
  139. package/src/inventory.ts +2 -20
  140. package/src/json-document.ts +5 -0
  141. package/src/keyof-comparisons.ts +3 -7
  142. package/src/kinds.ts +6 -406
  143. package/src/manifest.ts +5 -46
  144. package/src/mdx-entries.ts +3 -26
  145. package/src/partial.ts +2 -1
  146. package/src/paths.ts +27 -15
  147. package/src/position.ts +2 -1
  148. package/src/print.ts +3 -2
  149. package/src/product.ts +18 -0
  150. package/src/references.ts +14 -29
  151. package/src/report-schema.ts +364 -0
  152. package/src/report.ts +66 -229
  153. package/src/reporters/annotations.ts +2 -2
  154. package/src/reporters/json.ts +1 -1
  155. package/src/reporters/reporter.ts +2 -2
  156. package/src/reporters/reporters.ts +1 -1
  157. package/src/reporters/sarif.ts +17 -12
  158. package/src/reporters/template.ts +4 -20
  159. package/src/reporters/text.ts +3 -3
  160. package/src/resolve.ts +31 -38
  161. package/src/roots.ts +7 -20
  162. package/src/run.ts +10 -14
  163. package/src/schema.ts +4 -3
  164. package/src/scope.ts +4 -10
  165. package/src/serialization-contract.ts +4 -9
  166. package/src/session.ts +2 -1
  167. package/src/setup-failure.ts +5 -7
  168. package/src/suppress-file.ts +11 -15
  169. package/src/suppress-inline.ts +2 -2
  170. package/src/suppress.ts +39 -55
  171. package/src/symbol-keys.ts +2 -5
  172. package/src/template-field.ts +5 -18
  173. package/src/test-globals.ts +9 -27
  174. package/src/test-joins.ts +6 -8
  175. package/src/text/compare.ts +27 -0
  176. package/src/text/glob.ts +210 -0
  177. package/src/text/shell.ts +161 -0
  178. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  179. package/src/type-errors.ts +25 -36
  180. package/src/unseen-loads.ts +1 -12
  181. package/src/unwrap.ts +34 -0
  182. package/src/verbs/analyze.ts +38 -25
  183. package/src/verbs/describe.ts +13 -7
  184. package/src/verbs/explain.ts +7 -4
  185. package/src/verbs/print-config.ts +2 -1
  186. package/src/verbs/print-projects.ts +4 -2
  187. package/src/verbs/print-retained.ts +4 -2
  188. package/src/verbs/print-roots.ts +4 -2
  189. package/src/verbs/verb.ts +0 -6
  190. package/src/verbs/version.ts +4 -3
  191. package/src/workflow-steps.ts +9 -44
  192. package/src/workspace-manifests.ts +2 -2
  193. package/src/workspace-programs.ts +5 -3
  194. package/src/workspace-resolution.ts +21 -17
  195. package/src/workspace.ts +35 -121
  196. package/dist/src/glob.js +0 -65
  197. package/dist/src/pnpm-workspace.js +0 -120
  198. package/dist/src/version.js +0 -7
  199. package/src/glob.ts +0 -67
  200. package/src/pnpm-workspace.ts +0 -135
  201. package/src/version.ts +0 -7
package/src/suppress.ts CHANGED
@@ -7,6 +7,12 @@
7
7
  * withholds every such finding anyway, and stale otherwise.
8
8
  */
9
9
 
10
+ import {
11
+ type CODE,
12
+ KIND_ROWS,
13
+ SYMBOL_SUBJECTS,
14
+ type Subject,
15
+ } from "./contract/contract.generated.ts";
10
16
  import { ConfigError } from "./config.ts";
11
17
  import type { Finding } from "./finding.ts";
12
18
  import { positionKey, type Position } from "./position.ts";
@@ -15,15 +21,6 @@ import { isRef } from "./ref.ts";
15
21
  /** Which document a suppression is written in, spelled as a finding carries it. */
16
22
  export type Mechanism = "inline" | "ignore" | "baseline";
17
23
 
18
- /** The code a suppression that carries no reason is reported under. */
19
- export const SUPPRESSION_WITHOUT_REASON = "DS1701";
20
-
21
- /** The code an ignore entry or a baseline row that names no path is reported under. */
22
- export const UNSCOPED_ENTRY = "DS1702";
23
-
24
- /** The code a suppression that withholds nothing is reported under. */
25
- export const STALE_SUPPRESSION = "DS1703";
26
-
27
24
  /**
28
25
  * One suppression record: one code bound to one site. A directive naming two codes is two
29
26
  * records with one reason, and a directive above a line that declares two symbols is one
@@ -49,7 +46,7 @@ export interface SuppressionRecord {
49
46
  * nothing, and its finding reports the suppression as written.
50
47
  */
51
48
  export interface Refusal {
52
- readonly reported: typeof SUPPRESSION_WITHOUT_REASON | typeof UNSCOPED_ENTRY;
49
+ readonly reported: typeof CODE.suppressionWithoutReason | typeof CODE.unscopedIgnoreEntry;
53
50
  readonly code: string;
54
51
  /** The reference the entry names, empty for a directive. */
55
52
  readonly symbol: string;
@@ -158,48 +155,48 @@ export interface Ledger {
158
155
  readonly withheld: (finding: Finding) => boolean;
159
156
  }
160
157
 
161
- /**
162
- * The subject kinds that are a dependency row, each with the code that reports one. An
163
- * ignore entry or a baseline row naming the row's reference and file withholds its finding
164
- * and marks nothing, and an inline directive never binds one.
165
- */
166
- const DEPENDENCY_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
167
- ["dependency", ["DS1601"]],
168
- ["module-directive", ["DS1605"]],
169
- ]);
158
+ /** The subject of the kind each live code names. */
159
+ const SUBJECT_OF_CODE: ReadonlyMap<string, Subject> = new Map(
160
+ KIND_ROWS.map((row): [string, Subject] => [row.code, row.subject]),
161
+ );
162
+
163
+ /** The subject a finding about one symbol kind is about. */
164
+ function subjectOf(kind: string): Subject | undefined {
165
+ return Object.hasOwn(SYMBOL_SUBJECTS, kind)
166
+ ? SYMBOL_SUBJECTS[kind as keyof typeof SYMBOL_SUBJECTS]
167
+ : undefined;
168
+ }
169
+
170
+ /** Whether a subject is a row of a document rather than a declaration of the program. */
171
+ function rowOf(subject: Subject | undefined): boolean {
172
+ return subject === "file" || subject === "dependency" || subject === "record";
173
+ }
170
174
 
171
175
  /**
172
- * The subject kinds that are a row of a document rather than a declaration of the program,
173
- * each with the codes that report one. Apart from a dependency row, nothing withholds a
174
- * finding about one.
176
+ * Whether a finding's subject is a row of a document rather than a declaration. Apart
177
+ * from a dependency row, nothing withholds a finding about one.
175
178
  */
176
- const ROW_SUBJECTS: ReadonlyMap<string, readonly string[]> = new Map([
177
- ["file", ["DS1501", "DS1502"]],
178
- ...DEPENDENCY_SUBJECTS,
179
- ["root", ["DS1704"]],
180
- ["configured-declaration", ["DS1706"]],
181
- ["suppression", ["DS1701", "DS1702", "DS1703"]],
182
- ["edge", ["DS1705"]],
183
- ]);
184
-
185
- /** Whether a finding's subject is a row of a document rather than a declaration. */
186
179
  export function isRowSubject(kind: string): boolean {
187
- return ROW_SUBJECTS.has(kind);
180
+ return rowOf(subjectOf(kind));
188
181
  }
189
182
 
190
183
  /** Whether a code reports a row of a document. */
191
184
  export function isRowCode(code: string): boolean {
192
- return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
185
+ return rowOf(SUBJECT_OF_CODE.get(code));
193
186
  }
194
187
 
195
- /** Whether a finding's subject is a dependency row. */
188
+ /**
189
+ * Whether a finding's subject is a dependency row. An ignore entry or a baseline row
190
+ * naming the row's reference and file withholds its finding and marks nothing, and an
191
+ * inline directive never binds one.
192
+ */
196
193
  export function isDependencySubject(kind: string): boolean {
197
- return DEPENDENCY_SUBJECTS.has(kind);
194
+ return subjectOf(kind) === "dependency";
198
195
  }
199
196
 
200
197
  /** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
201
198
  export function isDependencyCode(code: string): boolean {
202
- return [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
199
+ return SUBJECT_OF_CODE.get(code) === "dependency";
203
200
  }
204
201
 
205
202
  /** Whether a code reports a row of a document that no record of any mechanism can name. */
@@ -208,33 +205,20 @@ export function isUnsuppressibleCode(code: string): boolean {
208
205
  }
209
206
 
210
207
  /**
211
- * The subject kinds that are a part of a declaration rather than a declaration: a part
208
+ * Whether a finding's subject is a part of the declaration its reference names: a part
212
209
  * has no reference of its own, so a finding about one names the declaration that holds
213
210
  * it, and the part is decided inside that declaration whatever uses it.
214
211
  */
215
- const PART_SUBJECTS: ReadonlySet<string> = new Set([
216
- "parameter",
217
- "receiver",
218
- "result",
219
- "statement",
220
- "case",
221
- "store",
222
- ]);
223
-
224
- /** Whether a finding's subject is a part of the declaration its reference names. */
225
212
  export function isPartSubject(kind: string): boolean {
226
- return PART_SUBJECTS.has(kind);
213
+ return subjectOf(kind) === "part";
227
214
  }
228
215
 
229
- /** The prefix of the codes of the part kinds, the intra-function range. */
230
- const PART_KIND_PREFIX = "DS18";
231
-
232
216
  /**
233
217
  * Whether a code is a part kind, whose record withholds its finding and marks nothing:
234
218
  * the record says the part is wanted, not that the declaration holding it is.
235
219
  */
236
220
  export function isPartKind(code: string): boolean {
237
- return code.startsWith(PART_KIND_PREFIX);
221
+ return SUBJECT_OF_CODE.get(code) === "part";
238
222
  }
239
223
 
240
224
  /** One finding's identity: its code and the position of the thing it names. */
@@ -254,14 +238,14 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
254
238
  if (record.code !== finding.code) {
255
239
  return false;
256
240
  }
257
- if (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
241
+ if (isDependencySubject(finding.symbol.kind)) {
258
242
  return (
259
243
  record.mechanism !== "inline" &&
260
244
  record.symbol === finding.symbol.ref &&
261
245
  record.path === finding.position.path
262
246
  );
263
247
  }
264
- if (ROW_SUBJECTS.has(finding.symbol.kind)) {
248
+ if (isRowSubject(finding.symbol.kind)) {
265
249
  return false;
266
250
  }
267
251
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
@@ -12,7 +12,6 @@ import {
12
12
  isIdentifier,
13
13
  isNoSubstitutionTemplateLiteral,
14
14
  isNumericLiteral,
15
- isParenthesizedExpression,
16
15
  isPropertyAccessExpression,
17
16
  isSpreadElement,
18
17
  isStringLiteral,
@@ -21,6 +20,7 @@ import {
21
20
  type Node,
22
21
  type SourceFile,
23
22
  } from "@typescript/native/ast";
23
+ import { parenthesesRemoved } from "./unwrap.ts";
24
24
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
25
25
  import type { AliasChains } from "./alias-chain.ts";
26
26
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
@@ -52,10 +52,7 @@ interface KeyedUse {
52
52
 
53
53
  /** The identifier an element access's argument names its key by, where it is a name. */
54
54
  export function keyNameOf(argument: Node): Node | undefined {
55
- let at = argument;
56
- while (isParenthesizedExpression(at)) {
57
- at = at.expression;
58
- }
55
+ const at = parenthesesRemoved(argument);
59
56
  if (isIdentifier(at)) {
60
57
  return at;
61
58
  }
@@ -20,6 +20,8 @@ import type { SymbolKind } from "./inventory.ts";
20
20
  import { memberNames } from "./member-names.ts";
21
21
  import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
+ import { bytewise } from "./text/compare.ts";
24
+ import { filesBelow } from "./fs/tree.ts";
23
25
 
24
26
  /** One template, by its path below the target root. */
25
27
  interface TemplateFile {
@@ -50,14 +52,6 @@ const READ_BY_MARKUP: ReadonlySet<SymbolKind> = new Set([...READ_BY_A_TEMPLATE,
50
52
  /** One identifier, as the grammar of the analyzed language spells one. */
51
53
  const IDENTIFIER = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
52
54
 
53
- /** Two strings ordered bytewise. */
54
- function compare(a: string, b: string): number {
55
- if (a === b) {
56
- return 0;
57
- }
58
- return a < b ? -1 : 1;
59
- }
60
-
61
55
  /**
62
56
  * Every template under the configured directories, read once for the run, in path
63
57
  * order and each file once. A directory is named relative to the target root, and
@@ -69,15 +63,8 @@ function compare(a: string, b: string): number {
69
63
  export function readTemplates(host: Host, targetRoot: string, analysis: Analysis): Templates {
70
64
  const files = new Map<string, string>();
71
65
  const read = (dir: string): void => {
72
- const entries = [...host.readDirectory(joinPath(targetRoot, dir))].sort((a, b) =>
73
- compare(a.name, b.name),
74
- );
75
- for (const entry of entries) {
76
- const path = joinPath(dir, entry.name);
77
- if (entry.directory) {
78
- read(path);
79
- continue;
80
- }
66
+ for (const below of filesBelow(host, joinPath(targetRoot, dir), () => true)) {
67
+ const path = joinPath(dir, below);
81
68
  const absolute = joinPath(targetRoot, path);
82
69
  if (!files.has(path) && host.kindOf(absolute) === "file") {
83
70
  files.set(path, host.readFile(absolute));
@@ -104,7 +91,7 @@ export function readTemplates(host: Host, targetRoot: string, analysis: Analysis
104
91
  delimiters: analysis.templateDelimiters,
105
92
  files: [...files]
106
93
  .map(([path, text]) => ({ path, text }))
107
- .sort((a, b) => compare(a.path, b.path)),
94
+ .sort((a, b) => bytewise(a.path, b.path)),
108
95
  };
109
96
  }
110
97
 
@@ -25,10 +25,12 @@ import {
25
25
  } from "@typescript/native/ast";
26
26
  import type { ParseFile } from "./configuration-files.ts";
27
27
  import type { Host } from "./host.ts";
28
- import { dirnamePath, joinPath } from "./paths.ts";
28
+ import { ancestors, dirnamePath, joinPath } from "./paths.ts";
29
29
  import { UNANSWERED } from "./query.ts";
30
30
  import type { Engine, ProjectView } from "./session.ts";
31
31
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
32
+ import { filesIn } from "./fs/tree.ts";
33
+ import { holdsManifest, INSTALLED_PACKAGES } from "./fs/manifest.ts";
32
34
 
33
35
  /** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
34
36
  export const GLOBALS_RUNNER = "vitest";
@@ -120,16 +122,7 @@ function setsGlobals(file: SourceFile): boolean {
120
122
 
121
123
  /** Whether the runner's configuration in one manifest's directory injects the globals. */
122
124
  function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
123
- let names: readonly string[];
124
- try {
125
- names = host
126
- .readDirectory(dir)
127
- .filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
128
- .map((entry) => entry.name);
129
- } catch {
130
- return false;
131
- }
132
- return names.some((name) => {
125
+ return filesIn(host, dir, (name) => RUNNER_CONFIG.test(name)).some((name) => {
133
126
  const path = joinPath(dir, name);
134
127
  try {
135
128
  return setsGlobals(parse(path, host.readFile(path)));
@@ -141,26 +134,15 @@ function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
141
134
 
142
135
  /** Whether a file declaring the globals is installed where the configuration resolves packages from. */
143
136
  function globalsInstalled(host: Host, configFile: string): boolean {
144
- for (let dir = dirnamePath(configFile); ; dir = dirnamePath(dir)) {
145
- if (host.kindOf(joinPath(dir, "node_modules", GLOBALS_RUNNER, "globals.d.ts")) === "file") {
146
- return true;
147
- }
148
- if (dirnamePath(dir) === dir) {
149
- return false;
150
- }
151
- }
137
+ return ancestors(dirnamePath(configFile)).some(
138
+ (dir) =>
139
+ host.kindOf(joinPath(dir, INSTALLED_PACKAGES, GLOBALS_RUNNER, "globals.d.ts")) === "file",
140
+ );
152
141
  }
153
142
 
154
143
  /** The directory of the manifest nearest one configuration file, at or above its own. */
155
144
  function manifestDir(host: Host, configFile: string): string | undefined {
156
- for (let dir = dirnamePath(configFile); ; dir = dirnamePath(dir)) {
157
- if (host.kindOf(joinPath(dir, "package.json")) === "file") {
158
- return dir;
159
- }
160
- if (dirnamePath(dir) === dir) {
161
- return undefined;
162
- }
163
- }
145
+ return ancestors(dirnamePath(configFile)).find((dir) => holdsManifest(host, dir));
164
146
  }
165
147
 
166
148
  function typesOf(value: unknown): readonly string[] | undefined {
package/src/test-joins.ts CHANGED
@@ -8,9 +8,11 @@
8
8
 
9
9
  import { sourcePaths } from "./file-facts.ts";
10
10
  import type { Host } from "./host.ts";
11
- import { dirnamePath, joinPath, relativePath } from "./paths.ts";
11
+ import { ancestors, dirnamePath, joinPath } from "./paths.ts";
12
12
  import type { Engine } from "./session.ts";
13
13
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
14
+ import { bytewise } from "./text/compare.ts";
15
+ import { holdsManifest } from "./fs/manifest.ts";
14
16
 
15
17
  const PREFERRED = "/tsconfig.json";
16
18
 
@@ -18,7 +20,7 @@ const PREFERRED = "/tsconfig.json";
18
20
  function byPreference(a: string, b: string): number {
19
21
  const first = a.endsWith(PREFERRED) ? 0 : 1;
20
22
  const second = b.endsWith(PREFERRED) ? 0 : 1;
21
- return first - second || (a < b ? -1 : a > b ? 1 : 0);
23
+ return first - second || bytewise(a, b);
22
24
  }
23
25
 
24
26
  /**
@@ -54,17 +56,13 @@ export function joinedTestFiles(
54
56
  if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
55
57
  continue;
56
58
  }
57
- for (let dir = dirnamePath(file); ; dir = dirnamePath(dir)) {
59
+ for (const dir of ancestors(dirnamePath(file), root)) {
58
60
  const [nearest] = byDir.get(dir) ?? [];
59
61
  if (nearest !== undefined) {
60
62
  joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
61
63
  break;
62
64
  }
63
- if (
64
- host.kindOf(joinPath(dir, "package.json")) === "file" ||
65
- dir === root ||
66
- relativePath(root, dir) === undefined
67
- ) {
65
+ if (holdsManifest(host, dir)) {
68
66
  break;
69
67
  }
70
68
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Two strings in the order of their UTF-8 bytes, the order every set of a run is read in.
3
+ * That is code point order, which UTF-16 code unit order (`<` on strings) breaks above
4
+ * U+FFFF: a surrogate unit is below U+E000 while the code point it encodes is above U+FFFF.
5
+ */
6
+ export function bytewise(a: string, b: string): number {
7
+ if (a === b) {
8
+ return 0;
9
+ }
10
+ const length = Math.min(a.length, b.length);
11
+ for (let at = 0; at < length; at += 1) {
12
+ const x = a.charCodeAt(at);
13
+ const y = b.charCodeAt(at);
14
+ if (x !== y) {
15
+ return ranked(x) < ranked(y) ? -1 : 1;
16
+ }
17
+ }
18
+ return a.length < b.length ? -1 : 1;
19
+ }
20
+
21
+ /** One UTF-16 unit moved so that a surrogate ranks above every unit of U+E000 to U+FFFF. */
22
+ function ranked(unit: number): number {
23
+ if (unit >= 0xe000) {
24
+ return unit - 0x800;
25
+ }
26
+ return unit >= 0xd800 ? unit + 0x2000 : unit;
27
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Glob patterns over paths in two forms. The configured form (the configuration, convention
3
+ * rows, bundler entries, workspace member patterns) has `*` and `?` within a segment, `**`
4
+ * across segments and brace lists. A `.gitignore` line has git's form: bracket expressions
5
+ * and backslash escapes, no brace lists (https://git-scm.com/docs/gitignore#_pattern_format).
6
+ * A configured root is not a glob: it names a symbol reference, and is read where it is used.
7
+ */
8
+
9
+ type GlobForm = "configured" | "gitignore";
10
+
11
+ /**
12
+ * One character of a pattern as the expression syntax reads it literally. The set escaped
13
+ * is closed: under the unicode flag an escape of any other character is refused outright.
14
+ */
15
+ function quoted(character: string): string {
16
+ return /[.*+?^${}()|[\]\\/]/u.test(character) ? `\\${character}` : character;
17
+ }
18
+
19
+ /**
20
+ * The ASCII ranges each POSIX class a bracket expression may name holds, as git's own
21
+ * character table reads them, each range written as its first and last character.
22
+ */
23
+ const CLASSES: ReadonlyMap<string, string> = new Map([
24
+ ["alnum", "09AZaz"],
25
+ ["alpha", "AZaz"],
26
+ ["blank", "\t\t "],
27
+ ["cntrl", "\0\x1f\x7f\x7f"],
28
+ ["digit", "09"],
29
+ ["graph", "!~"],
30
+ ["lower", "az"],
31
+ ["print", " ~"],
32
+ ["punct", "!/:@[`{~"],
33
+ ["space", "\t\n\r\r "],
34
+ ["upper", "AZ"],
35
+ ["xdigit", "09AFaf"],
36
+ ]);
37
+
38
+ /** What a pattern git cannot read matches: nothing, as git aborts the whole match. */
39
+ const NOTHING = "(?!)";
40
+
41
+ /** One code point as a member of an expression class. */
42
+ function member(point: number): string {
43
+ return `\\u{${point.toString(16)}}`;
44
+ }
45
+
46
+ /**
47
+ * The class a bracket expression opening at `open` denotes, and the index past its `]`,
48
+ * read as git's wildmatch reads it: a leading `!` or `^` negates it, a `]` first is a
49
+ * member, `\` escapes one character, `x-y` is a range (one whose `y` is below `x` holds
50
+ * `x` alone) and `[:name:]` a POSIX class. An unclosed bracket or an unknown class name
51
+ * matches nothing. Neither form matches the solidus.
52
+ */
53
+ function bracket(pattern: string, open: number): { source: string; end: number } {
54
+ let at = open + 1;
55
+ const negated = pattern[at] === "!" || pattern[at] === "^";
56
+ if (negated) {
57
+ at += 1;
58
+ }
59
+ const next = (): number | undefined => {
60
+ const point = pattern.codePointAt(at);
61
+ at += point !== undefined && point > 0xffff ? 2 : 1;
62
+ return point;
63
+ };
64
+ let members = "";
65
+ let low: number | undefined;
66
+ for (let first = true; ; first = false) {
67
+ let point = next();
68
+ if (point === undefined) {
69
+ return { source: NOTHING, end: pattern.length };
70
+ }
71
+ if (point === 0x5d && !first) {
72
+ return { source: negated ? `[^/${members}]` : `(?!/)[${members}]`, end: at };
73
+ }
74
+ if (point === 0x2d && low !== undefined && at < pattern.length && pattern[at] !== "]") {
75
+ let high = next();
76
+ if (high === 0x5c) {
77
+ high = next();
78
+ }
79
+ if (high === undefined) {
80
+ return { source: NOTHING, end: pattern.length };
81
+ }
82
+ members += high >= low ? `${member(low)}-${member(high)}` : "";
83
+ low = undefined;
84
+ continue;
85
+ }
86
+ if (point === 0x5b && pattern[at] === ":") {
87
+ const close = pattern.indexOf("]", at + 1);
88
+ if (close < 0) {
89
+ return { source: NOTHING, end: pattern.length };
90
+ }
91
+ if (close > at + 1 && pattern[close - 1] === ":") {
92
+ const ranges = CLASSES.get(pattern.slice(at + 1, close - 1));
93
+ if (ranges === undefined) {
94
+ return { source: NOTHING, end: pattern.length };
95
+ }
96
+ for (let one = 0; one < ranges.length; one += 2) {
97
+ members += `${member(ranges.charCodeAt(one))}-${member(ranges.charCodeAt(one + 1))}`;
98
+ }
99
+ low = undefined;
100
+ at = close + 1;
101
+ continue;
102
+ }
103
+ }
104
+ if (point === 0x5c) {
105
+ point = next();
106
+ if (point === undefined) {
107
+ return { source: NOTHING, end: pattern.length };
108
+ }
109
+ }
110
+ members += member(point);
111
+ low = point;
112
+ }
113
+ }
114
+
115
+ /** The expression source of one pattern with no brace list. */
116
+ function source(pattern: string, form: GlobForm): string {
117
+ let text = "";
118
+ let at = 0;
119
+ while (at < pattern.length) {
120
+ if (pattern.startsWith("**/", at)) {
121
+ text += "(?:[^/]*/)*";
122
+ at += 3;
123
+ continue;
124
+ }
125
+ if (pattern.startsWith("**", at)) {
126
+ text += ".*";
127
+ at += 2;
128
+ continue;
129
+ }
130
+ const character = pattern[at] ?? "";
131
+ if (form === "gitignore" && character === "[") {
132
+ const read = bracket(pattern, at);
133
+ text += read.source;
134
+ at = read.end;
135
+ continue;
136
+ }
137
+ if (form === "gitignore" && character === "\\" && at + 1 < pattern.length) {
138
+ text += quoted(pattern[at + 1] ?? "");
139
+ at += 2;
140
+ continue;
141
+ }
142
+ at += 1;
143
+ text += character === "*" ? "[^/]*" : character === "?" ? "[^/]" : quoted(character);
144
+ }
145
+ return text;
146
+ }
147
+
148
+ /** A pattern with every brace list expanded into its alternatives; a lone `{` stands for itself. */
149
+ export function braceExpanded(pattern: string): readonly string[] {
150
+ const open = pattern.indexOf("{");
151
+ const close = open < 0 ? -1 : pattern.indexOf("}", open);
152
+ if (close < 0) {
153
+ return [pattern];
154
+ }
155
+ const before = pattern.slice(0, open);
156
+ const after = pattern.slice(close + 1);
157
+ return pattern
158
+ .slice(open + 1, close)
159
+ .split(",")
160
+ .flatMap((member) => braceExpanded(before + member + after));
161
+ }
162
+
163
+ /** The expression one whole-path pattern of the configured form denotes. */
164
+ export function globExpression(pattern: string): RegExp {
165
+ const alternatives = braceExpanded(pattern).map((one) => source(one, "configured"));
166
+ return new RegExp(`^(?:${alternatives.join("|")})$`, "u");
167
+ }
168
+
169
+ /** The expression one `.gitignore` pattern denotes over a path below its file's directory. */
170
+ export function gitignoreExpression(pattern: string): RegExp {
171
+ return new RegExp(`^${source(pattern, "gitignore")}$`, "u");
172
+ }
173
+
174
+ const SEGMENTS = new Map<string, RegExp>();
175
+
176
+ /**
177
+ * Whether one directory name matches one segment of a workspace's member pattern, its brace
178
+ * lists already expanded. Neither `*` nor `?` matches a leading dot, as npm's workspaces read
179
+ * them: https://docs.npmjs.com/cli/using-npm/workspaces
180
+ */
181
+ export function segmentMatches(segment: string, name: string): boolean {
182
+ if (name.startsWith(".") && !segment.startsWith(".")) {
183
+ return false;
184
+ }
185
+ let expression = SEGMENTS.get(segment);
186
+ if (expression === undefined) {
187
+ expression = globExpression(segment);
188
+ SEGMENTS.set(segment, expression);
189
+ }
190
+ return expression.test(name);
191
+ }
192
+
193
+ /**
194
+ * Whether one path's segments match a member pattern's, `**` standing for any run of segments
195
+ * that starts with no dot.
196
+ */
197
+ export function pathMatches(pattern: readonly string[], path: readonly string[]): boolean {
198
+ const [head, ...rest] = pattern;
199
+ if (head === undefined) {
200
+ return path.length === 0;
201
+ }
202
+ if (head === "**") {
203
+ return (
204
+ pathMatches(rest, path) ||
205
+ (path.length > 0 && !(path[0] ?? "").startsWith(".") && pathMatches(pattern, path.slice(1)))
206
+ );
207
+ }
208
+ const [name, ...below] = path;
209
+ return name !== undefined && segmentMatches(head, name) && pathMatches(rest, below);
210
+ }