@cplieger/deadset-ts 3.8.0 → 3.9.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 +13 -13
- package/dist/bin/node-host.js +32 -2
- package/dist/src/analysis.js +28 -12
- package/dist/src/conformance.js +22 -0
- package/dist/src/destructuring.js +273 -0
- package/dist/src/findings/completion.js +9 -3
- package/dist/src/findings/self-check.js +14 -2
- package/dist/src/references.js +54 -15
- package/dist/src/report.js +237 -0
- package/dist/src/reporters/json.js +4 -0
- package/dist/src/reporters/reporter.js +1 -0
- package/dist/src/reporters/reporters.js +10 -0
- package/dist/src/reporters/text.js +34 -0
- package/dist/src/run.js +23 -10
- package/dist/src/verbs/analyze.js +184 -0
- package/dist/src/verbs/describe.js +45 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +65 -14
- package/src/conformance.ts +34 -0
- package/src/destructuring.ts +353 -0
- package/src/finding.ts +13 -0
- package/src/findings/completion.ts +9 -4
- package/src/findings/self-check.ts +23 -2
- package/src/host.ts +5 -0
- package/src/references.ts +80 -19
- package/src/report.ts +430 -0
- package/src/reporters/json.ts +7 -0
- package/src/reporters/reporter.ts +18 -0
- package/src/reporters/reporters.ts +13 -0
- package/src/reporters/text.ts +45 -0
- package/src/run.ts +40 -12
- package/src/verbs/analyze.ts +234 -0
- package/src/verbs/describe.ts +46 -0
- package/src/verbs/verb.ts +13 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { runAnalysis, type RunProject } from "../analysis.ts";
|
|
2
|
+
import { ConfigError, type Format } from "../config.ts";
|
|
3
|
+
import { RECORDED_CONFORMANCE, reportedConformance } from "../conformance.ts";
|
|
4
|
+
import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.ts";
|
|
5
|
+
import { packageScope } from "../inventory.ts";
|
|
6
|
+
import { normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
7
|
+
import type { Host } from "../host.ts";
|
|
8
|
+
import {
|
|
9
|
+
buildReport,
|
|
10
|
+
capFindings,
|
|
11
|
+
sortFindings,
|
|
12
|
+
type Report,
|
|
13
|
+
type ReportConfiguration,
|
|
14
|
+
} from "../report.ts";
|
|
15
|
+
import { json } from "../reporters/json.ts";
|
|
16
|
+
import { RENDERINGS } from "../reporters/reporters.ts";
|
|
17
|
+
import { resolve } from "../resolve.ts";
|
|
18
|
+
import type { Module } from "../scope.ts";
|
|
19
|
+
import { writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
20
|
+
import { CONTRACT_VERSION } from "../version.ts";
|
|
21
|
+
import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts";
|
|
22
|
+
|
|
23
|
+
/** The options `analyze` takes beside the ones every verb takes. */
|
|
24
|
+
export const ANALYZE_OPTIONS: VerbOptions = {
|
|
25
|
+
plain: ["report", "baseline-write", "exit-code"],
|
|
26
|
+
repeatable: ["format"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The two values `--exit-code` takes; on, the default, puts the run's verdict in the exit code. */
|
|
30
|
+
const EXIT_CODE_ON = "on";
|
|
31
|
+
const EXIT_CODE_OFF = "off";
|
|
32
|
+
|
|
33
|
+
/** Why a declared consumer is not loaded, which every report names it with. */
|
|
34
|
+
const NOT_LOADED = "this analyzer loads no consumer beside the target";
|
|
35
|
+
|
|
36
|
+
/** A path of the run no report can name, which ends the run with the failure code. */
|
|
37
|
+
class ReportPathError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "ReportPathError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The formats this analyzer renders, for a message that names them. */
|
|
45
|
+
function formatNames(): string {
|
|
46
|
+
return [...RENDERINGS.keys()].sort().join(", ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The renderings one invocation writes: the ones it named, else the configuration's. */
|
|
50
|
+
function formatsOf(named: readonly string[], configured: readonly Format[]): readonly Format[] {
|
|
51
|
+
const formats = named.length > 0 ? named : configured;
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
for (const format of formats) {
|
|
54
|
+
if (!RENDERINGS.has(format as Format)) {
|
|
55
|
+
throw new ConfigError(
|
|
56
|
+
"malformed",
|
|
57
|
+
"",
|
|
58
|
+
`${JSON.stringify(format)} is not a format this analyzer renders: the formats are ${formatNames()}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (seen.has(format)) {
|
|
62
|
+
throw new ConfigError("malformed", "", `the format ${JSON.stringify(format)} is named twice`);
|
|
63
|
+
}
|
|
64
|
+
seen.add(format);
|
|
65
|
+
}
|
|
66
|
+
return formats as readonly Format[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One directory of the run below the directory the run was invoked from, as a report names
|
|
71
|
+
* it. A report admits that directory and every path below it and none that climbs out, so a
|
|
72
|
+
* target elsewhere has no spelling a report can carry and fails the run.
|
|
73
|
+
*/
|
|
74
|
+
function runRelative(invoked: string, path: string): string {
|
|
75
|
+
if (normalizePath(invoked) === normalizePath(path)) {
|
|
76
|
+
return ".";
|
|
77
|
+
}
|
|
78
|
+
const below = relativePath(invoked, path);
|
|
79
|
+
if (below === undefined) {
|
|
80
|
+
throw new ReportPathError(
|
|
81
|
+
`a report names its target relative to the directory the run was invoked from, and ${invoked} does not contain ${path}: run from a directory that does`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return below;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Each project of the run as a report names it, by its configuration file below the target. */
|
|
88
|
+
function configurationsOf(
|
|
89
|
+
projects: readonly RunProject[],
|
|
90
|
+
targetRoot: string,
|
|
91
|
+
): readonly ReportConfiguration[] {
|
|
92
|
+
return projects.map((one) => {
|
|
93
|
+
const project = relativePath(targetRoot, one.configFile);
|
|
94
|
+
if (project === undefined) {
|
|
95
|
+
throw new ReportPathError(
|
|
96
|
+
`the project ${one.id} is opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return { id: one.id, project };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The name one module publishes itself under: the scope's, else its manifest's. */
|
|
104
|
+
function identityOf(module: Module, host: Host): string {
|
|
105
|
+
return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A count with its noun, so a message reads for one record as well as for several. */
|
|
109
|
+
function counted(n: number, noun: string): string {
|
|
110
|
+
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Every document one invocation asked for: the report, each rendering beside it, a baseline. */
|
|
114
|
+
interface Documents {
|
|
115
|
+
readonly report: string;
|
|
116
|
+
readonly formats: readonly Format[];
|
|
117
|
+
readonly baseline: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Analyzes the scope under the configuration and writes the JSON report to the path
|
|
122
|
+
* `--report` names, each rendering `--format` names beside it at the report's path with the
|
|
123
|
+
* format's suffix appended, and a baseline of every finding where `--baseline-write` names a
|
|
124
|
+
* path. Nothing is written to the output stream; diagnostics go to the error stream and the
|
|
125
|
+
* verdict is the exit code, or 0 under `--exit-code=off`.
|
|
126
|
+
*/
|
|
127
|
+
export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
|
|
128
|
+
const reportPath = option("report");
|
|
129
|
+
if (reportPath === undefined || reportPath === "") {
|
|
130
|
+
throw new ConfigError(
|
|
131
|
+
"malformed",
|
|
132
|
+
"",
|
|
133
|
+
"analyze writes its report to the path --report names, and no path was named",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const exitCode = option("exit-code") ?? EXIT_CODE_ON;
|
|
137
|
+
if (exitCode !== EXIT_CODE_ON && exitCode !== EXIT_CODE_OFF) {
|
|
138
|
+
throw new ConfigError(
|
|
139
|
+
"malformed",
|
|
140
|
+
"",
|
|
141
|
+
`--exit-code=${JSON.stringify(exitCode)}: the values are ${EXIT_CODE_ON} and ${EXIT_CODE_OFF}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const { config, provenance } = resolve(inputs());
|
|
145
|
+
const kind = config.targetKind;
|
|
146
|
+
if (kind === "") {
|
|
147
|
+
throw new ConfigError("missing-target-kind", "target.kind", "target.kind is not set");
|
|
148
|
+
}
|
|
149
|
+
const invoked = host.workingDirectory();
|
|
150
|
+
const baseline = option("baseline-write");
|
|
151
|
+
const documents: Documents = {
|
|
152
|
+
report: resolvePath(invoked, reportPath),
|
|
153
|
+
formats: formatsOf(repeated("format"), config.reporters.formats),
|
|
154
|
+
baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
|
|
155
|
+
};
|
|
156
|
+
const scoped = scope();
|
|
157
|
+
const targetRoot = scoped.target.path;
|
|
158
|
+
|
|
159
|
+
let version: string;
|
|
160
|
+
try {
|
|
161
|
+
version = host.analyzerVersion();
|
|
162
|
+
} catch (error: unknown) {
|
|
163
|
+
err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
164
|
+
return EXIT_FAILURE;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let report: Report;
|
|
168
|
+
let recorded: readonly Recorded[];
|
|
169
|
+
let verdict: number;
|
|
170
|
+
try {
|
|
171
|
+
const root = runRelative(invoked, targetRoot);
|
|
172
|
+
const { result, run } = runAnalysis(openClient(false), host, scoped, config, provenance, {
|
|
173
|
+
production: true,
|
|
174
|
+
});
|
|
175
|
+
const built = buildReport({
|
|
176
|
+
contractVersion: CONTRACT_VERSION,
|
|
177
|
+
version,
|
|
178
|
+
conformance: reportedConformance(RECORDED_CONFORMANCE),
|
|
179
|
+
target: { kind, root, identity: identityOf(scoped.target, host) },
|
|
180
|
+
configurations: configurationsOf(run.projects, targetRoot),
|
|
181
|
+
unavailable: scoped.consumers.map((consumer) => ({
|
|
182
|
+
id: identityOf(consumer, host),
|
|
183
|
+
reason: NOT_LOADED,
|
|
184
|
+
})),
|
|
185
|
+
result,
|
|
186
|
+
testFileRules: run.testFileRules,
|
|
187
|
+
});
|
|
188
|
+
// The baseline records every finding, so its rows are taken before the cap bounds what
|
|
189
|
+
// a rendering prints: a baseline missing a reported finding fails the next run on it.
|
|
190
|
+
recorded = recordedFindings(result);
|
|
191
|
+
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
192
|
+
verdict = verdictOf(result, config);
|
|
193
|
+
} catch (error: unknown) {
|
|
194
|
+
if (!(error instanceof ReportPathError)) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
err.write(`deadset-ts: ${error.message}\n`);
|
|
198
|
+
return EXIT_FAILURE;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
host.writeFile(documents.report, json(report));
|
|
203
|
+
for (const format of documents.formats) {
|
|
204
|
+
const rendering = RENDERINGS.get(format);
|
|
205
|
+
if (rendering !== undefined) {
|
|
206
|
+
host.writeFile(documents.report + rendering.suffix, rendering.render(report));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (documents.baseline !== undefined) {
|
|
210
|
+
host.writeFile(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
|
|
211
|
+
}
|
|
212
|
+
} catch (error: unknown) {
|
|
213
|
+
err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
214
|
+
return EXIT_FAILURE;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (report.totals.pending > 0) {
|
|
218
|
+
err.write(
|
|
219
|
+
`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (report.totals.stale_suppressions > 0) {
|
|
223
|
+
err.write(`deadset-ts: ${counted(report.totals.stale_suppressions, "stale suppression")}\n`);
|
|
224
|
+
}
|
|
225
|
+
if (exitCode === EXIT_CODE_OFF) {
|
|
226
|
+
if (verdict !== EXIT_CLEAN) {
|
|
227
|
+
err.write(
|
|
228
|
+
`deadset-ts: the exit code is configured off: the verdict of this run is ${String(verdict)}\n`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return EXIT_CLEAN;
|
|
232
|
+
}
|
|
233
|
+
return verdict;
|
|
234
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RECORDED_CONFORMANCE, type Conformance } from "../conformance.ts";
|
|
2
|
+
import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSION } from "../report.ts";
|
|
3
|
+
import { CONTRACT_VERSION } from "../version.ts";
|
|
4
|
+
import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
8
|
+
* report schema versions it reads, the languages it claims, and its recorded result over
|
|
9
|
+
* the conformance corpus, absent where it records none.
|
|
10
|
+
*/
|
|
11
|
+
export function describeDocument(version: string, recorded: Conformance | undefined): string {
|
|
12
|
+
const document = {
|
|
13
|
+
name: ANALYZER_NAME,
|
|
14
|
+
version,
|
|
15
|
+
contract_version: CONTRACT_VERSION,
|
|
16
|
+
schema_versions_accepted: [SCHEMA_VERSION],
|
|
17
|
+
languages: [LANGUAGE],
|
|
18
|
+
...(recorded === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: {
|
|
21
|
+
conformance: {
|
|
22
|
+
corpus_version: recorded.corpusVersion,
|
|
23
|
+
result: recorded.result,
|
|
24
|
+
digest: recorded.digest,
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Writes this analyzer's description to the output stream, and nothing to the error stream. */
|
|
32
|
+
export const describeVerb: Verb = ({ out, err, host, args }) => {
|
|
33
|
+
if (args.length > 0) {
|
|
34
|
+
err.write(`deadset-ts: describe takes no argument, got ${JSON.stringify(args[0])}\n`);
|
|
35
|
+
return EXIT_USAGE;
|
|
36
|
+
}
|
|
37
|
+
let version: string;
|
|
38
|
+
try {
|
|
39
|
+
version = host.analyzerVersion();
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
err.write(`deadset-ts: describe: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
42
|
+
return EXIT_FAILURE;
|
|
43
|
+
}
|
|
44
|
+
out.write(describeDocument(version, RECORDED_CONFORMANCE));
|
|
45
|
+
return EXIT_CLEAN;
|
|
46
|
+
};
|
package/src/verbs/verb.ts
CHANGED
|
@@ -28,6 +28,19 @@ export interface Invocation {
|
|
|
28
28
|
readonly openClient: (collectTiming: boolean) => Engine;
|
|
29
29
|
/** The emitter of every kind family, which a verb that reports findings runs. */
|
|
30
30
|
readonly emitters: Emitters;
|
|
31
|
+
/** The arguments after the verb's name, as written. */
|
|
32
|
+
readonly args: readonly string[];
|
|
33
|
+
/** The last value one of the verb's own options was given, undefined where none was. */
|
|
34
|
+
readonly option: (name: string) => string | undefined;
|
|
35
|
+
/** Every value a repeatable option of the verb was given, in the order they were written. */
|
|
36
|
+
readonly repeated: (name: string) => readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The options one verb takes beside the ones every verb takes, each taking a value. */
|
|
40
|
+
export interface VerbOptions {
|
|
41
|
+
readonly plain: readonly string[];
|
|
42
|
+
/** The options that may be written more than once, each occurrence one value. */
|
|
43
|
+
readonly repeatable: readonly string[];
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|