@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,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dependencies the target's manifest declares, and what the projects' own files
|
|
3
|
+
* need from outside the target. A file needs a package it imports, re-exports from,
|
|
4
|
+
* augments or references the types of, by name or through the file the specifier
|
|
5
|
+
* resolves to; a configuration needs one it names. A package one dependency pulls in
|
|
6
|
+
* for itself is that dependency's need, not the target's.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isExportDeclaration,
|
|
11
|
+
isExternalModuleReference,
|
|
12
|
+
isIdentifier,
|
|
13
|
+
isImportDeclaration,
|
|
14
|
+
isImportEqualsDeclaration,
|
|
15
|
+
isNamespaceExport,
|
|
16
|
+
isShorthandPropertyAssignment,
|
|
17
|
+
isStringLiteral,
|
|
18
|
+
isNoSubstitutionTemplateLiteral,
|
|
19
|
+
SyntaxKind,
|
|
20
|
+
type Node,
|
|
21
|
+
type SourceFile,
|
|
22
|
+
} from "@typescript/native/unstable/ast";
|
|
23
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
24
|
+
import type { FindingPosition } from "./finding.ts";
|
|
25
|
+
import type { Host } from "./host.ts";
|
|
26
|
+
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
27
|
+
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
28
|
+
import type { DependencySection, Module } from "./ref.ts";
|
|
29
|
+
import { DEFAULT_BATCH_CAP } from "./references.ts";
|
|
30
|
+
import type { Handle, ProjectView } from "./session.ts";
|
|
31
|
+
|
|
32
|
+
const MANIFEST = "package.json";
|
|
33
|
+
const MODULES_DIR = "node_modules";
|
|
34
|
+
const MODULES_SEGMENT = `/${MODULES_DIR}/`;
|
|
35
|
+
const TYPES_SCOPE = "@types/";
|
|
36
|
+
|
|
37
|
+
/** The manifest sections a dependency is declared in, each by the selector its reference spells. */
|
|
38
|
+
const SECTIONS: readonly (readonly [string, DependencySection])[] = [
|
|
39
|
+
["dependencies", "dependency"],
|
|
40
|
+
["devDependencies", "dev-dependency"],
|
|
41
|
+
["peerDependencies", "peer-dependency"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** The owner of a use no declaration of the inventory holds: a configuration's. */
|
|
45
|
+
const PROJECT_OWNER = "";
|
|
46
|
+
|
|
47
|
+
/** One dependency the target's manifest declares. */
|
|
48
|
+
export interface DeclaredDependency {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly section: DependencySection;
|
|
51
|
+
/** Where the manifest writes the dependency's key, below the target root. */
|
|
52
|
+
readonly position: FindingPosition;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What the installed copy of one declared dependency states about itself. */
|
|
56
|
+
export interface InstalledDependency {
|
|
57
|
+
/** Whether its manifest declares a command. */
|
|
58
|
+
readonly command: boolean;
|
|
59
|
+
/** The peer dependencies its manifest requires: every peer not marked optional. */
|
|
60
|
+
readonly peers: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What one project's own files and configuration need from outside the target. */
|
|
64
|
+
export interface ProjectNeeds {
|
|
65
|
+
/** Every package a use names or resolves to. */
|
|
66
|
+
readonly packages: ReadonlySet<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Per declaration of the inventory, the packages the uses written inside it name; a
|
|
69
|
+
* use is held by the innermost declaration around it. The empty key holds what the
|
|
70
|
+
* compiler configuration names, which no deletion removes.
|
|
71
|
+
*/
|
|
72
|
+
readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The run's dependency evidence. */
|
|
76
|
+
export interface Dependencies {
|
|
77
|
+
/** The scope every dependency's reference is written in: the target's manifest. */
|
|
78
|
+
readonly manifest: Module;
|
|
79
|
+
/** Every declared dependency, in the order the manifest writes them. */
|
|
80
|
+
readonly declared: readonly DeclaredDependency[];
|
|
81
|
+
/** The installed copy of each declared dependency the run found, by name. */
|
|
82
|
+
readonly installed: ReadonlyMap<string, InstalledDependency>;
|
|
83
|
+
/** Every package some project needs. */
|
|
84
|
+
readonly needed: ReadonlySet<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Per deletion candidate, the declared dependencies whose last use in the target is
|
|
87
|
+
* written inside it, sorted bytewise; a candidate holding no last use has no entry.
|
|
88
|
+
*/
|
|
89
|
+
readonly lastUses: ReadonlyMap<string, readonly string[]>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One JSON document's decoded value, or undefined where it cannot be read or parsed. */
|
|
97
|
+
function readJSON(host: Host, path: string): { text: string; value: unknown } | undefined {
|
|
98
|
+
let text: string;
|
|
99
|
+
try {
|
|
100
|
+
text = host.readFile(path);
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return { text, value: JSON.parse(text) };
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The package a bare module specifier names: its first segment, or its first two
|
|
113
|
+
* where it starts with a scope. A relative, absolute or scheme-prefixed specifier and
|
|
114
|
+
* a subpath import name no package.
|
|
115
|
+
*/
|
|
116
|
+
export function packageOfSpecifier(specifier: string): string | undefined {
|
|
117
|
+
if (/^[./#]/u.test(specifier) || /^[^/]*:/u.test(specifier)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const segments = specifier.split("/");
|
|
121
|
+
const [first, second] = segments;
|
|
122
|
+
if (first === undefined || first === "") {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (!first.startsWith("@")) {
|
|
126
|
+
return first;
|
|
127
|
+
}
|
|
128
|
+
return second === undefined || second === "" || first === "@" ? undefined : `${first}/${second}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The package an installed file belongs to, read below the last dependency directory of its path. */
|
|
132
|
+
function packageOfPath(path: string): string | undefined {
|
|
133
|
+
const at = path.lastIndexOf(MODULES_SEGMENT);
|
|
134
|
+
return at < 0 ? undefined : packageOfSpecifier(path.slice(at + MODULES_SEGMENT.length));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The packages a type reference names: the type package the compiler reads for it
|
|
139
|
+
* where the program holds a file of one, else the package of that name, else both.
|
|
140
|
+
*/
|
|
141
|
+
function typePackages(name: string, programPackages: ReadonlySet<string>): readonly string[] {
|
|
142
|
+
const named = packageOfSpecifier(name);
|
|
143
|
+
if (named === undefined) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
if (named.startsWith(TYPES_SCOPE)) {
|
|
147
|
+
return [named];
|
|
148
|
+
}
|
|
149
|
+
const typed = TYPES_SCOPE + (named.startsWith("@") ? named.slice(1).replace("/", "__") : named);
|
|
150
|
+
for (const candidate of [typed, named]) {
|
|
151
|
+
if (programPackages.has(candidate)) {
|
|
152
|
+
return [candidate];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [typed, named];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The packages the compiler configuration of one project names: its type packages,
|
|
160
|
+
* its JSX import source, the helper library it imports, its plugins, and every
|
|
161
|
+
* package a configuration file it extends through is read from.
|
|
162
|
+
*/
|
|
163
|
+
function configurationPackages(
|
|
164
|
+
host: Host,
|
|
165
|
+
configFile: string,
|
|
166
|
+
options: Readonly<Record<string, unknown>>,
|
|
167
|
+
programPackages: ReadonlySet<string>,
|
|
168
|
+
): Set<string> {
|
|
169
|
+
const found = new Set<string>();
|
|
170
|
+
const add = (name: unknown): void => {
|
|
171
|
+
const named = typeof name === "string" ? packageOfSpecifier(name) : undefined;
|
|
172
|
+
if (named !== undefined) {
|
|
173
|
+
found.add(named);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const types = options["types"];
|
|
177
|
+
if (Array.isArray(types)) {
|
|
178
|
+
for (const name of types) {
|
|
179
|
+
if (typeof name === "string") {
|
|
180
|
+
typePackages(name, programPackages).forEach((one) => found.add(one));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
add(options["jsxImportSource"]);
|
|
185
|
+
if (options["importHelpers"] === true) {
|
|
186
|
+
found.add("tslib");
|
|
187
|
+
}
|
|
188
|
+
const plugins = options["plugins"];
|
|
189
|
+
if (Array.isArray(plugins)) {
|
|
190
|
+
for (const plugin of plugins) {
|
|
191
|
+
add(isRecord(plugin) ? plugin["name"] : undefined);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// A local `extends` is followed, because that file is the project's configuration
|
|
196
|
+
// too; a package's configuration is that package's.
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
const pending = [configFile];
|
|
199
|
+
for (let file = pending.pop(); file !== undefined; file = pending.pop()) {
|
|
200
|
+
if (seen.has(file)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
seen.add(file);
|
|
204
|
+
const read = readJSON(host, file);
|
|
205
|
+
const extended = isRecord(read?.value) ? read.value["extends"] : undefined;
|
|
206
|
+
for (const entry of Array.isArray(extended) ? extended : [extended]) {
|
|
207
|
+
if (typeof entry !== "string") {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (entry.startsWith(".") || isAbsolutePath(entry)) {
|
|
211
|
+
const local = resolvePath(dirnamePath(file), entry);
|
|
212
|
+
pending.push(local.endsWith(".json") ? local : `${local}.json`);
|
|
213
|
+
} else {
|
|
214
|
+
add(entry);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A node that carries its own text: a name, or a literal module specifier. */
|
|
222
|
+
type Texted = Node & { readonly text: string };
|
|
223
|
+
|
|
224
|
+
/** Whether one node is a string the source writes literally, which a module specifier is. */
|
|
225
|
+
function isLiteralText(node: Node): node is Texted {
|
|
226
|
+
return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The import declaration one module specifier is written in, if it is an import's. */
|
|
230
|
+
function importOf(specifier: Node): Node | undefined {
|
|
231
|
+
const parent = specifier.parent as Node | undefined;
|
|
232
|
+
if (parent !== undefined && isExternalModuleReference(parent)) {
|
|
233
|
+
return parent.parent;
|
|
234
|
+
}
|
|
235
|
+
return parent;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The local names one import declaration binds. */
|
|
239
|
+
function bindingsOf(node: Node | undefined): readonly Texted[] {
|
|
240
|
+
if (node === undefined) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
if (isImportEqualsDeclaration(node)) {
|
|
244
|
+
return [node.name];
|
|
245
|
+
}
|
|
246
|
+
if (!isImportDeclaration(node) || node.importClause === undefined) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const clause = node.importClause;
|
|
250
|
+
const names: Texted[] = clause.name === undefined ? [] : [clause.name];
|
|
251
|
+
const bindings = clause.namedBindings;
|
|
252
|
+
if (bindings !== undefined) {
|
|
253
|
+
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
254
|
+
names.push(bindings.name);
|
|
255
|
+
} else {
|
|
256
|
+
names.push(...bindings.elements.map((element) => element.name));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return names;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Every package one project needs, and which declaration each use is written in.
|
|
264
|
+
* One local walk and one batched resolution per file: a use of an import binding is
|
|
265
|
+
* an identifier resolving to the binding's own symbol, so a local declaration that
|
|
266
|
+
* shadows the name is not one.
|
|
267
|
+
*/
|
|
268
|
+
export function projectNeeds<Brand>(
|
|
269
|
+
project: ProjectView<Brand>,
|
|
270
|
+
held: Inventory,
|
|
271
|
+
host: Host,
|
|
272
|
+
): ProjectNeeds {
|
|
273
|
+
const programPackages = new Set<string>();
|
|
274
|
+
for (const name of project.program.getSourceFileNames()) {
|
|
275
|
+
const named = packageOfPath(name);
|
|
276
|
+
if (named !== undefined) {
|
|
277
|
+
programPackages.add(named);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const packages = configurationPackages(
|
|
281
|
+
host,
|
|
282
|
+
project.configFile,
|
|
283
|
+
project.program.getCompilerOptions() as Readonly<Record<string, unknown>>,
|
|
284
|
+
programPackages,
|
|
285
|
+
);
|
|
286
|
+
const uses = new Map<string, Set<string>>();
|
|
287
|
+
const use = (owners: readonly string[], named: Iterable<string>): void => {
|
|
288
|
+
for (const one of named) {
|
|
289
|
+
packages.add(one);
|
|
290
|
+
for (const owner of owners) {
|
|
291
|
+
let set = uses.get(owner);
|
|
292
|
+
if (set === undefined) {
|
|
293
|
+
set = new Set();
|
|
294
|
+
uses.set(owner, set);
|
|
295
|
+
}
|
|
296
|
+
set.add(one);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
use([PROJECT_OWNER], packages);
|
|
301
|
+
|
|
302
|
+
for (const file of project.ownSourceFiles()) {
|
|
303
|
+
fileNeeds(project, held, file, programPackages, use);
|
|
304
|
+
}
|
|
305
|
+
return { packages, uses };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The declarations of the inventory a use written at one node belongs to. */
|
|
309
|
+
function ownersOf(file: SourceFile, held: Inventory, node: Node): readonly string[] {
|
|
310
|
+
const parent = node.parent as Node | undefined;
|
|
311
|
+
// A re-export names its module once for every specifier it declares, so the use is
|
|
312
|
+
// each specifier's: deleting one of two leaves the module used.
|
|
313
|
+
if (parent !== undefined && isExportDeclaration(parent) && parent.exportClause !== undefined) {
|
|
314
|
+
const clause = parent.exportClause;
|
|
315
|
+
const declared = isNamespaceExport(clause) ? [clause] : clause.elements;
|
|
316
|
+
const owners = declared
|
|
317
|
+
.map((one) => held.declarations.get(nodeKey(file, one)))
|
|
318
|
+
.filter((id): id is string => id !== undefined);
|
|
319
|
+
if (owners.length > 0) {
|
|
320
|
+
return owners;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (let at: Node | undefined = node; at !== undefined; at = at.parent as Node | undefined) {
|
|
324
|
+
const id = held.declarations.get(nodeKey(file, at));
|
|
325
|
+
if (id !== undefined) {
|
|
326
|
+
return [id];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return [PROJECT_OWNER];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function fileNeeds<Brand>(
|
|
333
|
+
project: ProjectView<Brand>,
|
|
334
|
+
held: Inventory,
|
|
335
|
+
file: SourceFile,
|
|
336
|
+
programPackages: ReadonlySet<string>,
|
|
337
|
+
use: (owners: readonly string[], named: Iterable<string>) => void,
|
|
338
|
+
): void {
|
|
339
|
+
const specifiers = [...file.imports, ...file.moduleAugmentations].filter(isLiteralText);
|
|
340
|
+
const bindings: { readonly node: Texted; readonly specifier: Texted }[] = [];
|
|
341
|
+
const bound = new Set<Node>();
|
|
342
|
+
const binding = new Set<Node>();
|
|
343
|
+
for (const specifier of specifiers) {
|
|
344
|
+
const names = bindingsOf(importOf(specifier));
|
|
345
|
+
if (names.length > 0) {
|
|
346
|
+
binding.add(specifier);
|
|
347
|
+
}
|
|
348
|
+
for (const name of names) {
|
|
349
|
+
bindings.push({ node: name, specifier });
|
|
350
|
+
bound.add(name);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const names = new Set(bindings.map(({ node }) => node.text));
|
|
354
|
+
const candidates: Node[] = [];
|
|
355
|
+
const visit = (node: Node): void => {
|
|
356
|
+
if (isIdentifier(node) && names.has(node.text) && !bound.has(node)) {
|
|
357
|
+
candidates.push(node);
|
|
358
|
+
}
|
|
359
|
+
node.forEachChild(visit);
|
|
360
|
+
};
|
|
361
|
+
if (names.size > 0) {
|
|
362
|
+
visit(file);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const asked = [...specifiers, ...bindings.map(({ node }) => node), ...candidates];
|
|
366
|
+
const resolved: (TSSymbol | undefined)[] = [];
|
|
367
|
+
for (let at = 0; at < asked.length; at += DEFAULT_BATCH_CAP) {
|
|
368
|
+
const handles: Handle<Brand>[] = asked
|
|
369
|
+
.slice(at, at + DEFAULT_BATCH_CAP)
|
|
370
|
+
.map((node) => project.handle(node));
|
|
371
|
+
resolved.push(...project.symbolsAt(handles));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const packagesOf = new Map<Node, readonly string[]>();
|
|
375
|
+
specifiers.forEach((specifier, index) => {
|
|
376
|
+
const named = new Set<string>();
|
|
377
|
+
const byName = packageOfSpecifier(specifier.text);
|
|
378
|
+
if (byName !== undefined) {
|
|
379
|
+
named.add(byName);
|
|
380
|
+
}
|
|
381
|
+
for (const declaration of resolved[index]?.declarations ?? []) {
|
|
382
|
+
const byPath = packageOfPath(declaration.path);
|
|
383
|
+
if (byPath !== undefined) {
|
|
384
|
+
named.add(byPath);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
packagesOf.set(specifier, [...named]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const bySymbol = new Map<number, readonly string[]>();
|
|
391
|
+
bindings.forEach(({ specifier }, index) => {
|
|
392
|
+
const symbol = resolved[specifiers.length + index];
|
|
393
|
+
if (symbol !== undefined) {
|
|
394
|
+
bySymbol.set(symbol.id, packagesOf.get(specifier) ?? []);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
for (const specifier of specifiers) {
|
|
399
|
+
// An import that binds names is used where a binding is; any other form, a bare
|
|
400
|
+
// import, a re-export, an `import()` or an augmentation, is a use where it stands.
|
|
401
|
+
const owners = binding.has(specifier) ? [] : ownersOf(file, held, specifier);
|
|
402
|
+
use(owners, packagesOf.get(specifier) ?? []);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const offset = specifiers.length + bindings.length;
|
|
406
|
+
candidates.forEach((node, index) => {
|
|
407
|
+
let symbol = resolved[offset + index];
|
|
408
|
+
const parent = node.parent as Node | undefined;
|
|
409
|
+
if (parent !== undefined && isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
410
|
+
symbol = project.shorthandValueAt(project.handle(node));
|
|
411
|
+
}
|
|
412
|
+
const named = symbol === undefined ? undefined : bySymbol.get(symbol.id);
|
|
413
|
+
if (named !== undefined) {
|
|
414
|
+
use(ownersOf(file, held, node), named);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
for (const reference of file.typeReferenceDirectives) {
|
|
419
|
+
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
424
|
+
function valueEnd(text: string, at: number): number {
|
|
425
|
+
const first = text.charAt(at);
|
|
426
|
+
if (first === '"') {
|
|
427
|
+
let end = at + 1;
|
|
428
|
+
while (end < text.length && text.charAt(end) !== '"') {
|
|
429
|
+
end += text.charAt(end) === "\\" ? 2 : 1;
|
|
430
|
+
}
|
|
431
|
+
return end + 1;
|
|
432
|
+
}
|
|
433
|
+
if (first === "{" || first === "[") {
|
|
434
|
+
let depth = 0;
|
|
435
|
+
let end = at;
|
|
436
|
+
while (end < text.length) {
|
|
437
|
+
const one = text.charAt(end);
|
|
438
|
+
if (one === '"') {
|
|
439
|
+
end = valueEnd(text, end);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (one === "{" || one === "[") {
|
|
443
|
+
depth += 1;
|
|
444
|
+
} else if (one === "}" || one === "]") {
|
|
445
|
+
depth -= 1;
|
|
446
|
+
if (depth === 0) {
|
|
447
|
+
return end + 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
end += 1;
|
|
451
|
+
}
|
|
452
|
+
return end;
|
|
453
|
+
}
|
|
454
|
+
let end = at;
|
|
455
|
+
while (end < text.length && !",}] \t\n\r".includes(text.charAt(end))) {
|
|
456
|
+
end += 1;
|
|
457
|
+
}
|
|
458
|
+
return end;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The offset of the first character at or after `at` that is not JSON whitespace. */
|
|
462
|
+
function skipWhitespace(text: string, at: number): number {
|
|
463
|
+
let end = at;
|
|
464
|
+
while (end < text.length && " \t\n\r".includes(text.charAt(end))) {
|
|
465
|
+
end += 1;
|
|
466
|
+
}
|
|
467
|
+
return end;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Each member of the object starting at `at`, by its decoded key, at the offset of the
|
|
472
|
+
* key's opening quote and of its value. A key written twice is its last occurrence,
|
|
473
|
+
* which is the one the parser kept.
|
|
474
|
+
*/
|
|
475
|
+
function membersAt(text: string, at: number): Map<string, { key: number; value: number }> {
|
|
476
|
+
const found = new Map<string, { key: number; value: number }>();
|
|
477
|
+
if (text.charAt(at) !== "{") {
|
|
478
|
+
return found;
|
|
479
|
+
}
|
|
480
|
+
let cursor = skipWhitespace(text, at + 1);
|
|
481
|
+
while (text.charAt(cursor) === '"') {
|
|
482
|
+
const keyEnd = valueEnd(text, cursor);
|
|
483
|
+
const key = JSON.parse(text.slice(cursor, keyEnd)) as string;
|
|
484
|
+
const value = skipWhitespace(text, skipWhitespace(text, keyEnd) + 1);
|
|
485
|
+
found.delete(key);
|
|
486
|
+
found.set(key, { key: cursor, value });
|
|
487
|
+
cursor = skipWhitespace(text, valueEnd(text, value));
|
|
488
|
+
if (text.charAt(cursor) === ",") {
|
|
489
|
+
cursor = skipWhitespace(text, cursor + 1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return found;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The line and column of one offset, a CR, an LF and a CRLF each ending a line. */
|
|
496
|
+
function lineAndColumn(text: string, offset: number): { line: number; column: number } {
|
|
497
|
+
let line = 1;
|
|
498
|
+
let start = 0;
|
|
499
|
+
for (let at = 0; at < offset; at += 1) {
|
|
500
|
+
const one = text.charAt(at);
|
|
501
|
+
if (one === "\n" || (one === "\r" && text.charAt(at + 1) !== "\n")) {
|
|
502
|
+
line += 1;
|
|
503
|
+
start = at + 1;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { line, column: offset - start + 1 };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The installed copy of one dependency, read where the package manager puts it for the target. */
|
|
510
|
+
function installedOf(host: Host, root: string, name: string): InstalledDependency | undefined {
|
|
511
|
+
for (let dir = root; ; dir = dirnamePath(dir)) {
|
|
512
|
+
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
513
|
+
if (read !== undefined && isRecord(read.value)) {
|
|
514
|
+
const manifest = read.value;
|
|
515
|
+
const bin = manifest["bin"];
|
|
516
|
+
const peers = manifest["peerDependencies"];
|
|
517
|
+
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
518
|
+
? manifest["peerDependenciesMeta"]
|
|
519
|
+
: {};
|
|
520
|
+
return {
|
|
521
|
+
command:
|
|
522
|
+
(typeof bin === "string" && bin !== "") || (isRecord(bin) && Object.keys(bin).length > 0),
|
|
523
|
+
peers: isRecord(peers)
|
|
524
|
+
? Object.keys(peers).filter((peer) => {
|
|
525
|
+
const about = meta[peer];
|
|
526
|
+
return !(isRecord(about) && about["optional"] === true);
|
|
527
|
+
})
|
|
528
|
+
: [],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (dirnamePath(dir) === dir) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Every dependency the manifest text declares, in the order the text writes them. */
|
|
538
|
+
function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDependency[] {
|
|
539
|
+
const sections = membersAt(text, skipWhitespace(text, 0));
|
|
540
|
+
const declared: { readonly dependency: DeclaredDependency; readonly at: number }[] = [];
|
|
541
|
+
for (const [member, section] of SECTIONS) {
|
|
542
|
+
const value = manifest[member];
|
|
543
|
+
const held = sections.get(member);
|
|
544
|
+
if (!isRecord(value) || held === undefined) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
for (const [name, { key }] of membersAt(text, held.value)) {
|
|
548
|
+
const { line, column } = lineAndColumn(text, key);
|
|
549
|
+
declared.push({
|
|
550
|
+
dependency: { name, section, position: { path: MANIFEST, line, column, endLine: line } },
|
|
551
|
+
at: key,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The run's dependency evidence: the target's manifest read with the position of every
|
|
560
|
+
* dependency it declares, the installed copy of each, what the projects need, and the
|
|
561
|
+
* last uses each deletion candidate holds.
|
|
562
|
+
*
|
|
563
|
+
* Only the manifest at the target root is read, as for entry points. A manifest the
|
|
564
|
+
* program cannot read declares nothing.
|
|
565
|
+
*/
|
|
566
|
+
export function dependenciesOf(
|
|
567
|
+
host: Host,
|
|
568
|
+
targetRoot: string,
|
|
569
|
+
perProject: readonly ProjectNeeds[],
|
|
570
|
+
candidates: readonly string[],
|
|
571
|
+
): Dependencies {
|
|
572
|
+
const read = readJSON(host, joinPath(targetRoot, MANIFEST));
|
|
573
|
+
const manifest = isRecord(read?.value) ? read.value : undefined;
|
|
574
|
+
const scope = packageScope(host, targetRoot)(MANIFEST);
|
|
575
|
+
const declared =
|
|
576
|
+
read !== undefined && manifest !== undefined ? declaredIn(read.text, manifest) : [];
|
|
577
|
+
|
|
578
|
+
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
579
|
+
const installed = new Map<string, InstalledDependency>();
|
|
580
|
+
for (const dependency of declared) {
|
|
581
|
+
if (!installed.has(dependency.name)) {
|
|
582
|
+
const found = installedOf(host, root, dependency.name);
|
|
583
|
+
if (found !== undefined) {
|
|
584
|
+
installed.set(dependency.name, found);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// A declaration several projects compile is one user, and a use any project makes is
|
|
590
|
+
// a use, so the uses are merged by declaration before anything is counted.
|
|
591
|
+
const needed = new Set<string>();
|
|
592
|
+
const uses = new Map<string, Set<string>>();
|
|
593
|
+
for (const project of perProject) {
|
|
594
|
+
project.packages.forEach((one) => needed.add(one));
|
|
595
|
+
for (const [id, used] of project.uses) {
|
|
596
|
+
const merged = uses.get(id) ?? new Set<string>();
|
|
597
|
+
used.forEach((one) => merged.add(one));
|
|
598
|
+
uses.set(id, merged);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const users = new Map<string, number>();
|
|
602
|
+
for (const used of uses.values()) {
|
|
603
|
+
used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));
|
|
604
|
+
}
|
|
605
|
+
const names = new Set(declared.map((dependency) => dependency.name));
|
|
606
|
+
const lastUses = new Map<string, readonly string[]>();
|
|
607
|
+
for (const id of candidates) {
|
|
608
|
+
const sole = [...(uses.get(id) ?? [])].filter((one) => users.get(one) === 1 && names.has(one));
|
|
609
|
+
if (sole.length > 0) {
|
|
610
|
+
lastUses.set(
|
|
611
|
+
id,
|
|
612
|
+
sole.sort((a, b) => (a < b ? -1 : 1)),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { manifest: scope, declared, installed, needed, lastUses };
|
|
617
|
+
}
|