@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4

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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +61 -10
  3. package/dist/src/augmentations.js +63 -1
  4. package/dist/src/build-scripts.js +437 -0
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +56 -9
  7. package/dist/src/convention-rows.js +39 -2
  8. package/dist/src/conventions.js +15 -0
  9. package/dist/src/dependencies.js +98 -1
  10. package/dist/src/discover.js +14 -82
  11. package/dist/src/entry-points.js +9 -18
  12. package/dist/src/exempt.js +8 -0
  13. package/dist/src/findings/completion.js +66 -28
  14. package/dist/src/findings/intra-function.js +4 -2
  15. package/dist/src/findings/reads-and-writes.js +7 -6
  16. package/dist/src/findings/unused-declarations.js +4 -1
  17. package/dist/src/graph.js +5 -3
  18. package/dist/src/intra-function-parts.js +45 -17
  19. package/dist/src/kinds.js +8 -8
  20. package/dist/src/manifest.js +18 -0
  21. package/dist/src/matrix.js +38 -4
  22. package/dist/src/query.js +1 -0
  23. package/dist/src/report.js +23 -4
  24. package/dist/src/reporters/annotations.js +15 -8
  25. package/dist/src/reporters/sarif.js +12 -8
  26. package/dist/src/roots.js +197 -42
  27. package/dist/src/serialization-contract.js +590 -12
  28. package/dist/src/sweep.js +7 -9
  29. package/dist/src/test-globals.js +234 -0
  30. package/dist/src/type-errors.js +11 -3
  31. package/dist/src/unseen-loads.js +107 -0
  32. package/dist/src/verbs/analyze.js +24 -3
  33. package/dist/src/workflow-steps.js +32 -15
  34. package/dist/src/yaml-document.js +355 -0
  35. package/jsr.json +1 -1
  36. package/package.json +2 -2
  37. package/src/analysis.ts +109 -12
  38. package/src/augmentations.ts +77 -2
  39. package/src/build-scripts.ts +564 -0
  40. package/src/conformance-record.ts +1 -1
  41. package/src/contextual-uses.ts +64 -12
  42. package/src/convention-rows.ts +47 -2
  43. package/src/conventions.ts +16 -0
  44. package/src/dependencies.ts +110 -1
  45. package/src/discover.ts +14 -94
  46. package/src/entry-points.ts +10 -25
  47. package/src/exempt.ts +16 -0
  48. package/src/file-facts.ts +3 -0
  49. package/src/findings/boundary.ts +2 -0
  50. package/src/findings/completion.ts +76 -30
  51. package/src/findings/intra-function.ts +4 -2
  52. package/src/findings/reads-and-writes.ts +7 -6
  53. package/src/findings/unused-declarations.ts +6 -1
  54. package/src/graph.ts +5 -3
  55. package/src/intra-function-parts.ts +55 -16
  56. package/src/kinds.ts +8 -8
  57. package/src/manifest.ts +21 -3
  58. package/src/matrix.ts +51 -4
  59. package/src/query.ts +11 -0
  60. package/src/report.ts +56 -16
  61. package/src/reporters/annotations.ts +21 -10
  62. package/src/reporters/reporter.ts +1 -3
  63. package/src/reporters/sarif.ts +16 -8
  64. package/src/roots.ts +226 -46
  65. package/src/serialization-contract.ts +758 -12
  66. package/src/sweep.ts +23 -9
  67. package/src/test-globals.ts +275 -0
  68. package/src/type-errors.ts +14 -3
  69. package/src/unseen-loads.ts +145 -0
  70. package/src/verbs/analyze.ts +28 -3
  71. package/src/workflow-steps.ts +35 -17
  72. package/src/yaml-document.ts +377 -0
package/dist/src/sweep.js CHANGED
@@ -84,10 +84,10 @@ export function runnerCalls(symbol) {
84
84
  * That run's roots are the graph's own and, of each test file, its top level and every
85
85
  * function it exports, which a test runner may call, and every export of a manual mock,
86
86
  * which the runner reads; any other export of a test file, and test-support code, which
87
- * runs where a test imports it, are judged as any file's are.
88
- * `held` seeds it as a mark or an exemption seeds a sweep.
87
+ * runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
88
+ * seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
89
89
  */
90
- export function reachedWithTheTests(graph, held) {
90
+ export function reachedWithTheTests(graph, held, namespaces = true) {
91
91
  const tests = graph.symbols.flatMap((symbol, at) => {
92
92
  const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
93
93
  const called = exported && runnerCalls(symbol);
@@ -97,9 +97,7 @@ export function reachedWithTheTests(graph, held) {
97
97
  ? [{ at, kind: "test-runner" }]
98
98
  : [];
99
99
  });
100
- return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
101
- production: false,
102
- });
100
+ return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), { production: false }, namespaces);
103
101
  }
104
102
  /**
105
103
  * The references a loaded consumer makes that the mode counts. A production mode counts
@@ -126,7 +124,7 @@ function flagged(graph, ids) {
126
124
  * included, while a root reaches through the references the mode counts. The held seed
127
125
  * runs first, so what it reaches is expanded under the wider rule.
128
126
  */
129
- function reachable(graph, held, mode) {
127
+ function reachable(graph, held, mode, namespaces = true) {
130
128
  const reached = graph.symbols.map(() => false);
131
129
  const expanded = graph.symbols.map(() => false);
132
130
  const queue = [];
@@ -154,7 +152,7 @@ function reachable(graph, held, mode) {
154
152
  if (!holding && mode.production && edge.test) {
155
153
  continue;
156
154
  }
157
- enter(edge.to, !edge.evaluation);
155
+ enter(edge.to, namespaces && !edge.evaluation);
158
156
  }
159
157
  }
160
158
  };
@@ -169,7 +167,7 @@ function reachable(graph, held, mode) {
169
167
  }
170
168
  // A consumer's reference reaches what it names as one of the target's own does.
171
169
  for (const edge of callsOf(graph, mode)) {
172
- enter(edge.to, !edge.evaluation);
170
+ enter(edge.to, namespaces && !edge.evaluation);
173
171
  }
174
172
  walk(false);
175
173
  return reached;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The globals a test runner injects into every file it runs, which no import names: a
3
+ * Vitest configuration whose `test.globals` is `true` gives each test and each file it
4
+ * loads `describe`, `it`, `expect` and `vi` (https://vitest.dev/config/globals). A program
5
+ * analyzed without the runner's declarations of them reads every use as an error, so a
6
+ * configuration under such a runner is read with them; and a file that calls one is
7
+ * written for the runner, as one that imports it is.
8
+ */
9
+ import { isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isExpressionStatement, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, SyntaxKind, } from "@typescript/native/ast";
10
+ import { dirnamePath, joinPath } from "./paths.js";
11
+ import { UNANSWERED } from "./query.js";
12
+ import { writtenPath } from "./workspace-programs.js";
13
+ /** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
14
+ export const GLOBALS_RUNNER = "vitest";
15
+ const GLOBALS_TYPES = "vitest/globals";
16
+ /** The configuration files the runner reads its settings from, by name. */
17
+ const RUNNER_CONFIG = /^(?:vitest|vite)\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u;
18
+ /** The globals `vitest/globals` declares that a test or a helper calls or reads a member of. */
19
+ const GLOBAL_NAMES = new Set([
20
+ "afterAll",
21
+ "afterEach",
22
+ "assert",
23
+ "assertType",
24
+ "beforeAll",
25
+ "beforeEach",
26
+ "bench",
27
+ "describe",
28
+ "expect",
29
+ "expectTypeOf",
30
+ "it",
31
+ "onTestFailed",
32
+ "onTestFinished",
33
+ "suite",
34
+ "test",
35
+ "vi",
36
+ "vitest",
37
+ ]);
38
+ /** The expression one wrapper that changes no value holds, and a call's first argument. */
39
+ function configObject(node) {
40
+ let at = node;
41
+ while (at !== undefined) {
42
+ if (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
43
+ at = at.expression;
44
+ }
45
+ else if (isCallExpression(at)) {
46
+ at = at.arguments[0];
47
+ }
48
+ else {
49
+ return at;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /** The value one object literal writes for one member as `name: value`. */
55
+ function member(object, name) {
56
+ if (object === undefined || !isObjectLiteralExpression(object)) {
57
+ return undefined;
58
+ }
59
+ for (const property of object.properties) {
60
+ if (isPropertyAssignment(property) &&
61
+ isIdentifier(property.name) &&
62
+ property.name.text === name) {
63
+ return configObject(property.initializer);
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ /** Whether one runner configuration's default export sets `test.globals` to `true`. */
69
+ function setsGlobals(file) {
70
+ for (const statement of file.statements) {
71
+ let exported;
72
+ if (isExportAssignment(statement)) {
73
+ exported = statement.expression;
74
+ }
75
+ else if (isExpressionStatement(statement) && isBinaryExpression(statement.expression)) {
76
+ const { left, operatorToken, right } = statement.expression;
77
+ if (operatorToken.kind === SyntaxKind.EqualsToken &&
78
+ isPropertyAccessExpression(left) &&
79
+ isIdentifier(left.expression) &&
80
+ left.expression.text === "module" &&
81
+ isIdentifier(left.name) &&
82
+ left.name.text === "exports") {
83
+ exported = right;
84
+ }
85
+ }
86
+ const object = exported === undefined ? undefined : configObject(exported);
87
+ const globals = member(member(object, "test"), "globals");
88
+ if (globals?.kind === SyntaxKind.TrueKeyword) {
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+ /** Whether the runner's configuration in one manifest's directory injects the globals. */
95
+ function injectsGlobals(host, parse, dir) {
96
+ let names;
97
+ try {
98
+ names = host
99
+ .readDirectory(dir)
100
+ .filter((entry) => !entry.directory && RUNNER_CONFIG.test(entry.name))
101
+ .map((entry) => entry.name);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ return names.some((name) => {
107
+ const path = joinPath(dir, name);
108
+ try {
109
+ return setsGlobals(parse(path, host.readFile(path)));
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ });
115
+ }
116
+ /** Whether a file declaring the globals is installed where the configuration resolves packages from. */
117
+ function globalsInstalled(host, configFile) {
118
+ for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
119
+ if (host.kindOf(joinPath(dir, "node_modules", GLOBALS_RUNNER, "globals.d.ts")) === "file") {
120
+ return true;
121
+ }
122
+ if (dirnamePath(dir) === dir) {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ /** The directory of the manifest nearest one configuration file, at or above its own. */
128
+ function manifestDir(host, configFile) {
129
+ for (let dir = dirnamePath(configFile);; dir = dirnamePath(dir)) {
130
+ if (host.kindOf(joinPath(dir, "package.json")) === "file") {
131
+ return dir;
132
+ }
133
+ if (dirnamePath(dir) === dir) {
134
+ return undefined;
135
+ }
136
+ }
137
+ }
138
+ function typesOf(value) {
139
+ return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : undefined;
140
+ }
141
+ /**
142
+ * How the snapshot reads each configuration of `configFiles` whose nearest manifest's
143
+ * runner configuration injects the globals and that can resolve their declarations: as
144
+ * `programs` reads it, else as itself, with the globals' declarations added to its `types`.
145
+ * A configuration the compiler cannot read is left for its project to refuse.
146
+ */
147
+ export function withTestGlobals(engine, host, parse, configFiles, programs) {
148
+ const found = new Map(programs);
149
+ const injects = new Map();
150
+ for (const configFile of configFiles) {
151
+ const dir = manifestDir(host, configFile);
152
+ if (dir === undefined || !globalsInstalled(host, configFile)) {
153
+ continue;
154
+ }
155
+ if (!injects.has(dir)) {
156
+ injects.set(dir, injectsGlobals(host, parse, dir));
157
+ }
158
+ if (injects.get(dir) !== true) {
159
+ continue;
160
+ }
161
+ let base;
162
+ try {
163
+ base = typesOf(engine.parseConfigFile(configFile).options.types) ?? [];
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ const held = found.get(configFile);
169
+ if (held?.kind === "program") {
170
+ const listed = typesOf(held.params.compilerOptions.types) ?? base;
171
+ if (!listed.includes(GLOBALS_TYPES)) {
172
+ found.set(configFile, {
173
+ kind: "program",
174
+ params: {
175
+ ...held.params,
176
+ compilerOptions: { ...held.params.compilerOptions, types: [...listed, GLOBALS_TYPES] },
177
+ },
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ let written = { extends: configFile };
183
+ if (held !== undefined) {
184
+ try {
185
+ written = JSON.parse(held.text);
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ }
191
+ const options = (written["compilerOptions"] ?? {});
192
+ const listed = typesOf(options["types"]) ?? base;
193
+ if (listed.includes(GLOBALS_TYPES)) {
194
+ continue;
195
+ }
196
+ found.set(configFile, {
197
+ kind: "configuration",
198
+ file: held?.file ?? writtenPath(host, configFile),
199
+ text: `${JSON.stringify({ ...written, compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] } }, null, 2)}\n`,
200
+ });
201
+ }
202
+ return found;
203
+ }
204
+ /**
205
+ * Whether one file names a global the runner injects, which is how a file written for the
206
+ * runner's globals reads: a call of one of the names or a read of a member of it that
207
+ * resolves to a declaration outside the project's own files, the runner's declaration
208
+ * file. A name the file binds, imported, declared or a parameter, resolves to its own.
209
+ */
210
+ export function namesTestGlobals(project, file) {
211
+ const named = [];
212
+ const visit = (node) => {
213
+ const callee = isCallExpression(node)
214
+ ? node.expression
215
+ : isPropertyAccessExpression(node)
216
+ ? node.expression
217
+ : undefined;
218
+ if (callee !== undefined && isIdentifier(callee) && GLOBAL_NAMES.has(callee.text)) {
219
+ named.push(callee);
220
+ }
221
+ node.forEachChild(visit);
222
+ };
223
+ file.forEachChild(visit);
224
+ if (named.length === 0) {
225
+ return false;
226
+ }
227
+ const own = project.ownPaths();
228
+ return project
229
+ .symbolsAt(named.map((node) => project.handle(node)))
230
+ .some((symbol) => symbol === UNANSWERED ||
231
+ (symbol !== undefined &&
232
+ symbol.declarations.length > 0 &&
233
+ symbol.declarations.every((handle) => !own.has(handle.path))));
234
+ }
@@ -13,6 +13,12 @@ import { isAnswered } from "./query.js";
13
13
  import { findWorkspace } from "./workspace.js";
14
14
  /** The compiler's codes for an import whose module it cannot find. */
15
15
  const MODULE_NOT_FOUND = new Set([2307, 2792]);
16
+ /**
17
+ * The compiler's codes for where the configuration places a file, which leave every type
18
+ * the file's statements carry as it is: an output that would overwrite an input (5055,
19
+ * 5056), a file outside `rootDir` (6059), one a composite project does not list (6307).
20
+ */
21
+ const CONFIGURATION_LEVEL = new Set([5055, 5056, 6059, 6307]);
16
22
  const MANIFEST = "package.json";
17
23
  const MANIFEST_SECTIONS = [
18
24
  "dependencies",
@@ -204,8 +210,8 @@ function messageOf(text, targetRoot) {
204
210
  }
205
211
  /**
206
212
  * Reads the errors one project's own files carry. A diagnostic marking unnecessary code
207
- * is no error here, and one in a file that is not the project's own is that file's
208
- * package's to answer for.
213
+ * or about where the configuration places a file is no error here, and one in a file that
214
+ * is not the project's own is that file's package's to answer for.
209
215
  */
210
216
  export function readErrors(project, errors, where) {
211
217
  let workspace = null;
@@ -223,7 +229,9 @@ export function readErrors(project, errors, where) {
223
229
  const skips = [];
224
230
  for (const { diagnostic, at } of errors) {
225
231
  const file = diagnostic.fileName === undefined ? undefined : files.get(diagnostic.fileName);
226
- if (file === undefined || diagnostic.reportsUnnecessary === true) {
232
+ if (file === undefined ||
233
+ diagnostic.reportsUnnecessary === true ||
234
+ CONFIGURATION_LEVEL.has(diagnostic.code)) {
227
235
  continue;
228
236
  }
229
237
  const path = relativePath(context.targetRoot, file.fileName) ?? file.fileName;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The loads of a project's production files whose target the analysis cannot resolve: an
3
+ * `import()` or `require()` call whose path is computed and starts with no relative literal
4
+ * naming a directory below the calling file's. The files such a load runs are roots the
5
+ * analysis does not see, so the configuration's roots are never complete.
6
+ */
7
+ import { isAsExpression, isBinaryExpression, isCallExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
8
+ import { dirnamePath, relativePath, resolvePath } from "./paths.js";
9
+ import { renderPosition } from "./position.js";
10
+ import { UNANSWERED } from "./query.js";
11
+ import { sourceFilesOf } from "./source-files.js";
12
+ /** The expression a wrapper that changes no value holds: parentheses and type assertions. */
13
+ function unwrapped(node) {
14
+ let at = node;
15
+ while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
16
+ at = at.expression;
17
+ }
18
+ return at;
19
+ }
20
+ /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
21
+ export function leadingLiteral(node) {
22
+ const at = unwrapped(node);
23
+ if (isTemplateExpression(at)) {
24
+ return at.head.text;
25
+ }
26
+ if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
27
+ const left = unwrapped(at.left);
28
+ return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
29
+ ? left.text
30
+ : leadingLiteral(at.left);
31
+ }
32
+ return undefined;
33
+ }
34
+ /**
35
+ * The directory below `dir` a computed load's leading literal names, which the entry
36
+ * rules root every file of, or none where it names a package, `dir` itself or a
37
+ * directory holding it, each of which would name files no rule roots.
38
+ */
39
+ export function computedLoadDirectory(dir, prefix) {
40
+ if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
41
+ return undefined;
42
+ }
43
+ const target = resolvePath(dir, prefix);
44
+ return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
45
+ }
46
+ /** Whether a computed path's leading literal names a package, whose files are none of the target's. */
47
+ function namesPackage(prefix) {
48
+ return (prefix !== undefined &&
49
+ prefix !== "" &&
50
+ !prefix.startsWith(".") &&
51
+ !prefix.startsWith("/") &&
52
+ !/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix));
53
+ }
54
+ /**
55
+ * Every unresolved load the project's production files write, in file then source order.
56
+ * `testFiles` holds the test files by their path below the target root. A `require` call
57
+ * loads a module only where the name resolves to no declaration of the project's own
58
+ * files: one the project declares is its own function.
59
+ */
60
+ export function unseenLoadsOf(project, configuration, targetRoot, testFiles) {
61
+ const tests = new Set(testFiles);
62
+ const sites = [];
63
+ for (const [path, file] of sourceFilesOf(project, targetRoot).byPath) {
64
+ if (tests.has(path) || file.isDeclarationFile) {
65
+ continue;
66
+ }
67
+ const dir = dirnamePath(file.fileName);
68
+ const visit = (node) => {
69
+ if (isCallExpression(node)) {
70
+ const callee = node.expression;
71
+ const required = isIdentifier(callee) && callee.text === "require";
72
+ const [first] = node.arguments;
73
+ const argument = first === undefined ? undefined : unwrapped(first);
74
+ if ((callee.kind === SyntaxKind.ImportKeyword || required) &&
75
+ argument !== undefined &&
76
+ !isStringLiteral(argument) &&
77
+ !isNoSubstitutionTemplateLiteral(argument)) {
78
+ const prefix = leadingLiteral(argument);
79
+ const seen = namesPackage(prefix) ||
80
+ (prefix !== undefined && computedLoadDirectory(dir, prefix) !== undefined);
81
+ if (!seen) {
82
+ sites.push({
83
+ position: renderPosition(file, targetRoot, node.getStart()),
84
+ require: required ? callee : undefined,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ node.forEachChild(visit);
90
+ };
91
+ file.forEachChild(visit);
92
+ }
93
+ const requires = sites.flatMap((site) => (site.require === undefined ? [] : [site.require]));
94
+ const own = project.ownPaths();
95
+ const local = new Set();
96
+ project.symbolsAt(requires.map((node) => project.handle(node))).forEach((symbol, at) => {
97
+ const node = requires[at];
98
+ if (node !== undefined &&
99
+ symbol !== UNANSWERED &&
100
+ symbol?.declarations.some((handle) => own.has(handle.path)) === true) {
101
+ local.add(node);
102
+ }
103
+ });
104
+ return sites
105
+ .filter((site) => site.require === undefined || !local.has(site.require))
106
+ .map((site) => ({ configuration, position: site.position }));
107
+ }
@@ -3,7 +3,7 @@ import { ConfigError, kindOf } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
5
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
- import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
6
+ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
7
  import { partialNotes } from "../partial.js";
8
8
  import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
9
9
  import { json } from "../reporters/json.js";
@@ -77,6 +77,24 @@ function unmappedNotes(entries, targetRoot) {
77
77
  };
78
78
  });
79
79
  }
80
+ /**
81
+ * One note per load of a production file the run cannot resolve, each site once: a file
82
+ * it loads is a root the run does not see, so `ts.roots_complete` lifts no cap there.
83
+ */
84
+ function unseenLoadNotes(loads) {
85
+ return [
86
+ ...new Map(loads.map((load) => {
87
+ const { path, line, column } = load.position;
88
+ const site = `${path}:${String(line)}:${String(column)}`;
89
+ const note = {
90
+ kind: "computed-load",
91
+ path: dirnamePath(path),
92
+ message: `${site} loads a module by a computed path the analysis cannot resolve, so the configurations compiling it report what no root reaches at probable at most, whatever ts.roots_complete says`,
93
+ };
94
+ return [site, note];
95
+ })).values(),
96
+ ];
97
+ }
80
98
  /** Each project the run dropped as a report names it, by its configuration file below the target. */
81
99
  function notBuiltOf(dropped, targetRoot) {
82
100
  return dropped.map((one) => {
@@ -222,7 +240,11 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
222
240
  typeErrorSkips: run.typeErrorSkips,
223
241
  unanswered: unansweredCounts(analysis),
224
242
  conventionsApplied: run.conventionsApplied,
225
- notes: unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
243
+ listsComponents: config.reporters.cascade === "full",
244
+ notes: [
245
+ ...unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
246
+ ...unseenLoadNotes(run.unseenLoads),
247
+ ],
226
248
  });
227
249
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
228
250
  verdict = verdictOf(result, config);
@@ -237,7 +259,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
237
259
  try {
238
260
  host.writeDocument(documents.report, json(report));
239
261
  const options = {
240
- failOn: config.reporters.failOn,
241
262
  readSource: (path) => host.readFile(joinPath(targetRoot, path)),
242
263
  template,
243
264
  };
@@ -74,24 +74,32 @@ export function runCommands(text, key = RUN_LINE) {
74
74
  return found;
75
75
  }
76
76
  /**
77
- * Every token the recipe lines of the target root's makefile hold, each with the directory
78
- * below the root it is read against: a recipe line is one that starts with a tab, joined
79
- * with the lines a trailing `\` continues it onto, and make runs each in a shell of its
80
- * own, so a `cd` moves the directory of the later commands of its line alone.
77
+ * The commands of the recipe lines of the target root's makefile, each line's in order: a
78
+ * recipe line is one that starts with a tab, joined with the lines a trailing `\`
79
+ * continues it onto.
81
80
  */
82
- function recipeTokens(host, root) {
81
+ function recipeLines(host, root) {
83
82
  const path = MAKEFILES.map((name) => joinPath(root, name)).find((candidate) => host.kindOf(candidate) === "file");
84
83
  const text = path === undefined ? undefined : readText(host, path);
85
84
  if (text === undefined) {
86
85
  return [];
87
86
  }
87
+ return text
88
+ .replace(/\\\r?\n/gu, " ")
89
+ .split(/\r?\n/u)
90
+ .filter((line) => line.startsWith("\t"))
91
+ .map((line) => withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS));
92
+ }
93
+ /**
94
+ * Every token the recipe lines of the target root's makefile hold, each with the directory
95
+ * below the root it is read against: make runs each line in a shell of its own, so a `cd`
96
+ * moves the directory of the later commands of its line alone.
97
+ */
98
+ function recipeTokens(host, root) {
88
99
  const found = [];
89
- for (const line of text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
90
- if (!line.startsWith("\t")) {
91
- continue;
92
- }
100
+ for (const line of recipeLines(host, root)) {
93
101
  let dir = root;
94
- for (const command of withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS)) {
102
+ for (const command of line) {
95
103
  const tokens = scriptTokens(command);
96
104
  const [first, target, ...rest] = tokens;
97
105
  if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
@@ -131,13 +139,25 @@ function withoutComment(line) {
131
139
  return line;
132
140
  }
133
141
  /**
134
- * Every token the target root's workflow steps, git hooks and makefile hold: the `run:`
142
+ * Every command the target root's workflow steps, git hooks and makefile run: the `run:`
135
143
  * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
136
144
  * and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
137
145
  * of a lefthook file and the makefile's recipe lines.
138
146
  */
147
+ export function workflowCommands(host, targetRoot) {
148
+ const root = resolvePath(host.workingDirectory(), targetRoot);
149
+ return [...stepCommands(host, root), ...recipeLines(host, root).flat()];
150
+ }
151
+ /** Every token the commands {@link workflowCommands} reads hold. */
139
152
  export function workflowTokens(host, targetRoot) {
140
153
  const root = resolvePath(host.workingDirectory(), targetRoot);
154
+ return [
155
+ ...stepCommands(host, root).flatMap((command) => scriptTokens(command)),
156
+ ...recipeTokens(host, root).map(({ token }) => token),
157
+ ];
158
+ }
159
+ /** The commands of the workflow steps, git hooks and hook managers at one root. */
160
+ function stepCommands(host, root) {
141
161
  const commands = [];
142
162
  for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
143
163
  commands.push(...runCommands(readText(host, path) ?? ""));
@@ -150,10 +170,7 @@ export function workflowTokens(host, targetRoot) {
150
170
  for (const [name, key] of HOOK_MANAGERS) {
151
171
  commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
152
172
  }
153
- return [
154
- ...commands.flatMap((command) => scriptTokens(command)),
155
- ...recipeTokens(host, root).map(({ token }) => token),
156
- ];
173
+ return commands;
157
174
  }
158
175
  /**
159
176
  * The own files the target root's workflow steps and git hooks name, as