@cplieger/deadset-ts 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +70 -14
- package/dist/src/components.js +5 -1
- package/dist/src/dependencies.js +493 -0
- package/dist/src/deprecation.js +83 -0
- package/dist/src/discover.js +1 -1
- package/dist/src/enum-group.js +217 -0
- package/dist/src/file-facts.js +101 -0
- package/dist/src/findings/boundary.js +1 -0
- package/dist/src/findings/completion.js +99 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
- package/dist/src/findings/emitters.js +8 -0
- package/dist/src/findings/interfaces.js +95 -2
- package/dist/src/findings/non-code-artifacts.js +97 -1
- package/dist/src/findings/reads-and-writes.js +199 -2
- package/dist/src/findings/unused-declarations.js +182 -1
- package/dist/src/findings/visibility-narrowing.js +239 -2
- package/dist/src/implementations.js +210 -0
- package/dist/src/inventory.js +2 -2
- package/dist/src/resolve.js +1 -1
- package/dist/src/stores.js +32 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +113 -16
- package/src/components.ts +6 -1
- package/src/dependencies.ts +617 -0
- package/src/deprecation.ts +101 -0
- package/src/discover.ts +1 -1
- package/src/enum-group.ts +265 -0
- package/src/file-facts.ts +143 -0
- package/src/findings/boundary.ts +39 -0
- package/src/findings/completion.ts +151 -0
- package/src/findings/dependencies-and-module-machinery.ts +79 -1
- package/src/findings/emitter.ts +21 -1
- package/src/findings/emitters.ts +13 -1
- package/src/findings/interfaces.ts +149 -3
- package/src/findings/non-code-artifacts.ts +124 -2
- package/src/findings/reads-and-writes.ts +310 -3
- package/src/findings/unused-declarations.ts +223 -2
- package/src/findings/visibility-narrowing.ts +324 -3
- package/src/implementations.ts +286 -0
- package/src/inventory.ts +2 -2
- package/src/resolve.ts +1 -1
- package/src/stores.ts +77 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declarations of one project carry the deprecation marker, the documentation tag
|
|
3
|
+
* the TypeScript tools recognize, as the checker reports a symbol's tags: gathered from
|
|
4
|
+
* every declaration of the symbol, so overloads, an accessor pair and a merged
|
|
5
|
+
* declaration share them. A declaration list's documentation covers every name it
|
|
6
|
+
* declares, and the checker attaches it to the first name alone, so the later names read
|
|
7
|
+
* the list's own tags from the tree.
|
|
8
|
+
*/
|
|
9
|
+
import { isVariableStatement, SyntaxKind } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { nodeKey } from "./inventory.js";
|
|
11
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
12
|
+
/** The tag the TypeScript tools recognize as marking a declaration deprecated. */
|
|
13
|
+
const DEPRECATED_TAG = "deprecated";
|
|
14
|
+
/** Whether one node's own documentation carries the deprecation tag. */
|
|
15
|
+
function documentedDeprecated(node) {
|
|
16
|
+
const docs = node.jsDoc ?? [];
|
|
17
|
+
return docs.some((doc) => (doc.tags ?? []).some((tag) => tag.kind === SyntaxKind.JSDocDeprecatedTag));
|
|
18
|
+
}
|
|
19
|
+
/** The node a declaration's symbol is resolved at: its name, or itself where it has none. */
|
|
20
|
+
function nameNode(node) {
|
|
21
|
+
return node.name ?? node;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The identifiers of every declaration of one project that carries the deprecation
|
|
25
|
+
* marker, in the inventory's order.
|
|
26
|
+
*
|
|
27
|
+
* The checker is asked only for a symbol with a declaration in a file whose tree holds
|
|
28
|
+
* the tag, so a project whose files hold none costs no round trip, and one that does
|
|
29
|
+
* costs one batch per file and one request per symbol that could carry the tag.
|
|
30
|
+
*/
|
|
31
|
+
export function deprecatedDeclarations(project, held) {
|
|
32
|
+
const marked = new Set();
|
|
33
|
+
const markedPaths = new Set();
|
|
34
|
+
const declared = [];
|
|
35
|
+
for (const file of project.ownSourceFiles()) {
|
|
36
|
+
const named = [];
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
if (documentedDeprecated(node)) {
|
|
39
|
+
markedPaths.add(file.path);
|
|
40
|
+
if (isVariableStatement(node)) {
|
|
41
|
+
for (const declaration of node.declarationList.declarations) {
|
|
42
|
+
const id = held.declarations.get(nodeKey(file, declaration));
|
|
43
|
+
if (id !== undefined) {
|
|
44
|
+
marked.add(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
50
|
+
if (id !== undefined && node !== file) {
|
|
51
|
+
named.push({ id, handle: project.handle(nameNode(node)) });
|
|
52
|
+
}
|
|
53
|
+
node.forEachChild(visit);
|
|
54
|
+
};
|
|
55
|
+
file.forEachChild(visit);
|
|
56
|
+
declared.push(named);
|
|
57
|
+
}
|
|
58
|
+
if (markedPaths.size > 0) {
|
|
59
|
+
const asked = new Map();
|
|
60
|
+
const carriesTag = (symbol) => {
|
|
61
|
+
const known = asked.get(symbol.id);
|
|
62
|
+
if (known !== undefined) {
|
|
63
|
+
return known;
|
|
64
|
+
}
|
|
65
|
+
const carries = symbol.declarations.some((declaration) => markedPaths.has(declaration.path)) &&
|
|
66
|
+
symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
|
|
67
|
+
asked.set(symbol.id, carries);
|
|
68
|
+
return carries;
|
|
69
|
+
};
|
|
70
|
+
for (const named of declared) {
|
|
71
|
+
for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
|
|
72
|
+
const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
|
|
73
|
+
project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
|
|
74
|
+
const one = batch[at];
|
|
75
|
+
if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
|
|
76
|
+
marked.add(one.id);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return held.symbols.filter((symbol) => marked.has(symbol.id)).map((symbol) => symbol.id);
|
|
83
|
+
}
|
package/dist/src/discover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DiagnosticCategory } from "@typescript/native/unstable/sync";
|
|
2
2
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
3
3
|
/** Directory names discovery does not descend into. */
|
|
4
|
-
const IGNORED_DIRECTORIES = new Set(["node_modules", ".git"]);
|
|
4
|
+
export const IGNORED_DIRECTORIES = new Set(["node_modules", ".git"]);
|
|
5
5
|
/** A compiler configuration file discovery recognises under a root. */
|
|
6
6
|
const CONFIG_FILE = /^tsconfig.*\.json$/u;
|
|
7
7
|
/**
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The enum-group class: every member of an `enum` declaration is held back when a value
|
|
3
|
+
* of the enum arrives by conversion rather than by a member's name. The conversions are
|
|
4
|
+
* a type assertion from a number or a string, an element access on the enum object
|
|
5
|
+
* keyed by anything but literal text, and a decoded value typed as the enum: an `any`
|
|
6
|
+
* or `unknown` expression asserted to it, or bound to a variable or returned from a
|
|
7
|
+
* function it annotates, where a number counts too. An enum's own value converts nothing.
|
|
8
|
+
*/
|
|
9
|
+
import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
11
|
+
import { aliasChains } from "./alias-chain.js";
|
|
12
|
+
import { nodeKey } from "./inventory.js";
|
|
13
|
+
import { renderPosition } from "./position.js";
|
|
14
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
15
|
+
/** The operand types a conversion produces an enum value from. */
|
|
16
|
+
const NUMBER = TypeFlags.NumberLike;
|
|
17
|
+
const STRING = TypeFlags.StringLike;
|
|
18
|
+
const DECODED = TypeFlags.Any | TypeFlags.Unknown;
|
|
19
|
+
/** The expression one node is, through however many parentheses are written around it. */
|
|
20
|
+
function unparenthesized(node) {
|
|
21
|
+
let held = node;
|
|
22
|
+
while (isParenthesizedExpression(held)) {
|
|
23
|
+
held = held.expression;
|
|
24
|
+
}
|
|
25
|
+
return held;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The name node a type node names its declaration by: the identifier of a type
|
|
29
|
+
* reference, the right side of a qualified one, through parentheses. A type that is no
|
|
30
|
+
* reference names no declaration.
|
|
31
|
+
*/
|
|
32
|
+
function typeName(type) {
|
|
33
|
+
let held = type;
|
|
34
|
+
while (held !== undefined && isParenthesizedTypeNode(held)) {
|
|
35
|
+
held = held.type;
|
|
36
|
+
}
|
|
37
|
+
if (held?.kind !== SyntaxKind.TypeReference) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const name = held.typeName;
|
|
41
|
+
if (name.kind === SyntaxKind.QualifiedName) {
|
|
42
|
+
return name.right;
|
|
43
|
+
}
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
/** The name node an expression names an object by: an identifier, or a property access's name. */
|
|
47
|
+
function objectName(expression) {
|
|
48
|
+
const held = unparenthesized(expression);
|
|
49
|
+
if (isIdentifier(held)) {
|
|
50
|
+
return held;
|
|
51
|
+
}
|
|
52
|
+
return isPropertyAccessExpression(held) ? held.name : undefined;
|
|
53
|
+
}
|
|
54
|
+
/** The return type annotation of the function a return statement leaves, where it has one. */
|
|
55
|
+
function returnTypeOf(statement) {
|
|
56
|
+
for (let at = statement.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
57
|
+
if (isFunctionDeclaration(at) ||
|
|
58
|
+
isMethodDeclaration(at) ||
|
|
59
|
+
isFunctionExpression(at) ||
|
|
60
|
+
isArrowFunction(at) ||
|
|
61
|
+
isGetAccessorDeclaration(at)) {
|
|
62
|
+
return at.type;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Every site of one file that may produce a value of a type it names, in source order. */
|
|
68
|
+
function sitesOf(file) {
|
|
69
|
+
const found = [];
|
|
70
|
+
const annotated = (type, value, at) => {
|
|
71
|
+
const name = typeName(type);
|
|
72
|
+
if (name !== undefined && value !== undefined) {
|
|
73
|
+
found.push({ form: "annotation", name, operand: value, at });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const visit = (node) => {
|
|
77
|
+
if (isAsExpression(node) || node.kind === SyntaxKind.TypeAssertionExpression) {
|
|
78
|
+
const assertion = node;
|
|
79
|
+
const name = typeName(assertion.type);
|
|
80
|
+
if (name !== undefined) {
|
|
81
|
+
found.push({ form: "assertion", name, operand: assertion.expression, at: node });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (isElementAccessExpression(node)) {
|
|
85
|
+
const key = node.argumentExpression;
|
|
86
|
+
const name = objectName(node.expression);
|
|
87
|
+
if (name !== undefined && !isStringLiteral(key) && !isNoSubstitutionTemplateLiteral(key)) {
|
|
88
|
+
found.push({ form: "lookup", name, operand: undefined, at: node });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (isVariableDeclaration(node)) {
|
|
92
|
+
annotated(node.type, node.initializer, node.initializer ?? node);
|
|
93
|
+
}
|
|
94
|
+
else if (isReturnStatement(node) && node.expression !== undefined) {
|
|
95
|
+
annotated(returnTypeOf(node), node.expression, node.expression);
|
|
96
|
+
}
|
|
97
|
+
node.forEachChild(visit);
|
|
98
|
+
};
|
|
99
|
+
file.forEachChild(visit);
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The parts of one type that are not values of an enum: its constituents where it is a
|
|
104
|
+
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
105
|
+
* source names, whichever number or string it also is.
|
|
106
|
+
*/
|
|
107
|
+
function partsOf(type) {
|
|
108
|
+
const parts = type.isUnionType() ? type.getTypes() : [type];
|
|
109
|
+
return parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The detail one site records about the enum it produces, or undefined where the
|
|
113
|
+
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
114
|
+
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
115
|
+
* the source names.
|
|
116
|
+
*/
|
|
117
|
+
function detailOf(site, enumName, operand) {
|
|
118
|
+
if (site.form === "lookup") {
|
|
119
|
+
return `looked up by an element access on ${enumName}`;
|
|
120
|
+
}
|
|
121
|
+
if (operand === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const parts = partsOf(operand);
|
|
125
|
+
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
126
|
+
return "converted from number";
|
|
127
|
+
}
|
|
128
|
+
if (parts.some((part) => (part.flags & STRING) !== 0)) {
|
|
129
|
+
return site.form === "assertion" ? "converted from string" : undefined;
|
|
130
|
+
}
|
|
131
|
+
if (parts.some((part) => (part.flags & DECODED) !== 0)) {
|
|
132
|
+
return `decoded as ${enumName}`;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The enum-group detector: every member of each enum declaration of the target that a
|
|
138
|
+
* conversion produces a value of, recorded at each conversion.
|
|
139
|
+
*/
|
|
140
|
+
export function enumGroup(input) {
|
|
141
|
+
const { project, held, targetRoot } = input;
|
|
142
|
+
const files = project.ownSourceFiles();
|
|
143
|
+
// The enums of the target, by the identifier of their declaration, with their names.
|
|
144
|
+
const enums = new Map();
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const visit = (node) => {
|
|
147
|
+
if (isEnumDeclaration(node)) {
|
|
148
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
149
|
+
if (id !== undefined) {
|
|
150
|
+
enums.set(id, node.name.getText());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
node.forEachChild(visit);
|
|
154
|
+
};
|
|
155
|
+
file.forEachChild(visit);
|
|
156
|
+
}
|
|
157
|
+
if (enums.size === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const sites = files.flatMap(sitesOf);
|
|
161
|
+
const named = new Map();
|
|
162
|
+
const chains = aliasChains(project, held);
|
|
163
|
+
for (let from = 0; from < sites.length; from += DEFAULT_BATCH_CAP) {
|
|
164
|
+
const run = sites.slice(from, from + DEFAULT_BATCH_CAP);
|
|
165
|
+
project.symbolsAt(run.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
166
|
+
const site = run[index];
|
|
167
|
+
if (symbol === undefined || site === undefined) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
171
|
+
if (target !== undefined) {
|
|
172
|
+
named.set(site, target.id);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
177
|
+
const operandType = new Map();
|
|
178
|
+
for (let from = 0; from < typed.length; from += DEFAULT_BATCH_CAP) {
|
|
179
|
+
const run = typed.slice(from, from + DEFAULT_BATCH_CAP);
|
|
180
|
+
project.checker
|
|
181
|
+
.getTypeAtLocation(run.map((site) => site.operand ?? site.at))
|
|
182
|
+
.forEach((type, index) => {
|
|
183
|
+
const site = run[index];
|
|
184
|
+
if (type !== undefined && site !== undefined) {
|
|
185
|
+
operandType.set(site, type);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const members = new Map();
|
|
190
|
+
for (const symbol of held.symbols) {
|
|
191
|
+
if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
|
|
192
|
+
const listed = members.get(symbol.parent);
|
|
193
|
+
if (listed === undefined) {
|
|
194
|
+
members.set(symbol.parent, [symbol.id]);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
listed.push(symbol.id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const found = [];
|
|
202
|
+
for (const site of sites) {
|
|
203
|
+
const enumId = named.get(site);
|
|
204
|
+
if (enumId === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const detail = detailOf(site, enums.get(enumId) ?? "", operandType.get(site));
|
|
208
|
+
if (detail === undefined) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
212
|
+
for (const id of members.get(enumId) ?? []) {
|
|
213
|
+
found.push({ id, detail, site: position });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the file kinds read about the target's files beside the sweep: every source
|
|
3
|
+
* file below the target root, whether or not a project includes it, and the files a
|
|
4
|
+
* program holds by inclusion rather than by an import.
|
|
5
|
+
*/
|
|
6
|
+
import { IGNORED_DIRECTORIES } from "./discover.js";
|
|
7
|
+
import { packageScope } from "./inventory.js";
|
|
8
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
9
|
+
import { renderRef } from "./ref.js";
|
|
10
|
+
/** A file name the compiler reads as a source file. */
|
|
11
|
+
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
12
|
+
/** A file name the compiler reads as a declaration file, which no build emits code from. */
|
|
13
|
+
const DECLARATION_FILE = /\.d\.(?:[^./]+\.)?[mc]?ts$/u;
|
|
14
|
+
/** A line break as the compiler counts lines. */
|
|
15
|
+
const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/gu;
|
|
16
|
+
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
17
|
+
function lastLine(text) {
|
|
18
|
+
const last = Math.max(text.length - 1, 0);
|
|
19
|
+
let line = 1;
|
|
20
|
+
for (const found of text.matchAll(LINE_BREAK)) {
|
|
21
|
+
if (found.index + found[0].length > last) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
line += 1;
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every source file below the target root, in path order. A directory that cannot be
|
|
30
|
+
* read is skipped, as discovery skips it, and a file that cannot be read is skipped
|
|
31
|
+
* with it: a tree the analysis may not enter holds no file it can report.
|
|
32
|
+
*/
|
|
33
|
+
export function sourceTree(host, targetRoot) {
|
|
34
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
35
|
+
const found = [];
|
|
36
|
+
const pending = [""];
|
|
37
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = host.readDirectory(joinPath(targetRoot, dir));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
47
|
+
if (entry.directory) {
|
|
48
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
49
|
+
pending.push(path);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
found.push({
|
|
64
|
+
path,
|
|
65
|
+
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
66
|
+
endLine: lastLine(text),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return found.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
71
|
+
}
|
|
72
|
+
/** Whether one file changes what the program sees by being included in it. */
|
|
73
|
+
function actsByInclusion(file) {
|
|
74
|
+
return (file.referencedFiles.length > 0 ||
|
|
75
|
+
file.typeReferenceDirectives.length > 0 ||
|
|
76
|
+
file.libReferenceDirectives.length > 0 ||
|
|
77
|
+
file.moduleAugmentations.length > 0 ||
|
|
78
|
+
file.ambientModuleNames.length > 0);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The paths below the target root of one project's own files the program holds by
|
|
82
|
+
* inclusion, in the order the program holds them, each once.
|
|
83
|
+
*/
|
|
84
|
+
export function heldByInclusion(project, targetRoot) {
|
|
85
|
+
const held = new Set();
|
|
86
|
+
const add = (fileName) => {
|
|
87
|
+
const path = relativePath(targetRoot, fileName);
|
|
88
|
+
if (path !== undefined) {
|
|
89
|
+
held.add(path);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
for (const file of project.ownSourceFiles()) {
|
|
93
|
+
if (actsByInclusion(file)) {
|
|
94
|
+
add(file.fileName);
|
|
95
|
+
}
|
|
96
|
+
for (const reference of file.referencedFiles) {
|
|
97
|
+
add(resolvePath(dirnamePath(file.fileName), reference.fileName));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...held];
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finding about a declaration carries beyond what its emitter decides, read from
|
|
3
|
+
* the run: the reachability class and the confidence, the relation that decided the
|
|
4
|
+
* subject, the dead component it falls in, the configurations it holds in, and the
|
|
5
|
+
* severity the configuration gives its code. The dials withhold what they name and
|
|
6
|
+
* every finding that falls with a root they withhold.
|
|
7
|
+
*/
|
|
8
|
+
import { componentId } from "../components.js";
|
|
9
|
+
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
|
+
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
11
|
+
export const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
12
|
+
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
13
|
+
export function capped(found, ceiling) {
|
|
14
|
+
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The severity the configuration gives one code: the key naming the code, else the key
|
|
18
|
+
* naming its family, else the code's default.
|
|
19
|
+
*/
|
|
20
|
+
export function severityOf(config, code, fallback) {
|
|
21
|
+
return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The reachability class of one declaration of the run. A declaration of a library's
|
|
25
|
+
* published API has callers outside the target, and the run loads no consumer that
|
|
26
|
+
* would name them, so it is `possible`; every other declaration has every reference
|
|
27
|
+
* in the loaded program, a private member and a `#private` name among them, and is
|
|
28
|
+
* `certain`.
|
|
29
|
+
*/
|
|
30
|
+
export function reachabilityClasses(swept) {
|
|
31
|
+
const union = swept.matrix.union;
|
|
32
|
+
const published = new Set(union.rooted
|
|
33
|
+
.filter((root) => root.kind === "published-api")
|
|
34
|
+
.map((root) => union.symbols[root.at]?.id));
|
|
35
|
+
return (id) => (published.has(id) ? "possible" : "certain");
|
|
36
|
+
}
|
|
37
|
+
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
38
|
+
export function findingComponents(swept) {
|
|
39
|
+
const held = new Map();
|
|
40
|
+
for (const component of swept.sweep.components) {
|
|
41
|
+
for (const member of component.members) {
|
|
42
|
+
held.set(member, component);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return (id) => {
|
|
46
|
+
const component = held.get(id);
|
|
47
|
+
return component === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: {
|
|
50
|
+
id: component.id,
|
|
51
|
+
root: component.roots.includes(id),
|
|
52
|
+
symbolCount: component.members.length,
|
|
53
|
+
deletableLines: component.deletableLines,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Every finding of the run, each with its component, in the order given: the dead
|
|
59
|
+
* component its subject falls in. A subject in none, a live declaration, a file no
|
|
60
|
+
* program builds or a manifest row, falls with nothing and is given a component of its
|
|
61
|
+
* own, numbered past the run's computed components in the order the findings come, so
|
|
62
|
+
* no two families mint one identifier.
|
|
63
|
+
*/
|
|
64
|
+
export function placed(swept, findings) {
|
|
65
|
+
const componentOf = findingComponents(swept);
|
|
66
|
+
const ids = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.ref, symbol.id]));
|
|
67
|
+
let minted = swept.sweep.components.length;
|
|
68
|
+
return findings.map((finding) => {
|
|
69
|
+
const id = ids.get(finding.symbol.ref);
|
|
70
|
+
const component = id === undefined ? undefined : componentOf(id);
|
|
71
|
+
if (component !== undefined) {
|
|
72
|
+
return { ...finding, component };
|
|
73
|
+
}
|
|
74
|
+
minted += 1;
|
|
75
|
+
return {
|
|
76
|
+
...finding,
|
|
77
|
+
component: {
|
|
78
|
+
id: componentId(minted),
|
|
79
|
+
root: true,
|
|
80
|
+
symbolCount: 1,
|
|
81
|
+
deletableLines: finding.symbol.sizeLines,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The findings no dial withholds. A finding at `allow` or below the minimum confidence
|
|
88
|
+
* is withheld, and so is every finding of the component of a root the dials withhold,
|
|
89
|
+
* because a symbol that falls with a root is dead only through it and its deletion
|
|
90
|
+
* does not compile while the root stays. A withheld finding that is no root withholds
|
|
91
|
+
* itself alone.
|
|
92
|
+
*/
|
|
93
|
+
export function reportable(findings, minConfidence) {
|
|
94
|
+
const dialed = (finding) => finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
95
|
+
const components = new Set(findings
|
|
96
|
+
.filter((finding) => finding.component.root && dialed(finding))
|
|
97
|
+
.map((finding) => finding.component.id));
|
|
98
|
+
return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
|
|
99
|
+
}
|
|
@@ -1,2 +1,66 @@
|
|
|
1
|
+
import { renderRef } from "../ref.js";
|
|
2
|
+
import { severityOf } from "./completion.js";
|
|
3
|
+
const UNUSED_DEPENDENCY = "DS1601";
|
|
4
|
+
/** The severity the kind ships at, which the configuration's keys override. */
|
|
5
|
+
const UNUSED_DEPENDENCY_SEVERITY = "deny";
|
|
6
|
+
const MESSAGES = {
|
|
7
|
+
dependency: "no import in the target needs the declared dependency",
|
|
8
|
+
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
9
|
+
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The dependencies the target needs: every package the projects' uses name, every
|
|
13
|
+
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
14
|
+
* runs outside anything the analysis reads, and every peer a needed package
|
|
15
|
+
* requires, which the target satisfies by declaring it.
|
|
16
|
+
*/
|
|
17
|
+
function neededBy(dependencies) {
|
|
18
|
+
const needed = new Set(dependencies.needed);
|
|
19
|
+
for (const [name, installed] of dependencies.installed) {
|
|
20
|
+
if (installed.command) {
|
|
21
|
+
needed.add(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const pending = [...needed];
|
|
25
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
26
|
+
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
27
|
+
if (!needed.has(peer)) {
|
|
28
|
+
needed.add(peer);
|
|
29
|
+
pending.push(peer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return needed;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A dependency, development dependency or peer dependency of the target's manifest
|
|
37
|
+
* that the target does not need, at the position of its key. Nothing is reported at
|
|
38
|
+
* `allow`. Every subject is a manifest entry, which is `certain` whatever the
|
|
39
|
+
* consumers, so no minimum confidence withholds one.
|
|
40
|
+
*/
|
|
41
|
+
function unusedDependencies(config, dependencies) {
|
|
42
|
+
if (severityOf(config, UNUSED_DEPENDENCY, UNUSED_DEPENDENCY_SEVERITY) === "allow") {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const needed = neededBy(dependencies);
|
|
46
|
+
return dependencies.declared
|
|
47
|
+
.filter((dependency) => !needed.has(dependency.name))
|
|
48
|
+
.map((dependency) => ({
|
|
49
|
+
code: UNUSED_DEPENDENCY,
|
|
50
|
+
position: dependency.position,
|
|
51
|
+
symbol: {
|
|
52
|
+
ref: renderRef(dependencies.manifest, {
|
|
53
|
+
of: "dependency",
|
|
54
|
+
name: dependency.name,
|
|
55
|
+
section: dependency.section,
|
|
56
|
+
}),
|
|
57
|
+
kind: "dependency",
|
|
58
|
+
name: dependency.name,
|
|
59
|
+
sizeLines: 1,
|
|
60
|
+
},
|
|
61
|
+
message: MESSAGES[dependency.section],
|
|
62
|
+
details: { dependencyClass: dependency.section },
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
1
65
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
2
|
-
export const dependenciesAndModuleMachinery = () =>
|
|
66
|
+
export const dependenciesAndModuleMachinery = ({ config, dependencies }) => unusedDependencies(config, dependencies);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
+
import { placed } from "./completion.js";
|
|
2
3
|
import { interfaces } from "./interfaces.js";
|
|
3
4
|
import { intraFunction } from "./intra-function.js";
|
|
4
5
|
import { nonCodeArtifacts } from "./non-code-artifacts.js";
|
|
@@ -17,3 +18,10 @@ export const EMITTERS = new Map([
|
|
|
17
18
|
["self-check", selfCheck],
|
|
18
19
|
["intra-function", intraFunction],
|
|
19
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Every finding of the run, each family's in table order, each placed in its component
|
|
23
|
+
* by the one step that numbers the components minted for subjects that fall with none.
|
|
24
|
+
*/
|
|
25
|
+
export function findingsOf(input) {
|
|
26
|
+
return placed(input.swept, [...EMITTERS.values()].flatMap((emit) => emit(input)));
|
|
27
|
+
}
|