@cplieger/deadset-ts 4.0.1 → 5.0.0

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 (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
7
- It reads your `tsconfig.json` projects, `allowJs` JavaScript included, with the type information of TypeScript 7.1.0-dev.20261003.1, its one dependency. Your project keeps its own TypeScript version and must also compile under that version. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
7
+ It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261003.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
8
8
 
9
9
  ## Why use it
10
10
 
@@ -17,7 +17,7 @@ deadset-ts is built for a CI gate on dead code. Each finding names code to delet
17
17
  - The same tree gives the same report, and a baseline keyed on code and symbol survives moved lines.
18
18
  - It writes text, JSON, GitHub annotations, SARIF 2.1.0 or a custom template.
19
19
 
20
- Consider [Knip](https://knip.dev) if you want issues fixed with `--fix`, plugins for over 100 tools, or Astro, MDX, Svelte and Vue files checked.
20
+ Consider [Knip](https://knip.dev) if you want issues fixed with `--fix`, plugins for over 100 tools, or MDX files checked.
21
21
 
22
22
  ## Install
23
23
 
@@ -90,7 +90,7 @@ A TypeScript caller that imports the source sets `allowImportingTsExtensions`, b
90
90
 
91
91
  ## Related projects
92
92
 
93
- deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
93
+ deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus except one reflective-lookup fixture, the gap [`conformance.json`](conformance.json) declares.
94
94
 
95
95
  - [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
96
96
  - [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
@@ -105,7 +105,7 @@ deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset
105
105
 
106
106
  ## Contributing
107
107
 
108
- See [CONTRIBUTING.md](https://github.com/cplieger/.github/blob/main/CONTRIBUTING.md).
108
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
109
109
 
110
110
  ## Disclaimer
111
111
 
@@ -0,0 +1,5 @@
1
+ import { mapperConnection } from "./mapper-connection.js";
2
+ const connection = mapperConnection((bytes) => process.stdout.write(bytes));
3
+ process.stdin.on("data", (chunk) => {
4
+ connection.receive(chunk);
5
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The content mapper the compiler runs for component files: JSON-RPC 2.0 messages, each
3
+ * framed by a `Content-Length` header, over the process's standard streams. It answers
4
+ * `transform` with {@link componentModule} and keeps no state between requests.
5
+ * The protocol is the compiler's own:
6
+ * https://github.com/microsoft/TypeScript/blob/f9f8d01292562242b6e7c7142e46ed7e470926f9/tsc/internal/contentmapper/hostimpl.go
7
+ */
8
+ import { componentModule } from "../src/component-files.js";
9
+ /** The JSON-RPC code of a method this mapper does not serve. */
10
+ const METHOD_NOT_FOUND = -32601;
11
+ /** The JSON-RPC code of a request whose parameters are not the protocol's. */
12
+ const INVALID_PARAMS = -32602;
13
+ const HEADER_END = new Uint8Array([0x0d, 0x0a, 0x0d, 0x0a]);
14
+ /** The offset of `needle` in `bytes` at or after `from`, or -1. */
15
+ function indexOf(bytes, needle, from) {
16
+ outer: for (let at = from; at + needle.length <= bytes.length; at += 1) {
17
+ for (let index = 0; index < needle.length; index += 1) {
18
+ if (bytes[at + index] !== needle[index]) {
19
+ continue outer;
20
+ }
21
+ }
22
+ return at;
23
+ }
24
+ return -1;
25
+ }
26
+ /** The answer to one message, or undefined for a notification, which takes none. */
27
+ function answer(message) {
28
+ if (message.id === undefined) {
29
+ return undefined;
30
+ }
31
+ const reply = (result) => ({ jsonrpc: "2.0", id: message.id, result });
32
+ const fail = (code, text) => ({
33
+ jsonrpc: "2.0",
34
+ id: message.id,
35
+ error: { code, message: text },
36
+ });
37
+ switch (message.method) {
38
+ case "initialize":
39
+ return reply({ positionEncoding: "utf-16", diagnosticSource: "deadset-ts" });
40
+ case "openProject":
41
+ return reply({});
42
+ case "closeProject":
43
+ return reply(null);
44
+ case "transform": {
45
+ const content = typeof message.params === "object" && message.params !== null
46
+ ? message.params.content
47
+ : undefined;
48
+ if (typeof content !== "string") {
49
+ return fail(INVALID_PARAMS, "transform: params.content is not a string");
50
+ }
51
+ const module = componentModule(content);
52
+ return reply({ text: module.text, extension: module.extension, mappings: module.mappings });
53
+ }
54
+ default:
55
+ return fail(METHOD_NOT_FOUND, `no method ${JSON.stringify(message.method)}`);
56
+ }
57
+ }
58
+ /**
59
+ * A connection over one byte stream. A message whose body is not JSON is answered with
60
+ * nothing, because it carries no id to answer to.
61
+ */
62
+ export function mapperConnection(write) {
63
+ const encoder = new TextEncoder();
64
+ const decoder = new TextDecoder();
65
+ let pending = new Uint8Array(0);
66
+ return {
67
+ receive(chunk) {
68
+ const joined = new Uint8Array(pending.length + chunk.length);
69
+ joined.set(pending);
70
+ joined.set(chunk, pending.length);
71
+ pending = joined;
72
+ for (;;) {
73
+ const headerEnd = indexOf(pending, HEADER_END, 0);
74
+ if (headerEnd < 0) {
75
+ return;
76
+ }
77
+ const header = decoder.decode(pending.subarray(0, headerEnd));
78
+ const length = Number(/content-length:\s*(\d+)/iu.exec(header)?.[1] ?? "0");
79
+ const bodyStart = headerEnd + HEADER_END.length;
80
+ if (pending.length < bodyStart + length) {
81
+ return;
82
+ }
83
+ const body = decoder.decode(pending.subarray(bodyStart, bodyStart + length));
84
+ pending = pending.slice(bodyStart + length);
85
+ let message;
86
+ try {
87
+ message = JSON.parse(body);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ const reply = answer(message);
93
+ if (reply !== undefined) {
94
+ const bytes = encoder.encode(JSON.stringify(reply));
95
+ write(encoder.encode(`Content-Length: ${String(bytes.length)}\r\n\r\n`));
96
+ write(bytes);
97
+ }
98
+ }
99
+ },
100
+ };
101
+ }
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, fchmodSync, fsyncSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
2
+ import { closeSync, fchmodSync, fsyncSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, extname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  /** The name the manifest of this package declares. */
6
7
  const PACKAGE_NAME = "@cplieger/deadset-ts";
@@ -75,6 +76,24 @@ function writeAtomically(path, pieces) {
75
76
  throw error;
76
77
  }
77
78
  }
79
+ /** A new directory under the platform's temporary directory. */
80
+ function temporaryDirectory() {
81
+ const path = mkdtempSync(join(tmpdir(), "deadset-ts-mapper-"));
82
+ return {
83
+ path,
84
+ remove: () => {
85
+ rmSync(path, { recursive: true, force: true });
86
+ },
87
+ };
88
+ }
89
+ /**
90
+ * The mapper beside this file, in the same form: the emitted tree runs the emitted
91
+ * mapper and a checkout runs the source through the runtime's own type stripping.
92
+ */
93
+ function componentMapperCommand() {
94
+ const here = fileURLToPath(import.meta.url);
95
+ return [process.execPath, join(dirname(here), `component-mapper${extname(here)}`)];
96
+ }
78
97
  /**
79
98
  * The filesystem of the platform the command runs on, and the version of the
80
99
  * package it was installed from.
@@ -98,10 +117,13 @@ export function nodeHost() {
98
117
  }
99
118
  return stat.isDirectory() ? "directory" : "file";
100
119
  },
120
+ realPath: (path) => realpathSync(path),
101
121
  analyzerVersion: () => {
102
122
  found ??= readVersion();
103
123
  return found;
104
124
  },
105
125
  writeDocument: writeAtomically,
126
+ temporaryDirectory,
127
+ componentMapperCommand,
106
128
  };
107
129
  }
@@ -7,15 +7,17 @@
7
7
  * carries a project's identity in its type so a handle cannot cross to another
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
+ import { lineOf, readComponent } from "./component-files.js";
10
11
  import { consumerLoads, consumerReferences, moduleIdentity, } from "./consumers.js";
11
12
  import { configuredDeclarations, configuredRef, declarationsNamed, } from "./configured-declarations.js";
12
13
  import { decorator } from "./decorator.js";
13
14
  import { dependenciesOf, projectNeeds } from "./dependencies.js";
14
15
  import { deprecatedDeclarations } from "./deprecation.js";
15
16
  import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "./discover.js";
17
+ import { conventionFailuresFor, readConventions, } from "./conventions.js";
16
18
  import { readEdgeSides } from "./edges.js";
17
19
  import { enumGroup } from "./enum-group.js";
18
- import { computeExemptions, disabledClasses, exemptionsOf, isUnansweredRecord, retainedIn, } from "./exempt.js";
20
+ import { computeExemptions, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
19
21
  import { heldByInclusion, sourceTree } from "./file-facts.js";
20
22
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
21
23
  import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
@@ -26,19 +28,23 @@ import { interfaceSatisfaction } from "./interface-satisfaction.js";
26
28
  import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
27
29
  import { inventory } from "./inventory.js";
28
30
  import { readManifest } from "./manifest.js";
29
- import { matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
30
- import { relativePath, resolvePath } from "./paths.js";
31
- import { byPosition } from "./position.js";
31
+ import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
32
+ import { joinPath, relativePath, resolvePath } from "./paths.js";
33
+ import { byPosition, isComponentFile } from "./position.js";
34
+ import { SetupError, setupLine } from "./setup-failure.js";
35
+ import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
32
36
  import { reflectiveLookup } from "./reflective-lookup.js";
33
37
  import { references, testFileRulesOf } from "./references.js";
34
38
  import { roots, unmatchedEverywhere } from "./roots.js";
35
39
  import { serializationContract } from "./serialization-contract.js";
36
- import { diagnosticsOf, runSession } from "./session.js";
40
+ import { supportReferences, testSupportFiles } from "./test-support.js";
41
+ import { diagnosticsOf, ownedAs, runSession } from "./session.js";
37
42
  import { accessorsOf, storesOf } from "./stores.js";
38
43
  import { isPartKind } from "./suppress.js";
39
44
  import { bindRows, readBaseline, readIgnoreFile } from "./suppress-file.js";
40
45
  import { inlineDirectives, inlineSuppressions } from "./suppress-inline.js";
41
46
  import { readTemplates, templateField } from "./template-field.js";
47
+ import { ownErrors, workspaceRun } from "./workspace-run.js";
42
48
  /** The setting whose source decides where a finding about a configured root sits. */
43
49
  const ROOTS_SETTING = "roots.patterns";
44
50
  /**
@@ -63,12 +69,29 @@ function compare(a, b) {
63
69
  return a < b ? -1 : 1;
64
70
  }
65
71
  /**
66
- * Reads every project the scope discovers, and every project of each consumer it
67
- * declares, in one snapshot, each checked for errors first. A target project is
68
- * enumerated, rooted and handed to `stage`; a consumer project is read for its references
69
- * to the target. A guess that does not load is dropped into `notBuilt` (see
70
- * {@link isGuess}); any other project that does not load or carries an error fails the
71
- * run, as does a run whose every guess was dropped.
72
+ * The manifests the analysis reads, each once: the target's own, then every workspace
73
+ * member's below the target root.
74
+ */
75
+ function conventionManifests(host, targetRoot, memberDirs) {
76
+ const found = new Map();
77
+ if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
78
+ found.set(targetRoot, { dir: targetRoot, path: "package.json" });
79
+ }
80
+ for (const dir of memberDirs) {
81
+ const below = relativePath(targetRoot, dir);
82
+ if (below !== undefined && !found.has(dir)) {
83
+ found.set(dir, { dir, path: `${below}/package.json` });
84
+ }
85
+ }
86
+ return [...found.values()];
87
+ }
88
+ /**
89
+ * Reads every project the scope discovers, and each declared consumer's, in one snapshot,
90
+ * each checked for errors first. `stage` is handed a target project and enumerates and roots
91
+ * it if it reads it; a consumer project is read for its references to the target. A type error
92
+ * in an own file skips the unit holding it. A guess that does not load or meets a setup failure
93
+ * is dropped into `notBuilt` (see {@link isGuess}); any other such project fails the run, as
94
+ * does a run whose every guess was dropped.
72
95
  */
73
96
  function readProjects(engine, host, scope, config, stage) {
74
97
  const targetRoot = scope.target.path;
@@ -76,14 +99,19 @@ function readProjects(engine, host, scope, config, stage) {
76
99
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
77
100
  const loads = consumerLoads(engine, host, scope, discovered.configFiles);
78
101
  const consumerOf = new Map(loads.flatMap((load) => load.configFiles.map((configFile) => [configFile, load.consumer])));
102
+ const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
103
+ const workspace = workspaceRun(engine, host, absoluteRoot, discovered.configFiles, readManifest(host, targetRoot));
104
+ const conventions = readConventions(host, (fileName, text) => engine.parseSourceFile(fileName, text), conventionManifests(host, absoluteRoot, workspace?.memberDirs ?? []), config.ts.disabledConventions);
79
105
  const options = {
80
- manifest: readManifest(host, targetRoot),
106
+ host,
107
+ manifest: workspace?.manifest ?? readManifest(host, targetRoot),
108
+ workspaceEntries: workspace?.entries,
81
109
  patterns: config.rootPatterns,
82
110
  entryFiles: config.ts.entryFiles,
111
+ conventions,
83
112
  testFiles: config.ts.testFiles,
84
113
  publishedAPI: config.targetKind === "library",
85
114
  };
86
- const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
87
115
  const notBuilt = [...discovered.notBuilt];
88
116
  const dropped = (configFile, error) => {
89
117
  notBuilt.push({
@@ -93,10 +121,36 @@ function readProjects(engine, host, scope, config, stage) {
93
121
  });
94
122
  };
95
123
  const failures = [];
124
+ const unbuildable = [];
125
+ const setupDropped = [];
96
126
  const consumed = new Map();
97
- const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (project) => {
98
- const errors = projectErrors(diagnosticsOf(project));
99
- const consumer = consumerOf.get(project.configFile);
127
+ const setupMet = (configFile, met) => {
128
+ const [first] = met;
129
+ if (first !== undefined && isGuess(discovered, configFile)) {
130
+ dropped(configFile, setupLine(first));
131
+ setupDropped.push(...met);
132
+ }
133
+ else {
134
+ unbuildable.push(...met);
135
+ }
136
+ };
137
+ const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
138
+ const consumer = consumerOf.get(opened.configFile);
139
+ const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
140
+ if (setup !== undefined) {
141
+ setupMet(opened.configFile, [
142
+ { setupClass: "workspace-member-without-source", detail: setup },
143
+ ]);
144
+ return undefined;
145
+ }
146
+ const referenceOnly = workspace === undefined || consumer !== undefined
147
+ ? () => false
148
+ : workspace.referenceOnly(opened.configFile, opened.rootFiles);
149
+ const project = workspace === undefined || consumer !== undefined
150
+ ? opened
151
+ : ownedAs(opened, (file, fromExternalLibrary) => workspace.ownFile(file, fromExternalLibrary));
152
+ const raw = projectErrors(diagnosticsOf(project));
153
+ const errors = { load: raw.load, check: ownErrors(raw.check, referenceOnly) };
100
154
  const [unloaded] = errors.load;
101
155
  if (unloaded !== undefined &&
102
156
  consumer === undefined &&
@@ -104,8 +158,37 @@ function readProjects(engine, host, scope, config, stage) {
104
158
  dropped(project.configFile, renderDiagnostic(unloaded, (path) => relativePath(absoluteRoot, path) ?? path));
105
159
  return undefined;
106
160
  }
107
- if (errors.load.length > 0 || errors.check.length > 0) {
108
- failures.push(...errors.load, ...errors.check);
161
+ if (errors.load.length > 0) {
162
+ failures.push(...errors.load);
163
+ return undefined;
164
+ }
165
+ const reading = readErrors(project, errors.check, {
166
+ host,
167
+ targetRoot: consumer === undefined
168
+ ? absoluteRoot
169
+ : resolvePath(host.workingDirectory(), consumer.path),
170
+ paths: pathsOf(engine, project.configFile),
171
+ });
172
+ if (reading.failing.length > 0) {
173
+ failures.push(...reading.failing);
174
+ return undefined;
175
+ }
176
+ if (consumer !== undefined && reading.missing.length > 0) {
177
+ unbuildable.push(...reading.missing.map((one) => ({
178
+ setupClass: "missing-consumer",
179
+ detail: `the consumer ${consumer.path} does not build, its dependencies not installed: ${one.detail}`,
180
+ })));
181
+ return undefined;
182
+ }
183
+ if (reading.missing.length > 0) {
184
+ setupMet(project.configFile, reading.missing);
185
+ return undefined;
186
+ }
187
+ const unconventional = consumer === undefined
188
+ ? conventionFailuresFor(conventions, project.configFile, project.ownSourceFiles().map((file) => file.fileName))
189
+ : [];
190
+ if (unconventional.length > 0) {
191
+ setupMet(project.configFile, unconventional);
109
192
  return undefined;
110
193
  }
111
194
  if (consumer !== undefined) {
@@ -117,22 +200,52 @@ function readProjects(engine, host, scope, config, stage) {
117
200
  }
118
201
  return undefined;
119
202
  }
120
- const held = inventory(project, host, targetRoot);
121
- const rooted = roots(project, held, targetRoot, options);
122
203
  const configuration = ids.get(project.configFile) ?? project.configFile;
123
- return stage(project, { configuration, configFile: project.configFile, held, rooted });
124
- }, discovered.derived ? "record" : "refuse");
204
+ const read = () => {
205
+ const held = inventory(project, host, targetRoot);
206
+ const found = roots(project, held, targetRoot, options);
207
+ const kept = skippedRoots(project, held, reading.skips);
208
+ const rooted = kept.length === 0
209
+ ? found
210
+ : {
211
+ ...found,
212
+ liveUnderReachability: [
213
+ ...found.liveUnderReachability,
214
+ ...kept.map((id) => ({ id, kind: "type-error", source: "" })),
215
+ ],
216
+ };
217
+ return {
218
+ configuration,
219
+ configFile: project.configFile,
220
+ held,
221
+ rooted,
222
+ typeErrorSkips: reading.skips.map((one) => one.record),
223
+ skipped: skippedUnits(reading.skips, absoluteRoot),
224
+ referenceOnly: project
225
+ .ownSourceFiles()
226
+ .filter((file) => referenceOnly(file.fileName))
227
+ .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
228
+ };
229
+ };
230
+ return stage(project, { configuration, read });
231
+ }, discovered.derived ? "record" : "refuse", workspace?.programs);
125
232
  for (const configFile of session.unopened) {
126
233
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
127
234
  throw new DiscoveryError(`${configFile} opened no project`, []);
128
235
  }
129
236
  dropped(configFile, "the compiler opened no project for the configuration");
130
237
  }
238
+ if (unbuildable.length > 0) {
239
+ throw new SetupError(unbuildable);
240
+ }
131
241
  if (failures.length > 0) {
132
242
  throw new DiscoveryError(`${String(failures.length)} error(s), so no answer was produced`, failures);
133
243
  }
134
244
  const answers = session.projects.filter((answer) => answer !== undefined);
135
245
  const [first] = notBuilt;
246
+ if (answers.length === 0 && setupDropped.length > 0) {
247
+ throw new SetupError(setupDropped);
248
+ }
136
249
  if (answers.length === 0 && first !== undefined) {
137
250
  throw new DiscoveryError(`no configuration discovery derived could be built; ${first.id}: ${first.error}`, []);
138
251
  }
@@ -144,8 +257,19 @@ function readProjects(engine, host, scope, config, stage) {
144
257
  },
145
258
  notBuilt,
146
259
  unanswered: session.unanswered,
260
+ componentsUnread: workspace?.componentsUnread ?? [],
261
+ conventionsApplied: conventions.applied,
147
262
  };
148
263
  }
264
+ /**
265
+ * The projects the run the scope and the configuration describe analyzes, each read
266
+ * for the errors and setup failures that decide whether it is analyzed, dropped or
267
+ * fails the run, exactly as the analysis reads them.
268
+ */
269
+ export function runProjects(engine, host, scope, config) {
270
+ const read = readProjects(engine, host, scope, config, (_project, opened) => opened.configuration);
271
+ return { configurations: read.answers, notBuilt: read.notBuilt, unanswered: read.unanswered };
272
+ }
149
273
  /**
150
274
  * The root set of the run the scope and the configuration describe.
151
275
  *
@@ -154,7 +278,7 @@ function readProjects(engine, host, scope, config, stage) {
154
278
  */
155
279
  export function runRoots(engine, host, scope, config, provenance) {
156
280
  const targetRoot = scope.target.path;
157
- const read = readProjects(engine, host, scope, config, (_project, projectRead) => projectRead);
281
+ const read = readProjects(engine, host, scope, config, (_project, opened) => opened.read());
158
282
  const projects = read.answers;
159
283
  const configurations = [];
160
284
  const merged = new Map();
@@ -217,36 +341,53 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
217
341
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
218
342
  : readTemplates(host, targetRoot, config.analysis);
219
343
  const consumers = scope.consumers.map((consumer) => consumer.path);
220
- const read = readProjects(engine, host, scope, config, (project, projectRead) => {
344
+ const read = readProjects(engine, host, scope, config, (project, opened) => {
345
+ const projectRead = opened.read();
221
346
  const resolved = references(project, projectRead.held, targetRoot, {
222
347
  testFiles: config.ts.testFiles,
223
348
  });
349
+ const support = testSupportFiles(projectRead.held.symbols, resolved.references, projectRead.rooted.liveUnderReachability, resolved.testFilePaths);
350
+ const made = supportReferences(resolved.references, support);
224
351
  const configured = {
225
352
  configuration: projectRead.configuration,
226
353
  symbols: projectRead.held.symbols,
227
- references: resolved.references,
354
+ references: made,
228
355
  roots: projectRead.rooted.liveUnderReachability,
229
356
  testFiles: resolved.testFilePaths,
357
+ ...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
358
+ ...(projectRead.referenceOnly.length === 0
359
+ ? {}
360
+ : { referenceOnly: projectRead.referenceOnly }),
230
361
  };
231
362
  const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
232
363
  disabled,
233
364
  mode,
234
- testFiles: new Set(resolved.testFilePaths),
365
+ testFiles: new Set([...resolved.testFilePaths, ...support]),
235
366
  });
236
367
  const unanswered = [
237
368
  ...projectRead.held.unanswered,
238
369
  ...projectRead.rooted.unanswered,
239
- ...resolved.references
370
+ ...made
240
371
  .filter((reference) => reference.resolution === "by-name")
241
372
  .map((reference) => reference.to),
242
373
  ...exempt.filter(isUnansweredRecord).map((record) => record.id),
243
374
  ];
375
+ const components = project.ownSourceFiles().filter(isComponentFile);
376
+ const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
244
377
  return {
245
378
  configFile: projectRead.configFile,
246
379
  configured,
247
380
  exempt,
248
381
  unanswered,
249
- extra: extra(project, projectRead, resolved.references),
382
+ typeErrorSkips: projectRead.typeErrorSkips,
383
+ skipped: projectRead.skipped,
384
+ componentFiles: components.map(pathOf),
385
+ componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
386
+ path: pathOf(file),
387
+ line: lineOf(file.originalText, warning.offset),
388
+ reason: warning.reason,
389
+ }))),
390
+ extra: extra(project, projectRead, made),
250
391
  };
251
392
  });
252
393
  const consumed = read.consumers.references;
@@ -272,8 +413,33 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
272
413
  consumers: read.consumers.loaded,
273
414
  notBuilt: read.notBuilt,
274
415
  unanswered: read.unanswered,
416
+ componentsUnread: read.componentsUnread,
417
+ conventionsApplied: read.conventionsApplied,
275
418
  };
276
419
  }
420
+ /**
421
+ * The run swept under one set of marks with the records that hold. A record a component
422
+ * file's markup holds counts only while the file is live, and what it holds may be what
423
+ * keeps the file live, so each file's markup joins only once a sweep without it finds the
424
+ * file live, until a sweep finds no further file live.
425
+ */
426
+ function holdingSweep(read, input) {
427
+ const paths = new Set(read.exempt.flatMap((record) => record.whileLive ?? []));
428
+ let live = new Set();
429
+ for (;;) {
430
+ const swept = {
431
+ ...input,
432
+ exempt: holdingWhile(read.exempt, live),
433
+ unanswered: read.unansweredHeld,
434
+ };
435
+ const result = sweepMatrix(read.matrix, swept);
436
+ const next = new Set(liveFilesAt(read.matrix, result, paths).map((one) => one.position.path));
437
+ if (next.size === live.size) {
438
+ return { swept, result };
439
+ }
440
+ live = next;
441
+ }
442
+ }
277
443
  /**
278
444
  * The sweep of a run read once, under one set of marks. Where a question went unanswered,
279
445
  * the run is swept once more without what that question could have kept live and without
@@ -282,8 +448,8 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
282
448
  * every declaration that makes such a reference.
283
449
  */
284
450
  function sweptOf(read, input) {
285
- const swept = { ...input, exempt: read.exempt, unanswered: read.unansweredHeld };
286
- const result = sweepMatrix(read.matrix, swept);
451
+ const { swept, result } = holdingSweep(read, input);
452
+ const exempt = swept.exempt ?? [];
287
453
  const held = new Set(read.unansweredHeld);
288
454
  if (held.size > 0) {
289
455
  // What a declaration reads where the checker did not answer is unknown, so a rule
@@ -302,7 +468,7 @@ function sweptOf(read, input) {
302
468
  })));
303
469
  const bare = {
304
470
  ...swept,
305
- exempt: read.exempt.filter((record) => !isUnansweredRecord(record)),
471
+ exempt: exempt.filter((record) => !isUnansweredRecord(record)),
306
472
  unanswered: [],
307
473
  };
308
474
  for (const candidate of sweepMatrix(answered, bare).candidates) {
@@ -316,10 +482,13 @@ function sweptOf(read, input) {
316
482
  return {
317
483
  matrix: read.matrix,
318
484
  sweep: result,
485
+ exempt,
319
486
  retained: retainedIn(read.matrix, swept),
320
487
  notBuilt: read.notBuilt,
321
488
  unanswered: read.unanswered,
322
489
  heldByUnanswered: [...held],
490
+ skipped: read.projects.flatMap((one) => one.skipped),
491
+ componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(compare),
323
492
  };
324
493
  }
325
494
  /**
@@ -341,11 +510,11 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
341
510
  const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
342
511
  deprecation: deprecatedDeclarations(project, read.held),
343
512
  accessors: accessorsOf(project, read.held),
344
- needs: projectNeeds(project, read.held, host),
513
+ needs: projectNeeds(project, read.held, host, targetRoot),
345
514
  implementations: implementations(project, read.held),
346
515
  heldByInclusion: heldByInclusion(project, targetRoot),
347
516
  rooted: read.rooted,
348
- directives: inlineDirectives(project, read.held, targetRoot),
517
+ directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
349
518
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
350
519
  parts: projectParts(project, read.held, references, targetRoot),
351
520
  }));
@@ -371,7 +540,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
371
540
  stores: storesOf(projects.map((one) => ({
372
541
  references: one.configured.references,
373
542
  accessors: one.extra.accessors,
374
- })), read.exempt, input.mode),
543
+ })), swept.exempt, input.mode),
375
544
  dependencies: dependenciesOf(host, targetRoot, projects.map((one) => one.extra.needs), swept.sweep.candidates.map((candidate) => candidate.id)),
376
545
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
377
546
  files: {
@@ -387,7 +556,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
387
556
  edges: readEdgeSides(host, targetRoot),
388
557
  },
389
558
  selfCheck,
390
- intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...read.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
559
+ intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
391
560
  };
392
561
  }
393
562
  /**
@@ -424,10 +593,18 @@ function readAnalysis(engine, host, scope, config, provenance, mode, detectors)
424
593
  id: one.configured.configuration,
425
594
  configFile: one.configFile,
426
595
  })),
427
- testFileRules: testFileRulesOf(config.ts.testFiles, read.projects.flatMap((one) => one.configured.testFiles)),
596
+ testFileRules: testFileRulesOf(config.ts.testFiles, read.projects.flatMap((one) => one.configured.testFiles), read.projects.flatMap((one) => one.configured.supportFiles ?? [])),
597
+ typeErrorSkips: read.projects.flatMap((one) => one.typeErrorSkips),
598
+ componentWarnings: [
599
+ ...new Map(read.projects
600
+ .flatMap((one) => one.componentWarnings)
601
+ .map((one) => [`${one.path}:${String(one.line)}:${one.reason}`, one])).values(),
602
+ ],
428
603
  consumers: read.consumers,
429
604
  notBuilt: read.notBuilt,
430
605
  unanswered: read.unanswered,
606
+ componentsUnread: read.componentsUnread,
607
+ conventionsApplied: read.conventionsApplied,
431
608
  },
432
609
  configured: read.projects.map((one) => one.configured),
433
610
  symbols,
@@ -475,6 +652,22 @@ export function runAnalysis(engine, host, scope, config, provenance, mode, detec
475
652
  const inputs = reader.inputsUnder(readBaseline(host, scope.target.path, reader.symbols));
476
653
  return analysisOf(reader, inputs);
477
654
  }
655
+ /**
656
+ * One count per configuration of the run in which the checker left a question unanswered:
657
+ * the questions asked in its project, and the declarations of its own the run holds for them.
658
+ */
659
+ export function unansweredCounts(analysis) {
660
+ const held = new Set(analysis.swept.heldByUnanswered);
661
+ return analysis.run.projects.flatMap((project) => {
662
+ const questions = analysis.run.unanswered.filter((one) => one.configFile === project.configFile).length;
663
+ if (questions === 0) {
664
+ return [];
665
+ }
666
+ const symbols = analysis.configured.find((one) => one.configuration === project.id)?.symbols;
667
+ const declarations = new Set((symbols ?? []).map((one) => one.id).filter((id) => held.has(id)));
668
+ return [{ configuration: project.id, questions, declarations: declarations.size }];
669
+ });
670
+ }
478
671
  function analysisOf(reader, inputs) {
479
672
  return {
480
673
  result: findingsPass(inputs),