@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
@@ -53,6 +53,7 @@ import {
53
53
  type SourceFile,
54
54
  type SpreadAssignment,
55
55
  } from "@typescript/native/ast";
56
+ import { parenthesesRemoved } from "./unwrap.ts";
56
57
  import {
57
58
  SignatureKind,
58
59
  SymbolFlags,
@@ -227,12 +228,6 @@ interface MethodForm {
227
228
  readonly invocation: Invocation;
228
229
  }
229
230
 
230
- function unparenthesized(expression: Expression): Expression {
231
- return isParenthesizedExpression(expression)
232
- ? unparenthesized(expression.expression)
233
- : expression;
234
- }
235
-
236
231
  /**
237
232
  * The invocation a call writes through a method of `Function.prototype`, read from
238
233
  * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
@@ -241,12 +236,12 @@ function unparenthesized(expression: Expression): Expression {
241
236
  * carries. Whether the method is the prototype's is resolution's to say.
242
237
  */
243
238
  function throughMethod(call: CallExpression): MethodForm | undefined {
244
- const expression = unparenthesized(call.expression);
239
+ const expression = parenthesesRemoved(call.expression);
245
240
  if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
246
241
  const [, ...rest] = call.arguments;
247
242
  let passed: readonly Expression[] = rest;
248
243
  if (expression.name.text === "apply") {
249
- const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
244
+ const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
250
245
  passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
251
246
  }
252
247
  return {
@@ -255,7 +250,7 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
255
250
  };
256
251
  }
257
252
  if (isCallExpression(expression)) {
258
- const bound = unparenthesized(expression.expression);
253
+ const bound = parenthesesRemoved(expression.expression);
259
254
  if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
260
255
  return {
261
256
  method: bound.name,
package/src/session.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  type Queries,
27
27
  type UnansweredQuestion,
28
28
  } from "./query.ts";
29
+ import { MANIFEST } from "./fs/manifest.ts";
29
30
 
30
31
  /**
31
32
  * The compiler the analysis reads through: a client over a protocol to a separate
@@ -115,7 +116,7 @@ export function openEngine(options: EngineOptions): Engine {
115
116
  if (components !== undefined && components.extensions.length > 0) {
116
117
  const made = components.host.temporaryDirectory();
117
118
  try {
118
- components.host.writeDocument(joinPath(made.path, "package.json"), [
119
+ components.host.writeDocument(joinPath(made.path, MANIFEST), [
119
120
  mapperManifest(components.host.componentMapperCommand()),
120
121
  ]);
121
122
  } catch (error: unknown) {
@@ -10,7 +10,7 @@ import type { Host } from "./host.ts";
10
10
  import type { InventorySymbol } from "./inventory.ts";
11
11
  import { jsonChunks } from "./json-chunks.ts";
12
12
  import { joinPath } from "./paths.ts";
13
- import { positionAt, walkDocument } from "./json-document.ts";
13
+ import { isRecord, positionAt, walkDocument } from "./json-document.ts";
14
14
  import type { Position } from "./position.ts";
15
15
  import { declarationKey } from "./ref.ts";
16
16
  import {
@@ -26,6 +26,7 @@ import {
26
26
  type SuppressionRecord,
27
27
  type Suppressions,
28
28
  } from "./suppress.ts";
29
+ import { message } from "./product.ts";
29
30
 
30
31
  /** The name and the location the grammar fixes for the ignore file. */
31
32
  export const IGNORE_FILE = "deadset-ignore.json";
@@ -79,10 +80,6 @@ function documentSite(shape: Shape): Position {
79
80
  return { path: shape.file, line: 0, column: 0 };
80
81
  }
81
82
 
82
- function isObject(value: unknown): value is Record<string, unknown> {
83
- return typeof value === "object" && value !== null && !Array.isArray(value);
84
- }
85
-
86
83
  /** What one entry or row resolves to. */
87
84
  interface Read {
88
85
  readonly records: readonly SuppressionRecord[];
@@ -100,7 +97,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
100
97
  const refuse = (text: string, want: string): never => {
101
98
  throw new SuppressionError(shape.mechanism, siteText(site), text, want);
102
99
  };
103
- if (!isObject(value)) {
100
+ if (!isRecord(value)) {
104
101
  return refuse(JSON.stringify(value), shape.recordWant);
105
102
  }
106
103
  for (const [key, held] of Object.entries(value)) {
@@ -200,9 +197,9 @@ function readDocument(
200
197
  try {
201
198
  parsed = JSON.parse(text);
202
199
  } catch (error: unknown) {
203
- return malformed(`${shape.file}: ${error instanceof Error ? error.message : String(error)}`);
200
+ return malformed(`${shape.file}: ${message(error)}`);
204
201
  }
205
- if (!isObject(parsed)) {
202
+ if (!isRecord(parsed)) {
206
203
  return malformed(shape.file);
207
204
  }
208
205
  const walk = walkDocument(text, shape.array);
@@ -328,9 +325,9 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
328
325
  try {
329
326
  parsed = JSON.parse(text);
330
327
  } catch (error: unknown) {
331
- return refuse(error instanceof Error ? error.message : String(error));
328
+ return refuse(message(error));
332
329
  }
333
- const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
330
+ const held = isRecord(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
334
331
  if (!Array.isArray(held)) {
335
332
  return refuse(BASELINE_SHAPE.documentWant);
336
333
  }
@@ -338,7 +335,7 @@ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonl
338
335
  try {
339
336
  readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
340
337
  } catch (error: unknown) {
341
- return refuse(error instanceof Error ? error.message : String(error));
338
+ return refuse(message(error));
342
339
  }
343
340
  // readRecord has checked every member is a string and the code and symbol are present.
344
341
  const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
@@ -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
+ }