@cplieger/deadset-ts 3.12.1 → 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 +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- 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/edges.js +4 -1
- 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/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- 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 +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- 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 +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- 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/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- 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/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- 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 +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- 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 +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- 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/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
package/src/value-reach.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The classes of the target a value of one type carries,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The classes of the target a value of one type carries, and the members of the other
|
|
3
|
+
* object types the target writes that it carries, as a serializer walking the value
|
|
4
|
+
* reaches them: through union and intersection constituents, array and tuple elements,
|
|
5
|
+
* and the data members and index signatures of the target's classes and of the
|
|
6
|
+
* interfaces and object types the target writes. The walk stops at `any`, `unknown`, a
|
|
7
|
+
* type parameter, a function and an interface or class of another program, so an
|
|
8
|
+
* instance held only there is missed.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
} from "@typescript/native/unstable/sync";
|
|
16
18
|
import { classNames, typesAt } from "./class-members.ts";
|
|
17
19
|
import type { Inventory } from "./inventory.ts";
|
|
20
|
+
import { must } from "./query.ts";
|
|
18
21
|
import type { ProjectView } from "./session.ts";
|
|
19
22
|
|
|
20
23
|
/** The object types whose members the walk reads whatever declares them. */
|
|
@@ -23,19 +26,32 @@ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
|
23
26
|
/** The symbols of an anonymous object type the target writes as a type. */
|
|
24
27
|
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
25
28
|
|
|
26
|
-
/**
|
|
29
|
+
/** What a value of one type carries that the target declares. */
|
|
30
|
+
export interface Reached {
|
|
31
|
+
/** The identifiers of the target's classes. */
|
|
32
|
+
readonly classes: ReadonlySet<string>;
|
|
33
|
+
/**
|
|
34
|
+
* The members of the interfaces and object types the target writes, other than its
|
|
35
|
+
* classes: each data member, and each method named `toJSON` or `toString`.
|
|
36
|
+
*/
|
|
37
|
+
readonly members: readonly TSSymbol[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What each type's values carry, each type asked of the checker once. */
|
|
27
41
|
export interface ValueReach {
|
|
28
|
-
|
|
29
|
-
classesOf(type: Type): ReadonlySet<string>;
|
|
42
|
+
reachedFrom(type: Type): Reached;
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
46
|
+
const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
47
|
+
|
|
32
48
|
/** The global array types, whose element type is what an array value carries. */
|
|
33
49
|
function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
|
|
34
|
-
const
|
|
50
|
+
const queries = project.queries;
|
|
35
51
|
return new Set(
|
|
36
52
|
["Array", "ReadonlyArray"].flatMap((name) => {
|
|
37
|
-
const symbol =
|
|
38
|
-
return symbol === undefined ? [] : [
|
|
53
|
+
const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
|
|
54
|
+
return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
|
|
39
55
|
}),
|
|
40
56
|
);
|
|
41
57
|
}
|
|
@@ -45,7 +61,7 @@ function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
|
|
|
45
61
|
* project's inventory.
|
|
46
62
|
*/
|
|
47
63
|
export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory): ValueReach {
|
|
48
|
-
const
|
|
64
|
+
const queries = project.queries;
|
|
49
65
|
const names = [...classNames(project, held)];
|
|
50
66
|
const declared = typesAt(
|
|
51
67
|
project,
|
|
@@ -59,14 +75,12 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
59
75
|
}
|
|
60
76
|
});
|
|
61
77
|
const arrays = arrayTargets(project);
|
|
62
|
-
const own =
|
|
78
|
+
const own = project.ownPaths();
|
|
63
79
|
const children = new Map<number, readonly Type[]>();
|
|
64
80
|
|
|
65
81
|
/** The generic target of a type reference, and the type itself otherwise. */
|
|
66
82
|
const targetOf = (type: ObjectType): Type =>
|
|
67
|
-
(type.objectFlags & ObjectFlags.Reference) !== 0
|
|
68
|
-
? type.getTarget()
|
|
69
|
-
: type;
|
|
83
|
+
(type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
|
|
70
84
|
|
|
71
85
|
const classOf = (type: Type): string | undefined => {
|
|
72
86
|
if (!type.isObjectType()) {
|
|
@@ -80,7 +94,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
80
94
|
if ((type.objectFlags & WRITTEN) !== 0) {
|
|
81
95
|
return true;
|
|
82
96
|
}
|
|
83
|
-
const symbol: TSSymbol | undefined =
|
|
97
|
+
const symbol: TSSymbol | undefined = must(queries.symbolOfType(type));
|
|
84
98
|
if (symbol === undefined) {
|
|
85
99
|
return false;
|
|
86
100
|
}
|
|
@@ -95,16 +109,37 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
95
109
|
|
|
96
110
|
/** The types a value of one object type holds in its data members. */
|
|
97
111
|
const memberTypes = (type: Type): readonly Type[] => {
|
|
98
|
-
const properties =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const types =
|
|
112
|
+
const properties = must(queries.propertiesOf(type)).filter(
|
|
113
|
+
(property) => (property.flags & SymbolFlags.Property) !== 0,
|
|
114
|
+
);
|
|
115
|
+
const types = queries.typesOfSymbols(properties).map(must);
|
|
102
116
|
return [
|
|
103
117
|
...types.filter((held): held is Type => held !== undefined),
|
|
104
|
-
...
|
|
118
|
+
...must(queries.indexInfosOf(type)).map((info) => info.valueType),
|
|
105
119
|
];
|
|
106
120
|
};
|
|
107
121
|
|
|
122
|
+
/** Whether the walk reads one type's members because the target writes the type, not as a class. */
|
|
123
|
+
const writtenObject = (type: Type): boolean => {
|
|
124
|
+
if (!type.isObjectType() || classOf(type) !== undefined) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const target = targetOf(type);
|
|
128
|
+
const isList =
|
|
129
|
+
type.isTypeReference() &&
|
|
130
|
+
(arrays.has(target.id) ||
|
|
131
|
+
(target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
|
|
132
|
+
return !isList && isWritten(type);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** The data members and conversion methods of one written object type. */
|
|
136
|
+
const carried = (type: Type): readonly TSSymbol[] =>
|
|
137
|
+
must(queries.propertiesOf(type)).filter(
|
|
138
|
+
(property) =>
|
|
139
|
+
(property.flags & SymbolFlags.Property) !== 0 ||
|
|
140
|
+
((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
|
|
141
|
+
);
|
|
142
|
+
|
|
108
143
|
/** The types a value of one type holds, read once per type. */
|
|
109
144
|
const childrenOf = (type: Type): readonly Type[] => {
|
|
110
145
|
const known = children.get(type.id);
|
|
@@ -113,7 +148,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
113
148
|
}
|
|
114
149
|
let found: readonly Type[] = [];
|
|
115
150
|
if (type.isUnionType() || type.isIntersectionType()) {
|
|
116
|
-
found =
|
|
151
|
+
found = must(queries.constituents(type));
|
|
117
152
|
} else if (type.isObjectType()) {
|
|
118
153
|
const target = targetOf(type);
|
|
119
154
|
const isList =
|
|
@@ -123,7 +158,7 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
123
158
|
if (classOf(type) !== undefined || (!isList && isWritten(type))) {
|
|
124
159
|
found = memberTypes(type);
|
|
125
160
|
} else if (isList && type.isTypeReference()) {
|
|
126
|
-
found =
|
|
161
|
+
found = must(queries.typeArguments(type));
|
|
127
162
|
}
|
|
128
163
|
}
|
|
129
164
|
children.set(type.id, found);
|
|
@@ -131,8 +166,9 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
131
166
|
};
|
|
132
167
|
|
|
133
168
|
return {
|
|
134
|
-
|
|
135
|
-
const
|
|
169
|
+
reachedFrom: (type) => {
|
|
170
|
+
const classes = new Set<string>();
|
|
171
|
+
const members: TSSymbol[] = [];
|
|
136
172
|
const seen = new Set<number>();
|
|
137
173
|
const pending: Type[] = [type];
|
|
138
174
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
@@ -142,11 +178,13 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
|
|
|
142
178
|
seen.add(next.id);
|
|
143
179
|
const owner = classOf(next);
|
|
144
180
|
if (owner !== undefined) {
|
|
145
|
-
|
|
181
|
+
classes.add(owner);
|
|
182
|
+
} else if (writtenObject(next)) {
|
|
183
|
+
members.push(...carried(next));
|
|
146
184
|
}
|
|
147
185
|
pending.push(...childrenOf(next));
|
|
148
186
|
}
|
|
149
|
-
return
|
|
187
|
+
return { classes, members };
|
|
150
188
|
},
|
|
151
189
|
};
|
|
152
190
|
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { runAnalysis, type RunProject } from "../analysis.ts";
|
|
1
|
+
import { runAnalysis, runBaselineWrite, type RunAnalysis, type RunProject } from "../analysis.ts";
|
|
2
2
|
import { ConfigError, type Format } from "../config.ts";
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
4
4
|
import { moduleIdentity } from "../consumers.ts";
|
|
5
|
-
import { analyzerProvenance,
|
|
5
|
+
import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
7
7
|
import type { Host } from "../host.ts";
|
|
8
|
+
import type { NotBuilt } from "../discover.ts";
|
|
9
|
+
import { partialNotes } from "../partial.ts";
|
|
8
10
|
import {
|
|
9
11
|
buildReport,
|
|
10
12
|
capFindings,
|
|
11
13
|
sortFindings,
|
|
12
14
|
type Report,
|
|
13
15
|
type ReportConfiguration,
|
|
16
|
+
type ReportNotBuilt,
|
|
14
17
|
} from "../report.ts";
|
|
15
18
|
import { json } from "../reporters/json.ts";
|
|
16
19
|
import { RENDERINGS } from "../reporters/reporters.ts";
|
|
17
20
|
import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
|
|
18
21
|
import { resolve } from "../resolve.ts";
|
|
19
|
-
import { writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
22
|
+
import { rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
20
23
|
import { CONTRACT_VERSION } from "../version.ts";
|
|
21
24
|
import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts";
|
|
22
25
|
|
|
@@ -81,6 +84,19 @@ function runRelative(invoked: string, path: string): string {
|
|
|
81
84
|
return below;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
88
|
+
function notBuiltOf(dropped: readonly NotBuilt[], targetRoot: string): readonly ReportNotBuilt[] {
|
|
89
|
+
return dropped.map((one) => {
|
|
90
|
+
const project = relativePath(targetRoot, one.configFile);
|
|
91
|
+
if (project === undefined) {
|
|
92
|
+
throw new ReportPathError(
|
|
93
|
+
`the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return { id: one.id, project, error: one.error };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
/** Each project of the run as a report names it, by its configuration file below the target. */
|
|
85
101
|
function configurationsOf(
|
|
86
102
|
projects: readonly RunProject[],
|
|
@@ -153,8 +169,9 @@ interface Documents {
|
|
|
153
169
|
/**
|
|
154
170
|
* Analyzes the scope under the configuration and writes the JSON report to the path
|
|
155
171
|
* `--report` names, each rendering `--format` names beside it at the report's path with the
|
|
156
|
-
* format's suffix appended, and
|
|
157
|
-
*
|
|
172
|
+
* format's suffix appended, and where `--baseline-write` names a path the baseline a fixpoint
|
|
173
|
+
* over the analysis records, the report then being its first round, which reads no
|
|
174
|
+
* baseline. The template format renders the template `--template` names. Nothing is written to
|
|
158
175
|
* the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
|
|
159
176
|
* under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
|
|
160
177
|
*/
|
|
@@ -202,11 +219,30 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
202
219
|
let report: Report;
|
|
203
220
|
let recorded: readonly Recorded[];
|
|
204
221
|
let verdict: number;
|
|
222
|
+
let partial: readonly string[];
|
|
205
223
|
try {
|
|
206
224
|
const root = runRelative(invoked, targetRoot);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
const mode = { production: true };
|
|
226
|
+
let analysis: RunAnalysis;
|
|
227
|
+
if (documents.baseline === undefined) {
|
|
228
|
+
analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
|
|
229
|
+
recorded = [];
|
|
230
|
+
} else {
|
|
231
|
+
const reason = rowReason(analyzerProvenance(version));
|
|
232
|
+
const write = runBaselineWrite(
|
|
233
|
+
openClient(false),
|
|
234
|
+
host,
|
|
235
|
+
scoped,
|
|
236
|
+
config,
|
|
237
|
+
provenance,
|
|
238
|
+
mode,
|
|
239
|
+
reason,
|
|
240
|
+
);
|
|
241
|
+
analysis = write.first;
|
|
242
|
+
recorded = write.rows;
|
|
243
|
+
}
|
|
244
|
+
const { result, run } = analysis;
|
|
245
|
+
partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
|
|
210
246
|
const built = buildReport({
|
|
211
247
|
contractVersion: CONTRACT_VERSION,
|
|
212
248
|
version,
|
|
@@ -214,6 +250,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
214
250
|
declaredGaps: DECLARED_GAPS,
|
|
215
251
|
target: { kind, root, identity: moduleIdentity(host, scoped.target) },
|
|
216
252
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
253
|
+
notBuilt: notBuiltOf(run.notBuilt, targetRoot),
|
|
217
254
|
loaded: run.consumers.map((consumer) => ({
|
|
218
255
|
id: consumer.id,
|
|
219
256
|
path: runRelative(invoked, consumer.path),
|
|
@@ -222,9 +259,6 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
222
259
|
result,
|
|
223
260
|
testFileRules: run.testFileRules,
|
|
224
261
|
});
|
|
225
|
-
// The baseline records every finding, so its rows are taken before the cap bounds what
|
|
226
|
-
// a rendering prints: a baseline missing a reported finding fails the next run on it.
|
|
227
|
-
recorded = recordedFindings(result);
|
|
228
262
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
229
263
|
verdict = verdictOf(result, config);
|
|
230
264
|
} catch (error: unknown) {
|
|
@@ -236,7 +270,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
236
270
|
}
|
|
237
271
|
|
|
238
272
|
try {
|
|
239
|
-
host.
|
|
273
|
+
host.writeDocument(documents.report, json(report));
|
|
240
274
|
const options = {
|
|
241
275
|
failOn: config.reporters.failOn,
|
|
242
276
|
readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
|
|
@@ -245,17 +279,20 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
245
279
|
for (const format of documents.formats) {
|
|
246
280
|
const rendering = RENDERINGS.get(format);
|
|
247
281
|
if (rendering !== undefined) {
|
|
248
|
-
host.
|
|
282
|
+
host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
|
|
249
283
|
}
|
|
250
284
|
}
|
|
251
285
|
if (documents.baseline !== undefined) {
|
|
252
|
-
host.
|
|
286
|
+
host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
|
|
253
287
|
}
|
|
254
288
|
} catch (error: unknown) {
|
|
255
289
|
err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
256
290
|
return EXIT_FAILURE;
|
|
257
291
|
}
|
|
258
292
|
|
|
293
|
+
for (const note of partial) {
|
|
294
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
295
|
+
}
|
|
259
296
|
if (report.totals.pending > 0) {
|
|
260
297
|
err.write(
|
|
261
298
|
`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
|
package/src/verbs/describe.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { CONFORMANCE, type Conformance } from "../conformance.ts";
|
|
2
|
-
import { ANALYZER_NAME, LANGUAGE,
|
|
2
|
+
import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSIONS_ACCEPTED } from "../report.ts";
|
|
3
3
|
import { CONTRACT_VERSION } from "../version.ts";
|
|
4
4
|
import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
8
8
|
* report schema versions it reads, the languages it claims, and its recorded result over
|
|
9
|
-
* the conformance corpus.
|
|
9
|
+
* the conformance corpus, a member the document omits where no run is recorded.
|
|
10
10
|
*/
|
|
11
|
-
export function describeDocument(version: string, recorded: Conformance): string {
|
|
11
|
+
export function describeDocument(version: string, recorded: Conformance | undefined): string {
|
|
12
12
|
const document = {
|
|
13
13
|
name: ANALYZER_NAME,
|
|
14
14
|
version,
|
|
15
15
|
contract_version: CONTRACT_VERSION,
|
|
16
|
-
schema_versions_accepted:
|
|
16
|
+
schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
|
|
17
17
|
languages: [LANGUAGE],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
...(recorded === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: {
|
|
21
|
+
conformance: {
|
|
22
|
+
corpus_version: recorded.corpusVersion,
|
|
23
|
+
result: recorded.result,
|
|
24
|
+
digest: recorded.digest,
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
23
27
|
};
|
|
24
28
|
return `${JSON.stringify(document, null, 2)}\n`;
|
|
25
29
|
}
|
package/src/verbs/explain.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { CompletedFinding } from "../finding.ts";
|
|
|
6
6
|
import { OUTSIDE, swept as judged } from "../graph.ts";
|
|
7
7
|
import type { InventorySymbol } from "../inventory.ts";
|
|
8
8
|
import { referenceKey, type Configured } from "../matrix.ts";
|
|
9
|
+
import { partialNotes } from "../partial.ts";
|
|
9
10
|
import { positionKey } from "../position.ts";
|
|
10
11
|
import type { Reference } from "../references.ts";
|
|
11
12
|
import { resolve } from "../resolve.ts";
|
|
@@ -344,6 +345,9 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
|
|
|
344
345
|
for (const one of retained) {
|
|
345
346
|
text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
|
|
346
347
|
}
|
|
348
|
+
} else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
|
|
349
|
+
text +=
|
|
350
|
+
"answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
|
|
347
351
|
} else {
|
|
348
352
|
text += liveness(analysis, subject, cascade);
|
|
349
353
|
}
|
|
@@ -351,12 +355,12 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
|
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
/**
|
|
354
|
-
* Explains one symbol from the analysis the report is built from
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
358
|
+
* Explains one symbol from the analysis the report is built from: why a reported symbol is
|
|
359
|
+
* reported, which exemption classes held a retained one back, that an unanswered question held
|
|
360
|
+
* a held one, and for any other declaration the relations that hold it live and the shortest
|
|
361
|
+
* path from a root or a loaded consumer, or the references it has where no path reaches it. A
|
|
362
|
+
* request naming no one declaration exits with the usage code and names what partially
|
|
363
|
+
* matches it. Where the analysis was partial, the error stream says so.
|
|
360
364
|
*/
|
|
361
365
|
export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
362
366
|
const named = namedSymbol(option);
|
|
@@ -365,6 +369,10 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
|
|
|
365
369
|
production: true,
|
|
366
370
|
});
|
|
367
371
|
const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
|
|
372
|
+
const notes = partialNotes({
|
|
373
|
+
notBuilt: analysis.run.notBuilt,
|
|
374
|
+
unanswered: analysis.run.unanswered,
|
|
375
|
+
});
|
|
368
376
|
if (subject === undefined) {
|
|
369
377
|
err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
|
|
370
378
|
for (const candidate of candidates) {
|
|
@@ -373,8 +381,11 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
|
|
|
373
381
|
if (candidates.length === 0) {
|
|
374
382
|
err.write(" no symbol of the target partially matches it either\n");
|
|
375
383
|
}
|
|
376
|
-
|
|
384
|
+
} else {
|
|
385
|
+
out.write(explanation(analysis, subject, config.reporters.cascade));
|
|
386
|
+
}
|
|
387
|
+
for (const note of notes) {
|
|
388
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
377
389
|
}
|
|
378
|
-
|
|
379
|
-
return EXIT_CLEAN;
|
|
390
|
+
return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
|
|
380
391
|
};
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
discoverProjects,
|
|
3
|
+
DiscoveryError,
|
|
4
|
+
isGuess,
|
|
5
|
+
projectErrors,
|
|
6
|
+
renderDiagnostic,
|
|
7
|
+
targetRelative,
|
|
8
|
+
type NotBuilt,
|
|
9
|
+
} from "../discover.ts";
|
|
10
|
+
import { partialNotes } from "../partial.ts";
|
|
11
|
+
import { relativePath, resolvePath } from "../paths.ts";
|
|
2
12
|
import { resolveMatrix } from "../resolve.ts";
|
|
3
13
|
import { diagnosticsOf, runSession } from "../session.ts";
|
|
4
14
|
import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
|
|
@@ -7,7 +17,8 @@ import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
|
|
|
7
17
|
* Writes the name of every project one run analyzes, one per line, having read each
|
|
8
18
|
* project's diagnostics first: the identifier the build matrix declares for it, or,
|
|
9
19
|
* where discovery derived the project, its configuration file's path below the
|
|
10
|
-
* target root.
|
|
20
|
+
* target root. A derived project that does not load is not one the run analyzes, so it
|
|
21
|
+
* is named on the error stream instead, as the analysis drops it.
|
|
11
22
|
*/
|
|
12
23
|
export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
13
24
|
const matrix = resolveMatrix(inputs());
|
|
@@ -15,12 +26,36 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
|
|
|
15
26
|
const engine = openClient(false);
|
|
16
27
|
const discovered = discoverProjects(engine, host, scoped, matrix);
|
|
17
28
|
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
29
|
+
const root = resolvePath(host.workingDirectory(), scoped.target.path);
|
|
30
|
+
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
18
31
|
const failures: string[] = [];
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
const session = runSession(
|
|
33
|
+
engine,
|
|
34
|
+
discovered.configFiles,
|
|
35
|
+
(project) => {
|
|
36
|
+
const id = ids.get(project.configFile) ?? project.configFile;
|
|
37
|
+
const errors = projectErrors(diagnosticsOf(project));
|
|
38
|
+
const [unloaded] = errors.load;
|
|
39
|
+
if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
|
|
40
|
+
const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
|
|
41
|
+
notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
|
|
45
|
+
return id;
|
|
46
|
+
},
|
|
47
|
+
discovered.derived ? "record" : "refuse",
|
|
48
|
+
);
|
|
49
|
+
for (const configFile of session.unopened) {
|
|
50
|
+
if (!isGuess(discovered, configFile)) {
|
|
51
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
52
|
+
}
|
|
53
|
+
notBuilt.push({
|
|
54
|
+
id: ids.get(configFile) ?? configFile,
|
|
55
|
+
configFile,
|
|
56
|
+
error: "the compiler opened no project for the configuration",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
24
59
|
if (failures.length > 0) {
|
|
25
60
|
for (const line of failures) {
|
|
26
61
|
err.write(`${line}\n`);
|
|
@@ -28,8 +63,19 @@ export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openCli
|
|
|
28
63
|
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
29
64
|
return EXIT_FAILURE;
|
|
30
65
|
}
|
|
66
|
+
const projects = session.projects.filter((id): id is string => id !== undefined);
|
|
67
|
+
if (projects.length === 0 && notBuilt.length > 0) {
|
|
68
|
+
for (const note of partialNotes({ notBuilt, unanswered: [] })) {
|
|
69
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
70
|
+
}
|
|
71
|
+
err.write("deadset-ts: no configuration discovery derived could be built\n");
|
|
72
|
+
return EXIT_FAILURE;
|
|
73
|
+
}
|
|
31
74
|
for (const id of projects) {
|
|
32
75
|
out.write(`${id}\n`);
|
|
33
76
|
}
|
|
77
|
+
for (const note of partialNotes({ notBuilt, unanswered: [] })) {
|
|
78
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
79
|
+
}
|
|
34
80
|
return EXIT_CLEAN;
|
|
35
81
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runSweep } from "../analysis.ts";
|
|
2
2
|
import { retainedLines } from "../exempt.ts";
|
|
3
|
+
import { partialNotes } from "../partial.ts";
|
|
3
4
|
import { resolve } from "../resolve.ts";
|
|
4
5
|
import { EXIT_CLEAN, type Verb } from "./verb.ts";
|
|
5
6
|
|
|
@@ -8,9 +9,9 @@ import { EXIT_CLEAN, type Verb } from "./verb.ts";
|
|
|
8
9
|
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
9
10
|
* several configurations found is one record, so the line names no configuration. The
|
|
10
11
|
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
11
|
-
* read it.
|
|
12
|
+
* read it. Where the analysis was partial, the error stream says so.
|
|
12
13
|
*/
|
|
13
|
-
export const printRetainedVerb: Verb = ({ out, host, inputs, scope, openClient }) => {
|
|
14
|
+
export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
14
15
|
const { config } = resolve(inputs());
|
|
15
16
|
const scoped = scope();
|
|
16
17
|
const swept = runSweep(openClient(false), host, scoped, config, {
|
|
@@ -20,5 +21,8 @@ export const printRetainedVerb: Verb = ({ out, host, inputs, scope, openClient }
|
|
|
20
21
|
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
21
22
|
out.write(`${line}\n`);
|
|
22
23
|
}
|
|
24
|
+
for (const note of partialNotes({ notBuilt: swept.notBuilt, unanswered: swept.unanswered })) {
|
|
25
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
26
|
+
}
|
|
23
27
|
return EXIT_CLEAN;
|
|
24
28
|
};
|
package/src/verbs/print-roots.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runRoots, type RunRoot } from "../analysis.ts";
|
|
2
|
+
import { partialNotes } from "../partial.ts";
|
|
2
3
|
import { resolve } from "../resolve.ts";
|
|
3
4
|
import { EXIT_CLEAN, EXIT_FINDINGS, type Verb } from "./verb.ts";
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ function rootLine(root: RunRoot, several: boolean): string {
|
|
|
20
21
|
/**
|
|
21
22
|
* Writes the root set of the run, one root per line, and then names on the error
|
|
22
23
|
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
23
|
-
* one such string fails the run. The line shape is this command's own rather than a
|
|
24
|
+
* one such string fails the run, and then where the analysis was partial. The line shape is this command's own rather than a
|
|
24
25
|
* Contract format, so that sort and cut read it.
|
|
25
26
|
*/
|
|
26
27
|
export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
@@ -34,5 +35,8 @@ export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient
|
|
|
34
35
|
for (const finding of answer.findings) {
|
|
35
36
|
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
36
37
|
}
|
|
38
|
+
for (const note of partialNotes({ notBuilt: answer.notBuilt, unanswered: answer.unanswered })) {
|
|
39
|
+
err.write(`deadset-ts: ${note}\n`);
|
|
40
|
+
}
|
|
37
41
|
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
38
42
|
};
|
package/src/version.ts
CHANGED