@cplieger/deadset-ts 5.0.0 → 5.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/dist/src/analysis.js +77 -10
- package/dist/src/configuration-files.js +107 -3
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +21 -17
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +4 -1
- package/dist/src/kinds.js +4 -3
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +199 -10
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +1 -1
- package/dist/src/stores.js +3 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +92 -13
- package/src/configuration-files.ts +136 -1
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +48 -17
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +4 -1
- package/src/kinds.ts +5 -3
- package/src/query.ts +26 -0
- package/src/references.ts +254 -21
- package/src/resolve.ts +14 -13
- package/src/roots.ts +11 -1
- package/src/stores.ts +9 -0
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/dist/src/analysis.js
CHANGED
|
@@ -14,10 +14,12 @@ import { decorator } from "./decorator.js";
|
|
|
14
14
|
import { dependenciesOf, projectNeeds } from "./dependencies.js";
|
|
15
15
|
import { deprecatedDeclarations } from "./deprecation.js";
|
|
16
16
|
import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "./discover.js";
|
|
17
|
-
import { conventionFailuresFor, readConventions, } from "./conventions.js";
|
|
17
|
+
import { conventionFailuresFor, generatedFiles, readConventions, } from "./conventions.js";
|
|
18
18
|
import { readEdgeSides } from "./edges.js";
|
|
19
19
|
import { enumGroup } from "./enum-group.js";
|
|
20
|
-
import {
|
|
20
|
+
import { generatedFile } from "./generated-file.js";
|
|
21
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
22
|
+
import { detectExemptions, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
|
|
21
23
|
import { heldByInclusion, sourceTree } from "./file-facts.js";
|
|
22
24
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
23
25
|
import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
|
|
@@ -110,6 +112,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
110
112
|
entryFiles: config.ts.entryFiles,
|
|
111
113
|
conventions,
|
|
112
114
|
testFiles: config.ts.testFiles,
|
|
115
|
+
parse: (fileName, text) => engine.parseSourceFile(fileName, text),
|
|
113
116
|
publishedAPI: config.targetKind === "library",
|
|
114
117
|
};
|
|
115
118
|
const notBuilt = [...discovered.notBuilt];
|
|
@@ -225,6 +228,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
225
228
|
.ownSourceFiles()
|
|
226
229
|
.filter((file) => referenceOnly(file.fileName))
|
|
227
230
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
231
|
+
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
228
232
|
};
|
|
229
233
|
};
|
|
230
234
|
return stage(project, { configuration, read });
|
|
@@ -321,6 +325,7 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
321
325
|
const DETECTORS = new Map([
|
|
322
326
|
["interface-satisfaction", interfaceSatisfaction],
|
|
323
327
|
["enum-group", enumGroup],
|
|
328
|
+
["generated-file", generatedFile],
|
|
324
329
|
["template-field", templateField],
|
|
325
330
|
["reflective-lookup", reflectiveLookup],
|
|
326
331
|
["decorator", decorator],
|
|
@@ -347,11 +352,27 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
347
352
|
testFiles: config.ts.testFiles,
|
|
348
353
|
});
|
|
349
354
|
const support = testSupportFiles(projectRead.held.symbols, resolved.references, projectRead.rooted.liveUnderReachability, resolved.testFilePaths);
|
|
355
|
+
const detected = detectExemptions({
|
|
356
|
+
project,
|
|
357
|
+
held: projectRead.held,
|
|
358
|
+
targetRoot,
|
|
359
|
+
templates,
|
|
360
|
+
ts: config.ts,
|
|
361
|
+
consumers,
|
|
362
|
+
...(config.analysis.generatedFiles === "include"
|
|
363
|
+
? {}
|
|
364
|
+
: { generated: projectRead.generated }),
|
|
365
|
+
}, detectors, {
|
|
366
|
+
disabled,
|
|
367
|
+
mode,
|
|
368
|
+
testFiles: new Set([...resolved.testFilePaths, ...support]),
|
|
369
|
+
});
|
|
370
|
+
const exempt = detected.records;
|
|
350
371
|
const made = supportReferences(resolved.references, support);
|
|
351
372
|
const configured = {
|
|
352
373
|
configuration: projectRead.configuration,
|
|
353
374
|
symbols: projectRead.held.symbols,
|
|
354
|
-
references: made,
|
|
375
|
+
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
355
376
|
roots: projectRead.rooted.liveUnderReachability,
|
|
356
377
|
testFiles: resolved.testFilePaths,
|
|
357
378
|
...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
|
|
@@ -359,11 +380,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
359
380
|
? {}
|
|
360
381
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
361
382
|
};
|
|
362
|
-
const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
|
|
363
|
-
disabled,
|
|
364
|
-
mode,
|
|
365
|
-
testFiles: new Set([...resolved.testFilePaths, ...support]),
|
|
366
|
-
});
|
|
367
383
|
const unanswered = [
|
|
368
384
|
...projectRead.held.unanswered,
|
|
369
385
|
...projectRead.rooted.unanswered,
|
|
@@ -440,6 +456,55 @@ function holdingSweep(read, input) {
|
|
|
440
456
|
live = next;
|
|
441
457
|
}
|
|
442
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* The test references exemption evidence written in a test file makes: each is a read of
|
|
461
|
+
* the declaration it would retain, made by the test file at the evidence's site.
|
|
462
|
+
*/
|
|
463
|
+
function evidenceReferences(symbols, inTestFiles) {
|
|
464
|
+
if (inTestFiles.length === 0) {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
const files = new Map(symbols
|
|
468
|
+
.filter((symbol) => symbol.kind === "file")
|
|
469
|
+
.map((symbol) => [symbol.position.path, symbol.id]));
|
|
470
|
+
return inTestFiles.flatMap((record) => {
|
|
471
|
+
const from = files.get(record.site.path);
|
|
472
|
+
return from === undefined
|
|
473
|
+
? []
|
|
474
|
+
: [
|
|
475
|
+
{
|
|
476
|
+
from,
|
|
477
|
+
to: record.id,
|
|
478
|
+
position: record.site,
|
|
479
|
+
use: "read",
|
|
480
|
+
resolution: "syntax",
|
|
481
|
+
test: true,
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
488
|
+
* reference counting as a read: what the write-only kind would report but for a record.
|
|
489
|
+
*/
|
|
490
|
+
function writtenOnly(read, mode) {
|
|
491
|
+
const written = new Set();
|
|
492
|
+
const readSome = new Set();
|
|
493
|
+
for (const { configured } of read.projects) {
|
|
494
|
+
for (const reference of configured.references) {
|
|
495
|
+
if (mode.production && reference.test) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (reference.use === "write" && reference.consumer === undefined) {
|
|
499
|
+
written.add(reference.to);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
readSome.add(reference.to);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return new Set([...written].filter((id) => !readSome.has(id)));
|
|
507
|
+
}
|
|
443
508
|
/**
|
|
444
509
|
* The sweep of a run read once, under one set of marks. Where a question went unanswered,
|
|
445
510
|
* the run is swept once more without what that question could have kept live and without
|
|
@@ -483,7 +548,7 @@ function sweptOf(read, input) {
|
|
|
483
548
|
matrix: read.matrix,
|
|
484
549
|
sweep: result,
|
|
485
550
|
exempt,
|
|
486
|
-
retained: retainedIn(read.matrix, swept),
|
|
551
|
+
retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
|
|
487
552
|
notBuilt: read.notBuilt,
|
|
488
553
|
unanswered: read.unanswered,
|
|
489
554
|
heldByUnanswered: [...held],
|
|
@@ -510,9 +575,10 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
510
575
|
const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
|
|
511
576
|
deprecation: deprecatedDeclarations(project, read.held),
|
|
512
577
|
accessors: accessorsOf(project, read.held),
|
|
513
|
-
needs: projectNeeds(project, read.held, host, targetRoot),
|
|
578
|
+
needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) => engine.parseSourceFile(fileName, text)),
|
|
514
579
|
implementations: implementations(project, read.held),
|
|
515
580
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
581
|
+
generated: [...read.generated.keys()],
|
|
516
582
|
rooted: read.rooted,
|
|
517
583
|
directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
|
|
518
584
|
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
@@ -546,6 +612,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
546
612
|
files: {
|
|
547
613
|
tree: sourceTree(host, targetRoot),
|
|
548
614
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
615
|
+
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
549
616
|
},
|
|
550
617
|
boundary: {
|
|
551
618
|
consumers: {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* every string either holds as it likes, so each one that spells a declared dependency
|
|
7
7
|
* uses it and each relative one that names a source file of the program roots the file.
|
|
8
8
|
*/
|
|
9
|
-
import { isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { isExportDeclaration, isImportDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
10
10
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
11
11
|
const MANIFEST = "package.json";
|
|
12
12
|
/** A configuration module's name, each form with the spelling a root reports it by. */
|
|
@@ -29,6 +29,15 @@ const DEPENDENCY_MEMBERS = [
|
|
|
29
29
|
];
|
|
30
30
|
/** The prefixes a string names a file with, relative to the file that writes it. */
|
|
31
31
|
const RELATIVE_PREFIXES = ["./", "../"];
|
|
32
|
+
/** The JavaScript extensions a relative specifier from a configuration file stands for TypeScript under. */
|
|
33
|
+
const COUNTERPARTS = [
|
|
34
|
+
[".js", [".ts", ".tsx"]],
|
|
35
|
+
[".jsx", [".tsx", ".ts"]],
|
|
36
|
+
[".mjs", [".mts"]],
|
|
37
|
+
[".cjs", [".cts"]],
|
|
38
|
+
];
|
|
39
|
+
/** The extensions appended to a relative specifier from a configuration file, in order. */
|
|
40
|
+
const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
32
41
|
/** The extensions the program's module resolution appends to a specifier written without one. */
|
|
33
42
|
const RESOLVED_EXTENSIONS = [
|
|
34
43
|
".ts",
|
|
@@ -85,7 +94,7 @@ export function moduleStrings(file) {
|
|
|
85
94
|
* in a directory that holds a manifest. A document that cannot be read or parsed as
|
|
86
95
|
* JSON holds no string.
|
|
87
96
|
*/
|
|
88
|
-
export function configurationFiles(host, targetRoot, files) {
|
|
97
|
+
export function configurationFiles(host, targetRoot, files, parse) {
|
|
89
98
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
90
99
|
const manifests = new Map();
|
|
91
100
|
const holdsManifest = (dir) => {
|
|
@@ -135,7 +144,102 @@ export function configurationFiles(host, targetRoot, files) {
|
|
|
135
144
|
documents.push({ path, strings: documentStrings(value) });
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
modules,
|
|
149
|
+
documents,
|
|
150
|
+
outside: parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** The relative specifiers one file's imports and re-exports name. */
|
|
154
|
+
function relativeSpecifiers(file) {
|
|
155
|
+
return file.statements.flatMap((statement) => {
|
|
156
|
+
const specifier = isImportDeclaration(statement) || isExportDeclaration(statement)
|
|
157
|
+
? statement.moduleSpecifier
|
|
158
|
+
: undefined;
|
|
159
|
+
return specifier !== undefined &&
|
|
160
|
+
isStringLiteral(specifier) &&
|
|
161
|
+
RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
|
|
162
|
+
? [specifier.text]
|
|
163
|
+
: [];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The module one relative specifier from a configuration file reaches: the path it names,
|
|
168
|
+
* that path with its JavaScript extension replaced by the TypeScript counterparts, that
|
|
169
|
+
* path with an extension appended, and a directory's `index` with one appended.
|
|
170
|
+
*/
|
|
171
|
+
function reachedBy(host, dir, specifier) {
|
|
172
|
+
const path = resolvePath(dir, specifier);
|
|
173
|
+
const isFile = (candidate) => host.kindOf(candidate) === "file";
|
|
174
|
+
// A data file such as `./package.json` is no module, so its strings use no dependency.
|
|
175
|
+
if (isFile(path) && APPENDED.some((extension) => path.endsWith(extension))) {
|
|
176
|
+
return path;
|
|
177
|
+
}
|
|
178
|
+
for (const [extension, counterparts] of COUNTERPARTS) {
|
|
179
|
+
if (path.endsWith(extension)) {
|
|
180
|
+
const stem = path.slice(0, -extension.length);
|
|
181
|
+
const found = counterparts.map((one) => stem + one).find(isFile);
|
|
182
|
+
if (found !== undefined) {
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const appended = APPENDED.map((extension) => path + extension).find(isFile);
|
|
188
|
+
if (appended !== undefined) {
|
|
189
|
+
return appended;
|
|
190
|
+
}
|
|
191
|
+
return host.kindOf(path) === "directory"
|
|
192
|
+
? APPENDED.map((extension) => joinPath(path, `index${extension}`)).find(isFile)
|
|
193
|
+
: undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The configuration modules of the directories that hold a manifest which the program
|
|
197
|
+
* does not hold, and the files they import by a relative specifier, until no further
|
|
198
|
+
* file joins. A file that cannot be read is skipped.
|
|
199
|
+
*/
|
|
200
|
+
function outsideModules(host, dirs, files, holdsManifest, parse) {
|
|
201
|
+
const pending = [];
|
|
202
|
+
for (const dir of [...dirs].sort()) {
|
|
203
|
+
if (!holdsManifest(dir)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
let names;
|
|
207
|
+
try {
|
|
208
|
+
names = host
|
|
209
|
+
.readDirectory(dir)
|
|
210
|
+
.filter((entry) => !entry.directory && configurationModuleForm(entry.name) !== undefined)
|
|
211
|
+
.map((entry) => entry.name)
|
|
212
|
+
.sort();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
218
|
+
}
|
|
219
|
+
const found = [];
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
222
|
+
if (seen.has(next.path) || files.byName.has(next.path)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
seen.add(next.path);
|
|
226
|
+
let text;
|
|
227
|
+
try {
|
|
228
|
+
text = host.readFile(next.path);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const file = parse(next.path, text);
|
|
234
|
+
found.push({ file, manifestDir: next.manifestDir });
|
|
235
|
+
for (const specifier of relativeSpecifiers(file)) {
|
|
236
|
+
const reached = reachedBy(host, dirnamePath(next.path), specifier);
|
|
237
|
+
if (reached !== undefined) {
|
|
238
|
+
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return found;
|
|
139
243
|
}
|
|
140
244
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|
|
141
245
|
export function declaredIn(host, dir) {
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "2.
|
|
5
|
+
corpusVersion: "2.2.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:ce7c9087ed7b2baf3de9fa2df436b500706f4f9e94fd9e0fed633054305f06a4",
|
|
8
8
|
},
|
|
9
|
-
gaps: [
|
|
10
|
-
{
|
|
11
|
-
fixture: "private-member-unread",
|
|
12
|
-
capability: "reflective-lookup",
|
|
13
|
-
reason: "The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
|
|
14
|
-
},
|
|
15
|
-
],
|
|
9
|
+
gaps: [],
|
|
16
10
|
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The uses contextual types make of the target's members, which no name node resolves to.
|
|
3
|
+
* An object-literal property writes the member of its name on each constituent of the
|
|
4
|
+
* literal's contextual type that declares one. A value reaching a position typed by another
|
|
5
|
+
* object type reads each of its members that type declares, through array elements and
|
|
6
|
+
* function parameters and results too; a class instance there is the
|
|
7
|
+
* interface-satisfaction class's evidence instead.
|
|
8
|
+
*/
|
|
9
|
+
import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { SignatureKind } from "@typescript/native/unstable/sync";
|
|
11
|
+
import { isAnswered } from "./query.js";
|
|
12
|
+
/** The member kinds a contextual type writes and reads: data members and the members of object types. */
|
|
13
|
+
const MEMBER_KINDS = new Set(["type-member", "class-member"]);
|
|
14
|
+
/** The name one literal property is written under, where the name is literal text. */
|
|
15
|
+
function propertyName(property) {
|
|
16
|
+
if (!isPropertyAssignment(property) &&
|
|
17
|
+
!isShorthandPropertyAssignment(property) &&
|
|
18
|
+
!isMethodDeclaration(property) &&
|
|
19
|
+
!isGetAccessorDeclaration(property) &&
|
|
20
|
+
!isSetAccessorDeclaration(property)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const name = property.name;
|
|
24
|
+
if (isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)) {
|
|
25
|
+
return { node: name, text: name.text };
|
|
26
|
+
}
|
|
27
|
+
if (isComputedPropertyName(name)) {
|
|
28
|
+
const key = name.expression;
|
|
29
|
+
if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
|
|
30
|
+
return { node: name, text: key.text };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/** How many element and signature steps the structural walk takes below a value's own type. */
|
|
36
|
+
const MAX_DEPTH = 3;
|
|
37
|
+
/**
|
|
38
|
+
* The contextual uses of one project's literals and values, asked in batches: one
|
|
39
|
+
* contextual type per literal and per value whose type carries a member of the target,
|
|
40
|
+
* one type per value, and each distinct type's properties once.
|
|
41
|
+
*/
|
|
42
|
+
export function contextualUses(project, held, chains, literals, flows, cap, properties) {
|
|
43
|
+
const queries = project.queries;
|
|
44
|
+
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
45
|
+
let batches = 0;
|
|
46
|
+
let lookups = 0;
|
|
47
|
+
const batchesOf = (count) => Math.ceil(count / cap);
|
|
48
|
+
const parts = new Map();
|
|
49
|
+
/** The object types one type is: itself, or its constituents at any depth. */
|
|
50
|
+
const partsOf = (type) => {
|
|
51
|
+
const known = parts.get(type.id);
|
|
52
|
+
if (known !== undefined) {
|
|
53
|
+
return known;
|
|
54
|
+
}
|
|
55
|
+
let found = [];
|
|
56
|
+
if (type.isUnionType() || type.isIntersectionType()) {
|
|
57
|
+
lookups += 1;
|
|
58
|
+
const constituents = queries.constituents(type);
|
|
59
|
+
found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
|
|
60
|
+
}
|
|
61
|
+
else if (type.isObjectType()) {
|
|
62
|
+
found = [type];
|
|
63
|
+
}
|
|
64
|
+
parts.set(type.id, found);
|
|
65
|
+
return found;
|
|
66
|
+
};
|
|
67
|
+
const tables = new Map();
|
|
68
|
+
/** The members one object type has, by name. */
|
|
69
|
+
const membersOf = (type) => {
|
|
70
|
+
const known = tables.get(type.id);
|
|
71
|
+
if (known !== undefined) {
|
|
72
|
+
return known;
|
|
73
|
+
}
|
|
74
|
+
const table = new Map();
|
|
75
|
+
const read = properties.of(type);
|
|
76
|
+
lookups += read.asked ? 1 : 0;
|
|
77
|
+
for (const property of isAnswered(read.properties) ? read.properties : []) {
|
|
78
|
+
table.set(property.name, {
|
|
79
|
+
ids: chains
|
|
80
|
+
.declarationsOf(property)
|
|
81
|
+
.filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
|
|
82
|
+
handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
tables.set(type.id, table);
|
|
86
|
+
return table;
|
|
87
|
+
};
|
|
88
|
+
const writes = [];
|
|
89
|
+
batches += batchesOf(literals.length);
|
|
90
|
+
const contexts = queries.contextualTypes(literals.map((one) => one.literal), cap);
|
|
91
|
+
literals.forEach(({ literal, from }, index) => {
|
|
92
|
+
const context = contexts[index];
|
|
93
|
+
if (context === undefined || !isAnswered(context)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const tablesOf = partsOf(context).map(membersOf);
|
|
97
|
+
for (const property of literal.properties) {
|
|
98
|
+
const name = propertyName(property);
|
|
99
|
+
if (name === undefined) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
|
|
103
|
+
if (ids.length > 0) {
|
|
104
|
+
writes.push({ at: name.node, from, ids });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const carried = flows.filter((flow) => !isObjectLiteralExpression(flow.value));
|
|
109
|
+
batches += batchesOf(carried.length);
|
|
110
|
+
const types = queries.typesAt(carried.map((flow) => flow.value), cap);
|
|
111
|
+
/** The members a position may read structurally: those of object types, not of classes. */
|
|
112
|
+
const readable = (member) => member.ids.filter((id) => kinds.get(id) === "type-member");
|
|
113
|
+
const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
|
|
114
|
+
const elements = new Map();
|
|
115
|
+
/** The element type of an array type, read once per type. */
|
|
116
|
+
const elementOf = (type) => {
|
|
117
|
+
if (elements.has(type.id)) {
|
|
118
|
+
return elements.get(type.id);
|
|
119
|
+
}
|
|
120
|
+
lookups += 1;
|
|
121
|
+
const isArray = queries.isArray(type);
|
|
122
|
+
let element;
|
|
123
|
+
if (isAnswered(isArray) && isArray) {
|
|
124
|
+
lookups += 1;
|
|
125
|
+
const held = queries.typeArguments(type);
|
|
126
|
+
element = isAnswered(held) ? held[0] : undefined;
|
|
127
|
+
}
|
|
128
|
+
elements.set(type.id, element);
|
|
129
|
+
return element;
|
|
130
|
+
};
|
|
131
|
+
const calls = new Map();
|
|
132
|
+
/** The parameter and return types of a type's one call signature, read once per type. */
|
|
133
|
+
const callOf = (type) => {
|
|
134
|
+
if (calls.has(type.id)) {
|
|
135
|
+
return calls.get(type.id);
|
|
136
|
+
}
|
|
137
|
+
lookups += 1;
|
|
138
|
+
const signatures = queries.signaturesOf(type, SignatureKind.Call);
|
|
139
|
+
let called;
|
|
140
|
+
const [signature, ...more] = isAnswered(signatures) ? signatures : [];
|
|
141
|
+
if (signature !== undefined && more.length === 0) {
|
|
142
|
+
const parameters = signature.parameters.map((_parameter, index) => queries.parameterType(signature, index));
|
|
143
|
+
const result = queries.returnTypeOf(signature);
|
|
144
|
+
lookups += parameters.length + 1;
|
|
145
|
+
called = {
|
|
146
|
+
parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
|
|
147
|
+
result: isAnswered(result) ? result : undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
calls.set(type.id, called);
|
|
151
|
+
return called;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Each object type a value of `source` is checked as against the object types of
|
|
155
|
+
* `target`: the value's own, and through array elements and through a function's
|
|
156
|
+
* parameters, checked the other way, and its result. A source that is one of the
|
|
157
|
+
* target's own object types matches it as it stands.
|
|
158
|
+
*/
|
|
159
|
+
const compared = (source, target, depth, into, seen) => {
|
|
160
|
+
const key = `${String(source?.id)}>${String(target?.id)}`;
|
|
161
|
+
if (source === undefined || target === undefined || source.id === target.id) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (depth > MAX_DEPTH || seen.has(key)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
seen.add(key);
|
|
168
|
+
const targets = partsOf(target);
|
|
169
|
+
const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
|
|
170
|
+
for (const one of sources) {
|
|
171
|
+
if (carries(one)) {
|
|
172
|
+
into.push({ source: one, targets });
|
|
173
|
+
}
|
|
174
|
+
const element = elementOf(one);
|
|
175
|
+
const called = element === undefined ? callOf(one) : undefined;
|
|
176
|
+
for (const part of targets) {
|
|
177
|
+
if (element !== undefined) {
|
|
178
|
+
compared(element, elementOf(part), depth + 1, into, seen);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const other = called === undefined ? undefined : callOf(part);
|
|
182
|
+
if (called === undefined || other === undefined) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
other.parameters.forEach((parameter, index) => {
|
|
186
|
+
compared(parameter, called.parameters[index], depth + 1, into, seen);
|
|
187
|
+
});
|
|
188
|
+
compared(called.result, other.result, depth + 1, into, seen);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/** Whether a value of one type may carry a member a position reads, at any depth the walk takes. */
|
|
193
|
+
const mayCarry = (type, depth) => depth <= MAX_DEPTH &&
|
|
194
|
+
partsOf(type).some((part) => {
|
|
195
|
+
if (carries(part)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
const element = elementOf(part);
|
|
199
|
+
if (element !== undefined) {
|
|
200
|
+
return mayCarry(element, depth + 1);
|
|
201
|
+
}
|
|
202
|
+
return callOf(part) !== undefined;
|
|
203
|
+
});
|
|
204
|
+
const sourced = carried.flatMap((flow, index) => {
|
|
205
|
+
const type = types[index];
|
|
206
|
+
return type === undefined || !isAnswered(type) || !mayCarry(type, 0) ? [] : [{ flow, type }];
|
|
207
|
+
});
|
|
208
|
+
batches += batchesOf(sourced.length);
|
|
209
|
+
const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
|
|
210
|
+
const reads = [];
|
|
211
|
+
sourced.forEach(({ flow, type }, index) => {
|
|
212
|
+
const position = positions[index];
|
|
213
|
+
if (position === undefined || !isAnswered(position)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const checked = [];
|
|
217
|
+
compared(type, position, 0, checked, new Set());
|
|
218
|
+
const ids = new Set();
|
|
219
|
+
for (const { source, targets } of checked) {
|
|
220
|
+
const tables = targets.map(membersOf);
|
|
221
|
+
for (const [name, member] of membersOf(source)) {
|
|
222
|
+
const read = tables.some((table) => {
|
|
223
|
+
const declared = table.get(name);
|
|
224
|
+
return (declared !== undefined &&
|
|
225
|
+
declared.handles.size > 0 &&
|
|
226
|
+
![...declared.handles].some((handle) => member.handles.has(handle)));
|
|
227
|
+
});
|
|
228
|
+
if (read) {
|
|
229
|
+
for (const id of readable(member)) {
|
|
230
|
+
ids.add(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (ids.size > 0) {
|
|
236
|
+
reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return { writes, reads, batches, lookups };
|
|
240
|
+
}
|
|
@@ -45,6 +45,7 @@ export const CONVENTION_ROWS = [
|
|
|
45
45
|
`<src>/content/config.${MODULE}`,
|
|
46
46
|
],
|
|
47
47
|
excludes: ["<src>/pages/**/_*", "<src>/pages/**/_*/**"],
|
|
48
|
+
generated: [".astro"],
|
|
48
49
|
moves: [
|
|
49
50
|
{
|
|
50
51
|
id: "src",
|
|
@@ -71,6 +72,7 @@ export const CONVENTION_ROWS = [
|
|
|
71
72
|
name: "next",
|
|
72
73
|
package: "next",
|
|
73
74
|
range: ">=13.4.0 <17.0.0",
|
|
75
|
+
generated: [".next"],
|
|
74
76
|
entries: [
|
|
75
77
|
`{,src/}app/**/{layout,page,loading,not-found,error,route,template,default,forbidden,unauthorized}.${SCRIPT}`,
|
|
76
78
|
`{,src/}app/global-{error,not-found}.${SCRIPT}`,
|
|
@@ -87,6 +89,7 @@ export const CONVENTION_ROWS = [
|
|
|
87
89
|
name: "nuxt",
|
|
88
90
|
package: "nuxt",
|
|
89
91
|
range: ">=3.0.0 <5.0.0",
|
|
92
|
+
generated: [".nuxt"],
|
|
90
93
|
entries: [
|
|
91
94
|
"<srcDir>/{app,error}.{vue,jsx,tsx}",
|
|
92
95
|
`<srcDir>/app.config.${MODULE}`,
|
|
@@ -181,6 +184,7 @@ export const CONVENTION_ROWS = [
|
|
|
181
184
|
package: "@react-router/dev",
|
|
182
185
|
range: ">=7.0.0 <9.0.0",
|
|
183
186
|
entries: APP_DIRECTORY_ENTRIES,
|
|
187
|
+
generated: [".react-router"],
|
|
184
188
|
moves: [
|
|
185
189
|
{
|
|
186
190
|
id: "app",
|
|
@@ -259,6 +263,7 @@ export const CONVENTION_ROWS = [
|
|
|
259
263
|
package: "@sveltejs/kit",
|
|
260
264
|
range: ">=2.0.0 <3.0.0",
|
|
261
265
|
entries: [...SVELTEKIT_ENTRIES, "<params>/*.{js,ts}"],
|
|
266
|
+
generated: [".svelte-kit"],
|
|
262
267
|
moves: [
|
|
263
268
|
{
|
|
264
269
|
id: "src",
|
|
@@ -323,6 +328,7 @@ export const CONVENTION_ROWS = [
|
|
|
323
328
|
package: "@sveltejs/kit",
|
|
324
329
|
range: ">=3.0.0 <4.0.0",
|
|
325
330
|
entries: [...SVELTEKIT_ENTRIES, "<params>.{js,ts}"],
|
|
331
|
+
generated: [".svelte-kit"],
|
|
326
332
|
moves: [
|
|
327
333
|
{
|
|
328
334
|
id: "src",
|