@cplieger/deadset-ts 8.0.0-dev.12 → 8.0.0-dev.13

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 (73) hide show
  1. package/README.md +2 -0
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/dist/src/build-scripts.js +6 -13
  4. package/dist/src/commands/read.js +156 -0
  5. package/dist/src/commands/shell.js +75 -0
  6. package/dist/src/commands/tools.js +460 -0
  7. package/dist/src/configuration-files.js +11 -25
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/dependencies.js +67 -115
  10. package/dist/src/load/synthesize.js +12 -5
  11. package/dist/src/manifest.js +27 -14
  12. package/dist/src/roots.js +18 -11
  13. package/dist/src/text/named-files.js +24 -3
  14. package/dist/src/text/yaml.js +464 -173
  15. package/dist/src/vendor/unbash/ansi-c.js +132 -0
  16. package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
  17. package/dist/src/vendor/unbash/arithmetic.js +591 -0
  18. package/dist/src/vendor/unbash/assignment.js +131 -0
  19. package/dist/src/vendor/unbash/chars.js +48 -0
  20. package/dist/src/vendor/unbash/command.js +41 -0
  21. package/dist/src/vendor/unbash/expansion.js +80 -0
  22. package/dist/src/vendor/unbash/glob.js +268 -0
  23. package/dist/src/vendor/unbash/heredoc.js +27 -0
  24. package/dist/src/vendor/unbash/internal-types.js +1 -0
  25. package/dist/src/vendor/unbash/lexer.js +4274 -0
  26. package/dist/src/vendor/unbash/limits.js +3 -0
  27. package/dist/src/vendor/unbash/parser.js +1366 -0
  28. package/dist/src/vendor/unbash/parts.js +57 -0
  29. package/dist/src/vendor/unbash/substitution-context.js +8 -0
  30. package/dist/src/vendor/unbash/traversal.js +212 -0
  31. package/dist/src/vendor/unbash/types.js +1 -0
  32. package/dist/src/vendor/unbash/word-value.js +61 -0
  33. package/dist/src/vendor/unbash/word.js +40 -0
  34. package/dist/src/workflow-steps.js +111 -133
  35. package/jsr.json +2 -1
  36. package/package.json +4 -2
  37. package/src/build-scripts.ts +6 -13
  38. package/src/commands/read.ts +230 -0
  39. package/src/commands/shell.ts +126 -0
  40. package/src/commands/tools.ts +618 -0
  41. package/src/configuration-files.ts +10 -26
  42. package/src/conformance-record.ts +1 -1
  43. package/src/dependencies.ts +99 -123
  44. package/src/load/synthesize.ts +18 -5
  45. package/src/manifest.ts +43 -25
  46. package/src/roots.ts +20 -13
  47. package/src/text/named-files.ts +26 -3
  48. package/src/text/yaml.ts +503 -179
  49. package/src/vendor/unbash/LICENSE +15 -0
  50. package/src/vendor/unbash/VENDORED.md +16 -0
  51. package/src/vendor/unbash/ansi-c.ts +132 -0
  52. package/src/vendor/unbash/arithmetic-parts.ts +53 -0
  53. package/src/vendor/unbash/arithmetic.ts +641 -0
  54. package/src/vendor/unbash/assignment.ts +163 -0
  55. package/src/vendor/unbash/chars.ts +48 -0
  56. package/src/vendor/unbash/command.ts +47 -0
  57. package/src/vendor/unbash/expansion.ts +144 -0
  58. package/src/vendor/unbash/glob.ts +284 -0
  59. package/src/vendor/unbash/heredoc.ts +34 -0
  60. package/src/vendor/unbash/internal-types.ts +94 -0
  61. package/src/vendor/unbash/lexer.ts +4352 -0
  62. package/src/vendor/unbash/limits.ts +3 -0
  63. package/src/vendor/unbash/parser.ts +1612 -0
  64. package/src/vendor/unbash/parts.ts +96 -0
  65. package/src/vendor/unbash/substitution-context.ts +20 -0
  66. package/src/vendor/unbash/traversal.ts +182 -0
  67. package/src/vendor/unbash/types.ts +660 -0
  68. package/src/vendor/unbash/word-value.ts +64 -0
  69. package/src/vendor/unbash/word.ts +50 -0
  70. package/src/workflow-steps.ts +153 -154
  71. package/dist/src/text/shell.js +0 -159
  72. package/src/text/named-files.inputs.json +0 -1522
  73. package/src/text/shell.ts +0 -161
@@ -0,0 +1,50 @@
1
+ // Code generated by scripts/vendor-unbash.ts from unbash 6.0.0 (968c14992a43dc06ce29c9f51858abd4a87851ea). DO NOT EDIT.
2
+ // @ts-nocheck
3
+ import type { Word, WordPart } from "./internal-types.ts";
4
+ import { unescapeBareValue, wordPartsValue } from "./word-value.ts";
5
+
6
+ export type PartsResolver = (source: string, word: Word, depth: number) => WordPart[] | undefined;
7
+
8
+ export class WordImpl implements Word {
9
+ type = "Word" as const;
10
+ text: string;
11
+ pos: number;
12
+ end: number;
13
+ #source: string | undefined;
14
+ #resolver: PartsResolver | undefined;
15
+ #depth: number;
16
+ #parts: WordPart[] | undefined | null;
17
+ #value: string | null = null;
18
+
19
+ constructor(text: string, pos: number, end: number, source?: string, resolver?: PartsResolver, depth = 0) {
20
+ this.text = text;
21
+ this.pos = pos;
22
+ this.end = end;
23
+ this.#source = source;
24
+ this.#resolver = resolver;
25
+ this.#depth = depth;
26
+ this.#parts = resolver === undefined ? undefined : null;
27
+ }
28
+
29
+ get value(): string {
30
+ if (this.#value === null) {
31
+ const parts = this.#source === undefined && this.#parts === null ? undefined : this.parts;
32
+ this.#value = parts ? wordPartsValue(parts) : unescapeBareValue(this.text);
33
+ }
34
+ return this.#value;
35
+ }
36
+
37
+ get parts(): WordPart[] | undefined {
38
+ if (this.#parts === null) {
39
+ this.#parts = this.#resolver?.(this.#source ?? "", this, this.#depth) ?? undefined;
40
+ }
41
+ return this.#parts;
42
+ }
43
+ set parts(v: WordPart[] | undefined) {
44
+ this.#parts = v ?? undefined;
45
+ }
46
+
47
+ toJSON() {
48
+ return { type: this.type, text: this.text, pos: this.pos, end: this.end, parts: this.parts, value: this.value };
49
+ }
50
+ }
@@ -1,60 +1,60 @@
1
1
  /**
2
- * The source files a CI workflow's `run:` steps and the repository's git hooks name by their
3
- * path from the target root. A workflow or hook file is read as text, line by line, with no
4
- * YAML parsed.
2
+ * The command strings beside a target root's packages: a CI workflow's `run:` steps, the git
3
+ * hooks, the hook managers' commands and the makefile's recipe lines, each with the directory
4
+ * it runs in. A workflow or hook manager file is read by `text/yaml.ts`, and one it does not
5
+ * read gives no command; each command is read by `commands/read.ts`.
5
6
  */
6
7
 
7
8
  import type { SourceFile } from "@typescript/native/ast";
9
+ import { pathsNamed, readCommands } from "./commands/read.ts";
8
10
  import type { Host } from "./host.ts";
9
- import { joinPath, relativePath, resolvePath } from "./paths.ts";
11
+ import { isRecord } from "./json-document.ts";
12
+ import { joinPath, resolvePath } from "./paths.ts";
10
13
  import type { SourceFiles } from "./source-files.ts";
11
14
  import { filesIn } from "./fs/tree.ts";
12
- import { commandsOf, scriptTokens, unquoted, withoutComment } from "./text/shell.ts";
15
+ import { yamlDocument } from "./text/yaml.ts";
13
16
 
14
- /** One file a workflow step names. */
17
+ /** One file a workflow step, git hook or recipe runs or names. */
15
18
  interface WorkflowEntry {
16
19
  readonly file: SourceFile;
17
- /** The token that named it, as the command writes it. */
20
+ /** The word or the path that named it, as the command writes it. */
18
21
  readonly source: string;
19
22
  }
20
23
 
21
- const WORKFLOWS = ".github/workflows";
24
+ /** One command string and the directory it runs in. */
25
+ interface CommandString {
26
+ readonly text: string;
27
+ /**
28
+ * The directory it runs in, absolute, or `undefined` where the file does not state what runs
29
+ * where: a directory holding a `${{ }}` expression, which GitHub Actions evaluates.
30
+ */
31
+ readonly dir: string | undefined;
32
+ /** The directory the file writes for it, as YAML decodes it, which layer 3 holds beside the text. */
33
+ readonly writtenDirectory: string | undefined;
34
+ }
22
35
 
23
- /** A line holding a run command: indentation and one sequence marker before `run:`. */
24
- const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
36
+ const WORKFLOWS = ".github/workflows";
25
37
 
26
38
  /** The directories whose every file is a git hook script, run as the shell runs it. */
27
39
  const HOOK_DIRS = [".husky", ".githooks"] as const;
28
40
 
29
- /** A line setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
30
- const WORKING_DIRECTORY = /^(\s*(?:- )?)working-directory:(.*)$/u;
31
-
32
- const LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml"] as const;
41
+ /** The key setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
42
+ const WORKING_DIRECTORY = "working-directory";
33
43
 
34
- /** A line setting the directory a lefthook command runs in. */
35
- const LEFTHOOK_ROOT = /^(\s*(?:- )?)root:(.*)$/u;
36
-
37
- /** The hook manager files whose commands are written under a key, with that key. */
38
- const HOOK_MANAGERS: readonly (readonly [string, RegExp])[] = [
39
- [".pre-commit-config.yaml", /^(\s*(?:- )?)entry:(.*)$/u],
40
- ...LEFTHOOK_FILES.map((name) => [name, RUN_LINE] as const),
44
+ /**
45
+ * The hook manager files, each with the key its commands are written under and the key of their
46
+ * directory: lefthook's `root`, https://lefthook.dev/configuration/root/
47
+ */
48
+ const HOOK_MANAGERS: readonly (readonly [string, string, string | undefined])[] = [
49
+ [".pre-commit-config.yaml", "entry", undefined],
50
+ ...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map(
51
+ (name) => [name, "run", "root"] as const,
52
+ ),
41
53
  ];
42
54
 
43
55
  /** The names GNU make reads a makefile under, in the order it tries them. */
44
56
  const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"] as const;
45
57
 
46
- const BLOCK_SCALAR = /^[|>][+-]?$/u;
47
-
48
- /** A token holding one of these is computed or a pattern, so it names no file. */
49
- const COMPUTED = /[${*?[]/u;
50
-
51
- const INDENT = /^\s*/u;
52
-
53
- /** The files directly in one directory whose name `accepts`, by path in name order. */
54
- function pathsIn(host: Host, dir: string, accepts: (name: string) => boolean): readonly string[] {
55
- return filesIn(host, dir, accepts).map((name) => joinPath(dir, name));
56
- }
57
-
58
58
  function readText(host: Host, path: string): string | undefined {
59
59
  try {
60
60
  return host.readFile(path);
@@ -63,138 +63,150 @@ function readText(host: Host, path: string): string | undefined {
63
63
  }
64
64
  }
65
65
 
66
- /** Every line one workflow file's `run:` steps hold, or every line of the values written under `key`. */
67
- export function runCommands(text: string, key: RegExp = RUN_LINE): readonly string[] {
68
- const lines = text.split(/\r?\n/u);
69
- const found: string[] = [];
70
- for (let at = 0; at < lines.length; at += 1) {
71
- const match = key.exec(lines[at] ?? "");
72
- if (match === null) {
73
- continue;
74
- }
75
- const column = (match[1] ?? "").length;
76
- const value = unquoted((match[2] ?? "").trim());
77
- if (!BLOCK_SCALAR.test(value)) {
78
- found.push(value);
79
- continue;
80
- }
81
- for (let next = at + 1; next < lines.length; next += 1) {
82
- const line = lines[next] ?? "";
83
- if (line.trim() !== "" && (INDENT.exec(line)?.[0].length ?? 0) <= column) {
84
- break;
85
- }
86
- found.push(line);
87
- }
88
- }
89
- return found;
66
+ /** The text a YAML scalar's value stands for, or `undefined` for a collection or a null. */
67
+ function scalarText(value: unknown): string | undefined {
68
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
69
+ ? String(value)
70
+ : undefined;
90
71
  }
91
72
 
92
- /**
93
- * The commands of the recipe lines of the target root's makefile, each line's in order: a
94
- * recipe line is one that starts with a tab, joined with the lines a trailing `\`
95
- * continues it onto.
96
- */
97
- function recipeLines(host: Host, root: string): readonly (readonly string[])[] {
98
- const path = MAKEFILES.map((name) => joinPath(root, name)).find(
99
- (candidate) => host.kindOf(candidate) === "file",
100
- );
101
- const text = path === undefined ? undefined : readText(host, path);
102
- if (text === undefined) {
103
- return [];
104
- }
105
- return text
106
- .replace(/\\\r?\n/gu, " ")
107
- .split(/\r?\n/u)
108
- .filter((line) => line.startsWith("\t"))
109
- .map((line) => commandsOf(withoutComment(line.slice(1).replace(/^[@+-]+/u, ""))));
73
+ /** The `defaults.run.working-directory` one mapping writes, as YAML decodes it. */
74
+ function defaultDirectory(mapping: Record<string, unknown>): string | undefined {
75
+ const defaults = mapping["defaults"];
76
+ const run = isRecord(defaults) ? defaults["run"] : undefined;
77
+ return isRecord(run) ? scalarText(run[WORKING_DIRECTORY]) : undefined;
110
78
  }
111
79
 
112
80
  /**
113
- * Every token the recipe lines of the target root's makefile hold, each with the directory
114
- * below the root it is read against: make runs each line in a shell of its own, so a `cd`
115
- * moves the directory of the later commands of its line alone.
81
+ * Every command one workflow or hook manager file writes under `key`, in every mapping of the
82
+ * document, each with the directory it runs in: its mapping's `dirKey` value, else, in a workflow
83
+ * file, the innermost `defaults.run.working-directory` of a mapping holding it, else `root`. A
84
+ * file `yamlDocument` does not read gives none.
116
85
  */
117
- function recipeTokens(host: Host, root: string): readonly { token: string; dir: string }[] {
118
- const found: { token: string; dir: string }[] = [];
119
- for (const line of recipeLines(host, root)) {
120
- let dir = root;
121
- for (const command of line) {
122
- const tokens = scriptTokens(command);
123
- const [first, target, ...rest] = tokens;
124
- if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
125
- dir = resolvePath(dir, target);
126
- continue;
127
- }
128
- for (const token of tokens) {
129
- found.push({ token, dir: relativePath(root, dir) ?? "" });
86
+ function keyedCommands(
87
+ text: string,
88
+ root: string,
89
+ key: string,
90
+ dirKey: string | undefined,
91
+ ): readonly CommandString[] {
92
+ const found: CommandString[] = [];
93
+ const visit = (node: unknown, inherited: string | undefined): void => {
94
+ if (Array.isArray(node)) {
95
+ for (const item of node) {
96
+ visit(item, inherited);
130
97
  }
98
+ return;
131
99
  }
132
- }
100
+ if (!isRecord(node)) {
101
+ return;
102
+ }
103
+ const here = (dirKey === WORKING_DIRECTORY ? defaultDirectory(node) : undefined) ?? inherited;
104
+ const command = scalarText(node[key]);
105
+ if (command !== undefined && command.trim() !== "") {
106
+ const written = (dirKey === undefined ? undefined : scalarText(node[dirKey])) ?? here;
107
+ found.push({
108
+ text: command,
109
+ dir:
110
+ written === undefined
111
+ ? root
112
+ : written.includes("${{")
113
+ ? undefined
114
+ : resolvePath(root, written),
115
+ writtenDirectory: written,
116
+ });
117
+ }
118
+ for (const value of Object.values(node)) {
119
+ visit(value, here);
120
+ }
121
+ };
122
+ visit(yamlDocument(text), undefined);
133
123
  return found;
134
124
  }
135
125
 
126
+ /** The path of each git hook script at `root`, every file in {@link HOOK_DIRS}. */
127
+ function hookScripts(host: Host, root: string): readonly string[] {
128
+ return HOOK_DIRS.flatMap((dir) =>
129
+ filesIn(host, joinPath(root, dir), () => true).map((name) => joinPath(root, dir, name)),
130
+ );
131
+ }
132
+
133
+ /** The makefile make reads at `root`, the first of {@link MAKEFILES} there. */
134
+ function makefileAt(host: Host, root: string): string | undefined {
135
+ return MAKEFILES.map((name) => joinPath(root, name)).find(
136
+ (candidate) => host.kindOf(candidate) === "file",
137
+ );
138
+ }
139
+
136
140
  /**
137
- * Every command the target root's workflow steps, git hooks and makefile run: the `run:`
138
- * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
139
- * and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
140
- * of a lefthook file and the makefile's recipe lines.
141
+ * Each recipe line of the target root's makefile: a line that starts with a tab, joined with
142
+ * the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it.
143
+ * Make runs each line in a shell of its own.
141
144
  */
142
- export function workflowCommands(host: Host, targetRoot: string): readonly string[] {
143
- const root = resolvePath(host.workingDirectory(), targetRoot);
144
- return [...stepCommands(host, root), ...recipeLines(host, root).flat()];
145
+ function recipeLines(host: Host, root: string): readonly CommandString[] {
146
+ const path = makefileAt(host, root);
147
+ const text = path === undefined ? undefined : readText(host, path);
148
+ return (text ?? "")
149
+ .replace(/\\\r?\n/gu, " ")
150
+ .split(/\r?\n/u)
151
+ .filter((line) => line.startsWith("\t"))
152
+ .map((line) => ({
153
+ text: line.slice(1).replace(/^[@+-]+/u, ""),
154
+ dir: root,
155
+ writtenDirectory: undefined,
156
+ }));
145
157
  }
146
158
 
147
159
  /**
148
- * Every directory the target root's workflow steps and lefthook commands are set to run in, as
149
- * written: each `working-directory` of a workflow file and each `root` of a lefthook file.
160
+ * Every command the target root's workflow steps, git hooks, hook managers and makefile run:
161
+ * the `run:` steps of the workflow files in `.github/workflows`, each file in `.husky` and
162
+ * `.githooks` as one shell script, the `entry:` commands of `.pre-commit-config.yaml`, the
163
+ * `run:` commands of a lefthook file and the makefile's recipe lines.
150
164
  */
151
- export function commandDirectories(host: Host, targetRoot: string): readonly string[] {
165
+ export function workflowCommands(host: Host, targetRoot: string): readonly CommandString[] {
152
166
  const root = resolvePath(host.workingDirectory(), targetRoot);
167
+ const workflows = filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name));
153
168
  return [
154
- ...workflowFiles(host, root).flatMap((path) =>
155
- runCommands(readText(host, path) ?? "", WORKING_DIRECTORY),
169
+ ...workflows.flatMap((name) =>
170
+ keyedCommands(
171
+ readText(host, joinPath(root, WORKFLOWS, name)) ?? "",
172
+ root,
173
+ "run",
174
+ WORKING_DIRECTORY,
175
+ ),
156
176
  ),
157
- ...LEFTHOOK_FILES.flatMap((name) =>
158
- runCommands(readText(host, joinPath(root, name)) ?? "", LEFTHOOK_ROOT),
177
+ ...hookScripts(host, root).map((path): CommandString => ({
178
+ text: readText(host, path) ?? "",
179
+ dir: root,
180
+ writtenDirectory: undefined,
181
+ })),
182
+ ...HOOK_MANAGERS.flatMap(([name, key, dirKey]) =>
183
+ keyedCommands(readText(host, joinPath(root, name)) ?? "", root, key, dirKey),
159
184
  ),
185
+ ...recipeLines(host, root),
160
186
  ];
161
187
  }
162
188
 
163
- /** The workflow files in `.github/workflows` at one root, by path in name order. */
164
- function workflowFiles(host: Host, root: string): readonly string[] {
165
- return pathsIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name));
166
- }
167
-
168
- /** Every token the commands {@link workflowCommands} reads hold. */
169
- export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
189
+ /**
190
+ * The whole text of each file {@link workflowCommands} reads a command from, each workflow file,
191
+ * git hook, hook manager file and makefile at the target root. Layer 3 holds it whole, so a part
192
+ * no command is read from, a comment included, or one the reading of the structure misses, still
193
+ * holds every file it names.
194
+ */
195
+ export function commandFileTexts(host: Host, targetRoot: string): readonly string[] {
170
196
  const root = resolvePath(host.workingDirectory(), targetRoot);
171
197
  return [
172
- ...stepCommands(host, root).flatMap((command) => scriptTokens(command)),
173
- ...recipeTokens(host, root).map(({ token }) => token),
174
- ];
175
- }
176
-
177
- /** The commands of the workflow steps, git hooks and hook managers at one root. */
178
- function stepCommands(host: Host, root: string): readonly string[] {
179
- const commands: string[] = [];
180
- for (const path of workflowFiles(host, root)) {
181
- commands.push(...runCommands(readText(host, path) ?? ""));
182
- }
183
- for (const dir of HOOK_DIRS) {
184
- for (const path of pathsIn(host, joinPath(root, dir), () => true)) {
185
- commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
186
- }
187
- }
188
- for (const [name, key] of HOOK_MANAGERS) {
189
- commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
190
- }
191
- return commands;
198
+ ...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) =>
199
+ joinPath(root, WORKFLOWS, name),
200
+ ),
201
+ ...hookScripts(host, root),
202
+ ...HOOK_MANAGERS.map(([name]) => joinPath(root, name)),
203
+ ...[makefileAt(host, root) ?? []].flat(),
204
+ ].flatMap((path) => readText(host, path) ?? []);
192
205
  }
193
206
 
194
207
  /**
195
- * The own files the target root's workflow steps and git hooks name, as
196
- * {@link workflowTokens} reads them: a token equal to a file's path below the target root,
197
- * with or without a leading `./`, whatever directory the step runs in.
208
+ * The own files the target root's workflow steps, git hooks and recipes run or name, as
209
+ * `pathsNamed` reads each command.
198
210
  */
199
211
  export function workflowEntries(
200
212
  host: Host,
@@ -202,24 +214,11 @@ export function workflowEntries(
202
214
  files: SourceFiles,
203
215
  ): readonly WorkflowEntry[] {
204
216
  const found: WorkflowEntry[] = [];
205
- for (const token of workflowTokens(host, targetRoot)) {
206
- if (COMPUTED.test(token)) {
207
- continue;
208
- }
209
- const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
210
- if (file !== undefined) {
211
- found.push({ file, source: token });
212
- }
213
- }
214
- const root = resolvePath(host.workingDirectory(), targetRoot);
215
- for (const { token, dir } of recipeTokens(host, root)) {
216
- const below =
217
- COMPUTED.test(token) || dir === ""
218
- ? undefined
219
- : relativePath(root, resolvePath(joinPath(root, dir), token));
220
- const file = below === undefined ? undefined : files.byPath.get(below);
221
- if (file !== undefined) {
222
- found.push({ file, source: token });
217
+ for (const { text, dir } of workflowCommands(host, targetRoot)) {
218
+ for (const { path, written } of pathsNamed(readCommands(text, dir))) {
219
+ for (const file of files.named(path)) {
220
+ found.push({ file, source: written });
221
+ }
223
222
  }
224
223
  }
225
224
  return found;
@@ -1,159 +0,0 @@
1
- /**
2
- * Shell command lines read as text, never run: the commands a line runs, the tokens the
3
- * Contract splits a command into, and the words POSIX token recognition reads.
4
- */
5
- /**
6
- * The shell's list and pipeline operators, each of which ends one command. An `&` beside a
7
- * redirection's `<` or `>` (`2>&1`, and bash's `&>log`) is part of the redirection.
8
- */
9
- const COMMAND_OPERATORS = /&&|\|\||;|\||(?<![<>])&(?!>)/u;
10
- /** Where a command splits into tokens: white space and the operators the Contract names. */
11
- const TOKEN_SEPARATORS = /\s+|&&|\|\||;|\|/u;
12
- /** A text written inside one pair of quotes. */
13
- const QUOTED = /^(["'])(.*)\1$/su;
14
- /** The commands one line runs, the text between two of the shell's list and pipeline operators. */
15
- export function commandsOf(line) {
16
- return line.split(COMMAND_OPERATORS);
17
- }
18
- /** One text with one pair of enclosing quotes removed. */
19
- export function unquoted(text) {
20
- return text.replace(QUOTED, "$2");
21
- }
22
- /**
23
- * Every token one script's command line holds, each with one pair of enclosing quotes
24
- * removed. A token holding a wildcard is a pattern rather than a path, and names no
25
- * entry point.
26
- */
27
- export function scriptTokens(command) {
28
- return command
29
- .split(TOKEN_SEPARATORS)
30
- .map(unquoted)
31
- .filter((token) => token !== "" && !token.includes("*"));
32
- }
33
- /** One shell line up to its comment: a `#` that starts a word outside quotes. */
34
- export function withoutComment(line) {
35
- let quote = "";
36
- for (let at = 0; at < line.length; at += 1) {
37
- const char = line[at];
38
- if (quote !== "") {
39
- if (char === quote) {
40
- quote = "";
41
- }
42
- else if (char === "\\" && quote === '"') {
43
- at += 1;
44
- }
45
- }
46
- else if (char === "'" || char === '"') {
47
- quote = char;
48
- }
49
- else if (char === "\\") {
50
- at += 1;
51
- }
52
- else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
53
- return line.slice(0, at);
54
- }
55
- }
56
- return line;
57
- }
58
- /** The characters that end a word outside quotes and separate nothing: blanks. */
59
- // Stryker disable next-line Regex: its \v to \V mutant is no valid escape under the u flag, so the whole module fails to parse
60
- const BLANK = /[ \t\r\f\v]/u;
61
- /** The characters that end a word outside quotes and begin an operator: newline included. */
62
- const OPERATOR = /[\n;&|()<>]/u;
63
- /** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
64
- const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
65
- /**
66
- * The words of one command line, as POSIX token recognition reads quotes, backslashes,
67
- * line continuations, comments and operators. A word an expansion may change (an unquoted
68
- * `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
69
- * is computed, and so is the rest of a line whose quote is never closed. A computed word
70
- * and each operator are undefined, so a flag's file is never read across them.
71
- */
72
- export function shellWords(command) {
73
- const words = [];
74
- let word;
75
- let computed = false;
76
- const end = () => {
77
- if (word !== undefined) {
78
- words.push(computed ? undefined : word);
79
- }
80
- word = undefined;
81
- computed = false;
82
- };
83
- for (let at = 0; at < command.length; at += 1) {
84
- const char = command.charAt(at);
85
- if (char === "\\" && command.charAt(at + 1) === "\n") {
86
- at += 1;
87
- }
88
- else if (BLANK.test(char)) {
89
- end();
90
- }
91
- else if (OPERATOR.test(char)) {
92
- end();
93
- words.push(undefined);
94
- }
95
- else if (char === "#" && word === undefined) {
96
- const newline = command.indexOf("\n", at);
97
- at = newline < 0 ? command.length : newline - 1;
98
- }
99
- else if (char === "\\") {
100
- at += 1;
101
- word = (word ?? "") + command.charAt(at);
102
- }
103
- else if (char === "'" || char === '"') {
104
- const close = closingQuote(command, at);
105
- if (close < 0) {
106
- word ??= "";
107
- computed = true;
108
- break;
109
- }
110
- const quoted = char === "'"
111
- ? { text: command.slice(at + 1, close), computed: false }
112
- : doubleQuoted(command.slice(at + 1, close));
113
- word = (word ?? "") + quoted.text;
114
- computed ||= quoted.computed;
115
- at = close;
116
- }
117
- else {
118
- computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
119
- word = (word ?? "") + char;
120
- }
121
- }
122
- end();
123
- return words;
124
- }
125
- /** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
126
- function doubleQuoted(inner) {
127
- let text = "";
128
- let computed = false;
129
- for (let at = 0; at < inner.length; at += 1) {
130
- const char = inner.charAt(at);
131
- const next = inner.charAt(at + 1);
132
- if (char === "\\" && next === "\n") {
133
- at += 1;
134
- }
135
- else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
136
- text += next;
137
- at += 1;
138
- }
139
- else {
140
- computed ||= char === "$" || char === "`";
141
- text += char;
142
- }
143
- }
144
- return { text, computed };
145
- }
146
- /** The index of the quote that closes the one at `open`, or -1 where none does. */
147
- function closingQuote(command, open) {
148
- const quote = command.charAt(open);
149
- for (let at = open + 1; at < command.length; at += 1) {
150
- const char = command.charAt(at);
151
- if (char === quote) {
152
- return at;
153
- }
154
- if (quote === '"' && char === "\\") {
155
- at += 1;
156
- }
157
- }
158
- return -1;
159
- }