@cplieger/deadset-ts 3.5.0 → 3.7.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/finding.js +0 -11
- package/dist/src/findings/boundary.js +1 -0
- package/dist/src/findings/completion.js +135 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +57 -1
- package/dist/src/findings/emitters.js +10 -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 +146 -2
- package/dist/src/findings/unused-declarations.js +150 -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/kinds.js +47 -45
- 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/finding.ts +76 -10
- package/src/findings/boundary.ts +39 -0
- package/src/findings/completion.ts +163 -0
- package/src/findings/dependencies-and-module-machinery.ts +64 -1
- package/src/findings/emitter.ts +24 -3
- package/src/findings/emitters.ts +13 -1
- package/src/findings/interfaces.ts +137 -3
- package/src/findings/non-code-artifacts.ts +124 -2
- package/src/findings/reads-and-writes.ts +196 -3
- package/src/findings/unused-declarations.ts +175 -1
- package/src/findings/visibility-narrowing.ts +319 -3
- package/src/implementations.ts +286 -0
- package/src/inventory.ts +2 -2
- package/src/kinds.ts +79 -45
- 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
|
+
}
|
package/dist/src/finding.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The members of a finding its emitter decides: the code, where the subject is,
|
|
3
|
-
* what the subject is, and the sentence the finding reports. They are the
|
|
4
|
-
* Contract's finding members of the same names, spelled in this language's case.
|
|
5
|
-
*
|
|
6
|
-
* Every other member the finding schema requires follows from these and from the
|
|
7
|
-
* run: the kind's name, its fixability and its ceiling come from the code's row of
|
|
8
|
-
* the issue-kind vocabulary, the severity and the confidence from the resolved
|
|
9
|
-
* configuration, the language from the analyzer. A report completes them, so an
|
|
10
|
-
* emitter cannot state one that disagrees with the vocabulary.
|
|
11
|
-
*/
|
|
12
1
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every finding carries beyond what its emitter decides, read from the run and from
|
|
3
|
+
* the code's row of the issue-kind vocabulary, and the dials that withhold a finding
|
|
4
|
+
* together with every finding that falls with a root they withhold.
|
|
5
|
+
*/
|
|
6
|
+
import { componentId } from "../components.js";
|
|
7
|
+
import { OUTSIDE } from "../graph.js";
|
|
8
|
+
import { KINDS } from "../kinds.js";
|
|
9
|
+
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
|
+
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
11
|
+
const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
12
|
+
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
13
|
+
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
|
+
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
|
+
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
|
+
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 completed, in the order given. A dead declaration carries its
|
|
59
|
+
* candidate's relation, counts, configurations and component; a live one carries no
|
|
60
|
+
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
61
|
+
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
62
|
+
* numbered past the computed ones in finding order, so no two families mint one.
|
|
63
|
+
*/
|
|
64
|
+
export function completed(input, findings) {
|
|
65
|
+
const { swept } = input;
|
|
66
|
+
const union = swept.matrix.union;
|
|
67
|
+
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
68
|
+
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
69
|
+
const classOf = reachabilityClasses(swept);
|
|
70
|
+
const componentOf = findingComponents(swept);
|
|
71
|
+
let minted = swept.sweep.components.length;
|
|
72
|
+
return findings.map((finding) => {
|
|
73
|
+
const row = KINDS.get(finding.code);
|
|
74
|
+
if (row === undefined) {
|
|
75
|
+
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
76
|
+
}
|
|
77
|
+
const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
|
|
78
|
+
const symbol = union.symbols[at];
|
|
79
|
+
const judged = union.subject[at] === true ? symbol : undefined;
|
|
80
|
+
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
81
|
+
const found = symbol === undefined ? undefined : componentOf(symbol.id);
|
|
82
|
+
if (candidate !== undefined && found === undefined) {
|
|
83
|
+
throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
|
|
84
|
+
}
|
|
85
|
+
let component = found;
|
|
86
|
+
if (component === undefined) {
|
|
87
|
+
minted += 1;
|
|
88
|
+
component = {
|
|
89
|
+
id: componentId(minted),
|
|
90
|
+
root: true,
|
|
91
|
+
symbolCount: 1,
|
|
92
|
+
deletableLines: finding.symbol.sizeLines,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const counts = candidate === undefined
|
|
96
|
+
? union.made[at]
|
|
97
|
+
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
98
|
+
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
99
|
+
return {
|
|
100
|
+
code: finding.code,
|
|
101
|
+
kind: row.name,
|
|
102
|
+
language: "ts",
|
|
103
|
+
position: finding.position,
|
|
104
|
+
symbol: finding.symbol,
|
|
105
|
+
reachabilityClass,
|
|
106
|
+
confidence: capped(reachabilityClass, row.maxClass),
|
|
107
|
+
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
108
|
+
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
109
|
+
generated: false,
|
|
110
|
+
component,
|
|
111
|
+
retainedBy: [],
|
|
112
|
+
configurations: candidate?.configurations ??
|
|
113
|
+
(judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
|
|
114
|
+
consumersLoaded: input.boundary.consumers.loaded,
|
|
115
|
+
fixability: row.fixability,
|
|
116
|
+
severity: severityOf(input.config, finding.code, row.defaultSeverity),
|
|
117
|
+
message: finding.message,
|
|
118
|
+
details: finding.details ?? {},
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The findings no dial withholds. A finding at `allow` or below the minimum confidence
|
|
124
|
+
* is withheld, and so is every finding of the component of a root the dials withhold,
|
|
125
|
+
* because a symbol that falls with a root is dead only through it and its deletion
|
|
126
|
+
* does not compile while the root stays. A withheld finding that is no root withholds
|
|
127
|
+
* itself alone.
|
|
128
|
+
*/
|
|
129
|
+
export function reportable(findings, minConfidence) {
|
|
130
|
+
const dialed = (finding) => finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
|
|
131
|
+
const components = new Set(findings
|
|
132
|
+
.filter((finding) => finding.component.root && dialed(finding))
|
|
133
|
+
.map((finding) => finding.component.id));
|
|
134
|
+
return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
|
|
135
|
+
}
|
|
@@ -1,2 +1,58 @@
|
|
|
1
|
+
import { renderRef } from "../ref.js";
|
|
2
|
+
const UNUSED_DEPENDENCY = "DS1601";
|
|
3
|
+
const MESSAGES = {
|
|
4
|
+
dependency: "no import in the target needs the declared dependency",
|
|
5
|
+
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
6
|
+
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The dependencies the target needs: every package the projects' uses name, every
|
|
10
|
+
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
11
|
+
* runs outside anything the analysis reads, and every peer a needed package
|
|
12
|
+
* requires, which the target satisfies by declaring it.
|
|
13
|
+
*/
|
|
14
|
+
function neededBy(dependencies) {
|
|
15
|
+
const needed = new Set(dependencies.needed);
|
|
16
|
+
for (const [name, installed] of dependencies.installed) {
|
|
17
|
+
if (installed.command) {
|
|
18
|
+
needed.add(name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const pending = [...needed];
|
|
22
|
+
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
23
|
+
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
24
|
+
if (!needed.has(peer)) {
|
|
25
|
+
needed.add(peer);
|
|
26
|
+
pending.push(peer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return needed;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A dependency, development dependency or peer dependency of the target's manifest
|
|
34
|
+
* that the target does not need, at the position of its key.
|
|
35
|
+
*/
|
|
36
|
+
function unusedDependencies(dependencies) {
|
|
37
|
+
const needed = neededBy(dependencies);
|
|
38
|
+
return dependencies.declared
|
|
39
|
+
.filter((dependency) => !needed.has(dependency.name))
|
|
40
|
+
.map((dependency) => ({
|
|
41
|
+
code: UNUSED_DEPENDENCY,
|
|
42
|
+
position: dependency.position,
|
|
43
|
+
symbol: {
|
|
44
|
+
ref: renderRef(dependencies.manifest, {
|
|
45
|
+
of: "dependency",
|
|
46
|
+
name: dependency.name,
|
|
47
|
+
section: dependency.section,
|
|
48
|
+
}),
|
|
49
|
+
kind: "dependency",
|
|
50
|
+
name: dependency.name,
|
|
51
|
+
sizeLines: 1,
|
|
52
|
+
},
|
|
53
|
+
message: MESSAGES[dependency.section],
|
|
54
|
+
details: { dependencyClass: dependency.section },
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
1
57
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|
|
2
|
-
export const dependenciesAndModuleMachinery = () =>
|
|
58
|
+
export const dependenciesAndModuleMachinery = ({ dependencies }) => unusedDependencies(dependencies);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
|
|
2
|
+
import { completed, reportable } 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,12 @@ export const EMITTERS = new Map([
|
|
|
17
18
|
["self-check", selfCheck],
|
|
18
19
|
["intra-function", intraFunction],
|
|
19
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Every finding of the run the dials do not withhold, each family's in table order: every
|
|
23
|
+
* family's findings completed by one step, then withheld by one step, so a root one
|
|
24
|
+
* family reports withholds what another family reports in its component.
|
|
25
|
+
*/
|
|
26
|
+
export function findingsOf(input) {
|
|
27
|
+
const found = [...EMITTERS.values()].flatMap((emit) => emit(input));
|
|
28
|
+
return reportable(completed(input, found), input.config.analysis.minConfidence);
|
|
29
|
+
}
|