@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
|
} as const;
|
|
@@ -1280,6 +1280,13 @@ export const EVIDENCE = [
|
|
|
1280
1280
|
"DS1602",
|
|
1281
1281
|
],
|
|
1282
1282
|
},
|
|
1283
|
+
{
|
|
1284
|
+
evidence: "unwritten-run-path",
|
|
1285
|
+
rule: "confidence.35",
|
|
1286
|
+
class: "possible",
|
|
1287
|
+
languages: ["ts"],
|
|
1288
|
+
codes: ["DS1601", "DS1602"],
|
|
1289
|
+
},
|
|
1283
1290
|
{
|
|
1284
1291
|
evidence: "schema-operation-default",
|
|
1285
1292
|
rule: "schemas.10",
|
package/src/dependencies.ts
CHANGED
|
@@ -519,7 +519,7 @@ interface CommandUses {
|
|
|
519
519
|
readonly words: ReadonlySet<string>;
|
|
520
520
|
/** The package each module a read command preloads by name is in. */
|
|
521
521
|
readonly preloads: ReadonlySet<string>;
|
|
522
|
-
/** The text
|
|
522
|
+
/** The whole text of every string, which the dependency rule reads: each package or command name in it is a use. */
|
|
523
523
|
readonly held: string;
|
|
524
524
|
}
|
|
525
525
|
|
|
@@ -23,13 +23,8 @@ import {
|
|
|
23
23
|
type TestFileReading,
|
|
24
24
|
} from "./convention-rows.ts";
|
|
25
25
|
import { documentStrings } from "./configuration-files.ts";
|
|
26
|
-
import { readCommands } from "../commands/read.ts";
|
|
27
|
-
import {
|
|
28
|
-
nodeTestGlobs,
|
|
29
|
-
type RunnerInput,
|
|
30
|
-
type ScriptCommand,
|
|
31
|
-
type TestGlob,
|
|
32
|
-
} from "./test-runners.ts";
|
|
26
|
+
import { readCommands, type CommandReading } from "../commands/read.ts";
|
|
27
|
+
import { nodeTestGlobs, type RunnerInput, type TestGlob } from "./test-runners.ts";
|
|
33
28
|
import {
|
|
34
29
|
documentModule,
|
|
35
30
|
membersNamed,
|
|
@@ -814,12 +809,12 @@ function packageKeyUses(
|
|
|
814
809
|
);
|
|
815
810
|
}
|
|
816
811
|
|
|
817
|
-
/**
|
|
818
|
-
function
|
|
812
|
+
/** What the command reader reads each of a manifest's `scripts` as, in the manifest's directory. */
|
|
813
|
+
function scriptReadings(manifest: unknown, dir: string): readonly CommandReading[] {
|
|
819
814
|
const scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
820
815
|
return isRecord(scripts)
|
|
821
816
|
? Object.values(scripts).flatMap((text) =>
|
|
822
|
-
typeof text === "string" ? readCommands(text, dir)
|
|
817
|
+
typeof text === "string" ? [readCommands(text, dir)] : [],
|
|
823
818
|
)
|
|
824
819
|
: [];
|
|
825
820
|
}
|
|
@@ -846,6 +841,7 @@ export function readConventions(
|
|
|
846
841
|
const read = readFilesOnce(host, parse);
|
|
847
842
|
for (const manifest of manifests) {
|
|
848
843
|
const declared = readJSON(host, joinPath(manifest.dir, MANIFEST));
|
|
844
|
+
const scripts = scriptReadings(declared, manifest.dir);
|
|
849
845
|
let input: RunnerInput | undefined;
|
|
850
846
|
const runnerInput = (): RunnerInput => {
|
|
851
847
|
input ??= {
|
|
@@ -861,7 +857,7 @@ export function readConventions(
|
|
|
861
857
|
}
|
|
862
858
|
return undefined;
|
|
863
859
|
},
|
|
864
|
-
scripts:
|
|
860
|
+
scripts: scripts.flatMap((one) => one.commands),
|
|
865
861
|
};
|
|
866
862
|
return input;
|
|
867
863
|
};
|
|
@@ -965,7 +961,7 @@ export function readConventions(
|
|
|
965
961
|
});
|
|
966
962
|
}
|
|
967
963
|
}
|
|
968
|
-
testGlobs.push(...nodeTestGlobs(
|
|
964
|
+
testGlobs.push(...nodeTestGlobs(scripts.flatMap((one) => one.tests)));
|
|
969
965
|
}
|
|
970
966
|
const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
|
|
971
967
|
unique.sort(
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* source reads them. A value a reader needs that the text does not write out names no file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { CommandTests } from "../commands/read.ts";
|
|
8
9
|
import { readJSON, readJSONFile } from "../fs/manifest.ts";
|
|
9
10
|
import type { Host } from "../host.ts";
|
|
10
11
|
import { ancestors, dirnamePath, joinPath, relativePath, resolvePath } from "../paths.ts";
|
|
@@ -27,7 +28,7 @@ export interface TestGlob {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** One simple command of a manifest's scripts, a dynamic word `undefined`, in the directory it runs in. */
|
|
30
|
-
|
|
31
|
+
interface ScriptCommand {
|
|
31
32
|
readonly dir: string;
|
|
32
33
|
readonly words: readonly (string | undefined)[];
|
|
33
34
|
}
|
|
@@ -140,8 +141,6 @@ interface OptionTable {
|
|
|
140
141
|
/** The options that take the next word, or the text after `=`, as their value. */
|
|
141
142
|
readonly valued: ReadonlySet<string>;
|
|
142
143
|
readonly flags: ReadonlySet<string>;
|
|
143
|
-
/** Whether a long option the table does not name is read where its value follows `=`. */
|
|
144
|
-
readonly assigned?: boolean;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
/** One command's options and operands. */
|
|
@@ -182,7 +181,7 @@ function parsed(words: readonly (string | undefined)[], table: OptionTable): Par
|
|
|
182
181
|
keep(name, value);
|
|
183
182
|
} else if (table.flags.has(name)) {
|
|
184
183
|
keep(name, equals < 0 ? "true" : word.slice(equals + 1));
|
|
185
|
-
} else
|
|
184
|
+
} else {
|
|
186
185
|
return undefined;
|
|
187
186
|
}
|
|
188
187
|
}
|
|
@@ -813,32 +812,6 @@ export const AVA_6 = avaRunner(AVA_CONFIG, ["cjs", "mjs", "js"]);
|
|
|
813
812
|
/** AVA 8, which reads no `ava.config.cjs` and defaults to no `.cjs` test. */
|
|
814
813
|
export const AVA_8 = avaRunner(["ava.config.js", "ava.config.mjs"], ["mjs", "js"]);
|
|
815
814
|
|
|
816
|
-
/**
|
|
817
|
-
* The options of `node` (https://nodejs.org/api/cli.html) a test run writes: the ones that take
|
|
818
|
-
* a value, the flags, and any long option whose value follows `=`.
|
|
819
|
-
*/
|
|
820
|
-
const NODE_OPTIONS: OptionTable = {
|
|
821
|
-
valued: new Set([
|
|
822
|
-
...["-r", "--require", "--import", "--loader", "--experimental-loader", "--env-file"],
|
|
823
|
-
...["--env-file-if-exists", "--conditions", "-C", "--input-type", "--watch-path"],
|
|
824
|
-
...["--test-reporter", "--test-reporter-destination", "--test-concurrency"],
|
|
825
|
-
...["--test-name-pattern", "--test-skip-pattern", "--test-timeout", "--test-shard"],
|
|
826
|
-
...["--test-coverage-include", "--test-coverage-exclude", "--test-coverage-branches"],
|
|
827
|
-
...["--test-coverage-functions", "--test-coverage-lines", "--test-isolation"],
|
|
828
|
-
...["--test-global-setup"],
|
|
829
|
-
]),
|
|
830
|
-
flags: new Set([
|
|
831
|
-
...["--test", "--test-only", "--watch", "--test-force-exit", "--test-update-snapshots"],
|
|
832
|
-
...["--test-randomize", "--experimental-test-coverage", "--experimental-test-module-mocks"],
|
|
833
|
-
...["--experimental-test-snapshots", "--experimental-strip-types", "--no-strip-types"],
|
|
834
|
-
...["--no-experimental-strip-types", "--experimental-transform-types", "--enable-source-maps"],
|
|
835
|
-
...["--experimental-vm-modules", "--experimental-detect-module", "--no-warnings"],
|
|
836
|
-
...["--no-experimental-detect-module", "--experimental-require-module", "--trace-warnings"],
|
|
837
|
-
...["--no-experimental-require-module", "--expose-gc", "--inspect", "--inspect-brk"],
|
|
838
|
-
]),
|
|
839
|
-
assigned: true,
|
|
840
|
-
};
|
|
841
|
-
|
|
842
815
|
/** The patterns `node --test` runs with no operand (`doc/api/test.md`), the TypeScript ones where types are stripped. */
|
|
843
816
|
function nodeTestDefaults(stripped: boolean): readonly string[] {
|
|
844
817
|
const forms = ["**/*.test", "**/*-test", "**/*_test", "**/test-*", "**/test", "**/test/**/*"];
|
|
@@ -849,20 +822,14 @@ function nodeTestDefaults(stripped: boolean): readonly string[] {
|
|
|
849
822
|
}
|
|
850
823
|
|
|
851
824
|
/**
|
|
852
|
-
* The files each `node --test`
|
|
853
|
-
* read in the command's directory, or the default patterns where
|
|
854
|
-
*
|
|
825
|
+
* The files each `node --test` run of a manifest's scripts runs, as the command reader reads
|
|
826
|
+
* it: its operands, each a glob read in the command's directory, or the default patterns where
|
|
827
|
+
* it writes none.
|
|
855
828
|
*/
|
|
856
|
-
export function nodeTestGlobs(
|
|
857
|
-
return
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
const stripped =
|
|
864
|
-
!read.values.has("--no-strip-types") && !read.values.has("--no-experimental-strip-types");
|
|
865
|
-
const patterns = read.operands.length > 0 ? read.operands : nodeTestDefaults(stripped);
|
|
866
|
-
return patterns.map((one) => belowGlob(command.dir, one));
|
|
867
|
-
});
|
|
829
|
+
export function nodeTestGlobs(runs: readonly CommandTests[]): readonly TestGlob[] {
|
|
830
|
+
return runs.flatMap((run) =>
|
|
831
|
+
(run.patterns.length > 0 ? run.patterns : nodeTestDefaults(run.stripped)).map((one) =>
|
|
832
|
+
belowGlob(run.dir, one),
|
|
833
|
+
),
|
|
834
|
+
);
|
|
868
835
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
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
|
|
|
8
8
|
import type { SourceFile } from "@typescript/native/ast";
|
|
9
|
+
import type { HeldText } from "../commands/held.ts";
|
|
9
10
|
import { pathsNamed, readCommands, type CommandReading } from "../commands/read.ts";
|
|
11
|
+
import { PART } from "../commands/shell.ts";
|
|
10
12
|
import type { Host } from "../host.ts";
|
|
11
13
|
import { isRecord } from "../json-document.ts";
|
|
12
14
|
import { joinPath, resolvePath } from "../paths.ts";
|
|
@@ -29,8 +31,6 @@ interface CommandString {
|
|
|
29
31
|
* where: a directory holding a `${{ }}` expression, which GitHub Actions evaluates.
|
|
30
32
|
*/
|
|
31
33
|
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
34
|
}
|
|
35
35
|
|
|
36
36
|
const WORKFLOWS = ".github/workflows";
|
|
@@ -41,16 +41,32 @@ const HOOK_DIRS = [".husky", ".githooks"] as const;
|
|
|
41
41
|
/** The key setting the directory a workflow step, or each step a `defaults.run` covers, runs in. */
|
|
42
42
|
const WORKING_DIRECTORY = "working-directory";
|
|
43
43
|
|
|
44
|
+
/** One hook manager's file. */
|
|
45
|
+
interface HookManager {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** The key its commands are written under. */
|
|
48
|
+
readonly key: string;
|
|
49
|
+
/** The key of a command's directory: lefthook's `root`, https://lefthook.dev/configuration/root/ */
|
|
50
|
+
readonly dirKey?: string;
|
|
51
|
+
/** Whether its hooks name script files it runs, as lefthook's do. */
|
|
52
|
+
readonly scripts?: true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HOOK_MANAGERS: readonly HookManager[] = [
|
|
56
|
+
{ name: ".pre-commit-config.yaml", key: "entry" },
|
|
57
|
+
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map((name): HookManager => ({
|
|
58
|
+
name,
|
|
59
|
+
key: "run",
|
|
60
|
+
dirKey: "root",
|
|
61
|
+
scripts: true,
|
|
62
|
+
})),
|
|
63
|
+
];
|
|
64
|
+
|
|
44
65
|
/**
|
|
45
|
-
* The
|
|
46
|
-
*
|
|
66
|
+
* The directories lefthook searches for a hook's scripts after the file's `source_dir` and
|
|
67
|
+
* `source_dir_local`: https://github.com/evilmartians/lefthook/blob/master/internal/command/run.go
|
|
47
68
|
*/
|
|
48
|
-
const
|
|
49
|
-
[".pre-commit-config.yaml", "entry", undefined],
|
|
50
|
-
...["lefthook.yml", "lefthook.yaml", ".lefthook.yml"].map(
|
|
51
|
-
(name) => [name, "run", "root"] as const,
|
|
52
|
-
),
|
|
53
|
-
];
|
|
69
|
+
const LEFTHOOK_DIRS = [".config/lefthook", ".config/lefthook-local"] as const;
|
|
54
70
|
|
|
55
71
|
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
56
72
|
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"] as const;
|
|
@@ -112,7 +128,6 @@ function keyedCommands(
|
|
|
112
128
|
: written.includes("${{")
|
|
113
129
|
? undefined
|
|
114
130
|
: resolvePath(root, written),
|
|
115
|
-
writtenDirectory: written,
|
|
116
131
|
});
|
|
117
132
|
}
|
|
118
133
|
for (const value of Object.values(node)) {
|
|
@@ -123,6 +138,52 @@ function keyedCommands(
|
|
|
123
138
|
return found;
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The command lefthook runs for each script a lefthook file's hooks name, each key of a hook's
|
|
143
|
+
* `scripts` and each `script` of a job in its `jobs`, a group's among them, from each directory
|
|
144
|
+
* it searches: its `runner`, then the path `<dir>/<hook>/<name>` quoted as lefthook quotes it,
|
|
145
|
+
* run in the target root: https://lefthook.dev/configuration/Scripts/
|
|
146
|
+
*/
|
|
147
|
+
function lefthookScripts(text: string, root: string): readonly CommandString[] {
|
|
148
|
+
const document = yamlDocument(text);
|
|
149
|
+
if (!isRecord(document)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const dirs = [
|
|
153
|
+
scalarText(document["source_dir"]) ?? ".lefthook/",
|
|
154
|
+
scalarText(document["source_dir_local"]) ?? ".lefthook-local/",
|
|
155
|
+
...LEFTHOOK_DIRS,
|
|
156
|
+
];
|
|
157
|
+
const found: CommandString[] = [];
|
|
158
|
+
const add = (hook: string, name: string, runner: string | undefined): void => {
|
|
159
|
+
for (const dir of dirs) {
|
|
160
|
+
const path = `'${joinPath(dir, hook, name).replaceAll("'", "'\\''")}'`;
|
|
161
|
+
found.push({
|
|
162
|
+
text: runner === undefined || runner.trim() === "" ? path : `${runner} ${path}`,
|
|
163
|
+
dir: root,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const jobs = (hook: string, list: unknown): void => {
|
|
168
|
+
for (const job of Array.isArray(list) ? list : []) {
|
|
169
|
+
const script = isRecord(job) ? scalarText(job["script"]) : undefined;
|
|
170
|
+
if (isRecord(job) && script !== undefined) {
|
|
171
|
+
add(hook, script, scalarText(job["runner"]));
|
|
172
|
+
}
|
|
173
|
+
const group = isRecord(job) ? job["group"] : undefined;
|
|
174
|
+
jobs(hook, isRecord(group) ? group["jobs"] : undefined);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
for (const [hook, value] of Object.entries(document)) {
|
|
178
|
+
const scripts = isRecord(value) ? value["scripts"] : undefined;
|
|
179
|
+
for (const [name, options] of Object.entries(isRecord(scripts) ? scripts : {})) {
|
|
180
|
+
add(hook, name, isRecord(options) ? scalarText(options["runner"]) : undefined);
|
|
181
|
+
}
|
|
182
|
+
jobs(hook, isRecord(value) ? value["jobs"] : undefined);
|
|
183
|
+
}
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
|
|
126
187
|
/** The path of each git hook script at `root`, every file in {@link HOOK_DIRS}. */
|
|
127
188
|
function hookScripts(host: Host, root: string): readonly string[] {
|
|
128
189
|
return HOOK_DIRS.flatMap((dir) =>
|
|
@@ -137,10 +198,43 @@ function makefileAt(host: Host, root: string): string | undefined {
|
|
|
137
198
|
);
|
|
138
199
|
}
|
|
139
200
|
|
|
201
|
+
/**
|
|
202
|
+
* One recipe line as make hands it to the shell: each `$$` a `$`, and each reference to a
|
|
203
|
+
* variable or a function, which make expands first, a part only the run knows:
|
|
204
|
+
* https://www.gnu.org/software/make/manual/html_node/Reference.html
|
|
205
|
+
*/
|
|
206
|
+
function expandedByMake(line: string): string {
|
|
207
|
+
let expanded = "";
|
|
208
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
209
|
+
const char = line.charAt(at);
|
|
210
|
+
const next = line.charAt(at + 1);
|
|
211
|
+
if (char !== "$") {
|
|
212
|
+
expanded += char;
|
|
213
|
+
} else if (next === "$") {
|
|
214
|
+
expanded += "$";
|
|
215
|
+
at += 1;
|
|
216
|
+
} else {
|
|
217
|
+
let end = at + 1;
|
|
218
|
+
if (next === "(" || next === "{") {
|
|
219
|
+
const close = next === "(" ? ")" : "}";
|
|
220
|
+
for (let depth = 0; end < line.length; end += 1) {
|
|
221
|
+
depth += line.charAt(end) === next ? 1 : line.charAt(end) === close ? -1 : 0;
|
|
222
|
+
if (depth === 0) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
expanded += PART;
|
|
228
|
+
at = Math.min(end, line.length);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return expanded;
|
|
232
|
+
}
|
|
233
|
+
|
|
140
234
|
/**
|
|
141
235
|
* 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.
|
|
236
|
+
* the lines a trailing `\` continues it onto, less the `@`, `-` and `+` make reads before it,
|
|
237
|
+
* as make expands it. Make runs each line in a shell of its own.
|
|
144
238
|
*/
|
|
145
239
|
function recipeLines(host: Host, root: string): readonly CommandString[] {
|
|
146
240
|
const path = makefileAt(host, root);
|
|
@@ -150,9 +244,8 @@ function recipeLines(host: Host, root: string): readonly CommandString[] {
|
|
|
150
244
|
.split(/\r?\n/u)
|
|
151
245
|
.filter((line) => line.startsWith("\t"))
|
|
152
246
|
.map((line) => ({
|
|
153
|
-
text: line.slice(1).replace(/^[@+-]+/u, ""),
|
|
247
|
+
text: expandedByMake(line.slice(1).replace(/^[@+-]+/u, "")),
|
|
154
248
|
dir: root,
|
|
155
|
-
writtenDirectory: undefined,
|
|
156
249
|
}));
|
|
157
250
|
}
|
|
158
251
|
|
|
@@ -160,7 +253,7 @@ function recipeLines(host: Host, root: string): readonly CommandString[] {
|
|
|
160
253
|
* Every command the target root's workflow steps, git hooks, hook managers and makefile run:
|
|
161
254
|
* the `run:` steps of the workflow files in `.github/workflows`, each file in `.husky` and
|
|
162
255
|
* `.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.
|
|
256
|
+
* `run:` commands and the scripts of a lefthook file and the makefile's recipe lines.
|
|
164
257
|
*/
|
|
165
258
|
export function workflowCommands(host: Host, targetRoot: string): readonly CommandString[] {
|
|
166
259
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
@@ -177,49 +270,145 @@ export function workflowCommands(host: Host, targetRoot: string): readonly Comma
|
|
|
177
270
|
...hookScripts(host, root).map((path): CommandString => ({
|
|
178
271
|
text: readText(host, path) ?? "",
|
|
179
272
|
dir: root,
|
|
180
|
-
writtenDirectory: undefined,
|
|
181
273
|
})),
|
|
182
|
-
...HOOK_MANAGERS.flatMap((
|
|
183
|
-
|
|
184
|
-
|
|
274
|
+
...HOOK_MANAGERS.flatMap(({ name, key, dirKey, scripts }) => {
|
|
275
|
+
const text = readText(host, joinPath(root, name)) ?? "";
|
|
276
|
+
return [
|
|
277
|
+
...keyedCommands(text, root, key, dirKey),
|
|
278
|
+
...(scripts === true ? lefthookScripts(text, root) : []),
|
|
279
|
+
];
|
|
280
|
+
}),
|
|
185
281
|
...recipeLines(host, root),
|
|
186
282
|
];
|
|
187
283
|
}
|
|
188
284
|
|
|
189
285
|
/**
|
|
190
286
|
* 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
|
|
192
|
-
* no command is read from, a comment included, or one the reading of the structure misses, still
|
|
193
|
-
* holds every file it names.
|
|
287
|
+
* git hook, hook manager file and makefile at the target root, which the dependency rule reads.
|
|
194
288
|
*/
|
|
195
289
|
export function commandFileTexts(host: Host, targetRoot: string): readonly string[] {
|
|
290
|
+
return commandFiles(host, targetRoot).flatMap(({ path }) => readText(host, path) ?? []);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Each file {@link workflowCommands} reads a command from, with the key a YAML file writes its commands under. */
|
|
294
|
+
function commandFiles(
|
|
295
|
+
host: Host,
|
|
296
|
+
targetRoot: string,
|
|
297
|
+
): readonly { readonly path: string; readonly key: string | undefined }[] {
|
|
196
298
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
197
299
|
return [
|
|
198
|
-
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) =>
|
|
199
|
-
joinPath(root, WORKFLOWS, name),
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
...
|
|
203
|
-
...
|
|
204
|
-
|
|
300
|
+
...filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name)).map((name) => ({
|
|
301
|
+
path: joinPath(root, WORKFLOWS, name),
|
|
302
|
+
key: "run",
|
|
303
|
+
})),
|
|
304
|
+
...hookScripts(host, root).map((path) => ({ path, key: undefined })),
|
|
305
|
+
...HOOK_MANAGERS.map(({ name, key }) => ({ path: joinPath(root, name), key })),
|
|
306
|
+
...[makefileAt(host, root) ?? []].flat().map((path) => ({ path, key: undefined })),
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The filters of a workflow's `on` key, which name the files and refs whose change triggers it
|
|
312
|
+
* and run none: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onpushpull_requestpull_request_targetpathspaths-ignore
|
|
313
|
+
*/
|
|
314
|
+
const TRIGGER_FILTERS = new Set([
|
|
315
|
+
"paths",
|
|
316
|
+
"paths-ignore",
|
|
317
|
+
"branches",
|
|
318
|
+
"branches-ignore",
|
|
319
|
+
"tags",
|
|
320
|
+
"tags-ignore",
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The values of one YAML file that are no command, each with its key: every scalar but one
|
|
325
|
+
* written under `key`, which is read as a command string, and one of a trigger filter. A file
|
|
326
|
+
* `yamlDocument` does not read is one value, its whole text. No key and no comment is a value.
|
|
327
|
+
*/
|
|
328
|
+
function yamlValues(text: string, key: string): readonly HeldText[] {
|
|
329
|
+
const document = yamlDocument(text);
|
|
330
|
+
if (document === undefined) {
|
|
331
|
+
return [{ text, shell: false }];
|
|
332
|
+
}
|
|
333
|
+
const found: HeldText[] = [];
|
|
334
|
+
// `trigger` is 1 for the value of the top-level `on`, a map of events, and 2 for an event's.
|
|
335
|
+
const visit = (node: unknown, under: string | undefined, trigger = 0): void => {
|
|
336
|
+
if (Array.isArray(node)) {
|
|
337
|
+
for (const item of node) {
|
|
338
|
+
visit(item, under);
|
|
339
|
+
}
|
|
340
|
+
} else if (isRecord(node)) {
|
|
341
|
+
for (const [name, value] of Object.entries(node)) {
|
|
342
|
+
const on = node === document && (name === "on" || name === "true");
|
|
343
|
+
if (
|
|
344
|
+
(name !== key || scalarText(value) === undefined) &&
|
|
345
|
+
!(trigger === 2 && TRIGGER_FILTERS.has(name))
|
|
346
|
+
) {
|
|
347
|
+
visit(value, name, on ? 1 : trigger === 1 ? 2 : 0);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
const value = scalarText(node);
|
|
352
|
+
if (value !== undefined) {
|
|
353
|
+
found.push({ text: value, shell: false, ...(under === undefined ? {} : { key: under }) });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
visit(document, undefined);
|
|
358
|
+
return found;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The lines of a makefile that are no recipe line, joined with the lines a trailing `\`
|
|
363
|
+
* continues them onto, each without the comment a `#` make does not read as escaped starts.
|
|
364
|
+
*/
|
|
365
|
+
function makefileValues(text: string): readonly HeldText[] {
|
|
366
|
+
return text
|
|
367
|
+
.replace(/\\\r?\n/gu, " ")
|
|
368
|
+
.split(/\r?\n/u)
|
|
369
|
+
.filter((line) => !line.startsWith("\t"))
|
|
370
|
+
.map((line) => line.replace(/(?<!\\)#.*$/u, ""))
|
|
371
|
+
.filter((line) => line.trim() !== "")
|
|
372
|
+
.map((line) => ({ text: line, shell: false }));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The texts layer 3 reads of the target root's command files: each command string, and each
|
|
377
|
+
* value of a workflow file, a hook manager file and the makefile that is no command.
|
|
378
|
+
*/
|
|
379
|
+
function heldTexts(host: Host, targetRoot: string): readonly HeldText[] {
|
|
380
|
+
const makefile = makefileAt(host, resolvePath(host.workingDirectory(), targetRoot));
|
|
381
|
+
return [
|
|
382
|
+
...workflowCommands(host, targetRoot).map(({ text }) => ({ text, shell: true })),
|
|
383
|
+
...commandFiles(host, targetRoot).flatMap(({ path, key }) => {
|
|
384
|
+
const text = readText(host, path);
|
|
385
|
+
return text === undefined
|
|
386
|
+
? []
|
|
387
|
+
: key !== undefined
|
|
388
|
+
? yamlValues(text, key)
|
|
389
|
+
: path === makefile
|
|
390
|
+
? makefileValues(text)
|
|
391
|
+
: [];
|
|
392
|
+
}),
|
|
393
|
+
];
|
|
205
394
|
}
|
|
206
395
|
|
|
207
396
|
/** What the target root's workflow steps, git hooks, hook managers and makefile run, read once per run. */
|
|
208
397
|
export interface Workflows {
|
|
209
|
-
/** Each command read
|
|
398
|
+
/** Each command read. */
|
|
210
399
|
readonly readings: readonly CommandReading[];
|
|
211
|
-
/** The whole text of each file a command is read from. */
|
|
400
|
+
/** The whole text of each file a command is read from, which the dependency rule reads. */
|
|
212
401
|
readonly texts: readonly string[];
|
|
402
|
+
/** What layer 3 reads of them: each command string, and each value that is no command. */
|
|
403
|
+
readonly held: readonly HeldText[];
|
|
213
404
|
}
|
|
214
405
|
|
|
215
406
|
/** Reads the target root's workflow steps, git hooks, hook managers and makefile. */
|
|
216
407
|
export function readWorkflows(host: Host, targetRoot: string): Workflows {
|
|
217
408
|
return {
|
|
218
|
-
readings: workflowCommands(host, targetRoot).map(({ text, dir
|
|
219
|
-
const reading = readCommands(text, dir);
|
|
220
|
-
return { ...reading, held: [reading.held, writtenDirectory ?? ""].join("\n") };
|
|
221
|
-
}),
|
|
409
|
+
readings: workflowCommands(host, targetRoot).map(({ text, dir }) => readCommands(text, dir)),
|
|
222
410
|
texts: commandFileTexts(host, targetRoot),
|
|
411
|
+
held: heldTexts(host, targetRoot),
|
|
223
412
|
};
|
|
224
413
|
}
|
|
225
414
|
|
package/src/file-facts.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface FileFacts {
|
|
|
54
54
|
readonly inOperands?: ReadonlySet<string>;
|
|
55
55
|
/** The loads of production files whose target the analysis cannot resolve. */
|
|
56
56
|
readonly unseenLoads?: readonly UnseenLoad[];
|
|
57
|
+
/**
|
|
58
|
+
* The directories below the target root, `.` for the root, of the packages a command of
|
|
59
|
+
* which may run a file whose path no word of it writes (`roots.13`).
|
|
60
|
+
*/
|
|
61
|
+
readonly unwrittenRuns?: ReadonlySet<string>;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
@@ -164,8 +164,19 @@ function fileNeverImported(input: EmitterInput): readonly Finding[] {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The findings of the files family, `DS1600` to `DS1699`, each at the class `possible` in a
|
|
169
|
+
* package a command of which may run a file whose path no word of it writes, since that
|
|
170
|
+
* command may run the file (confidence.35).
|
|
171
|
+
*/
|
|
172
|
+
export const nonCodeArtifacts: Emitter = (input) => {
|
|
173
|
+
const unwritten = [...(input.files.unwrittenRuns ?? [])];
|
|
174
|
+
return [...fileNeverBuilt(input), ...fileNeverImported(input)].map((finding) =>
|
|
175
|
+
unwritten.some(
|
|
176
|
+
(dir) =>
|
|
177
|
+
dir === "." || finding.position.path === dir || finding.position.path.startsWith(`${dir}/`),
|
|
178
|
+
)
|
|
179
|
+
? { ...finding, reachabilityClass: "possible" as const }
|
|
180
|
+
: finding,
|
|
181
|
+
);
|
|
182
|
+
};
|