@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
|
};
|
|
@@ -372,7 +372,7 @@ export const KIND_ROWS = [
|
|
|
372
372
|
subject: "declaration",
|
|
373
373
|
holdsLive: true,
|
|
374
374
|
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.",
|
|
375
|
-
precondition: "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
|
|
375
|
+
precondition: "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.",
|
|
376
376
|
details: { narrower_visibility: "required", name_literal: "optional" },
|
|
377
377
|
defaultEnabled: true,
|
|
378
378
|
defaultSeverity: "warn",
|
|
@@ -387,7 +387,7 @@ export const KIND_ROWS = [
|
|
|
387
387
|
subject: "declaration",
|
|
388
388
|
holdsLive: true,
|
|
389
389
|
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.",
|
|
390
|
-
precondition: "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
|
|
390
|
+
precondition: "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.",
|
|
391
391
|
details: { narrower_visibility: "required", name_literal: "optional" },
|
|
392
392
|
defaultEnabled: true,
|
|
393
393
|
defaultSeverity: "warn",
|
|
@@ -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
|
/**
|
package/dist/src/exempt.js
CHANGED
|
@@ -10,7 +10,6 @@ import { EXEMPTION_CLASSES } from "./contract/contract.generated.js";
|
|
|
10
10
|
import { TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
|
|
11
11
|
import { byPosition, positionKey } from "./position.js";
|
|
12
12
|
import { Unanswerable } from "./query.js";
|
|
13
|
-
import { sweep } from "./sweep.js";
|
|
14
13
|
import { bytewise } from "./text/compare.js";
|
|
15
14
|
/**
|
|
16
15
|
* The classes `exemptions.disabled` switches off. A class that runs on another language
|
|
@@ -130,13 +129,6 @@ export function exemptionsOf(found) {
|
|
|
130
129
|
return true;
|
|
131
130
|
});
|
|
132
131
|
}
|
|
133
|
-
/**
|
|
134
|
-
* The records that hold while the component files at `live` are live: every record that
|
|
135
|
-
* holds whatever is live, and each one a live file's markup holds, as a record of the run.
|
|
136
|
-
*/
|
|
137
|
-
export function holdingWhile(records, live) {
|
|
138
|
-
return exemptionsOf(records.flatMap(({ whileLive, ...record }) => whileLive === undefined || live.has(whileLive) ? [record] : []));
|
|
139
|
-
}
|
|
140
132
|
/** The records per declaration, each declaration's in the order given. */
|
|
141
133
|
function byDeclaration(records) {
|
|
142
134
|
const grouped = new Map();
|
|
@@ -151,24 +143,6 @@ function byDeclaration(records) {
|
|
|
151
143
|
}
|
|
152
144
|
return grouped;
|
|
153
145
|
}
|
|
154
|
-
/**
|
|
155
|
-
* The records that held back a declaration: one some configuration's graph, swept
|
|
156
|
-
* again with no exemption and its marks, judges a candidate, whatever another
|
|
157
|
-
* configuration answers, or one in `writeOnly`, which the run would report as written
|
|
158
|
-
* and never read without the record's read. A declaration a relation or a mark holds
|
|
159
|
-
* live in every configuration is held back by no record. The order is the
|
|
160
|
-
* declarations' site order, and for one declaration the input's.
|
|
161
|
-
*/
|
|
162
|
-
export function retainedIn(matrix, input, writeOnly = new Set()) {
|
|
163
|
-
const exempt = input.exempt ?? [];
|
|
164
|
-
if (exempt.length === 0) {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
const bare = { marked: input.marked, mode: input.mode };
|
|
168
|
-
const dead = new Set(matrix.graphs.flatMap((graph) => sweep(graph, bare).candidates.map((candidate) => candidate.id)));
|
|
169
|
-
const held = byDeclaration(exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)));
|
|
170
|
-
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
171
|
-
}
|
|
172
146
|
/**
|
|
173
147
|
* One line per held-back declaration, in site order: its reference and position, then
|
|
174
148
|
* each record's class, site and detail, all separated by tabs.
|