@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.2
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 +108 -10
- package/dist/src/augmentations.js +62 -18
- 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 +98 -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 +172 -40
- 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 +30 -5
- package/dist/src/findings/visibility-narrowing.js +29 -5
- 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 +57 -10
- 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 +38 -4
- 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 +8 -5
- package/dist/src/sweep.js +27 -10
- package/dist/src/symbol-keys.js +103 -2
- package/dist/src/test-support.js +119 -18
- 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 +146 -11
- package/src/augmentations.ts +76 -22
- 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 +128 -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 +15 -0
- package/src/findings/completion.ts +222 -46
- 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 +36 -5
- package/src/findings/visibility-narrowing.ts +40 -5
- 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 +104 -15
- 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 +48 -3
- 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 +10 -5
- package/src/sweep.ts +32 -11
- package/src/symbol-keys.ts +129 -2
- package/src/test-support.ts +155 -18
- 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,4 +1,5 @@
|
|
|
1
1
|
import { KINDS } from "../kinds.js";
|
|
2
|
+
import { parameterRef } from "../ref.js";
|
|
2
3
|
import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
|
|
3
4
|
import { reachedWithTheTests } from "../sweep.js";
|
|
4
5
|
/** A list of rules as a sentence names them. */
|
|
@@ -8,24 +9,43 @@ function spelled(rules) {
|
|
|
8
9
|
}
|
|
9
10
|
return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
|
|
10
11
|
}
|
|
11
|
-
/** The
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
|
|
13
|
+
const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
|
|
14
|
+
/**
|
|
15
|
+
* The clause a message ends with: the other rules that report the part, or that none is
|
|
16
|
+
* known. A parameter of a file the compiler does not check is no report of the compiler's.
|
|
17
|
+
*/
|
|
18
|
+
function overlapClause(overlap, part) {
|
|
19
|
+
const rules = part.parameter?.checked === false
|
|
20
|
+
? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
|
|
21
|
+
: overlap;
|
|
22
|
+
return rules.length === 1 && rules[0] === "none known"
|
|
14
23
|
? "no other rule is known to report it"
|
|
15
|
-
: `also reported by ${spelled(
|
|
24
|
+
: `also reported by ${spelled(rules)}`;
|
|
16
25
|
}
|
|
17
26
|
/**
|
|
18
|
-
* Whether one parameter
|
|
19
|
-
*
|
|
20
|
-
*
|
|
27
|
+
* Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
|
|
28
|
+
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
29
|
+
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
30
|
+
* declared unread, and one no read parameter follows is deleted: kept where the call's
|
|
31
|
+
* inference may read its annotation, at `probable` where nothing reads the annotation.
|
|
21
32
|
*/
|
|
22
|
-
function
|
|
33
|
+
function parameterVerdict(part, free, freeButValued) {
|
|
23
34
|
const place = part.parameter;
|
|
24
35
|
if (place?.inValue !== true && free.has(part.declaration)) {
|
|
25
|
-
return
|
|
36
|
+
return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
|
|
26
37
|
}
|
|
27
38
|
const valued = place?.inValue === true || freeButValued.has(part.declaration);
|
|
28
|
-
|
|
39
|
+
if (!valued) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (place?.beforeARead === true) {
|
|
43
|
+
return { fix: "rename" };
|
|
44
|
+
}
|
|
45
|
+
if (place?.annotated === "steers") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
|
|
29
49
|
}
|
|
30
50
|
/**
|
|
31
51
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
@@ -41,7 +61,7 @@ function reported(input, part, callersUnknown) {
|
|
|
41
61
|
const { free, freeButValued, discardedEverywhere } = input.intraFunction;
|
|
42
62
|
switch (part.code) {
|
|
43
63
|
case UNUSED_PARAMETER:
|
|
44
|
-
return
|
|
64
|
+
return parameterVerdict(part, free, freeButValued) !== undefined;
|
|
45
65
|
case UNUSED_RESULT:
|
|
46
66
|
return (free.has(part.declaration) &&
|
|
47
67
|
discardedEverywhere.has(part.declaration) &&
|
|
@@ -96,20 +116,31 @@ export const intraFunction = (input) => {
|
|
|
96
116
|
if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
|
|
97
117
|
continue;
|
|
98
118
|
}
|
|
119
|
+
const verdict = part.code === UNUSED_PARAMETER
|
|
120
|
+
? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
|
|
121
|
+
: undefined;
|
|
122
|
+
const message = verdict?.fix === "rename"
|
|
123
|
+
? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
|
|
124
|
+
: part.message;
|
|
99
125
|
found.push({
|
|
100
126
|
code: part.code,
|
|
101
127
|
position: part.position,
|
|
102
128
|
symbol: {
|
|
103
|
-
|
|
129
|
+
...(part.code === UNUSED_PARAMETER
|
|
130
|
+
? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
|
|
131
|
+
: { ref: declaration.ref }),
|
|
104
132
|
kind: part.kind,
|
|
105
133
|
name: part.name,
|
|
106
134
|
sizeLines: part.position.endLine - part.position.line + 1,
|
|
107
135
|
},
|
|
108
|
-
message: `${
|
|
136
|
+
message: `${message}; ${overlapClause(overlap, part)}`,
|
|
109
137
|
details: {
|
|
110
138
|
...(part.kind === "store" ? { writePositions: [part.position] } : {}),
|
|
111
139
|
overlap,
|
|
112
140
|
},
|
|
141
|
+
...(verdict?.fix === "delete" && verdict.confidence !== "certain"
|
|
142
|
+
? { reachabilityClass: verdict.confidence }
|
|
143
|
+
: {}),
|
|
113
144
|
});
|
|
114
145
|
}
|
|
115
146
|
return found;
|
|
@@ -43,41 +43,25 @@ function fileNeverBuilt({ config, swept, files }) {
|
|
|
43
43
|
.map((file) => fileFinding(FILE_NEVER_BUILT, file, message));
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* The paths of
|
|
47
|
-
* file
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* The paths of the files a reference from a live declaration of another file reaches, a
|
|
47
|
+
* loaded consumer's file included, a root names, or a declared edge names a declaration in,
|
|
48
|
+
* and of every file a reference written anywhere reaches. A file holding a declaration the
|
|
49
|
+
* sweep judged live is kept whatever held it live, and so is a file a program holds by
|
|
50
|
+
* inclusion, which acts on every file of the program without an import naming it. A
|
|
51
|
+
* reference from a file's top level counts once the file is kept.
|
|
50
52
|
*/
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
+
function keptFiles({ swept, files, boundary }) {
|
|
54
|
+
const union = swept.matrix.union;
|
|
55
|
+
const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
|
|
56
|
+
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
57
|
+
const kept = new Set(files.heldByInclusion);
|
|
58
|
+
const referenced = new Set();
|
|
53
59
|
for (const edge of union.consumed) {
|
|
54
60
|
const to = union.symbols[edge.to];
|
|
55
61
|
if (to !== undefined) {
|
|
56
|
-
|
|
62
|
+
kept.add(to.position.path);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
union.out.forEach((edges, from) => {
|
|
60
|
-
const fromPath = union.symbols[from]?.position.path;
|
|
61
|
-
for (const edge of edges) {
|
|
62
|
-
const to = union.symbols[edge.to];
|
|
63
|
-
if (to !== undefined && to.position.path !== fromPath) {
|
|
64
|
-
reached.add(to.position.path);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
return reached;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* The paths of the files an import reaches, a root names, or a declared edge names a
|
|
72
|
-
* declaration in. A file holding a declaration the sweep judged live is kept whatever
|
|
73
|
-
* held it live, and so is a file a program holds by inclusion, which acts on every
|
|
74
|
-
* file of the program without an import naming it.
|
|
75
|
-
*/
|
|
76
|
-
function keptFiles({ swept, files, boundary }) {
|
|
77
|
-
const union = swept.matrix.union;
|
|
78
|
-
const edgeNamed = new Set(boundary.edges.map((side) => side.symbol));
|
|
79
|
-
const dead = new Set(swept.sweep.candidates.map((candidate) => candidate.id));
|
|
80
|
-
const kept = new Set([...reachedFromOutside(union), ...files.heldByInclusion]);
|
|
81
65
|
for (const root of union.rooted) {
|
|
82
66
|
const named = union.symbols[root.at];
|
|
83
67
|
if (named !== undefined) {
|
|
@@ -89,14 +73,63 @@ function keptFiles({ swept, files, boundary }) {
|
|
|
89
73
|
kept.add(symbol.position.path);
|
|
90
74
|
}
|
|
91
75
|
}
|
|
92
|
-
|
|
76
|
+
const fromTopLevel = new Map();
|
|
77
|
+
const pending = [];
|
|
78
|
+
const keep = (path) => {
|
|
79
|
+
if (!kept.has(path)) {
|
|
80
|
+
kept.add(path);
|
|
81
|
+
pending.push(path);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
union.out.forEach((edges, from) => {
|
|
85
|
+
const source = union.symbols[from];
|
|
86
|
+
if (source === undefined) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const edge of edges) {
|
|
90
|
+
const to = union.symbols[edge.to]?.position.path;
|
|
91
|
+
if (to === undefined || to === source.position.path) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
referenced.add(to);
|
|
95
|
+
if (source.kind === FILE_SUBJECT) {
|
|
96
|
+
fromTopLevel.set(source.position.path, [
|
|
97
|
+
...(fromTopLevel.get(source.position.path) ?? []),
|
|
98
|
+
to,
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
else if (!dead.has(source.id)) {
|
|
102
|
+
pending.push(to);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
pending.push(...kept);
|
|
107
|
+
for (let path = pending.pop(); path !== undefined; path = pending.pop()) {
|
|
108
|
+
kept.add(path);
|
|
109
|
+
for (const to of fromTopLevel.get(path) ?? []) {
|
|
110
|
+
keep(to);
|
|
111
|
+
}
|
|
112
|
+
fromTopLevel.delete(path);
|
|
113
|
+
}
|
|
114
|
+
return { kept, referenced };
|
|
93
115
|
}
|
|
94
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Every file of the run that no reference from live code reaches and no root or edge
|
|
118
|
+
* names. A file only dead code imports is found dead under reachability alone.
|
|
119
|
+
*/
|
|
95
120
|
function fileNeverImported(input) {
|
|
96
|
-
const kept = keptFiles(input);
|
|
121
|
+
const { kept, referenced } = keptFiles(input);
|
|
97
122
|
return input.swept.matrix.union.symbols
|
|
98
123
|
.filter((symbol) => symbol.kind === FILE_SUBJECT && !kept.has(symbol.position.path))
|
|
99
|
-
.map((symbol) =>
|
|
124
|
+
.map((symbol) => {
|
|
125
|
+
const file = { path: symbol.position.path, ref: symbol.ref, endLine: symbol.endLine };
|
|
126
|
+
return referenced.has(symbol.position.path)
|
|
127
|
+
? {
|
|
128
|
+
...fileFinding(FILE_NEVER_IMPORTED, file, "only code that is itself dead imports this file, and no root names it or a declaration in it"),
|
|
129
|
+
unreachableOnly: true,
|
|
130
|
+
}
|
|
131
|
+
: fileFinding(FILE_NEVER_IMPORTED, file, "no import reaches this file and no root names it or a declaration in it");
|
|
132
|
+
});
|
|
100
133
|
}
|
|
101
134
|
/** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
|
|
102
135
|
export const nonCodeArtifacts = (input) => [
|
|
@@ -41,8 +41,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
41
41
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
42
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
43
43
|
* member nothing names; a test-support declaration test code references as test-only
|
|
44
|
-
* use; then deprecation, a test reference,
|
|
45
|
-
* unused export being the
|
|
44
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
45
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
46
|
+
* unreachable kind's.
|
|
46
47
|
*/
|
|
47
48
|
function codeOf(judged) {
|
|
48
49
|
const { candidate, symbol } = judged;
|
|
@@ -65,7 +66,9 @@ function codeOf(judged) {
|
|
|
65
66
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
66
67
|
return "DS1006";
|
|
67
68
|
}
|
|
68
|
-
if (candidate.
|
|
69
|
+
if (candidate.testRefs > 0 &&
|
|
70
|
+
(candidate.productionRefs === 0 ||
|
|
71
|
+
(candidate.relation === "reachability" && candidate.keptByTests))) {
|
|
69
72
|
return "DS1004";
|
|
70
73
|
}
|
|
71
74
|
if (MEMBERS.has(symbol.kind)) {
|
|
@@ -109,7 +112,9 @@ function messageOf(code, judged) {
|
|
|
109
112
|
case "DS1006":
|
|
110
113
|
return `deprecated ${word} has no production reference`;
|
|
111
114
|
case "DS1004":
|
|
112
|
-
return
|
|
115
|
+
return candidate.productionRefs === 0
|
|
116
|
+
? `${word} is referenced only from test files and never from production code`
|
|
117
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
113
118
|
}
|
|
114
119
|
const subject = code === "DS1003"
|
|
115
120
|
? `${visibilityWord(symbol)}${word}`
|
|
@@ -162,7 +167,27 @@ function brandMembers(input) {
|
|
|
162
167
|
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
163
168
|
const BRANDS = new Set(["type-member", "class-member"]);
|
|
164
169
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
165
|
-
export const unusedDeclarations = (input) =>
|
|
170
|
+
export const unusedDeclarations = (input) => [
|
|
171
|
+
...familyFindings(input, false),
|
|
172
|
+
...unusedImports(input),
|
|
173
|
+
];
|
|
174
|
+
/**
|
|
175
|
+
* The import bindings their file never reads, under the unexported code. A generated
|
|
176
|
+
* file's are the generator's to remove unless the run includes generated files.
|
|
177
|
+
*/
|
|
178
|
+
function unusedImports(input) {
|
|
179
|
+
const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
|
|
180
|
+
return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
|
|
181
|
+
? []
|
|
182
|
+
: [
|
|
183
|
+
{
|
|
184
|
+
code: "DS1002",
|
|
185
|
+
position: binding.position,
|
|
186
|
+
symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
|
|
187
|
+
message: `import binding ${binding.name} is never read in its file`,
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
166
191
|
/**
|
|
167
192
|
* The finding each member of a dead container would carry were its container live: what
|
|
168
193
|
* a record on such a member is matched against, since the report carries the container's
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OUTSIDE } from "../graph.js";
|
|
2
2
|
import { declarationKey } from "../ref.js";
|
|
3
|
+
import { runnerCalls } from "../sweep.js";
|
|
3
4
|
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
4
5
|
const UNNECESSARY_EXPORT = "DS1101";
|
|
5
6
|
const UNREACHABLE_EXPORT = "DS1103";
|
|
@@ -24,8 +25,27 @@ const WORDS = {
|
|
|
24
25
|
function publishes(kinds) {
|
|
25
26
|
return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The root kinds that name a file without entering it by its exports: a configuration
|
|
30
|
+
* string or a script token runs the file, and no tool reads a name it exports.
|
|
31
|
+
*/
|
|
32
|
+
const RUN_ONLY = new Set(["configuration-string", "script"]);
|
|
33
|
+
/** Whether a root makes its file an entry file, or its declaration an entry, which a tool may read by name. */
|
|
27
34
|
function entersByName(kinds) {
|
|
28
|
-
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
|
|
35
|
+
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
39
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
40
|
+
*/
|
|
41
|
+
const PER_NAME = new Set(["document-import"]);
|
|
42
|
+
/**
|
|
43
|
+
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
44
|
+
* enters only an export the runner calls, so another export of a test file is judged.
|
|
45
|
+
*/
|
|
46
|
+
function fileEntersByName(kinds, symbol) {
|
|
47
|
+
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
|
|
48
|
+
return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
|
|
29
49
|
}
|
|
30
50
|
/**
|
|
31
51
|
* Folds the run's references into the reach of each declaration.
|
|
@@ -123,7 +143,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
123
143
|
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
124
144
|
symbol.kind === "export-alias" ||
|
|
125
145
|
entersByName(facts.rootedBy[at]) ||
|
|
126
|
-
|
|
146
|
+
fileEntersByName(facts.rootedBy[file], symbol) ||
|
|
127
147
|
(facts.named[at] ?? 0) === 0) {
|
|
128
148
|
return undefined;
|
|
129
149
|
}
|
|
@@ -148,7 +168,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
148
168
|
* export no code outside the target can import, because the manifest declares
|
|
149
169
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
150
170
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
151
|
-
* declaration only a test file references, and a deprecated one.
|
|
171
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
152
172
|
*/
|
|
153
173
|
function unreachable(facts, at, symbol, candidate, input) {
|
|
154
174
|
if (!input.boundary.encapsulated ||
|
|
@@ -156,7 +176,7 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
156
176
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
157
177
|
candidate.testOfDeadCode ||
|
|
158
178
|
symbol.kind === "interface" ||
|
|
159
|
-
(candidate.
|
|
179
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
160
180
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
|
|
161
181
|
return undefined;
|
|
162
182
|
}
|
|
@@ -185,7 +205,11 @@ export function narrowings(input) {
|
|
|
185
205
|
return;
|
|
186
206
|
}
|
|
187
207
|
const candidate = candidates.get(symbol.id);
|
|
188
|
-
|
|
208
|
+
// A declaration of test code, a test file's or test-support code's, that only test code
|
|
209
|
+
// references is a candidate, live in the run that counts test references.
|
|
210
|
+
const live = candidate === undefined ||
|
|
211
|
+
(facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
|
|
212
|
+
const found = candidate === undefined || live
|
|
189
213
|
? narrowing(facts, at, symbol, world)
|
|
190
214
|
: unreachable(facts, at, symbol, candidate, input);
|
|
191
215
|
if (found !== undefined) {
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
const HEADER_LINE = /^\/\/ Code generated .* DO NOT EDIT\.$/u;
|
|
9
9
|
/** The documentation tag `@generated` as a word of a comment. */
|
|
10
10
|
const GENERATED_TAG = /(?<![\w@$])@generated(?![\w$-])/u;
|
|
11
|
+
/** A first comment naming the tool that wrote the file. */
|
|
12
|
+
const GENERATED_BY = /^\/\/ Generated by \S/u;
|
|
13
|
+
/** The word a generator's notice holds, beside one of {@link KEEP_OUT}. */
|
|
14
|
+
const GENERATED_WORD = /\bgenerated\b/iu;
|
|
15
|
+
/** The phrases a generator's notice asks a reader not to edit the file by. */
|
|
16
|
+
const KEEP_OUT = /\b(?:do not edit|will be overwritten|should not make any changes)\b/iu;
|
|
11
17
|
/** The comments before a text's first token, a leading hashbang line skipped. */
|
|
12
18
|
function leadingComments(text) {
|
|
13
19
|
const comments = [];
|
|
@@ -48,6 +54,11 @@ export function generatedByComment(text) {
|
|
|
48
54
|
if (comments.some((one) => GENERATED_TAG.test(one))) {
|
|
49
55
|
return "generated by the @generated tag its leading comments hold";
|
|
50
56
|
}
|
|
57
|
+
const notice = comments.join("\n");
|
|
58
|
+
if (GENERATED_BY.test(comments[0] ?? "") ||
|
|
59
|
+
(GENERATED_WORD.test(notice) && KEEP_OUT.test(notice))) {
|
|
60
|
+
return "generated by the notice its leading comments hold";
|
|
61
|
+
}
|
|
51
62
|
return undefined;
|
|
52
63
|
}
|
|
53
64
|
/**
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which files the `.gitignore` files of a tree ignore, read as git reads them:
|
|
3
|
+
* https://git-scm.com/docs/gitignore#_pattern_format
|
|
4
|
+
*/
|
|
5
|
+
import { globExpression } from "./glob.js";
|
|
6
|
+
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
7
|
+
/** The rules one `.gitignore` text writes, in order. */
|
|
8
|
+
function rulesOf(text) {
|
|
9
|
+
const rules = [];
|
|
10
|
+
for (const raw of text.split(/\r?\n/u)) {
|
|
11
|
+
let line = raw.replace(/(?<!\\)\s+$/u, "");
|
|
12
|
+
if (line === "" || line.startsWith("#")) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const negated = line.startsWith("!");
|
|
16
|
+
line = negated || line.startsWith("\\") ? line.slice(1) : line;
|
|
17
|
+
const directoryOnly = line.endsWith("/");
|
|
18
|
+
line = line.replace(/\/+$/u, "");
|
|
19
|
+
if (line === "") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// A separator at the start or in the middle anchors the pattern to the file's
|
|
23
|
+
// directory; with none, the pattern matches at any depth below it.
|
|
24
|
+
const anchored = line.includes("/");
|
|
25
|
+
const pattern = line.replace(/^\//u, "");
|
|
26
|
+
rules.push({
|
|
27
|
+
negated,
|
|
28
|
+
directoryOnly,
|
|
29
|
+
expression: globExpression(anchored ? pattern : `**/${pattern}`),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return rules;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether the `.gitignore` files at and below `root` ignore one absolute path. A path
|
|
36
|
+
* outside `root` is not ignored, and a file below an ignored directory stays ignored
|
|
37
|
+
* whatever a later pattern says, as git cannot re-include it.
|
|
38
|
+
*/
|
|
39
|
+
export function gitIgnored(host, root) {
|
|
40
|
+
const read = new Map();
|
|
41
|
+
const rulesAt = (dir) => {
|
|
42
|
+
let rules = read.get(dir);
|
|
43
|
+
if (rules === undefined) {
|
|
44
|
+
try {
|
|
45
|
+
rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
rules = [];
|
|
49
|
+
}
|
|
50
|
+
read.set(dir, rules);
|
|
51
|
+
}
|
|
52
|
+
return rules;
|
|
53
|
+
};
|
|
54
|
+
/** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
|
|
55
|
+
const decided = (path, directory) => {
|
|
56
|
+
const dirs = [];
|
|
57
|
+
for (let at = dirnamePath(path); relativePath(root, at) !== undefined; at = dirnamePath(at)) {
|
|
58
|
+
dirs.unshift(at);
|
|
59
|
+
}
|
|
60
|
+
dirs.unshift(root);
|
|
61
|
+
let ignored = false;
|
|
62
|
+
for (const dir of dirs) {
|
|
63
|
+
const below = relativePath(dir, path);
|
|
64
|
+
for (const rule of below === undefined ? [] : rulesAt(dir)) {
|
|
65
|
+
if ((directory || !rule.directoryOnly) && rule.expression.test(below ?? "")) {
|
|
66
|
+
ignored = !rule.negated;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return ignored;
|
|
71
|
+
};
|
|
72
|
+
return (path) => {
|
|
73
|
+
const below = relativePath(root, path);
|
|
74
|
+
if (below === undefined) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const parts = below.split("/");
|
|
78
|
+
for (let count = 1; count <= parts.length; count += 1) {
|
|
79
|
+
const at = joinPath(root, parts.slice(0, count).join("/"));
|
|
80
|
+
if (decided(at, count < parts.length)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/src/graph.js
CHANGED
|
@@ -56,6 +56,8 @@ const NAMES_A_CALLER = {
|
|
|
56
56
|
worker: true,
|
|
57
57
|
"html-entry": true,
|
|
58
58
|
"workflow-step": true,
|
|
59
|
+
"computed-import": true,
|
|
60
|
+
"document-import": true,
|
|
59
61
|
};
|
|
60
62
|
/** Whether the sweep judges a declaration of one kind. */
|
|
61
63
|
export function swept(kind) {
|
|
@@ -72,7 +74,7 @@ export function namesACaller(kind) {
|
|
|
72
74
|
* from such a declaration still counts toward its target, because reference counting
|
|
73
75
|
* asks whether any declaration names the target. A root naming nothing is dropped.
|
|
74
76
|
*/
|
|
75
|
-
export function graphOf(symbols, references, roots, testFiles, supportFiles = []) {
|
|
77
|
+
export function graphOf(symbols, references, roots, testFiles, supportFiles = [], ambient = new Set()) {
|
|
76
78
|
const supports = new Set(supportFiles);
|
|
77
79
|
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
78
80
|
const at = (id) => index.get(id) ?? OUTSIDE;
|
|
@@ -123,6 +125,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
|
|
|
123
125
|
parent,
|
|
124
126
|
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
125
127
|
support: symbols.map((symbol) => supports.has(symbol.position.path)),
|
|
128
|
+
ambient: symbols.map((symbol) => ambient.has(symbol.id)),
|
|
126
129
|
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
127
130
|
exportsOf,
|
|
128
131
|
rooted,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import bindings of one file: the names its import declarations bind, which only
|
|
3
|
+
* the file itself can read.
|
|
4
|
+
*/
|
|
5
|
+
import { isExportSpecifier, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isJsxElement, isJsxFragment, isJsxSelfClosingElement, isNamespaceImport, SyntaxKind, } from "@typescript/native/ast";
|
|
6
|
+
/** The extensions of the files whose import bindings are judged: no markup can read them. */
|
|
7
|
+
const JUDGED = /\.(?:[mc]?ts|tsx|[mc]?js|jsx)$/u;
|
|
8
|
+
/** The compiler's `jsx` settings whose emit calls no factory the file must hold. */
|
|
9
|
+
const AUTOMATIC_RUNTIME = new Set([4, 5, "react-jsx", "react-jsxdev"]);
|
|
10
|
+
/** A doc comment, where a JavaScript file names the types it imports. */
|
|
11
|
+
const DOC_COMMENT = /\/\*\*[\s\S]*?\*\//gu;
|
|
12
|
+
/** The parts of a doc comment the compiler resolves names in: a `{…}` type or link, a `@see` target. */
|
|
13
|
+
const DOC_REFERENCE = /\{[^{}]*\}|@see\s+[A-Za-z_$][\w$.]*/gu;
|
|
14
|
+
/** The factory a pragma comment names: `@jsx h` or `@jsxFrag Fragment`. */
|
|
15
|
+
const PRAGMA = /@jsx(?:Frag)?\s+([A-Za-z_$][\w$]*)/gu;
|
|
16
|
+
/**
|
|
17
|
+
* The names one file's import declarations bind: the default, named and namespace names of
|
|
18
|
+
* an `import` declaration, and the name of an `import x = require()` declaration that is
|
|
19
|
+
* not exported. None is judged in a declaration file or a component file.
|
|
20
|
+
*/
|
|
21
|
+
export function importBindings(file) {
|
|
22
|
+
if (file.isDeclarationFile || !JUDGED.test(file.fileName)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const found = [];
|
|
26
|
+
for (const statement of file.statements) {
|
|
27
|
+
if (isImportDeclaration(statement)) {
|
|
28
|
+
const clause = statement.importClause;
|
|
29
|
+
if (clause?.name !== undefined) {
|
|
30
|
+
found.push(clause.name);
|
|
31
|
+
}
|
|
32
|
+
const named = clause?.namedBindings;
|
|
33
|
+
if (named !== undefined && isNamespaceImport(named)) {
|
|
34
|
+
found.push(named.name);
|
|
35
|
+
}
|
|
36
|
+
else if (named !== undefined) {
|
|
37
|
+
for (const element of named.elements) {
|
|
38
|
+
found.push(element.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (isImportEqualsDeclaration(statement) &&
|
|
43
|
+
isExternalModuleReference(statement.moduleReference) &&
|
|
44
|
+
isIdentifier(statement.name) &&
|
|
45
|
+
!(statement.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.ExportKeyword)) {
|
|
46
|
+
found.push(statement.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
/** Whether one identifier names a binding in a local export, which reads it whatever it resolves to. */
|
|
52
|
+
export function exportsLocally(use) {
|
|
53
|
+
return isExportSpecifier(use.parent);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The names a file reads without an identifier the program resolves: each name a type, a
|
|
57
|
+
* link or a `@see` tag of its doc comments spells, and, where it writes JSX the compiler
|
|
58
|
+
* turns into factory calls, the factories the configuration or a pragma names.
|
|
59
|
+
*/
|
|
60
|
+
export function implicitReads(file, options) {
|
|
61
|
+
const names = new Set();
|
|
62
|
+
for (const comment of file.text.match(DOC_COMMENT) ?? []) {
|
|
63
|
+
for (const reference of comment.match(DOC_REFERENCE) ?? []) {
|
|
64
|
+
for (const word of reference.match(/[A-Za-z_$][\w$]*/gu) ?? []) {
|
|
65
|
+
names.add(word);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (AUTOMATIC_RUNTIME.has(options.jsx) || !writesJsx(file)) {
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
for (const factory of [options.jsxFactory, options.jsxFragmentFactory]) {
|
|
73
|
+
names.add(typeof factory === "string" ? (factory.split(".")[0] ?? factory) : "React");
|
|
74
|
+
}
|
|
75
|
+
for (const match of file.text.matchAll(PRAGMA)) {
|
|
76
|
+
if (match[1] !== undefined) {
|
|
77
|
+
names.add(match[1]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return names;
|
|
81
|
+
}
|
|
82
|
+
/** Whether a file holds a JSX element or fragment. */
|
|
83
|
+
function writesJsx(file) {
|
|
84
|
+
let found = false;
|
|
85
|
+
const visit = (node) => {
|
|
86
|
+
if (found) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (isJsxElement(node) || isJsxSelfClosingElement(node) || isJsxFragment(node)) {
|
|
90
|
+
found = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
node.forEachChild(visit);
|
|
94
|
+
};
|
|
95
|
+
file.forEachChild(visit);
|
|
96
|
+
return found;
|
|
97
|
+
}
|