@cplieger/deadset-ts 5.1.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 +72 -33
- package/dist/src/configuration-files.js +1 -1
- package/dist/src/contextual-uses.js +21 -14
- package/dist/src/exempt.js +23 -8
- package/dist/src/file-facts.js +23 -20
- package/dist/src/graph.js +1 -1
- package/dist/src/inventory.js +13 -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/references.js +30 -1
- package/dist/src/roots.js +120 -36
- package/dist/src/session.js +4 -3
- package/dist/src/template-field.js +60 -3
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +110 -44
- package/src/configuration-files.ts +1 -1
- package/src/contextual-uses.ts +20 -16
- package/src/exempt.ts +28 -8
- package/src/file-facts.ts +24 -20
- package/src/graph.ts +4 -4
- package/src/inventory.ts +15 -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/references.ts +46 -7
- package/src/roots.ts +141 -43
- package/src/session.ts +4 -2
- package/src/template-field.ts +65 -3
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
|
@@ -19,8 +19,9 @@ import { readEdgeSides } from "./edges.js";
|
|
|
19
19
|
import { enumGroup } from "./enum-group.js";
|
|
20
20
|
import { generatedFile } from "./generated-file.js";
|
|
21
21
|
import { sourceFilesOf } from "./source-files.js";
|
|
22
|
-
import {
|
|
23
|
-
import { heldByInclusion, sourceTree } from "./file-facts.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";
|
|
24
25
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
25
26
|
import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
|
|
26
27
|
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
@@ -29,9 +30,9 @@ import { injectionContainer } from "./injection-container.js";
|
|
|
29
30
|
import { interfaceSatisfaction } from "./interface-satisfaction.js";
|
|
30
31
|
import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
|
|
31
32
|
import { inventory } from "./inventory.js";
|
|
32
|
-
import { readManifest } from "./manifest.js";
|
|
33
|
+
import { packageEntries, readManifest } from "./manifest.js";
|
|
33
34
|
import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
|
|
34
|
-
import { joinPath, relativePath, resolvePath } from "./paths.js";
|
|
35
|
+
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
35
36
|
import { byPosition, isComponentFile } from "./position.js";
|
|
36
37
|
import { SetupError, setupLine } from "./setup-failure.js";
|
|
37
38
|
import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
|
|
@@ -39,6 +40,7 @@ import { reflectiveLookup } from "./reflective-lookup.js";
|
|
|
39
40
|
import { references, testFileRulesOf } from "./references.js";
|
|
40
41
|
import { roots, unmatchedEverywhere } from "./roots.js";
|
|
41
42
|
import { serializationContract } from "./serialization-contract.js";
|
|
43
|
+
import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
|
|
42
44
|
import { supportReferences, testSupportFiles } from "./test-support.js";
|
|
43
45
|
import { diagnosticsOf, ownedAs, runSession } from "./session.js";
|
|
44
46
|
import { accessorsOf, storesOf } from "./stores.js";
|
|
@@ -107,7 +109,10 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
107
109
|
const options = {
|
|
108
110
|
host,
|
|
109
111
|
manifest: workspace?.manifest ?? readManifest(host, targetRoot),
|
|
110
|
-
|
|
112
|
+
otherPackages: [
|
|
113
|
+
...(workspace?.entries ?? []),
|
|
114
|
+
...packageEntries(host, normalizePath(absoluteRoot), discovered.configFiles, new Set(workspace?.memberDirs ?? [])),
|
|
115
|
+
],
|
|
111
116
|
patterns: config.rootPatterns,
|
|
112
117
|
entryFiles: config.ts.entryFiles,
|
|
113
118
|
conventions,
|
|
@@ -127,6 +132,8 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
127
132
|
const unbuildable = [];
|
|
128
133
|
const setupDropped = [];
|
|
129
134
|
const consumed = new Map();
|
|
135
|
+
const heldFiles = new Set();
|
|
136
|
+
let outsideTests = [];
|
|
130
137
|
const setupMet = (configFile, met) => {
|
|
131
138
|
const [first] = met;
|
|
132
139
|
if (first !== undefined && isGuess(discovered, configFile)) {
|
|
@@ -231,8 +238,14 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
231
238
|
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
232
239
|
};
|
|
233
240
|
};
|
|
241
|
+
for (const file of project.ownSourceFiles()) {
|
|
242
|
+
heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
|
|
243
|
+
}
|
|
234
244
|
return stage(project, { configuration, read });
|
|
235
|
-
}, 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
|
+
});
|
|
236
249
|
for (const configFile of session.unopened) {
|
|
237
250
|
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
238
251
|
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
@@ -263,6 +276,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
263
276
|
unanswered: session.unanswered,
|
|
264
277
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
265
278
|
conventionsApplied: conventions.applied,
|
|
279
|
+
outsideTests,
|
|
266
280
|
};
|
|
267
281
|
}
|
|
268
282
|
/**
|
|
@@ -351,8 +365,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
351
365
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
352
366
|
testFiles: config.ts.testFiles,
|
|
353
367
|
});
|
|
354
|
-
const
|
|
355
|
-
const detected = detectExemptions({
|
|
368
|
+
const evidence = evidenceRecords({
|
|
356
369
|
project,
|
|
357
370
|
held: projectRead.held,
|
|
358
371
|
targetRoot,
|
|
@@ -362,52 +375,78 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
362
375
|
...(config.analysis.generatedFiles === "include"
|
|
363
376
|
? {}
|
|
364
377
|
: { generated: projectRead.generated }),
|
|
365
|
-
}, detectors,
|
|
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, {
|
|
366
410
|
disabled,
|
|
367
411
|
mode,
|
|
368
|
-
testFiles: new Set([...
|
|
412
|
+
testFiles: new Set([...one.testFiles, ...support]),
|
|
369
413
|
});
|
|
370
414
|
const exempt = detected.records;
|
|
371
|
-
const made = supportReferences(
|
|
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();
|
|
372
418
|
const configured = {
|
|
373
419
|
configuration: projectRead.configuration,
|
|
374
420
|
symbols: projectRead.held.symbols,
|
|
375
421
|
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
376
422
|
roots: projectRead.rooted.liveUnderReachability,
|
|
377
|
-
testFiles:
|
|
378
|
-
...(
|
|
423
|
+
testFiles: one.testFiles,
|
|
424
|
+
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
379
425
|
...(projectRead.referenceOnly.length === 0
|
|
380
426
|
? {}
|
|
381
427
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
382
428
|
};
|
|
383
|
-
const unanswered = [
|
|
384
|
-
...projectRead.held.unanswered,
|
|
385
|
-
...projectRead.rooted.unanswered,
|
|
386
|
-
...made
|
|
387
|
-
.filter((reference) => reference.resolution === "by-name")
|
|
388
|
-
.map((reference) => reference.to),
|
|
389
|
-
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
390
|
-
];
|
|
391
|
-
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
392
|
-
const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
393
429
|
return {
|
|
394
430
|
configFile: projectRead.configFile,
|
|
395
431
|
configured,
|
|
396
432
|
exempt,
|
|
397
|
-
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
|
+
],
|
|
398
441
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
399
442
|
skipped: projectRead.skipped,
|
|
400
|
-
componentFiles:
|
|
401
|
-
componentWarnings:
|
|
402
|
-
|
|
403
|
-
line: lineOf(file.originalText, warning.offset),
|
|
404
|
-
reason: warning.reason,
|
|
405
|
-
}))),
|
|
406
|
-
extra: extra(project, projectRead, made),
|
|
443
|
+
componentFiles: one.componentFiles,
|
|
444
|
+
componentWarnings: one.componentWarnings,
|
|
445
|
+
extra: one.extra,
|
|
407
446
|
};
|
|
408
447
|
});
|
|
409
448
|
const consumed = read.consumers.references;
|
|
410
|
-
const projects =
|
|
449
|
+
const projects = answers.map((one) => consumed.length === 0
|
|
411
450
|
? one
|
|
412
451
|
: {
|
|
413
452
|
...one,
|
|
@@ -421,7 +460,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
421
460
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
422
461
|
unansweredHeld: [
|
|
423
462
|
...new Set([
|
|
424
|
-
...
|
|
463
|
+
...answers.flatMap((one) => one.unanswered),
|
|
425
464
|
...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
|
|
426
465
|
]),
|
|
427
466
|
],
|
|
@@ -39,7 +39,7 @@ const COUNTERPARTS = [
|
|
|
39
39
|
/** The extensions appended to a relative specifier from a configuration file, in order. */
|
|
40
40
|
const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
41
41
|
/** The extensions the program's module resolution appends to a specifier written without one. */
|
|
42
|
-
const RESOLVED_EXTENSIONS = [
|
|
42
|
+
export const RESOLVED_EXTENSIONS = [
|
|
43
43
|
".ts",
|
|
44
44
|
".tsx",
|
|
45
45
|
".d.ts",
|
|
@@ -32,8 +32,6 @@ function propertyName(property) {
|
|
|
32
32
|
}
|
|
33
33
|
return undefined;
|
|
34
34
|
}
|
|
35
|
-
/** How many element and signature steps the structural walk takes below a value's own type. */
|
|
36
|
-
const MAX_DEPTH = 3;
|
|
37
35
|
/**
|
|
38
36
|
* The contextual uses of one project's literals and values, asked in batches: one
|
|
39
37
|
* contextual type per literal and per value whose type carries a member of the target,
|
|
@@ -154,14 +152,15 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
154
152
|
* Each object type a value of `source` is checked as against the object types of
|
|
155
153
|
* `target`: the value's own, and through array elements and through a function's
|
|
156
154
|
* 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.
|
|
155
|
+
* target's own object types matches it as it stands. Each pair is compared once, which
|
|
156
|
+
* ends the walk over a recursive type.
|
|
158
157
|
*/
|
|
159
|
-
const compared = (source, target,
|
|
158
|
+
const compared = (source, target, into, seen) => {
|
|
160
159
|
const key = `${String(source?.id)}>${String(target?.id)}`;
|
|
161
160
|
if (source === undefined || target === undefined || source.id === target.id) {
|
|
162
161
|
return;
|
|
163
162
|
}
|
|
164
|
-
if (
|
|
163
|
+
if (seen.has(key)) {
|
|
165
164
|
return;
|
|
166
165
|
}
|
|
167
166
|
seen.add(key);
|
|
@@ -175,7 +174,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
175
174
|
const called = element === undefined ? callOf(one) : undefined;
|
|
176
175
|
for (const part of targets) {
|
|
177
176
|
if (element !== undefined) {
|
|
178
|
-
compared(element, elementOf(part),
|
|
177
|
+
compared(element, elementOf(part), into, seen);
|
|
179
178
|
continue;
|
|
180
179
|
}
|
|
181
180
|
const other = called === undefined ? undefined : callOf(part);
|
|
@@ -183,27 +182,35 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
183
182
|
continue;
|
|
184
183
|
}
|
|
185
184
|
other.parameters.forEach((parameter, index) => {
|
|
186
|
-
compared(parameter, called.parameters[index],
|
|
185
|
+
compared(parameter, called.parameters[index], into, seen);
|
|
187
186
|
});
|
|
188
|
-
compared(called.result, other.result,
|
|
187
|
+
compared(called.result, other.result, into, seen);
|
|
189
188
|
}
|
|
190
189
|
}
|
|
191
190
|
};
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Whether a value of one type may carry a member a position reads, at any depth the
|
|
193
|
+
* walk takes. A type met again on the way adds nothing its first visit did not.
|
|
194
|
+
*/
|
|
195
|
+
const mayCarry = (type, seen = new Set()) => {
|
|
196
|
+
if (seen.has(type.id)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
seen.add(type.id);
|
|
200
|
+
return partsOf(type).some((part) => {
|
|
195
201
|
if (carries(part)) {
|
|
196
202
|
return true;
|
|
197
203
|
}
|
|
198
204
|
const element = elementOf(part);
|
|
199
205
|
if (element !== undefined) {
|
|
200
|
-
return mayCarry(element,
|
|
206
|
+
return mayCarry(element, seen);
|
|
201
207
|
}
|
|
202
208
|
return callOf(part) !== undefined;
|
|
203
209
|
});
|
|
210
|
+
};
|
|
204
211
|
const sourced = carried.flatMap((flow, index) => {
|
|
205
212
|
const type = types[index];
|
|
206
|
-
return type === undefined || !isAnswered(type) || !mayCarry(type
|
|
213
|
+
return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
|
|
207
214
|
});
|
|
208
215
|
batches += batchesOf(sourced.length);
|
|
209
216
|
const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
|
|
@@ -214,7 +221,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
214
221
|
return;
|
|
215
222
|
}
|
|
216
223
|
const checked = [];
|
|
217
|
-
compared(type, position,
|
|
224
|
+
compared(type, position, checked, new Set());
|
|
218
225
|
const ids = new Set();
|
|
219
226
|
for (const { source, targets } of checked) {
|
|
220
227
|
const tables = targets.map(membersOf);
|
package/dist/src/exempt.js
CHANGED
|
@@ -92,12 +92,18 @@ export function computeExemptions(input, detectors, holding) {
|
|
|
92
92
|
}
|
|
93
93
|
/** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
|
|
94
94
|
export function detectExemptions(input, detectors, holding) {
|
|
95
|
+
return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Every record one project's detectors found, one per piece of evidence, before the
|
|
99
|
+
* mode decides which of them hold: what {@link heldRecords} reads.
|
|
100
|
+
*/
|
|
101
|
+
export function evidenceRecords(input, detectors, disabled) {
|
|
95
102
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
96
103
|
const found = [];
|
|
97
|
-
const inTestFiles = [];
|
|
98
104
|
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
99
105
|
const detect = detectors.get(exemptionClass);
|
|
100
|
-
if (detect === undefined ||
|
|
106
|
+
if (detect === undefined || disabled.has(exemptionClass)) {
|
|
101
107
|
continue;
|
|
102
108
|
}
|
|
103
109
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
@@ -105,17 +111,26 @@ export function detectExemptions(input, detectors, holding) {
|
|
|
105
111
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
106
112
|
continue;
|
|
107
113
|
}
|
|
108
|
-
|
|
114
|
+
found.push({
|
|
109
115
|
id: evidence.id,
|
|
110
116
|
class: exemptionClass,
|
|
111
117
|
detail: evidence.detail,
|
|
112
118
|
site: evidence.site,
|
|
113
119
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
126
|
+
export function heldRecords(records, holding) {
|
|
127
|
+
const found = [];
|
|
128
|
+
const inTestFiles = [];
|
|
129
|
+
for (const record of records) {
|
|
130
|
+
if (holding.mode.production && holding.testFiles.has(record.site.path)) {
|
|
131
|
+
inTestFiles.push(record);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
119
134
|
found.push(record);
|
|
120
135
|
}
|
|
121
136
|
}
|
package/dist/src/file-facts.js
CHANGED
|
@@ -26,12 +26,10 @@ function lastLine(text) {
|
|
|
26
26
|
return line;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* read is skipped, as discovery skips it
|
|
31
|
-
* with it: a tree the analysis may not enter holds no file it can report.
|
|
29
|
+
* The path below the target root of every source file there, in path order. A directory
|
|
30
|
+
* that cannot be read is skipped, as discovery skips it.
|
|
32
31
|
*/
|
|
33
|
-
export function
|
|
34
|
-
const scopeOf = packageScope(host, targetRoot);
|
|
32
|
+
export function sourcePaths(host, targetRoot) {
|
|
35
33
|
const found = [];
|
|
36
34
|
const pending = [""];
|
|
37
35
|
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
@@ -50,24 +48,29 @@ export function sourceTree(host, targetRoot) {
|
|
|
50
48
|
}
|
|
51
49
|
continue;
|
|
52
50
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
let text;
|
|
57
|
-
try {
|
|
58
|
-
text = host.readFile(joinPath(targetRoot, path));
|
|
51
|
+
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
52
|
+
found.push(path);
|
|
59
53
|
}
|
|
60
|
-
catch {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
found.push({
|
|
64
|
-
path,
|
|
65
|
-
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
66
|
-
endLine: lastLine(text),
|
|
67
|
-
});
|
|
68
54
|
}
|
|
69
55
|
}
|
|
70
|
-
return found.sort((a, b) => (a
|
|
56
|
+
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every source file below the target root, in path order. A file that cannot be read is
|
|
60
|
+
* skipped: a tree the analysis may not enter holds no file it can report.
|
|
61
|
+
*/
|
|
62
|
+
export function sourceTree(host, targetRoot) {
|
|
63
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
64
|
+
return sourcePaths(host, targetRoot).flatMap((path) => {
|
|
65
|
+
let text;
|
|
66
|
+
try {
|
|
67
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
|
|
73
|
+
});
|
|
71
74
|
}
|
|
72
75
|
/** Whether one file changes what the program sees by being included in it. */
|
|
73
76
|
function actsByInclusion(file) {
|
package/dist/src/graph.js
CHANGED
|
@@ -94,7 +94,7 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
|
|
97
|
-
if (reference.consumer !== undefined) {
|
|
97
|
+
if (reference.consumer !== undefined || reference.unheld === true) {
|
|
98
98
|
consumed.push(edge);
|
|
99
99
|
continue;
|
|
100
100
|
}
|
package/dist/src/inventory.js
CHANGED
|
@@ -213,7 +213,18 @@ function nameNodeOf(node) {
|
|
|
213
213
|
* needs the declaration this enumeration made for it.
|
|
214
214
|
*/
|
|
215
215
|
export function nodeKey(file, node) {
|
|
216
|
-
return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
|
|
216
|
+
return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
|
|
217
|
+
}
|
|
218
|
+
/** `export default`, up to the keyword that stands where an unnamed default export's name would. */
|
|
219
|
+
const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
|
|
220
|
+
/**
|
|
221
|
+
* Where an unnamed declaration is placed: at `default` when it is the default export, so it
|
|
222
|
+
* never shares the file's own position, else at its first token.
|
|
223
|
+
*/
|
|
224
|
+
function unnamedStart(node) {
|
|
225
|
+
const start = node.getStart();
|
|
226
|
+
const prefix = DEFAULT_EXPORT.exec(node.getText());
|
|
227
|
+
return prefix === null ? start : start + prefix[0].length;
|
|
217
228
|
}
|
|
218
229
|
/** The kind a class element takes: a method, or a property or accessor. */
|
|
219
230
|
function classMemberKind(node) {
|
|
@@ -287,12 +298,11 @@ export function inventory(project, host, targetRoot) {
|
|
|
287
298
|
}
|
|
288
299
|
}
|
|
289
300
|
function declare(node, owner, kind, name, named) {
|
|
290
|
-
const at = name ?? node;
|
|
291
301
|
const record = keep({
|
|
292
302
|
file: owner.file,
|
|
293
303
|
node,
|
|
294
304
|
module: owner.module,
|
|
295
|
-
position: renderPosition(owner.file, targetRoot,
|
|
305
|
+
position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
|
|
296
306
|
endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
|
|
297
307
|
kind,
|
|
298
308
|
parentChain: owner.kind === "file" ? [] : chainOf(owner),
|
package/dist/src/manifest.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The entry points a package manifest names, which are roots because something
|
|
3
3
|
* outside the analyzed program reaches them: a consumer importing the package, or a
|
|
4
|
-
* shell running its command or one of its scripts.
|
|
5
|
-
*
|
|
6
|
-
* One call reads one manifest, the one in the directory it is given.
|
|
4
|
+
* shell running its command or one of its scripts. {@link readManifest} reads one
|
|
5
|
+
* directory's manifest, {@link packageEntries} those of the packages below the target root.
|
|
7
6
|
*/
|
|
8
|
-
import { isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
7
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
9
8
|
/** The manifest's own name. */
|
|
10
9
|
const MANIFEST = "package.json";
|
|
11
10
|
/** The three members that name an entry point beside `exports` and `bin`. */
|
|
@@ -144,3 +143,29 @@ export function readManifest(host, targetRoot) {
|
|
|
144
143
|
declaresExports: manifest["exports"] !== undefined,
|
|
145
144
|
};
|
|
146
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* The entry points every package manifest between the target root and each compiler
|
|
148
|
+
* configuration names: each directory holding a `package.json` that a configuration sits
|
|
149
|
+
* in or below, the target root and `known` excluded, whose manifests the run reads
|
|
150
|
+
* otherwise. Each entry is spelled under its manifest's path below the target root, and
|
|
151
|
+
* is published by its own manifest's rule, as the target's own entries are.
|
|
152
|
+
*/
|
|
153
|
+
export function packageEntries(host, targetRoot, configFiles, known) {
|
|
154
|
+
const dirs = new Set();
|
|
155
|
+
for (const configFile of configFiles) {
|
|
156
|
+
for (let dir = dirnamePath(resolvePath(targetRoot, configFile)); dir !== targetRoot && relativePath(targetRoot, dir) !== undefined; dir = dirnamePath(dir)) {
|
|
157
|
+
if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
|
|
158
|
+
dirs.add(dir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [...dirs].sort().flatMap((dir) => {
|
|
163
|
+
const manifest = readManifest(host, dir);
|
|
164
|
+
const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
|
|
165
|
+
return manifest.entries.map((entry) => ({
|
|
166
|
+
...entry,
|
|
167
|
+
member: `${where} ${entry.member}`,
|
|
168
|
+
published: entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The test files no compiler configuration of a run holds. The runner executes them all
|
|
3
|
+
* the same, so each module one names is a test reference: the own file a relative
|
|
4
|
+
* specifier names, and the declarations that file exports under the names read from it.
|
|
5
|
+
* Such a file is read without a program, so what its body references beyond the modules
|
|
6
|
+
* it names is not known.
|
|
7
|
+
*/
|
|
8
|
+
import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { RESOLVED_EXTENSIONS } from "./configuration-files.js";
|
|
10
|
+
import { emittedFrom } from "./emit-map.js";
|
|
11
|
+
import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
|
|
12
|
+
import { renderPosition } from "./position.js";
|
|
13
|
+
import { nameComponent } from "./ref.js";
|
|
14
|
+
/** The paths below the target root one relative specifier may name, written path first. */
|
|
15
|
+
function candidates(dir, specifier) {
|
|
16
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const path = normalizePath(dir === "" ? specifier : `${dir}/${specifier}`);
|
|
20
|
+
return [
|
|
21
|
+
path,
|
|
22
|
+
...emittedFrom(path, { sourceDirs: [] }),
|
|
23
|
+
...RESOLVED_EXTENSIONS.map((extension) => path + extension),
|
|
24
|
+
...RESOLVED_EXTENSIONS.map((extension) => `${path}/index${extension}`),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
/** The text of a specifier written as literal text, or undefined for any other expression. */
|
|
28
|
+
function literalText(node) {
|
|
29
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))
|
|
30
|
+
? node.text
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every module one file names: an import declaration, a re-export, and an `import()` call
|
|
35
|
+
* of literal text anywhere in the file. A namespace form, a star re-export, a bare import
|
|
36
|
+
* and an `import()` evaluate the module.
|
|
37
|
+
*/
|
|
38
|
+
function modulesNamed(file) {
|
|
39
|
+
const found = [];
|
|
40
|
+
const name = (specifier, names, evaluates) => {
|
|
41
|
+
const text = literalText(specifier);
|
|
42
|
+
if (text !== undefined) {
|
|
43
|
+
found.push({ specifier, text, names, evaluates });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const statement of file.statements) {
|
|
47
|
+
if (isImportDeclaration(statement)) {
|
|
48
|
+
const clause = statement.importClause;
|
|
49
|
+
const bindings = clause?.namedBindings;
|
|
50
|
+
name(statement.moduleSpecifier, [
|
|
51
|
+
...(clause?.name === undefined ? [] : ["default"]),
|
|
52
|
+
...(bindings === undefined || isNamespaceImport(bindings)
|
|
53
|
+
? []
|
|
54
|
+
: bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
|
|
55
|
+
], bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings));
|
|
56
|
+
}
|
|
57
|
+
else if (isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
|
|
58
|
+
const clause = statement.exportClause;
|
|
59
|
+
const star = clause === undefined || isNamespaceExport(clause);
|
|
60
|
+
name(statement.moduleSpecifier, star ? [] : clause.elements.map((one) => (one.propertyName ?? one.name).text), star);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const visit = (node) => {
|
|
64
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
65
|
+
const [argument] = node.arguments;
|
|
66
|
+
if (argument !== undefined) {
|
|
67
|
+
name(argument, [], true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
node.forEachChild(visit);
|
|
71
|
+
};
|
|
72
|
+
file.forEachChild(visit);
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
/** Each of the given paths below the target root, parsed; one that cannot be read is skipped. */
|
|
76
|
+
export function testFilesAt(host, targetRoot, paths, parse) {
|
|
77
|
+
return paths.flatMap((path) => {
|
|
78
|
+
const fileName = joinPath(targetRoot, path);
|
|
79
|
+
let text;
|
|
80
|
+
try {
|
|
81
|
+
text = host.readFile(fileName);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return [{ path, file: parse(fileName, text) }];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The references the given test files make to one project's own files through the
|
|
91
|
+
* modules they name ({@link modulesNamed}). A name read from a module references each
|
|
92
|
+
* top-level declaration the file exports under it; an evaluating form references the
|
|
93
|
+
* file. A query after the path names the file as {@link queriedModulePath} reads it.
|
|
94
|
+
*/
|
|
95
|
+
export function outsideTestReferences(tests, held, targetRoot) {
|
|
96
|
+
const fileAt = new Map(held.symbols
|
|
97
|
+
.filter((symbol) => symbol.kind === "file")
|
|
98
|
+
.map((symbol) => [symbol.position.path, symbol.id]));
|
|
99
|
+
// Keyed on the reference's fragment, which spells the export name where the display
|
|
100
|
+
// name of a default export is its declared name.
|
|
101
|
+
const exported = new Map();
|
|
102
|
+
for (const symbol of held.symbols) {
|
|
103
|
+
if (symbol.exported) {
|
|
104
|
+
const fragment = symbol.ref.slice(symbol.ref.lastIndexOf("#") + 1).replace(/:alias$/u, "");
|
|
105
|
+
const key = `${symbol.parent}\u0000${fragment}`;
|
|
106
|
+
exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const found = [];
|
|
110
|
+
for (const { path, file } of tests) {
|
|
111
|
+
const from = `${path}#`;
|
|
112
|
+
const dir = dirnamePath(path) === "." ? "" : dirnamePath(path);
|
|
113
|
+
for (const named of modulesNamed(file)) {
|
|
114
|
+
const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
|
|
115
|
+
if (written === undefined) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const fileId = candidates(dir, written)
|
|
119
|
+
.map((one) => fileAt.get(one))
|
|
120
|
+
.find((one) => one !== undefined);
|
|
121
|
+
if (fileId === undefined) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const position = renderPosition(file, targetRoot, named.specifier.getStart());
|
|
125
|
+
const read = (to, use) => {
|
|
126
|
+
found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
|
|
127
|
+
};
|
|
128
|
+
if (named.evaluates) {
|
|
129
|
+
read(fileId, "evaluation");
|
|
130
|
+
}
|
|
131
|
+
for (const name of named.names) {
|
|
132
|
+
for (const id of exported.get(`${fileId}\u0000${nameComponent(name)}`) ?? []) {
|
|
133
|
+
read(id, "read");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|