@cplieger/deadset-ts 8.0.0-dev.18 → 8.0.0-dev.19
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/dist/src/analysis.js +4 -1
- package/dist/src/commands/held.js +499 -0
- package/dist/src/commands/read.js +16 -10
- package/dist/src/commands/shell.js +109 -15
- package/dist/src/commands/tools.js +1540 -149
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +7 -0
- package/dist/src/entries/conventions.js +7 -6
- package/dist/src/entries/test-runners.js +6 -40
- package/dist/src/entries/workflow-steps.js +202 -28
- package/dist/src/findings/non-code-artifacts.js +11 -5
- package/dist/src/roots/manifest.js +47 -4
- package/dist/src/roots/root-set.js +14 -16
- package/dist/src/text/named-files.js +257 -34
- package/dist/src/verbs/analyze.js +16 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +13 -1
- package/src/commands/held.ts +632 -0
- package/src/commands/read.ts +26 -12
- package/src/commands/shell.ts +117 -15
- package/src/commands/tools.ts +1972 -202
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +7 -0
- package/src/dependencies.ts +1 -1
- package/src/entries/conventions.ts +8 -12
- package/src/entries/test-runners.ts +12 -45
- package/src/entries/workflow-steps.ts +227 -38
- package/src/file-facts.ts +5 -0
- package/src/findings/non-code-artifacts.ts +16 -5
- package/src/roots/manifest.ts +53 -8
- package/src/roots/root-set.ts +18 -16
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "6.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:e4af2b9bcd07fd745101bf3215cd21e9df1448bdc332a791f25d5676cf15c794",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
};
|
|
@@ -1206,6 +1206,13 @@ export const EVIDENCE = [
|
|
|
1206
1206
|
"DS1602",
|
|
1207
1207
|
],
|
|
1208
1208
|
},
|
|
1209
|
+
{
|
|
1210
|
+
evidence: "unwritten-run-path",
|
|
1211
|
+
rule: "confidence.35",
|
|
1212
|
+
class: "possible",
|
|
1213
|
+
languages: ["ts"],
|
|
1214
|
+
codes: ["DS1601", "DS1602"],
|
|
1215
|
+
},
|
|
1209
1216
|
{
|
|
1210
1217
|
evidence: "schema-operation-default",
|
|
1211
1218
|
rule: "schemas.10",
|
|
@@ -9,7 +9,7 @@ import { lineOf } from "../component-files.js";
|
|
|
9
9
|
import { CONVENTION_ROWS, } from "./convention-rows.js";
|
|
10
10
|
import { documentStrings } from "./configuration-files.js";
|
|
11
11
|
import { readCommands } from "../commands/read.js";
|
|
12
|
-
import { nodeTestGlobs
|
|
12
|
+
import { nodeTestGlobs } from "./test-runners.js";
|
|
13
13
|
import { documentModule, membersNamed, staticModule, textsOf, valuesAt, } from "./static-module.js";
|
|
14
14
|
import { ancestors, cannotStartPackageName, dirnamePath, joinPath, normalizePath, relativePath, resolvePath, } from "../paths.js";
|
|
15
15
|
import { satisfies } from "../version-range.js";
|
|
@@ -520,11 +520,11 @@ function packageKeyUses(read, dir, manifest, keys) {
|
|
|
520
520
|
text.startsWith(`${name}/`) ||
|
|
521
521
|
(text.startsWith(`${name}:`) && text.length > name.length + 1)));
|
|
522
522
|
}
|
|
523
|
-
/**
|
|
524
|
-
function
|
|
523
|
+
/** What the command reader reads each of a manifest's `scripts` as, in the manifest's directory. */
|
|
524
|
+
function scriptReadings(manifest, dir) {
|
|
525
525
|
const scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
526
526
|
return isRecord(scripts)
|
|
527
|
-
? Object.values(scripts).flatMap((text) => typeof text === "string" ? readCommands(text, dir)
|
|
527
|
+
? Object.values(scripts).flatMap((text) => typeof text === "string" ? [readCommands(text, dir)] : [])
|
|
528
528
|
: [];
|
|
529
529
|
}
|
|
530
530
|
/**
|
|
@@ -543,6 +543,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
543
543
|
const read = readFilesOnce(host, parse);
|
|
544
544
|
for (const manifest of manifests) {
|
|
545
545
|
const declared = readJSON(host, joinPath(manifest.dir, MANIFEST));
|
|
546
|
+
const scripts = scriptReadings(declared, manifest.dir);
|
|
546
547
|
let input;
|
|
547
548
|
const runnerInput = () => {
|
|
548
549
|
input ??= {
|
|
@@ -558,7 +559,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
558
559
|
}
|
|
559
560
|
return undefined;
|
|
560
561
|
},
|
|
561
|
-
scripts:
|
|
562
|
+
scripts: scripts.flatMap((one) => one.commands),
|
|
562
563
|
};
|
|
563
564
|
return input;
|
|
564
565
|
};
|
|
@@ -659,7 +660,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
659
660
|
});
|
|
660
661
|
}
|
|
661
662
|
}
|
|
662
|
-
testGlobs.push(...nodeTestGlobs(
|
|
663
|
+
testGlobs.push(...nodeTestGlobs(scripts.flatMap((one) => one.tests)));
|
|
663
664
|
}
|
|
664
665
|
const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
|
|
665
666
|
unique.sort((a, b) => bytewise(a.name, b.name) ||
|
|
@@ -107,7 +107,7 @@ function parsed(words, table) {
|
|
|
107
107
|
else if (table.flags.has(name)) {
|
|
108
108
|
keep(name, equals < 0 ? "true" : word.slice(equals + 1));
|
|
109
109
|
}
|
|
110
|
-
else
|
|
110
|
+
else {
|
|
111
111
|
return undefined;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -632,31 +632,6 @@ const AVA_CONFIG = ["ava.config.js", "ava.config.cjs", "ava.config.mjs"];
|
|
|
632
632
|
export const AVA_6 = avaRunner(AVA_CONFIG, ["cjs", "mjs", "js"]);
|
|
633
633
|
/** AVA 8, which reads no `ava.config.cjs` and defaults to no `.cjs` test. */
|
|
634
634
|
export const AVA_8 = avaRunner(["ava.config.js", "ava.config.mjs"], ["mjs", "js"]);
|
|
635
|
-
/**
|
|
636
|
-
* The options of `node` (https://nodejs.org/api/cli.html) a test run writes: the ones that take
|
|
637
|
-
* a value, the flags, and any long option whose value follows `=`.
|
|
638
|
-
*/
|
|
639
|
-
const NODE_OPTIONS = {
|
|
640
|
-
valued: new Set([
|
|
641
|
-
...["-r", "--require", "--import", "--loader", "--experimental-loader", "--env-file"],
|
|
642
|
-
...["--env-file-if-exists", "--conditions", "-C", "--input-type", "--watch-path"],
|
|
643
|
-
...["--test-reporter", "--test-reporter-destination", "--test-concurrency"],
|
|
644
|
-
...["--test-name-pattern", "--test-skip-pattern", "--test-timeout", "--test-shard"],
|
|
645
|
-
...["--test-coverage-include", "--test-coverage-exclude", "--test-coverage-branches"],
|
|
646
|
-
...["--test-coverage-functions", "--test-coverage-lines", "--test-isolation"],
|
|
647
|
-
...["--test-global-setup"],
|
|
648
|
-
]),
|
|
649
|
-
flags: new Set([
|
|
650
|
-
...["--test", "--test-only", "--watch", "--test-force-exit", "--test-update-snapshots"],
|
|
651
|
-
...["--test-randomize", "--experimental-test-coverage", "--experimental-test-module-mocks"],
|
|
652
|
-
...["--experimental-test-snapshots", "--experimental-strip-types", "--no-strip-types"],
|
|
653
|
-
...["--no-experimental-strip-types", "--experimental-transform-types", "--enable-source-maps"],
|
|
654
|
-
...["--experimental-vm-modules", "--experimental-detect-module", "--no-warnings"],
|
|
655
|
-
...["--no-experimental-detect-module", "--experimental-require-module", "--trace-warnings"],
|
|
656
|
-
...["--no-experimental-require-module", "--expose-gc", "--inspect", "--inspect-brk"],
|
|
657
|
-
]),
|
|
658
|
-
assigned: true,
|
|
659
|
-
};
|
|
660
635
|
/** The patterns `node --test` runs with no operand (`doc/api/test.md`), the TypeScript ones where types are stripped. */
|
|
661
636
|
function nodeTestDefaults(stripped) {
|
|
662
637
|
const forms = ["**/*.test", "**/*-test", "**/*_test", "**/test-*", "**/test", "**/test/**/*"];
|
|
@@ -666,19 +641,10 @@ function nodeTestDefaults(stripped) {
|
|
|
666
641
|
];
|
|
667
642
|
}
|
|
668
643
|
/**
|
|
669
|
-
* The files each `node --test`
|
|
670
|
-
* read in the command's directory, or the default patterns where
|
|
671
|
-
*
|
|
644
|
+
* The files each `node --test` run of a manifest's scripts runs, as the command reader reads
|
|
645
|
+
* it: its operands, each a glob read in the command's directory, or the default patterns where
|
|
646
|
+
* it writes none.
|
|
672
647
|
*/
|
|
673
|
-
export function nodeTestGlobs(
|
|
674
|
-
return
|
|
675
|
-
const words = afterProgram(command, ["node"]);
|
|
676
|
-
const read = words === undefined ? undefined : parsed(words, NODE_OPTIONS);
|
|
677
|
-
if (!read?.values.has("--test")) {
|
|
678
|
-
return [];
|
|
679
|
-
}
|
|
680
|
-
const stripped = !read.values.has("--no-strip-types") && !read.values.has("--no-experimental-strip-types");
|
|
681
|
-
const patterns = read.operands.length > 0 ? read.operands : nodeTestDefaults(stripped);
|
|
682
|
-
return patterns.map((one) => belowGlob(command.dir, one));
|
|
683
|
-
});
|
|
648
|
+
export function nodeTestGlobs(runs) {
|
|
649
|
+
return runs.flatMap((run) => (run.patterns.length > 0 ? run.patterns : nodeTestDefaults(run.stripped)).map((one) => belowGlob(run.dir, one)));
|
|
684
650
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
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
|
|
4
|
-
* it runs in. A workflow or hook manager file is read by `text/yaml.ts`, and one it does not
|
|
3
|
+
* hooks, the hook managers' commands and scripts and the makefile's recipe lines, each with the
|
|
4
|
+
* directory it runs in. A workflow or hook manager file is read by `text/yaml.ts`, and one it does not
|
|
5
5
|
* read gives no command; each command is read by `commands/read.ts`.
|
|
6
6
|
*/
|
|
7
7
|
import { pathsNamed, readCommands } from "../commands/read.js";
|
|
8
|
+
import { PART } from "../commands/shell.js";
|
|
8
9
|
import { isRecord } from "../json-document.js";
|
|
9
10
|
import { joinPath, resolvePath } from "../paths.js";
|
|
10
11
|
import { filesIn } from "../fs/tree.js";
|
|
@@ -14,14 +15,20 @@ const WORKFLOWS = ".github/workflows";
|
|
|
14
15
|
const HOOK_DIRS = [".husky", ".githooks"];
|
|
15
16
|
/** The key setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
|
|
16
17
|
const WORKING_DIRECTORY = "working-directory";
|
|
17
|
-
/**
|
|
18
|
-
* The hook manager files, each with the key its commands are written under and the key of their
|
|
19
|
-
* directory: lefthook's `root`, https://lefthook.dev/configuration/root/
|
|
20
|
-
*/
|
|
21
18
|
const HOOK_MANAGERS = [
|
|
22
|
-
|
|
23
|
-
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map((name) =>
|
|
19
|
+
{ name: ".pre-commit-config.yaml", key: "entry" },
|
|
20
|
+
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map((name) => ({
|
|
21
|
+
name,
|
|
22
|
+
key: "run",
|
|
23
|
+
dirKey: "root",
|
|
24
|
+
scripts: true,
|
|
25
|
+
})),
|
|
24
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* The directories lefthook searches for a hook's scripts after the file's `source_dir` and
|
|
29
|
+
* `source_dir_local`: https://github.com/evilmartians/lefthook/blob/master/internal/command/run.go
|
|
30
|
+
*/
|
|
31
|
+
const LEFTHOOK_DIRS = [".config/lefthook", ".config/lefthook-local"];
|
|
25
32
|
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
26
33
|
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"];
|
|
27
34
|
function readText(host, path) {
|
|
@@ -73,7 +80,6 @@ function keyedCommands(text, root, key, dirKey) {
|
|
|
73
80
|
: written.includes("${{")
|
|
74
81
|
? undefined
|
|
75
82
|
: resolvePath(root, written),
|
|
76
|
-
writtenDirectory: written,
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
for (const value of Object.values(node)) {
|
|
@@ -83,6 +89,51 @@ function keyedCommands(text, root, key, dirKey) {
|
|
|
83
89
|
visit(yamlDocument(text), undefined);
|
|
84
90
|
return found;
|
|
85
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The command lefthook runs for each script a lefthook file's hooks name, each key of a hook's
|
|
94
|
+
* `scripts` and each `script` of a job in its `jobs`, a group's among them, from each directory
|
|
95
|
+
* it searches: its `runner`, then the path `<dir>/<hook>/<name>` quoted as lefthook quotes it,
|
|
96
|
+
* run in the target root: https://lefthook.dev/configuration/Scripts/
|
|
97
|
+
*/
|
|
98
|
+
function lefthookScripts(text, root) {
|
|
99
|
+
const document = yamlDocument(text);
|
|
100
|
+
if (!isRecord(document)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const dirs = [
|
|
104
|
+
scalarText(document["source_dir"]) ?? ".lefthook/",
|
|
105
|
+
scalarText(document["source_dir_local"]) ?? ".lefthook-local/",
|
|
106
|
+
...LEFTHOOK_DIRS,
|
|
107
|
+
];
|
|
108
|
+
const found = [];
|
|
109
|
+
const add = (hook, name, runner) => {
|
|
110
|
+
for (const dir of dirs) {
|
|
111
|
+
const path = `'${joinPath(dir, hook, name).replaceAll("'", "'\\''")}'`;
|
|
112
|
+
found.push({
|
|
113
|
+
text: runner === undefined || runner.trim() === "" ? path : `${runner} ${path}`,
|
|
114
|
+
dir: root,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const jobs = (hook, list) => {
|
|
119
|
+
for (const job of Array.isArray(list) ? list : []) {
|
|
120
|
+
const script = isRecord(job) ? scalarText(job["script"]) : undefined;
|
|
121
|
+
if (isRecord(job) && script !== undefined) {
|
|
122
|
+
add(hook, script, scalarText(job["runner"]));
|
|
123
|
+
}
|
|
124
|
+
const group = isRecord(job) ? job["group"] : undefined;
|
|
125
|
+
jobs(hook, isRecord(group) ? group["jobs"] : undefined);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
for (const [hook, value] of Object.entries(document)) {
|
|
129
|
+
const scripts = isRecord(value) ? value["scripts"] : undefined;
|
|
130
|
+
for (const [name, options] of Object.entries(isRecord(scripts) ? scripts : {})) {
|
|
131
|
+
add(hook, name, isRecord(options) ? scalarText(options["runner"]) : undefined);
|
|
132
|
+
}
|
|
133
|
+
jobs(hook, isRecord(value) ? value["jobs"] : undefined);
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
86
137
|
/** The path of each git hook script at `root`, every file in {@link HOOK_DIRS}. */
|
|
87
138
|
function hookScripts(host, root) {
|
|
88
139
|
return HOOK_DIRS.flatMap((dir) => filesIn(host, joinPath(root, dir), () => true).map((name) => joinPath(root, dir, name)));
|
|
@@ -91,10 +142,44 @@ function hookScripts(host, root) {
|
|
|
91
142
|
function makefileAt(host, root) {
|
|
92
143
|
return MAKEFILES.map((name) => joinPath(root, name)).find((candidate) => host.kindOf(candidate) === "file");
|
|
93
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* One recipe line as make hands it to the shell: each `$$` a `$`, and each reference to a
|
|
147
|
+
* variable or a function, which make expands first, a part only the run knows:
|
|
148
|
+
* https://www.gnu.org/software/make/manual/html_node/Reference.html
|
|
149
|
+
*/
|
|
150
|
+
function expandedByMake(line) {
|
|
151
|
+
let expanded = "";
|
|
152
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
153
|
+
const char = line.charAt(at);
|
|
154
|
+
const next = line.charAt(at + 1);
|
|
155
|
+
if (char !== "$") {
|
|
156
|
+
expanded += char;
|
|
157
|
+
}
|
|
158
|
+
else if (next === "$") {
|
|
159
|
+
expanded += "$";
|
|
160
|
+
at += 1;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
let end = at + 1;
|
|
164
|
+
if (next === "(" || next === "{") {
|
|
165
|
+
const close = next === "(" ? ")" : "}";
|
|
166
|
+
for (let depth = 0; end < line.length; end += 1) {
|
|
167
|
+
depth += line.charAt(end) === next ? 1 : line.charAt(end) === close ? -1 : 0;
|
|
168
|
+
if (depth === 0) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
expanded += PART;
|
|
174
|
+
at = Math.min(end, line.length);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return expanded;
|
|
178
|
+
}
|
|
94
179
|
/**
|
|
95
180
|
* Each recipe line of the target root's makefile: a line that starts with a tab, joined with
|
|
96
|
-
* the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it
|
|
97
|
-
* Make runs each line in a shell of its own.
|
|
181
|
+
* the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it,
|
|
182
|
+
* as make expands it. Make runs each line in a shell of its own.
|
|
98
183
|
*/
|
|
99
184
|
function recipeLines(host, root) {
|
|
100
185
|
const path = makefileAt(host, root);
|
|
@@ -104,16 +189,15 @@ function recipeLines(host, root) {
|
|
|
104
189
|
.split(/\r?\n/u)
|
|
105
190
|
.filter((line) => line.startsWith("\t"))
|
|
106
191
|
.map((line) => ({
|
|
107
|
-
text: line.slice(1).replace(/^[@+-]+/u, ""),
|
|
192
|
+
text: expandedByMake(line.slice(1).replace(/^[@+-]+/u, "")),
|
|
108
193
|
dir: root,
|
|
109
|
-
writtenDirectory: undefined,
|
|
110
194
|
}));
|
|
111
195
|
}
|
|
112
196
|
/**
|
|
113
197
|
* Every command the target root's workflow steps, git hooks, hook managers and makefile run:
|
|
114
198
|
* the `run:` steps of the workflow files in `.github/workflows`, each file in `.husky` and
|
|
115
199
|
* `.githooks` as one shell script, the `entry:` commands of `.pre-commit-config.yaml`, the
|
|
116
|
-
* `run:` commands of a lefthook file and the makefile's recipe lines.
|
|
200
|
+
* `run:` commands and the scripts of a lefthook file and the makefile's recipe lines.
|
|
117
201
|
*/
|
|
118
202
|
export function workflowCommands(host, targetRoot) {
|
|
119
203
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
@@ -123,35 +207,125 @@ export function workflowCommands(host, targetRoot) {
|
|
|
123
207
|
...hookScripts(host, root).map((path) => ({
|
|
124
208
|
text: readText(host, path) ?? "",
|
|
125
209
|
dir: root,
|
|
126
|
-
writtenDirectory: undefined,
|
|
127
210
|
})),
|
|
128
|
-
...HOOK_MANAGERS.flatMap((
|
|
211
|
+
...HOOK_MANAGERS.flatMap(({ name, key, dirKey, scripts }) => {
|
|
212
|
+
const text = readText(host, joinPath(root, name)) ?? "";
|
|
213
|
+
return [
|
|
214
|
+
...keyedCommands(text, root, key, dirKey),
|
|
215
|
+
...(scripts === true ? lefthookScripts(text, root) : []),
|
|
216
|
+
];
|
|
217
|
+
}),
|
|
129
218
|
...recipeLines(host, root),
|
|
130
219
|
];
|
|
131
220
|
}
|
|
132
221
|
/**
|
|
133
222
|
* The whole text of each file {@link workflowCommands} reads a command from, each workflow file,
|
|
134
|
-
* git hook, hook manager file and makefile at the target root
|
|
135
|
-
* no command is read from, a comment included, or one the reading of the structure misses, still
|
|
136
|
-
* holds every file it names.
|
|
223
|
+
* git hook, hook manager file and makefile at the target root, which the dependency rule reads.
|
|
137
224
|
*/
|
|
138
225
|
export function commandFileTexts(host, targetRoot) {
|
|
226
|
+
return commandFiles(host, targetRoot).flatMap(({ path }) => readText(host, path) ?? []);
|
|
227
|
+
}
|
|
228
|
+
/** Each file {@link workflowCommands} reads a command from, with the key a YAML file writes its commands under. */
|
|
229
|
+
function commandFiles(host, targetRoot) {
|
|
139
230
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
140
231
|
return [
|
|
141
|
-
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
232
|
+
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) => ({
|
|
233
|
+
path: joinPath(root, WORKFLOWS, name),
|
|
234
|
+
key: "run",
|
|
235
|
+
})),
|
|
236
|
+
...hookScripts(host, root).map((path) => ({ path, key: undefined })),
|
|
237
|
+
...HOOK_MANAGERS.map(({ name, key }) => ({ path: joinPath(root, name), key })),
|
|
238
|
+
...[makefileAt(host, root) ?? []].flat().map((path) => ({ path, key: undefined })),
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The filters of a workflow's `on` key, which name the files and refs whose change triggers it
|
|
243
|
+
* and run none: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onpushpull_requestpull_request_targetpathspaths-ignore
|
|
244
|
+
*/
|
|
245
|
+
const TRIGGER_FILTERS = new Set([
|
|
246
|
+
"paths",
|
|
247
|
+
"paths-ignore",
|
|
248
|
+
"branches",
|
|
249
|
+
"branches-ignore",
|
|
250
|
+
"tags",
|
|
251
|
+
"tags-ignore",
|
|
252
|
+
]);
|
|
253
|
+
/**
|
|
254
|
+
* The values of one YAML file that are no command, each with its key: every scalar but one
|
|
255
|
+
* written under `key`, which is read as a command string, and one of a trigger filter. A file
|
|
256
|
+
* `yamlDocument` does not read is one value, its whole text. No key and no comment is a value.
|
|
257
|
+
*/
|
|
258
|
+
function yamlValues(text, key) {
|
|
259
|
+
const document = yamlDocument(text);
|
|
260
|
+
if (document === undefined) {
|
|
261
|
+
return [{ text, shell: false }];
|
|
262
|
+
}
|
|
263
|
+
const found = [];
|
|
264
|
+
// `trigger` is 1 for the value of the top-level `on`, a map of events, and 2 for an event's.
|
|
265
|
+
const visit = (node, under, trigger = 0) => {
|
|
266
|
+
if (Array.isArray(node)) {
|
|
267
|
+
for (const item of node) {
|
|
268
|
+
visit(item, under);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (isRecord(node)) {
|
|
272
|
+
for (const [name, value] of Object.entries(node)) {
|
|
273
|
+
const on = node === document && (name === "on" || name === "true");
|
|
274
|
+
if ((name !== key || scalarText(value) === undefined) &&
|
|
275
|
+
!(trigger === 2 && TRIGGER_FILTERS.has(name))) {
|
|
276
|
+
visit(value, name, on ? 1 : trigger === 1 ? 2 : 0);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const value = scalarText(node);
|
|
282
|
+
if (value !== undefined) {
|
|
283
|
+
found.push({ text: value, shell: false, ...(under === undefined ? {} : { key: under }) });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
visit(document, undefined);
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The lines of a makefile that are no recipe line, joined with the lines a trailing `\`
|
|
292
|
+
* continues them onto, each without the comment a `#` make does not read as escaped starts.
|
|
293
|
+
*/
|
|
294
|
+
function makefileValues(text) {
|
|
295
|
+
return text
|
|
296
|
+
.replace(/\\\r?\n/gu, " ")
|
|
297
|
+
.split(/\r?\n/u)
|
|
298
|
+
.filter((line) => !line.startsWith("\t"))
|
|
299
|
+
.map((line) => line.replace(/(?<!\\)#.*$/u, ""))
|
|
300
|
+
.filter((line) => line.trim() !== "")
|
|
301
|
+
.map((line) => ({ text: line, shell: false }));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The texts layer 3 reads of the target root's command files: each command string, and each
|
|
305
|
+
* value of a workflow file, a hook manager file and the makefile that is no command.
|
|
306
|
+
*/
|
|
307
|
+
function heldTexts(host, targetRoot) {
|
|
308
|
+
const makefile = makefileAt(host, resolvePath(host.workingDirectory(), targetRoot));
|
|
309
|
+
return [
|
|
310
|
+
...workflowCommands(host, targetRoot).map(({ text }) => ({ text, shell: true })),
|
|
311
|
+
...commandFiles(host, targetRoot).flatMap(({ path, key }) => {
|
|
312
|
+
const text = readText(host, path);
|
|
313
|
+
return text === undefined
|
|
314
|
+
? []
|
|
315
|
+
: key !== undefined
|
|
316
|
+
? yamlValues(text, key)
|
|
317
|
+
: path === makefile
|
|
318
|
+
? makefileValues(text)
|
|
319
|
+
: [];
|
|
320
|
+
}),
|
|
321
|
+
];
|
|
146
322
|
}
|
|
147
323
|
/** Reads the target root's workflow steps, git hooks, hook managers and makefile. */
|
|
148
324
|
export function readWorkflows(host, targetRoot) {
|
|
149
325
|
return {
|
|
150
|
-
readings: workflowCommands(host, targetRoot).map(({ text, dir
|
|
151
|
-
const reading = readCommands(text, dir);
|
|
152
|
-
return { ...reading, held: [reading.held, writtenDirectory ?? ""].join("\n") };
|
|
153
|
-
}),
|
|
326
|
+
readings: workflowCommands(host, targetRoot).map(({ text, dir }) => readCommands(text, dir)),
|
|
154
327
|
texts: commandFileTexts(host, targetRoot),
|
|
328
|
+
held: heldTexts(host, targetRoot),
|
|
155
329
|
};
|
|
156
330
|
}
|
|
157
331
|
/**
|
|
@@ -130,8 +130,14 @@ function fileNeverImported(input) {
|
|
|
130
130
|
: fileFinding(CODE.fileNeverImported, file, "no import reaches this file and no root names it or a declaration in it");
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The findings of the files family, `DS1600` to `DS1699`, each at the class `possible` in a
|
|
135
|
+
* package a command of which may run a file whose path no word of it writes, since that
|
|
136
|
+
* command may run the file (confidence.35).
|
|
137
|
+
*/
|
|
138
|
+
export const nonCodeArtifacts = (input) => {
|
|
139
|
+
const unwritten = [...(input.files.unwrittenRuns ?? [])];
|
|
140
|
+
return [...fileNeverBuilt(input), ...fileNeverImported(input)].map((finding) => unwritten.some((dir) => dir === "." || finding.position.path === dir || finding.position.path.startsWith(`${dir}/`))
|
|
141
|
+
? { ...finding, reachabilityClass: "possible" }
|
|
142
|
+
: finding);
|
|
143
|
+
};
|
|
@@ -27,8 +27,9 @@ function below(member, key) {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* The whole text of nodemon's configuration, the manifest's `nodemonConfig` member and the
|
|
30
|
-
* `nodemon.json` beside it, by the member each is.
|
|
31
|
-
* commands from keys besides `exec`, `events`
|
|
30
|
+
* `nodemon.json` beside it, by the member each is. The dependency rule reads it whole, and
|
|
31
|
+
* layer 3 each of its values, because nodemon runs commands from keys besides `exec`, `events`
|
|
32
|
+
* among them: https://github.com/remy/nodemon#config-files
|
|
32
33
|
*/
|
|
33
34
|
export function nodemonTexts(host, dir, manifest) {
|
|
34
35
|
const member = manifest["nodemonConfig"];
|
|
@@ -44,6 +45,39 @@ export function nodemonTexts(host, dir, manifest) {
|
|
|
44
45
|
...(file === undefined ? [] : [{ member: "nodemon.json", text: file }]),
|
|
45
46
|
];
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The texts layer 3 reads of one text of nodemon's configuration: each value of it but its
|
|
50
|
+
* `exec` command, which is read as a command string of its own, those under `events` read as
|
|
51
|
+
* command strings too, and the whole text where it does not parse. Its keys are never read.
|
|
52
|
+
*/
|
|
53
|
+
function nodemonValues(text) {
|
|
54
|
+
let config;
|
|
55
|
+
try {
|
|
56
|
+
config = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [{ text, shell: false }];
|
|
60
|
+
}
|
|
61
|
+
const found = [];
|
|
62
|
+
const visit = (value, shell) => {
|
|
63
|
+
if (Array.isArray(value) || isRecord(value)) {
|
|
64
|
+
for (const one of Object.values(value)) {
|
|
65
|
+
visit(one, shell);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (typeof value === "string" ||
|
|
69
|
+
typeof value === "number" ||
|
|
70
|
+
typeof value === "boolean") {
|
|
71
|
+
found.push({ text: String(value), shell });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (const [key, value] of Object.entries(isRecord(config) ? config : { "": config })) {
|
|
75
|
+
if (key !== "exec" || typeof value !== "string") {
|
|
76
|
+
visit(value, key === "events");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
47
81
|
/**
|
|
48
82
|
* The command strings one manifest runs, each at the member it is written under: each of its
|
|
49
83
|
* `scripts`, and the `exec` command of nodemon's configuration, in the manifest's `nodemonConfig`
|
|
@@ -141,10 +175,19 @@ export function readManifest(host, targetRoot) {
|
|
|
141
175
|
for (const path of new Set(pathsNamed(readCommands(command, ".")).map((one) => one.path))) {
|
|
142
176
|
found.push({ member, path, role: "script", published: false });
|
|
143
177
|
}
|
|
144
|
-
found.push({
|
|
178
|
+
found.push({
|
|
179
|
+
member,
|
|
180
|
+
path: ".",
|
|
181
|
+
role: "script",
|
|
182
|
+
published: false,
|
|
183
|
+
held: [{ text: command, shell: true, tested: member.startsWith("scripts[") }],
|
|
184
|
+
});
|
|
145
185
|
}
|
|
146
186
|
for (const { member, text } of nodemonTexts(host, targetRoot, manifest)) {
|
|
147
|
-
|
|
187
|
+
const held = nodemonValues(text);
|
|
188
|
+
if (held.length > 0) {
|
|
189
|
+
found.push({ member, path: ".", role: "script", published: false, held });
|
|
190
|
+
}
|
|
148
191
|
}
|
|
149
192
|
return {
|
|
150
193
|
entries: found.map((entry) => ({
|
|
@@ -16,6 +16,7 @@ import { typeQueryAliases } from "../type-query-alias.js";
|
|
|
16
16
|
import { bytewise } from "../text/compare.js";
|
|
17
17
|
import { holdsManifest } from "../fs/manifest.js";
|
|
18
18
|
import { globExpression } from "../text/glob.js";
|
|
19
|
+
import { heldWords } from "../commands/held.js";
|
|
19
20
|
import { spelledFiles } from "../text/named-files.js";
|
|
20
21
|
import { extensionOf, OUTPUT_EXTENSIONS } from "../extensions.js";
|
|
21
22
|
import { publishedAPI, releaseTagged } from "./published-reach.js";
|
|
@@ -159,11 +160,12 @@ export function matchRef(pattern, ref) {
|
|
|
159
160
|
return matchGlob(Array.from(pattern), Array.from(ref));
|
|
160
161
|
}
|
|
161
162
|
/**
|
|
162
|
-
* Per package, the own files its held texts name, as `spelledFiles` reads them,
|
|
163
|
-
* root enters with all they export: a command can use no more of such a file than
|
|
164
|
-
* keeps live. A manifest's are its entries' held texts, and the target root's package
|
|
165
|
-
* workflow steps, git hooks and makefile recipes beside its own, with the
|
|
166
|
-
* file they are read from.
|
|
163
|
+
* Per package, the own files the words of its held texts name, as `spelledFiles` reads them,
|
|
164
|
+
* but those a root enters with all they export: a command can use no more of such a file than
|
|
165
|
+
* the root keeps live. A manifest's are its entries' held texts, and the target root's package
|
|
166
|
+
* has the workflow steps, git hooks and makefile recipes beside its own, with the values of
|
|
167
|
+
* each file they are read from. A package is listed too where a word that may write the file
|
|
168
|
+
* a command runs writes no path text.
|
|
167
169
|
*/
|
|
168
170
|
function scriptHolds(entries, files, entered, host, targetRoot, workflows) {
|
|
169
171
|
const rooted = new Set(entered.filter((one) => one.exports && one.names === undefined).map((one) => one.file.fileName));
|
|
@@ -171,24 +173,20 @@ function scriptHolds(entries, files, entered, host, targetRoot, workflows) {
|
|
|
171
173
|
.filter((file) => !rooted.has(file.fileName))
|
|
172
174
|
.map((file) => ({ file, paths: [file.fileName, ...files.writtenAs(file)] }));
|
|
173
175
|
const cwd = host.workingDirectory();
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
resolvePath(cwd, targetRoot),
|
|
177
|
-
[...workflows.readings.map((reading) => reading.held), ...workflows.texts],
|
|
178
|
-
],
|
|
179
|
-
]);
|
|
176
|
+
const root = resolvePath(cwd, targetRoot);
|
|
177
|
+
const commands = new Map([[root, [...workflows.held]]]);
|
|
180
178
|
for (const entry of entries) {
|
|
181
179
|
if (entry.held !== undefined) {
|
|
182
180
|
const dir = resolvePath(cwd, entry.dir);
|
|
183
|
-
commands.set(dir, [...(commands.get(dir) ?? []), entry.held]);
|
|
181
|
+
commands.set(dir, [...(commands.get(dir) ?? []), ...entry.held]);
|
|
184
182
|
}
|
|
185
183
|
}
|
|
186
184
|
const held = [];
|
|
187
185
|
for (const [dir, texts] of commands) {
|
|
188
|
-
const named = spelledFiles(texts
|
|
189
|
-
if (named.length > 0) {
|
|
190
|
-
const paths = named.map((file) => relativePath(targetRoot, file.fileName) ?? file.fileName);
|
|
191
|
-
held.push({ dir, files: paths });
|
|
186
|
+
const named = spelledFiles(heldWords(texts), dir, root, spelled);
|
|
187
|
+
if (named.files.length > 0 || named.unwritten) {
|
|
188
|
+
const paths = named.files.map((file) => relativePath(targetRoot, file.fileName) ?? file.fileName);
|
|
189
|
+
held.push({ dir, files: paths, unwritten: named.unwritten });
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
return held;
|