@cplieger/deadset-ts 8.0.0-dev.17 → 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 +30 -72
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- 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 +9 -2
- 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/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/non-code-artifacts.js +11 -5
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/manifest.js +47 -4
- package/dist/src/roots/published-reach.js +4 -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/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +54 -108
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- 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 +9 -2
- 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/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/file-facts.ts +5 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/non-code-artifacts.ts +16 -5
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/manifest.ts +53 -8
- package/src/roots/published-reach.ts +9 -4
- package/src/roots/root-set.ts +18 -16
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
|
@@ -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;
|
|
@@ -423,7 +423,7 @@ export const KIND_ROWS = [
|
|
|
423
423
|
holdsLive: true,
|
|
424
424
|
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
|
|
425
425
|
precondition:
|
|
426
|
-
"Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type
|
|
426
|
+
"Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type is not reported when a reference from outside the package reaches its values through its own package's exported declarations: a reference to an exported function or method whose parameters or results name the type, to an exported variable or constant whose type names it, to an exported field or property whose type names it, an embedded field included, or to an exported member of the type itself, a field promoted through an embedding included. A type a reference from outside the package names, or whose values such a reference reaches, passes the reach on to each type its definition names other than through an unexported member, whether it is exported or not, so an unexported type an exported function returns passes it to the types its exported fields name. A type names another through every composite that spells it: a pointer, an array, a slice, a map key or value, a channel, a function signature and a type argument. The referencing code holds values of the type, so unexporting it would leave an exported declaration naming a type its users cannot name. A package-level declaration whose name a string literal spells carries the class grammar/analysis/confidence.md gives it.",
|
|
427
427
|
details: { narrower_visibility: "required", name_literal: "optional" },
|
|
428
428
|
defaultEnabled: true,
|
|
429
429
|
defaultSeverity: "warn",
|
|
@@ -439,7 +439,7 @@ export const KIND_ROWS = [
|
|
|
439
439
|
holdsLive: true,
|
|
440
440
|
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
|
|
441
441
|
precondition:
|
|
442
|
-
"Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type
|
|
442
|
+
"Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type is not reported when a reference from outside the target module reaches its values through its own package's exported declarations, as DS1501 defines that reach, because the referencing code holds values of the type, and moving it behind an internal boundary would leave an exported declaration naming a type its users cannot name.",
|
|
443
443
|
details: { narrower_visibility: "required", name_literal: "optional" },
|
|
444
444
|
defaultEnabled: true,
|
|
445
445
|
defaultSeverity: "warn",
|
|
@@ -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
|
|