@cplieger/deadset-ts 5.1.1 → 5.2.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 +7 -4
- package/dist/src/config.js +3 -0
- package/dist/src/configuration-files.js +23 -24
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +25 -4
- package/dist/src/convention-rows.js +7 -0
- package/dist/src/conventions.js +97 -33
- package/dist/src/dependencies.js +8 -0
- package/dist/src/entry-point-gaps.js +10 -26
- package/dist/src/entry-points.js +4 -0
- package/dist/src/findings/completion.js +27 -11
- package/dist/src/findings/interfaces.js +2 -6
- package/dist/src/findings/intra-function.js +15 -3
- package/dist/src/findings/unused-declarations.js +5 -6
- package/dist/src/findings/visibility-narrowing.js +6 -1
- package/dist/src/generated-file.js +72 -12
- package/dist/src/graph.js +5 -4
- package/dist/src/html-entries.js +137 -0
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +5 -1
- package/dist/src/matrix.js +9 -3
- package/dist/src/namespace-objects.js +158 -0
- package/dist/src/partial.js +1 -1
- package/dist/src/references.js +120 -3
- package/dist/src/roots.js +52 -3
- package/dist/src/sweep.js +17 -0
- package/dist/src/test-support.js +35 -23
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +12 -5
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +28 -24
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +31 -4
- package/src/convention-rows.ts +15 -0
- package/src/conventions.ts +119 -30
- package/src/dependencies.ts +9 -0
- package/src/entry-point-gaps.ts +11 -28
- package/src/entry-points.ts +7 -1
- package/src/exempt.ts +1 -4
- package/src/file-facts.ts +1 -1
- package/src/findings/completion.ts +31 -11
- package/src/findings/interfaces.ts +2 -9
- package/src/findings/intra-function.ts +17 -3
- package/src/findings/unused-declarations.ts +5 -8
- package/src/findings/visibility-narrowing.ts +7 -1
- package/src/generated-file.ts +80 -12
- package/src/graph.ts +5 -4
- package/src/html-entries.ts +170 -0
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +9 -3
- package/src/matrix.ts +21 -3
- package/src/namespace-objects.ts +216 -0
- package/src/partial.ts +1 -1
- package/src/references.ts +161 -3
- package/src/roots.ts +69 -10
- package/src/sweep.ts +19 -0
- package/src/test-support.ts +34 -22
- package/src/version.ts +1 -1
package/dist/src/references.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { contextualUses } from "./contextual-uses.js";
|
|
4
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
5
|
+
import { specifierCandidates } from "./configuration-files.js";
|
|
6
|
+
import { namespaceUses } from "./namespace-objects.js";
|
|
5
7
|
import { globExpression } from "./glob.js";
|
|
6
8
|
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
7
9
|
import { overrideReferences } from "./overrides.js";
|
|
@@ -169,6 +171,44 @@ function markStores(node, writes, reads) {
|
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
}
|
|
174
|
+
/** Whether one node is `import.meta.url`. */
|
|
175
|
+
function isImportMetaURL(node) {
|
|
176
|
+
const at = unparenthesized(node);
|
|
177
|
+
return (isPropertyAccessExpression(at) &&
|
|
178
|
+
isMetaProperty(at.expression) &&
|
|
179
|
+
at.expression.keywordToken === SyntaxKind.ImportKeyword &&
|
|
180
|
+
at.name.text === "url");
|
|
181
|
+
}
|
|
182
|
+
/** The module URL one node builds, where it builds one. */
|
|
183
|
+
function moduleURLOf(node) {
|
|
184
|
+
const relative = (argument) => {
|
|
185
|
+
const at = argument === undefined ? undefined : unparenthesized(argument);
|
|
186
|
+
return at !== undefined &&
|
|
187
|
+
(isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at)) &&
|
|
188
|
+
/^\.\.?\//u.test(at.text)
|
|
189
|
+
? at.text
|
|
190
|
+
: undefined;
|
|
191
|
+
};
|
|
192
|
+
if (isNewExpression(node) && isIdentifier(node.expression) && node.expression.text === "URL") {
|
|
193
|
+
const [reference, base] = node.arguments ?? [];
|
|
194
|
+
const specifier = relative(reference);
|
|
195
|
+
return specifier !== undefined && base !== undefined && isImportMetaURL(base)
|
|
196
|
+
? { specifier, constructor: node.expression, at: node }
|
|
197
|
+
: undefined;
|
|
198
|
+
}
|
|
199
|
+
if (isCallExpression(node)) {
|
|
200
|
+
const callee = unparenthesized(node.expression);
|
|
201
|
+
const specifier = relative(node.arguments[0]);
|
|
202
|
+
if (specifier !== undefined &&
|
|
203
|
+
isPropertyAccessExpression(callee) &&
|
|
204
|
+
isMetaProperty(callee.expression) &&
|
|
205
|
+
callee.expression.keywordToken === SyntaxKind.ImportKeyword &&
|
|
206
|
+
callee.name.text === "resolve") {
|
|
207
|
+
return { specifier, constructor: undefined, at: node };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
172
212
|
/** Whether one node is a string literal or a template literal with no substitution. */
|
|
173
213
|
function isLiteralText(node) {
|
|
174
214
|
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
@@ -286,7 +326,10 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
286
326
|
const reads = [];
|
|
287
327
|
const links = [];
|
|
288
328
|
const evaluations = [];
|
|
329
|
+
const wholes = [];
|
|
330
|
+
const urls = [];
|
|
289
331
|
const decorated = [];
|
|
332
|
+
const namespaces = namespaceUses(file);
|
|
290
333
|
const literals = [];
|
|
291
334
|
const flows = [];
|
|
292
335
|
const writes = new Set();
|
|
@@ -315,6 +358,13 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
315
358
|
if (unread.has(node)) {
|
|
316
359
|
return;
|
|
317
360
|
}
|
|
361
|
+
const namespace = namespaces.get(node);
|
|
362
|
+
if (namespace?.kind === "whole") {
|
|
363
|
+
wholes.push({ specifier: namespace.specifier, at: node, from: enclosing });
|
|
364
|
+
}
|
|
365
|
+
else if (namespace?.kind === "indexed") {
|
|
366
|
+
found.push({ node: namespace.literal, shorthand: undefined, from: enclosing, use: "read" });
|
|
367
|
+
}
|
|
318
368
|
if (isName(node)) {
|
|
319
369
|
if (!declared.has(node.pos)) {
|
|
320
370
|
const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
|
|
@@ -344,6 +394,10 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
344
394
|
if (evaluated !== undefined) {
|
|
345
395
|
evaluations.push({ node: evaluated, from: enclosing });
|
|
346
396
|
}
|
|
397
|
+
const url = moduleURLOf(node);
|
|
398
|
+
if (url !== undefined) {
|
|
399
|
+
urls.push(url);
|
|
400
|
+
}
|
|
347
401
|
const held = declarations.get(nodeKey(file, node));
|
|
348
402
|
const outer = enclosing;
|
|
349
403
|
if (held !== undefined) {
|
|
@@ -358,7 +412,7 @@ function sitesOf(file, declarations, fileId, unread) {
|
|
|
358
412
|
enclosing = outer;
|
|
359
413
|
};
|
|
360
414
|
file.forEachChild(visit);
|
|
361
|
-
return { uses: found, literals, flows, reads, links, evaluations, decorated };
|
|
415
|
+
return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
|
|
362
416
|
}
|
|
363
417
|
/** The declaration kinds of a property an object literal declares. */
|
|
364
418
|
const LITERAL_PROPERTY_KINDS = new Set([
|
|
@@ -583,6 +637,30 @@ export function references(project, held, targetRoot, options) {
|
|
|
583
637
|
}
|
|
584
638
|
return topLevel.get(fileId) ?? [];
|
|
585
639
|
};
|
|
640
|
+
const exportsCache = new Map();
|
|
641
|
+
/**
|
|
642
|
+
* Every declaration one module exports, each re-export walked along its chain. A module
|
|
643
|
+
* the checker left unanswered may be any file, so it reads every file's exports.
|
|
644
|
+
*/
|
|
645
|
+
const exportsRead = (module) => {
|
|
646
|
+
if (module === UNANSWERED) {
|
|
647
|
+
return [...fileIds].flatMap((id) => declaredIn(id)
|
|
648
|
+
.filter((symbol) => symbol.exported)
|
|
649
|
+
.map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })));
|
|
650
|
+
}
|
|
651
|
+
const known = exportsCache.get(module.id);
|
|
652
|
+
if (known !== undefined) {
|
|
653
|
+
return known;
|
|
654
|
+
}
|
|
655
|
+
const table = project.queries.exportsOfModule(module);
|
|
656
|
+
const read = table === UNANSWERED
|
|
657
|
+
? chains.declarationsOf(module).flatMap((id) => declaredIn(id)
|
|
658
|
+
.filter((symbol) => symbol.exported)
|
|
659
|
+
.map((symbol) => ({ id: symbol.id, stepped: false, guessed: true })))
|
|
660
|
+
: table.flatMap((exported) => chains.chainOf(exported));
|
|
661
|
+
exportsCache.set(module.id, read);
|
|
662
|
+
return read;
|
|
663
|
+
};
|
|
586
664
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
587
665
|
const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
|
|
588
666
|
/**
|
|
@@ -746,7 +824,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
746
824
|
}
|
|
747
825
|
}
|
|
748
826
|
}
|
|
749
|
-
for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
|
|
827
|
+
for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
|
|
750
828
|
const test = testFile.get(file) === true;
|
|
751
829
|
// A component file's markup may use any binding its blocks declare at the top level.
|
|
752
830
|
const markup = isComponentFile(file);
|
|
@@ -774,6 +852,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
774
852
|
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
775
853
|
...links.map((link) => link.node),
|
|
776
854
|
...evaluations.map((evaluation) => evaluation.node),
|
|
855
|
+
...wholes.map((whole) => whole.specifier),
|
|
856
|
+
...urls.flatMap((url) => url.constructor ?? []),
|
|
777
857
|
...bindings.map((binding) => binding.node),
|
|
778
858
|
]),
|
|
779
859
|
];
|
|
@@ -842,6 +922,43 @@ export function references(project, held, targetRoot, options) {
|
|
|
842
922
|
}
|
|
843
923
|
}
|
|
844
924
|
}
|
|
925
|
+
for (const url of urls) {
|
|
926
|
+
const constructor = url.constructor === undefined ? undefined : answered.get(url.constructor);
|
|
927
|
+
if (typeof constructor === "object" &&
|
|
928
|
+
constructor.declarations.some((handle) => ownPaths.has(handle.path))) {
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const loaded = specifierCandidates(dirnamePath(file.fileName), url.specifier)
|
|
932
|
+
.map((path) => fileIdByName.get(path))
|
|
933
|
+
.find((id) => id !== undefined);
|
|
934
|
+
if (loaded !== undefined) {
|
|
935
|
+
found.push({
|
|
936
|
+
from: fileId,
|
|
937
|
+
to: loaded,
|
|
938
|
+
position: renderPosition(file, root, url.at.getStart()),
|
|
939
|
+
use: "evaluation",
|
|
940
|
+
resolution: "syntax",
|
|
941
|
+
test,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const whole of wholes) {
|
|
946
|
+
const module = answered.get(whole.specifier);
|
|
947
|
+
if (module === undefined) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const position = renderPosition(file, root, whole.at.getStart());
|
|
951
|
+
for (const target of exportsRead(module)) {
|
|
952
|
+
found.push({
|
|
953
|
+
from: whole.from,
|
|
954
|
+
to: target.id,
|
|
955
|
+
position,
|
|
956
|
+
use: "read",
|
|
957
|
+
resolution: target.guessed ? "by-name" : "batch",
|
|
958
|
+
test,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
845
962
|
if (markup) {
|
|
846
963
|
for (const symbol of declaredIn(fileId)) {
|
|
847
964
|
found.push({
|
package/dist/src/roots.js
CHANGED
|
@@ -13,6 +13,7 @@ import { conventionEntries } from "./conventions.js";
|
|
|
13
13
|
import { entryPoints } from "./entry-points.js";
|
|
14
14
|
import { globExpression } from "./glob.js";
|
|
15
15
|
import { nodeKey } from "./inventory.js";
|
|
16
|
+
import { relativePath, resolvePath } from "./paths.js";
|
|
16
17
|
import { byPosition } from "./position.js";
|
|
17
18
|
import { UNANSWERED } from "./query.js";
|
|
18
19
|
import { sourceFilesOf } from "./source-files.js";
|
|
@@ -23,6 +24,42 @@ const ENTRY_KINDS = {
|
|
|
23
24
|
run: "manifest-binary",
|
|
24
25
|
script: "script",
|
|
25
26
|
};
|
|
27
|
+
/** The rules whose rooted files a manifest target is read back to by name. */
|
|
28
|
+
const READ_BACK_RULES = new Set([
|
|
29
|
+
"configuration-file",
|
|
30
|
+
"configuration-string",
|
|
31
|
+
"html-entry",
|
|
32
|
+
]);
|
|
33
|
+
/** The extensions a manifest target's name drops: the ones a build writes. */
|
|
34
|
+
const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
|
|
35
|
+
/** The extension a source file's name drops. */
|
|
36
|
+
const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
|
|
37
|
+
/**
|
|
38
|
+
* The rooted files one manifest target reads back to by name: its path below the
|
|
39
|
+
* manifest's directory, past its first directory where it names one, without its output
|
|
40
|
+
* extension, is the name, and a rooted file is one whose path below the same directory,
|
|
41
|
+
* without its source extension, ends with that name at a directory boundary.
|
|
42
|
+
*/
|
|
43
|
+
function readBackByName(host, entry, rooted) {
|
|
44
|
+
const dir = resolvePath(host.workingDirectory(), entry.dir);
|
|
45
|
+
const below = relativePath(dir, resolvePath(host.workingDirectory(), entry.path));
|
|
46
|
+
if (below === undefined || below === "") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const cut = below.indexOf("/");
|
|
50
|
+
const name = (cut === -1 ? below : below.slice(cut + 1)).replace(OUTPUT_EXTENSION, "");
|
|
51
|
+
if (name === "") {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const found = new Map();
|
|
55
|
+
for (const file of rooted) {
|
|
56
|
+
const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
|
|
57
|
+
if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
|
|
58
|
+
found.set(file.fileName, file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...found.values()];
|
|
62
|
+
}
|
|
26
63
|
/** Whether a configured string carries either special character. */
|
|
27
64
|
function isPattern(text) {
|
|
28
65
|
return text.includes("*") || text.includes("?");
|
|
@@ -125,6 +162,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
125
162
|
const enter = (file, kind, source, exports) => {
|
|
126
163
|
entered.push({ file, kind, source, exports });
|
|
127
164
|
};
|
|
165
|
+
const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse);
|
|
128
166
|
const published = [];
|
|
129
167
|
const entries = [
|
|
130
168
|
...options.manifest.entries.map((entry) => ({
|
|
@@ -134,14 +172,25 @@ export function roots(project, held, targetRoot, options) {
|
|
|
134
172
|
})),
|
|
135
173
|
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
136
174
|
];
|
|
175
|
+
const scripted = entries.flatMap(({ entry }) => entry.role === "script" ? files.named(entry.path) : []);
|
|
176
|
+
const readBackTo = [
|
|
177
|
+
...scripted,
|
|
178
|
+
...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
|
|
179
|
+
];
|
|
180
|
+
const entryFiles = (entry) => {
|
|
181
|
+
const named = files.named(entry.path);
|
|
182
|
+
return named.length > 0 || entry.role === "script"
|
|
183
|
+
? named
|
|
184
|
+
: readBackByName(options.host, entry, readBackTo);
|
|
185
|
+
};
|
|
137
186
|
for (const { entry, publishes } of entries) {
|
|
138
187
|
if (options.publishedAPI && publishes) {
|
|
139
|
-
published.push(...
|
|
188
|
+
published.push(...entryFiles(entry));
|
|
140
189
|
}
|
|
141
190
|
}
|
|
142
191
|
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
143
192
|
for (const { entry } of entries) {
|
|
144
|
-
for (const file of
|
|
193
|
+
for (const file of entryFiles(entry)) {
|
|
145
194
|
const kind = ENTRY_KINDS[entry.role];
|
|
146
195
|
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
147
196
|
}
|
|
@@ -159,7 +208,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
159
208
|
: conventionEntries(options.conventions, files)) {
|
|
160
209
|
enter(file, "convention", row, true);
|
|
161
210
|
}
|
|
162
|
-
for (const point of
|
|
211
|
+
for (const point of points) {
|
|
163
212
|
enter(point.file, point.rule, point.source, point.exports);
|
|
164
213
|
}
|
|
165
214
|
for (const file of files.byPath.values()) {
|
package/dist/src/sweep.js
CHANGED
|
@@ -70,6 +70,23 @@ export function sweep(graph, input) {
|
|
|
70
70
|
});
|
|
71
71
|
return { liveUnder: live, candidates };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
75
|
+
* That run's roots are the graph's own and each test file, being the file's top level
|
|
76
|
+
* and every declaration it exports, which a test runner may call; `held` seeds it as a
|
|
77
|
+
* mark or an exemption seeds a sweep.
|
|
78
|
+
*/
|
|
79
|
+
export function reachedWithTheTests(graph, held) {
|
|
80
|
+
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
81
|
+
const top = symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
82
|
+
return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
|
|
83
|
+
? [{ at, kind: "test-runner" }]
|
|
84
|
+
: [];
|
|
85
|
+
});
|
|
86
|
+
return reachable({ ...graph, rooted: [...graph.rooted, ...tests] }, flagged(graph, held), {
|
|
87
|
+
production: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
73
90
|
/**
|
|
74
91
|
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
75
92
|
* none a consumer's test file made, as it counts none of the target's.
|
package/dist/src/test-support.js
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test-support code:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Test-support code: the files only tests reach. A test reaches each one through a chain
|
|
3
|
+
* of references, and every reference to a declaration of one, an import for its effects
|
|
4
|
+
* included, is written in a test file or in another such file, so it is judged with the
|
|
5
|
+
* tests: what it references is referenced by test code.
|
|
6
6
|
*/
|
|
7
7
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
8
8
|
const SKIP_ROOT = "type-error";
|
|
9
9
|
/**
|
|
10
|
-
* The files of one project that are test-support code, below the target root
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The files of one project that are test-support code, below the target root: the
|
|
11
|
+
* largest set of files a test file reaches, none of them a test file or holding a root,
|
|
12
|
+
* whose every user outside the set is a test file. A root has a caller outside the
|
|
13
|
+
* reference graph: a runtime, a tool, a configuration, or a consumer of a library's
|
|
14
|
+
* published API. `testFiles` are the files the test-file rules classified.
|
|
14
15
|
*/
|
|
15
16
|
export function testSupportFiles(symbols, references, roots, testFiles) {
|
|
16
17
|
const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
|
|
17
18
|
const users = new Map();
|
|
19
|
+
const uses = new Map();
|
|
20
|
+
const add = (map, key, value) => {
|
|
21
|
+
const held = map.get(key);
|
|
22
|
+
if (held === undefined) {
|
|
23
|
+
map.set(key, new Set([value]));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
held.add(value);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
18
29
|
for (const reference of references) {
|
|
19
30
|
const used = fileOf.get(reference.to);
|
|
20
31
|
const from = reference.position.path;
|
|
21
32
|
if (reference.consumer !== undefined || used === undefined || used === from) {
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
users.set(used, new Set([from]));
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
held.add(from);
|
|
30
|
-
}
|
|
35
|
+
add(users, used, from);
|
|
36
|
+
add(uses, from, used);
|
|
31
37
|
}
|
|
32
38
|
const tests = new Set(testFiles);
|
|
33
39
|
const rooted = new Set(roots.filter((root) => root.kind !== SKIP_ROOT).flatMap((root) => fileOf.get(root.id) ?? []));
|
|
34
|
-
const
|
|
35
|
-
const
|
|
40
|
+
const reached = new Set();
|
|
41
|
+
const queue = [...tests];
|
|
42
|
+
for (let path = queue.pop(); path !== undefined; path = queue.pop()) {
|
|
43
|
+
for (const next of uses.get(path) ?? []) {
|
|
44
|
+
if (!reached.has(next) && !tests.has(next)) {
|
|
45
|
+
reached.add(next);
|
|
46
|
+
queue.push(next);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const support = new Set([...reached].filter((path) => !rooted.has(path)));
|
|
36
51
|
for (let changed = true; changed;) {
|
|
37
52
|
changed = false;
|
|
38
|
-
for (const path of
|
|
39
|
-
if (support.has(path)) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
53
|
+
for (const path of support) {
|
|
42
54
|
const from = users.get(path) ?? new Set();
|
|
43
|
-
if ([...from].every((one) => tests.has(one) || support.has(one))) {
|
|
44
|
-
support.
|
|
55
|
+
if (![...from].every((one) => tests.has(one) || support.has(one))) {
|
|
56
|
+
support.delete(path);
|
|
45
57
|
changed = true;
|
|
46
58
|
}
|
|
47
59
|
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "10.0.1",
|
|
36
36
|
"@types/node": "24.19.1",
|
|
37
|
-
"eslint": "10.
|
|
37
|
+
"eslint": "10.12.0",
|
|
38
38
|
"fast-check": "4.10.2",
|
|
39
39
|
"prettier": "3.9.9",
|
|
40
40
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
package/src/analysis.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
declarationsNamed,
|
|
23
23
|
} from "./configured-declarations.ts";
|
|
24
24
|
import { decorator } from "./decorator.ts";
|
|
25
|
-
import { dependenciesOf, projectNeeds } from "./dependencies.ts";
|
|
25
|
+
import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.ts";
|
|
26
26
|
import { deprecatedDeclarations, type Deprecation } from "./deprecation.ts";
|
|
27
27
|
import {
|
|
28
28
|
discoverProjects,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "./conventions.ts";
|
|
44
44
|
import { readEdgeSides } from "./edges.ts";
|
|
45
45
|
import { enumGroup } from "./enum-group.ts";
|
|
46
|
-
import { generatedFile } from "./generated-file.ts";
|
|
46
|
+
import { generatedFile, generatedFilesOf } from "./generated-file.ts";
|
|
47
47
|
import { sourceFilesOf } from "./source-files.ts";
|
|
48
48
|
import {
|
|
49
49
|
evidenceRecords,
|
|
@@ -191,7 +191,7 @@ interface ProjectRead {
|
|
|
191
191
|
readonly typeErrorSkips: readonly TypeErrorSkip[];
|
|
192
192
|
/** The lines each of those skips withholds every finding on. */
|
|
193
193
|
readonly skipped: readonly SkippedUnit[];
|
|
194
|
-
/** The project's generated files, by path below the target root, each with the
|
|
194
|
+
/** The project's generated files, by path below the target root, each with the detail its retentions name. */
|
|
195
195
|
readonly generated: ReadonlyMap<string, string>;
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -222,6 +222,8 @@ interface ReadProjects<Answer> {
|
|
|
222
222
|
readonly componentsUnread: readonly ComponentsUnread[];
|
|
223
223
|
/** The convention rows the run applied. */
|
|
224
224
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
225
|
+
/** The dependencies the applied rows' short names use. */
|
|
226
|
+
readonly conventionUses: readonly string[];
|
|
225
227
|
/** The test files below the target root no project the run reads holds, parsed. */
|
|
226
228
|
readonly outsideTests: readonly OutsideTest[];
|
|
227
229
|
}
|
|
@@ -435,6 +437,7 @@ function readProjects<Answer>(
|
|
|
435
437
|
...kept.map((id) => ({ id, kind: "type-error" as const, source: "" })),
|
|
436
438
|
],
|
|
437
439
|
};
|
|
440
|
+
const own = sourceFilesOf(project, targetRoot);
|
|
438
441
|
return {
|
|
439
442
|
configuration,
|
|
440
443
|
configFile: project.configFile,
|
|
@@ -446,7 +449,7 @@ function readProjects<Answer>(
|
|
|
446
449
|
.ownSourceFiles()
|
|
447
450
|
.filter((file) => referenceOnly(file.fileName))
|
|
448
451
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
449
|
-
generated:
|
|
452
|
+
generated: generatedFilesOf(own, generatedFiles(conventions, own)),
|
|
450
453
|
};
|
|
451
454
|
};
|
|
452
455
|
for (const file of project.ownSourceFiles()) {
|
|
@@ -504,6 +507,7 @@ function readProjects<Answer>(
|
|
|
504
507
|
unanswered: session.unanswered,
|
|
505
508
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
506
509
|
conventionsApplied: conventions.applied,
|
|
510
|
+
conventionUses: conventions.uses,
|
|
507
511
|
outsideTests,
|
|
508
512
|
};
|
|
509
513
|
}
|
|
@@ -688,6 +692,8 @@ interface ReadRun<Extra> {
|
|
|
688
692
|
readonly componentsUnread: readonly ComponentsUnread[];
|
|
689
693
|
/** The convention rows the run applied. */
|
|
690
694
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
695
|
+
/** The dependencies the applied rows' short names use. */
|
|
696
|
+
readonly conventionUses: readonly string[];
|
|
691
697
|
}
|
|
692
698
|
|
|
693
699
|
/**
|
|
@@ -839,6 +845,7 @@ function readRun<Extra>(
|
|
|
839
845
|
unanswered: read.unanswered,
|
|
840
846
|
componentsUnread: read.componentsUnread,
|
|
841
847
|
conventionsApplied: read.conventionsApplied,
|
|
848
|
+
conventionUses: read.conventionUses,
|
|
842
849
|
};
|
|
843
850
|
}
|
|
844
851
|
|
|
@@ -1093,7 +1100,7 @@ function emitterInputOver(
|
|
|
1093
1100
|
dependencies: dependenciesOf(
|
|
1094
1101
|
host,
|
|
1095
1102
|
targetRoot,
|
|
1096
|
-
projects.map((one) => one.extra.needs),
|
|
1103
|
+
[...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
|
|
1097
1104
|
swept.sweep.candidates.map((candidate) => candidate.id),
|
|
1098
1105
|
),
|
|
1099
1106
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
package/src/config.ts
CHANGED
|
@@ -232,9 +232,12 @@ export function defaultConfig(): Config {
|
|
|
232
232
|
ts: {
|
|
233
233
|
testFiles: [
|
|
234
234
|
"**/*.test.{ts,tsx,mts,cts}",
|
|
235
|
+
"**/*.test-d.{ts,tsx,mts,cts}",
|
|
236
|
+
"**/*.spec-d.{ts,tsx,mts,cts}",
|
|
235
237
|
"**/*.spec.*",
|
|
236
238
|
"**/__tests__/**",
|
|
237
239
|
"**/__mocks__/**",
|
|
240
|
+
"**/test-d/**",
|
|
238
241
|
],
|
|
239
242
|
entryFiles: [],
|
|
240
243
|
componentExtensions: [".vue", ".svelte", ".astro"],
|
|
@@ -237,33 +237,36 @@ function relativeSpecifiers(file: SourceFile): readonly string[] {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* The
|
|
241
|
-
*
|
|
242
|
-
*
|
|
240
|
+
* The paths one relative specifier from a configuration file may reach, in the order they
|
|
241
|
+
* are tried: the path it names where that ends with a module extension, that path with its
|
|
242
|
+
* JavaScript extension replaced by the TypeScript counterparts, that path with an
|
|
243
|
+
* extension appended, and a directory's `index` with one appended. A data file such as
|
|
244
|
+
* `./package.json` is no module, so it is no candidate.
|
|
243
245
|
*/
|
|
244
|
-
function
|
|
246
|
+
export function specifierCandidates(dir: string, specifier: string): readonly string[] {
|
|
245
247
|
const path = resolvePath(dir, specifier);
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return path;
|
|
248
|
+
const candidates: string[] = [];
|
|
249
|
+
if (APPENDED.some((extension) => path.endsWith(extension))) {
|
|
250
|
+
candidates.push(path);
|
|
250
251
|
}
|
|
251
252
|
for (const [extension, counterparts] of COUNTERPARTS) {
|
|
252
253
|
if (path.endsWith(extension)) {
|
|
253
254
|
const stem = path.slice(0, -extension.length);
|
|
254
|
-
|
|
255
|
-
if (found !== undefined) {
|
|
256
|
-
return found;
|
|
257
|
-
}
|
|
255
|
+
candidates.push(...counterparts.map((one) => stem + one));
|
|
258
256
|
}
|
|
259
257
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
258
|
+
candidates.push(...APPENDED.map((extension) => path + extension));
|
|
259
|
+
candidates.push(...APPENDED.map((extension) => joinPath(path, `index${extension}`)));
|
|
260
|
+
return candidates;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The file on disk one relative specifier from a configuration file reaches, or undefined. */
|
|
264
|
+
export function resolveRelativeSpecifier(
|
|
265
|
+
host: Host,
|
|
266
|
+
dir: string,
|
|
267
|
+
specifier: string,
|
|
268
|
+
): string | undefined {
|
|
269
|
+
return specifierCandidates(dir, specifier).find((path) => host.kindOf(path) === "file");
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
/**
|
|
@@ -311,7 +314,7 @@ function outsideModules(
|
|
|
311
314
|
const file = parse(next.path, text);
|
|
312
315
|
found.push({ file, manifestDir: next.manifestDir });
|
|
313
316
|
for (const specifier of relativeSpecifiers(file)) {
|
|
314
|
-
const reached =
|
|
317
|
+
const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
|
|
315
318
|
if (reached !== undefined) {
|
|
316
319
|
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
317
320
|
}
|
|
@@ -347,12 +350,13 @@ export function dependenciesNamed(text: string, declared: readonly string[]): re
|
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
/**
|
|
350
|
-
* The own files one string names read against a directory:
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
+
* The own files one string names read against a directory: the file it names directly or
|
|
354
|
+
* through the emit mapping, or the file it names with an extension the module resolution
|
|
355
|
+
* appends. An empty string, one starting with `/` and one holding a glob character names
|
|
356
|
+
* none.
|
|
353
357
|
*/
|
|
354
358
|
export function filesNamedBy(files: SourceFiles, dir: string, text: string): readonly SourceFile[] {
|
|
355
|
-
if (
|
|
359
|
+
if (text === "" || text.startsWith("/") || /[*?[{]/u.test(text)) {
|
|
356
360
|
return [];
|
|
357
361
|
}
|
|
358
362
|
const path = resolvePath(dir, text);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "3.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:db52e3164347cab70f45f438b4ac9ce89573be9303503daeb237a3c40361b001",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|