@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,606 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isArrayLiteralExpression,
|
|
3
|
+
isBinaryExpression,
|
|
4
|
+
isDeleteExpression,
|
|
5
|
+
isElementAccessExpression,
|
|
6
|
+
isForInStatement,
|
|
7
|
+
isForOfStatement,
|
|
8
|
+
isObjectLiteralExpression,
|
|
9
|
+
isParenthesizedExpression,
|
|
10
|
+
isPostfixUnaryExpression,
|
|
11
|
+
isPrefixUnaryExpression,
|
|
12
|
+
isPropertyAccessExpression,
|
|
13
|
+
isPropertyAssignment,
|
|
14
|
+
isShorthandPropertyAssignment,
|
|
15
|
+
isSpreadAssignment,
|
|
16
|
+
isSpreadElement,
|
|
17
|
+
SyntaxKind,
|
|
18
|
+
type Node,
|
|
19
|
+
type SourceFile,
|
|
20
|
+
} from "@typescript/native/unstable/ast";
|
|
21
|
+
import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
22
|
+
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
23
|
+
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
24
|
+
import type { ProjectView } from "./session.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Every use one project's own files make of the project's own declarations.
|
|
28
|
+
*
|
|
29
|
+
* The pass is one local walk per file and one batched resolution per file, never a
|
|
30
|
+
* query per symbol: a per-symbol reference query is the shape whose answers depend on
|
|
31
|
+
* the order the symbols are asked about, so no answer here depends on that order. The
|
|
32
|
+
* tree is on this side of the client boundary and the resolution is not, which is why a
|
|
33
|
+
* file's name nodes are gathered whole and handed over together.
|
|
34
|
+
*
|
|
35
|
+
* What the pass records and what it does not: a reference carries its use, its
|
|
36
|
+
* position, the declaration that encloses it and the classification of the file that
|
|
37
|
+
* made it. It carries no mode. Which of the references a run counts is the reading the
|
|
38
|
+
* sweep makes of this set, so the mode is one value the run decides once and this pass
|
|
39
|
+
* never sees it.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** How one reference uses the declaration it names. */
|
|
43
|
+
export type Use = "read" | "write";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which accessor answered for one reference, so that the cost of a run is attributable
|
|
47
|
+
* and a table of references shows the path each row took.
|
|
48
|
+
*
|
|
49
|
+
* - `batch` is the batched lookup over a file's name nodes, which answers for almost
|
|
50
|
+
* every one of them.
|
|
51
|
+
* - `resolved-symbol` is the per-node lookup for the residue a batch left.
|
|
52
|
+
* - `shorthand` is the per-node lookup for `{ a }`, whose name resolves to the property
|
|
53
|
+
* the literal declares rather than to the declaration that name reads.
|
|
54
|
+
* - `alias` is a step along an import or export chain, from one link to what it names.
|
|
55
|
+
*/
|
|
56
|
+
export type Resolution = "batch" | "resolved-symbol" | "shorthand" | "alias";
|
|
57
|
+
|
|
58
|
+
/** One use of one declaration by one declaration. */
|
|
59
|
+
export interface Reference {
|
|
60
|
+
/**
|
|
61
|
+
* The identifier of the declaration the referencing name is written inside. A
|
|
62
|
+
* reference written outside every declaration of a file belongs to the file and
|
|
63
|
+
* carries the file's own identifier, the way an import belongs to the file that
|
|
64
|
+
* writes it rather than to the first declaration below it.
|
|
65
|
+
*/
|
|
66
|
+
readonly from: string;
|
|
67
|
+
/** The identifier of the declaration the reference names. */
|
|
68
|
+
readonly to: string;
|
|
69
|
+
/** Where the referencing name is written. */
|
|
70
|
+
readonly position: Position;
|
|
71
|
+
readonly use: Use;
|
|
72
|
+
readonly resolution: Resolution;
|
|
73
|
+
/** Whether the file that made the reference is classified as a test file. */
|
|
74
|
+
readonly test: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One rule that classified files as test files, and how many files it matched. */
|
|
78
|
+
export interface TestFileRule {
|
|
79
|
+
/** The rule's name, as a report's own rule list spells one. */
|
|
80
|
+
readonly rule: string;
|
|
81
|
+
readonly matched: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What one project's reference pass cost at the client boundary. */
|
|
85
|
+
export interface ReferenceCost {
|
|
86
|
+
/**
|
|
87
|
+
* The name nodes the batches asked about, summed over the files. It is the size of
|
|
88
|
+
* the question rather than a cost of its own, and it is reported so that the number
|
|
89
|
+
* of batches is readable against it.
|
|
90
|
+
*/
|
|
91
|
+
readonly batched: number;
|
|
92
|
+
/**
|
|
93
|
+
* Batched lookups: per file, the name-node count divided by the batch cap, rounded
|
|
94
|
+
* up. It is what keeps the pass's cost off the identifier count.
|
|
95
|
+
*/
|
|
96
|
+
readonly fileBatches: number;
|
|
97
|
+
/** Per-node lookups for the name nodes a batch left unresolved. */
|
|
98
|
+
readonly residueFallbacks: number;
|
|
99
|
+
/** Per-node lookups for a shorthand property assignment's value. */
|
|
100
|
+
readonly shorthandLookups: number;
|
|
101
|
+
/** Per-symbol lookups that step from one alias to what it names, one per alias. */
|
|
102
|
+
readonly aliasSteps: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One project's references, in position order, and what reading them cost. */
|
|
106
|
+
export interface References {
|
|
107
|
+
/** The compiler configuration the project was opened from. */
|
|
108
|
+
readonly configFile: string;
|
|
109
|
+
readonly references: readonly Reference[];
|
|
110
|
+
/** The rules that classified files as test files, ordered by rule. */
|
|
111
|
+
readonly testFileRules: readonly TestFileRule[];
|
|
112
|
+
readonly cost: ReferenceCost;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** How one run resolves references. */
|
|
116
|
+
export interface ReferenceOptions {
|
|
117
|
+
/**
|
|
118
|
+
* The greatest number of name nodes one batched lookup asks about. A batch is
|
|
119
|
+
* answered by an array of its own length, so an uncapped batch over a large file
|
|
120
|
+
* makes one answer proportional to that file. Absent takes {@link DEFAULT_BATCH_CAP}.
|
|
121
|
+
*/
|
|
122
|
+
readonly batchCap?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Glob patterns, relative to the target root, naming the files that are test files.
|
|
125
|
+
* Each pattern is a rule of its own and reports the files it matched.
|
|
126
|
+
*/
|
|
127
|
+
readonly testFiles: readonly string[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The batch cap a run takes when the configuration names none. It bounds the size of
|
|
132
|
+
* one answer rather than the work: the median file is one batch at this cap and the
|
|
133
|
+
* largest production file a handful.
|
|
134
|
+
*/
|
|
135
|
+
export const DEFAULT_BATCH_CAP = 4096;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The operators whose left side one assignment stores into. A compound assignment is
|
|
139
|
+
* among them: it reads its target only to write the result back, so nothing else
|
|
140
|
+
* receives what it read, and a declaration whose every other reference is one of these
|
|
141
|
+
* carries no information out of itself.
|
|
142
|
+
*/
|
|
143
|
+
const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
|
|
144
|
+
SyntaxKind.EqualsToken,
|
|
145
|
+
SyntaxKind.PlusEqualsToken,
|
|
146
|
+
SyntaxKind.MinusEqualsToken,
|
|
147
|
+
SyntaxKind.AsteriskEqualsToken,
|
|
148
|
+
SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
149
|
+
SyntaxKind.SlashEqualsToken,
|
|
150
|
+
SyntaxKind.PercentEqualsToken,
|
|
151
|
+
SyntaxKind.LessThanLessThanEqualsToken,
|
|
152
|
+
SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
153
|
+
SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
154
|
+
SyntaxKind.AmpersandEqualsToken,
|
|
155
|
+
SyntaxKind.BarEqualsToken,
|
|
156
|
+
SyntaxKind.BarBarEqualsToken,
|
|
157
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
158
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
159
|
+
SyntaxKind.CaretEqualsToken,
|
|
160
|
+
]);
|
|
161
|
+
|
|
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
|
+
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
179
|
+
function isName(node: Node): boolean {
|
|
180
|
+
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The expression one node is, through however many parentheses are written around it. */
|
|
184
|
+
function unparenthesized(node: Node): Node {
|
|
185
|
+
let held = node;
|
|
186
|
+
while (isParenthesizedExpression(held)) {
|
|
187
|
+
held = held.expression;
|
|
188
|
+
}
|
|
189
|
+
return held;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Records the name one store writes into.
|
|
194
|
+
*
|
|
195
|
+
* A property access writes the member it names. An element access writes the collection
|
|
196
|
+
* it stores into, because a collection a module only ever stores into holds nothing
|
|
197
|
+
* anything reads, and the key is no declaration of the target. A destructuring target
|
|
198
|
+
* writes each name it binds, through the spreads and the nested patterns it binds them
|
|
199
|
+
* in.
|
|
200
|
+
*/
|
|
201
|
+
function markWrite(target: Node, writes: Set<number>): void {
|
|
202
|
+
const held = unparenthesized(target);
|
|
203
|
+
if (isName(held)) {
|
|
204
|
+
writes.add(held.pos);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (isPropertyAccessExpression(held)) {
|
|
208
|
+
if (isName(held.name)) {
|
|
209
|
+
writes.add(held.name.pos);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (isElementAccessExpression(held)) {
|
|
214
|
+
markWrite(held.expression, writes);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (isArrayLiteralExpression(held)) {
|
|
218
|
+
for (const element of held.elements) {
|
|
219
|
+
markWrite(element, writes);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (isObjectLiteralExpression(held)) {
|
|
224
|
+
for (const property of held.properties) {
|
|
225
|
+
if (isPropertyAssignment(property)) {
|
|
226
|
+
markWrite(property.initializer, writes);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (isShorthandPropertyAssignment(property)) {
|
|
230
|
+
markWrite(property.name, writes);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (isSpreadAssignment(property)) {
|
|
234
|
+
markWrite(property.expression, writes);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isSpreadElement(held)) {
|
|
240
|
+
markWrite(held.expression, writes);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Records every store one node performs: an assignment, a compound assignment, an
|
|
246
|
+
* increment or a decrement, a `delete`, and an iteration that assigns to a name declared
|
|
247
|
+
* elsewhere. An iteration that declares its own name stores into nothing the inventory
|
|
248
|
+
* holds.
|
|
249
|
+
*/
|
|
250
|
+
function markStores(node: Node, writes: Set<number>): void {
|
|
251
|
+
if (isBinaryExpression(node)) {
|
|
252
|
+
if (ASSIGNMENTS.has(node.operatorToken.kind)) {
|
|
253
|
+
markWrite(node.left, writes);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) {
|
|
258
|
+
if (
|
|
259
|
+
node.operator === SyntaxKind.PlusPlusToken ||
|
|
260
|
+
node.operator === SyntaxKind.MinusMinusToken
|
|
261
|
+
) {
|
|
262
|
+
markWrite(node.operand, writes);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (isDeleteExpression(node)) {
|
|
267
|
+
markWrite(node.expression, writes);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (isForInStatement(node) || isForOfStatement(node)) {
|
|
271
|
+
if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
|
|
272
|
+
markWrite(node.initializer, writes);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** One name node the walk found, and what the walk knows about it. */
|
|
278
|
+
interface Site {
|
|
279
|
+
/** The name node, whose position the reference carries. */
|
|
280
|
+
readonly node: Node;
|
|
281
|
+
/**
|
|
282
|
+
* The shorthand property assignment this name belongs to, where it belongs to one.
|
|
283
|
+
* Its value is the declaration the name reads, while the name itself declares the
|
|
284
|
+
* property the literal carries.
|
|
285
|
+
*/
|
|
286
|
+
readonly shorthand: Node | undefined;
|
|
287
|
+
readonly from: string;
|
|
288
|
+
readonly use: Use;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Every name node one file holds that is a use rather than a declaration, in source
|
|
293
|
+
* order, each with the declaration that encloses it and the use it makes.
|
|
294
|
+
*
|
|
295
|
+
* One walk answers all three, because the three facts are positional and a second walk
|
|
296
|
+
* would have to agree with this one about which nodes are names. A name that is the
|
|
297
|
+
* declared name of the declaration it belongs to is left out: it is where that
|
|
298
|
+
* declaration is written rather than a use of it, which is what makes a declaration
|
|
299
|
+
* referenced only from its own site an unreferenced declaration.
|
|
300
|
+
*/
|
|
301
|
+
function sitesOf(
|
|
302
|
+
file: SourceFile,
|
|
303
|
+
declarations: ReadonlyMap<string, string>,
|
|
304
|
+
fileId: string,
|
|
305
|
+
): Site[] {
|
|
306
|
+
const found: Site[] = [];
|
|
307
|
+
const writes = new Set<number>();
|
|
308
|
+
const declared = new Set<number>();
|
|
309
|
+
const shorthands = new Map<number, Node>();
|
|
310
|
+
let enclosing = fileId;
|
|
311
|
+
|
|
312
|
+
const markNames = (node: Node): void => {
|
|
313
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
314
|
+
shorthands.set(node.name.pos, node);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// A property access carries the member it names where a declaration carries its
|
|
318
|
+
// declared name, and it is the one form of the grammar that does.
|
|
319
|
+
if (isPropertyAccessExpression(node)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const named = node as { readonly name?: Node; readonly propertyName?: Node };
|
|
323
|
+
for (const name of [named.name, named.propertyName]) {
|
|
324
|
+
if (name !== undefined && isName(name)) {
|
|
325
|
+
declared.add(name.pos);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const visit = (node: Node): void => {
|
|
331
|
+
if (isName(node)) {
|
|
332
|
+
if (!declared.has(node.pos)) {
|
|
333
|
+
found.push({
|
|
334
|
+
node,
|
|
335
|
+
shorthand: shorthands.get(node.pos),
|
|
336
|
+
from: enclosing,
|
|
337
|
+
use: writes.has(node.pos) ? "write" : "read",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
markNames(node);
|
|
343
|
+
markStores(node, writes);
|
|
344
|
+
const held = declarations.get(nodeKey(file, node));
|
|
345
|
+
const outer = enclosing;
|
|
346
|
+
if (held !== undefined) {
|
|
347
|
+
enclosing = held;
|
|
348
|
+
}
|
|
349
|
+
node.forEachChild(visit);
|
|
350
|
+
enclosing = outer;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
file.forEachChild(visit);
|
|
354
|
+
return found;
|
|
355
|
+
}
|
|
356
|
+
|
|
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
|
+
/**
|
|
416
|
+
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
417
|
+
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
418
|
+
* its pattern in the configured list.
|
|
419
|
+
*/
|
|
420
|
+
function patternRule(index: number): string {
|
|
421
|
+
return `test-file-pattern-${String(index + 1)}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
425
|
+
function compare(a: string, b: string): number {
|
|
426
|
+
if (a === b) {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
return a < b ? -1 : 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** One declaration one symbol reaches, and whether a step along an alias reached it. */
|
|
433
|
+
interface Target {
|
|
434
|
+
readonly id: string;
|
|
435
|
+
readonly stepped: boolean;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Every reference one project's own files make to the project's own declarations, in
|
|
440
|
+
* position order, and the test-file rules that classified those files.
|
|
441
|
+
*
|
|
442
|
+
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
443
|
+
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
444
|
+
* against, which is the root the inventory was read against.
|
|
445
|
+
*
|
|
446
|
+
* The order is by the referencing position, then by the declaration named, the
|
|
447
|
+
* declaration referencing it and the use. That is a total order because one name
|
|
448
|
+
* references one declaration once from one declaration, while a chain of aliases reaches
|
|
449
|
+
* several declarations at one position.
|
|
450
|
+
*/
|
|
451
|
+
export function references<Brand>(
|
|
452
|
+
project: ProjectView<Brand>,
|
|
453
|
+
held: Inventory,
|
|
454
|
+
targetRoot: string,
|
|
455
|
+
options: ReferenceOptions,
|
|
456
|
+
): References {
|
|
457
|
+
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
458
|
+
const declarations = held.declarations;
|
|
459
|
+
const patterns = options.testFiles.map(expressionOf);
|
|
460
|
+
const matched = options.testFiles.map(() => 0);
|
|
461
|
+
const found: Reference[] = [];
|
|
462
|
+
const reached = new Map<number, readonly Target[]>();
|
|
463
|
+
let batched = 0;
|
|
464
|
+
let fileBatches = 0;
|
|
465
|
+
let residueFallbacks = 0;
|
|
466
|
+
let shorthandLookups = 0;
|
|
467
|
+
let aliasSteps = 0;
|
|
468
|
+
|
|
469
|
+
const files = project.ownSourceFiles();
|
|
470
|
+
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
471
|
+
|
|
472
|
+
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
473
|
+
const declaredAt = (node: Node): string | undefined =>
|
|
474
|
+
declarations.get(nodeKey(node.getSourceFile(), node));
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The declarations one symbol names: the declaration it is, and every link of the
|
|
478
|
+
* alias chain it stands at the head of. A re-export something imports is used and so
|
|
479
|
+
* is the declaration behind it, so both are named; a link this project does not
|
|
480
|
+
* declare is stepped over rather than recorded.
|
|
481
|
+
*
|
|
482
|
+
* A declaration handle carries the file it is in and the syntax it is written as, so
|
|
483
|
+
* both questions the walk asks of a declaration are answered before it is resolved: a
|
|
484
|
+
* declaration outside the project's own files belongs to another program and is never
|
|
485
|
+
* read, which is what keeps the pass from fetching a library file to discover that the
|
|
486
|
+
* inventory does not hold what is in it.
|
|
487
|
+
*/
|
|
488
|
+
const targetsOf = (symbol: TSSymbol): readonly Target[] => {
|
|
489
|
+
const cached = reached.get(symbol.id);
|
|
490
|
+
if (cached !== undefined) {
|
|
491
|
+
return cached;
|
|
492
|
+
}
|
|
493
|
+
const targets: Target[] = [];
|
|
494
|
+
const walked = new Set<number>();
|
|
495
|
+
let at: TSSymbol | undefined = symbol;
|
|
496
|
+
let stepped = false;
|
|
497
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
498
|
+
walked.add(at.id);
|
|
499
|
+
let alias = false;
|
|
500
|
+
for (const handle of at.declarations) {
|
|
501
|
+
alias = alias || ALIASES.has(handle.kind);
|
|
502
|
+
if (!ownFiles.has(handle.path)) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const node = project.declarationAt(handle)?.node;
|
|
506
|
+
const id = node === undefined ? undefined : declaredAt(node);
|
|
507
|
+
if (id !== undefined && !targets.some((target) => target.id === id)) {
|
|
508
|
+
targets.push({ id, stepped });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!alias) {
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
at = project.aliasStepOf(at);
|
|
515
|
+
aliasSteps += 1;
|
|
516
|
+
stepped = true;
|
|
517
|
+
}
|
|
518
|
+
reached.set(symbol.id, targets);
|
|
519
|
+
return targets;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
523
|
+
const symbolFor = (site: Site, answered: ReadonlyMap<number, TSSymbol>): TSSymbol | undefined => {
|
|
524
|
+
if (site.shorthand !== undefined) {
|
|
525
|
+
shorthandLookups += 1;
|
|
526
|
+
return project.shorthandValueAt(project.handle(site.shorthand));
|
|
527
|
+
}
|
|
528
|
+
const batched = answered.get(site.node.pos);
|
|
529
|
+
if (batched !== undefined) {
|
|
530
|
+
return batched;
|
|
531
|
+
}
|
|
532
|
+
residueFallbacks += 1;
|
|
533
|
+
return project.resolvedSymbolAt(project.handle(site.node));
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
for (const file of files) {
|
|
537
|
+
const path = renderPosition(file, targetRoot, 0).path;
|
|
538
|
+
let test = false;
|
|
539
|
+
patterns.forEach((pattern, index) => {
|
|
540
|
+
if (pattern.test(path)) {
|
|
541
|
+
matched[index] = (matched[index] ?? 0) + 1;
|
|
542
|
+
test = true;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
const sites = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
546
|
+
|
|
547
|
+
// One batch per capped run of the file's name nodes. A shorthand is left out of
|
|
548
|
+
// the batch: its name resolves to the property the literal declares, so the batch's
|
|
549
|
+
// answer for it would name something this project's inventory never holds.
|
|
550
|
+
const batching = sites.filter((site) => site.shorthand === undefined);
|
|
551
|
+
const answered = new Map<number, TSSymbol>();
|
|
552
|
+
batched += batching.length;
|
|
553
|
+
for (let from = 0; from < batching.length; from += cap) {
|
|
554
|
+
const run = batching.slice(from, from + cap);
|
|
555
|
+
const answers = project.symbolsAt(run.map((site) => project.handle(site.node)));
|
|
556
|
+
fileBatches += 1;
|
|
557
|
+
run.forEach((site, index) => {
|
|
558
|
+
const symbol = answers[index];
|
|
559
|
+
if (symbol !== undefined) {
|
|
560
|
+
answered.set(site.node.pos, symbol);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (const site of sites) {
|
|
566
|
+
const direct: Resolution =
|
|
567
|
+
site.shorthand !== undefined
|
|
568
|
+
? "shorthand"
|
|
569
|
+
: answered.has(site.node.pos)
|
|
570
|
+
? "batch"
|
|
571
|
+
: "resolved-symbol";
|
|
572
|
+
const symbol = symbolFor(site, answered);
|
|
573
|
+
if (symbol === undefined) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const position = renderPosition(file, targetRoot, site.node.getStart());
|
|
577
|
+
for (const target of targetsOf(symbol)) {
|
|
578
|
+
found.push({
|
|
579
|
+
from: site.from,
|
|
580
|
+
to: target.id,
|
|
581
|
+
position,
|
|
582
|
+
use: site.use,
|
|
583
|
+
resolution: target.stepped ? "alias" : direct,
|
|
584
|
+
test,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
found.sort(
|
|
591
|
+
(a, b) =>
|
|
592
|
+
byPosition(a.position, b.position) ||
|
|
593
|
+
compare(a.to, b.to) ||
|
|
594
|
+
compare(a.from, b.from) ||
|
|
595
|
+
compare(a.use, b.use),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
configFile: project.configFile,
|
|
600
|
+
references: found,
|
|
601
|
+
testFileRules: options.testFiles
|
|
602
|
+
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
603
|
+
.sort((a, b) => compare(a.rule, b.rule)),
|
|
604
|
+
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
|
|
605
|
+
};
|
|
606
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { printConfig } from "./print.ts";
|
|
|
11
11
|
import { resolve } from "./resolve.ts";
|
|
12
12
|
import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
|
|
13
13
|
import { diagnosticsOf, openEngine, runSession, type Engine } from "./session.ts";
|
|
14
|
-
import { CONTRACT_VERSION
|
|
14
|
+
import { CONTRACT_VERSION } from "./version.ts";
|
|
15
15
|
|
|
16
16
|
/** One output stream of the command line. `process.stdout` and `process.stderr` satisfy it. */
|
|
17
17
|
export interface Writer {
|
|
@@ -186,8 +186,8 @@ function scopeOf(host: Host, options: Options): Scope {
|
|
|
186
186
|
* An option asking for a source edit is refused before the verb is read: this
|
|
187
187
|
* analyzer reports and never edits a source file.
|
|
188
188
|
*
|
|
189
|
-
* `host` is the
|
|
190
|
-
*
|
|
189
|
+
* `host` is the platform the run reads, the version it reports included;
|
|
190
|
+
* `openClient` opens the compiler client a verb that reads projects needs.
|
|
191
191
|
* Both are parameters because both are the platform, which no module below this one
|
|
192
192
|
* names: the command entry binds them, and a caller that wants to watch what a run
|
|
193
193
|
* reads supplies its own.
|
|
@@ -210,7 +210,7 @@ export function run(
|
|
|
210
210
|
|
|
211
211
|
const verb = args[0];
|
|
212
212
|
if (verb === "version") {
|
|
213
|
-
out.write(`deadset-ts ${
|
|
213
|
+
out.write(`deadset-ts ${host.analyzerVersion()}\ncontract ${CONTRACT_VERSION}\n`);
|
|
214
214
|
return EXIT_CLEAN;
|
|
215
215
|
}
|
|
216
216
|
if (verb === "print-config") {
|