@cplieger/deadset-ts 1.2.0 → 2.1.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 +44 -5
- package/{bin/deadset-ts.ts → dist/bin/deadset-ts.js} +2 -3
- package/dist/bin/node-host.js +74 -0
- package/dist/src/config.js +63 -0
- package/dist/src/discover.js +171 -0
- package/dist/src/host.js +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/inventory.js +693 -0
- package/dist/src/kinds.js +12 -0
- package/dist/src/paths.js +86 -0
- package/dist/src/position.js +52 -0
- package/dist/src/print.js +56 -0
- package/dist/src/ref.js +117 -0
- package/dist/src/references.js +420 -0
- package/dist/src/resolve.js +482 -0
- package/dist/src/run.js +249 -0
- package/dist/src/schema.js +335 -0
- package/dist/src/scope.js +116 -0
- package/dist/src/session.js +101 -0
- package/dist/src/version.js +7 -0
- package/jsr.json +1 -1
- package/package.json +9 -4
- package/src/host.ts +10 -2
- package/src/index.ts +13 -1
- package/src/inventory.ts +53 -8
- package/src/references.ts +606 -0
- package/src/run.ts +4 -4
- package/src/session.ts +41 -22
- package/src/version.ts +3 -18
- package/bin/deadset-ts.mjs +0 -24
- package/bin/node-host.ts +0 -28
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { API } from "@typescript/native/unstable/sync";
|
|
2
|
+
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
|
+
/**
|
|
4
|
+
* Opens the compiler client the analysis reads through.
|
|
5
|
+
*
|
|
6
|
+
* Closing twice releases once: a run that fails before its snapshot opens closes
|
|
7
|
+
* the client where it failed, and the outer release runs whatever happened, so the
|
|
8
|
+
* second call has nothing left to do.
|
|
9
|
+
*/
|
|
10
|
+
export function openEngine(options) {
|
|
11
|
+
const api = new API({ collectTiming: options.collectTiming });
|
|
12
|
+
let closed = false;
|
|
13
|
+
return {
|
|
14
|
+
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
15
|
+
updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
|
|
16
|
+
getTimingInfo: () => api.getTimingInfo(),
|
|
17
|
+
close: () => {
|
|
18
|
+
if (closed) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
closed = true;
|
|
22
|
+
api.close();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function viewOf(project) {
|
|
27
|
+
// The snapshot is never updated, so a project's own file set is the same answer
|
|
28
|
+
// every time it is asked for. It is read once because the question is not free:
|
|
29
|
+
// deciding it consults the metadata of every file the program holds, the compiler's
|
|
30
|
+
// own library files included, and a run asks for the set once per pass.
|
|
31
|
+
let ownFiles;
|
|
32
|
+
return {
|
|
33
|
+
configFile: project.configFileName,
|
|
34
|
+
program: project.program,
|
|
35
|
+
checker: project.checker,
|
|
36
|
+
ownSourceFiles: () => {
|
|
37
|
+
ownFiles ??= project.program
|
|
38
|
+
.getSourceFileNames()
|
|
39
|
+
// The metadata decides which files belong to the target, and it is read by
|
|
40
|
+
// name. Fetching each file first and filtering afterwards costs one round
|
|
41
|
+
// trip per library file the answer then discards, and a program holds far
|
|
42
|
+
// more library files than target files.
|
|
43
|
+
.filter((name) => {
|
|
44
|
+
const metadata = project.program.getSourceFileMetadata(name);
|
|
45
|
+
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
46
|
+
})
|
|
47
|
+
.map((name) => project.program.getSourceFile(name))
|
|
48
|
+
.filter((file) => file !== undefined);
|
|
49
|
+
return ownFiles;
|
|
50
|
+
},
|
|
51
|
+
handle: (node) => ({ node }),
|
|
52
|
+
symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
|
|
53
|
+
resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
|
|
54
|
+
shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
|
|
55
|
+
aliasStepOf: (symbol) => {
|
|
56
|
+
const held = project.checker.getImmediateAliasedSymbol(symbol);
|
|
57
|
+
// An alias that resolves to nothing answers with the checker's own unknown
|
|
58
|
+
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
59
|
+
// nothing at all.
|
|
60
|
+
return held === undefined || held.declarations.length === 0 ? undefined : held;
|
|
61
|
+
},
|
|
62
|
+
declarationAt: (handle) => {
|
|
63
|
+
const node = handle.resolve(project);
|
|
64
|
+
return node === undefined ? undefined : { node };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The three diagnostic sets one project carries, read before any analysis. */
|
|
69
|
+
export function diagnosticsOf(project) {
|
|
70
|
+
return {
|
|
71
|
+
configFile: project.configFile,
|
|
72
|
+
syntactic: project.program.getSyntacticDiagnostics(),
|
|
73
|
+
semantic: project.program.getSemanticDiagnostics(),
|
|
74
|
+
configParsing: project.program.getConfigFileParsingDiagnostics(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Runs `visit` over every project the compiler configurations name, inside one
|
|
79
|
+
* snapshot of one client.
|
|
80
|
+
*
|
|
81
|
+
* The snapshot is opened once with every configuration, so each becomes a project
|
|
82
|
+
* with its own program and checker; it is never updated, because the analysis
|
|
83
|
+
* reads and the tree does not change under it. It is disposed once when the last
|
|
84
|
+
* project has been visited, and the client is released after it, whether the visit
|
|
85
|
+
* completed or threw.
|
|
86
|
+
*/
|
|
87
|
+
export function runSession(engine, configFiles, visit) {
|
|
88
|
+
try {
|
|
89
|
+
const snapshot = engine.updateSnapshot(configFiles);
|
|
90
|
+
try {
|
|
91
|
+
const projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
|
|
92
|
+
return { projects, timing: engine.getTimingInfo() };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
snapshot.dispose();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
engine.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Contract version this analyzer implements. It is the resolved value of
|
|
3
|
+
* `contract_version` when no configuration source supplies one, and it moves
|
|
4
|
+
* independently of the analyzer's own version, which is the one the host answers
|
|
5
|
+
* from the package the analyzer was installed from.
|
|
6
|
+
*/
|
|
7
|
+
export const CONTRACT_VERSION = "2.0.0";
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
7
7
|
"license": "GPL-3.0-or-later",
|
|
8
8
|
"bin": {
|
|
9
|
-
"deadset-ts": "./bin/deadset-ts.
|
|
9
|
+
"deadset-ts": "./dist/bin/deadset-ts.js"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
|
-
".":
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./dist/src/index.js"
|
|
15
|
+
}
|
|
13
16
|
},
|
|
14
17
|
"engines": {
|
|
15
18
|
"node": ">=24"
|
|
16
19
|
},
|
|
17
20
|
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"prepack": "npm run build",
|
|
18
23
|
"typecheck": "tsc -p tsconfig.json",
|
|
19
24
|
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
20
25
|
"test": "vitest --run",
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
"files": [
|
|
59
64
|
"LICENSE",
|
|
60
65
|
"NOTICE",
|
|
61
|
-
"
|
|
66
|
+
"dist/",
|
|
62
67
|
"src/",
|
|
63
68
|
"!src/**/*.test.ts",
|
|
64
69
|
"jsr.json"
|
package/src/host.ts
CHANGED
|
@@ -8,8 +8,9 @@ export interface DirectoryEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* The
|
|
12
|
-
* against
|
|
11
|
+
* The platform one run reads: the filesystem, the directory relative paths
|
|
12
|
+
* resolve against, and the version of the package this analyzer was installed
|
|
13
|
+
* from.
|
|
13
14
|
*
|
|
14
15
|
* It is a parameter rather than an import because a published module that reads
|
|
15
16
|
* the filesystem directly carries the whole platform's type surface into every
|
|
@@ -26,4 +27,11 @@ export interface Host {
|
|
|
26
27
|
readDirectory(path: string): readonly DirectoryEntry[];
|
|
27
28
|
/** What one path names, `"absent"` where nothing is there. */
|
|
28
29
|
kindOf(path: string): PathKind;
|
|
30
|
+
/**
|
|
31
|
+
* The version of the package this analyzer was installed from, or a throw
|
|
32
|
+
* naming why it cannot be known. It is a method rather than a value because
|
|
33
|
+
* locating that package is a filesystem walk, and a host that cannot do it
|
|
34
|
+
* says so at the one moment the value is asked for.
|
|
35
|
+
*/
|
|
36
|
+
analyzerVersion(): string;
|
|
29
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,24 @@ export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
|
|
|
2
2
|
export type { DirectoryEntry, Host, PathKind } from "./host.ts";
|
|
3
3
|
export {
|
|
4
4
|
inventory,
|
|
5
|
+
nodeKey,
|
|
5
6
|
type Inventory,
|
|
6
7
|
type InventoryCost,
|
|
7
8
|
type InventorySymbol,
|
|
8
9
|
type SymbolKind,
|
|
9
10
|
type Visibility,
|
|
10
11
|
} from "./inventory.ts";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_BATCH_CAP,
|
|
14
|
+
references,
|
|
15
|
+
type Reference,
|
|
16
|
+
type ReferenceCost,
|
|
17
|
+
type ReferenceOptions,
|
|
18
|
+
type References,
|
|
19
|
+
type Resolution,
|
|
20
|
+
type TestFileRule,
|
|
21
|
+
type Use,
|
|
22
|
+
} from "./references.ts";
|
|
11
23
|
export {
|
|
12
24
|
byPosition,
|
|
13
25
|
positionKey,
|
|
@@ -26,4 +38,4 @@ export {
|
|
|
26
38
|
type Fragment,
|
|
27
39
|
type Module,
|
|
28
40
|
} from "./ref.ts";
|
|
29
|
-
export { CONTRACT_VERSION
|
|
41
|
+
export { CONTRACT_VERSION } from "./version.ts";
|
package/src/inventory.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isModuleBlock,
|
|
16
16
|
isModuleDeclaration,
|
|
17
17
|
isNamespaceExport,
|
|
18
|
+
isParenthesizedTypeNode,
|
|
18
19
|
isPrivateIdentifier,
|
|
19
20
|
isPropertyDeclaration,
|
|
20
21
|
isPropertySignatureDeclaration,
|
|
@@ -133,6 +134,17 @@ export interface Inventory {
|
|
|
133
134
|
/** The compiler configuration the project was opened from. */
|
|
134
135
|
readonly configFile: string;
|
|
135
136
|
readonly symbols: readonly InventorySymbol[];
|
|
137
|
+
/**
|
|
138
|
+
* Each declaring node's {@link nodeKey} mapped to the identifier of the declaration
|
|
139
|
+
* enumerated there. A reading that starts from a node of the same program, rather
|
|
140
|
+
* than from a position, resolves the declaration through this map instead of
|
|
141
|
+
* rendering the position a second time, so the rule that decides a declaration's
|
|
142
|
+
* identifier has one owner.
|
|
143
|
+
*
|
|
144
|
+
* A getter and a setter of one name are one declaration and two nodes, so both nodes
|
|
145
|
+
* name it.
|
|
146
|
+
*/
|
|
147
|
+
readonly declarations: ReadonlyMap<string, string>;
|
|
136
148
|
readonly cost: InventoryCost;
|
|
137
149
|
/**
|
|
138
150
|
* The module specifiers of the bare star re-exports the project's files carry, in
|
|
@@ -316,6 +328,31 @@ function memberKey(component: Component, isStatic: boolean): string {
|
|
|
316
328
|
return `${component.computed ? "[" : ""}${component.text}${isStatic ? ":static" : ""}`;
|
|
317
329
|
}
|
|
318
330
|
|
|
331
|
+
/** The type one node is, through however many parentheses are written around it. */
|
|
332
|
+
function unparenthesizedType(type: Node): Node {
|
|
333
|
+
let held = type;
|
|
334
|
+
while (isParenthesizedTypeNode(held)) {
|
|
335
|
+
held = held.type;
|
|
336
|
+
}
|
|
337
|
+
return held;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The direct constituents of one type. Parentheses are syntax and are not a
|
|
342
|
+
* constituent, so a parenthesized type contributes the constituents it holds: the
|
|
343
|
+
* constituents of `(A | B) & C` are `A`, `B` and `C`. A union or an intersection
|
|
344
|
+
* written without them stays one constituent.
|
|
345
|
+
*/
|
|
346
|
+
function constituentsOf(type: Node): readonly Node[] {
|
|
347
|
+
const held = unparenthesizedType(type);
|
|
348
|
+
if (!isUnionTypeNode(held) && !isIntersectionTypeNode(held)) {
|
|
349
|
+
return [held];
|
|
350
|
+
}
|
|
351
|
+
return held.types.flatMap((member) =>
|
|
352
|
+
isParenthesizedTypeNode(member) ? constituentsOf(member) : [member],
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
319
356
|
/**
|
|
320
357
|
* The members one type alias declares: the members of the object type it writes, and
|
|
321
358
|
* of every object type that is a direct constituent of a union or an intersection it
|
|
@@ -327,11 +364,7 @@ function memberKey(component: Component, isStatic: boolean): string {
|
|
|
327
364
|
* neither of them is the one a reference to it would name.
|
|
328
365
|
*/
|
|
329
366
|
function aliasMembers(type: Node): readonly Node[] {
|
|
330
|
-
const literals =
|
|
331
|
-
? [type]
|
|
332
|
-
: isUnionTypeNode(type) || isIntersectionTypeNode(type)
|
|
333
|
-
? type.types.filter(isTypeLiteralNode)
|
|
334
|
-
: [];
|
|
367
|
+
const literals = constituentsOf(type).filter(isTypeLiteralNode);
|
|
335
368
|
if (literals.length < 2) {
|
|
336
369
|
return literals[0]?.members ?? [];
|
|
337
370
|
}
|
|
@@ -407,8 +440,15 @@ function nameNodeOf(node: Node): Node | undefined {
|
|
|
407
440
|
return (node as { readonly name?: Node }).name;
|
|
408
441
|
}
|
|
409
442
|
|
|
410
|
-
/**
|
|
411
|
-
|
|
443
|
+
/**
|
|
444
|
+
* One node's identity inside one program, which is how two readings of that program
|
|
445
|
+
* name one declaration.
|
|
446
|
+
*
|
|
447
|
+
* The two mechanisms behind the member set resolve a member separately and meet here;
|
|
448
|
+
* so does the reference pass, which starts from a resolved symbol's declaration and
|
|
449
|
+
* needs the declaration this enumeration made for it.
|
|
450
|
+
*/
|
|
451
|
+
export function nodeKey(file: SourceFile, node: Node): string {
|
|
412
452
|
return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
|
|
413
453
|
}
|
|
414
454
|
|
|
@@ -828,8 +868,10 @@ export function inventory<Brand>(
|
|
|
828
868
|
const endLine = renderPosition(file, targetRoot, Math.max(node.end - 1, 0)).line;
|
|
829
869
|
if (held !== undefined) {
|
|
830
870
|
// A getter and a setter of one name are one symbol and one reference, so the
|
|
831
|
-
// second declaration widens the first record rather than making a second.
|
|
871
|
+
// second declaration widens the first record rather than making a second. Both
|
|
872
|
+
// nodes name that record, so a reading that starts from either one finds it.
|
|
832
873
|
held.endLine = Math.max(held.endLine, endLine);
|
|
874
|
+
byNode.set(nodeKey(file, node), held);
|
|
833
875
|
return;
|
|
834
876
|
}
|
|
835
877
|
const at = name ?? node;
|
|
@@ -884,6 +926,9 @@ export function inventory<Brand>(
|
|
|
884
926
|
return {
|
|
885
927
|
configFile: project.configFile,
|
|
886
928
|
symbols,
|
|
929
|
+
declarations: new Map(
|
|
930
|
+
[...byNode].map(([key, record]) => [key, positionKey(record.position)] as const),
|
|
931
|
+
),
|
|
887
932
|
cost: { batches: 1, exportTables, memberTables },
|
|
888
933
|
starReExports,
|
|
889
934
|
outsideOwnFiles,
|