@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +106 -10
- package/dist/src/augmentations.js +21 -8
- package/dist/src/automatic-types.js +189 -0
- package/dist/src/base-url.js +18 -0
- package/dist/src/components.js +7 -6
- package/dist/src/config.js +1 -0
- package/dist/src/configuration-files.js +50 -12
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +75 -0
- package/dist/src/conventions.js +101 -5
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +183 -28
- package/dist/src/discover.js +132 -11
- package/dist/src/entry-point-gaps.js +7 -10
- package/dist/src/entry-points.js +112 -11
- package/dist/src/findings/completion.js +116 -21
- package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +44 -13
- package/dist/src/findings/non-code-artifacts.js +66 -33
- package/dist/src/findings/unused-declarations.js +21 -1
- package/dist/src/findings/visibility-narrowing.js +22 -3
- package/dist/src/generated-file.js +11 -0
- package/dist/src/git-ignore.js +86 -0
- package/dist/src/graph.js +4 -1
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +297 -45
- package/dist/src/inventory.js +52 -8
- package/dist/src/kinds.js +13 -13
- package/dist/src/matrix.js +18 -9
- package/dist/src/mdx-entries.js +197 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/overrides.js +41 -10
- package/dist/src/print.js +1 -0
- package/dist/src/ref.js +10 -0
- package/dist/src/references.js +4 -1
- package/dist/src/report.js +4 -2
- package/dist/src/resolve.js +2 -0
- package/dist/src/roots.js +110 -38
- package/dist/src/schema.js +1 -0
- package/dist/src/serialization-contract.js +54 -8
- package/dist/src/session.js +7 -0
- package/dist/src/source-files.js +39 -14
- package/dist/src/suppress-file.js +10 -2
- package/dist/src/suppress-inline.js +18 -2
- package/dist/src/suppress.js +7 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +121 -17
- package/dist/src/type-errors.js +34 -7
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/verbs/analyze.js +16 -0
- package/dist/src/verbs/explain.js +4 -2
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +119 -22
- package/dist/src/workspace-programs.js +7 -3
- package/dist/src/workspace-resolution.js +53 -10
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +143 -10
- package/src/augmentations.ts +21 -10
- package/src/automatic-types.ts +246 -0
- package/src/base-url.ts +24 -0
- package/src/components.ts +7 -6
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +66 -12
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +103 -0
- package/src/conventions.ts +121 -7
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +247 -41
- package/src/discover.ts +156 -13
- package/src/entry-point-gaps.ts +8 -11
- package/src/entry-points.ts +133 -11
- package/src/finding.ts +10 -0
- package/src/findings/completion.ts +147 -24
- package/src/findings/dependencies-and-module-machinery.ts +90 -28
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +56 -14
- package/src/findings/non-code-artifacts.ts +81 -41
- package/src/findings/unused-declarations.ts +25 -1
- package/src/findings/visibility-narrowing.ts +32 -3
- package/src/generated-file.ts +16 -0
- package/src/git-ignore.ts +98 -0
- package/src/graph.ts +6 -0
- package/src/host.ts +5 -9
- package/src/import-bindings.ts +128 -0
- package/src/indexed-access.ts +152 -30
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +394 -59
- package/src/inventory.ts +71 -7
- package/src/kinds.ts +13 -13
- package/src/matrix.ts +32 -14
- package/src/mdx-entries.ts +241 -0
- package/src/module-declarations.ts +11 -13
- package/src/overrides.ts +51 -12
- package/src/print.ts +1 -0
- package/src/ref.ts +12 -0
- package/src/references.ts +10 -1
- package/src/report.ts +16 -3
- package/src/resolve.ts +3 -0
- package/src/roots.ts +163 -45
- package/src/schema.ts +1 -0
- package/src/serialization-contract.ts +55 -4
- package/src/session.ts +7 -0
- package/src/source-files.ts +66 -16
- package/src/suppress-file.ts +12 -1
- package/src/suppress-inline.ts +18 -1
- package/src/suppress.ts +9 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +149 -16
- package/src/type-errors.ts +38 -6
- package/src/type-parameter-reads.ts +227 -0
- package/src/verbs/analyze.ts +22 -0
- package/src/verbs/explain.ts +5 -2
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +127 -22
- package/src/workspace-programs.ts +7 -3
- package/src/workspace-resolution.ts +62 -10
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The source files a CI workflow's `run:` steps
|
|
3
|
-
* A workflow file is read as text, line by line, with no
|
|
2
|
+
* The source files a CI workflow's `run:` steps and the repository's git hooks name by their
|
|
3
|
+
* path from the target root. A workflow or hook file is read as text, line by line, with no
|
|
4
|
+
* YAML parsed.
|
|
4
5
|
*/
|
|
5
6
|
import { scriptTokens } from "./manifest.js";
|
|
6
|
-
import { joinPath, resolvePath } from "./paths.js";
|
|
7
|
+
import { joinPath, relativePath, resolvePath } from "./paths.js";
|
|
7
8
|
const WORKFLOWS = ".github/workflows";
|
|
8
9
|
/** A line holding a run command: indentation and one sequence marker before `run:`. */
|
|
9
10
|
const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
|
|
11
|
+
/** The directories whose every file is a git hook script, run as the shell runs it. */
|
|
12
|
+
const HOOK_DIRS = [".husky", ".githooks"];
|
|
13
|
+
/** The hook manager files whose commands are written under a key, with that key. */
|
|
14
|
+
const HOOK_MANAGERS = [
|
|
15
|
+
[".pre-commit-config.yaml", /^(\s*(?:- )?)entry:(.*)$/u],
|
|
16
|
+
["lefthook.yml", RUN_LINE],
|
|
17
|
+
["lefthook.yaml", RUN_LINE],
|
|
18
|
+
[".lefthook.yml", RUN_LINE],
|
|
19
|
+
];
|
|
20
|
+
/** The names GNU make reads a makefile under, in the order it tries them. */
|
|
21
|
+
const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"];
|
|
22
|
+
/** The operators that end one shell command of a recipe line. */
|
|
23
|
+
const COMMAND_SEPARATORS = /&&|\|\||;|\|/u;
|
|
10
24
|
const BLOCK_SCALAR = /^[|>][+-]?$/u;
|
|
11
25
|
const QUOTED = /^(["'])(.*)\1$/su;
|
|
12
26
|
/** A token holding one of these is computed or a pattern, so it names no file. */
|
|
13
27
|
const COMPUTED = /[${*?[]/u;
|
|
14
28
|
const INDENT = /^\s*/u;
|
|
15
|
-
|
|
29
|
+
/** The files directly in one directory whose name `accepts`, in name order. */
|
|
30
|
+
function filesIn(host, dir, accepts) {
|
|
16
31
|
let entries;
|
|
17
32
|
try {
|
|
18
33
|
entries = host.readDirectory(dir);
|
|
@@ -21,16 +36,24 @@ function workflowFiles(host, dir) {
|
|
|
21
36
|
return [];
|
|
22
37
|
}
|
|
23
38
|
return entries
|
|
24
|
-
.filter((entry) => !entry.directory &&
|
|
39
|
+
.filter((entry) => !entry.directory && accepts(entry.name))
|
|
25
40
|
.map((entry) => joinPath(dir, entry.name))
|
|
26
41
|
.sort();
|
|
27
42
|
}
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
function readText(host, path) {
|
|
44
|
+
try {
|
|
45
|
+
return host.readFile(path);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Every command line one workflow file's `run:` steps, or the steps under `key`, hold. */
|
|
52
|
+
export function runCommands(text, key = RUN_LINE) {
|
|
30
53
|
const lines = text.split(/\r?\n/u);
|
|
31
54
|
const found = [];
|
|
32
55
|
for (let at = 0; at < lines.length; at += 1) {
|
|
33
|
-
const match =
|
|
56
|
+
const match = key.exec(lines[at] ?? "");
|
|
34
57
|
if (match === null) {
|
|
35
58
|
continue;
|
|
36
59
|
}
|
|
@@ -50,28 +73,92 @@ export function runCommands(text) {
|
|
|
50
73
|
}
|
|
51
74
|
return found;
|
|
52
75
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
81
|
+
*/
|
|
82
|
+
function recipeTokens(host, root) {
|
|
83
|
+
const path = MAKEFILES.map((name) => joinPath(root, name)).find((candidate) => host.kindOf(candidate) === "file");
|
|
84
|
+
const text = path === undefined ? undefined : readText(host, path);
|
|
85
|
+
if (text === undefined) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
56
88
|
const found = [];
|
|
57
|
-
for (const
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
text = host.readFile(path);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
89
|
+
for (const line of text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
|
|
90
|
+
if (!line.startsWith("\t")) {
|
|
63
91
|
continue;
|
|
64
92
|
}
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
let dir = root;
|
|
94
|
+
for (const command of withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS)) {
|
|
95
|
+
const tokens = scriptTokens(command);
|
|
96
|
+
const [first, target, ...rest] = tokens;
|
|
97
|
+
if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
|
|
98
|
+
dir = resolvePath(dir, target);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const token of tokens) {
|
|
102
|
+
found.push({ token, dir: relativePath(root, dir) ?? "" });
|
|
103
|
+
}
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
106
|
return found;
|
|
70
107
|
}
|
|
108
|
+
/** One shell line up to its comment: a `#` that starts a word outside quotes. */
|
|
109
|
+
function withoutComment(line) {
|
|
110
|
+
let quote = "";
|
|
111
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
112
|
+
const char = line[at];
|
|
113
|
+
if (quote !== "") {
|
|
114
|
+
if (char === quote) {
|
|
115
|
+
quote = "";
|
|
116
|
+
}
|
|
117
|
+
else if (char === "\\" && quote === '"') {
|
|
118
|
+
at += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (char === "'" || char === '"') {
|
|
122
|
+
quote = char;
|
|
123
|
+
}
|
|
124
|
+
else if (char === "\\") {
|
|
125
|
+
at += 1;
|
|
126
|
+
}
|
|
127
|
+
else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
|
|
128
|
+
return line.slice(0, at);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return line;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Every token the target root's workflow steps, git hooks and makefile hold: the `run:`
|
|
135
|
+
* steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
|
|
136
|
+
* and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
|
|
137
|
+
* of a lefthook file and the makefile's recipe lines.
|
|
138
|
+
*/
|
|
139
|
+
export function workflowTokens(host, targetRoot) {
|
|
140
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
141
|
+
const commands = [];
|
|
142
|
+
for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
|
|
143
|
+
commands.push(...runCommands(readText(host, path) ?? ""));
|
|
144
|
+
}
|
|
145
|
+
for (const dir of HOOK_DIRS) {
|
|
146
|
+
for (const path of filesIn(host, joinPath(root, dir), () => true)) {
|
|
147
|
+
commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const [name, key] of HOOK_MANAGERS) {
|
|
151
|
+
commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
|
|
152
|
+
}
|
|
153
|
+
return [
|
|
154
|
+
...commands.flatMap((command) => scriptTokens(command)),
|
|
155
|
+
...recipeTokens(host, root).map(({ token }) => token),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
71
158
|
/**
|
|
72
|
-
* The own files the
|
|
73
|
-
*
|
|
74
|
-
* without a leading `./`, whatever directory the step runs in.
|
|
159
|
+
* The own files the target root's workflow steps and git hooks name, as
|
|
160
|
+
* {@link workflowTokens} reads them: a token equal to a file's path below the target root,
|
|
161
|
+
* with or without a leading `./`, whatever directory the step runs in.
|
|
75
162
|
*/
|
|
76
163
|
export function workflowEntries(host, targetRoot, files) {
|
|
77
164
|
const found = [];
|
|
@@ -84,5 +171,15 @@ export function workflowEntries(host, targetRoot, files) {
|
|
|
84
171
|
found.push({ file, source: token });
|
|
85
172
|
}
|
|
86
173
|
}
|
|
174
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
175
|
+
for (const { token, dir } of recipeTokens(host, root)) {
|
|
176
|
+
const below = COMPUTED.test(token) || dir === ""
|
|
177
|
+
? undefined
|
|
178
|
+
: relativePath(root, resolvePath(joinPath(root, dir), token));
|
|
179
|
+
const file = below === undefined ? undefined : files.byPath.get(below);
|
|
180
|
+
if (file !== undefined) {
|
|
181
|
+
found.push({ file, source: token });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
87
184
|
return found;
|
|
88
185
|
}
|
|
@@ -7,18 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ModuleKind, ModuleResolutionKind } from "@typescript/native/sync";
|
|
9
9
|
import { isStringLiteralLikeNode } from "@typescript/native/ast";
|
|
10
|
+
import { baseUrlPaths } from "./base-url.js";
|
|
10
11
|
import { dirnamePath, resolvePath } from "./paths.js";
|
|
11
12
|
/** The most questions one round trip carries. */
|
|
12
13
|
const BATCH = 2048;
|
|
13
14
|
/**
|
|
14
15
|
* The options a configuration's program is built with: its own, less the ones that
|
|
15
|
-
* lay out an emitted program's inputs and outputs
|
|
16
|
+
* lay out an emitted program's inputs and outputs, and with `paths` read against the
|
|
17
|
+
* effective `baseUrl` as {@link baseUrlPaths} states. The analysis emits nothing, and a
|
|
16
18
|
* member's source is outside the importer's root directory by construction, which
|
|
17
19
|
* the compiler refuses under such a layout.
|
|
18
20
|
*/
|
|
19
21
|
function analysisOptions(parsed) {
|
|
20
22
|
const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
|
|
21
|
-
|
|
23
|
+
const paths = baseUrlPaths(parsed.options);
|
|
24
|
+
const options = paths === undefined ? kept : { ...kept, paths };
|
|
25
|
+
return composite === true ? { ...options, declaration: true } : options;
|
|
22
26
|
}
|
|
23
27
|
/** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
|
|
24
28
|
function conditionsOf(parsed, mode) {
|
|
@@ -73,7 +77,7 @@ function configurationOf(engine, host, reading) {
|
|
|
73
77
|
const raw = options["pathsBasePath"];
|
|
74
78
|
const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
|
|
75
79
|
const paths = {};
|
|
76
|
-
for (const [key, targets] of Object.entries(options.paths ?? {})) {
|
|
80
|
+
for (const [key, targets] of Object.entries(baseUrlPaths(options) ?? options.paths ?? {})) {
|
|
77
81
|
paths[key] = targets.map((target) => resolvePath(base, target));
|
|
78
82
|
}
|
|
79
83
|
for (const entry of reading.entries) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which file an import of a workspace package's name is read from
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the package
|
|
7
|
-
* import nothing answers for is unresolved.
|
|
2
|
+
* Which file an import of a workspace package's name is read from: the package
|
|
3
|
+
* `node_modules` links the importer to or, with no link, the one its manifest depends on.
|
|
4
|
+
* A source file the compiler reaches stands; any other file reads back to its source or to
|
|
5
|
+
* a manifest target that is source, an ignored file to the source its path mirrors, a file
|
|
6
|
+
* the package neither compiles nor writes is read as installed, and the rest is unresolved.
|
|
8
7
|
*/
|
|
9
8
|
import { emittedFrom } from "./emit-map.js";
|
|
9
|
+
import { gitIgnored } from "./git-ignore.js";
|
|
10
10
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
11
11
|
import { satisfies } from "./version-range.js";
|
|
12
12
|
/** The manifest members that name an entry point for the subpath `.` without `exports`. */
|
|
@@ -163,6 +163,27 @@ export function workspaceResolver(host, workspace) {
|
|
|
163
163
|
links.set(key, found);
|
|
164
164
|
return found;
|
|
165
165
|
};
|
|
166
|
+
const root = workspace.members[0]?.dir;
|
|
167
|
+
const ignored = root === undefined ? () => false : gitIgnored(host, root);
|
|
168
|
+
const manifests = new Map();
|
|
169
|
+
/** The nearest manifest at or above one directory, up to the workspace's root. */
|
|
170
|
+
const nearestManifest = (dir) => {
|
|
171
|
+
if (manifests.has(dir)) {
|
|
172
|
+
return manifests.get(dir);
|
|
173
|
+
}
|
|
174
|
+
let found;
|
|
175
|
+
try {
|
|
176
|
+
const value = JSON.parse(host.readFile(joinPath(dir, "package.json")));
|
|
177
|
+
found = isRecord(value) ? { dir, manifest: value } : undefined;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
const up = dirnamePath(dir);
|
|
181
|
+
found =
|
|
182
|
+
up === dir || root === undefined || !inside(root, up) ? undefined : nearestManifest(up);
|
|
183
|
+
}
|
|
184
|
+
manifests.set(dir, found);
|
|
185
|
+
return found;
|
|
186
|
+
};
|
|
166
187
|
const linkOf = (candidates, fromDir) => {
|
|
167
188
|
const name = candidates[0]?.name ?? "";
|
|
168
189
|
const linked = linkedFrom(name, fromDir);
|
|
@@ -174,13 +195,15 @@ export function workspaceResolver(host, workspace) {
|
|
|
174
195
|
if (scope?.name === name) {
|
|
175
196
|
return { kind: "member", member: scope };
|
|
176
197
|
}
|
|
177
|
-
|
|
198
|
+
// A package below the root that no workspace lists depends on a member as a member does.
|
|
199
|
+
const owner = scope ?? nearestManifest(fromDir);
|
|
200
|
+
const manifest = owner?.manifest;
|
|
178
201
|
const declared = DEPENDENCY_MEMBERS.flatMap((field) => {
|
|
179
202
|
const held = manifest?.[field];
|
|
180
203
|
const specifier = isRecord(held) ? held[name] : undefined;
|
|
181
204
|
return typeof specifier === "string" ? [specifier] : [];
|
|
182
205
|
});
|
|
183
|
-
const matched = candidates.filter((member) => declared.some((specifier) => dependsOn(specifier, member,
|
|
206
|
+
const matched = candidates.filter((member) => declared.some((specifier) => dependsOn(specifier, member, owner?.dir ?? fromDir)));
|
|
184
207
|
const [one, two] = matched;
|
|
185
208
|
if (one === undefined) {
|
|
186
209
|
return { kind: "none" };
|
|
@@ -191,6 +214,21 @@ export function workspaceResolver(host, workspace) {
|
|
|
191
214
|
};
|
|
192
215
|
/** The files a member's configurations compile, which is what makes a file there source. */
|
|
193
216
|
const compiled = (member, path) => member.configurations.some((configuration) => configuration.fileNames.has(path));
|
|
217
|
+
/**
|
|
218
|
+
* The member's source an ignored output path mirrors: the path below one of its
|
|
219
|
+
* directories, the deepest first, read below the member's own directory as an output
|
|
220
|
+
* directory's tree mirrors its root directory's, which a build leaves in an ignored
|
|
221
|
+
* directory however its configuration writes it.
|
|
222
|
+
*/
|
|
223
|
+
const mirrored = (member, path) => {
|
|
224
|
+
for (let dir = dirnamePath(path); inside(member.dir, dir); dir = dirnamePath(dir)) {
|
|
225
|
+
const found = emittedFrom(path, { outDir: dir, sourceDirs: [member.dir] }).find((candidate) => SOURCE.test(candidate) && !ignored(candidate) && host.kindOf(candidate) === "file");
|
|
226
|
+
if (found !== undefined) {
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
};
|
|
194
232
|
/**
|
|
195
233
|
* The member's source one of its paths is, or is compiled from: a TypeScript source
|
|
196
234
|
* file, the source an output path maps back to, or a file one of the member's
|
|
@@ -212,6 +250,9 @@ export function workspaceResolver(host, workspace) {
|
|
|
212
250
|
if (found !== undefined) {
|
|
213
251
|
return found;
|
|
214
252
|
}
|
|
253
|
+
if (ignored(path)) {
|
|
254
|
+
return mirrored(member, path);
|
|
255
|
+
}
|
|
215
256
|
return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
|
|
216
257
|
};
|
|
217
258
|
/** The targets a member's manifest names for one subpath, the selected one first. */
|
|
@@ -270,9 +311,11 @@ export function workspaceResolver(host, workspace) {
|
|
|
270
311
|
}
|
|
271
312
|
const { member } = linked;
|
|
272
313
|
// A member no compiler configuration builds is read as the package manager
|
|
273
|
-
// installed it, wherever the compiler's resolution found it
|
|
314
|
+
// installed it, wherever the compiler's resolution found it, unless what it found is
|
|
315
|
+
// a file the tree ignores, which no install wrote.
|
|
274
316
|
if (answered !== undefined &&
|
|
275
|
-
(member.configurations.length === 0
|
|
317
|
+
((member.configurations.length === 0 && !ignored(answered.resolvedFileName)) ||
|
|
318
|
+
!inside(member.dir, answered.resolvedFileName))) {
|
|
276
319
|
return { kind: "default" };
|
|
277
320
|
}
|
|
278
321
|
if (answered !== undefined) {
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/node": "24.19.2",
|
|
44
44
|
"eslint": "10.12.0",
|
|
45
45
|
"fast-check": "4.10.2",
|
|
46
|
-
"prettier": "3.9.
|
|
46
|
+
"prettier": "3.9.10",
|
|
47
47
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
48
48
|
"typescript-eslint": "8.71.1",
|
|
49
49
|
"vitest": "5.0.4"
|
package/src/analysis.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* another project's checker, which holds only for views the session hands out.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isStringLiteralLikeNode } from "@typescript/native/ast";
|
|
9
10
|
import { lineOf, readComponent } from "./component-files.ts";
|
|
10
11
|
import { kindOf, type Config, type Provenance } from "./config.ts";
|
|
11
12
|
import {
|
|
@@ -26,9 +27,11 @@ import {
|
|
|
26
27
|
discoverProjects,
|
|
27
28
|
DiscoveryError,
|
|
28
29
|
isGuess,
|
|
30
|
+
MISSING_GLOBAL_TYPE,
|
|
29
31
|
projectErrors,
|
|
30
32
|
renderDiagnostic,
|
|
31
33
|
targetRelative,
|
|
34
|
+
withBaseUrlPaths,
|
|
32
35
|
type LocatedDiagnostic,
|
|
33
36
|
type NotBuilt,
|
|
34
37
|
} from "./discover.ts";
|
|
@@ -70,7 +73,7 @@ import { injectionContainer } from "./injection-container.ts";
|
|
|
70
73
|
import { interfaceSatisfaction, settledSatisfaction } from "./interface-satisfaction.ts";
|
|
71
74
|
import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
|
|
72
75
|
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
73
|
-
import { packageEntries, readManifest } from "./manifest.ts";
|
|
76
|
+
import { packageEntries, readManifest, type ManifestEntry } from "./manifest.ts";
|
|
74
77
|
import {
|
|
75
78
|
liveFilesAt,
|
|
76
79
|
matrixOf,
|
|
@@ -112,7 +115,7 @@ import type { Scope } from "./scope.ts";
|
|
|
112
115
|
import { serializationContract } from "./serialization-contract.ts";
|
|
113
116
|
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
114
117
|
import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
|
|
115
|
-
import { supportReferences, testSupportFiles } from "./test-support.ts";
|
|
118
|
+
import { supportReferences, testSupportFiles, type TestEvidence } from "./test-support.ts";
|
|
116
119
|
import {
|
|
117
120
|
diagnosticsOf,
|
|
118
121
|
ownedAs,
|
|
@@ -390,6 +393,11 @@ function readProjects<Answer>(
|
|
|
390
393
|
publishedMembers: new Set(
|
|
391
394
|
(workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
|
|
392
395
|
),
|
|
396
|
+
runFiles: new Set(
|
|
397
|
+
opened.flatMap((one) =>
|
|
398
|
+
one.fileNames().map((name) => resolvePath(host.workingDirectory(), name)),
|
|
399
|
+
),
|
|
400
|
+
),
|
|
393
401
|
};
|
|
394
402
|
};
|
|
395
403
|
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
@@ -420,6 +428,12 @@ function readProjects<Answer>(
|
|
|
420
428
|
const joins = discovered.derived
|
|
421
429
|
? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
|
|
422
430
|
: new Map<string, readonly string[]>();
|
|
431
|
+
const programs = withBaseUrlPaths(
|
|
432
|
+
engine,
|
|
433
|
+
host,
|
|
434
|
+
[...discovered.configFiles, ...consumerOf.keys()],
|
|
435
|
+
workspace?.programs ?? new Map(),
|
|
436
|
+
);
|
|
423
437
|
const session = runSession(
|
|
424
438
|
engine,
|
|
425
439
|
[...discovered.configFiles, ...consumerOf.keys()],
|
|
@@ -514,11 +528,29 @@ function readProjects<Answer>(
|
|
|
514
528
|
error: renderDiagnostic(one, (path) => relativePath(absoluteRoot, path) ?? path),
|
|
515
529
|
})),
|
|
516
530
|
);
|
|
531
|
+
const missingGlobal = raw.unfound.find(
|
|
532
|
+
({ diagnostic }) => diagnostic.code === MISSING_GLOBAL_TYPE,
|
|
533
|
+
);
|
|
534
|
+
const skips =
|
|
535
|
+
missingGlobal === undefined
|
|
536
|
+
? reading.skips
|
|
537
|
+
: [
|
|
538
|
+
...reading.skips,
|
|
539
|
+
...project.ownSourceFiles().map((file) => ({
|
|
540
|
+
record: {
|
|
541
|
+
path: relativePath(absoluteRoot, file.fileName) ?? file.fileName,
|
|
542
|
+
line: 1,
|
|
543
|
+
message: missingGlobal.diagnostic.text,
|
|
544
|
+
},
|
|
545
|
+
file,
|
|
546
|
+
unit: file,
|
|
547
|
+
})),
|
|
548
|
+
];
|
|
517
549
|
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
518
550
|
const read = (): ProjectRead => {
|
|
519
551
|
const held = inventory(project, host, targetRoot);
|
|
520
552
|
const found = roots(project, held, targetRoot, options);
|
|
521
|
-
const kept = skippedRoots(project, held,
|
|
553
|
+
const kept = skippedRoots(project, held, skips);
|
|
522
554
|
const rooted =
|
|
523
555
|
kept.length === 0
|
|
524
556
|
? found
|
|
@@ -535,8 +567,8 @@ function readProjects<Answer>(
|
|
|
535
567
|
configFile: project.configFile,
|
|
536
568
|
held,
|
|
537
569
|
rooted,
|
|
538
|
-
typeErrorSkips:
|
|
539
|
-
skipped: skippedUnits(
|
|
570
|
+
typeErrorSkips: skips.map((one) => one.record),
|
|
571
|
+
skipped: skippedUnits(skips, absoluteRoot),
|
|
540
572
|
referenceOnly: project
|
|
541
573
|
.ownSourceFiles()
|
|
542
574
|
.filter((file) => referenceOnly(file.fileName))
|
|
@@ -551,9 +583,7 @@ function readProjects<Answer>(
|
|
|
551
583
|
return stage(project, { configuration, read });
|
|
552
584
|
},
|
|
553
585
|
discovered.derived ? "record" : "refuse",
|
|
554
|
-
joins.size === 0
|
|
555
|
-
? workspace?.programs
|
|
556
|
-
: withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins),
|
|
586
|
+
joins.size === 0 ? programs : withJoinedTests(engine, host, programs, joins),
|
|
557
587
|
() => {
|
|
558
588
|
outsideTests = testFilesAt(
|
|
559
589
|
host,
|
|
@@ -759,6 +789,8 @@ interface SweptProject<Extra> {
|
|
|
759
789
|
/** The component files the project holds of its own, by path below the target root. */
|
|
760
790
|
readonly componentFiles: readonly string[];
|
|
761
791
|
readonly componentWarnings: readonly ComponentWarning[];
|
|
792
|
+
/** The manifest entries the project reads back to no file of its own. */
|
|
793
|
+
readonly unmappedEntries: readonly ManifestEntry[];
|
|
762
794
|
readonly extra: Extra;
|
|
763
795
|
}
|
|
764
796
|
|
|
@@ -770,6 +802,8 @@ interface ReadProject<Extra> {
|
|
|
770
802
|
readonly evidence: readonly Exemption[];
|
|
771
803
|
readonly componentFiles: readonly string[];
|
|
772
804
|
readonly componentWarnings: readonly ComponentWarning[];
|
|
805
|
+
/** Per own file below the target root, the module specifiers its imports write. */
|
|
806
|
+
readonly imports: ReadonlyMap<string, readonly string[]>;
|
|
773
807
|
readonly extra: Extra;
|
|
774
808
|
}
|
|
775
809
|
|
|
@@ -800,6 +834,64 @@ interface ReadRun<Extra> {
|
|
|
800
834
|
readonly nestedDirs: readonly string[];
|
|
801
835
|
}
|
|
802
836
|
|
|
837
|
+
/** The manifest section of a package test evidence counts, and the sections that keep one out of it. */
|
|
838
|
+
const DEV_SECTION = "devDependencies";
|
|
839
|
+
const OTHER_SECTIONS = ["dependencies", "peerDependencies", "optionalDependencies"] as const;
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The imports of a run's own files and the manifests below the target root that test
|
|
843
|
+
* evidence reads: a package is a devDependency of a file where the nearest manifest at or
|
|
844
|
+
* above it that declares the package declares it under `devDependencies` alone.
|
|
845
|
+
*/
|
|
846
|
+
function testEvidence(
|
|
847
|
+
host: Host,
|
|
848
|
+
targetRoot: string,
|
|
849
|
+
projects: readonly { readonly imports: ReadonlyMap<string, readonly string[]> }[],
|
|
850
|
+
): TestEvidence {
|
|
851
|
+
const imports = new Map<string, readonly string[]>();
|
|
852
|
+
for (const one of projects) {
|
|
853
|
+
for (const [path, specifiers] of one.imports) {
|
|
854
|
+
imports.set(path, specifiers);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>();
|
|
858
|
+
const manifestAt = (dir: string): Readonly<Record<string, unknown>> | undefined => {
|
|
859
|
+
if (!manifests.has(dir)) {
|
|
860
|
+
let value: unknown;
|
|
861
|
+
try {
|
|
862
|
+
value = JSON.parse(host.readFile(joinPath(dir, "package.json")));
|
|
863
|
+
} catch {
|
|
864
|
+
value = undefined;
|
|
865
|
+
}
|
|
866
|
+
manifests.set(
|
|
867
|
+
dir,
|
|
868
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
869
|
+
? (value as Record<string, unknown>)
|
|
870
|
+
: undefined,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
return manifests.get(dir);
|
|
874
|
+
};
|
|
875
|
+
const declares = (section: unknown, name: string): boolean =>
|
|
876
|
+
typeof section === "object" && section !== null && Object.hasOwn(section, name);
|
|
877
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
878
|
+
return {
|
|
879
|
+
imports,
|
|
880
|
+
devDependency: (path, name) => {
|
|
881
|
+
for (let dir = dirnamePath(joinPath(root, path)); ; dir = dirnamePath(dir)) {
|
|
882
|
+
const manifest = manifestAt(dir);
|
|
883
|
+
const other = OTHER_SECTIONS.some((one) => declares(manifest?.[one], name));
|
|
884
|
+
if (declares(manifest?.[DEV_SECTION], name) || other) {
|
|
885
|
+
return !other;
|
|
886
|
+
}
|
|
887
|
+
if (normalizePath(dir) === normalizePath(root) || dirnamePath(dir) === dir) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
803
895
|
/**
|
|
804
896
|
* Reads every project of the run, its references and exemptions, and `extra`'s answer,
|
|
805
897
|
* while the project's view is open, and merges the projects into one matrix in the run's
|
|
@@ -856,6 +948,14 @@ function readRun<Extra>(
|
|
|
856
948
|
testFiles: resolved.testFilePaths,
|
|
857
949
|
evidence,
|
|
858
950
|
componentFiles: components.map(pathOf),
|
|
951
|
+
imports: new Map(
|
|
952
|
+
project.ownSourceFiles().flatMap((file) => {
|
|
953
|
+
const path = relativePath(targetRoot, file.fileName);
|
|
954
|
+
return path === undefined
|
|
955
|
+
? []
|
|
956
|
+
: [[path, file.imports.filter(isStringLiteralLikeNode).map((one) => one.text)]];
|
|
957
|
+
}),
|
|
958
|
+
),
|
|
859
959
|
componentWarnings: components.flatMap((file) =>
|
|
860
960
|
readComponent(file.originalText, extensionOf(file.fileName)).warnings.map((warning) => ({
|
|
861
961
|
path: pathOf(file),
|
|
@@ -882,6 +982,7 @@ function readRun<Extra>(
|
|
|
882
982
|
withOutside.flatMap((one) => one.references),
|
|
883
983
|
withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
|
|
884
984
|
withOutside.flatMap((one) => one.testFiles),
|
|
985
|
+
testEvidence(host, targetRoot, withOutside),
|
|
885
986
|
);
|
|
886
987
|
const settled = settledSatisfaction(
|
|
887
988
|
withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })),
|
|
@@ -903,8 +1004,9 @@ function readRun<Extra>(
|
|
|
903
1004
|
symbols: projectRead.held.symbols,
|
|
904
1005
|
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
905
1006
|
roots: projectRead.rooted.liveUnderReachability,
|
|
906
|
-
testFiles: one.testFiles,
|
|
1007
|
+
testFiles: [...one.testFiles, ...supportFiles],
|
|
907
1008
|
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
1009
|
+
...(projectRead.held.ambient.size === 0 ? {} : { ambient: projectRead.held.ambient }),
|
|
908
1010
|
...(projectRead.referenceOnly.length === 0
|
|
909
1011
|
? {}
|
|
910
1012
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
@@ -925,6 +1027,7 @@ function readRun<Extra>(
|
|
|
925
1027
|
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
926
1028
|
],
|
|
927
1029
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
1030
|
+
unmappedEntries: projectRead.rooted.unmappedEntries,
|
|
928
1031
|
skipped: projectRead.skipped,
|
|
929
1032
|
componentFiles: one.componentFiles,
|
|
930
1033
|
componentWarnings: one.componentWarnings,
|
|
@@ -1190,7 +1293,8 @@ export function readEmitterRun(
|
|
|
1190
1293
|
* The workspace members whose dependencies the run judges: `read` holds those a built
|
|
1191
1294
|
* project holds a file below, `unbuilt` the others whose own directory, the nearest
|
|
1192
1295
|
* manifest's above the configuration, holds a configuration the run could not build. A
|
|
1193
|
-
* member no configuration holds a file of is in neither.
|
|
1296
|
+
* member no configuration holds a file of is in neither. A nested package no built project
|
|
1297
|
+
* holds a file below joins `unbuilt` the same way.
|
|
1194
1298
|
*/
|
|
1195
1299
|
function judgedMembers(
|
|
1196
1300
|
host: Host,
|
|
@@ -1214,6 +1318,16 @@ function judgedMembers(
|
|
|
1214
1318
|
judged.unbuilt.push(dir);
|
|
1215
1319
|
}
|
|
1216
1320
|
}
|
|
1321
|
+
for (const dir of read.nestedDirs) {
|
|
1322
|
+
const below = relativePath(root, dir);
|
|
1323
|
+
if (
|
|
1324
|
+
below !== undefined &&
|
|
1325
|
+
owners.has(dir) &&
|
|
1326
|
+
![...held].some((path) => path.startsWith(`${below}/`))
|
|
1327
|
+
) {
|
|
1328
|
+
judged.unbuilt.push(dir);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1217
1331
|
return judged;
|
|
1218
1332
|
}
|
|
1219
1333
|
|
|
@@ -1359,6 +1473,7 @@ function readAnalysis(
|
|
|
1359
1473
|
componentsUnread: read.componentsUnread,
|
|
1360
1474
|
librariesUnfound: read.librariesUnfound,
|
|
1361
1475
|
conventionsApplied: read.conventionsApplied,
|
|
1476
|
+
unmappedEntries: unmappedEverywhere(read.projects),
|
|
1362
1477
|
},
|
|
1363
1478
|
configured: read.projects.map((one) => one.configured),
|
|
1364
1479
|
symbols,
|
|
@@ -1410,6 +1525,22 @@ function inputsOver(
|
|
|
1410
1525
|
};
|
|
1411
1526
|
}
|
|
1412
1527
|
|
|
1528
|
+
/** The manifest entries every project of the run reads back to no file, in the first project's order. */
|
|
1529
|
+
function unmappedEverywhere(
|
|
1530
|
+
projects: readonly { readonly unmappedEntries: readonly ManifestEntry[] }[],
|
|
1531
|
+
): readonly ManifestEntry[] {
|
|
1532
|
+
const key = (entry: ManifestEntry): string => `${entry.dir}\u0000${entry.member}`;
|
|
1533
|
+
const [first, ...rest] = projects;
|
|
1534
|
+
const others = rest.map((one) => new Set(one.unmappedEntries.map(key)));
|
|
1535
|
+
const found = new Map<string, ManifestEntry>();
|
|
1536
|
+
for (const entry of first?.unmappedEntries ?? []) {
|
|
1537
|
+
if (others.every((one) => one.has(key(entry)))) {
|
|
1538
|
+
found.set(key(entry), entry);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return [...found.values()];
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1413
1544
|
/** One project of a run: the name the run gives it and the file it was opened from. */
|
|
1414
1545
|
export interface RunProject {
|
|
1415
1546
|
readonly id: string;
|
|
@@ -1439,6 +1570,8 @@ interface RunFacts {
|
|
|
1439
1570
|
readonly librariesUnfound: readonly LibraryUnfound[];
|
|
1440
1571
|
/** The convention rows the run applied. */
|
|
1441
1572
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
1573
|
+
/** The manifest entries no project of the run reads back to a file, each once. */
|
|
1574
|
+
readonly unmappedEntries: readonly ManifestEntry[];
|
|
1442
1575
|
}
|
|
1443
1576
|
|
|
1444
1577
|
/** One configuration whose component files a run cannot read. */
|