@cplieger/deadset-ts 1.1.0 → 2.0.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.
@@ -0,0 +1,420 @@
1
+ import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
2
+ import { nodeKey } from "./inventory.js";
3
+ import { byPosition, renderPosition } from "./position.js";
4
+ /**
5
+ * The batch cap a run takes when the configuration names none. It bounds the size of
6
+ * one answer rather than the work: the median file is one batch at this cap and the
7
+ * largest production file a handful.
8
+ */
9
+ export const DEFAULT_BATCH_CAP = 4096;
10
+ /**
11
+ * The operators whose left side one assignment stores into. A compound assignment is
12
+ * among them: it reads its target only to write the result back, so nothing else
13
+ * receives what it read, and a declaration whose every other reference is one of these
14
+ * carries no information out of itself.
15
+ */
16
+ const ASSIGNMENTS = new Set([
17
+ SyntaxKind.EqualsToken,
18
+ SyntaxKind.PlusEqualsToken,
19
+ SyntaxKind.MinusEqualsToken,
20
+ SyntaxKind.AsteriskEqualsToken,
21
+ SyntaxKind.AsteriskAsteriskEqualsToken,
22
+ SyntaxKind.SlashEqualsToken,
23
+ SyntaxKind.PercentEqualsToken,
24
+ SyntaxKind.LessThanLessThanEqualsToken,
25
+ SyntaxKind.GreaterThanGreaterThanEqualsToken,
26
+ SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
27
+ SyntaxKind.AmpersandEqualsToken,
28
+ SyntaxKind.BarEqualsToken,
29
+ SyntaxKind.BarBarEqualsToken,
30
+ SyntaxKind.AmpersandAmpersandEqualsToken,
31
+ SyntaxKind.QuestionQuestionEqualsToken,
32
+ SyntaxKind.CaretEqualsToken,
33
+ ]);
34
+ /**
35
+ * The declaration forms that name something declared elsewhere. Each is a link rather
36
+ * than a declaration of what it names, so a reference through one names the next link
37
+ * and, at the end of the chain, the declaration itself: a re-export something imports
38
+ * is used, and so is the declaration behind it.
39
+ */
40
+ const ALIASES = new Set([
41
+ SyntaxKind.ImportClause,
42
+ SyntaxKind.ImportEqualsDeclaration,
43
+ SyntaxKind.ImportSpecifier,
44
+ SyntaxKind.NamespaceImport,
45
+ SyntaxKind.ExportAssignment,
46
+ SyntaxKind.ExportSpecifier,
47
+ SyntaxKind.NamespaceExport,
48
+ ]);
49
+ /** Whether one node is an identifier or a private name, the two forms a name takes. */
50
+ function isName(node) {
51
+ return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
52
+ }
53
+ /** The expression one node is, through however many parentheses are written around it. */
54
+ function unparenthesized(node) {
55
+ let held = node;
56
+ while (isParenthesizedExpression(held)) {
57
+ held = held.expression;
58
+ }
59
+ return held;
60
+ }
61
+ /**
62
+ * Records the name one store writes into.
63
+ *
64
+ * A property access writes the member it names. An element access writes the collection
65
+ * it stores into, because a collection a module only ever stores into holds nothing
66
+ * anything reads, and the key is no declaration of the target. A destructuring target
67
+ * writes each name it binds, through the spreads and the nested patterns it binds them
68
+ * in.
69
+ */
70
+ function markWrite(target, writes) {
71
+ const held = unparenthesized(target);
72
+ if (isName(held)) {
73
+ writes.add(held.pos);
74
+ return;
75
+ }
76
+ if (isPropertyAccessExpression(held)) {
77
+ if (isName(held.name)) {
78
+ writes.add(held.name.pos);
79
+ }
80
+ return;
81
+ }
82
+ if (isElementAccessExpression(held)) {
83
+ markWrite(held.expression, writes);
84
+ return;
85
+ }
86
+ if (isArrayLiteralExpression(held)) {
87
+ for (const element of held.elements) {
88
+ markWrite(element, writes);
89
+ }
90
+ return;
91
+ }
92
+ if (isObjectLiteralExpression(held)) {
93
+ for (const property of held.properties) {
94
+ if (isPropertyAssignment(property)) {
95
+ markWrite(property.initializer, writes);
96
+ continue;
97
+ }
98
+ if (isShorthandPropertyAssignment(property)) {
99
+ markWrite(property.name, writes);
100
+ continue;
101
+ }
102
+ if (isSpreadAssignment(property)) {
103
+ markWrite(property.expression, writes);
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ if (isSpreadElement(held)) {
109
+ markWrite(held.expression, writes);
110
+ }
111
+ }
112
+ /**
113
+ * Records every store one node performs: an assignment, a compound assignment, an
114
+ * increment or a decrement, a `delete`, and an iteration that assigns to a name declared
115
+ * elsewhere. An iteration that declares its own name stores into nothing the inventory
116
+ * holds.
117
+ */
118
+ function markStores(node, writes) {
119
+ if (isBinaryExpression(node)) {
120
+ if (ASSIGNMENTS.has(node.operatorToken.kind)) {
121
+ markWrite(node.left, writes);
122
+ }
123
+ return;
124
+ }
125
+ if (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) {
126
+ if (node.operator === SyntaxKind.PlusPlusToken ||
127
+ node.operator === SyntaxKind.MinusMinusToken) {
128
+ markWrite(node.operand, writes);
129
+ }
130
+ return;
131
+ }
132
+ if (isDeleteExpression(node)) {
133
+ markWrite(node.expression, writes);
134
+ return;
135
+ }
136
+ if (isForInStatement(node) || isForOfStatement(node)) {
137
+ if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
138
+ markWrite(node.initializer, writes);
139
+ }
140
+ }
141
+ }
142
+ /**
143
+ * Every name node one file holds that is a use rather than a declaration, in source
144
+ * order, each with the declaration that encloses it and the use it makes.
145
+ *
146
+ * One walk answers all three, because the three facts are positional and a second walk
147
+ * would have to agree with this one about which nodes are names. A name that is the
148
+ * declared name of the declaration it belongs to is left out: it is where that
149
+ * declaration is written rather than a use of it, which is what makes a declaration
150
+ * referenced only from its own site an unreferenced declaration.
151
+ */
152
+ function sitesOf(file, declarations, fileId) {
153
+ const found = [];
154
+ const writes = new Set();
155
+ const declared = new Set();
156
+ const shorthands = new Map();
157
+ let enclosing = fileId;
158
+ const markNames = (node) => {
159
+ if (isShorthandPropertyAssignment(node)) {
160
+ shorthands.set(node.name.pos, node);
161
+ return;
162
+ }
163
+ // A property access carries the member it names where a declaration carries its
164
+ // declared name, and it is the one form of the grammar that does.
165
+ if (isPropertyAccessExpression(node)) {
166
+ return;
167
+ }
168
+ const named = node;
169
+ for (const name of [named.name, named.propertyName]) {
170
+ if (name !== undefined && isName(name)) {
171
+ declared.add(name.pos);
172
+ }
173
+ }
174
+ };
175
+ const visit = (node) => {
176
+ if (isName(node)) {
177
+ if (!declared.has(node.pos)) {
178
+ found.push({
179
+ node,
180
+ shorthand: shorthands.get(node.pos),
181
+ from: enclosing,
182
+ use: writes.has(node.pos) ? "write" : "read",
183
+ });
184
+ }
185
+ return;
186
+ }
187
+ markNames(node);
188
+ markStores(node, writes);
189
+ const held = declarations.get(nodeKey(file, node));
190
+ const outer = enclosing;
191
+ if (held !== undefined) {
192
+ enclosing = held;
193
+ }
194
+ node.forEachChild(visit);
195
+ enclosing = outer;
196
+ };
197
+ file.forEachChild(visit);
198
+ return found;
199
+ }
200
+ /**
201
+ * One literal run of a pattern, with every character the expression syntax claims
202
+ * escaped.
203
+ *
204
+ * The set is the syntax characters and the solidus, and it is closed: an escape of any
205
+ * other character is refused outright by the expression syntax under the unicode flag,
206
+ * so escaping a hyphen, which needs none outside a character class, would refuse every
207
+ * pattern that names a hyphenated path.
208
+ */
209
+ function quoted(text) {
210
+ return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
211
+ }
212
+ /**
213
+ * The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
214
+ * do not, a brace list is an alternation of its members, and every other character
215
+ * stands for itself.
216
+ */
217
+ function expressionOf(pattern) {
218
+ let source = "^";
219
+ let at = 0;
220
+ while (at < pattern.length) {
221
+ const rest = pattern.slice(at);
222
+ if (rest.startsWith("**/")) {
223
+ source += "(?:[^/]*/)*";
224
+ at += 3;
225
+ continue;
226
+ }
227
+ if (rest.startsWith("**")) {
228
+ source += ".*";
229
+ at += 2;
230
+ continue;
231
+ }
232
+ const character = pattern[at] ?? "";
233
+ at += 1;
234
+ if (character === "*") {
235
+ source += "[^/]*";
236
+ continue;
237
+ }
238
+ if (character === "?") {
239
+ source += "[^/]";
240
+ continue;
241
+ }
242
+ if (character === "{") {
243
+ const close = pattern.indexOf("}", at);
244
+ if (close < 0) {
245
+ source += "\\{";
246
+ continue;
247
+ }
248
+ source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
249
+ at = close + 1;
250
+ continue;
251
+ }
252
+ source += quoted(character);
253
+ }
254
+ return new RegExp(`${source}$`, "u");
255
+ }
256
+ /**
257
+ * The name one test-file rule takes. A rule's name is a word list the report's own
258
+ * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
259
+ * its pattern in the configured list.
260
+ */
261
+ function patternRule(index) {
262
+ return `test-file-pattern-${String(index + 1)}`;
263
+ }
264
+ /** Two strings ordered bytewise, which is the order every set of a run is read in. */
265
+ function compare(a, b) {
266
+ if (a === b) {
267
+ return 0;
268
+ }
269
+ return a < b ? -1 : 1;
270
+ }
271
+ /**
272
+ * Every reference one project's own files make to the project's own declarations, in
273
+ * position order, and the test-file rules that classified those files.
274
+ *
275
+ * `held` is the inventory of the same project, whose declaration map is what a resolved
276
+ * symbol is looked up in; `targetRoot` is the absolute path every position is rendered
277
+ * against, which is the root the inventory was read against.
278
+ *
279
+ * The order is by the referencing position, then by the declaration named, the
280
+ * declaration referencing it and the use. That is a total order because one name
281
+ * references one declaration once from one declaration, while a chain of aliases reaches
282
+ * several declarations at one position.
283
+ */
284
+ export function references(project, held, targetRoot, options) {
285
+ const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
286
+ const declarations = held.declarations;
287
+ const patterns = options.testFiles.map(expressionOf);
288
+ const matched = options.testFiles.map(() => 0);
289
+ const found = [];
290
+ const reached = new Map();
291
+ let batched = 0;
292
+ let fileBatches = 0;
293
+ let residueFallbacks = 0;
294
+ let shorthandLookups = 0;
295
+ let aliasSteps = 0;
296
+ const files = project.ownSourceFiles();
297
+ const ownFiles = new Set(files.map((file) => file.fileName));
298
+ /** The identifier of the declaration one node of this program is, where it is one. */
299
+ const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
300
+ /**
301
+ * The declarations one symbol names: the declaration it is, and every link of the
302
+ * alias chain it stands at the head of. A re-export something imports is used and so
303
+ * is the declaration behind it, so both are named; a link this project does not
304
+ * declare is stepped over rather than recorded.
305
+ *
306
+ * A declaration handle carries the file it is in and the syntax it is written as, so
307
+ * both questions the walk asks of a declaration are answered before it is resolved: a
308
+ * declaration outside the project's own files belongs to another program and is never
309
+ * read, which is what keeps the pass from fetching a library file to discover that the
310
+ * inventory does not hold what is in it.
311
+ */
312
+ const targetsOf = (symbol) => {
313
+ const cached = reached.get(symbol.id);
314
+ if (cached !== undefined) {
315
+ return cached;
316
+ }
317
+ const targets = [];
318
+ const walked = new Set();
319
+ let at = symbol;
320
+ let stepped = false;
321
+ while (at !== undefined && !walked.has(at.id)) {
322
+ walked.add(at.id);
323
+ let alias = false;
324
+ for (const handle of at.declarations) {
325
+ alias = alias || ALIASES.has(handle.kind);
326
+ if (!ownFiles.has(handle.path)) {
327
+ continue;
328
+ }
329
+ const node = project.declarationAt(handle)?.node;
330
+ const id = node === undefined ? undefined : declaredAt(node);
331
+ if (id !== undefined && !targets.some((target) => target.id === id)) {
332
+ targets.push({ id, stepped });
333
+ }
334
+ }
335
+ if (!alias) {
336
+ break;
337
+ }
338
+ at = project.aliasStepOf(at);
339
+ aliasSteps += 1;
340
+ stepped = true;
341
+ }
342
+ reached.set(symbol.id, targets);
343
+ return targets;
344
+ };
345
+ /** The symbol one site resolves to, asking only the accessor the site's form names. */
346
+ const symbolFor = (site, answered) => {
347
+ if (site.shorthand !== undefined) {
348
+ shorthandLookups += 1;
349
+ return project.shorthandValueAt(project.handle(site.shorthand));
350
+ }
351
+ const batched = answered.get(site.node.pos);
352
+ if (batched !== undefined) {
353
+ return batched;
354
+ }
355
+ residueFallbacks += 1;
356
+ return project.resolvedSymbolAt(project.handle(site.node));
357
+ };
358
+ for (const file of files) {
359
+ const path = renderPosition(file, targetRoot, 0).path;
360
+ let test = false;
361
+ patterns.forEach((pattern, index) => {
362
+ if (pattern.test(path)) {
363
+ matched[index] = (matched[index] ?? 0) + 1;
364
+ test = true;
365
+ }
366
+ });
367
+ const sites = sitesOf(file, declarations, declaredAt(file) ?? "");
368
+ // One batch per capped run of the file's name nodes. A shorthand is left out of
369
+ // the batch: its name resolves to the property the literal declares, so the batch's
370
+ // answer for it would name something this project's inventory never holds.
371
+ const batching = sites.filter((site) => site.shorthand === undefined);
372
+ const answered = new Map();
373
+ batched += batching.length;
374
+ for (let from = 0; from < batching.length; from += cap) {
375
+ const run = batching.slice(from, from + cap);
376
+ const answers = project.symbolsAt(run.map((site) => project.handle(site.node)));
377
+ fileBatches += 1;
378
+ run.forEach((site, index) => {
379
+ const symbol = answers[index];
380
+ if (symbol !== undefined) {
381
+ answered.set(site.node.pos, symbol);
382
+ }
383
+ });
384
+ }
385
+ for (const site of sites) {
386
+ const direct = site.shorthand !== undefined
387
+ ? "shorthand"
388
+ : answered.has(site.node.pos)
389
+ ? "batch"
390
+ : "resolved-symbol";
391
+ const symbol = symbolFor(site, answered);
392
+ if (symbol === undefined) {
393
+ continue;
394
+ }
395
+ const position = renderPosition(file, targetRoot, site.node.getStart());
396
+ for (const target of targetsOf(symbol)) {
397
+ found.push({
398
+ from: site.from,
399
+ to: target.id,
400
+ position,
401
+ use: site.use,
402
+ resolution: target.stepped ? "alias" : direct,
403
+ test,
404
+ });
405
+ }
406
+ }
407
+ }
408
+ found.sort((a, b) => byPosition(a.position, b.position) ||
409
+ compare(a.to, b.to) ||
410
+ compare(a.from, b.from) ||
411
+ compare(a.use, b.use));
412
+ return {
413
+ configFile: project.configFile,
414
+ references: found,
415
+ testFileRules: options.testFiles
416
+ .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
417
+ .sort((a, b) => compare(a.rule, b.rule)),
418
+ cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
419
+ };
420
+ }