@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/src/analysis.ts
CHANGED
|
@@ -46,7 +46,8 @@ import { enumGroup } from "./enum-group.ts";
|
|
|
46
46
|
import { generatedFile } from "./generated-file.ts";
|
|
47
47
|
import { sourceFilesOf } from "./source-files.ts";
|
|
48
48
|
import {
|
|
49
|
-
|
|
49
|
+
evidenceRecords,
|
|
50
|
+
heldRecords,
|
|
50
51
|
disabledClasses,
|
|
51
52
|
exemptionsOf,
|
|
52
53
|
holdingWhile,
|
|
@@ -56,7 +57,8 @@ import {
|
|
|
56
57
|
type Detectors,
|
|
57
58
|
} from "./exempt.ts";
|
|
58
59
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
59
|
-
import { heldByInclusion, sourceTree } from "./file-facts.ts";
|
|
60
|
+
import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.ts";
|
|
61
|
+
import { globExpression } from "./glob.ts";
|
|
60
62
|
import type { Finding } from "./finding.ts";
|
|
61
63
|
import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
|
|
62
64
|
import type { EmitterInput } from "./findings/emitter.ts";
|
|
@@ -72,7 +74,7 @@ import { injectionContainer } from "./injection-container.ts";
|
|
|
72
74
|
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
73
75
|
import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
|
|
74
76
|
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
75
|
-
import { readManifest } from "./manifest.ts";
|
|
77
|
+
import { packageEntries, readManifest } from "./manifest.ts";
|
|
76
78
|
import {
|
|
77
79
|
liveFilesAt,
|
|
78
80
|
matrixOf,
|
|
@@ -82,7 +84,7 @@ import {
|
|
|
82
84
|
type Matrix,
|
|
83
85
|
type SweepResult,
|
|
84
86
|
} from "./matrix.ts";
|
|
85
|
-
import { joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
87
|
+
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
86
88
|
import type { UnansweredQuestion } from "./query.ts";
|
|
87
89
|
import { byPosition, isComponentFile, type Position } from "./position.ts";
|
|
88
90
|
import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
|
|
@@ -99,6 +101,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
|
|
|
99
101
|
import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
|
|
100
102
|
import type { Scope } from "./scope.ts";
|
|
101
103
|
import { serializationContract } from "./serialization-contract.ts";
|
|
104
|
+
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
102
105
|
import { supportReferences, testSupportFiles } from "./test-support.ts";
|
|
103
106
|
import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
104
107
|
import { accessorsOf, storesOf } from "./stores.ts";
|
|
@@ -219,6 +222,8 @@ interface ReadProjects<Answer> {
|
|
|
219
222
|
readonly componentsUnread: readonly ComponentsUnread[];
|
|
220
223
|
/** The convention rows the run applied. */
|
|
221
224
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
225
|
+
/** The test files below the target root no project the run reads holds, parsed. */
|
|
226
|
+
readonly outsideTests: readonly OutsideTest[];
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
/**
|
|
@@ -284,7 +289,15 @@ function readProjects<Answer>(
|
|
|
284
289
|
const options = {
|
|
285
290
|
host,
|
|
286
291
|
manifest: workspace?.manifest ?? readManifest(host, targetRoot),
|
|
287
|
-
|
|
292
|
+
otherPackages: [
|
|
293
|
+
...(workspace?.entries ?? []),
|
|
294
|
+
...packageEntries(
|
|
295
|
+
host,
|
|
296
|
+
normalizePath(absoluteRoot),
|
|
297
|
+
discovered.configFiles,
|
|
298
|
+
new Set(workspace?.memberDirs ?? []),
|
|
299
|
+
),
|
|
300
|
+
],
|
|
288
301
|
patterns: config.rootPatterns,
|
|
289
302
|
entryFiles: config.ts.entryFiles,
|
|
290
303
|
conventions,
|
|
@@ -304,6 +317,8 @@ function readProjects<Answer>(
|
|
|
304
317
|
const unbuildable: SetupFailure[] = [];
|
|
305
318
|
const setupDropped: SetupFailure[] = [];
|
|
306
319
|
const consumed = new Map<string, Reference>();
|
|
320
|
+
const heldFiles = new Set<string>();
|
|
321
|
+
let outsideTests: readonly OutsideTest[] = [];
|
|
307
322
|
const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
|
|
308
323
|
const [first] = met;
|
|
309
324
|
if (first !== undefined && isGuess(discovered, configFile)) {
|
|
@@ -434,10 +449,24 @@ function readProjects<Answer>(
|
|
|
434
449
|
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
435
450
|
};
|
|
436
451
|
};
|
|
452
|
+
for (const file of project.ownSourceFiles()) {
|
|
453
|
+
heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
|
|
454
|
+
}
|
|
437
455
|
return stage(project, { configuration, read });
|
|
438
456
|
},
|
|
439
457
|
discovered.derived ? "record" : "refuse",
|
|
440
458
|
workspace?.programs,
|
|
459
|
+
() => {
|
|
460
|
+
const patterns = config.ts.testFiles.map(globExpression);
|
|
461
|
+
outsideTests = testFilesAt(
|
|
462
|
+
host,
|
|
463
|
+
targetRoot,
|
|
464
|
+
sourcePaths(host, targetRoot).filter(
|
|
465
|
+
(path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path)),
|
|
466
|
+
),
|
|
467
|
+
options.parse,
|
|
468
|
+
);
|
|
469
|
+
},
|
|
441
470
|
);
|
|
442
471
|
for (const configFile of session.unopened) {
|
|
443
472
|
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
@@ -475,6 +504,7 @@ function readProjects<Answer>(
|
|
|
475
504
|
unanswered: session.unanswered,
|
|
476
505
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
477
506
|
conventionsApplied: conventions.applied,
|
|
507
|
+
outsideTests,
|
|
478
508
|
};
|
|
479
509
|
}
|
|
480
510
|
|
|
@@ -630,6 +660,17 @@ interface SweptProject<Extra> {
|
|
|
630
660
|
readonly extra: Extra;
|
|
631
661
|
}
|
|
632
662
|
|
|
663
|
+
/** What one project's stage reads while its view is open, before the run's test-support code is known. */
|
|
664
|
+
interface ReadProject<Extra> {
|
|
665
|
+
readonly projectRead: ProjectRead;
|
|
666
|
+
readonly references: readonly Reference[];
|
|
667
|
+
readonly testFiles: readonly string[];
|
|
668
|
+
readonly evidence: readonly Exemption[];
|
|
669
|
+
readonly componentFiles: readonly string[];
|
|
670
|
+
readonly componentWarnings: readonly ComponentWarning[];
|
|
671
|
+
readonly extra: Extra;
|
|
672
|
+
}
|
|
673
|
+
|
|
633
674
|
/** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
|
|
634
675
|
interface ReadRun<Extra> {
|
|
635
676
|
readonly matrix: Matrix;
|
|
@@ -674,18 +715,12 @@ function readRun<Extra>(
|
|
|
674
715
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
675
716
|
: readTemplates(host, targetRoot, config.analysis);
|
|
676
717
|
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
677
|
-
const read = readProjects<
|
|
718
|
+
const read = readProjects<ReadProject<Extra>>(engine, host, scope, config, (project, opened) => {
|
|
678
719
|
const projectRead = opened.read();
|
|
679
720
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
680
721
|
testFiles: config.ts.testFiles,
|
|
681
722
|
});
|
|
682
|
-
const
|
|
683
|
-
projectRead.held.symbols,
|
|
684
|
-
resolved.references,
|
|
685
|
-
projectRead.rooted.liveUnderReachability,
|
|
686
|
-
resolved.testFilePaths,
|
|
687
|
-
);
|
|
688
|
-
const detected = detectExemptions(
|
|
723
|
+
const evidence = evidenceRecords(
|
|
689
724
|
{
|
|
690
725
|
project,
|
|
691
726
|
held: projectRead.held,
|
|
@@ -698,56 +733,87 @@ function readRun<Extra>(
|
|
|
698
733
|
: { generated: projectRead.generated }),
|
|
699
734
|
},
|
|
700
735
|
detectors,
|
|
701
|
-
|
|
702
|
-
disabled,
|
|
703
|
-
mode,
|
|
704
|
-
testFiles: new Set([...resolved.testFilePaths, ...support]),
|
|
705
|
-
},
|
|
736
|
+
disabled,
|
|
706
737
|
);
|
|
738
|
+
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
739
|
+
const pathOf = (file: (typeof components)[number]): string =>
|
|
740
|
+
relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
741
|
+
return {
|
|
742
|
+
projectRead,
|
|
743
|
+
references: resolved.references,
|
|
744
|
+
testFiles: resolved.testFilePaths,
|
|
745
|
+
evidence,
|
|
746
|
+
componentFiles: components.map(pathOf),
|
|
747
|
+
componentWarnings: components.flatMap((file) =>
|
|
748
|
+
readComponent(file.originalText).warnings.map((warning) => ({
|
|
749
|
+
path: pathOf(file),
|
|
750
|
+
line: lineOf(file.originalText, warning.offset),
|
|
751
|
+
reason: warning.reason,
|
|
752
|
+
})),
|
|
753
|
+
),
|
|
754
|
+
extra: extra(project, projectRead, resolved.references),
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
const outsidePaths = read.outsideTests.map((one) => one.path);
|
|
758
|
+
const withOutside = read.answers.map((one) => ({
|
|
759
|
+
...one,
|
|
760
|
+
references: [
|
|
761
|
+
...one.references,
|
|
762
|
+
...outsideTestReferences(read.outsideTests, one.projectRead.held, targetRoot),
|
|
763
|
+
],
|
|
764
|
+
testFiles: [...one.testFiles, ...outsidePaths],
|
|
765
|
+
}));
|
|
766
|
+
// Test-support code is code only test code imports in any configuration of the run: a
|
|
767
|
+
// configuration that holds no test file says nothing about what the tests import.
|
|
768
|
+
const support = testSupportFiles(
|
|
769
|
+
withOutside.flatMap((one) => one.projectRead.held.symbols),
|
|
770
|
+
withOutside.flatMap((one) => one.references),
|
|
771
|
+
withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
|
|
772
|
+
withOutside.flatMap((one) => one.testFiles),
|
|
773
|
+
);
|
|
774
|
+
const answers = withOutside.map((one): SweptProject<Extra> => {
|
|
775
|
+
const { projectRead } = one;
|
|
776
|
+
const detected = heldRecords(one.evidence, {
|
|
777
|
+
disabled,
|
|
778
|
+
mode,
|
|
779
|
+
testFiles: new Set([...one.testFiles, ...support]),
|
|
780
|
+
});
|
|
707
781
|
const exempt = detected.records;
|
|
708
|
-
const made = supportReferences(
|
|
782
|
+
const made = supportReferences(one.references, support);
|
|
783
|
+
const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
|
|
784
|
+
const supportFiles = [...support].filter((path) => held.has(path)).sort();
|
|
709
785
|
const configured: Configured = {
|
|
710
786
|
configuration: projectRead.configuration,
|
|
711
787
|
symbols: projectRead.held.symbols,
|
|
712
788
|
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
713
789
|
roots: projectRead.rooted.liveUnderReachability,
|
|
714
|
-
testFiles:
|
|
715
|
-
...(
|
|
790
|
+
testFiles: one.testFiles,
|
|
791
|
+
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
716
792
|
...(projectRead.referenceOnly.length === 0
|
|
717
793
|
? {}
|
|
718
794
|
: { referenceOnly: projectRead.referenceOnly }),
|
|
719
795
|
};
|
|
720
|
-
const unanswered = [
|
|
721
|
-
...projectRead.held.unanswered,
|
|
722
|
-
...projectRead.rooted.unanswered,
|
|
723
|
-
...made
|
|
724
|
-
.filter((reference) => reference.resolution === "by-name")
|
|
725
|
-
.map((reference) => reference.to),
|
|
726
|
-
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
727
|
-
];
|
|
728
|
-
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
729
|
-
const pathOf = (file: (typeof components)[number]): string =>
|
|
730
|
-
relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
731
796
|
return {
|
|
732
797
|
configFile: projectRead.configFile,
|
|
733
798
|
configured,
|
|
734
799
|
exempt,
|
|
735
|
-
unanswered
|
|
800
|
+
unanswered: [
|
|
801
|
+
...projectRead.held.unanswered,
|
|
802
|
+
...projectRead.rooted.unanswered,
|
|
803
|
+
...made
|
|
804
|
+
.filter((reference) => reference.resolution === "by-name")
|
|
805
|
+
.map((reference) => reference.to),
|
|
806
|
+
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
807
|
+
],
|
|
736
808
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
737
809
|
skipped: projectRead.skipped,
|
|
738
|
-
componentFiles:
|
|
739
|
-
componentWarnings:
|
|
740
|
-
|
|
741
|
-
path: pathOf(file),
|
|
742
|
-
line: lineOf(file.originalText, warning.offset),
|
|
743
|
-
reason: warning.reason,
|
|
744
|
-
})),
|
|
745
|
-
),
|
|
746
|
-
extra: extra(project, projectRead, made),
|
|
810
|
+
componentFiles: one.componentFiles,
|
|
811
|
+
componentWarnings: one.componentWarnings,
|
|
812
|
+
extra: one.extra,
|
|
747
813
|
};
|
|
748
814
|
});
|
|
749
815
|
const consumed = read.consumers.references;
|
|
750
|
-
const projects =
|
|
816
|
+
const projects = answers.map((one) =>
|
|
751
817
|
consumed.length === 0
|
|
752
818
|
? one
|
|
753
819
|
: {
|
|
@@ -763,7 +829,7 @@ function readRun<Extra>(
|
|
|
763
829
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
764
830
|
unansweredHeld: [
|
|
765
831
|
...new Set([
|
|
766
|
-
...
|
|
832
|
+
...answers.flatMap((one) => one.unanswered),
|
|
767
833
|
...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
|
|
768
834
|
]),
|
|
769
835
|
],
|
|
@@ -57,7 +57,7 @@ const COUNTERPARTS: readonly (readonly [string, readonly string[]])[] = [
|
|
|
57
57
|
const APPENDED = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"] as const;
|
|
58
58
|
|
|
59
59
|
/** The extensions the program's module resolution appends to a specifier written without one. */
|
|
60
|
-
const RESOLVED_EXTENSIONS = [
|
|
60
|
+
export const RESOLVED_EXTENSIONS = [
|
|
61
61
|
".ts",
|
|
62
62
|
".tsx",
|
|
63
63
|
".d.ts",
|
package/src/contextual-uses.ts
CHANGED
|
@@ -99,9 +99,6 @@ interface Checked {
|
|
|
99
99
|
readonly targets: readonly Type[];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/** How many element and signature steps the structural walk takes below a value's own type. */
|
|
103
|
-
const MAX_DEPTH = 3;
|
|
104
|
-
|
|
105
102
|
/** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
|
|
106
103
|
interface Member {
|
|
107
104
|
readonly ids: readonly string[];
|
|
@@ -254,12 +251,12 @@ export function contextualUses<Brand>(
|
|
|
254
251
|
* Each object type a value of `source` is checked as against the object types of
|
|
255
252
|
* `target`: the value's own, and through array elements and through a function's
|
|
256
253
|
* parameters, checked the other way, and its result. A source that is one of the
|
|
257
|
-
* target's own object types matches it as it stands.
|
|
254
|
+
* target's own object types matches it as it stands. Each pair is compared once, which
|
|
255
|
+
* ends the walk over a recursive type.
|
|
258
256
|
*/
|
|
259
257
|
const compared = (
|
|
260
258
|
source: Type | undefined,
|
|
261
259
|
target: Type | undefined,
|
|
262
|
-
depth: number,
|
|
263
260
|
into: Checked[],
|
|
264
261
|
seen: Set<string>,
|
|
265
262
|
): void => {
|
|
@@ -267,7 +264,7 @@ export function contextualUses<Brand>(
|
|
|
267
264
|
if (source === undefined || target === undefined || source.id === target.id) {
|
|
268
265
|
return;
|
|
269
266
|
}
|
|
270
|
-
if (
|
|
267
|
+
if (seen.has(key)) {
|
|
271
268
|
return;
|
|
272
269
|
}
|
|
273
270
|
seen.add(key);
|
|
@@ -281,7 +278,7 @@ export function contextualUses<Brand>(
|
|
|
281
278
|
const called = element === undefined ? callOf(one) : undefined;
|
|
282
279
|
for (const part of targets) {
|
|
283
280
|
if (element !== undefined) {
|
|
284
|
-
compared(element, elementOf(part),
|
|
281
|
+
compared(element, elementOf(part), into, seen);
|
|
285
282
|
continue;
|
|
286
283
|
}
|
|
287
284
|
const other = called === undefined ? undefined : callOf(part);
|
|
@@ -289,30 +286,37 @@ export function contextualUses<Brand>(
|
|
|
289
286
|
continue;
|
|
290
287
|
}
|
|
291
288
|
other.parameters.forEach((parameter, index) => {
|
|
292
|
-
compared(parameter, called.parameters[index],
|
|
289
|
+
compared(parameter, called.parameters[index], into, seen);
|
|
293
290
|
});
|
|
294
|
-
compared(called.result, other.result,
|
|
291
|
+
compared(called.result, other.result, into, seen);
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
294
|
};
|
|
298
295
|
|
|
299
|
-
/**
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Whether a value of one type may carry a member a position reads, at any depth the
|
|
298
|
+
* walk takes. A type met again on the way adds nothing its first visit did not.
|
|
299
|
+
*/
|
|
300
|
+
const mayCarry = (type: Type, seen: Set<number> = new Set()): boolean => {
|
|
301
|
+
if (seen.has(type.id)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
seen.add(type.id);
|
|
305
|
+
return partsOf(type).some((part) => {
|
|
303
306
|
if (carries(part)) {
|
|
304
307
|
return true;
|
|
305
308
|
}
|
|
306
309
|
const element = elementOf(part);
|
|
307
310
|
if (element !== undefined) {
|
|
308
|
-
return mayCarry(element,
|
|
311
|
+
return mayCarry(element, seen);
|
|
309
312
|
}
|
|
310
313
|
return callOf(part) !== undefined;
|
|
311
314
|
});
|
|
315
|
+
};
|
|
312
316
|
|
|
313
317
|
const sourced = carried.flatMap((flow, index) => {
|
|
314
318
|
const type = types[index];
|
|
315
|
-
return type === undefined || !isAnswered(type) || !mayCarry(type
|
|
319
|
+
return type === undefined || !isAnswered(type) || !mayCarry(type) ? [] : [{ flow, type }];
|
|
316
320
|
});
|
|
317
321
|
batches += batchesOf(sourced.length);
|
|
318
322
|
const positions = queries.contextualTypes(
|
|
@@ -326,7 +330,7 @@ export function contextualUses<Brand>(
|
|
|
326
330
|
return;
|
|
327
331
|
}
|
|
328
332
|
const checked: Checked[] = [];
|
|
329
|
-
compared(type, position,
|
|
333
|
+
compared(type, position, checked, new Set());
|
|
330
334
|
const ids = new Set<string>();
|
|
331
335
|
for (const { source, targets } of checked) {
|
|
332
336
|
const tables = targets.map(membersOf);
|
package/src/exempt.ts
CHANGED
|
@@ -187,12 +187,23 @@ export function detectExemptions<Brand>(
|
|
|
187
187
|
detectors: Detectors,
|
|
188
188
|
holding: Holding,
|
|
189
189
|
): Detected {
|
|
190
|
+
return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Every record one project's detectors found, one per piece of evidence, before the
|
|
195
|
+
* mode decides which of them hold: what {@link heldRecords} reads.
|
|
196
|
+
*/
|
|
197
|
+
export function evidenceRecords<Brand>(
|
|
198
|
+
input: DetectorInput<Brand>,
|
|
199
|
+
detectors: Detectors,
|
|
200
|
+
disabled: ReadonlySet<ExemptionClass>,
|
|
201
|
+
): readonly Exemption[] {
|
|
190
202
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
191
203
|
const found: Exemption[] = [];
|
|
192
|
-
const inTestFiles: Exemption[] = [];
|
|
193
204
|
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
194
205
|
const detect = detectors.get(exemptionClass);
|
|
195
|
-
if (detect === undefined ||
|
|
206
|
+
if (detect === undefined || disabled.has(exemptionClass)) {
|
|
196
207
|
continue;
|
|
197
208
|
}
|
|
198
209
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
@@ -200,17 +211,26 @@ export function detectExemptions<Brand>(
|
|
|
200
211
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
201
212
|
continue;
|
|
202
213
|
}
|
|
203
|
-
|
|
214
|
+
found.push({
|
|
204
215
|
id: evidence.id,
|
|
205
216
|
class: exemptionClass,
|
|
206
217
|
detail: evidence.detail,
|
|
207
218
|
site: evidence.site,
|
|
208
219
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
227
|
+
export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
|
|
228
|
+
const found: Exemption[] = [];
|
|
229
|
+
const inTestFiles: Exemption[] = [];
|
|
230
|
+
for (const record of records) {
|
|
231
|
+
if (holding.mode.production && holding.testFiles.has(record.site.path)) {
|
|
232
|
+
inTestFiles.push(record);
|
|
233
|
+
} else {
|
|
214
234
|
found.push(record);
|
|
215
235
|
}
|
|
216
236
|
}
|
package/src/file-facts.ts
CHANGED
|
@@ -65,13 +65,11 @@ function lastLine(text: string): number {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
* read is skipped, as discovery skips it
|
|
70
|
-
* with it: a tree the analysis may not enter holds no file it can report.
|
|
68
|
+
* The path below the target root of every source file there, in path order. A directory
|
|
69
|
+
* that cannot be read is skipped, as discovery skips it.
|
|
71
70
|
*/
|
|
72
|
-
export function
|
|
73
|
-
const
|
|
74
|
-
const found: TreeFile[] = [];
|
|
71
|
+
export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
|
|
72
|
+
const found: string[] = [];
|
|
75
73
|
const pending = [""];
|
|
76
74
|
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
77
75
|
let entries: readonly { readonly name: string; readonly directory: boolean }[];
|
|
@@ -88,23 +86,29 @@ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[]
|
|
|
88
86
|
}
|
|
89
87
|
continue;
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
89
|
+
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
90
|
+
found.push(path);
|
|
93
91
|
}
|
|
94
|
-
let text: string;
|
|
95
|
-
try {
|
|
96
|
-
text = host.readFile(joinPath(targetRoot, path));
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
found.push({
|
|
101
|
-
path,
|
|
102
|
-
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
103
|
-
endLine: lastLine(text),
|
|
104
|
-
});
|
|
105
92
|
}
|
|
106
93
|
}
|
|
107
|
-
return found.sort((a, b) => (a
|
|
94
|
+
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every source file below the target root, in path order. A file that cannot be read is
|
|
99
|
+
* skipped: a tree the analysis may not enter holds no file it can report.
|
|
100
|
+
*/
|
|
101
|
+
export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
|
|
102
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
103
|
+
return sourcePaths(host, targetRoot).flatMap((path) => {
|
|
104
|
+
let text: string;
|
|
105
|
+
try {
|
|
106
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
|
|
111
|
+
});
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
/** Whether one file changes what the program sees by being included in it. */
|
package/src/graph.ts
CHANGED
|
@@ -122,9 +122,9 @@ export interface Graph {
|
|
|
122
122
|
/** Every root naming a declaration the inventory holds, in the order the root set gives them. */
|
|
123
123
|
readonly rooted: readonly Rooted[];
|
|
124
124
|
/**
|
|
125
|
-
* Every reference a loaded consumer makes to a declaration the
|
|
126
|
-
* order given.
|
|
127
|
-
* names live under both relations and reaches on from there.
|
|
125
|
+
* Every reference a loaded consumer or an unheld file makes to a declaration the
|
|
126
|
+
* inventory holds, in the order given. Such a file is a caller the analysis read, so the
|
|
127
|
+
* sweep holds what it names live under both relations and reaches on from there.
|
|
128
128
|
*/
|
|
129
129
|
readonly consumed: readonly Edge[];
|
|
130
130
|
/** The position of the declaration one identifier names, or {@link OUTSIDE}. */
|
|
@@ -167,7 +167,7 @@ export function graphOf(
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
|
|
170
|
-
if (reference.consumer !== undefined) {
|
|
170
|
+
if (reference.consumer !== undefined || reference.unheld === true) {
|
|
171
171
|
consumed.push(edge);
|
|
172
172
|
continue;
|
|
173
173
|
}
|
package/src/inventory.ts
CHANGED
|
@@ -470,7 +470,20 @@ function nameNodeOf(node: Node): Node | undefined {
|
|
|
470
470
|
* needs the declaration this enumeration made for it.
|
|
471
471
|
*/
|
|
472
472
|
export function nodeKey(file: SourceFile, node: Node): string {
|
|
473
|
-
return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
|
|
473
|
+
return `${file.fileName}:${String(node.pos)}:${String(node.end)}:${String(node.kind)}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** `export default`, up to the keyword that stands where an unnamed default export's name would. */
|
|
477
|
+
const DEFAULT_EXPORT = /^export\s+(?=default\b)/u;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Where an unnamed declaration is placed: at `default` when it is the default export, so it
|
|
481
|
+
* never shares the file's own position, else at its first token.
|
|
482
|
+
*/
|
|
483
|
+
function unnamedStart(node: Node): number {
|
|
484
|
+
const start = node.getStart();
|
|
485
|
+
const prefix = DEFAULT_EXPORT.exec(node.getText());
|
|
486
|
+
return prefix === null ? start : start + prefix[0].length;
|
|
474
487
|
}
|
|
475
488
|
|
|
476
489
|
/** The kind a class element takes: a method, or a property or accessor. */
|
|
@@ -565,12 +578,11 @@ export function inventory<Brand>(
|
|
|
565
578
|
name: Node | undefined,
|
|
566
579
|
named: { readonly component: Component; readonly localName: string | undefined },
|
|
567
580
|
): Building {
|
|
568
|
-
const at = name ?? node;
|
|
569
581
|
const record = keep({
|
|
570
582
|
file: owner.file,
|
|
571
583
|
node,
|
|
572
584
|
module: owner.module,
|
|
573
|
-
position: renderPosition(owner.file, targetRoot,
|
|
585
|
+
position: renderPosition(owner.file, targetRoot, name?.getStart() ?? unnamedStart(node)),
|
|
574
586
|
endLine: renderPosition(owner.file, targetRoot, Math.max(node.end - 1, 0)).line,
|
|
575
587
|
kind,
|
|
576
588
|
parentChain: owner.kind === "file" ? [] : chainOf(owner),
|
package/src/manifest.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
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
7
|
|
|
9
8
|
import type { Host } from "./host.ts";
|
|
10
|
-
import { isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
9
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
11
10
|
|
|
12
11
|
/** The manifest's own name. */
|
|
13
12
|
const MANIFEST = "package.json";
|
|
@@ -205,3 +204,40 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
|
205
204
|
declaresExports: manifest["exports"] !== undefined,
|
|
206
205
|
};
|
|
207
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The entry points every package manifest between the target root and each compiler
|
|
210
|
+
* configuration names: each directory holding a `package.json` that a configuration sits
|
|
211
|
+
* in or below, the target root and `known` excluded, whose manifests the run reads
|
|
212
|
+
* otherwise. Each entry is spelled under its manifest's path below the target root, and
|
|
213
|
+
* is published by its own manifest's rule, as the target's own entries are.
|
|
214
|
+
*/
|
|
215
|
+
export function packageEntries(
|
|
216
|
+
host: Host,
|
|
217
|
+
targetRoot: string,
|
|
218
|
+
configFiles: readonly string[],
|
|
219
|
+
known: ReadonlySet<string>,
|
|
220
|
+
): readonly ManifestEntry[] {
|
|
221
|
+
const dirs = new Set<string>();
|
|
222
|
+
for (const configFile of configFiles) {
|
|
223
|
+
for (
|
|
224
|
+
let dir = dirnamePath(resolvePath(targetRoot, configFile));
|
|
225
|
+
dir !== targetRoot && relativePath(targetRoot, dir) !== undefined;
|
|
226
|
+
dir = dirnamePath(dir)
|
|
227
|
+
) {
|
|
228
|
+
if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
|
|
229
|
+
dirs.add(dir);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return [...dirs].sort().flatMap((dir) => {
|
|
234
|
+
const manifest = readManifest(host, dir);
|
|
235
|
+
const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
|
|
236
|
+
return manifest.entries.map((entry) => ({
|
|
237
|
+
...entry,
|
|
238
|
+
member: `${where} ${entry.member}`,
|
|
239
|
+
published:
|
|
240
|
+
entry.published && (!manifest.declaresExports || entry.member.startsWith("exports")),
|
|
241
|
+
}));
|
|
242
|
+
});
|
|
243
|
+
}
|