@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,101 @@
|
|
|
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
|
+
|
|
10
|
+
import { isVariableStatement, SyntaxKind, type Node } from "@typescript/native/unstable/ast";
|
|
11
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
12
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
13
|
+
import { DEFAULT_BATCH_CAP } from "./references.ts";
|
|
14
|
+
import type { Handle, ProjectView } from "./session.ts";
|
|
15
|
+
|
|
16
|
+
/** The tag the TypeScript tools recognize as marking a declaration deprecated. */
|
|
17
|
+
const DEPRECATED_TAG = "deprecated";
|
|
18
|
+
|
|
19
|
+
/** Whether one node's own documentation carries the deprecation tag. */
|
|
20
|
+
function documentedDeprecated(node: Node): boolean {
|
|
21
|
+
const docs = (node as { readonly jsDoc?: readonly Node[] }).jsDoc ?? [];
|
|
22
|
+
return docs.some((doc) =>
|
|
23
|
+
((doc as { readonly tags?: readonly Node[] }).tags ?? []).some(
|
|
24
|
+
(tag) => tag.kind === SyntaxKind.JSDocDeprecatedTag,
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The node a declaration's symbol is resolved at: its name, or itself where it has none. */
|
|
30
|
+
function nameNode(node: Node): Node {
|
|
31
|
+
return (node as { readonly name?: Node }).name ?? node;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The identifiers of every declaration of one project that carries the deprecation
|
|
36
|
+
* marker, in the inventory's order.
|
|
37
|
+
*
|
|
38
|
+
* The checker is asked only for a symbol with a declaration in a file whose tree holds
|
|
39
|
+
* the tag, so a project whose files hold none costs no round trip, and one that does
|
|
40
|
+
* costs one batch per file and one request per symbol that could carry the tag.
|
|
41
|
+
*/
|
|
42
|
+
export function deprecatedDeclarations<Brand>(
|
|
43
|
+
project: ProjectView<Brand>,
|
|
44
|
+
held: Inventory,
|
|
45
|
+
): readonly string[] {
|
|
46
|
+
const marked = new Set<string>();
|
|
47
|
+
const markedPaths = new Set<string>();
|
|
48
|
+
const declared: { readonly id: string; readonly handle: Handle<Brand> }[][] = [];
|
|
49
|
+
|
|
50
|
+
for (const file of project.ownSourceFiles()) {
|
|
51
|
+
const named: { readonly id: string; readonly handle: Handle<Brand> }[] = [];
|
|
52
|
+
const visit = (node: Node): void => {
|
|
53
|
+
if (documentedDeprecated(node)) {
|
|
54
|
+
markedPaths.add(file.path);
|
|
55
|
+
if (isVariableStatement(node)) {
|
|
56
|
+
for (const declaration of node.declarationList.declarations) {
|
|
57
|
+
const id = held.declarations.get(nodeKey(file, declaration));
|
|
58
|
+
if (id !== undefined) {
|
|
59
|
+
marked.add(id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
65
|
+
if (id !== undefined && node !== file) {
|
|
66
|
+
named.push({ id, handle: project.handle(nameNode(node)) });
|
|
67
|
+
}
|
|
68
|
+
node.forEachChild(visit);
|
|
69
|
+
};
|
|
70
|
+
file.forEachChild(visit);
|
|
71
|
+
declared.push(named);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (markedPaths.size > 0) {
|
|
75
|
+
const asked = new Map<number, boolean>();
|
|
76
|
+
const carriesTag = (symbol: TSSymbol): boolean => {
|
|
77
|
+
const known = asked.get(symbol.id);
|
|
78
|
+
if (known !== undefined) {
|
|
79
|
+
return known;
|
|
80
|
+
}
|
|
81
|
+
const carries =
|
|
82
|
+
symbol.declarations.some((declaration) => markedPaths.has(declaration.path)) &&
|
|
83
|
+
symbol.getJsDocTags(project.checker).some((tag) => tag.name === DEPRECATED_TAG);
|
|
84
|
+
asked.set(symbol.id, carries);
|
|
85
|
+
return carries;
|
|
86
|
+
};
|
|
87
|
+
for (const named of declared) {
|
|
88
|
+
for (let start = 0; start < named.length; start += DEFAULT_BATCH_CAP) {
|
|
89
|
+
const batch = named.slice(start, start + DEFAULT_BATCH_CAP);
|
|
90
|
+
project.symbolsAt(batch.map((one) => one.handle)).forEach((symbol, at) => {
|
|
91
|
+
const one = batch[at];
|
|
92
|
+
if (symbol !== undefined && one !== undefined && carriesTag(symbol)) {
|
|
93
|
+
marked.add(one.id);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return held.symbols.filter((symbol) => marked.has(symbol.id)).map((symbol) => symbol.id);
|
|
101
|
+
}
|
package/src/discover.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Scope } from "./scope.ts";
|
|
|
7
7
|
import type { Engine } from "./session.ts";
|
|
8
8
|
|
|
9
9
|
/** Directory names discovery does not descend into. */
|
|
10
|
-
const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set(["node_modules", ".git"]);
|
|
10
|
+
export const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set(["node_modules", ".git"]);
|
|
11
11
|
|
|
12
12
|
/** A compiler configuration file discovery recognises under a root. */
|
|
13
13
|
const CONFIG_FILE = /^tsconfig.*\.json$/u;
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
|
|
10
|
+
import {
|
|
11
|
+
isArrowFunction,
|
|
12
|
+
isAsExpression,
|
|
13
|
+
isElementAccessExpression,
|
|
14
|
+
isEnumDeclaration,
|
|
15
|
+
isFunctionDeclaration,
|
|
16
|
+
isFunctionExpression,
|
|
17
|
+
isGetAccessorDeclaration,
|
|
18
|
+
isIdentifier,
|
|
19
|
+
isMethodDeclaration,
|
|
20
|
+
isNoSubstitutionTemplateLiteral,
|
|
21
|
+
isParenthesizedExpression,
|
|
22
|
+
isParenthesizedTypeNode,
|
|
23
|
+
isPropertyAccessExpression,
|
|
24
|
+
isReturnStatement,
|
|
25
|
+
isStringLiteral,
|
|
26
|
+
isVariableDeclaration,
|
|
27
|
+
SyntaxKind,
|
|
28
|
+
type Node,
|
|
29
|
+
type SourceFile,
|
|
30
|
+
} from "@typescript/native/unstable/ast";
|
|
31
|
+
import { TypeFlags, type Type } from "@typescript/native/unstable/sync";
|
|
32
|
+
import { aliasChains } from "./alias-chain.ts";
|
|
33
|
+
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
34
|
+
import { nodeKey } from "./inventory.ts";
|
|
35
|
+
import { renderPosition } from "./position.ts";
|
|
36
|
+
import { DEFAULT_BATCH_CAP } from "./references.ts";
|
|
37
|
+
|
|
38
|
+
/** The operand types a conversion produces an enum value from. */
|
|
39
|
+
const NUMBER = TypeFlags.NumberLike;
|
|
40
|
+
const STRING = TypeFlags.StringLike;
|
|
41
|
+
const DECODED = TypeFlags.Any | TypeFlags.Unknown;
|
|
42
|
+
|
|
43
|
+
/** How one site produces a value of the type it names. */
|
|
44
|
+
type Form = "assertion" | "lookup" | "annotation";
|
|
45
|
+
|
|
46
|
+
/** One place that may produce a value of an enum. */
|
|
47
|
+
interface Site {
|
|
48
|
+
readonly form: Form;
|
|
49
|
+
/** The name node whose symbol says which declaration the site names. */
|
|
50
|
+
readonly name: Node;
|
|
51
|
+
/** The expression whose type decides the conversion, absent for a lookup. */
|
|
52
|
+
readonly operand: Node | undefined;
|
|
53
|
+
/** Where the evidence is recorded. */
|
|
54
|
+
readonly at: Node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The expression one node is, through however many parentheses are written around it. */
|
|
58
|
+
function unparenthesized(node: Node): Node {
|
|
59
|
+
let held = node;
|
|
60
|
+
while (isParenthesizedExpression(held)) {
|
|
61
|
+
held = held.expression;
|
|
62
|
+
}
|
|
63
|
+
return held;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The name node a type node names its declaration by: the identifier of a type
|
|
68
|
+
* reference, the right side of a qualified one, through parentheses. A type that is no
|
|
69
|
+
* reference names no declaration.
|
|
70
|
+
*/
|
|
71
|
+
function typeName(type: Node | undefined): Node | undefined {
|
|
72
|
+
let held = type;
|
|
73
|
+
while (held !== undefined && isParenthesizedTypeNode(held)) {
|
|
74
|
+
held = held.type;
|
|
75
|
+
}
|
|
76
|
+
if (held?.kind !== SyntaxKind.TypeReference) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const name = (held as unknown as { readonly typeName: Node }).typeName;
|
|
80
|
+
if (name.kind === SyntaxKind.QualifiedName) {
|
|
81
|
+
return (name as unknown as { readonly right: Node }).right;
|
|
82
|
+
}
|
|
83
|
+
return name;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The name node an expression names an object by: an identifier, or a property access's name. */
|
|
87
|
+
function objectName(expression: Node): Node | undefined {
|
|
88
|
+
const held = unparenthesized(expression);
|
|
89
|
+
if (isIdentifier(held)) {
|
|
90
|
+
return held;
|
|
91
|
+
}
|
|
92
|
+
return isPropertyAccessExpression(held) ? held.name : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The return type annotation of the function a return statement leaves, where it has one. */
|
|
96
|
+
function returnTypeOf(statement: Node): Node | undefined {
|
|
97
|
+
for (let at = statement.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
98
|
+
if (
|
|
99
|
+
isFunctionDeclaration(at) ||
|
|
100
|
+
isMethodDeclaration(at) ||
|
|
101
|
+
isFunctionExpression(at) ||
|
|
102
|
+
isArrowFunction(at) ||
|
|
103
|
+
isGetAccessorDeclaration(at)
|
|
104
|
+
) {
|
|
105
|
+
return at.type;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every site of one file that may produce a value of a type it names, in source order. */
|
|
112
|
+
function sitesOf(file: SourceFile): readonly Site[] {
|
|
113
|
+
const found: Site[] = [];
|
|
114
|
+
const annotated = (type: Node | undefined, value: Node | undefined, at: Node): void => {
|
|
115
|
+
const name = typeName(type);
|
|
116
|
+
if (name !== undefined && value !== undefined) {
|
|
117
|
+
found.push({ form: "annotation", name, operand: value, at });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const visit = (node: Node): void => {
|
|
121
|
+
if (isAsExpression(node) || node.kind === SyntaxKind.TypeAssertionExpression) {
|
|
122
|
+
const assertion = node as unknown as { readonly type: Node; readonly expression: Node };
|
|
123
|
+
const name = typeName(assertion.type);
|
|
124
|
+
if (name !== undefined) {
|
|
125
|
+
found.push({ form: "assertion", name, operand: assertion.expression, at: node });
|
|
126
|
+
}
|
|
127
|
+
} else if (isElementAccessExpression(node)) {
|
|
128
|
+
const key = node.argumentExpression;
|
|
129
|
+
const name = objectName(node.expression);
|
|
130
|
+
if (name !== undefined && !isStringLiteral(key) && !isNoSubstitutionTemplateLiteral(key)) {
|
|
131
|
+
found.push({ form: "lookup", name, operand: undefined, at: node });
|
|
132
|
+
}
|
|
133
|
+
} else if (isVariableDeclaration(node)) {
|
|
134
|
+
annotated(node.type, node.initializer, node.initializer ?? node);
|
|
135
|
+
} else if (isReturnStatement(node) && node.expression !== undefined) {
|
|
136
|
+
annotated(returnTypeOf(node), node.expression, node.expression);
|
|
137
|
+
}
|
|
138
|
+
node.forEachChild(visit);
|
|
139
|
+
};
|
|
140
|
+
file.forEachChild(visit);
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The parts of one type that are not values of an enum: its constituents where it is a
|
|
146
|
+
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
147
|
+
* source names, whichever number or string it also is.
|
|
148
|
+
*/
|
|
149
|
+
function partsOf(type: Type): readonly Type[] {
|
|
150
|
+
const parts = type.isUnionType() ? type.getTypes() : [type];
|
|
151
|
+
return parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The detail one site records about the enum it produces, or undefined where the
|
|
156
|
+
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
157
|
+
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
158
|
+
* the source names.
|
|
159
|
+
*/
|
|
160
|
+
function detailOf(site: Site, enumName: string, operand: Type | undefined): string | undefined {
|
|
161
|
+
if (site.form === "lookup") {
|
|
162
|
+
return `looked up by an element access on ${enumName}`;
|
|
163
|
+
}
|
|
164
|
+
if (operand === undefined) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const parts = partsOf(operand);
|
|
168
|
+
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
169
|
+
return "converted from number";
|
|
170
|
+
}
|
|
171
|
+
if (parts.some((part) => (part.flags & STRING) !== 0)) {
|
|
172
|
+
return site.form === "assertion" ? "converted from string" : undefined;
|
|
173
|
+
}
|
|
174
|
+
if (parts.some((part) => (part.flags & DECODED) !== 0)) {
|
|
175
|
+
return `decoded as ${enumName}`;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The enum-group detector: every member of each enum declaration of the target that a
|
|
182
|
+
* conversion produces a value of, recorded at each conversion.
|
|
183
|
+
*/
|
|
184
|
+
export function enumGroup<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
185
|
+
const { project, held, targetRoot } = input;
|
|
186
|
+
const files = project.ownSourceFiles();
|
|
187
|
+
|
|
188
|
+
// The enums of the target, by the identifier of their declaration, with their names.
|
|
189
|
+
const enums = new Map<string, string>();
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const visit = (node: Node): void => {
|
|
192
|
+
if (isEnumDeclaration(node)) {
|
|
193
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
194
|
+
if (id !== undefined) {
|
|
195
|
+
enums.set(id, node.name.getText());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
node.forEachChild(visit);
|
|
199
|
+
};
|
|
200
|
+
file.forEachChild(visit);
|
|
201
|
+
}
|
|
202
|
+
if (enums.size === 0) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const sites = files.flatMap(sitesOf);
|
|
207
|
+
const named = new Map<Site, string>();
|
|
208
|
+
const chains = aliasChains(project, held);
|
|
209
|
+
for (let from = 0; from < sites.length; from += DEFAULT_BATCH_CAP) {
|
|
210
|
+
const run = sites.slice(from, from + DEFAULT_BATCH_CAP);
|
|
211
|
+
project.symbolsAt(run.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
212
|
+
const site = run[index];
|
|
213
|
+
if (symbol === undefined || site === undefined) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
217
|
+
if (target !== undefined) {
|
|
218
|
+
named.set(site, target.id);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
224
|
+
const operandType = new Map<Site, Type>();
|
|
225
|
+
for (let from = 0; from < typed.length; from += DEFAULT_BATCH_CAP) {
|
|
226
|
+
const run = typed.slice(from, from + DEFAULT_BATCH_CAP);
|
|
227
|
+
project.checker
|
|
228
|
+
.getTypeAtLocation(run.map((site) => site.operand ?? site.at))
|
|
229
|
+
.forEach((type, index) => {
|
|
230
|
+
const site = run[index];
|
|
231
|
+
if (type !== undefined && site !== undefined) {
|
|
232
|
+
operandType.set(site, type);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const members = new Map<string, string[]>();
|
|
238
|
+
for (const symbol of held.symbols) {
|
|
239
|
+
if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
|
|
240
|
+
const listed = members.get(symbol.parent);
|
|
241
|
+
if (listed === undefined) {
|
|
242
|
+
members.set(symbol.parent, [symbol.id]);
|
|
243
|
+
} else {
|
|
244
|
+
listed.push(symbol.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const found: Evidence[] = [];
|
|
250
|
+
for (const site of sites) {
|
|
251
|
+
const enumId = named.get(site);
|
|
252
|
+
if (enumId === undefined) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const detail = detailOf(site, enums.get(enumId) ?? "", operandType.get(site));
|
|
256
|
+
if (detail === undefined) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
260
|
+
for (const id of members.get(enumId) ?? []) {
|
|
261
|
+
found.push({ id, detail, site: position });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return found;
|
|
265
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
|
|
7
|
+
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { IGNORED_DIRECTORIES } from "./discover.ts";
|
|
9
|
+
import type { Host } from "./host.ts";
|
|
10
|
+
import { packageScope } from "./inventory.ts";
|
|
11
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
12
|
+
import { renderRef } from "./ref.ts";
|
|
13
|
+
import type { ProjectView } from "./session.ts";
|
|
14
|
+
|
|
15
|
+
/** A file name the compiler reads as a source file. */
|
|
16
|
+
const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
|
|
17
|
+
|
|
18
|
+
/** A file name the compiler reads as a declaration file, which no build emits code from. */
|
|
19
|
+
const DECLARATION_FILE = /\.d\.(?:[^./]+\.)?[mc]?ts$/u;
|
|
20
|
+
|
|
21
|
+
/** A line break as the compiler counts lines. */
|
|
22
|
+
const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/gu;
|
|
23
|
+
|
|
24
|
+
/** One source file below the target root. */
|
|
25
|
+
export interface TreeFile {
|
|
26
|
+
/** The path below the target root, with the solidus as separator. */
|
|
27
|
+
readonly path: string;
|
|
28
|
+
/** The stable reference of the file's module. */
|
|
29
|
+
readonly ref: string;
|
|
30
|
+
/** The line of the file's last character, counted from one. */
|
|
31
|
+
readonly endLine: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The target's files, as the file kinds read them. */
|
|
35
|
+
export interface FileFacts {
|
|
36
|
+
/**
|
|
37
|
+
* Every source file below the target root a compiler configuration can include, in
|
|
38
|
+
* path order: a source extension, not a declaration file, and not below a directory
|
|
39
|
+
* discovery skips.
|
|
40
|
+
*/
|
|
41
|
+
readonly tree: readonly TreeFile[];
|
|
42
|
+
/**
|
|
43
|
+
* The paths below the target root of the files a program holds by inclusion rather
|
|
44
|
+
* than by an import. A file that carries a triple-slash directive, augments a module
|
|
45
|
+
* or the global scope, or declares an ambient module changes what every file of the
|
|
46
|
+
* program sees for as long as the program includes it, and a file a triple-slash path
|
|
47
|
+
* reference names is included by the file that names it.
|
|
48
|
+
*/
|
|
49
|
+
readonly heldByInclusion: ReadonlySet<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
53
|
+
function lastLine(text: string): number {
|
|
54
|
+
const last = Math.max(text.length - 1, 0);
|
|
55
|
+
let line = 1;
|
|
56
|
+
for (const found of text.matchAll(LINE_BREAK)) {
|
|
57
|
+
if (found.index + found[0].length > last) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
line += 1;
|
|
61
|
+
}
|
|
62
|
+
return line;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every source file below the target root, in path order. A directory that cannot be
|
|
67
|
+
* read is skipped, as discovery skips it, and a file that cannot be read is skipped
|
|
68
|
+
* with it: a tree the analysis may not enter holds no file it can report.
|
|
69
|
+
*/
|
|
70
|
+
export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
|
|
71
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
72
|
+
const found: TreeFile[] = [];
|
|
73
|
+
const pending = [""];
|
|
74
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
75
|
+
let entries: readonly { readonly name: string; readonly directory: boolean }[];
|
|
76
|
+
try {
|
|
77
|
+
entries = host.readDirectory(joinPath(targetRoot, dir));
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
83
|
+
if (entry.directory) {
|
|
84
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
85
|
+
pending.push(path);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
let text: string;
|
|
93
|
+
try {
|
|
94
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
found.push({
|
|
99
|
+
path,
|
|
100
|
+
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
101
|
+
endLine: lastLine(text),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return found.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether one file changes what the program sees by being included in it. */
|
|
109
|
+
function actsByInclusion(file: SourceFile): boolean {
|
|
110
|
+
return (
|
|
111
|
+
file.referencedFiles.length > 0 ||
|
|
112
|
+
file.typeReferenceDirectives.length > 0 ||
|
|
113
|
+
file.libReferenceDirectives.length > 0 ||
|
|
114
|
+
file.moduleAugmentations.length > 0 ||
|
|
115
|
+
file.ambientModuleNames.length > 0
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The paths below the target root of one project's own files the program holds by
|
|
121
|
+
* inclusion, in the order the program holds them, each once.
|
|
122
|
+
*/
|
|
123
|
+
export function heldByInclusion<Brand>(
|
|
124
|
+
project: ProjectView<Brand>,
|
|
125
|
+
targetRoot: string,
|
|
126
|
+
): readonly string[] {
|
|
127
|
+
const held = new Set<string>();
|
|
128
|
+
const add = (fileName: string): void => {
|
|
129
|
+
const path = relativePath(targetRoot, fileName);
|
|
130
|
+
if (path !== undefined) {
|
|
131
|
+
held.add(path);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
for (const file of project.ownSourceFiles()) {
|
|
135
|
+
if (actsByInclusion(file)) {
|
|
136
|
+
add(file.fileName);
|
|
137
|
+
}
|
|
138
|
+
for (const reference of file.referencedFiles) {
|
|
139
|
+
add(resolvePath(dirnamePath(file.fileName), reference.fileName));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...held];
|
|
143
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Finding } from "../finding.ts";
|
|
2
|
+
|
|
3
|
+
/** Which side of a declared cross-language edge, as the edges document spells it. */
|
|
4
|
+
export type EdgeSideName = "provides" | "used_by";
|
|
5
|
+
|
|
6
|
+
/** One side of a declared cross-language edge that names a symbol of this analyzer's language. */
|
|
7
|
+
export interface EdgeSide {
|
|
8
|
+
/** The edge's identifier, as the edges document spells it. */
|
|
9
|
+
readonly edge: string;
|
|
10
|
+
readonly side: EdgeSideName;
|
|
11
|
+
/** The stable symbol reference the side names. */
|
|
12
|
+
readonly symbol: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One finding a declared edge holds back: the finding the analyzer would report about
|
|
17
|
+
* the symbol one side of the edge names, which a report carries only inside that side's
|
|
18
|
+
* evaluation, because the code that may use the symbol is in the other language.
|
|
19
|
+
*/
|
|
20
|
+
export interface PendingFinding extends EdgeSide {
|
|
21
|
+
readonly finding: Finding;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What the run knows about the code outside the target that can reach its declarations. */
|
|
25
|
+
export interface Boundary {
|
|
26
|
+
readonly consumers: {
|
|
27
|
+
/** The identifier of every consumer the scope declares. */
|
|
28
|
+
readonly declared: readonly string[];
|
|
29
|
+
/** The identifier of every declared consumer the run loaded. */
|
|
30
|
+
readonly loaded: readonly string[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether the target's manifest declares `exports`, so code outside the target can
|
|
34
|
+
* import no file the manifest does not name.
|
|
35
|
+
*/
|
|
36
|
+
readonly encapsulated: boolean;
|
|
37
|
+
/** Every side of a declared cross-language edge that names a symbol of this language. */
|
|
38
|
+
readonly edges: readonly EdgeSide[];
|
|
39
|
+
}
|