@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
|
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
|
|
|
87
87
|
}
|
|
88
88
|
return at + 1;
|
|
89
89
|
}
|
|
90
|
-
/** The
|
|
91
|
-
function
|
|
90
|
+
/** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
|
|
91
|
+
function substitutionEnd(text, from) {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let at = from;
|
|
94
|
+
while (at < text.length) {
|
|
95
|
+
const char = text[at];
|
|
96
|
+
if (char === '"' || char === "'") {
|
|
97
|
+
at = stringEnd(text, at);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "`") {
|
|
101
|
+
at = templateEnd(text, at, () => undefined);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === "{") {
|
|
105
|
+
depth += 1;
|
|
106
|
+
}
|
|
107
|
+
else if (char === "}") {
|
|
108
|
+
depth -= 1;
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
return at + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
at += 1;
|
|
114
|
+
}
|
|
115
|
+
return at;
|
|
116
|
+
}
|
|
117
|
+
/** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
|
|
118
|
+
function templateEnd(text, from, substitution) {
|
|
119
|
+
let at = from + 1;
|
|
120
|
+
while (at < text.length && text[at] !== "`") {
|
|
121
|
+
if (text[at] === "\\") {
|
|
122
|
+
at += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
126
|
+
const start = at + 2;
|
|
127
|
+
const end = substitutionEnd(text, start);
|
|
128
|
+
substitution(text.slice(start, end - 1), start);
|
|
129
|
+
at = end;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
at += 1;
|
|
133
|
+
}
|
|
134
|
+
return at + 1;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The identifiers one action's inner text writes outside a quoted string, with their offsets
|
|
138
|
+
* in it. In `script` text, a component's markup, a template literal's substitutions are code.
|
|
139
|
+
*/
|
|
140
|
+
function identifiersOf(inner, script = false) {
|
|
92
141
|
const found = [];
|
|
93
142
|
let at = 0;
|
|
94
143
|
while (at < inner.length) {
|
|
95
144
|
const char = inner[at] ?? "";
|
|
145
|
+
if (char === "`" && script) {
|
|
146
|
+
at = templateEnd(inner, at, (expression, offset) => {
|
|
147
|
+
for (const one of identifiersOf(expression, true)) {
|
|
148
|
+
found.push({ name: one.name, offset: offset + one.offset });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
96
153
|
if (char === '"' || char === "'" || char === "`") {
|
|
97
154
|
at = stringEnd(inner, at);
|
|
98
155
|
continue;
|
|
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
|
|
|
142
199
|
}
|
|
143
200
|
/** Every identifier the markup of one component file writes in an action. */
|
|
144
201
|
function markupNamesOf(text) {
|
|
145
|
-
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
|
|
202
|
+
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
|
|
146
203
|
name,
|
|
147
204
|
offset: one.start + offset,
|
|
148
205
|
action: one.action,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values a program hands to a position the checker types from its context: the
|
|
3
|
+
* initializer of a declaration whose type is written, an assignment's right side, an
|
|
4
|
+
* argument, a returned value, an arrow function's expression body, an array element, an
|
|
5
|
+
* object property, and the operand of `as` or `satisfies`.
|
|
6
|
+
*/
|
|
7
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isIdentifier, isNewExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
|
+
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
9
|
+
const STORES = new Set([
|
|
10
|
+
SyntaxKind.EqualsToken,
|
|
11
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
12
|
+
SyntaxKind.BarBarEqualsToken,
|
|
13
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
14
|
+
]);
|
|
15
|
+
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
16
|
+
function writesType(node) {
|
|
17
|
+
return node.type !== undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The values one written expression hands on: the expression itself, or what it
|
|
21
|
+
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
22
|
+
* one of their operands.
|
|
23
|
+
*/
|
|
24
|
+
export function valuesOf(expression) {
|
|
25
|
+
if (isParenthesizedExpression(expression)) {
|
|
26
|
+
return valuesOf(expression.expression);
|
|
27
|
+
}
|
|
28
|
+
if (isConditionalExpression(expression)) {
|
|
29
|
+
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
30
|
+
}
|
|
31
|
+
if (isBinaryExpression(expression)) {
|
|
32
|
+
const operator = expression.operatorToken.kind;
|
|
33
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
34
|
+
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
35
|
+
}
|
|
36
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
37
|
+
return valuesOf(expression.right);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [expression];
|
|
41
|
+
}
|
|
42
|
+
/** The expressions one node hands to a position typed by its context. */
|
|
43
|
+
export function handedOn(node) {
|
|
44
|
+
if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
45
|
+
node.initializer !== undefined &&
|
|
46
|
+
writesType(node)) {
|
|
47
|
+
return [node.initializer];
|
|
48
|
+
}
|
|
49
|
+
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
50
|
+
return [node.right];
|
|
51
|
+
}
|
|
52
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
53
|
+
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
54
|
+
}
|
|
55
|
+
if (isReturnStatement(node)) {
|
|
56
|
+
return node.expression === undefined ? [] : [node.expression];
|
|
57
|
+
}
|
|
58
|
+
if (isArrowFunction(node)) {
|
|
59
|
+
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
60
|
+
}
|
|
61
|
+
if (isArrayLiteralExpression(node)) {
|
|
62
|
+
return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
|
|
63
|
+
}
|
|
64
|
+
if (isPropertyAssignment(node)) {
|
|
65
|
+
return [node.initializer];
|
|
66
|
+
}
|
|
67
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
68
|
+
return isIdentifier(node.name) ? [node.name] : [];
|
|
69
|
+
}
|
|
70
|
+
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
71
|
+
return [node.expression];
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@eslint/js": "10.0.1",
|
|
36
|
-
"@types/node": "24.19.
|
|
36
|
+
"@types/node": "24.19.1",
|
|
37
37
|
"eslint": "10.11.0",
|
|
38
38
|
"fast-check": "4.10.2",
|
|
39
39
|
"prettier": "3.9.9",
|
package/src/analysis.ts
CHANGED
|
@@ -36,14 +36,18 @@ import {
|
|
|
36
36
|
} from "./discover.ts";
|
|
37
37
|
import {
|
|
38
38
|
conventionFailuresFor,
|
|
39
|
+
generatedFiles,
|
|
39
40
|
readConventions,
|
|
40
41
|
type AppliedConvention,
|
|
41
42
|
type ConventionManifest,
|
|
42
43
|
} from "./conventions.ts";
|
|
43
44
|
import { readEdgeSides } from "./edges.ts";
|
|
44
45
|
import { enumGroup } from "./enum-group.ts";
|
|
46
|
+
import { generatedFile } from "./generated-file.ts";
|
|
47
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
45
48
|
import {
|
|
46
|
-
|
|
49
|
+
evidenceRecords,
|
|
50
|
+
heldRecords,
|
|
47
51
|
disabledClasses,
|
|
48
52
|
exemptionsOf,
|
|
49
53
|
holdingWhile,
|
|
@@ -53,7 +57,8 @@ import {
|
|
|
53
57
|
type Detectors,
|
|
54
58
|
} from "./exempt.ts";
|
|
55
59
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
56
|
-
import { heldByInclusion, sourceTree } from "./file-facts.ts";
|
|
60
|
+
import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.ts";
|
|
61
|
+
import { globExpression } from "./glob.ts";
|
|
57
62
|
import type { Finding } from "./finding.ts";
|
|
58
63
|
import { findingsPass, recordedFindings, rowKey, type PassResult } from "./findings-pass.ts";
|
|
59
64
|
import type { EmitterInput } from "./findings/emitter.ts";
|
|
@@ -69,7 +74,7 @@ import { injectionContainer } from "./injection-container.ts";
|
|
|
69
74
|
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
70
75
|
import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
|
|
71
76
|
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
72
|
-
import { readManifest } from "./manifest.ts";
|
|
77
|
+
import { packageEntries, readManifest } from "./manifest.ts";
|
|
73
78
|
import {
|
|
74
79
|
liveFilesAt,
|
|
75
80
|
matrixOf,
|
|
@@ -79,7 +84,7 @@ import {
|
|
|
79
84
|
type Matrix,
|
|
80
85
|
type SweepResult,
|
|
81
86
|
} from "./matrix.ts";
|
|
82
|
-
import { joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
87
|
+
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
83
88
|
import type { UnansweredQuestion } from "./query.ts";
|
|
84
89
|
import { byPosition, isComponentFile, type Position } from "./position.ts";
|
|
85
90
|
import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
|
|
@@ -96,6 +101,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
|
|
|
96
101
|
import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
|
|
97
102
|
import type { Scope } from "./scope.ts";
|
|
98
103
|
import { serializationContract } from "./serialization-contract.ts";
|
|
104
|
+
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
99
105
|
import { supportReferences, testSupportFiles } from "./test-support.ts";
|
|
100
106
|
import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
101
107
|
import { accessorsOf, storesOf } from "./stores.ts";
|
|
@@ -185,6 +191,8 @@ interface ProjectRead {
|
|
|
185
191
|
readonly typeErrorSkips: readonly TypeErrorSkip[];
|
|
186
192
|
/** The lines each of those skips withholds every finding on. */
|
|
187
193
|
readonly skipped: readonly SkippedUnit[];
|
|
194
|
+
/** The project's generated files, by path below the target root, each with the row naming it. */
|
|
195
|
+
readonly generated: ReadonlyMap<string, string>;
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
/** One target project the run analyzes, as its stage is handed it. */
|
|
@@ -214,6 +222,8 @@ interface ReadProjects<Answer> {
|
|
|
214
222
|
readonly componentsUnread: readonly ComponentsUnread[];
|
|
215
223
|
/** The convention rows the run applied. */
|
|
216
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[];
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
/**
|
|
@@ -279,11 +289,20 @@ function readProjects<Answer>(
|
|
|
279
289
|
const options = {
|
|
280
290
|
host,
|
|
281
291
|
manifest: workspace?.manifest ?? readManifest(host, targetRoot),
|
|
282
|
-
|
|
292
|
+
otherPackages: [
|
|
293
|
+
...(workspace?.entries ?? []),
|
|
294
|
+
...packageEntries(
|
|
295
|
+
host,
|
|
296
|
+
normalizePath(absoluteRoot),
|
|
297
|
+
discovered.configFiles,
|
|
298
|
+
new Set(workspace?.memberDirs ?? []),
|
|
299
|
+
),
|
|
300
|
+
],
|
|
283
301
|
patterns: config.rootPatterns,
|
|
284
302
|
entryFiles: config.ts.entryFiles,
|
|
285
303
|
conventions,
|
|
286
304
|
testFiles: config.ts.testFiles,
|
|
305
|
+
parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
|
|
287
306
|
publishedAPI: config.targetKind === "library",
|
|
288
307
|
};
|
|
289
308
|
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
@@ -298,6 +317,8 @@ function readProjects<Answer>(
|
|
|
298
317
|
const unbuildable: SetupFailure[] = [];
|
|
299
318
|
const setupDropped: SetupFailure[] = [];
|
|
300
319
|
const consumed = new Map<string, Reference>();
|
|
320
|
+
const heldFiles = new Set<string>();
|
|
321
|
+
let outsideTests: readonly OutsideTest[] = [];
|
|
301
322
|
const setupMet = (configFile: string, met: readonly SetupFailure[]): void => {
|
|
302
323
|
const [first] = met;
|
|
303
324
|
if (first !== undefined && isGuess(discovered, configFile)) {
|
|
@@ -425,12 +446,27 @@ function readProjects<Answer>(
|
|
|
425
446
|
.ownSourceFiles()
|
|
426
447
|
.filter((file) => referenceOnly(file.fileName))
|
|
427
448
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
449
|
+
generated: generatedFiles(conventions, sourceFilesOf(project, targetRoot)),
|
|
428
450
|
};
|
|
429
451
|
};
|
|
452
|
+
for (const file of project.ownSourceFiles()) {
|
|
453
|
+
heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
|
|
454
|
+
}
|
|
430
455
|
return stage(project, { configuration, read });
|
|
431
456
|
},
|
|
432
457
|
discovered.derived ? "record" : "refuse",
|
|
433
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
|
+
},
|
|
434
470
|
);
|
|
435
471
|
for (const configFile of session.unopened) {
|
|
436
472
|
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
@@ -468,6 +504,7 @@ function readProjects<Answer>(
|
|
|
468
504
|
unanswered: session.unanswered,
|
|
469
505
|
componentsUnread: workspace?.componentsUnread ?? [],
|
|
470
506
|
conventionsApplied: conventions.applied,
|
|
507
|
+
outsideTests,
|
|
471
508
|
};
|
|
472
509
|
}
|
|
473
510
|
|
|
@@ -599,6 +636,7 @@ export interface ComponentWarning {
|
|
|
599
636
|
const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
600
637
|
["interface-satisfaction", interfaceSatisfaction],
|
|
601
638
|
["enum-group", enumGroup],
|
|
639
|
+
["generated-file", generatedFile],
|
|
602
640
|
["template-field", templateField],
|
|
603
641
|
["reflective-lookup", reflectiveLookup],
|
|
604
642
|
["decorator", decorator],
|
|
@@ -622,6 +660,17 @@ interface SweptProject<Extra> {
|
|
|
622
660
|
readonly extra: Extra;
|
|
623
661
|
}
|
|
624
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
|
+
|
|
625
674
|
/** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
|
|
626
675
|
interface ReadRun<Extra> {
|
|
627
676
|
readonly matrix: Matrix;
|
|
@@ -666,56 +715,34 @@ function readRun<Extra>(
|
|
|
666
715
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
667
716
|
: readTemplates(host, targetRoot, config.analysis);
|
|
668
717
|
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
669
|
-
const read = readProjects<
|
|
718
|
+
const read = readProjects<ReadProject<Extra>>(engine, host, scope, config, (project, opened) => {
|
|
670
719
|
const projectRead = opened.read();
|
|
671
720
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
672
721
|
testFiles: config.ts.testFiles,
|
|
673
722
|
});
|
|
674
|
-
const
|
|
675
|
-
projectRead.held.symbols,
|
|
676
|
-
resolved.references,
|
|
677
|
-
projectRead.rooted.liveUnderReachability,
|
|
678
|
-
resolved.testFilePaths,
|
|
679
|
-
);
|
|
680
|
-
const made = supportReferences(resolved.references, support);
|
|
681
|
-
const configured: Configured = {
|
|
682
|
-
configuration: projectRead.configuration,
|
|
683
|
-
symbols: projectRead.held.symbols,
|
|
684
|
-
references: made,
|
|
685
|
-
roots: projectRead.rooted.liveUnderReachability,
|
|
686
|
-
testFiles: resolved.testFilePaths,
|
|
687
|
-
...(support.size === 0 ? {} : { supportFiles: [...support].sort() }),
|
|
688
|
-
...(projectRead.referenceOnly.length === 0
|
|
689
|
-
? {}
|
|
690
|
-
: { referenceOnly: projectRead.referenceOnly }),
|
|
691
|
-
};
|
|
692
|
-
const exempt = computeExemptions(
|
|
693
|
-
{ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
|
|
694
|
-
detectors,
|
|
723
|
+
const evidence = evidenceRecords(
|
|
695
724
|
{
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
725
|
+
project,
|
|
726
|
+
held: projectRead.held,
|
|
727
|
+
targetRoot,
|
|
728
|
+
templates,
|
|
729
|
+
ts: config.ts,
|
|
730
|
+
consumers,
|
|
731
|
+
...(config.analysis.generatedFiles === "include"
|
|
732
|
+
? {}
|
|
733
|
+
: { generated: projectRead.generated }),
|
|
699
734
|
},
|
|
735
|
+
detectors,
|
|
736
|
+
disabled,
|
|
700
737
|
);
|
|
701
|
-
const unanswered = [
|
|
702
|
-
...projectRead.held.unanswered,
|
|
703
|
-
...projectRead.rooted.unanswered,
|
|
704
|
-
...made
|
|
705
|
-
.filter((reference) => reference.resolution === "by-name")
|
|
706
|
-
.map((reference) => reference.to),
|
|
707
|
-
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
708
|
-
];
|
|
709
738
|
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
710
739
|
const pathOf = (file: (typeof components)[number]): string =>
|
|
711
740
|
relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
712
741
|
return {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
typeErrorSkips: projectRead.typeErrorSkips,
|
|
718
|
-
skipped: projectRead.skipped,
|
|
742
|
+
projectRead,
|
|
743
|
+
references: resolved.references,
|
|
744
|
+
testFiles: resolved.testFilePaths,
|
|
745
|
+
evidence,
|
|
719
746
|
componentFiles: components.map(pathOf),
|
|
720
747
|
componentWarnings: components.flatMap((file) =>
|
|
721
748
|
readComponent(file.originalText).warnings.map((warning) => ({
|
|
@@ -724,11 +751,69 @@ function readRun<Extra>(
|
|
|
724
751
|
reason: warning.reason,
|
|
725
752
|
})),
|
|
726
753
|
),
|
|
727
|
-
extra: extra(project, projectRead,
|
|
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
|
+
});
|
|
781
|
+
const exempt = detected.records;
|
|
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();
|
|
785
|
+
const configured: Configured = {
|
|
786
|
+
configuration: projectRead.configuration,
|
|
787
|
+
symbols: projectRead.held.symbols,
|
|
788
|
+
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
789
|
+
roots: projectRead.rooted.liveUnderReachability,
|
|
790
|
+
testFiles: one.testFiles,
|
|
791
|
+
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
792
|
+
...(projectRead.referenceOnly.length === 0
|
|
793
|
+
? {}
|
|
794
|
+
: { referenceOnly: projectRead.referenceOnly }),
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
configFile: projectRead.configFile,
|
|
798
|
+
configured,
|
|
799
|
+
exempt,
|
|
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
|
+
],
|
|
808
|
+
typeErrorSkips: projectRead.typeErrorSkips,
|
|
809
|
+
skipped: projectRead.skipped,
|
|
810
|
+
componentFiles: one.componentFiles,
|
|
811
|
+
componentWarnings: one.componentWarnings,
|
|
812
|
+
extra: one.extra,
|
|
728
813
|
};
|
|
729
814
|
});
|
|
730
815
|
const consumed = read.consumers.references;
|
|
731
|
-
const projects =
|
|
816
|
+
const projects = answers.map((one) =>
|
|
732
817
|
consumed.length === 0
|
|
733
818
|
? one
|
|
734
819
|
: {
|
|
@@ -744,7 +829,7 @@ function readRun<Extra>(
|
|
|
744
829
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
745
830
|
unansweredHeld: [
|
|
746
831
|
...new Set([
|
|
747
|
-
...
|
|
832
|
+
...answers.flatMap((one) => one.unanswered),
|
|
748
833
|
...consumed.filter((one) => one.resolution === "by-name").map((one) => one.to),
|
|
749
834
|
]),
|
|
750
835
|
],
|
|
@@ -784,6 +869,61 @@ function holdingSweep<Extra>(
|
|
|
784
869
|
}
|
|
785
870
|
}
|
|
786
871
|
|
|
872
|
+
/**
|
|
873
|
+
* The test references exemption evidence written in a test file makes: each is a read of
|
|
874
|
+
* the declaration it would retain, made by the test file at the evidence's site.
|
|
875
|
+
*/
|
|
876
|
+
function evidenceReferences(
|
|
877
|
+
symbols: readonly InventorySymbol[],
|
|
878
|
+
inTestFiles: readonly Exemption[],
|
|
879
|
+
): readonly Reference[] {
|
|
880
|
+
if (inTestFiles.length === 0) {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
const files = new Map(
|
|
884
|
+
symbols
|
|
885
|
+
.filter((symbol) => symbol.kind === "file")
|
|
886
|
+
.map((symbol) => [symbol.position.path, symbol.id]),
|
|
887
|
+
);
|
|
888
|
+
return inTestFiles.flatMap((record) => {
|
|
889
|
+
const from = files.get(record.site.path);
|
|
890
|
+
return from === undefined
|
|
891
|
+
? []
|
|
892
|
+
: [
|
|
893
|
+
{
|
|
894
|
+
from,
|
|
895
|
+
to: record.id,
|
|
896
|
+
position: record.site,
|
|
897
|
+
use: "read",
|
|
898
|
+
resolution: "syntax",
|
|
899
|
+
test: true,
|
|
900
|
+
},
|
|
901
|
+
];
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
907
|
+
* reference counting as a read: what the write-only kind would report but for a record.
|
|
908
|
+
*/
|
|
909
|
+
function writtenOnly<Extra>(read: ReadRun<Extra>, mode: Mode): ReadonlySet<string> {
|
|
910
|
+
const written = new Set<string>();
|
|
911
|
+
const readSome = new Set<string>();
|
|
912
|
+
for (const { configured } of read.projects) {
|
|
913
|
+
for (const reference of configured.references) {
|
|
914
|
+
if (mode.production && reference.test) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (reference.use === "write" && reference.consumer === undefined) {
|
|
918
|
+
written.add(reference.to);
|
|
919
|
+
} else {
|
|
920
|
+
readSome.add(reference.to);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return new Set([...written].filter((id) => !readSome.has(id)));
|
|
925
|
+
}
|
|
926
|
+
|
|
787
927
|
/**
|
|
788
928
|
* The sweep of a run read once, under one set of marks. Where a question went unanswered,
|
|
789
929
|
* the run is swept once more without what that question could have kept live and without
|
|
@@ -834,7 +974,7 @@ function sweptOf<Extra>(
|
|
|
834
974
|
matrix: read.matrix,
|
|
835
975
|
sweep: result,
|
|
836
976
|
exempt,
|
|
837
|
-
retained: retainedIn(read.matrix, swept),
|
|
977
|
+
retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
|
|
838
978
|
notBuilt: read.notBuilt,
|
|
839
979
|
unanswered: read.unanswered,
|
|
840
980
|
heldByUnanswered: [...held],
|
|
@@ -874,6 +1014,7 @@ interface EmitterExtra {
|
|
|
874
1014
|
readonly needs: ReturnType<typeof projectNeeds>;
|
|
875
1015
|
readonly implementations: ReturnType<typeof implementations>;
|
|
876
1016
|
readonly heldByInclusion: readonly string[];
|
|
1017
|
+
readonly generated: readonly string[];
|
|
877
1018
|
readonly rooted: Roots;
|
|
878
1019
|
readonly directives: readonly InlineDirective[];
|
|
879
1020
|
readonly declarationsNamed: readonly boolean[];
|
|
@@ -900,9 +1041,12 @@ function readEmitterRun(
|
|
|
900
1041
|
(project, read, references) => ({
|
|
901
1042
|
deprecation: deprecatedDeclarations(project, read.held),
|
|
902
1043
|
accessors: accessorsOf(project, read.held),
|
|
903
|
-
needs: projectNeeds(project, read.held, host, targetRoot)
|
|
1044
|
+
needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) =>
|
|
1045
|
+
engine.parseSourceFile(fileName, text),
|
|
1046
|
+
),
|
|
904
1047
|
implementations: implementations(project, read.held),
|
|
905
1048
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
1049
|
+
generated: [...read.generated.keys()],
|
|
906
1050
|
rooted: read.rooted,
|
|
907
1051
|
directives: inlineDirectives(project, read.held, targetRoot).filter(
|
|
908
1052
|
(directive) => !read.referenceOnly.includes(directive.site.path),
|
|
@@ -956,6 +1100,7 @@ function emitterInputOver(
|
|
|
956
1100
|
files: {
|
|
957
1101
|
tree: sourceTree(host, targetRoot),
|
|
958
1102
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
1103
|
+
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
959
1104
|
},
|
|
960
1105
|
boundary: {
|
|
961
1106
|
consumers: {
|