@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.
- package/README.md +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +229 -36
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/references.js +209 -30
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +31 -6
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +394 -70
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- 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
|
|
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
|
|
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](
|
|
108
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
109
109
|
|
|
110
110
|
## Disclaimer
|
|
111
111
|
|
|
@@ -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
|
+
}
|
package/dist/bin/node-host.js
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/dist/src/analysis.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
|
108
|
-
failures.push(...errors.load
|
|
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
|
-
|
|
124
|
-
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
286
|
-
const
|
|
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:
|
|
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
|
-
})),
|
|
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([...
|
|
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),
|