@cplieger/deadset-ts 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +119 -23
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +48 -7
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +223 -61
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +3 -2
- package/src/reporters/template.ts +50 -8
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
package/dist/src/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
|
-
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
2
|
import { DiscoveryError } from "./discover.js";
|
|
3
|
+
import { queriesOf, UnansweredLog, } from "./query.js";
|
|
4
4
|
/**
|
|
5
5
|
* Opens the compiler client the analysis reads through.
|
|
6
6
|
*
|
|
@@ -13,7 +13,9 @@ export function openEngine(options) {
|
|
|
13
13
|
let closed = false;
|
|
14
14
|
return {
|
|
15
15
|
parseConfigFile: (file) => api.parseConfigFile(file),
|
|
16
|
-
|
|
16
|
+
createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
|
|
17
|
+
batch: (questions) => api.batch(...questions),
|
|
18
|
+
ask: (_accessor, _locations, question) => question(),
|
|
17
19
|
getTimingInfo: () => api.getTimingInfo(),
|
|
18
20
|
close: () => {
|
|
19
21
|
if (closed) {
|
|
@@ -24,46 +26,53 @@ export function openEngine(options) {
|
|
|
24
26
|
},
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
|
-
function viewOf(project) {
|
|
29
|
+
function viewOf(project, configFile, engine, log) {
|
|
30
|
+
const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
|
|
28
31
|
// The snapshot is never updated, so a project's own file set is the same answer
|
|
29
32
|
// every time it is asked for. It is read once because the question is not free:
|
|
30
33
|
// deciding it consults the metadata of every file the program holds, the compiler's
|
|
31
34
|
// own library files included, and a run asks for the set once per pass.
|
|
32
35
|
let ownFiles;
|
|
36
|
+
let ownKeys;
|
|
37
|
+
const ownSourceFiles = () => {
|
|
38
|
+
ownFiles ??= project.program
|
|
39
|
+
.getSourceFileNames()
|
|
40
|
+
// The metadata decides which files belong to the target, and it is read by
|
|
41
|
+
// name. Fetching each file first and filtering afterwards costs one round
|
|
42
|
+
// trip per library file the answer then discards, and a program holds far
|
|
43
|
+
// more library files than target files.
|
|
44
|
+
.filter((name) => {
|
|
45
|
+
const metadata = project.program.getSourceFileMetadata(name);
|
|
46
|
+
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
47
|
+
})
|
|
48
|
+
.map((name) => project.program.getSourceFile(name))
|
|
49
|
+
.filter((file) => file !== undefined);
|
|
50
|
+
return ownFiles;
|
|
51
|
+
};
|
|
33
52
|
return {
|
|
34
|
-
configFile
|
|
53
|
+
configFile,
|
|
35
54
|
program: project.program,
|
|
36
|
-
|
|
37
|
-
ownSourceFiles
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// name. Fetching each file first and filtering afterwards costs one round
|
|
42
|
-
// trip per library file the answer then discards, and a program holds far
|
|
43
|
-
// more library files than target files.
|
|
44
|
-
.filter((name) => {
|
|
45
|
-
const metadata = project.program.getSourceFileMetadata(name);
|
|
46
|
-
return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
|
|
47
|
-
})
|
|
48
|
-
.map((name) => project.program.getSourceFile(name))
|
|
49
|
-
.filter((file) => file !== undefined);
|
|
50
|
-
return ownFiles;
|
|
55
|
+
queries,
|
|
56
|
+
ownSourceFiles,
|
|
57
|
+
ownPaths: () => {
|
|
58
|
+
ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
59
|
+
return ownKeys;
|
|
51
60
|
},
|
|
52
61
|
handle: (node) => ({ node }),
|
|
53
|
-
symbolsAt: (handles) =>
|
|
54
|
-
resolvedSymbolAt: ({ node }) =>
|
|
55
|
-
shorthandValueAt: ({ node }) =>
|
|
62
|
+
symbolsAt: (handles, cap) => queries.symbolsAt(handles.map(({ node }) => node), cap),
|
|
63
|
+
resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
|
|
64
|
+
shorthandValueAt: ({ node }) => queries.shorthandValue(node),
|
|
56
65
|
aliasStepOf: (symbol) => {
|
|
57
66
|
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
58
67
|
// fails an assertion and the session ends.
|
|
59
68
|
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
60
69
|
return undefined;
|
|
61
70
|
}
|
|
62
|
-
const held =
|
|
71
|
+
const held = queries.immediateAliased(symbol);
|
|
63
72
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
64
73
|
// symbol, which declares nothing; a symbol that is no alias answers with
|
|
65
74
|
// nothing at all.
|
|
66
|
-
return held ===
|
|
75
|
+
return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
|
|
67
76
|
},
|
|
68
77
|
declarationAt: (handle) => {
|
|
69
78
|
const node = handle.resolve(project);
|
|
@@ -78,11 +87,17 @@ function viewOf(project) {
|
|
|
78
87
|
*/
|
|
79
88
|
export function narrowedTo(project, keep) {
|
|
80
89
|
let kept;
|
|
90
|
+
let keys;
|
|
91
|
+
const ownSourceFiles = () => {
|
|
92
|
+
kept ??= project.ownSourceFiles().filter(keep);
|
|
93
|
+
return kept;
|
|
94
|
+
};
|
|
81
95
|
return {
|
|
82
96
|
...project,
|
|
83
|
-
ownSourceFiles
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
ownSourceFiles,
|
|
98
|
+
ownPaths: () => {
|
|
99
|
+
keys ??= new Set(ownSourceFiles().map((file) => file.path));
|
|
100
|
+
return keys;
|
|
86
101
|
},
|
|
87
102
|
};
|
|
88
103
|
}
|
|
@@ -93,6 +108,14 @@ export function diagnosticsOf(project) {
|
|
|
93
108
|
syntactic: project.program.getSyntacticDiagnostics(),
|
|
94
109
|
semantic: project.program.getSemanticDiagnostics(),
|
|
95
110
|
configParsing: project.program.getConfigFileParsingDiagnostics(),
|
|
111
|
+
locate: (fileName, offset) => {
|
|
112
|
+
const file = project.program.getSourceFile(fileName);
|
|
113
|
+
if (file === undefined || offset < 0 || offset > file.text.length) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const { line, character } = file.getLineAndCharacterOfPosition(offset);
|
|
117
|
+
return { line: line + 1, column: character + 1 };
|
|
118
|
+
},
|
|
96
119
|
};
|
|
97
120
|
}
|
|
98
121
|
/**
|
|
@@ -103,24 +126,41 @@ export function diagnosticsOf(project) {
|
|
|
103
126
|
* with its own program and checker; it is never updated, because the analysis
|
|
104
127
|
* reads and the tree does not change under it. The order the snapshot lists its
|
|
105
128
|
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
106
|
-
* instead
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
129
|
+
* instead. A configuration the snapshot opened no project for ends the run under
|
|
130
|
+
* `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
|
|
131
|
+
* the last project has been visited, and the client is released after it, whether the
|
|
132
|
+
* visit completed or threw.
|
|
110
133
|
*/
|
|
111
|
-
export function runSession(engine, configFiles, visit) {
|
|
134
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse") {
|
|
112
135
|
try {
|
|
113
|
-
const snapshot = engine.
|
|
136
|
+
const snapshot = engine.createSnapshot(configFiles);
|
|
114
137
|
try {
|
|
115
|
-
const opened = new Map(
|
|
116
|
-
const
|
|
138
|
+
const opened = new Map();
|
|
139
|
+
for (const project of snapshot.getProjects()) {
|
|
140
|
+
if (project.configFileName !== undefined) {
|
|
141
|
+
opened.set(project.configFileName, project);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const log = new UnansweredLog();
|
|
145
|
+
const projects = [];
|
|
146
|
+
const missing = [];
|
|
147
|
+
for (const configFile of configFiles) {
|
|
117
148
|
const project = opened.get(configFile);
|
|
118
149
|
if (project === undefined) {
|
|
119
|
-
|
|
150
|
+
if (unopened === "refuse") {
|
|
151
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
152
|
+
}
|
|
153
|
+
missing.push(configFile);
|
|
154
|
+
continue;
|
|
120
155
|
}
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
156
|
+
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
projects,
|
|
160
|
+
unopened: missing,
|
|
161
|
+
timing: engine.getTimingInfo(),
|
|
162
|
+
unanswered: log.questions,
|
|
163
|
+
};
|
|
124
164
|
}
|
|
125
165
|
finally {
|
|
126
166
|
snapshot.dispose();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* one record shape, so one decoder and one matcher serve both, and a row moves into the
|
|
5
5
|
* ignore file by gaining a maintainer's reason.
|
|
6
6
|
*/
|
|
7
|
+
import { jsonChunks } from "./json-chunks.js";
|
|
7
8
|
import { joinPath } from "./paths.js";
|
|
8
9
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
9
10
|
import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
|
|
@@ -213,5 +214,5 @@ export function writeBaseline(findings, provenance) {
|
|
|
213
214
|
}
|
|
214
215
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
215
216
|
});
|
|
216
|
-
return
|
|
217
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
217
218
|
}
|
package/dist/src/sweep.js
CHANGED
|
@@ -16,7 +16,10 @@ import { namesACaller, OUTSIDE } from "./graph.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export function sweep(graph, input) {
|
|
18
18
|
const marked = flagged(graph, input.marked);
|
|
19
|
-
const exempt = flagged(graph,
|
|
19
|
+
const exempt = flagged(graph, [
|
|
20
|
+
...(input.exempt ?? []).map((record) => record.id),
|
|
21
|
+
...(input.unanswered ?? []),
|
|
22
|
+
]);
|
|
20
23
|
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
21
24
|
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
22
25
|
// names a caller, and a mark, are live under reference counting as well; a root that
|
package/dist/src/value-reach.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
11
11
|
import { classNames, typesAt } from "./class-members.js";
|
|
12
|
+
import { must } from "./query.js";
|
|
12
13
|
/** The object types whose members the walk reads whatever declares them. */
|
|
13
14
|
const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
14
15
|
/** The symbols of an anonymous object type the target writes as a type. */
|
|
@@ -17,10 +18,10 @@ const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
|
17
18
|
const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
|
|
18
19
|
/** The global array types, whose element type is what an array value carries. */
|
|
19
20
|
function arrayTargets(project) {
|
|
20
|
-
const
|
|
21
|
+
const queries = project.queries;
|
|
21
22
|
return new Set(["Array", "ReadonlyArray"].flatMap((name) => {
|
|
22
|
-
const symbol =
|
|
23
|
-
return symbol === undefined ? [] : [
|
|
23
|
+
const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
|
|
24
|
+
return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
|
|
24
25
|
}));
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -28,7 +29,7 @@ function arrayTargets(project) {
|
|
|
28
29
|
* project's inventory.
|
|
29
30
|
*/
|
|
30
31
|
export function valueReach(project, held) {
|
|
31
|
-
const
|
|
32
|
+
const queries = project.queries;
|
|
32
33
|
const names = [...classNames(project, held)];
|
|
33
34
|
const declared = typesAt(project, names.map(([, node]) => node));
|
|
34
35
|
const classOfTarget = new Map();
|
|
@@ -39,12 +40,10 @@ export function valueReach(project, held) {
|
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
const arrays = arrayTargets(project);
|
|
42
|
-
const own =
|
|
43
|
+
const own = project.ownPaths();
|
|
43
44
|
const children = new Map();
|
|
44
45
|
/** The generic target of a type reference, and the type itself otherwise. */
|
|
45
|
-
const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0
|
|
46
|
-
? type.getTarget()
|
|
47
|
-
: type;
|
|
46
|
+
const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
|
|
48
47
|
const classOf = (type) => {
|
|
49
48
|
if (!type.isObjectType()) {
|
|
50
49
|
return undefined;
|
|
@@ -56,7 +55,7 @@ export function valueReach(project, held) {
|
|
|
56
55
|
if ((type.objectFlags & WRITTEN) !== 0) {
|
|
57
56
|
return true;
|
|
58
57
|
}
|
|
59
|
-
const symbol =
|
|
58
|
+
const symbol = must(queries.symbolOfType(type));
|
|
60
59
|
if (symbol === undefined) {
|
|
61
60
|
return false;
|
|
62
61
|
}
|
|
@@ -68,13 +67,11 @@ export function valueReach(project, held) {
|
|
|
68
67
|
};
|
|
69
68
|
/** The types a value of one object type holds in its data members. */
|
|
70
69
|
const memberTypes = (type) => {
|
|
71
|
-
const properties =
|
|
72
|
-
|
|
73
|
-
.filter((property) => (property.flags & SymbolFlags.Property) !== 0);
|
|
74
|
-
const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
|
|
70
|
+
const properties = must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0);
|
|
71
|
+
const types = queries.typesOfSymbols(properties).map(must);
|
|
75
72
|
return [
|
|
76
73
|
...types.filter((held) => held !== undefined),
|
|
77
|
-
...
|
|
74
|
+
...must(queries.indexInfosOf(type)).map((info) => info.valueType),
|
|
78
75
|
];
|
|
79
76
|
};
|
|
80
77
|
/** Whether the walk reads one type's members because the target writes the type, not as a class. */
|
|
@@ -89,9 +86,7 @@ export function valueReach(project, held) {
|
|
|
89
86
|
return !isList && isWritten(type);
|
|
90
87
|
};
|
|
91
88
|
/** The data members and conversion methods of one written object type. */
|
|
92
|
-
const carried = (type) =>
|
|
93
|
-
.getPropertiesOfType(type)
|
|
94
|
-
.filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
|
|
89
|
+
const carried = (type) => must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
|
|
95
90
|
((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
|
|
96
91
|
/** The types a value of one type holds, read once per type. */
|
|
97
92
|
const childrenOf = (type) => {
|
|
@@ -101,7 +96,7 @@ export function valueReach(project, held) {
|
|
|
101
96
|
}
|
|
102
97
|
let found = [];
|
|
103
98
|
if (type.isUnionType() || type.isIntersectionType()) {
|
|
104
|
-
found =
|
|
99
|
+
found = must(queries.constituents(type));
|
|
105
100
|
}
|
|
106
101
|
else if (type.isObjectType()) {
|
|
107
102
|
const target = targetOf(type);
|
|
@@ -112,7 +107,7 @@ export function valueReach(project, held) {
|
|
|
112
107
|
found = memberTypes(type);
|
|
113
108
|
}
|
|
114
109
|
else if (isList && type.isTypeReference()) {
|
|
115
|
-
found =
|
|
110
|
+
found = must(queries.typeArguments(type));
|
|
116
111
|
}
|
|
117
112
|
}
|
|
118
113
|
children.set(type.id, found);
|
|
@@ -4,6 +4,7 @@ import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
|
4
4
|
import { moduleIdentity } from "../consumers.js";
|
|
5
5
|
import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
|
+
import { partialNotes } from "../partial.js";
|
|
7
8
|
import { buildReport, capFindings, sortFindings, } from "../report.js";
|
|
8
9
|
import { json } from "../reporters/json.js";
|
|
9
10
|
import { RENDERINGS } from "../reporters/reporters.js";
|
|
@@ -61,6 +62,16 @@ function runRelative(invoked, path) {
|
|
|
61
62
|
}
|
|
62
63
|
return below;
|
|
63
64
|
}
|
|
65
|
+
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
66
|
+
function notBuiltOf(dropped, targetRoot) {
|
|
67
|
+
return dropped.map((one) => {
|
|
68
|
+
const project = relativePath(targetRoot, one.configFile);
|
|
69
|
+
if (project === undefined) {
|
|
70
|
+
throw new ReportPathError(`the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`);
|
|
71
|
+
}
|
|
72
|
+
return { id: one.id, project, error: one.error };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
64
75
|
/** Each project of the run as a report names it, by its configuration file below the target. */
|
|
65
76
|
function configurationsOf(projects, targetRoot) {
|
|
66
77
|
return projects.map((one) => {
|
|
@@ -148,6 +159,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
148
159
|
let report;
|
|
149
160
|
let recorded;
|
|
150
161
|
let verdict;
|
|
162
|
+
let partial;
|
|
151
163
|
try {
|
|
152
164
|
const root = runRelative(invoked, targetRoot);
|
|
153
165
|
const mode = { production: true };
|
|
@@ -163,6 +175,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
163
175
|
recorded = write.rows;
|
|
164
176
|
}
|
|
165
177
|
const { result, run } = analysis;
|
|
178
|
+
partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
|
|
166
179
|
const built = buildReport({
|
|
167
180
|
contractVersion: CONTRACT_VERSION,
|
|
168
181
|
version,
|
|
@@ -170,6 +183,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
170
183
|
declaredGaps: DECLARED_GAPS,
|
|
171
184
|
target: { kind, root, identity: moduleIdentity(host, scoped.target) },
|
|
172
185
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
186
|
+
notBuilt: notBuiltOf(run.notBuilt, targetRoot),
|
|
173
187
|
loaded: run.consumers.map((consumer) => ({
|
|
174
188
|
id: consumer.id,
|
|
175
189
|
path: runRelative(invoked, consumer.path),
|
|
@@ -189,7 +203,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
189
203
|
return EXIT_FAILURE;
|
|
190
204
|
}
|
|
191
205
|
try {
|
|
192
|
-
host.
|
|
206
|
+
host.writeDocument(documents.report, json(report));
|
|
193
207
|
const options = {
|
|
194
208
|
failOn: config.reporters.failOn,
|
|
195
209
|
readSource: (path) => host.readFile(joinPath(targetRoot, path)),
|
|
@@ -198,17 +212,20 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
198
212
|
for (const format of documents.formats) {
|
|
199
213
|
const rendering = RENDERINGS.get(format);
|
|
200
214
|
if (rendering !== undefined) {
|
|
201
|
-
host.
|
|
215
|
+
host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
218
|
if (documents.baseline !== undefined) {
|
|
205
|
-
host.
|
|
219
|
+
host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
|
|
206
220
|
}
|
|
207
221
|
}
|
|
208
222
|
catch (error) {
|
|
209
223
|
err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
210
224
|
return EXIT_FAILURE;
|
|
211
225
|
}
|
|
226
|
+
for (const note of partial) {
|
|
227
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
228
|
+
}
|
|
212
229
|
if (report.totals.pending > 0) {
|
|
213
230
|
err.write(`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`);
|
|
214
231
|
}
|
|
@@ -3,6 +3,7 @@ import { listing } from "../components.js";
|
|
|
3
3
|
import { ConfigError } from "../config.js";
|
|
4
4
|
import { OUTSIDE, swept as judged } from "../graph.js";
|
|
5
5
|
import { referenceKey } from "../matrix.js";
|
|
6
|
+
import { partialNotes } from "../partial.js";
|
|
6
7
|
import { positionKey } from "../position.js";
|
|
7
8
|
import { resolve } from "../resolve.js";
|
|
8
9
|
import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
|
|
@@ -282,18 +283,22 @@ function explanation(analysis, subject, cascade) {
|
|
|
282
283
|
text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
|
|
283
284
|
}
|
|
284
285
|
}
|
|
286
|
+
else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
|
|
287
|
+
text +=
|
|
288
|
+
"answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
|
|
289
|
+
}
|
|
285
290
|
else {
|
|
286
291
|
text += liveness(analysis, subject, cascade);
|
|
287
292
|
}
|
|
288
293
|
return text + namedElsewhere(analysis.result.findings, subject);
|
|
289
294
|
}
|
|
290
295
|
/**
|
|
291
|
-
* Explains one symbol from the analysis the report is built from
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
296
|
+
* Explains one symbol from the analysis the report is built from: why a reported symbol is
|
|
297
|
+
* reported, which exemption classes held a retained one back, that an unanswered question held
|
|
298
|
+
* a held one, and for any other declaration the relations that hold it live and the shortest
|
|
299
|
+
* path from a root or a loaded consumer, or the references it has where no path reaches it. A
|
|
300
|
+
* request naming no one declaration exits with the usage code and names what partially
|
|
301
|
+
* matches it. Where the analysis was partial, the error stream says so.
|
|
297
302
|
*/
|
|
298
303
|
export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
299
304
|
const named = namedSymbol(option);
|
|
@@ -302,6 +307,10 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
|
|
|
302
307
|
production: true,
|
|
303
308
|
});
|
|
304
309
|
const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
|
|
310
|
+
const notes = partialNotes({
|
|
311
|
+
notBuilt: analysis.run.notBuilt,
|
|
312
|
+
unanswered: analysis.run.unanswered,
|
|
313
|
+
});
|
|
305
314
|
if (subject === undefined) {
|
|
306
315
|
err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
|
|
307
316
|
for (const candidate of candidates) {
|
|
@@ -310,8 +319,12 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
|
|
|
310
319
|
if (candidates.length === 0) {
|
|
311
320
|
err.write(" no symbol of the target partially matches it either\n");
|
|
312
321
|
}
|
|
313
|
-
return EXIT_USAGE;
|
|
314
322
|
}
|
|
315
|
-
|
|
316
|
-
|
|
323
|
+
else {
|
|
324
|
+
out.write(explanation(analysis, subject, config.reporters.cascade));
|
|
325
|
+
}
|
|
326
|
+
for (const note of notes) {
|
|
327
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
328
|
+
}
|
|
329
|
+
return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
|
|
317
330
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "../discover.js";
|
|
2
|
+
import { partialNotes } from "../partial.js";
|
|
3
|
+
import { relativePath, resolvePath } from "../paths.js";
|
|
2
4
|
import { resolveMatrix } from "../resolve.js";
|
|
3
5
|
import { diagnosticsOf, runSession } from "../session.js";
|
|
4
6
|
import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
|
|
@@ -6,7 +8,8 @@ import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
|
|
|
6
8
|
* Writes the name of every project one run analyzes, one per line, having read each
|
|
7
9
|
* project's diagnostics first: the identifier the build matrix declares for it, or,
|
|
8
10
|
* where discovery derived the project, its configuration file's path below the
|
|
9
|
-
* target root.
|
|
11
|
+
* target root. A derived project that does not load is not one the run analyzes, so it
|
|
12
|
+
* is named on the error stream instead, as the analysis drops it.
|
|
10
13
|
*/
|
|
11
14
|
export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
12
15
|
const matrix = resolveMatrix(inputs());
|
|
@@ -14,12 +17,31 @@ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient })
|
|
|
14
17
|
const engine = openClient(false);
|
|
15
18
|
const discovered = discoverProjects(engine, host, scoped, matrix);
|
|
16
19
|
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
20
|
+
const root = resolvePath(host.workingDirectory(), scoped.target.path);
|
|
21
|
+
const notBuilt = [...discovered.notBuilt];
|
|
17
22
|
const failures = [];
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const session = runSession(engine, discovered.configFiles, (project) => {
|
|
24
|
+
const id = ids.get(project.configFile) ?? project.configFile;
|
|
25
|
+
const errors = projectErrors(diagnosticsOf(project));
|
|
26
|
+
const [unloaded] = errors.load;
|
|
27
|
+
if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
|
|
28
|
+
const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
|
|
29
|
+
notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
|
|
33
|
+
return id;
|
|
34
|
+
}, discovered.derived ? "record" : "refuse");
|
|
35
|
+
for (const configFile of session.unopened) {
|
|
36
|
+
if (!isGuess(discovered, configFile)) {
|
|
37
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
38
|
+
}
|
|
39
|
+
notBuilt.push({
|
|
40
|
+
id: ids.get(configFile) ?? configFile,
|
|
41
|
+
configFile,
|
|
42
|
+
error: "the compiler opened no project for the configuration",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
23
45
|
if (failures.length > 0) {
|
|
24
46
|
for (const line of failures) {
|
|
25
47
|
err.write(`${line}\n`);
|
|
@@ -27,8 +49,19 @@ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient })
|
|
|
27
49
|
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
28
50
|
return EXIT_FAILURE;
|
|
29
51
|
}
|
|
52
|
+
const projects = session.projects.filter((id) => id !== undefined);
|
|
53
|
+
if (projects.length === 0 && notBuilt.length > 0) {
|
|
54
|
+
for (const note of partialNotes({ notBuilt, unanswered: [] })) {
|
|
55
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
56
|
+
}
|
|
57
|
+
err.write("deadset-ts: no configuration discovery derived could be built\n");
|
|
58
|
+
return EXIT_FAILURE;
|
|
59
|
+
}
|
|
30
60
|
for (const id of projects) {
|
|
31
61
|
out.write(`${id}\n`);
|
|
32
62
|
}
|
|
63
|
+
for (const note of partialNotes({ notBuilt, unanswered: [] })) {
|
|
64
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
65
|
+
}
|
|
33
66
|
return EXIT_CLEAN;
|
|
34
67
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runSweep } from "../analysis.js";
|
|
2
2
|
import { retainedLines } from "../exempt.js";
|
|
3
|
+
import { partialNotes } from "../partial.js";
|
|
3
4
|
import { resolve } from "../resolve.js";
|
|
4
5
|
import { EXIT_CLEAN } from "./verb.js";
|
|
5
6
|
/**
|
|
@@ -7,9 +8,9 @@ import { EXIT_CLEAN } from "./verb.js";
|
|
|
7
8
|
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
8
9
|
* several configurations found is one record, so the line names no configuration. The
|
|
9
10
|
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
10
|
-
* read it.
|
|
11
|
+
* read it. Where the analysis was partial, the error stream says so.
|
|
11
12
|
*/
|
|
12
|
-
export const printRetainedVerb = ({ out, host, inputs, scope, openClient }) => {
|
|
13
|
+
export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
13
14
|
const { config } = resolve(inputs());
|
|
14
15
|
const scoped = scope();
|
|
15
16
|
const swept = runSweep(openClient(false), host, scoped, config, {
|
|
@@ -19,5 +20,8 @@ export const printRetainedVerb = ({ out, host, inputs, scope, openClient }) => {
|
|
|
19
20
|
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
20
21
|
out.write(`${line}\n`);
|
|
21
22
|
}
|
|
23
|
+
for (const note of partialNotes({ notBuilt: swept.notBuilt, unanswered: swept.unanswered })) {
|
|
24
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
25
|
+
}
|
|
22
26
|
return EXIT_CLEAN;
|
|
23
27
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runRoots } from "../analysis.js";
|
|
2
|
+
import { partialNotes } from "../partial.js";
|
|
2
3
|
import { resolve } from "../resolve.js";
|
|
3
4
|
import { EXIT_CLEAN, EXIT_FINDINGS } from "./verb.js";
|
|
4
5
|
/**
|
|
@@ -18,7 +19,7 @@ function rootLine(root, several) {
|
|
|
18
19
|
/**
|
|
19
20
|
* Writes the root set of the run, one root per line, and then names on the error
|
|
20
21
|
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
21
|
-
* one such string fails the run. The line shape is this command's own rather than a
|
|
22
|
+
* one such string fails the run, and then where the analysis was partial. The line shape is this command's own rather than a
|
|
22
23
|
* Contract format, so that sort and cut read it.
|
|
23
24
|
*/
|
|
24
25
|
export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
@@ -32,5 +33,8 @@ export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) =>
|
|
|
32
33
|
for (const finding of answer.findings) {
|
|
33
34
|
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
34
35
|
}
|
|
36
|
+
for (const note of partialNotes({ notBuilt: answer.notBuilt, unanswered: answer.unanswered })) {
|
|
37
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
38
|
+
}
|
|
35
39
|
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
36
40
|
};
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@typescript/native": "npm:typescript@7.0.
|
|
31
|
+
"@typescript/native": "npm:typescript@7.1.0-dev.20261003.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/js": "10.0.1",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"prettier": "3.9.9",
|
|
39
39
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
40
40
|
"typescript-eslint": "8.71.0",
|
|
41
|
-
"vitest": "5.0.
|
|
41
|
+
"vitest": "5.0.3"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|