@cplieger/deadset-ts 5.0.0 → 5.1.1
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 +1 -1
- package/dist/src/analysis.js +145 -39
- package/dist/src/configuration-files.js +108 -4
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +247 -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 +35 -16
- package/dist/src/file-facts.js +23 -20
- 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/graph.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +17 -4
- package/dist/src/kinds.js +4 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +229 -11
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +121 -37
- package/dist/src/session.js +4 -3
- package/dist/src/stores.js +3 -0
- package/dist/src/template-field.js +60 -3
- 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 +194 -49
- package/src/configuration-files.ts +137 -2
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +358 -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 +67 -16
- package/src/file-facts.ts +26 -20
- 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/graph.ts +4 -4
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +19 -4
- package/src/kinds.ts +5 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/query.ts +26 -0
- package/src/references.ts +294 -22
- package/src/resolve.ts +14 -13
- package/src/roots.ts +152 -44
- package/src/session.ts +4 -2
- package/src/stores.ts +9 -0
- package/src/template-field.ts +65 -3
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ A TypeScript caller that imports the source sets `allowImportingTsExtensions`, b
|
|
|
90
90
|
|
|
91
91
|
## Related projects
|
|
92
92
|
|
|
93
|
-
deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus
|
|
93
|
+
deadset-ts implements the [deadset Contract](https://github.com/cplieger/deadset-spec), which fixes the issue codes, the report schema and the exit codes. It passes every fixture of the Contract's conformance corpus.
|
|
94
94
|
|
|
95
95
|
- [deadset-go](https://github.com/cplieger/deadset-go) is the same analysis for Go modules.
|
|
96
96
|
- [deadset](https://github.com/cplieger/deadset) runs both analyzers as one command and merges their reports, resolving the edges between Go and TypeScript code.
|
package/dist/src/analysis.js
CHANGED
|
@@ -14,11 +14,14 @@ 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 {
|
|
21
|
-
import {
|
|
20
|
+
import { generatedFile } from "./generated-file.js";
|
|
21
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
22
|
+
import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
|
|
23
|
+
import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
|
|
24
|
+
import { globExpression } from "./glob.js";
|
|
22
25
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
23
26
|
import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
|
|
24
27
|
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
@@ -27,9 +30,9 @@ import { injectionContainer } from "./injection-container.js";
|
|
|
27
30
|
import { interfaceSatisfaction } from "./interface-satisfaction.js";
|
|
28
31
|
import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
|
|
29
32
|
import { inventory } from "./inventory.js";
|
|
30
|
-
import { readManifest } from "./manifest.js";
|
|
33
|
+
import { packageEntries, readManifest } from "./manifest.js";
|
|
31
34
|
import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
|
|
32
|
-
import { joinPath, relativePath, resolvePath } from "./paths.js";
|
|
35
|
+
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
33
36
|
import { byPosition, isComponentFile } from "./position.js";
|
|
34
37
|
import { SetupError, setupLine } from "./setup-failure.js";
|
|
35
38
|
import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
|
|
@@ -37,6 +40,7 @@ import { reflectiveLookup } from "./reflective-lookup.js";
|
|
|
37
40
|
import { references, testFileRulesOf } from "./references.js";
|
|
38
41
|
import { roots, unmatchedEverywhere } from "./roots.js";
|
|
39
42
|
import { serializationContract } from "./serialization-contract.js";
|
|
43
|
+
import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
|
|
40
44
|
import { supportReferences, testSupportFiles } from "./test-support.js";
|
|
41
45
|
import { diagnosticsOf, ownedAs, runSession } from "./session.js";
|
|
42
46
|
import { accessorsOf, storesOf } from "./stores.js";
|
|
@@ -105,11 +109,15 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
105
109
|
const options = {
|
|
106
110
|
host,
|
|
107
111
|
manifest: workspace?.manifest ?? readManifest(host, targetRoot),
|
|
108
|
-
|
|
112
|
+
otherPackages: [
|
|
113
|
+
...(workspace?.entries ?? []),
|
|
114
|
+
...packageEntries(host, normalizePath(absoluteRoot), discovered.configFiles, new Set(workspace?.memberDirs ?? [])),
|
|
115
|
+
],
|
|
109
116
|
patterns: config.rootPatterns,
|
|
110
117
|
entryFiles: config.ts.entryFiles,
|
|
111
118
|
conventions,
|
|
112
119
|
testFiles: config.ts.testFiles,
|
|
120
|
+
parse: (fileName, text) => engine.parseSourceFile(fileName, text),
|
|
113
121
|
publishedAPI: config.targetKind === "library",
|
|
114
122
|
};
|
|
115
123
|
const notBuilt = [...discovered.notBuilt];
|
|
@@ -124,6 +132,8 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
124
132
|
const unbuildable = [];
|
|
125
133
|
const setupDropped = [];
|
|
126
134
|
const consumed = new Map();
|
|
135
|
+
const heldFiles = new Set();
|
|
136
|
+
let outsideTests = [];
|
|
127
137
|
const setupMet = (configFile, met) => {
|
|
128
138
|
const [first] = met;
|
|
129
139
|
if (first !== undefined && isGuess(discovered, configFile)) {
|
|
@@ -225,10 +235,17 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
225
235
|
.ownSourceFiles()
|
|
226
236
|
.filter((file) => referenceOnly(file.fileName))
|
|
227
237
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
238
|
+
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
228
239
|
};
|
|
229
240
|
};
|
|
241
|
+
for (const file of project.ownSourceFiles()) {
|
|
242
|
+
heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
|
|
243
|
+
}
|
|
230
244
|
return stage(project, { configuration, read });
|
|
231
|
-
}, discovered.derived ? "record" : "refuse", workspace?.programs)
|
|
245
|
+
}, discovered.derived ? "record" : "refuse", workspace?.programs, () => {
|
|
246
|
+
const patterns = config.ts.testFiles.map(globExpression);
|
|
247
|
+
outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path))), options.parse);
|
|
248
|
+
});
|
|
232
249
|
for (const configFile of session.unopened) {
|
|
233
250
|
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
234
251
|
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
@@ -259,6 +276,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
259
276
|
unanswered: session.unanswered,
|
|
260
277
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
261
278
|
conventionsApplied: conventions.applied,
|
|
279
|
+
outsideTests,
|
|
262
280
|
};
|
|
263
281
|
}
|
|
264
282
|
/**
|
|
@@ -321,6 +339,7 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
321
339
|
const DETECTORS = new Map([
|
|
322
340
|
["interface-satisfaction", interfaceSatisfaction],
|
|
323
341
|
["enum-group", enumGroup],
|
|
342
|
+
["generated-file", generatedFile],
|
|
324
343
|
["template-field", templateField],
|
|
325
344
|
["reflective-lookup", reflectiveLookup],
|
|
326
345
|
["decorator", decorator],
|
|
@@ -346,52 +365,88 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
346
365
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
347
366
|
testFiles: config.ts.testFiles,
|
|
348
367
|
});
|
|
349
|
-
const
|
|
350
|
-
|
|
368
|
+
const evidence = evidenceRecords({
|
|
369
|
+
project,
|
|
370
|
+
held: projectRead.held,
|
|
371
|
+
targetRoot,
|
|
372
|
+
templates,
|
|
373
|
+
ts: config.ts,
|
|
374
|
+
consumers,
|
|
375
|
+
...(config.analysis.generatedFiles === "include"
|
|
376
|
+
? {}
|
|
377
|
+
: { generated: projectRead.generated }),
|
|
378
|
+
}, detectors, disabled);
|
|
379
|
+
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
380
|
+
const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
381
|
+
return {
|
|
382
|
+
projectRead,
|
|
383
|
+
references: resolved.references,
|
|
384
|
+
testFiles: resolved.testFilePaths,
|
|
385
|
+
evidence,
|
|
386
|
+
componentFiles: components.map(pathOf),
|
|
387
|
+
componentWarnings: components.flatMap((file) => readComponent(file.originalText).warnings.map((warning) => ({
|
|
388
|
+
path: pathOf(file),
|
|
389
|
+
line: lineOf(file.originalText, warning.offset),
|
|
390
|
+
reason: warning.reason,
|
|
391
|
+
}))),
|
|
392
|
+
extra: extra(project, projectRead, resolved.references),
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
const outsidePaths = read.outsideTests.map((one) => one.path);
|
|
396
|
+
const withOutside = read.answers.map((one) => ({
|
|
397
|
+
...one,
|
|
398
|
+
references: [
|
|
399
|
+
...one.references,
|
|
400
|
+
...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
|
|
401
|
+
],
|
|
402
|
+
testFiles: [...one.testFiles, ...outsidePaths],
|
|
403
|
+
}));
|
|
404
|
+
// Test-support code is code only test code imports in any configuration of the run: a
|
|
405
|
+
// configuration that holds no test file says nothing about what the tests import.
|
|
406
|
+
const support = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles));
|
|
407
|
+
const answers = withOutside.map((one) => {
|
|
408
|
+
const { projectRead } = one;
|
|
409
|
+
const detected = heldRecords(one.evidence, {
|
|
410
|
+
disabled,
|
|
411
|
+
mode,
|
|
412
|
+
testFiles: new Set([...one.testFiles, ...support]),
|
|
413
|
+
});
|
|
414
|
+
const exempt = detected.records;
|
|
415
|
+
const made = supportReferences(one.references, support);
|
|
416
|
+
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
417
|
+
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
351
418
|
const configured = {
|
|
352
419
|
configuration: projectRead.configuration,
|
|
353
420
|
symbols: projectRead.held.symbols,
|
|
354
|
-
references: made,
|
|
421
|
+
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
355
422
|
roots: projectRead.rooted.liveUnderReachability,
|
|
356
|
-
testFiles:
|
|
357
|
-
...(
|
|
423
|
+
testFiles: one.testFiles,
|
|
424
|
+
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
358
425
|
...(projectRead.referenceOnly.length === 0
|
|
359
426
|
? {}
|
|
360
427
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
361
428
|
};
|
|
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
|
-
const unanswered = [
|
|
368
|
-
...projectRead.held.unanswered,
|
|
369
|
-
...projectRead.rooted.unanswered,
|
|
370
|
-
...made
|
|
371
|
-
.filter((reference) => reference.resolution === "by-name")
|
|
372
|
-
.map((reference) => reference.to),
|
|
373
|
-
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
374
|
-
];
|
|
375
|
-
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
376
|
-
const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
377
429
|
return {
|
|
378
430
|
configFile: projectRead.configFile,
|
|
379
431
|
configured,
|
|
380
432
|
exempt,
|
|
381
|
-
unanswered
|
|
433
|
+
unanswered: [
|
|
434
|
+
...projectRead.held.unanswered,
|
|
435
|
+
...projectRead.rooted.unanswered,
|
|
436
|
+
...made
|
|
437
|
+
.filter((reference) => reference.resolution === "by-name")
|
|
438
|
+
.map((reference) => reference.to),
|
|
439
|
+
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
440
|
+
],
|
|
382
441
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
383
442
|
skipped: projectRead.skipped,
|
|
384
|
-
componentFiles:
|
|
385
|
-
componentWarnings:
|
|
386
|
-
|
|
387
|
-
line: lineOf(file.originalText, warning.offset),
|
|
388
|
-
reason: warning.reason,
|
|
389
|
-
}))),
|
|
390
|
-
extra: extra(project, projectRead, made),
|
|
443
|
+
componentFiles: one.componentFiles,
|
|
444
|
+
componentWarnings: one.componentWarnings,
|
|
445
|
+
extra: one.extra,
|
|
391
446
|
};
|
|
392
447
|
});
|
|
393
448
|
const consumed = read.consumers.references;
|
|
394
|
-
const projects =
|
|
449
|
+
const projects = answers.map((one) => consumed.length === 0
|
|
395
450
|
? one
|
|
396
451
|
: {
|
|
397
452
|
...one,
|
|
@@ -405,7 +460,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
405
460
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
406
461
|
unansweredHeld: [
|
|
407
462
|
...new Set([
|
|
408
|
-
...
|
|
463
|
+
...answers.flatMap((one) => one.unanswered),
|
|
409
464
|
...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
|
|
410
465
|
]),
|
|
411
466
|
],
|
|
@@ -440,6 +495,55 @@ function holdingSweep(read, input) {
|
|
|
440
495
|
live = next;
|
|
441
496
|
}
|
|
442
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* The test references exemption evidence written in a test file makes: each is a read of
|
|
500
|
+
* the declaration it would retain, made by the test file at the evidence's site.
|
|
501
|
+
*/
|
|
502
|
+
function evidenceReferences(symbols, inTestFiles) {
|
|
503
|
+
if (inTestFiles.length === 0) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
const files = new Map(symbols
|
|
507
|
+
.filter((symbol) => symbol.kind === "file")
|
|
508
|
+
.map((symbol) => [symbol.position.path, symbol.id]));
|
|
509
|
+
return inTestFiles.flatMap((record) => {
|
|
510
|
+
const from = files.get(record.site.path);
|
|
511
|
+
return from === undefined
|
|
512
|
+
? []
|
|
513
|
+
: [
|
|
514
|
+
{
|
|
515
|
+
from,
|
|
516
|
+
to: record.id,
|
|
517
|
+
position: record.site,
|
|
518
|
+
use: "read",
|
|
519
|
+
resolution: "syntax",
|
|
520
|
+
test: true,
|
|
521
|
+
},
|
|
522
|
+
];
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
527
|
+
* reference counting as a read: what the write-only kind would report but for a record.
|
|
528
|
+
*/
|
|
529
|
+
function writtenOnly(read, mode) {
|
|
530
|
+
const written = new Set();
|
|
531
|
+
const readSome = new Set();
|
|
532
|
+
for (const { configured } of read.projects) {
|
|
533
|
+
for (const reference of configured.references) {
|
|
534
|
+
if (mode.production && reference.test) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (reference.use === "write" && reference.consumer === undefined) {
|
|
538
|
+
written.add(reference.to);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
readSome.add(reference.to);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return new Set([...written].filter((id) => !readSome.has(id)));
|
|
546
|
+
}
|
|
443
547
|
/**
|
|
444
548
|
* The sweep of a run read once, under one set of marks. Where a question went unanswered,
|
|
445
549
|
* the run is swept once more without what that question could have kept live and without
|
|
@@ -483,7 +587,7 @@ function sweptOf(read, input) {
|
|
|
483
587
|
matrix: read.matrix,
|
|
484
588
|
sweep: result,
|
|
485
589
|
exempt,
|
|
486
|
-
retained: retainedIn(read.matrix, swept),
|
|
590
|
+
retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
|
|
487
591
|
notBuilt: read.notBuilt,
|
|
488
592
|
unanswered: read.unanswered,
|
|
489
593
|
heldByUnanswered: [...held],
|
|
@@ -510,9 +614,10 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
510
614
|
const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
|
|
511
615
|
deprecation: deprecatedDeclarations(project, read.held),
|
|
512
616
|
accessors: accessorsOf(project, read.held),
|
|
513
|
-
needs: projectNeeds(project, read.held, host, targetRoot),
|
|
617
|
+
needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) => engine.parseSourceFile(fileName, text)),
|
|
514
618
|
implementations: implementations(project, read.held),
|
|
515
619
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
620
|
+
generated: [...read.generated.keys()],
|
|
516
621
|
rooted: read.rooted,
|
|
517
622
|
directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
|
|
518
623
|
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
@@ -546,6 +651,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
546
651
|
files: {
|
|
547
652
|
tree: sourceTree(host, targetRoot),
|
|
548
653
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
654
|
+
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
549
655
|
},
|
|
550
656
|
boundary: {
|
|
551
657
|
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,8 +29,17 @@ 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
|
-
const RESOLVED_EXTENSIONS = [
|
|
42
|
+
export const RESOLVED_EXTENSIONS = [
|
|
34
43
|
".ts",
|
|
35
44
|
".tsx",
|
|
36
45
|
".d.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
|
};
|