@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.10
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.
- package/README.md +1 -1
- package/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +181 -341
- package/dist/src/augmentations.js +106 -13
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/build-scripts.js +425 -0
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +2 -3
- package/dist/src/config.js +6 -82
- package/dist/src/configuration-files.js +28 -176
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +3 -30
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/contract/config.generated.js +558 -0
- package/dist/src/contract/contract.generated.js +1244 -0
- package/dist/src/contract/report.generated.js +862 -0
- package/dist/src/convention-rows.js +73 -3
- package/dist/src/conventions.js +36 -66
- package/dist/src/dependencies.js +103 -40
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +2 -8
- package/dist/src/entry-points.js +11 -27
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +1 -63
- package/dist/src/exempt.js +21 -40
- package/dist/src/failure.js +36 -0
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +139 -72
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +5 -5
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +13 -10
- package/dist/src/findings/non-code-artifacts.js +7 -8
- package/dist/src/findings/reads-and-writes.js +16 -13
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +41 -32
- package/dist/src/findings/visibility-narrowing.js +19 -16
- package/dist/src/findings-pass.js +8 -15
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +72 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/generated-file.js +7 -4
- package/dist/src/git-ignore.js +39 -26
- package/dist/src/graph.js +5 -3
- package/dist/src/html-entries.js +6 -33
- package/dist/src/in-operands.js +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/intra-function-parts.js +63 -43
- package/dist/src/inventory.js +2 -21
- package/dist/src/javascript-file.js +33 -0
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +3 -366
- package/dist/src/load/classify.js +270 -0
- package/dist/src/load/diagnostics.js +80 -0
- package/dist/src/load/discover.js +277 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +27 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +313 -0
- package/dist/src/load/specifiers.js +115 -0
- package/dist/src/load/synthesize.js +140 -0
- package/dist/src/load/test-globals.js +207 -0
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
- package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
- package/dist/src/{workspace.js → load/workspace.js} +62 -124
- package/dist/src/manifest.js +49 -54
- package/dist/src/matrix.js +76 -4
- package/dist/src/mdx-entries.js +3 -26
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/query.js +1 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +48 -30
- package/dist/src/report-schema.js +51 -0
- package/dist/src/report.js +57 -41
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +16 -10
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -729
- package/dist/src/roots.js +273 -87
- package/dist/src/run.js +66 -77
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +38 -115
- package/dist/src/serialization-contract.js +595 -21
- package/dist/src/session.js +34 -29
- package/dist/src/source-files.js +1 -1
- package/dist/src/suppress-file.js +25 -28
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +33 -51
- package/dist/src/sweep.js +7 -9
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-support.js +8 -11
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/named-files.js +54 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/text/yaml.js +393 -0
- package/dist/src/type-errors.js +11 -232
- package/dist/src/unseen-loads.js +100 -0
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +166 -126
- package/dist/src/verbs/describe.js +9 -16
- package/dist/src/verbs/explain.js +10 -7
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +6 -4
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +14 -3
- package/dist/src/workflow-steps.js +62 -64
- package/jsr.json +1 -1
- package/package.json +4 -3
- package/src/analysis.ts +245 -502
- package/src/augmentations.ts +135 -17
- package/src/automatic-types.ts +11 -29
- package/src/build-scripts.ts +543 -0
- package/src/component-layer.ts +4 -2
- package/src/components.ts +2 -4
- package/src/config.ts +31 -302
- package/src/configuration-files.ts +34 -179
- package/src/configured-declarations.ts +18 -23
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +6 -57
- package/src/contextual-uses.ts +64 -12
- package/src/contract/config.generated.ts +818 -0
- package/src/contract/contract.generated.ts +1308 -0
- package/src/contract/report.generated.ts +1237 -0
- package/src/convention-rows.ts +85 -3
- package/src/conventions.ts +46 -84
- package/src/dependencies.ts +118 -46
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +3 -9
- package/src/entry-points.ts +12 -38
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +1 -75
- package/src/exempt.ts +36 -50
- package/src/failure.ts +63 -0
- package/src/file-facts.ts +9 -24
- package/src/finding.ts +14 -9
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +181 -84
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +8 -9
- package/src/findings/emitters.ts +7 -7
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +13 -11
- package/src/findings/non-code-artifacts.ts +9 -11
- package/src/findings/reads-and-writes.ts +20 -16
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +45 -32
- package/src/findings/visibility-narrowing.ts +20 -17
- package/src/findings-pass.ts +10 -19
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +92 -0
- package/src/fs/tree.ts +110 -0
- package/src/generated-file.ts +7 -4
- package/src/git-ignore.ts +50 -26
- package/src/graph.ts +5 -3
- package/src/host.ts +12 -5
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/injection-container.ts +1 -1
- package/src/intra-function-parts.ts +73 -44
- package/src/inventory.ts +2 -20
- package/src/javascript-file.ts +37 -0
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +5 -413
- package/src/load/classify.ts +397 -0
- package/src/load/diagnostics.ts +144 -0
- package/src/load/discover.ts +455 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +73 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +504 -0
- package/src/load/specifiers.ts +168 -0
- package/src/load/synthesize.ts +211 -0
- package/src/load/test-globals.ts +251 -0
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
- package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
- package/src/{workspace.ts → load/workspace.ts} +90 -134
- package/src/manifest.ts +81 -75
- package/src/matrix.ts +122 -4
- package/src/mdx-entries.ts +3 -26
- package/src/outside-tests.ts +1 -1
- package/src/partial.ts +3 -2
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -92
- package/src/product.ts +18 -0
- package/src/query.ts +11 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +52 -31
- package/src/report-schema.ts +68 -0
- package/src/report.ts +101 -224
- package/src/reporters/annotations.ts +22 -11
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -4
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +29 -16
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +283 -1114
- package/src/roots.ts +339 -104
- package/src/run.ts +75 -91
- package/src/schema.ts +391 -388
- package/src/scope.ts +46 -138
- package/src/serialization-contract.ts +763 -22
- package/src/session.ts +70 -54
- package/src/source-files.ts +1 -1
- package/src/suppress-file.ts +24 -33
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +43 -58
- package/src/sweep.ts +23 -9
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +15 -25
- package/src/test-support.ts +17 -13
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/named-files.inputs.json +1522 -0
- package/src/text/named-files.ts +68 -0
- package/src/text/shell.ts +161 -0
- package/src/text/yaml.ts +423 -0
- package/src/type-errors.ts +18 -288
- package/src/unseen-loads.ts +134 -0
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +224 -162
- package/src/verbs/describe.ts +14 -15
- package/src/verbs/explain.ts +10 -11
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +6 -4
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +2 -8
- package/src/verbs/version.ts +15 -3
- package/src/workflow-steps.ts +74 -64
- package/dist/src/base-url.js +0 -18
- package/dist/src/discover.js +0 -468
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/setup-failure.js +0 -23
- package/dist/src/test-joins.js +0 -101
- package/dist/src/version.js +0 -7
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -653
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/setup-failure.ts +0 -39
- package/src/test-joins.ts +0 -120
- package/src/version.ts +0 -7
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a command's text names, read as plain text: the text is searched for each file's
|
|
3
|
+
* names, which every spelling of its path contains, so no part of it is split, unquoted or
|
|
4
|
+
* skipped first.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dirnamePath, normalizePath, relativePath } from "../paths.ts";
|
|
8
|
+
|
|
9
|
+
/** The extension a source or an emitted file carries: a JavaScript or TypeScript one, or a declaration's. */
|
|
10
|
+
const SOURCE_EXTENSION = /\.(?:d\.[cm]?ts|[cm]?[jt]s|[jt]sx)$/u;
|
|
11
|
+
|
|
12
|
+
/** One file, and every path it is spelled by: its own and each one the project writes it to. */
|
|
13
|
+
interface Spelled<File> {
|
|
14
|
+
readonly file: File;
|
|
15
|
+
readonly paths: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The names whose presence in a text holds one path below `dir`: its last name without the
|
|
20
|
+
* extension, or with it where nothing else is left, and for an `index` file its directory's
|
|
21
|
+
* last name, and `.` too where that is `dir`. Every spelling of the path relative to `dir`,
|
|
22
|
+
* with or without `./`, `/` or `\`, its extension or its `/index` part, and every longer path
|
|
23
|
+
* ending in one, holds one of them. None is empty, because an empty name is in every text.
|
|
24
|
+
*/
|
|
25
|
+
function stemsOf(path: string, dir: string): readonly string[] {
|
|
26
|
+
const last = path.slice(path.lastIndexOf("/") + 1);
|
|
27
|
+
const bare = last.replace(SOURCE_EXTENSION, "");
|
|
28
|
+
const name = bare === "" ? last : bare;
|
|
29
|
+
if (name !== "index") {
|
|
30
|
+
return [name];
|
|
31
|
+
}
|
|
32
|
+
const parent = dirnamePath(path);
|
|
33
|
+
return [
|
|
34
|
+
name,
|
|
35
|
+
parent.slice(parent.lastIndexOf("/") + 1),
|
|
36
|
+
...(parent === normalizePath(dir) ? ["."] : []),
|
|
37
|
+
].filter((stem) => stem !== "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every file of `files` below `dir` that `text` names: one whose {@link stemsOf} a path of it
|
|
42
|
+
* spells appears anywhere in the text. A file the text does not name may be held too, and no
|
|
43
|
+
* file it names is missed.
|
|
44
|
+
*/
|
|
45
|
+
export function spelledFiles<File>(
|
|
46
|
+
text: string,
|
|
47
|
+
dir: string,
|
|
48
|
+
files: readonly Spelled<File>[],
|
|
49
|
+
): readonly File[] {
|
|
50
|
+
const byStem = new Map<string, File[]>();
|
|
51
|
+
for (const { file, paths } of files) {
|
|
52
|
+
const [own] = paths;
|
|
53
|
+
if (own === undefined || relativePath(dir, own) === undefined) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const stem of new Set(paths.flatMap((path) => stemsOf(path, dir)))) {
|
|
57
|
+
byStem.set(stem, [...(byStem.get(stem) ?? []), file]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const found = new Set<File>();
|
|
61
|
+
// One pass per stem length: each place of the text is looked up once for every length.
|
|
62
|
+
for (const length of new Set([...byStem.keys()].map((stem) => stem.length))) {
|
|
63
|
+
for (let at = 0; at + length <= text.length; at += 1) {
|
|
64
|
+
byStem.get(text.slice(at, at + length))?.forEach((file) => found.add(file));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...found];
|
|
68
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* The shell's list and pipeline operators, each of which ends one command. An `&` beside a
|
|
8
|
+
* redirection's `<` or `>` (`2>&1`, and bash's `&>log`) is part of the redirection.
|
|
9
|
+
*/
|
|
10
|
+
const COMMAND_OPERATORS = /&&|\|\||;|\||(?<![<>])&(?!>)/u;
|
|
11
|
+
|
|
12
|
+
/** Where a command splits into tokens: white space and the operators the Contract names. */
|
|
13
|
+
const TOKEN_SEPARATORS = /\s+|&&|\|\||;|\|/u;
|
|
14
|
+
|
|
15
|
+
/** A text written inside one pair of quotes. */
|
|
16
|
+
const QUOTED = /^(["'])(.*)\1$/su;
|
|
17
|
+
|
|
18
|
+
/** The commands one line runs, the text between two of the shell's list and pipeline operators. */
|
|
19
|
+
export function commandsOf(line: string): readonly string[] {
|
|
20
|
+
return line.split(COMMAND_OPERATORS);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One text with one pair of enclosing quotes removed. */
|
|
24
|
+
export function unquoted(text: string): string {
|
|
25
|
+
return text.replace(QUOTED, "$2");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every token one script's command line holds, each with one pair of enclosing quotes
|
|
30
|
+
* removed. A token holding a wildcard is a pattern rather than a path, and names no
|
|
31
|
+
* entry point.
|
|
32
|
+
*/
|
|
33
|
+
export function scriptTokens(command: string): readonly string[] {
|
|
34
|
+
return command
|
|
35
|
+
.split(TOKEN_SEPARATORS)
|
|
36
|
+
.map(unquoted)
|
|
37
|
+
.filter((token) => token !== "" && !token.includes("*"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One shell line up to its comment: a `#` that starts a word outside quotes. */
|
|
41
|
+
export function withoutComment(line: string): string {
|
|
42
|
+
let quote = "";
|
|
43
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
44
|
+
const char = line[at];
|
|
45
|
+
if (quote !== "") {
|
|
46
|
+
if (char === quote) {
|
|
47
|
+
quote = "";
|
|
48
|
+
} else if (char === "\\" && quote === '"') {
|
|
49
|
+
at += 1;
|
|
50
|
+
}
|
|
51
|
+
} else if (char === "'" || char === '"') {
|
|
52
|
+
quote = char;
|
|
53
|
+
} else if (char === "\\") {
|
|
54
|
+
at += 1;
|
|
55
|
+
} else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
|
|
56
|
+
return line.slice(0, at);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return line;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
63
|
+
// 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
|
|
64
|
+
const BLANK = /[ \t\r\f\v]/u;
|
|
65
|
+
|
|
66
|
+
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
67
|
+
const OPERATOR = /[\n;&|()<>]/u;
|
|
68
|
+
|
|
69
|
+
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
70
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
74
|
+
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
75
|
+
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
76
|
+
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
77
|
+
* and each operator are undefined, so a flag's file is never read across them.
|
|
78
|
+
*/
|
|
79
|
+
export function shellWords(command: string): readonly (string | undefined)[] {
|
|
80
|
+
const words: (string | undefined)[] = [];
|
|
81
|
+
let word: string | undefined;
|
|
82
|
+
let computed = false;
|
|
83
|
+
const end = (): void => {
|
|
84
|
+
if (word !== undefined) {
|
|
85
|
+
words.push(computed ? undefined : word);
|
|
86
|
+
}
|
|
87
|
+
word = undefined;
|
|
88
|
+
computed = false;
|
|
89
|
+
};
|
|
90
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
91
|
+
const char = command.charAt(at);
|
|
92
|
+
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
93
|
+
at += 1;
|
|
94
|
+
} else if (BLANK.test(char)) {
|
|
95
|
+
end();
|
|
96
|
+
} else if (OPERATOR.test(char)) {
|
|
97
|
+
end();
|
|
98
|
+
words.push(undefined);
|
|
99
|
+
} else if (char === "#" && word === undefined) {
|
|
100
|
+
const newline = command.indexOf("\n", at);
|
|
101
|
+
at = newline < 0 ? command.length : newline - 1;
|
|
102
|
+
} else if (char === "\\") {
|
|
103
|
+
at += 1;
|
|
104
|
+
word = (word ?? "") + command.charAt(at);
|
|
105
|
+
} else if (char === "'" || char === '"') {
|
|
106
|
+
const close = closingQuote(command, at);
|
|
107
|
+
if (close < 0) {
|
|
108
|
+
word ??= "";
|
|
109
|
+
computed = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
const quoted =
|
|
113
|
+
char === "'"
|
|
114
|
+
? { text: command.slice(at + 1, close), computed: false }
|
|
115
|
+
: doubleQuoted(command.slice(at + 1, close));
|
|
116
|
+
word = (word ?? "") + quoted.text;
|
|
117
|
+
computed ||= quoted.computed;
|
|
118
|
+
at = close;
|
|
119
|
+
} else {
|
|
120
|
+
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
121
|
+
word = (word ?? "") + char;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
end();
|
|
125
|
+
return words;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
129
|
+
function doubleQuoted(inner: string): { readonly text: string; readonly computed: boolean } {
|
|
130
|
+
let text = "";
|
|
131
|
+
let computed = false;
|
|
132
|
+
for (let at = 0; at < inner.length; at += 1) {
|
|
133
|
+
const char = inner.charAt(at);
|
|
134
|
+
const next = inner.charAt(at + 1);
|
|
135
|
+
if (char === "\\" && next === "\n") {
|
|
136
|
+
at += 1;
|
|
137
|
+
} else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
138
|
+
text += next;
|
|
139
|
+
at += 1;
|
|
140
|
+
} else {
|
|
141
|
+
computed ||= char === "$" || char === "`";
|
|
142
|
+
text += char;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { text, computed };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
149
|
+
function closingQuote(command: string, open: number): number {
|
|
150
|
+
const quote = command.charAt(open);
|
|
151
|
+
for (let at = open + 1; at < command.length; at += 1) {
|
|
152
|
+
const char = command.charAt(at);
|
|
153
|
+
if (char === quote) {
|
|
154
|
+
return at;
|
|
155
|
+
}
|
|
156
|
+
if (quote === '"' && char === "\\") {
|
|
157
|
+
at += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
package/src/text/yaml.ts
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value of a YAML configuration document in the subset tool configurations write:
|
|
3
|
+
* block mappings and sequences, flow mappings and sequences, and plain, single-quoted and
|
|
4
|
+
* double-quoted scalars, with comments. A document using anything else, an anchor, an
|
|
5
|
+
* alias, a tag, a block scalar, a complex key or a second document, has no value here,
|
|
6
|
+
* as a JSON document that does not parse has none.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isRecord } from "../json-document.ts";
|
|
10
|
+
|
|
11
|
+
/** One line of the document with its comment removed, by its indentation and its content. */
|
|
12
|
+
interface Line {
|
|
13
|
+
readonly indent: number;
|
|
14
|
+
readonly text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A failure to read the document within the subset; it ends the whole reading. */
|
|
18
|
+
class Unsupported extends Error {}
|
|
19
|
+
|
|
20
|
+
/** The characters after which a quote opens a quoted scalar rather than standing in a plain one. */
|
|
21
|
+
const SCALAR_START = new Set(["", ":", "-", "[", "{", ","]);
|
|
22
|
+
|
|
23
|
+
/** The plain scalars YAML's core schema reads as something other than a string. */
|
|
24
|
+
const CORE: ReadonlyMap<string, unknown> = new Map<string, unknown>([
|
|
25
|
+
["true", true],
|
|
26
|
+
["True", true],
|
|
27
|
+
["TRUE", true],
|
|
28
|
+
["false", false],
|
|
29
|
+
["False", false],
|
|
30
|
+
["FALSE", false],
|
|
31
|
+
["null", null],
|
|
32
|
+
["Null", null],
|
|
33
|
+
["NULL", null],
|
|
34
|
+
["~", null],
|
|
35
|
+
["", null],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const NUMBER = /^[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/u;
|
|
39
|
+
|
|
40
|
+
/** The text of one line before its comment: a `#` at the line's start or after a blank, outside quotes. */
|
|
41
|
+
function withoutComment(line: string): string {
|
|
42
|
+
let quote: string | undefined;
|
|
43
|
+
let previous = "";
|
|
44
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
45
|
+
const char = line.charAt(at);
|
|
46
|
+
if (quote !== undefined) {
|
|
47
|
+
if (quote === '"' && char === "\\") {
|
|
48
|
+
at += 1;
|
|
49
|
+
} else if (char === quote) {
|
|
50
|
+
quote = undefined;
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if ((char === "'" || char === '"') && SCALAR_START.has(previous)) {
|
|
55
|
+
quote = char;
|
|
56
|
+
} else if (char === "#" && (at === 0 || /\s/u.test(line.charAt(at - 1)))) {
|
|
57
|
+
return line.slice(0, at);
|
|
58
|
+
}
|
|
59
|
+
if (!/\s/u.test(char)) {
|
|
60
|
+
previous = char;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return line;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The value one scalar's text denotes, quoted or plain. */
|
|
67
|
+
function scalar(text: string): unknown {
|
|
68
|
+
const value = text.trim();
|
|
69
|
+
if (value.startsWith("'")) {
|
|
70
|
+
if (!value.endsWith("'") || value.length < 2) {
|
|
71
|
+
throw new Unsupported();
|
|
72
|
+
}
|
|
73
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
74
|
+
}
|
|
75
|
+
if (value.startsWith('"')) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(value) as unknown;
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Unsupported();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (/^[&*!|>%@`?]/u.test(value) || value === "---" || value === "...") {
|
|
83
|
+
throw new Unsupported();
|
|
84
|
+
}
|
|
85
|
+
if (/:(?:\s|$)/u.test(value)) {
|
|
86
|
+
throw new Unsupported();
|
|
87
|
+
}
|
|
88
|
+
if (CORE.has(value)) {
|
|
89
|
+
return CORE.get(value);
|
|
90
|
+
}
|
|
91
|
+
return NUMBER.test(value) ? Number(value) : value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Reads one flow collection or flow scalar from `text` at `at`, answering the value and where it ends. */
|
|
95
|
+
function flow(text: string, start: number): { readonly value: unknown; readonly end: number } {
|
|
96
|
+
let at = start;
|
|
97
|
+
const skip = (): void => {
|
|
98
|
+
while (at < text.length && /\s/u.test(text.charAt(at))) {
|
|
99
|
+
at += 1;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
skip();
|
|
103
|
+
const open = text.charAt(at);
|
|
104
|
+
if (open === "[" || open === "{") {
|
|
105
|
+
const close = open === "[" ? "]" : "}";
|
|
106
|
+
const items: unknown[] = [];
|
|
107
|
+
const object: Record<string, unknown> = {};
|
|
108
|
+
at += 1;
|
|
109
|
+
for (;;) {
|
|
110
|
+
skip();
|
|
111
|
+
if (text.charAt(at) === close) {
|
|
112
|
+
return { value: open === "[" ? items : object, end: at + 1 };
|
|
113
|
+
}
|
|
114
|
+
const item = flow(text, at);
|
|
115
|
+
at = item.end;
|
|
116
|
+
skip();
|
|
117
|
+
if (open === "{") {
|
|
118
|
+
if (text.charAt(at) !== ":" || typeof item.value !== "string") {
|
|
119
|
+
throw new Unsupported();
|
|
120
|
+
}
|
|
121
|
+
const held = flow(text, at + 1);
|
|
122
|
+
object[item.value] = held.value;
|
|
123
|
+
at = held.end;
|
|
124
|
+
skip();
|
|
125
|
+
} else {
|
|
126
|
+
items.push(item.value);
|
|
127
|
+
}
|
|
128
|
+
if (text.charAt(at) === ",") {
|
|
129
|
+
at += 1;
|
|
130
|
+
} else if (text.charAt(at) !== close) {
|
|
131
|
+
throw new Unsupported();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (open === "'" || open === '"') {
|
|
136
|
+
let end = at + 1;
|
|
137
|
+
for (; end < text.length; end += 1) {
|
|
138
|
+
const char = text.charAt(end);
|
|
139
|
+
if (open === '"' && char === "\\") {
|
|
140
|
+
end += 1;
|
|
141
|
+
} else if (char === open && !(open === "'" && text.charAt(end + 1) === "'")) {
|
|
142
|
+
break;
|
|
143
|
+
} else if (char === open) {
|
|
144
|
+
end += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (end >= text.length) {
|
|
148
|
+
throw new Unsupported();
|
|
149
|
+
}
|
|
150
|
+
return { value: scalar(text.slice(at, end + 1)), end: end + 1 };
|
|
151
|
+
}
|
|
152
|
+
let end = at;
|
|
153
|
+
while (end < text.length && !",]}".includes(text.charAt(end))) {
|
|
154
|
+
if (text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
end += 1;
|
|
158
|
+
}
|
|
159
|
+
return { value: scalar(text.slice(at, end)), end };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A value written on one or more lines of a block, read whole: a flow node, else one plain scalar. */
|
|
163
|
+
function flowValue(text: string): unknown {
|
|
164
|
+
if (!/^["'[{]/u.test(text)) {
|
|
165
|
+
return scalar(text);
|
|
166
|
+
}
|
|
167
|
+
const read = flow(text, 0);
|
|
168
|
+
if (text.slice(read.end).trim() !== "") {
|
|
169
|
+
throw new Unsupported();
|
|
170
|
+
}
|
|
171
|
+
return read.value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The position of a mapping key's `:` in one line's content, or -1 where the content is no mapping entry. */
|
|
175
|
+
function keyEnd(text: string): number {
|
|
176
|
+
let quote: string | undefined;
|
|
177
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
178
|
+
const char = text.charAt(at);
|
|
179
|
+
if (quote !== undefined) {
|
|
180
|
+
if (quote === '"' && char === "\\") {
|
|
181
|
+
at += 1;
|
|
182
|
+
} else if (char === quote) {
|
|
183
|
+
quote = undefined;
|
|
184
|
+
}
|
|
185
|
+
} else if (at === 0 && (char === "'" || char === '"')) {
|
|
186
|
+
quote = char;
|
|
187
|
+
} else if (char === "[" || char === "{") {
|
|
188
|
+
return -1;
|
|
189
|
+
} else if (char === ":" && (at + 1 === text.length || /\s/u.test(text.charAt(at + 1)))) {
|
|
190
|
+
return at;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** How many flow collections one text opens and leaves unclosed, quotes read through. */
|
|
197
|
+
function openCollections(text: string): number {
|
|
198
|
+
let depth = 0;
|
|
199
|
+
let quote: string | undefined;
|
|
200
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
201
|
+
const char = text.charAt(at);
|
|
202
|
+
if (quote !== undefined) {
|
|
203
|
+
if (quote === '"' && char === "\\") {
|
|
204
|
+
at += 1;
|
|
205
|
+
} else if (char === quote) {
|
|
206
|
+
quote = undefined;
|
|
207
|
+
}
|
|
208
|
+
} else if (
|
|
209
|
+
(char === "'" || char === '"') &&
|
|
210
|
+
SCALAR_START.has(text.slice(0, at).trim().slice(-1))
|
|
211
|
+
) {
|
|
212
|
+
quote = char;
|
|
213
|
+
} else if (char === "[" || char === "{") {
|
|
214
|
+
depth += 1;
|
|
215
|
+
} else if (char === "]" || char === "}") {
|
|
216
|
+
depth -= 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return depth;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Whether one line's content is a block sequence item. */
|
|
223
|
+
function isItem(text: string): boolean {
|
|
224
|
+
return text === "-" || text.startsWith("- ");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Reads the document's lines as one value. */
|
|
228
|
+
class Reader {
|
|
229
|
+
private at = 0;
|
|
230
|
+
private readonly lines: Line[];
|
|
231
|
+
|
|
232
|
+
constructor(lines: Line[]) {
|
|
233
|
+
this.lines = lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The whole document's value. */
|
|
237
|
+
document(): unknown {
|
|
238
|
+
const first = this.lines[0];
|
|
239
|
+
if (first === undefined) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const value = this.block(first.indent);
|
|
243
|
+
if (this.at < this.lines.length) {
|
|
244
|
+
throw new Unsupported();
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The block node whose first line is the next one, at `indent`. */
|
|
250
|
+
private block(indent: number): unknown {
|
|
251
|
+
const line = this.lines[this.at];
|
|
252
|
+
if (line === undefined) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
if (isItem(line.text)) {
|
|
256
|
+
return this.sequence(indent);
|
|
257
|
+
}
|
|
258
|
+
if (keyEnd(line.text) >= 0) {
|
|
259
|
+
return this.mapping(indent);
|
|
260
|
+
}
|
|
261
|
+
return this.multiline(indent);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A scalar or a flow collection whose text runs over the next lines indented deeper than `indent`. */
|
|
265
|
+
private multiline(indent: number): unknown {
|
|
266
|
+
const parts: string[] = [];
|
|
267
|
+
for (
|
|
268
|
+
let line = this.lines[this.at];
|
|
269
|
+
line !== undefined && (parts.length === 0 || line.indent > indent);
|
|
270
|
+
line = this.lines[this.at]
|
|
271
|
+
) {
|
|
272
|
+
parts.push(line.text);
|
|
273
|
+
this.at += 1;
|
|
274
|
+
}
|
|
275
|
+
return flowValue(parts.join(" "));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The value after a key or an item marker: inline text, else the deeper block below it. */
|
|
279
|
+
private valueAfter(inline: string, indent: number, sequenceAtIndent: boolean): unknown {
|
|
280
|
+
if (inline !== "") {
|
|
281
|
+
const parts = [inline];
|
|
282
|
+
// A flow collection's closing bracket may stand at the key's own column.
|
|
283
|
+
for (
|
|
284
|
+
let line = this.lines[this.at];
|
|
285
|
+
line !== undefined && (line.indent > indent || openCollections(parts.join(" ")) > 0);
|
|
286
|
+
line = this.lines[this.at]
|
|
287
|
+
) {
|
|
288
|
+
parts.push(line.text);
|
|
289
|
+
this.at += 1;
|
|
290
|
+
}
|
|
291
|
+
return flowValue(parts.join(" "));
|
|
292
|
+
}
|
|
293
|
+
const next = this.lines[this.at];
|
|
294
|
+
if (next === undefined) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text))) {
|
|
298
|
+
return this.block(next.indent);
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private mapping(indent: number): Record<string, unknown> {
|
|
304
|
+
const object: Record<string, unknown> = {};
|
|
305
|
+
for (let line = this.lines[this.at]; line?.indent === indent; line = this.lines[this.at]) {
|
|
306
|
+
const end = keyEnd(line.text);
|
|
307
|
+
if (end < 0) {
|
|
308
|
+
throw new Unsupported();
|
|
309
|
+
}
|
|
310
|
+
const key = scalar(line.text.slice(0, end));
|
|
311
|
+
if (typeof key === "object" && key !== null) {
|
|
312
|
+
throw new Unsupported();
|
|
313
|
+
}
|
|
314
|
+
this.at += 1;
|
|
315
|
+
object[String(key)] = this.valueAfter(line.text.slice(end + 1).trim(), indent, true);
|
|
316
|
+
}
|
|
317
|
+
const left = this.lines[this.at];
|
|
318
|
+
if (left !== undefined && left.indent > indent) {
|
|
319
|
+
throw new Unsupported();
|
|
320
|
+
}
|
|
321
|
+
return object;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private sequence(indent: number): unknown[] {
|
|
325
|
+
const items: unknown[] = [];
|
|
326
|
+
for (
|
|
327
|
+
let line = this.lines[this.at];
|
|
328
|
+
line?.indent === indent && isItem(line.text);
|
|
329
|
+
line = this.lines[this.at]
|
|
330
|
+
) {
|
|
331
|
+
const content = line.text.slice(1);
|
|
332
|
+
const inline = content.trimStart();
|
|
333
|
+
if (inline !== "" && (isItem(inline) || keyEnd(inline) >= 0)) {
|
|
334
|
+
// A node on the item's own line continues at its own column: read it as a block there.
|
|
335
|
+
const moved = { indent: indent + 1 + content.length - inline.length, text: inline };
|
|
336
|
+
this.lines[this.at] = moved;
|
|
337
|
+
items.push(this.block(moved.indent));
|
|
338
|
+
} else {
|
|
339
|
+
this.at += 1;
|
|
340
|
+
items.push(this.valueAfter(inline, indent, false));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const left = this.lines[this.at];
|
|
344
|
+
if (left !== undefined && left.indent > indent) {
|
|
345
|
+
throw new Unsupported();
|
|
346
|
+
}
|
|
347
|
+
return items;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The JSON value one YAML document's text denotes, or undefined where it lies outside the subset. */
|
|
352
|
+
export function yamlDocument(text: string): unknown {
|
|
353
|
+
const lines: Line[] = [];
|
|
354
|
+
for (const raw of text.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
|
|
355
|
+
if (/^ *\t/u.test(raw)) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const content = withoutComment(raw).trimEnd();
|
|
359
|
+
const trimmed = content.trimStart();
|
|
360
|
+
if (trimmed === "") {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (trimmed === "---" && lines.length === 0) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (trimmed.startsWith("%") || trimmed === "---" || trimmed === "...") {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
lines.push({ indent: content.length - trimmed.length, text: trimmed });
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
return new Reader(lines).document();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof Unsupported) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** What one top-level key of a YAML document holds, read by {@link yamlKey}. */
|
|
382
|
+
type YamlKey =
|
|
383
|
+
| { readonly kind: "value"; readonly value: unknown }
|
|
384
|
+
| { readonly kind: "absent" }
|
|
385
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
386
|
+
|
|
387
|
+
/** The key one line of the document's top-level mapping opens, or undefined where it opens none. */
|
|
388
|
+
function topLevelKey(raw: string): string | undefined {
|
|
389
|
+
const content = withoutComment(raw).trimEnd();
|
|
390
|
+
const end = /^\s/u.test(content) ? -1 : keyEnd(content);
|
|
391
|
+
if (end < 0) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const key = scalar(content.slice(0, end));
|
|
396
|
+
return typeof key === "object" && key !== null ? undefined : String(key);
|
|
397
|
+
} catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The value of one key of a document's top-level block mapping, read from that key's lines
|
|
404
|
+
* alone, so a document whose other keys lie outside the subset still yields it. A key
|
|
405
|
+
* declared twice, or one whose own lines lie outside the subset, is refused.
|
|
406
|
+
*/
|
|
407
|
+
export function yamlKey(text: string, key: string): YamlKey {
|
|
408
|
+
const lines = text.replace(/^\uFEFF/u, "").split(/\r?\n/u);
|
|
409
|
+
const starts = lines.flatMap((line, at) => (topLevelKey(line) === undefined ? [] : [at]));
|
|
410
|
+
const named = starts.filter((at) => topLevelKey(lines[at] ?? "") === key);
|
|
411
|
+
const [start] = named;
|
|
412
|
+
if (start === undefined) {
|
|
413
|
+
return { kind: "absent" };
|
|
414
|
+
}
|
|
415
|
+
if (named.length > 1) {
|
|
416
|
+
return { kind: "refused", reason: `the ${key} key is declared more than once` };
|
|
417
|
+
}
|
|
418
|
+
const end = starts.find((at) => at > start) ?? lines.length;
|
|
419
|
+
const value = yamlDocument(lines.slice(start, end).join("\n"));
|
|
420
|
+
return isRecord(value)
|
|
421
|
+
? { kind: "value", value: value[key] }
|
|
422
|
+
: { kind: "refused", reason: `the value of ${key} is outside the YAML subset read here` };
|
|
423
|
+
}
|