@cplieger/deadset-ts 2.2.0 → 2.3.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/README.md +5 -3
- package/dist/src/analysis.js +109 -0
- package/dist/src/config.js +6 -0
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/index.js +1 -0
- package/dist/src/manifest.js +120 -0
- package/dist/src/references.js +15 -79
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +48 -3
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +169 -0
- package/src/config.ts +7 -0
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +174 -0
- package/src/references.ts +15 -83
- package/src/roots.ts +467 -0
- package/src/run.ts +55 -4
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry points a package manifest names, which are roots because something
|
|
3
|
+
* outside the analyzed program reaches them: a consumer importing the package, or a
|
|
4
|
+
* shell running its command.
|
|
5
|
+
*
|
|
6
|
+
* Only the target's own manifest is read. A manifest below the target root belongs
|
|
7
|
+
* to another package, whose entry points are that package's roots and not this
|
|
8
|
+
* target's.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Host } from "./host.ts";
|
|
12
|
+
import { isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
13
|
+
|
|
14
|
+
/** The manifest's own name. */
|
|
15
|
+
const MANIFEST = "package.json";
|
|
16
|
+
|
|
17
|
+
/** The three members that name an entry point beside `exports` and `bin`. */
|
|
18
|
+
const LEGACY_MEMBERS = ["main", "module", "types"] as const;
|
|
19
|
+
|
|
20
|
+
/** What reaches one entry point the manifest names. */
|
|
21
|
+
export type EntryRole =
|
|
22
|
+
/** A consumer importing the package, which is a caller the analysis cannot see. */
|
|
23
|
+
| "import"
|
|
24
|
+
/** A shell running the package's command, which is a caller that exists. */
|
|
25
|
+
| "run";
|
|
26
|
+
|
|
27
|
+
/** One entry point the manifest names. */
|
|
28
|
+
export interface ManifestEntry {
|
|
29
|
+
/**
|
|
30
|
+
* The member that named it, spelled as a reader of the manifest reads one:
|
|
31
|
+
* `main`, `bin["deadset-ts"]`, `exports["."]["import"]`. It is what a maintainer
|
|
32
|
+
* edits, so it is what a root reports as the string that named it.
|
|
33
|
+
*/
|
|
34
|
+
readonly member: string;
|
|
35
|
+
/** The target the member carries, resolved against the manifest's directory. */
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly role: EntryRole;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the member declares what a consumer may import, which is `exports` and
|
|
40
|
+
* the three members that preceded it. A command is published too, and is not this:
|
|
41
|
+
* `exports` is the surface a library's published API is read from.
|
|
42
|
+
*/
|
|
43
|
+
readonly published: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether one member's value names a file of this package rather than another
|
|
52
|
+
* package or a URL. A target written with no leading dot is a path too, which is
|
|
53
|
+
* how the members that preceded `exports` are written.
|
|
54
|
+
*/
|
|
55
|
+
function isTarget(value: string, requireDot: boolean): boolean {
|
|
56
|
+
if (value === "" || isAbsolutePath(value) || value.includes("://")) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return !requireDot || value.startsWith("./");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One member's spelling below another: `exports` then `exports["."]`. */
|
|
63
|
+
function below(member: string, key: string): string {
|
|
64
|
+
return `${member}[${JSON.stringify(key)}]`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every target one `exports` value carries, at the member each is written under.
|
|
69
|
+
*
|
|
70
|
+
* The value is a string, an object whose keys are subpaths or conditions, or an
|
|
71
|
+
* array of alternatives, nested to any depth, and a `null` blocks a subpath rather
|
|
72
|
+
* than naming a target. Every level is walked because a condition names a different
|
|
73
|
+
* file of the same package and each is an entry point of its own; a key's meaning
|
|
74
|
+
* is not read, because a subpath and a condition both lead to a target and the
|
|
75
|
+
* walk needs no more than that.
|
|
76
|
+
*/
|
|
77
|
+
function exportTargets(value: unknown, member: string, found: ManifestEntry[]): void {
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
if (isTarget(value, true)) {
|
|
80
|
+
found.push({ member, path: value, role: "import", published: true });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
value.forEach((alternative, index) => {
|
|
86
|
+
exportTargets(alternative, below(member, String(index)), found);
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!isRecord(value)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const [key, held] of Object.entries(value)) {
|
|
94
|
+
exportTargets(held, below(member, key), found);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The manifest at the target root, decoded, or undefined where there is none to read. */
|
|
99
|
+
function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | undefined {
|
|
100
|
+
let text: string;
|
|
101
|
+
try {
|
|
102
|
+
text = host.readFile(joinPath(targetRoot, MANIFEST));
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
let value: unknown;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
// A manifest this program cannot read names no entry point, which is the same
|
|
111
|
+
// answer as an absent one. What a manifest's contents say is not refused here:
|
|
112
|
+
// the package manager owns that refusal, and an analysis of a tree it rejects
|
|
113
|
+
// is not the run's subject.
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return isRecord(value) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** What the target's own manifest declares about where the package is entered. */
|
|
120
|
+
export interface Manifest {
|
|
121
|
+
/**
|
|
122
|
+
* Every entry point the manifest names, in the order the members are read: `main`,
|
|
123
|
+
* `module`, `types`, then each `bin` command, then every target of `exports`. Each
|
|
124
|
+
* path is absolute.
|
|
125
|
+
*/
|
|
126
|
+
readonly entries: readonly ManifestEntry[];
|
|
127
|
+
/**
|
|
128
|
+
* Whether an `exports` member is written at all. A package that declares one has
|
|
129
|
+
* said exactly what a consumer may import, so its published API is what `exports`
|
|
130
|
+
* reaches; a package that declares none publishes whatever the members that
|
|
131
|
+
* preceded it name.
|
|
132
|
+
*/
|
|
133
|
+
readonly declaresExports: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What the target's own manifest declares about where the package is entered.
|
|
138
|
+
*
|
|
139
|
+
* A target is taken as written. It is not resolved to a source file here, because
|
|
140
|
+
* which file a target names is a fact about the compiler configuration that emits
|
|
141
|
+
* it and there is one such configuration per project, while there is one manifest.
|
|
142
|
+
*/
|
|
143
|
+
export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
144
|
+
const manifest = manifestOf(host, targetRoot);
|
|
145
|
+
if (manifest === undefined) {
|
|
146
|
+
return { entries: [], declaresExports: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const found: ManifestEntry[] = [];
|
|
150
|
+
for (const member of LEGACY_MEMBERS) {
|
|
151
|
+
const value = manifest[member];
|
|
152
|
+
if (typeof value === "string" && isTarget(value, false)) {
|
|
153
|
+
found.push({ member, path: value, role: "import", published: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const bin = manifest["bin"];
|
|
158
|
+
if (typeof bin === "string" && isTarget(bin, false)) {
|
|
159
|
+
found.push({ member: "bin", path: bin, role: "run", published: false });
|
|
160
|
+
} else if (isRecord(bin)) {
|
|
161
|
+
for (const [command, value] of Object.entries(bin)) {
|
|
162
|
+
if (typeof value === "string" && isTarget(value, false)) {
|
|
163
|
+
found.push({ member: below("bin", command), path: value, role: "run", published: false });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
exportTargets(manifest["exports"], "exports", found);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
|
|
172
|
+
declaresExports: manifest["exports"] !== undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
type Node,
|
|
19
19
|
type SourceFile,
|
|
20
20
|
} from "@typescript/native/unstable/ast";
|
|
21
|
-
import type
|
|
21
|
+
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
22
|
+
import { globExpression } from "./glob.ts";
|
|
22
23
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
23
24
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
24
25
|
import type { ProjectView } from "./session.ts";
|
|
@@ -159,22 +160,6 @@ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
|
159
160
|
SyntaxKind.CaretEqualsToken,
|
|
160
161
|
]);
|
|
161
162
|
|
|
162
|
-
/**
|
|
163
|
-
* The declaration forms that name something declared elsewhere. Each is a link rather
|
|
164
|
-
* than a declaration of what it names, so a reference through one names the next link
|
|
165
|
-
* and, at the end of the chain, the declaration itself: a re-export something imports
|
|
166
|
-
* is used, and so is the declaration behind it.
|
|
167
|
-
*/
|
|
168
|
-
const ALIASES: ReadonlySet<SyntaxKind> = new Set([
|
|
169
|
-
SyntaxKind.ImportClause,
|
|
170
|
-
SyntaxKind.ImportEqualsDeclaration,
|
|
171
|
-
SyntaxKind.ImportSpecifier,
|
|
172
|
-
SyntaxKind.NamespaceImport,
|
|
173
|
-
SyntaxKind.ExportAssignment,
|
|
174
|
-
SyntaxKind.ExportSpecifier,
|
|
175
|
-
SyntaxKind.NamespaceExport,
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
163
|
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
179
164
|
function isName(node: Node): boolean {
|
|
180
165
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
@@ -354,64 +339,6 @@ function sitesOf(
|
|
|
354
339
|
return found;
|
|
355
340
|
}
|
|
356
341
|
|
|
357
|
-
/**
|
|
358
|
-
* One literal run of a pattern, with every character the expression syntax claims
|
|
359
|
-
* escaped.
|
|
360
|
-
*
|
|
361
|
-
* The set is the syntax characters and the solidus, and it is closed: an escape of any
|
|
362
|
-
* other character is refused outright by the expression syntax under the unicode flag,
|
|
363
|
-
* so escaping a hyphen, which needs none outside a character class, would refuse every
|
|
364
|
-
* pattern that names a hyphenated path.
|
|
365
|
-
*/
|
|
366
|
-
function quoted(text: string): string {
|
|
367
|
-
return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
|
|
372
|
-
* do not, a brace list is an alternation of its members, and every other character
|
|
373
|
-
* stands for itself.
|
|
374
|
-
*/
|
|
375
|
-
function expressionOf(pattern: string): RegExp {
|
|
376
|
-
let source = "^";
|
|
377
|
-
let at = 0;
|
|
378
|
-
while (at < pattern.length) {
|
|
379
|
-
const rest = pattern.slice(at);
|
|
380
|
-
if (rest.startsWith("**/")) {
|
|
381
|
-
source += "(?:[^/]*/)*";
|
|
382
|
-
at += 3;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (rest.startsWith("**")) {
|
|
386
|
-
source += ".*";
|
|
387
|
-
at += 2;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
const character = pattern[at] ?? "";
|
|
391
|
-
at += 1;
|
|
392
|
-
if (character === "*") {
|
|
393
|
-
source += "[^/]*";
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (character === "?") {
|
|
397
|
-
source += "[^/]";
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
if (character === "{") {
|
|
401
|
-
const close = pattern.indexOf("}", at);
|
|
402
|
-
if (close < 0) {
|
|
403
|
-
source += "\\{";
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
|
|
407
|
-
at = close + 1;
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
source += quoted(character);
|
|
411
|
-
}
|
|
412
|
-
return new RegExp(`${source}$`, "u");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
342
|
/**
|
|
416
343
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
417
344
|
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
@@ -456,7 +383,7 @@ export function references<Brand>(
|
|
|
456
383
|
): References {
|
|
457
384
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
458
385
|
const declarations = held.declarations;
|
|
459
|
-
const patterns = options.testFiles.map(
|
|
386
|
+
const patterns = options.testFiles.map(globExpression);
|
|
460
387
|
const matched = options.testFiles.map(() => 0);
|
|
461
388
|
const found: Reference[] = [];
|
|
462
389
|
const reached = new Map<number, readonly Target[]>();
|
|
@@ -479,11 +406,17 @@ export function references<Brand>(
|
|
|
479
406
|
* is the declaration behind it, so both are named; a link this project does not
|
|
480
407
|
* declare is stepped over rather than recorded.
|
|
481
408
|
*
|
|
482
|
-
* A
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
409
|
+
* A symbol is asked to step only where the binder flagged it an alias, which is the
|
|
410
|
+
* same condition the step itself asserts on. The declaration's syntax is not that
|
|
411
|
+
* condition and does not stand in for it: a default export of an expression is
|
|
412
|
+
* written as one of the alias forms and names nothing declared elsewhere, so it is
|
|
413
|
+
* recorded as the declaration it is and the chain ends there.
|
|
414
|
+
*
|
|
415
|
+
* A declaration handle carries the file it is in, so the question the walk asks of a
|
|
416
|
+
* declaration is answered before it is resolved: a declaration outside the project's
|
|
417
|
+
* own files belongs to another program and is never read, which is what keeps the
|
|
418
|
+
* pass from fetching a library file to discover that the inventory does not hold what
|
|
419
|
+
* is in it.
|
|
487
420
|
*/
|
|
488
421
|
const targetsOf = (symbol: TSSymbol): readonly Target[] => {
|
|
489
422
|
const cached = reached.get(symbol.id);
|
|
@@ -496,9 +429,8 @@ export function references<Brand>(
|
|
|
496
429
|
let stepped = false;
|
|
497
430
|
while (at !== undefined && !walked.has(at.id)) {
|
|
498
431
|
walked.add(at.id);
|
|
499
|
-
|
|
432
|
+
const alias = (at.flags & SymbolFlags.Alias) !== 0;
|
|
500
433
|
for (const handle of at.declarations) {
|
|
501
|
-
alias = alias || ALIASES.has(handle.kind);
|
|
502
434
|
if (!ownFiles.has(handle.path)) {
|
|
503
435
|
continue;
|
|
504
436
|
}
|