@cplieger/deadset-ts 1.1.0 → 1.2.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 +4 -2
- package/bin/deadset-ts.mjs +24 -0
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/index.ts +26 -0
- package/src/inventory.ts +941 -0
- package/src/paths.ts +20 -0
- package/src/position.ts +75 -0
- package/src/ref.ts +179 -0
- package/src/session.ts +16 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 1.5.0 and ships the analyzer's foundation: one compiler session per run over every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, and the report-only guard. `analyze` and `explain` are not implemented yet and exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
11
|
+
**Status: pre-release.** This version implements contract 1.5.0 and ships the analyzer's foundation: one compiler session per run over every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, and the report-only guard. `analyze` and `explain` are not implemented yet and exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -16,9 +16,11 @@ deadset-ts is report-only. It never edits source, and it refuses any `--fix` fla
|
|
|
16
16
|
npm i -D @cplieger/deadset-ts
|
|
17
17
|
# or
|
|
18
18
|
npx jsr add -D @cplieger/deadset-ts
|
|
19
|
+
# or run the command without installing it
|
|
20
|
+
npx @cplieger/deadset-ts version
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
The package ships TypeScript source, not compiled JavaScript
|
|
23
|
+
The package ships TypeScript source, not compiled JavaScript, and there is no build step on either side. The `deadset-ts` command runs on plain Node.js 24 or later; its entry strips the types of the package's own source itself, because Node refuses to do that for a file under `node_modules`. The exported API is source too, so a consumer compiles it with its own TypeScript, or imports it under Deno from the JSR package; plain Node cannot import it.
|
|
22
24
|
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node refuses to strip types from a .ts file under node_modules, so the
|
|
3
|
+
// TypeScript entry cannot start from an install. A load hook scoped to this
|
|
4
|
+
// package's own files supplies the stripped source instead.
|
|
5
|
+
// https://nodejs.org/api/typescript.html#type-stripping-in-dependencies
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { registerHooks, stripTypeScriptTypes } from "node:module";
|
|
8
|
+
|
|
9
|
+
const packageRoot = new URL("../", import.meta.url).href;
|
|
10
|
+
|
|
11
|
+
registerHooks({
|
|
12
|
+
load(url, context, nextLoad) {
|
|
13
|
+
if (!url.startsWith(packageRoot) || !url.endsWith(".ts")) {
|
|
14
|
+
return nextLoad(url, context);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
format: "module",
|
|
18
|
+
source: stripTypeScriptTypes(readFileSync(new URL(url), "utf8")),
|
|
19
|
+
shortCircuit: true,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await import("./deadset-ts.ts");
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": "./bin/deadset-ts.mjs"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./src/index.ts"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@eslint/js": "10.0.1",
|
|
30
30
|
"@types/node": "24.13.5",
|
|
31
31
|
"eslint": "10.10.0",
|
|
32
|
+
"fast-check": "4.10.1",
|
|
32
33
|
"prettier": "3.9.7",
|
|
33
34
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
34
35
|
"typescript-eslint": "8.70.0",
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
|
|
2
2
|
export type { DirectoryEntry, Host, PathKind } from "./host.ts";
|
|
3
|
+
export {
|
|
4
|
+
inventory,
|
|
5
|
+
type Inventory,
|
|
6
|
+
type InventoryCost,
|
|
7
|
+
type InventorySymbol,
|
|
8
|
+
type SymbolKind,
|
|
9
|
+
type Visibility,
|
|
10
|
+
} from "./inventory.ts";
|
|
11
|
+
export {
|
|
12
|
+
byPosition,
|
|
13
|
+
positionKey,
|
|
14
|
+
PositionError,
|
|
15
|
+
renderPosition,
|
|
16
|
+
type Position,
|
|
17
|
+
} from "./position.ts";
|
|
18
|
+
export {
|
|
19
|
+
computedComponent,
|
|
20
|
+
isRef,
|
|
21
|
+
nameComponent,
|
|
22
|
+
REF_EXPRESSIONS,
|
|
23
|
+
renderRef,
|
|
24
|
+
type Component,
|
|
25
|
+
type DependencySection,
|
|
26
|
+
type Fragment,
|
|
27
|
+
type Module,
|
|
28
|
+
} from "./ref.ts";
|
|
3
29
|
export { CONTRACT_VERSION, version } from "./version.ts";
|
package/src/inventory.ts
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isClassDeclaration,
|
|
3
|
+
isComputedPropertyName,
|
|
4
|
+
isEnumDeclaration,
|
|
5
|
+
isEnumMember,
|
|
6
|
+
isExportAssignment,
|
|
7
|
+
isExportDeclaration,
|
|
8
|
+
isFunctionDeclaration,
|
|
9
|
+
isGetAccessorDeclaration,
|
|
10
|
+
isIdentifier,
|
|
11
|
+
isInterfaceDeclaration,
|
|
12
|
+
isIntersectionTypeNode,
|
|
13
|
+
isMethodDeclaration,
|
|
14
|
+
isMethodSignatureDeclaration,
|
|
15
|
+
isModuleBlock,
|
|
16
|
+
isModuleDeclaration,
|
|
17
|
+
isNamespaceExport,
|
|
18
|
+
isPrivateIdentifier,
|
|
19
|
+
isPropertyDeclaration,
|
|
20
|
+
isPropertySignatureDeclaration,
|
|
21
|
+
isSetAccessorDeclaration,
|
|
22
|
+
isTypeAliasDeclaration,
|
|
23
|
+
isTypeLiteralNode,
|
|
24
|
+
isUnionTypeNode,
|
|
25
|
+
isVariableStatement,
|
|
26
|
+
ModifierFlags,
|
|
27
|
+
type Node,
|
|
28
|
+
type SourceFile,
|
|
29
|
+
} from "@typescript/native/unstable/ast";
|
|
30
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
31
|
+
import type { Host } from "./host.ts";
|
|
32
|
+
import { dirnamePath, joinPath, relativePath } from "./paths.ts";
|
|
33
|
+
import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
|
|
34
|
+
import { computedComponent, nameComponent, renderRef, type Component, type Module } from "./ref.ts";
|
|
35
|
+
import type { Handle, ProjectView } from "./session.ts";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The symbol inventory of one project: every declaration the target's own files
|
|
39
|
+
* hold, at its own position, with the container it belongs to and the reference
|
|
40
|
+
* that survives an edit above it.
|
|
41
|
+
*
|
|
42
|
+
* Two mechanisms produce it and neither is redundant. A container's symbol table is
|
|
43
|
+
* the binder's own answer, so it holds a member of a declaration that merges with
|
|
44
|
+
* another and it holds a member whatever its visibility, a private name included;
|
|
45
|
+
* a container's syntactic member list holds a member whose key is computed, which
|
|
46
|
+
* the binder resolves later and does not put in the table. The set is the union,
|
|
47
|
+
* keyed by the member's own reference component, so a member both sources name
|
|
48
|
+
* appears once.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What one declaration is, from the vocabulary the Contract's finding schema
|
|
53
|
+
* closes. A Go rendering of the same vocabulary names other members of it; these
|
|
54
|
+
* are the ones a TypeScript declaration takes.
|
|
55
|
+
*/
|
|
56
|
+
export type SymbolKind =
|
|
57
|
+
| "file"
|
|
58
|
+
| "function"
|
|
59
|
+
| "class"
|
|
60
|
+
| "interface"
|
|
61
|
+
| "type"
|
|
62
|
+
| "enum"
|
|
63
|
+
| "namespace"
|
|
64
|
+
| "variable"
|
|
65
|
+
| "export-alias"
|
|
66
|
+
| "method"
|
|
67
|
+
| "class-member"
|
|
68
|
+
| "interface-method"
|
|
69
|
+
| "type-member"
|
|
70
|
+
| "enum-member"
|
|
71
|
+
| "type-parameter";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* How a member may be named from outside the container that declares it.
|
|
75
|
+
*
|
|
76
|
+
* The last two are two facts rather than one. A `private` modifier is a
|
|
77
|
+
* compile-time constraint only, so the member is still reachable at run time by its
|
|
78
|
+
* name through a string index, a decorator, a container or a serializer; a private
|
|
79
|
+
* name is not reachable by any of them, because the name does not exist outside the
|
|
80
|
+
* class body. An exemption class that finds its evidence in a name therefore
|
|
81
|
+
* applies to the first and never to the second, and the Contract's exemption
|
|
82
|
+
* vocabulary records that per class.
|
|
83
|
+
*/
|
|
84
|
+
export type Visibility = "public" | "protected" | "private" | "private-name";
|
|
85
|
+
|
|
86
|
+
/** One declaration of the target. */
|
|
87
|
+
export interface InventorySymbol {
|
|
88
|
+
/** The identifier of the declaration's own position, which the graph keys on. */
|
|
89
|
+
readonly id: string;
|
|
90
|
+
/** The stable symbol reference, which survives an edit above the declaration. */
|
|
91
|
+
readonly ref: string;
|
|
92
|
+
/**
|
|
93
|
+
* The display name a text line renders: the chain from the container inward, each
|
|
94
|
+
* component in the spelling a reference gives it, and the declared local name of a
|
|
95
|
+
* default export, whose reference carries its export name instead.
|
|
96
|
+
*/
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly kind: SymbolKind;
|
|
99
|
+
readonly position: Position;
|
|
100
|
+
/** The line the declaration ends on, so its size in source lines is readable. */
|
|
101
|
+
readonly endLine: number;
|
|
102
|
+
/** The container's id: the file for a module-level declaration, empty for a file. */
|
|
103
|
+
readonly parent: string;
|
|
104
|
+
/**
|
|
105
|
+
* Whether the export table of the module or namespace that holds the declaration
|
|
106
|
+
* names it. It is a fact about that one table, so it is false for every member of
|
|
107
|
+
* a class, an interface, a type or an enum and for every type parameter, whose
|
|
108
|
+
* reach outside its container is {@link InventorySymbol.visibility} and the state
|
|
109
|
+
* of the container the parent names.
|
|
110
|
+
*/
|
|
111
|
+
readonly exported: boolean;
|
|
112
|
+
readonly visibility: Visibility;
|
|
113
|
+
/** Whether the declaration is a static member of a class. */
|
|
114
|
+
readonly static: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What one project's inventory cost at the client boundary. */
|
|
118
|
+
export interface InventoryCost {
|
|
119
|
+
/** Batched symbol lookups. One pass over every module and container of the project. */
|
|
120
|
+
readonly batches: number;
|
|
121
|
+
/** Export-table reads: one per module and one per namespace. */
|
|
122
|
+
readonly exportTables: number;
|
|
123
|
+
/**
|
|
124
|
+
* Member-table reads: one per interface and enum, and two per class, because a
|
|
125
|
+
* class keeps its instance members and its static members in two tables. Never
|
|
126
|
+
* one per member.
|
|
127
|
+
*/
|
|
128
|
+
readonly memberTables: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One project's declarations, in position order, and what reading them cost. */
|
|
132
|
+
export interface Inventory {
|
|
133
|
+
/** The compiler configuration the project was opened from. */
|
|
134
|
+
readonly configFile: string;
|
|
135
|
+
readonly symbols: readonly InventorySymbol[];
|
|
136
|
+
readonly cost: InventoryCost;
|
|
137
|
+
/**
|
|
138
|
+
* The module specifiers of the bare star re-exports the project's files carry, in
|
|
139
|
+
* the order they were read. A bare star re-export declares nothing here: the names
|
|
140
|
+
* it carries forward are the declarations of the module it names, so it is a
|
|
141
|
+
* reference to that module and not a declaration of this one.
|
|
142
|
+
*/
|
|
143
|
+
readonly starReExports: readonly string[];
|
|
144
|
+
/**
|
|
145
|
+
* The number of member declarations that were left out because the file declaring
|
|
146
|
+
* them is not one of the target's own files. Such a declaration belongs to another
|
|
147
|
+
* program, so the target's inventory does not name it, and the count is reported so
|
|
148
|
+
* that the omission is a number rather than a silence.
|
|
149
|
+
*/
|
|
150
|
+
readonly outsideOwnFiles: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A declaration under construction, before its reference is rendered. */
|
|
154
|
+
interface Building {
|
|
155
|
+
readonly file: SourceFile;
|
|
156
|
+
readonly node: Node;
|
|
157
|
+
readonly module: Module;
|
|
158
|
+
readonly position: Position;
|
|
159
|
+
endLine: number;
|
|
160
|
+
readonly kind: SymbolKind;
|
|
161
|
+
/** The chain of the container, without this declaration's own component. */
|
|
162
|
+
readonly parentChain: readonly Component[];
|
|
163
|
+
/**
|
|
164
|
+
* This declaration's own component. A default export's is its export name, which
|
|
165
|
+
* is where the component and the declared name part company.
|
|
166
|
+
*/
|
|
167
|
+
readonly component: Component;
|
|
168
|
+
/** The declared name, where it is not what the component spells. */
|
|
169
|
+
readonly localName: string | undefined;
|
|
170
|
+
readonly parent: Building | undefined;
|
|
171
|
+
readonly visibility: Visibility;
|
|
172
|
+
readonly static: boolean;
|
|
173
|
+
exported: boolean;
|
|
174
|
+
/** The type parameter this declaration is, where it is one. */
|
|
175
|
+
readonly typeParameter: string | undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A container whose members are read from a symbol table, from the tree, or both. */
|
|
179
|
+
interface Container {
|
|
180
|
+
readonly owner: Building;
|
|
181
|
+
/** The container's name node, which is what a batch resolves to its symbol. */
|
|
182
|
+
readonly nameNode: Node | undefined;
|
|
183
|
+
readonly members: readonly Node[];
|
|
184
|
+
readonly memberKind: SymbolKind;
|
|
185
|
+
/** Whether the container is a class, whose static members are a second table. */
|
|
186
|
+
readonly isClass: boolean;
|
|
187
|
+
/** Whether the container keeps its members in the export table, as an enum does. */
|
|
188
|
+
readonly membersAreExports: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A module or namespace whose export table says which of its declarations it exports. */
|
|
192
|
+
interface ExportScope {
|
|
193
|
+
readonly owner: Building;
|
|
194
|
+
readonly nameNode: Node;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const MANIFEST = "package.json";
|
|
198
|
+
|
|
199
|
+
/** One manifest that names a package, and the directory below the root that holds it. */
|
|
200
|
+
interface Named {
|
|
201
|
+
readonly name: string;
|
|
202
|
+
readonly at: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolves each file's package scope: the name of the nearest manifest at or above
|
|
207
|
+
* its directory up to the target root, and the file's path below that manifest.
|
|
208
|
+
* Where no manifest up to the root carries a name, the package is a single dot and
|
|
209
|
+
* the path is read from the root, which cannot collide, because a package name never
|
|
210
|
+
* starts with a dot.
|
|
211
|
+
*
|
|
212
|
+
* One directory is read at most once, because a package holds many files and a
|
|
213
|
+
* manifest read is a filesystem call.
|
|
214
|
+
*/
|
|
215
|
+
function packages(host: Host, root: string): (path: string) => Module {
|
|
216
|
+
const named = new Map<string, Named | undefined>();
|
|
217
|
+
|
|
218
|
+
const manifestName = (dir: string): string | undefined => {
|
|
219
|
+
let text: string;
|
|
220
|
+
try {
|
|
221
|
+
text = host.readFile(joinPath(root, dir, MANIFEST));
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
let value: unknown;
|
|
226
|
+
try {
|
|
227
|
+
value = JSON.parse(text);
|
|
228
|
+
} catch {
|
|
229
|
+
// A manifest this program cannot read names no package, which is the same
|
|
230
|
+
// answer as an absent one: the file is then scoped by the target root.
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
if (typeof value !== "object" || value === null) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
const name = (value as Record<string, unknown>)["name"];
|
|
237
|
+
return typeof name === "string" && name !== "" ? name : undefined;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const nearest = (dir: string): Named | undefined => {
|
|
241
|
+
if (named.has(dir)) {
|
|
242
|
+
return named.get(dir);
|
|
243
|
+
}
|
|
244
|
+
const name = manifestName(dir);
|
|
245
|
+
const found =
|
|
246
|
+
name !== undefined ? { name, at: dir } : dir === "" ? undefined : nearest(parentOf(dir));
|
|
247
|
+
named.set(dir, found);
|
|
248
|
+
return found;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return (path) => {
|
|
252
|
+
const found = nearest(parentOf(path));
|
|
253
|
+
if (found === undefined) {
|
|
254
|
+
return { package: ".", path };
|
|
255
|
+
}
|
|
256
|
+
return { package: found.name, path: found.at === "" ? path : path.slice(found.at.length + 1) };
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The directory one path below the root sits in, and the root itself as the empty path. */
|
|
261
|
+
function parentOf(path: string): string {
|
|
262
|
+
const dir = dirnamePath(path);
|
|
263
|
+
return dir === "." ? "" : dir;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The modifier flags one node carries, and zero for a node that carries none. */
|
|
267
|
+
function modifiersOf(node: Node): number {
|
|
268
|
+
return (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The type parameters one node declares, and none for a node that declares none. */
|
|
272
|
+
function typeParametersOf(node: Node): readonly Node[] {
|
|
273
|
+
if (
|
|
274
|
+
isFunctionDeclaration(node) ||
|
|
275
|
+
isMethodDeclaration(node) ||
|
|
276
|
+
isMethodSignatureDeclaration(node)
|
|
277
|
+
) {
|
|
278
|
+
return node.typeParameters ?? [];
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The reference component one declaration name node spells, and undefined for a
|
|
285
|
+
* name the grammar does not represent: a binding pattern, and a computed key whose
|
|
286
|
+
* source text carries a closing bracket.
|
|
287
|
+
*/
|
|
288
|
+
function componentOf(name: Node | undefined): Component | undefined {
|
|
289
|
+
if (name === undefined) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (isComputedPropertyName(name)) {
|
|
293
|
+
const text = name.expression.getText();
|
|
294
|
+
return text.includes("]") ? undefined : { text, computed: true };
|
|
295
|
+
}
|
|
296
|
+
if (isIdentifier(name) || isPrivateIdentifier(name)) {
|
|
297
|
+
return { text: name.getText(), computed: false };
|
|
298
|
+
}
|
|
299
|
+
if (isStringOrNumericName(name)) {
|
|
300
|
+
// A string or numeric key's own text carries its quotes and its numeric
|
|
301
|
+
// spelling; the reference carries the value, which the component then quotes.
|
|
302
|
+
return { text: literalTextOf(name), computed: false };
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isStringOrNumericName(name: Node): boolean {
|
|
308
|
+
const text = name.getText();
|
|
309
|
+
return (
|
|
310
|
+
text.startsWith('"') || text.startsWith("'") || text.startsWith("`") || /^[0-9]/u.test(text)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** One member's identity inside its container: its component, and its side of a class. */
|
|
315
|
+
function memberKey(component: Component, isStatic: boolean): string {
|
|
316
|
+
return `${component.computed ? "[" : ""}${component.text}${isStatic ? ":static" : ""}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The members one type alias declares: the members of the object type it writes, and
|
|
321
|
+
* of every object type that is a direct constituent of a union or an intersection it
|
|
322
|
+
* writes. A constituent naming another type declares nothing here, because that
|
|
323
|
+
* declaration carries its own members.
|
|
324
|
+
*
|
|
325
|
+
* A name two constituents both declare is left out of all of them. The alias stands
|
|
326
|
+
* for such a member, because the two declarations are one member of the type and
|
|
327
|
+
* neither of them is the one a reference to it would name.
|
|
328
|
+
*/
|
|
329
|
+
function aliasMembers(type: Node): readonly Node[] {
|
|
330
|
+
const literals = isTypeLiteralNode(type)
|
|
331
|
+
? [type]
|
|
332
|
+
: isUnionTypeNode(type) || isIntersectionTypeNode(type)
|
|
333
|
+
? type.types.filter(isTypeLiteralNode)
|
|
334
|
+
: [];
|
|
335
|
+
if (literals.length < 2) {
|
|
336
|
+
return literals[0]?.members ?? [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const declaring = new Map<string, number>();
|
|
340
|
+
for (const literal of literals) {
|
|
341
|
+
for (const key of new Set(literal.members.map(keyOf))) {
|
|
342
|
+
if (key !== undefined) {
|
|
343
|
+
declaring.set(key, (declaring.get(key) ?? 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return literals.flatMap((literal) =>
|
|
348
|
+
literal.members.filter((member) => {
|
|
349
|
+
const key = keyOf(member);
|
|
350
|
+
return key !== undefined && declaring.get(key) === 1;
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** One type member's identity, and undefined for a member the grammar cannot name. */
|
|
356
|
+
function keyOf(member: Node): string | undefined {
|
|
357
|
+
const component = componentOf(nameNodeOf(member));
|
|
358
|
+
return component === undefined ? undefined : memberKey(component, false);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The value one string or numeric key denotes, which is what the reference names. */
|
|
362
|
+
function literalTextOf(name: Node): string {
|
|
363
|
+
const text = name.getText();
|
|
364
|
+
if (text.startsWith('"') || text.startsWith("'") || text.startsWith("`")) {
|
|
365
|
+
return text.slice(1, -1);
|
|
366
|
+
}
|
|
367
|
+
return text;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The component one declaration's reference carries, and its declared name where the
|
|
372
|
+
* two differ.
|
|
373
|
+
*
|
|
374
|
+
* A declaration its module exports as the default is named `default`, because that is
|
|
375
|
+
* its export name and a module has at most one, so the spelling is unambiguous and it
|
|
376
|
+
* holds for an anonymous declaration too. Its declared name is a fact about the source
|
|
377
|
+
* rather than about the export, so it travels beside the component as the display name.
|
|
378
|
+
*/
|
|
379
|
+
function exportedAs(
|
|
380
|
+
node: Node,
|
|
381
|
+
name: Node | undefined,
|
|
382
|
+
): { readonly component: Component | undefined; readonly localName: string | undefined } {
|
|
383
|
+
const declared = componentOf(name);
|
|
384
|
+
if ((modifiersOf(node) & ModifierFlags.Default) === 0) {
|
|
385
|
+
return { component: declared, localName: undefined };
|
|
386
|
+
}
|
|
387
|
+
return { component: { text: "default", computed: false }, localName: declared?.text };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The visibility one member declaration's modifiers and name give it. */
|
|
391
|
+
function visibilityOf(node: Node, name: Node | undefined): Visibility {
|
|
392
|
+
if (name !== undefined && isPrivateIdentifier(name)) {
|
|
393
|
+
return "private-name";
|
|
394
|
+
}
|
|
395
|
+
const flags = modifiersOf(node);
|
|
396
|
+
if ((flags & ModifierFlags.Private) !== 0) {
|
|
397
|
+
return "private";
|
|
398
|
+
}
|
|
399
|
+
if ((flags & ModifierFlags.Protected) !== 0) {
|
|
400
|
+
return "protected";
|
|
401
|
+
}
|
|
402
|
+
return "public";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The name node of one declaration, for the kinds that carry one. */
|
|
406
|
+
function nameNodeOf(node: Node): Node | undefined {
|
|
407
|
+
return (node as { readonly name?: Node }).name;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** One node's identity across the two mechanisms, which resolve it separately. */
|
|
411
|
+
function nodeKey(file: SourceFile, node: Node): string {
|
|
412
|
+
return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The kind a class element takes: a method, or a property or accessor. */
|
|
416
|
+
function classMemberKind(node: Node): SymbolKind {
|
|
417
|
+
return isMethodDeclaration(node) ? "method" : "class-member";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The kind a type element takes: a method of an interface, or any other member. */
|
|
421
|
+
function typeMemberKind(node: Node, inInterface: boolean): SymbolKind {
|
|
422
|
+
return inInterface && isMethodSignatureDeclaration(node) ? "interface-method" : "type-member";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Every declaration one project's own files hold.
|
|
427
|
+
*
|
|
428
|
+
* `targetRoot` is the absolute path every position is rendered against; a file the
|
|
429
|
+
* program holds that is not below it ends the enumeration rather than being passed
|
|
430
|
+
* over. `host` is the filesystem the manifests that name each file's package are
|
|
431
|
+
* read through, and nothing else is read from it.
|
|
432
|
+
*
|
|
433
|
+
* The order is by rendered position, so two enumerations of one project return the
|
|
434
|
+
* same list whatever order the program reported its files in.
|
|
435
|
+
*/
|
|
436
|
+
export function inventory<Brand>(
|
|
437
|
+
project: ProjectView<Brand>,
|
|
438
|
+
host: Host,
|
|
439
|
+
targetRoot: string,
|
|
440
|
+
): Inventory {
|
|
441
|
+
const files = project.ownSourceFiles();
|
|
442
|
+
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
443
|
+
const moduleOf = packages(host, targetRoot);
|
|
444
|
+
|
|
445
|
+
const building: Building[] = [];
|
|
446
|
+
const byNode = new Map<string, Building>();
|
|
447
|
+
const exportScopes: ExportScope[] = [];
|
|
448
|
+
const containers: Container[] = [];
|
|
449
|
+
const starReExports: string[] = [];
|
|
450
|
+
let outsideOwnFiles = 0;
|
|
451
|
+
|
|
452
|
+
const keep = (record: Building): Building => {
|
|
453
|
+
building.push(record);
|
|
454
|
+
byNode.set(nodeKey(record.file, record.node), record);
|
|
455
|
+
return record;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
const path = relativePath(targetRoot, file.fileName);
|
|
460
|
+
if (path === undefined) {
|
|
461
|
+
// renderPosition states the refusal, with the root it was rendered against.
|
|
462
|
+
renderPosition(file, targetRoot, 0);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const module = moduleOf(path);
|
|
466
|
+
const fileRecord = keep({
|
|
467
|
+
file,
|
|
468
|
+
node: file,
|
|
469
|
+
module,
|
|
470
|
+
position: renderPosition(file, targetRoot, 0),
|
|
471
|
+
endLine: renderPosition(file, targetRoot, Math.max(file.end - 1, 0)).line,
|
|
472
|
+
kind: "file",
|
|
473
|
+
parentChain: [],
|
|
474
|
+
// A file's reference names its module, so no component of it is ever spelled;
|
|
475
|
+
// the path is what a text line renders for it.
|
|
476
|
+
component: { text: path, computed: false },
|
|
477
|
+
localName: path,
|
|
478
|
+
parent: undefined,
|
|
479
|
+
visibility: "public",
|
|
480
|
+
static: false,
|
|
481
|
+
exported: false,
|
|
482
|
+
typeParameter: undefined,
|
|
483
|
+
});
|
|
484
|
+
exportScopes.push({ owner: fileRecord, nameNode: file });
|
|
485
|
+
walkStatements(file.statements, fileRecord);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** One declaration of a module, a namespace or a container body. */
|
|
489
|
+
function walkStatements(statements: readonly Node[], owner: Building): void {
|
|
490
|
+
for (const statement of statements) {
|
|
491
|
+
walkStatement(statement, owner);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function declare(
|
|
496
|
+
node: Node,
|
|
497
|
+
owner: Building,
|
|
498
|
+
kind: SymbolKind,
|
|
499
|
+
name: Node | undefined,
|
|
500
|
+
named: { readonly component: Component; readonly localName: string | undefined },
|
|
501
|
+
): Building {
|
|
502
|
+
const at = name ?? node;
|
|
503
|
+
const record = keep({
|
|
504
|
+
file: owner.file,
|
|
505
|
+
node,
|
|
506
|
+
module: owner.module,
|
|
507
|
+
position: renderPosition(owner.file, targetRoot, at.getStart()),
|
|
508
|
+
endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
|
|
509
|
+
kind,
|
|
510
|
+
parentChain: owner.kind === "file" ? [] : chainOf(owner),
|
|
511
|
+
component: named.component,
|
|
512
|
+
localName: named.localName,
|
|
513
|
+
parent: owner,
|
|
514
|
+
visibility: visibilityOf(node, name),
|
|
515
|
+
static: (modifiersOf(node) & ModifierFlags.Static) !== 0,
|
|
516
|
+
exported: false,
|
|
517
|
+
typeParameter: undefined,
|
|
518
|
+
});
|
|
519
|
+
declareTypeParameters(node, record);
|
|
520
|
+
return record;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function declareTypeParameters(node: Node, owner: Building): void {
|
|
524
|
+
for (const parameter of typeParametersOf(node)) {
|
|
525
|
+
const name = nameNodeOf(parameter);
|
|
526
|
+
if (name === undefined) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
keep({
|
|
530
|
+
file: owner.file,
|
|
531
|
+
node: parameter,
|
|
532
|
+
module: owner.module,
|
|
533
|
+
position: renderPosition(owner.file, targetRoot, name.getStart()),
|
|
534
|
+
endLine: renderPosition(owner.file, targetRoot, Math.max(parameter.end - 1, 0)).line,
|
|
535
|
+
kind: "type-parameter",
|
|
536
|
+
parentChain: owner.parentChain,
|
|
537
|
+
component: owner.component,
|
|
538
|
+
localName: undefined,
|
|
539
|
+
parent: owner,
|
|
540
|
+
visibility: "public",
|
|
541
|
+
static: owner.static,
|
|
542
|
+
exported: false,
|
|
543
|
+
typeParameter: name.getText(),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function walkStatement(node: Node, owner: Building): void {
|
|
549
|
+
if (isFunctionDeclaration(node)) {
|
|
550
|
+
const named = exportedAs(node, node.name);
|
|
551
|
+
if (named.component !== undefined) {
|
|
552
|
+
declare(node, owner, "function", node.name, { ...named, component: named.component });
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (isClassDeclaration(node)) {
|
|
557
|
+
const named = exportedAs(node, node.name);
|
|
558
|
+
if (named.component === undefined) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const record = declare(node, owner, "class", node.name, {
|
|
562
|
+
...named,
|
|
563
|
+
component: named.component,
|
|
564
|
+
});
|
|
565
|
+
containers.push({
|
|
566
|
+
owner: record,
|
|
567
|
+
nameNode: node.name,
|
|
568
|
+
members: node.members,
|
|
569
|
+
memberKind: "class-member",
|
|
570
|
+
isClass: true,
|
|
571
|
+
membersAreExports: false,
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (isInterfaceDeclaration(node)) {
|
|
576
|
+
const named = exportedAs(node, node.name);
|
|
577
|
+
if (named.component === undefined) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const record = declare(node, owner, "interface", node.name, {
|
|
581
|
+
...named,
|
|
582
|
+
component: named.component,
|
|
583
|
+
});
|
|
584
|
+
containers.push({
|
|
585
|
+
owner: record,
|
|
586
|
+
nameNode: node.name,
|
|
587
|
+
members: node.members,
|
|
588
|
+
memberKind: "interface-method",
|
|
589
|
+
isClass: false,
|
|
590
|
+
membersAreExports: false,
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (isTypeAliasDeclaration(node)) {
|
|
595
|
+
const named = exportedAs(node, node.name);
|
|
596
|
+
if (named.component === undefined) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const record = declare(node, owner, "type", node.name, {
|
|
600
|
+
...named,
|
|
601
|
+
component: named.component,
|
|
602
|
+
});
|
|
603
|
+
// A type alias keeps no member table of its own: the members belong to the
|
|
604
|
+
// object types its declaration writes, so the declaration is what carries them.
|
|
605
|
+
const members = aliasMembers(node.type);
|
|
606
|
+
if (members.length > 0) {
|
|
607
|
+
containers.push({
|
|
608
|
+
owner: record,
|
|
609
|
+
nameNode: undefined,
|
|
610
|
+
members,
|
|
611
|
+
memberKind: "type-member",
|
|
612
|
+
isClass: false,
|
|
613
|
+
membersAreExports: false,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (isEnumDeclaration(node)) {
|
|
619
|
+
const named = exportedAs(node, node.name);
|
|
620
|
+
if (named.component === undefined) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const record = declare(node, owner, "enum", node.name, {
|
|
624
|
+
...named,
|
|
625
|
+
component: named.component,
|
|
626
|
+
});
|
|
627
|
+
containers.push({
|
|
628
|
+
owner: record,
|
|
629
|
+
nameNode: node.name,
|
|
630
|
+
members: node.members,
|
|
631
|
+
memberKind: "enum-member",
|
|
632
|
+
isClass: false,
|
|
633
|
+
membersAreExports: true,
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (isModuleDeclaration(node)) {
|
|
638
|
+
const component = componentOf(node.name);
|
|
639
|
+
if (component === undefined) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const record = declare(node, owner, "namespace", node.name, {
|
|
643
|
+
component,
|
|
644
|
+
localName: undefined,
|
|
645
|
+
});
|
|
646
|
+
exportScopes.push({ owner: record, nameNode: node.name });
|
|
647
|
+
const body = node.body;
|
|
648
|
+
if (body === undefined) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (isModuleBlock(body)) {
|
|
652
|
+
walkStatements(body.statements, record);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
walkStatement(body, record);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (isVariableStatement(node)) {
|
|
659
|
+
for (const declaration of node.declarationList.declarations) {
|
|
660
|
+
const component = componentOf(declaration.name);
|
|
661
|
+
if (component === undefined) {
|
|
662
|
+
// A destructuring pattern declares binding elements, which the reference
|
|
663
|
+
// grammar does not represent; the statement stands for them.
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
declare(declaration, owner, "variable", declaration.name, {
|
|
667
|
+
component,
|
|
668
|
+
localName: undefined,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (isExportDeclaration(node)) {
|
|
674
|
+
walkExportDeclaration(node, owner);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (isExportAssignment(node) && !node.isExportEquals) {
|
|
678
|
+
declare(node, owner, "export-alias", undefined, {
|
|
679
|
+
component: { text: "default", computed: false },
|
|
680
|
+
localName: undefined,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function walkExportDeclaration(
|
|
686
|
+
node: { readonly exportClause?: Node; readonly moduleSpecifier?: Node },
|
|
687
|
+
owner: Building,
|
|
688
|
+
): void {
|
|
689
|
+
const clause = node.exportClause;
|
|
690
|
+
if (clause === undefined) {
|
|
691
|
+
const specifier = node.moduleSpecifier;
|
|
692
|
+
starReExports.push(specifier === undefined ? "" : literalTextOf(specifier));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (isNamespaceExport(clause)) {
|
|
696
|
+
const component = componentOf(clause.name);
|
|
697
|
+
if (component !== undefined) {
|
|
698
|
+
declare(clause, owner, "export-alias", clause.name, { component, localName: undefined });
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
for (const element of (clause as { readonly elements?: readonly Node[] }).elements ?? []) {
|
|
703
|
+
const name = nameNodeOf(element);
|
|
704
|
+
const component = componentOf(name);
|
|
705
|
+
if (component !== undefined) {
|
|
706
|
+
declare(element, owner, "export-alias", name, { component, localName: undefined });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// One batch for every module and every container the walk found, because a
|
|
712
|
+
// checker accessor with an array overload costs one round trip for the batch and
|
|
713
|
+
// one per node without it.
|
|
714
|
+
const batched: Node[] = [
|
|
715
|
+
...exportScopes.map((scope) => scope.nameNode),
|
|
716
|
+
...containers.map((container) => container.nameNode).filter((node) => node !== undefined),
|
|
717
|
+
];
|
|
718
|
+
const handles: Handle<Brand>[] = batched.map((node) => project.handle(node));
|
|
719
|
+
const resolved = project.symbolsAt(handles);
|
|
720
|
+
const symbolOf = new Map<Node, TSSymbol>();
|
|
721
|
+
batched.forEach((node, index) => {
|
|
722
|
+
const symbol = resolved[index];
|
|
723
|
+
if (symbol !== undefined) {
|
|
724
|
+
symbolOf.set(node, symbol);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
let exportTables = 0;
|
|
729
|
+
for (const scope of exportScopes) {
|
|
730
|
+
const symbol = symbolOf.get(scope.nameNode);
|
|
731
|
+
if (symbol === undefined) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
exportTables += 1;
|
|
735
|
+
for (const [, exported] of symbol.getExports()) {
|
|
736
|
+
for (const declaration of exported.declarations) {
|
|
737
|
+
const held = project.declarationAt(declaration);
|
|
738
|
+
if (held === undefined) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const record = byNode.get(nodeKey(held.node.getSourceFile(), held.node));
|
|
742
|
+
if (record === undefined) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
record.exported = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let memberTables = 0;
|
|
751
|
+
for (const container of containers) {
|
|
752
|
+
const symbol = container.nameNode === undefined ? undefined : symbolOf.get(container.nameNode);
|
|
753
|
+
const seen = new Map<string, Building>();
|
|
754
|
+
if (symbol !== undefined) {
|
|
755
|
+
if (!container.membersAreExports) {
|
|
756
|
+
memberTables += 1;
|
|
757
|
+
outsideOwnFiles += readTable(container, symbol.getMembers(), seen, false);
|
|
758
|
+
}
|
|
759
|
+
if (container.membersAreExports || container.isClass) {
|
|
760
|
+
memberTables += 1;
|
|
761
|
+
outsideOwnFiles += readTable(container, symbol.getExports(), seen, container.isClass);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
completeFromTree(container, seen);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* One container's members as its symbol table holds them, each positioned at the
|
|
769
|
+
* declaration the table names. A member with no declaration is the language's own
|
|
770
|
+
* (a class's `prototype`), and a member declared outside the target's own files
|
|
771
|
+
* belongs to another program; neither is a declaration of the target.
|
|
772
|
+
*/
|
|
773
|
+
function readTable(
|
|
774
|
+
container: Container,
|
|
775
|
+
table: ReadonlyMap<string, TSSymbol>,
|
|
776
|
+
seen: Map<string, Building>,
|
|
777
|
+
areStatic: boolean,
|
|
778
|
+
): number {
|
|
779
|
+
let outside = 0;
|
|
780
|
+
for (const [, member] of table) {
|
|
781
|
+
const declarations = member.declarations
|
|
782
|
+
.map((handle) => project.declarationAt(handle))
|
|
783
|
+
.filter((held): held is Handle<Brand> => held !== undefined);
|
|
784
|
+
if (declarations.length === 0) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const own = declarations.filter((held) => ownFiles.has(held.node.getSourceFile().fileName));
|
|
788
|
+
if (own.length === 0) {
|
|
789
|
+
outside += declarations.length;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
for (const held of own) {
|
|
793
|
+
keepMember(container, held.node, seen, areStatic);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return outside;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* One container's members as its own declaration writes them, for the members its
|
|
801
|
+
* symbol table does not hold: a member whose key is computed is bound to the
|
|
802
|
+
* container's type later than the table is built.
|
|
803
|
+
*/
|
|
804
|
+
function completeFromTree(container: Container, seen: Map<string, Building>): void {
|
|
805
|
+
for (const member of container.members) {
|
|
806
|
+
keepMember(container, member, seen, (modifiersOf(member) & ModifierFlags.Static) !== 0);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function keepMember(
|
|
811
|
+
container: Container,
|
|
812
|
+
node: Node,
|
|
813
|
+
seen: Map<string, Building>,
|
|
814
|
+
areStatic: boolean,
|
|
815
|
+
): void {
|
|
816
|
+
const kind = memberKindOf(container, node);
|
|
817
|
+
if (kind === undefined) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const name = nameNodeOf(node);
|
|
821
|
+
const component = componentOf(name);
|
|
822
|
+
if (component === undefined) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const file = node.getSourceFile();
|
|
826
|
+
const key = memberKey(component, areStatic);
|
|
827
|
+
const held = seen.get(key);
|
|
828
|
+
const endLine = renderPosition(file, targetRoot, Math.max(node.end - 1, 0)).line;
|
|
829
|
+
if (held !== undefined) {
|
|
830
|
+
// 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.
|
|
832
|
+
held.endLine = Math.max(held.endLine, endLine);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const at = name ?? node;
|
|
836
|
+
const record = keep({
|
|
837
|
+
file,
|
|
838
|
+
node,
|
|
839
|
+
module: container.owner.module,
|
|
840
|
+
position: renderPosition(file, targetRoot, at.getStart()),
|
|
841
|
+
endLine,
|
|
842
|
+
kind,
|
|
843
|
+
parentChain: chainOf(container.owner),
|
|
844
|
+
component,
|
|
845
|
+
localName: undefined,
|
|
846
|
+
parent: container.owner,
|
|
847
|
+
visibility: visibilityOf(node, name),
|
|
848
|
+
static: areStatic,
|
|
849
|
+
exported: false,
|
|
850
|
+
typeParameter: undefined,
|
|
851
|
+
});
|
|
852
|
+
seen.set(key, record);
|
|
853
|
+
declareTypeParameters(node, record);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function memberKindOf(container: Container, node: Node): SymbolKind | undefined {
|
|
857
|
+
if (container.isClass) {
|
|
858
|
+
if (
|
|
859
|
+
isPropertyDeclaration(node) ||
|
|
860
|
+
isMethodDeclaration(node) ||
|
|
861
|
+
isGetAccessorDeclaration(node) ||
|
|
862
|
+
isSetAccessorDeclaration(node)
|
|
863
|
+
) {
|
|
864
|
+
return classMemberKind(node);
|
|
865
|
+
}
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
if (container.memberKind === "enum-member") {
|
|
869
|
+
return isEnumMember(node) ? "enum-member" : undefined;
|
|
870
|
+
}
|
|
871
|
+
if (isPropertySignatureDeclaration(node) || isMethodSignatureDeclaration(node)) {
|
|
872
|
+
return typeMemberKind(node, container.memberKind === "interface-method");
|
|
873
|
+
}
|
|
874
|
+
if (isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) {
|
|
875
|
+
return "type-member";
|
|
876
|
+
}
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const symbols = building
|
|
881
|
+
.map(render)
|
|
882
|
+
.sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
configFile: project.configFile,
|
|
886
|
+
symbols,
|
|
887
|
+
cost: { batches: 1, exportTables, memberTables },
|
|
888
|
+
starReExports,
|
|
889
|
+
outsideOwnFiles,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** The chain of components that names one declaration, read from the module inward. */
|
|
894
|
+
function chainOf(record: Building): readonly Component[] {
|
|
895
|
+
return [...record.parentChain, record.component];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* One declaration as the inventory reports it. The display name is the chain in the
|
|
900
|
+
* spelling a reference gives it, except where the declaration carries a name of its
|
|
901
|
+
* own the reference does not: a file, whose reference names its module, and a default
|
|
902
|
+
* export, whose reference carries its export name.
|
|
903
|
+
*/
|
|
904
|
+
function render(record: Building): InventorySymbol {
|
|
905
|
+
const chain = chainOf(record);
|
|
906
|
+
const spelled = chain
|
|
907
|
+
.map((component) =>
|
|
908
|
+
component.computed ? computedComponent(component.text) : nameComponent(component.text),
|
|
909
|
+
)
|
|
910
|
+
.join(".");
|
|
911
|
+
const name =
|
|
912
|
+
record.localName ??
|
|
913
|
+
(record.typeParameter === undefined ? spelled : `${spelled}<${record.typeParameter}>`);
|
|
914
|
+
return {
|
|
915
|
+
id: positionKey(record.position),
|
|
916
|
+
ref: refOf(record, chain),
|
|
917
|
+
name,
|
|
918
|
+
kind: record.kind,
|
|
919
|
+
position: record.position,
|
|
920
|
+
endLine: record.endLine,
|
|
921
|
+
parent: record.parent === undefined ? "" : positionKey(record.parent.position),
|
|
922
|
+
exported: record.exported,
|
|
923
|
+
visibility: record.visibility,
|
|
924
|
+
static: record.static,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function refOf(record: Building, chain: readonly Component[]): string {
|
|
929
|
+
if (record.kind === "file") {
|
|
930
|
+
return renderRef(record.module, { of: "module" });
|
|
931
|
+
}
|
|
932
|
+
if (record.kind === "export-alias") {
|
|
933
|
+
return renderRef(record.module, { of: "alias", name: record.component.text });
|
|
934
|
+
}
|
|
935
|
+
return renderRef(record.module, {
|
|
936
|
+
of: "declaration",
|
|
937
|
+
chain,
|
|
938
|
+
static: record.static,
|
|
939
|
+
...(record.typeParameter === undefined ? {} : { typeParameter: record.typeParameter }),
|
|
940
|
+
});
|
|
941
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -71,3 +71,23 @@ export function dirnamePath(path: string): string {
|
|
|
71
71
|
export function resolvePath(against: string, path: string): string {
|
|
72
72
|
return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One path as it reads below a root, or undefined when it is not below the root.
|
|
77
|
+
* Both are normalized first, so a root written with a trailing separator or a `.`
|
|
78
|
+
* segment reaches the same answer, and the comparison is then lexical, so a root
|
|
79
|
+
* and a path spelled through different symbolic links name nothing in common. The
|
|
80
|
+
* root itself is not below itself.
|
|
81
|
+
*
|
|
82
|
+
* The caller resolves both against the same directory before calling: a relative
|
|
83
|
+
* root and an absolute path are two spellings this cannot reconcile.
|
|
84
|
+
*/
|
|
85
|
+
export function relativePath(root: string, path: string): string | undefined {
|
|
86
|
+
const at = normalizePath(root);
|
|
87
|
+
const prefix = at.endsWith(SEPARATOR) ? at : at + SEPARATOR;
|
|
88
|
+
const under = normalizePath(path);
|
|
89
|
+
if (!under.startsWith(prefix) || under.length === prefix.length) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return under.slice(prefix.length);
|
|
93
|
+
}
|
package/src/position.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
2
|
+
import { relativePath } from "./paths.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where one declaration is written, as every report renders it.
|
|
6
|
+
*
|
|
7
|
+
* The column counts UTF-16 code units, which is the unit the SARIF specification
|
|
8
|
+
* and the Language Server Protocol count in, and which is also the unit the
|
|
9
|
+
* compiler's own positions are measured in, so no conversion stands between the
|
|
10
|
+
* two.
|
|
11
|
+
*/
|
|
12
|
+
export interface Position {
|
|
13
|
+
/** The declaring file's path below the target root, with the solidus as separator. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
/** The line the declaration starts on, counted from one. */
|
|
16
|
+
readonly line: number;
|
|
17
|
+
/** The column, counted from one in UTF-16 code units. */
|
|
18
|
+
readonly column: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A file the program holds whose path is not below the target root, which leaves
|
|
23
|
+
* the analysis unable to name a declaration it found. It ends the run rather than
|
|
24
|
+
* being passed over, so a position that does not render is never silently dropped.
|
|
25
|
+
*/
|
|
26
|
+
export class PositionError extends Error {
|
|
27
|
+
/** The file as the program named it. */
|
|
28
|
+
readonly file: string;
|
|
29
|
+
|
|
30
|
+
constructor(file: string, root: string) {
|
|
31
|
+
super(`${file} is not below ${root}`);
|
|
32
|
+
this.name = "PositionError";
|
|
33
|
+
this.file = file;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The position of one offset of one file, rendered against the target root.
|
|
39
|
+
*
|
|
40
|
+
* `offset` is a UTF-16 code unit offset into the file's text, which is what every
|
|
41
|
+
* node of the tree carries.
|
|
42
|
+
*/
|
|
43
|
+
export function renderPosition(file: SourceFile, root: string, offset: number): Position {
|
|
44
|
+
const path = relativePath(root, file.fileName);
|
|
45
|
+
if (path === undefined) {
|
|
46
|
+
throw new PositionError(file.fileName, root);
|
|
47
|
+
}
|
|
48
|
+
const { line, character } = file.getLineAndCharacterOfPosition(offset);
|
|
49
|
+
return { path, line: line + 1, column: character + 1 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The identifier of the declaration written at one position: the path, the line and
|
|
54
|
+
* the column joined by colons. It is what the graph keys on inside one run, and
|
|
55
|
+
* what the configuration intersection compares across projects, because a handle is
|
|
56
|
+
* meaningful only inside the program that produced it while a rendered position is
|
|
57
|
+
* meaningful anywhere.
|
|
58
|
+
*/
|
|
59
|
+
export function positionKey(position: Position): string {
|
|
60
|
+
return `${position.path}:${String(position.line)}:${String(position.column)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Two positions ordered by file, line and column, which is the order the inventory
|
|
65
|
+
* reads in and the order every set of a run is reported in.
|
|
66
|
+
*/
|
|
67
|
+
export function byPosition(a: Position, b: Position): number {
|
|
68
|
+
if (a.path !== b.path) {
|
|
69
|
+
return a.path < b.path ? -1 : 1;
|
|
70
|
+
}
|
|
71
|
+
if (a.line !== b.line) {
|
|
72
|
+
return a.line - b.line;
|
|
73
|
+
}
|
|
74
|
+
return a.column - b.column;
|
|
75
|
+
}
|
package/src/ref.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stable symbol reference: the identifier that names one declaration across
|
|
3
|
+
* runs, across reports, in an ignore entry, in a configured root and in a
|
|
4
|
+
* cross-language edge. It is built from names and containers only, so an edit above
|
|
5
|
+
* a declaration leaves it unchanged, and it is compared as bytes, so there is one
|
|
6
|
+
* canonical spelling per declaration and this module is where that spelling is
|
|
7
|
+
* written.
|
|
8
|
+
*
|
|
9
|
+
* The position key of {@link ./position.ts} is the other identifier and has the
|
|
10
|
+
* other job: it locates a finding inside one run, and the two never mix.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The language prefix, which namespaces every reference this analyzer emits. */
|
|
14
|
+
const PREFIX = "ts://";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The building blocks of the reference grammar, spelled in the intersection of the
|
|
18
|
+
* two regular-expression dialects a reference is checked in: Go's `regexp` and an
|
|
19
|
+
* ECMAScript `RegExp` with no flags. Neither dialect offers a Unicode identifier
|
|
20
|
+
* property without a flag, so a character outside ASCII is admitted as a class;
|
|
21
|
+
* the expressions check the form of a reference and never validate a name, because
|
|
22
|
+
* the compiler that accepted the declaration has already validated it.
|
|
23
|
+
*/
|
|
24
|
+
const TS_IDENT = String.raw`(?:[A-Za-z_$]|[^\x00-\x7F])(?:[A-Za-z0-9_$]|[^\x00-\x7F])*`;
|
|
25
|
+
const TS_QUOTED = String.raw`'(?:[^'\\\r\n]|\\['\\nr])*'`;
|
|
26
|
+
const TS_COMPUTED = String.raw`\[[^\]\r\n\t ]+\]`;
|
|
27
|
+
const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
|
|
28
|
+
const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
|
|
29
|
+
const TS_PACKAGE = String.raw`(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
|
|
30
|
+
const TS_FILE = String.raw`(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
|
|
31
|
+
|
|
32
|
+
const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
|
|
33
|
+
const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One expression per form the grammar defines, each anchored and each the whole of
|
|
37
|
+
* a reference. Several forms share one expression because they differ in what the
|
|
38
|
+
* symbol is rather than in how the reference is spelled, so the list is the set of
|
|
39
|
+
* spellings and not the set of subjects. A reference is valid when it matches one.
|
|
40
|
+
*
|
|
41
|
+
* They are written here rather than read from the Contract at run time: the Contract
|
|
42
|
+
* is data a reader outside this program consults, and a product that read it while
|
|
43
|
+
* running would need it installed beside itself. A test compares this list against
|
|
44
|
+
* the release this analyzer is written against, so a grammar that grows a form
|
|
45
|
+
* arrives here as a failing test.
|
|
46
|
+
*/
|
|
47
|
+
export const REF_EXPRESSIONS: readonly string[] = [
|
|
48
|
+
`^${SCOPE}#$`,
|
|
49
|
+
`^${SCOPE}#${TS_HEAD}$`,
|
|
50
|
+
`^${SCOPE}#${TS_HEAD}:alias$`,
|
|
51
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
|
|
52
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
|
|
53
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
|
|
54
|
+
`^${MANIFEST_SCOPE}#${TS_PACKAGE}:(?:dependency|dev-dependency|peer-dependency)$`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const REF_FORMS: readonly RegExp[] = REF_EXPRESSIONS.map(
|
|
58
|
+
(expression) => new RegExp(expression, "u"),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const IDENTIFIER = new RegExp(`^${TS_IDENT}$`, "u");
|
|
62
|
+
const PRIVATE_NAME = new RegExp(`^#${TS_IDENT}$`, "u");
|
|
63
|
+
const ASCII_WHITESPACE = /[\t\n\v\f\r ]/gu;
|
|
64
|
+
|
|
65
|
+
/** The section of a manifest that declares a dependency. */
|
|
66
|
+
export type DependencySection = "dependency" | "dev-dependency" | "peer-dependency";
|
|
67
|
+
|
|
68
|
+
/** The scope half of a reference: the package a file belongs to, and its path in it. */
|
|
69
|
+
export interface Module {
|
|
70
|
+
/**
|
|
71
|
+
* The name of the package the file belongs to, or a single dot where no manifest
|
|
72
|
+
* at or above it up to the target root carries one. The two cannot collide,
|
|
73
|
+
* because a package name never starts with a dot.
|
|
74
|
+
*/
|
|
75
|
+
readonly package: string;
|
|
76
|
+
/** The file's path inside that package, with its own extension. */
|
|
77
|
+
readonly path: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One component of a fragment: a declared name, or the source text of a computed
|
|
82
|
+
* key. The two are spelled differently and cannot be told apart from the text
|
|
83
|
+
* alone, so the caller says which it has.
|
|
84
|
+
*/
|
|
85
|
+
export interface Component {
|
|
86
|
+
/** The declared name, or the source text between the brackets of a computed key. */
|
|
87
|
+
readonly text: string;
|
|
88
|
+
/** Whether the text is a computed key rather than a name. */
|
|
89
|
+
readonly computed: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What one reference's fragment names. */
|
|
93
|
+
export type Fragment =
|
|
94
|
+
/** The module itself, which is the parent of every module-level declaration. */
|
|
95
|
+
| { readonly of: "module" }
|
|
96
|
+
/**
|
|
97
|
+
* One declaration, named by the chain of components from the module inward with
|
|
98
|
+
* its own last. A static member of a class is always marked, whether or not the
|
|
99
|
+
* class also declares an instance member of that name, so the spelling does not
|
|
100
|
+
* depend on what else the class declares. A type parameter is the declaring
|
|
101
|
+
* function's or method's own chain followed by the parameter's name.
|
|
102
|
+
*/
|
|
103
|
+
| {
|
|
104
|
+
readonly of: "declaration";
|
|
105
|
+
readonly chain: readonly Component[];
|
|
106
|
+
readonly static: boolean;
|
|
107
|
+
readonly typeParameter?: string;
|
|
108
|
+
}
|
|
109
|
+
/** One export specifier, a symbol of its own beside the declaration it names. */
|
|
110
|
+
| { readonly of: "alias"; readonly name: string }
|
|
111
|
+
/** One dependency of a manifest, scoped by that manifest. */
|
|
112
|
+
| { readonly of: "dependency"; readonly name: string; readonly section: DependencySection };
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The canonical spelling of one declared name as a fragment component: bare when
|
|
116
|
+
* the name is an identifier name, reserved words included; with its leading number
|
|
117
|
+
* sign kept when the name is a private name, because the sign is part of the name
|
|
118
|
+
* in the language and a class may declare both spellings side by side; and in
|
|
119
|
+
* single quotes otherwise. The quotes are single so that a reference sits inside a
|
|
120
|
+
* JSON string with no escaping, which is where every document carries one.
|
|
121
|
+
*/
|
|
122
|
+
export function nameComponent(name: string): string {
|
|
123
|
+
if (IDENTIFIER.test(name) || PRIVATE_NAME.test(name)) {
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
const escaped = name
|
|
127
|
+
.replaceAll("\\", "\\\\")
|
|
128
|
+
.replaceAll("'", "\\'")
|
|
129
|
+
.replaceAll("\n", "\\n")
|
|
130
|
+
.replaceAll("\r", "\\r");
|
|
131
|
+
return `'${escaped}'`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The canonical spelling of one computed key: its source text between square
|
|
136
|
+
* brackets with every ASCII whitespace character removed. The text is compared as
|
|
137
|
+
* written, so two spellings of one well-known symbol are two references, which is
|
|
138
|
+
* right, because they are two declarations.
|
|
139
|
+
*/
|
|
140
|
+
export function computedComponent(text: string): string {
|
|
141
|
+
return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function spell(component: Component): string {
|
|
145
|
+
return component.computed ? computedComponent(component.text) : nameComponent(component.text);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The reference of one symbol. The chain of a declaration fragment is read from the
|
|
150
|
+
* module-level declaration inward and is never empty; the module fragment is what
|
|
151
|
+
* names the module itself.
|
|
152
|
+
*/
|
|
153
|
+
export function renderRef(module: Module, fragment: Fragment): string {
|
|
154
|
+
const scope = `${PREFIX}${module.package}/${module.path}`;
|
|
155
|
+
switch (fragment.of) {
|
|
156
|
+
case "module":
|
|
157
|
+
return `${scope}#`;
|
|
158
|
+
case "alias":
|
|
159
|
+
return `${scope}#${nameComponent(fragment.name)}:alias`;
|
|
160
|
+
case "dependency":
|
|
161
|
+
return `${scope}#${fragment.name}:${fragment.section}`;
|
|
162
|
+
case "declaration": {
|
|
163
|
+
const chain = fragment.chain.map(spell).join(".");
|
|
164
|
+
const isStatic = fragment.static ? ":static" : "";
|
|
165
|
+
const parameter = fragment.typeParameter === undefined ? "" : `<${fragment.typeParameter}>`;
|
|
166
|
+
return `${scope}#${chain}${isStatic}${parameter}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether one string is a reference of this language's grammar, in its canonical
|
|
173
|
+
* spelling. A string that is not, whether misspelled or spelled in a form the
|
|
174
|
+
* grammar does not define, matches no symbol; a reference of another language is
|
|
175
|
+
* not one of this language's either, which is what the language prefix is for.
|
|
176
|
+
*/
|
|
177
|
+
export function isRef(value: string): boolean {
|
|
178
|
+
return REF_FORMS.some((form) => form.test(value));
|
|
179
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
Checker,
|
|
4
4
|
Diagnostic,
|
|
5
5
|
DocumentIdentifier,
|
|
6
|
+
NodeHandle,
|
|
6
7
|
Project,
|
|
7
8
|
Program,
|
|
8
9
|
Snapshot,
|
|
@@ -101,6 +102,17 @@ export interface ProjectView<Brand> {
|
|
|
101
102
|
* resolve.
|
|
102
103
|
*/
|
|
103
104
|
symbolsAt(handles: readonly Handle<Brand>[]): readonly (TSSymbol | undefined)[];
|
|
105
|
+
/**
|
|
106
|
+
* The node one symbol's declaration handle names, read in this project's program,
|
|
107
|
+
* or `undefined` where this project's program does not hold that file.
|
|
108
|
+
*
|
|
109
|
+
* A symbol is shared by every project of the snapshot and remembers the project it
|
|
110
|
+
* was first seen in, so resolving its handle without saying which program to read
|
|
111
|
+
* would answer out of whichever project reached the symbol first. The project is
|
|
112
|
+
* named here instead, and the answer becomes a handle of this project like any
|
|
113
|
+
* other. The read is local once the file has been fetched.
|
|
114
|
+
*/
|
|
115
|
+
declarationAt(handle: NodeHandle): Handle<Brand> | undefined;
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/**
|
|
@@ -149,6 +161,10 @@ function viewOf<Brand>(project: Project): ProjectView<Brand> {
|
|
|
149
161
|
.filter((file): file is SourceFile => file !== undefined),
|
|
150
162
|
handle: (node) => ({ node }),
|
|
151
163
|
symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
|
|
164
|
+
declarationAt: (handle) => {
|
|
165
|
+
const node = handle.resolve(project);
|
|
166
|
+
return node === undefined ? undefined : { node };
|
|
167
|
+
},
|
|
152
168
|
};
|
|
153
169
|
}
|
|
154
170
|
|
package/src/version.ts
CHANGED
|
@@ -5,7 +5,7 @@ import manifest from "../package.json" with { type: "json" };
|
|
|
5
5
|
* `contract_version` when no configuration source supplies one, and it moves
|
|
6
6
|
* independently of {@link version}.
|
|
7
7
|
*/
|
|
8
|
-
export const CONTRACT_VERSION = "1.
|
|
8
|
+
export const CONTRACT_VERSION = "1.6.0";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* This analyzer's own version, which the published package carries rather than
|