@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
|
@@ -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) => [
|
|
@@ -162,7 +162,27 @@ function brandMembers(input) {
|
|
|
162
162
|
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
163
163
|
const BRANDS = new Set(["type-member", "class-member"]);
|
|
164
164
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
165
|
-
export const unusedDeclarations = (input) =>
|
|
165
|
+
export const unusedDeclarations = (input) => [
|
|
166
|
+
...familyFindings(input, false),
|
|
167
|
+
...unusedImports(input),
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* The import bindings their file never reads, under the unexported code. A generated
|
|
171
|
+
* file's are the generator's to remove unless the run includes generated files.
|
|
172
|
+
*/
|
|
173
|
+
function unusedImports(input) {
|
|
174
|
+
const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
|
|
175
|
+
return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
|
|
176
|
+
? []
|
|
177
|
+
: [
|
|
178
|
+
{
|
|
179
|
+
code: "DS1002",
|
|
180
|
+
position: binding.position,
|
|
181
|
+
symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
|
|
182
|
+
message: `import binding ${binding.name} is never read in its file`,
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
166
186
|
/**
|
|
167
187
|
* The finding each member of a dead container would carry were its container live: what
|
|
168
188
|
* 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,22 @@ 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
|
+
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
39
|
+
* enters only an export the runner calls, so another export of a test file is judged.
|
|
40
|
+
*/
|
|
41
|
+
function fileEntersByName(kinds, symbol) {
|
|
42
|
+
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
43
|
+
return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
|
|
29
44
|
}
|
|
30
45
|
/**
|
|
31
46
|
* Folds the run's references into the reach of each declaration.
|
|
@@ -123,7 +138,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
123
138
|
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
124
139
|
symbol.kind === "export-alias" ||
|
|
125
140
|
entersByName(facts.rootedBy[at]) ||
|
|
126
|
-
|
|
141
|
+
fileEntersByName(facts.rootedBy[file], symbol) ||
|
|
127
142
|
(facts.named[at] ?? 0) === 0) {
|
|
128
143
|
return undefined;
|
|
129
144
|
}
|
|
@@ -185,7 +200,11 @@ export function narrowings(input) {
|
|
|
185
200
|
return;
|
|
186
201
|
}
|
|
187
202
|
const candidate = candidates.get(symbol.id);
|
|
188
|
-
|
|
203
|
+
// A declaration of test code, a test file's or test-support code's, that only test code
|
|
204
|
+
// references is a candidate, live in the run that counts test references.
|
|
205
|
+
const live = candidate === undefined ||
|
|
206
|
+
(facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
|
|
207
|
+
const found = candidate === undefined || live
|
|
189
208
|
? narrowing(facts, at, symbol, world)
|
|
190
209
|
: unreachable(facts, at, symbol, candidate, input);
|
|
191
210
|
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
|
+
}
|