@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
package/dist/src/references.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
|
+
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
3
4
|
import { globExpression } from "./glob.js";
|
|
4
5
|
import { nodeKey } from "./inventory.js";
|
|
5
6
|
import { byPosition, renderPosition } from "./position.js";
|
|
@@ -205,6 +206,7 @@ function linksOf(file, node, declarations, enclosing) {
|
|
|
205
206
|
*/
|
|
206
207
|
function sitesOf(file, declarations, fileId) {
|
|
207
208
|
const found = [];
|
|
209
|
+
const reads = [];
|
|
208
210
|
const links = [];
|
|
209
211
|
const evaluations = [];
|
|
210
212
|
const decorated = [];
|
|
@@ -243,6 +245,9 @@ function sitesOf(file, declarations, fileId) {
|
|
|
243
245
|
}
|
|
244
246
|
markNames(node);
|
|
245
247
|
markStores(node, writes);
|
|
248
|
+
for (const read of propertyReadsOf(node)) {
|
|
249
|
+
reads.push({ ...read, from: enclosing });
|
|
250
|
+
}
|
|
246
251
|
links.push(...linksOf(file, node, declarations, enclosing));
|
|
247
252
|
const evaluated = evaluatedBy(node);
|
|
248
253
|
if (evaluated !== undefined) {
|
|
@@ -262,7 +267,7 @@ function sitesOf(file, declarations, fileId) {
|
|
|
262
267
|
enclosing = outer;
|
|
263
268
|
};
|
|
264
269
|
file.forEachChild(visit);
|
|
265
|
-
return { uses: found, links, evaluations, decorated };
|
|
270
|
+
return { uses: found, reads, links, evaluations, decorated };
|
|
266
271
|
}
|
|
267
272
|
/**
|
|
268
273
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
@@ -272,6 +277,22 @@ function sitesOf(file, declarations, fileId) {
|
|
|
272
277
|
function patternRule(index) {
|
|
273
278
|
return `test-file-pattern-${String(index + 1)}`;
|
|
274
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
282
|
+
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
283
|
+
*/
|
|
284
|
+
export function testFileRulesOf(testFiles, paths) {
|
|
285
|
+
const distinct = [...new Set(paths)];
|
|
286
|
+
return testFiles
|
|
287
|
+
.map((pattern, index) => {
|
|
288
|
+
const expression = globExpression(pattern);
|
|
289
|
+
return {
|
|
290
|
+
rule: patternRule(index),
|
|
291
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.sort((a, b) => compare(a.rule, b.rule));
|
|
295
|
+
}
|
|
275
296
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
276
297
|
function compare(a, b) {
|
|
277
298
|
if (a === b) {
|
|
@@ -281,7 +302,7 @@ function compare(a, b) {
|
|
|
281
302
|
}
|
|
282
303
|
/**
|
|
283
304
|
* Every reference one project's own files make to the project's own declarations, in
|
|
284
|
-
* position order, and the test-file rules
|
|
305
|
+
* position order, and the files the test-file rules classified.
|
|
285
306
|
*
|
|
286
307
|
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
287
308
|
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
@@ -297,10 +318,10 @@ export function references(project, held, targetRoot, options) {
|
|
|
297
318
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
298
319
|
const declarations = held.declarations;
|
|
299
320
|
const patterns = options.testFiles.map(globExpression);
|
|
300
|
-
const matched = options.testFiles.map(() => 0);
|
|
301
321
|
const found = [];
|
|
302
322
|
const tests = [];
|
|
303
323
|
const chains = aliasChains(project, held);
|
|
324
|
+
const destructured = destructuring(project, cap);
|
|
304
325
|
let batched = 0;
|
|
305
326
|
let fileBatches = 0;
|
|
306
327
|
let residueFallbacks = 0;
|
|
@@ -332,17 +353,11 @@ export function references(project, held, targetRoot, options) {
|
|
|
332
353
|
};
|
|
333
354
|
for (const file of files) {
|
|
334
355
|
const path = renderPosition(file, targetRoot, 0).path;
|
|
335
|
-
const
|
|
336
|
-
hits.forEach((hit, index) => {
|
|
337
|
-
if (hit) {
|
|
338
|
-
matched[index] = (matched[index] ?? 0) + 1;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
const test = hits.includes(true);
|
|
356
|
+
const test = patterns.some((pattern) => pattern.test(path));
|
|
342
357
|
if (test) {
|
|
343
358
|
tests.push(path);
|
|
344
359
|
}
|
|
345
|
-
const { uses, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
360
|
+
const { uses, reads, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
346
361
|
// A decorator is written inside the declaration it is attached to and receives it,
|
|
347
362
|
// so the declaration is used where the decorator is written, whatever the
|
|
348
363
|
// decorator does with it.
|
|
@@ -417,6 +432,25 @@ export function references(project, held, targetRoot, options) {
|
|
|
417
432
|
}
|
|
418
433
|
}
|
|
419
434
|
}
|
|
435
|
+
// A pattern names a property of the value it destructures, which no name node of
|
|
436
|
+
// the pattern resolves to: a shorthand's name is the local it binds.
|
|
437
|
+
destructured.resolve(reads).forEach((symbol, index) => {
|
|
438
|
+
const read = reads[index];
|
|
439
|
+
if (symbol === undefined || read === undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const position = renderPosition(file, targetRoot, read.key.getStart());
|
|
443
|
+
for (const target of chains.chainOf(symbol)) {
|
|
444
|
+
found.push({
|
|
445
|
+
from: read.from,
|
|
446
|
+
to: target.id,
|
|
447
|
+
position,
|
|
448
|
+
use: "read",
|
|
449
|
+
resolution: "destructured",
|
|
450
|
+
test,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
});
|
|
420
454
|
for (const site of uses) {
|
|
421
455
|
const direct = site.shorthand !== undefined
|
|
422
456
|
? "shorthand"
|
|
@@ -449,10 +483,15 @@ export function references(project, held, targetRoot, options) {
|
|
|
449
483
|
return {
|
|
450
484
|
configFile: project.configFile,
|
|
451
485
|
references: found,
|
|
452
|
-
testFileRules: options.testFiles
|
|
453
|
-
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
454
|
-
.sort((a, b) => compare(a.rule, b.rule)),
|
|
455
486
|
testFilePaths: tests.sort(compare),
|
|
456
|
-
cost: {
|
|
487
|
+
cost: {
|
|
488
|
+
batched,
|
|
489
|
+
fileBatches,
|
|
490
|
+
residueFallbacks,
|
|
491
|
+
shorthandLookups,
|
|
492
|
+
aliasSteps: chains.steps,
|
|
493
|
+
patternBatches: destructured.cost.patternBatches,
|
|
494
|
+
patternLookups: destructured.cost.patternLookups,
|
|
495
|
+
},
|
|
457
496
|
};
|
|
458
497
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report one analysis writes: the Contract's report document, member for member and
|
|
3
|
+
* in the order its schema lists them, built from what one run answered. Every reporter
|
|
4
|
+
* renders this document, so no two renderings disagree about which findings exist.
|
|
5
|
+
*/
|
|
6
|
+
/** The version of the report schema this analyzer writes a report to. */
|
|
7
|
+
export const SCHEMA_VERSION = "6.0.0";
|
|
8
|
+
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
9
|
+
export const ANALYZER_NAME = "deadset-ts";
|
|
10
|
+
/** The language this analyzer claims. */
|
|
11
|
+
export const LANGUAGE = "ts";
|
|
12
|
+
/** Two strings ordered bytewise. */
|
|
13
|
+
function compare(a, b) {
|
|
14
|
+
if (a === b) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
return a < b ? -1 : 1;
|
|
18
|
+
}
|
|
19
|
+
function wirePosition(position) {
|
|
20
|
+
return {
|
|
21
|
+
path: position.path,
|
|
22
|
+
line: position.line,
|
|
23
|
+
column: position.column,
|
|
24
|
+
end_line: position.endLine,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function wireSymbol(symbol) {
|
|
28
|
+
return { ref: symbol.ref, name: symbol.name, position: wirePosition(symbol.position) };
|
|
29
|
+
}
|
|
30
|
+
/** The details one code carries, in the order the finding schema lists them. */
|
|
31
|
+
function wireDetails(details) {
|
|
32
|
+
const { entry } = details;
|
|
33
|
+
return {
|
|
34
|
+
...(details.narrowerVisibility === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { narrower_visibility: details.narrowerVisibility }),
|
|
37
|
+
...(details.implementations === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { implementations: details.implementations.map(wireSymbol) }),
|
|
40
|
+
...(details.writePositions === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { write_positions: details.writePositions.map(wirePosition) }),
|
|
43
|
+
...(details.dependencyClass === undefined ? {} : { dependency_class: details.dependencyClass }),
|
|
44
|
+
...(details.mechanism === undefined ? {} : { mechanism: details.mechanism }),
|
|
45
|
+
...(entry === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: {
|
|
48
|
+
entry: {
|
|
49
|
+
code: entry.code,
|
|
50
|
+
...(entry.symbol === undefined ? {} : { symbol: entry.symbol }),
|
|
51
|
+
...(entry.path === undefined ? {} : { path: entry.path }),
|
|
52
|
+
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** One finding as the finding schema spells it, member for member in its order. */
|
|
58
|
+
export function wireFinding(finding) {
|
|
59
|
+
return {
|
|
60
|
+
code: finding.code,
|
|
61
|
+
kind: finding.kind,
|
|
62
|
+
language: finding.language,
|
|
63
|
+
position: wirePosition(finding.position),
|
|
64
|
+
symbol: {
|
|
65
|
+
ref: finding.symbol.ref,
|
|
66
|
+
kind: finding.symbol.kind,
|
|
67
|
+
name: finding.symbol.name,
|
|
68
|
+
size_lines: finding.symbol.sizeLines,
|
|
69
|
+
},
|
|
70
|
+
reachability_class: finding.reachabilityClass,
|
|
71
|
+
confidence: finding.confidence,
|
|
72
|
+
...(finding.livenessRelation === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { liveness_relation: finding.livenessRelation }),
|
|
75
|
+
test_only: finding.testOnly,
|
|
76
|
+
generated: finding.generated,
|
|
77
|
+
component: {
|
|
78
|
+
id: finding.component.id,
|
|
79
|
+
root: finding.component.root,
|
|
80
|
+
symbol_count: finding.component.symbolCount,
|
|
81
|
+
deletable_lines: finding.component.deletableLines,
|
|
82
|
+
},
|
|
83
|
+
retained_by: finding.retainedBy,
|
|
84
|
+
configurations: finding.configurations,
|
|
85
|
+
consumers_loaded: finding.consumersLoaded,
|
|
86
|
+
fixability: finding.fixability,
|
|
87
|
+
severity: finding.severity,
|
|
88
|
+
message: finding.message,
|
|
89
|
+
details: wireDetails(finding.details),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function wireEvaluation(evaluation) {
|
|
93
|
+
return {
|
|
94
|
+
edge: evaluation.edge,
|
|
95
|
+
side: evaluation.side,
|
|
96
|
+
symbol: evaluation.symbol,
|
|
97
|
+
state: evaluation.state,
|
|
98
|
+
...(evaluation.finding === undefined ? {} : { finding: wireFinding(evaluation.finding) }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function wireStale(stale) {
|
|
102
|
+
return {
|
|
103
|
+
code: stale.code,
|
|
104
|
+
mechanism: stale.mechanism,
|
|
105
|
+
entry: {
|
|
106
|
+
code: stale.entry.code,
|
|
107
|
+
...(stale.entry.symbol === undefined ? {} : { symbol: stale.entry.symbol }),
|
|
108
|
+
path: stale.entry.path,
|
|
109
|
+
reason: stale.entry.reason,
|
|
110
|
+
},
|
|
111
|
+
position: {
|
|
112
|
+
path: stale.position.path,
|
|
113
|
+
line: stale.position.line,
|
|
114
|
+
column: stale.position.column,
|
|
115
|
+
},
|
|
116
|
+
symbol: stale.symbol,
|
|
117
|
+
message: stale.message,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** The canonical key over two findings: path, line, column, code, then the symbol reference. */
|
|
121
|
+
function canonical(a, b) {
|
|
122
|
+
return (compare(a.position.path, b.position.path) ||
|
|
123
|
+
a.position.line - b.position.line ||
|
|
124
|
+
a.position.column - b.position.column ||
|
|
125
|
+
compare(a.code, b.code) ||
|
|
126
|
+
compare(a.symbol.ref, b.symbol.ref));
|
|
127
|
+
}
|
|
128
|
+
/** The canonical key over two stale suppressions, each component from the record's own member. */
|
|
129
|
+
function canonicalStale(a, b) {
|
|
130
|
+
return (compare(a.position.path, b.position.path) ||
|
|
131
|
+
a.position.line - b.position.line ||
|
|
132
|
+
a.position.column - b.position.column ||
|
|
133
|
+
compare(a.code, b.code) ||
|
|
134
|
+
compare(a.symbol, b.symbol));
|
|
135
|
+
}
|
|
136
|
+
/** Edge evaluations by edge, then side, then their compact encodings. */
|
|
137
|
+
function byEdge(a, b) {
|
|
138
|
+
return (compare(a.edge, b.edge) ||
|
|
139
|
+
compare(a.side, b.side) ||
|
|
140
|
+
compare(JSON.stringify(a), JSON.stringify(b)));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The lines a reader deletes by acting on every finding: each distinct component a root
|
|
144
|
+
* finding names, counted once however many of its roots the findings hold.
|
|
145
|
+
*/
|
|
146
|
+
function deletableLines(findings) {
|
|
147
|
+
const counted = new Map();
|
|
148
|
+
for (const finding of findings) {
|
|
149
|
+
if (finding.component.root) {
|
|
150
|
+
counted.set(finding.component.id, finding.component.deletable_lines);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let total = 0;
|
|
154
|
+
for (const lines of counted.values()) {
|
|
155
|
+
total += lines;
|
|
156
|
+
}
|
|
157
|
+
return total;
|
|
158
|
+
}
|
|
159
|
+
/** The totals of one finding set, counted over the whole set. */
|
|
160
|
+
function totalsOf(findings, result) {
|
|
161
|
+
const bySeverity = { allow: 0, warn: 0, deny: 0 };
|
|
162
|
+
for (const finding of findings) {
|
|
163
|
+
bySeverity[finding.severity] += 1;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
findings: findings.length,
|
|
167
|
+
by_severity: bySeverity,
|
|
168
|
+
deletable_lines: deletableLines(findings),
|
|
169
|
+
suppressions_in_effect: result.totals.suppressionsInEffect,
|
|
170
|
+
reasons_recorded: result.totals.reasonsRecorded,
|
|
171
|
+
stale_suppressions: result.staleSuppressions.length,
|
|
172
|
+
pending: result.totals.pending,
|
|
173
|
+
omitted: 0,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Builds the report of one run, every array in the order the Contract fixes for it. */
|
|
177
|
+
export function buildReport(input) {
|
|
178
|
+
const findings = input.result.findings.map(wireFinding).sort(canonical);
|
|
179
|
+
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
180
|
+
.sort((a, b) => compare(a.id, b.id))
|
|
181
|
+
.map((one) => ({ id: one.id, role: "consumer", reason: one.reason }));
|
|
182
|
+
return {
|
|
183
|
+
schema_version: SCHEMA_VERSION,
|
|
184
|
+
contract_version: input.contractVersion,
|
|
185
|
+
analyzer: {
|
|
186
|
+
name: ANALYZER_NAME,
|
|
187
|
+
version: input.version,
|
|
188
|
+
languages: [LANGUAGE],
|
|
189
|
+
schema_versions_accepted: [SCHEMA_VERSION],
|
|
190
|
+
conformance: {
|
|
191
|
+
corpus_version: input.conformance.corpusVersion,
|
|
192
|
+
result: input.conformance.result,
|
|
193
|
+
digest: input.conformance.digest,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
target: { kind: input.target.kind, root: input.target.root, identity: input.target.identity },
|
|
197
|
+
configurations: [...input.configurations]
|
|
198
|
+
.sort((a, b) => compare(a.id, b.id))
|
|
199
|
+
.map((one) => ({ id: one.id, project: one.project })),
|
|
200
|
+
configurations_not_built: [],
|
|
201
|
+
consumers: { declared: unavailable.length, loaded: [], unavailable },
|
|
202
|
+
findings,
|
|
203
|
+
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
204
|
+
stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
|
|
205
|
+
declared_gaps: [],
|
|
206
|
+
excluded_by_cgo: [],
|
|
207
|
+
test_file_rules: [...input.testFileRules]
|
|
208
|
+
.sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
|
|
209
|
+
.map((one) => ({ rule: one.rule, matched: one.matched })),
|
|
210
|
+
totals: totalsOf(findings, input.result),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The report with its findings in the order a configuration asks for. By size, a finding
|
|
215
|
+
* whose component a deletion shrinks more comes first, then one whose subject is larger,
|
|
216
|
+
* then the canonical key, so the order stays total; by position it is the canonical key.
|
|
217
|
+
*/
|
|
218
|
+
export function sortFindings(report, by) {
|
|
219
|
+
const bySize = (a, b) => b.component.deletable_lines - a.component.deletable_lines ||
|
|
220
|
+
b.symbol.size_lines - a.symbol.size_lines ||
|
|
221
|
+
canonical(a, b);
|
|
222
|
+
return { ...report, findings: [...report.findings].sort(by === "size" ? bySize : canonical) };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The report holding the first `most` findings of its order, the rest counted as omitted.
|
|
226
|
+
* Every other total keeps describing the whole set. Zero keeps every finding.
|
|
227
|
+
*/
|
|
228
|
+
export function capFindings(report, most) {
|
|
229
|
+
if (most <= 0 || report.findings.length <= most) {
|
|
230
|
+
return report;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
...report,
|
|
234
|
+
findings: report.findings.slice(0, most),
|
|
235
|
+
totals: { ...report.totals, omitted: report.findings.length - most },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The indentation of the report document, which with the schema's member order fixes its bytes. */
|
|
2
|
+
const INDENT = 2;
|
|
3
|
+
/** The report document itself: the report, indented, with a closing newline. */
|
|
4
|
+
export const json = (report) => `${JSON.stringify(report, null, INDENT)}\n`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { json } from "./json.js";
|
|
2
|
+
import { text } from "./text.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every format this analyzer renders, each by its reporter and its file suffix. A format
|
|
5
|
+
* absent from the table is refused rather than silently not written.
|
|
6
|
+
*/
|
|
7
|
+
export const RENDERINGS = new Map([
|
|
8
|
+
["text", { render: text, suffix: ".txt" }],
|
|
9
|
+
["json", { render: json, suffix: ".json" }],
|
|
10
|
+
]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The kind token and the confidence a stale suppression is rendered with, both fixed. */
|
|
2
|
+
const STALE_KIND = "suppression";
|
|
3
|
+
const STALE_CONFIDENCE = "certain";
|
|
4
|
+
/** A count with the word for it, so the summary reads as a sentence. */
|
|
5
|
+
function counted(n, one, many) {
|
|
6
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
7
|
+
}
|
|
8
|
+
/** The report's totals in one line, in the order the report declares them. */
|
|
9
|
+
function summary(totals) {
|
|
10
|
+
const { allow, warn, deny } = totals.by_severity;
|
|
11
|
+
return [
|
|
12
|
+
`summary: ${counted(totals.findings, "finding", "findings")} (${String(allow)} allow, ${String(warn)} warn, ${String(deny)} deny)`,
|
|
13
|
+
counted(totals.deletable_lines, "deletable line", "deletable lines"),
|
|
14
|
+
counted(totals.suppressions_in_effect, "suppression in effect", "suppressions in effect"),
|
|
15
|
+
counted(totals.reasons_recorded, "reason recorded", "reasons recorded"),
|
|
16
|
+
counted(totals.stale_suppressions, "stale suppression", "stale suppressions"),
|
|
17
|
+
`${String(totals.pending)} pending`,
|
|
18
|
+
`${String(totals.omitted)} omitted`,
|
|
19
|
+
].join(", ");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One line per finding in the report's order, then one per stale suppression, then the
|
|
23
|
+
* summary. A finding line is `path:line:col: kind name: message [confidence] (CODE)`; the
|
|
24
|
+
* summary is deliberately not in that shape, so a filter for finding lines yields exactly
|
|
25
|
+
* them. Nothing else is written: no timestamp, duration or host detail.
|
|
26
|
+
*/
|
|
27
|
+
export const text = (report) => {
|
|
28
|
+
const lines = report.findings.map((found) => `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})`);
|
|
29
|
+
for (const stale of report.stale_suppressions) {
|
|
30
|
+
lines.push(`${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})`);
|
|
31
|
+
}
|
|
32
|
+
lines.push(summary(report.totals));
|
|
33
|
+
return `${lines.join("\n")}\n`;
|
|
34
|
+
};
|
package/dist/src/run.js
CHANGED
|
@@ -8,6 +8,8 @@ import { printConfigVerb } from "./verbs/print-config.js";
|
|
|
8
8
|
import { printProjectsVerb } from "./verbs/print-projects.js";
|
|
9
9
|
import { printRetainedVerb } from "./verbs/print-retained.js";
|
|
10
10
|
import { printRootsVerb } from "./verbs/print-roots.js";
|
|
11
|
+
import { ANALYZE_OPTIONS, analyzeVerb } from "./verbs/analyze.js";
|
|
12
|
+
import { describeVerb } from "./verbs/describe.js";
|
|
11
13
|
import { EXIT_FAILURE, EXIT_USAGE } from "./verbs/verb.js";
|
|
12
14
|
import { versionVerb } from "./verbs/version.js";
|
|
13
15
|
/**
|
|
@@ -15,13 +17,13 @@ import { versionVerb } from "./verbs/version.js";
|
|
|
15
17
|
* with no `run` is listed, and invoking it is a usage error.
|
|
16
18
|
*/
|
|
17
19
|
const VERBS = [
|
|
18
|
-
{ name: "analyze" },
|
|
20
|
+
{ name: "analyze", run: analyzeVerb, options: ANALYZE_OPTIONS },
|
|
19
21
|
{ name: "explain" },
|
|
20
22
|
{ name: "print-config", run: printConfigVerb },
|
|
21
23
|
{ name: "print-projects", run: printProjectsVerb },
|
|
22
24
|
{ name: "print-roots", run: printRootsVerb },
|
|
23
25
|
{ name: "print-retained", run: printRetainedVerb },
|
|
24
|
-
{ name: "describe" },
|
|
26
|
+
{ name: "describe", run: describeVerb },
|
|
25
27
|
{ name: "version", run: versionVerb },
|
|
26
28
|
];
|
|
27
29
|
const USAGE = `usage: deadset-ts <verb> [options]\nverbs: ${VERBS.map((verb) => verb.name).join(", ")}\n`;
|
|
@@ -55,6 +57,8 @@ const PLAIN_OPTIONS = new Set(["target", "config", "central", "scope"]);
|
|
|
55
57
|
const INTEGER_OPTIONS = new Set(["max-findings"]);
|
|
56
58
|
/** An integer as an option writes it: an optional minus sign and digits alone. */
|
|
57
59
|
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
60
|
+
/** No option beyond the ones every verb takes. */
|
|
61
|
+
const NO_OPTIONS = { plain: [], repeatable: [] };
|
|
58
62
|
/**
|
|
59
63
|
* The first option whose name carries a source-edit token, because every verb
|
|
60
64
|
* reports and never edits. The match is on the name alone, so an option whose
|
|
@@ -73,24 +77,30 @@ function sourceEditOption(args) {
|
|
|
73
77
|
}
|
|
74
78
|
return undefined;
|
|
75
79
|
}
|
|
76
|
-
/** Reads one verb's options, refusing a name this command
|
|
77
|
-
function readOptions(args) {
|
|
80
|
+
/** Reads one verb's options, refusing a name neither this command nor the verb offers. */
|
|
81
|
+
function readOptions(args, own) {
|
|
78
82
|
const named = new Map();
|
|
83
|
+
const repeated = new Map();
|
|
79
84
|
for (const arg of args) {
|
|
80
85
|
if (!arg.startsWith("-")) {
|
|
81
86
|
throw new ConfigError("malformed", "", `unexpected argument ${JSON.stringify(arg)}`);
|
|
82
87
|
}
|
|
83
88
|
const [spelled, ...rest] = arg.split("=");
|
|
84
89
|
const name = (spelled ?? "").replace(/^-+/u, "");
|
|
85
|
-
if (!PLAIN_OPTIONS.has(name) &&
|
|
90
|
+
if (!PLAIN_OPTIONS.has(name) &&
|
|
91
|
+
!SETTING_OPTIONS.has(name) &&
|
|
92
|
+
!own.plain.includes(name) &&
|
|
93
|
+
!own.repeatable.includes(name)) {
|
|
86
94
|
throw new ConfigError("malformed", "", `unknown option ${JSON.stringify(spelled ?? arg)}`);
|
|
87
95
|
}
|
|
88
96
|
if (rest.length === 0) {
|
|
89
97
|
throw new ConfigError("malformed", "", `option ${JSON.stringify(spelled ?? arg)} takes a value`);
|
|
90
98
|
}
|
|
91
|
-
|
|
99
|
+
const value = rest.join("=");
|
|
100
|
+
named.set(name, value);
|
|
101
|
+
repeated.set(name, [...(repeated.get(name) ?? []), value]);
|
|
92
102
|
}
|
|
93
|
-
return { named };
|
|
103
|
+
return { named, repeated };
|
|
94
104
|
}
|
|
95
105
|
function readFileOr(host, path, orUndefined) {
|
|
96
106
|
try {
|
|
@@ -160,7 +170,7 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
160
170
|
const name = args[0];
|
|
161
171
|
const verb = VERBS.find((listed) => listed.name === name);
|
|
162
172
|
if (verb?.run !== undefined) {
|
|
163
|
-
return invoke(verb.run, args.slice(1), { out, err, host }, openClient);
|
|
173
|
+
return invoke(verb.run, verb.options ?? NO_OPTIONS, args.slice(1), { out, err, host }, openClient);
|
|
164
174
|
}
|
|
165
175
|
if (verb !== undefined) {
|
|
166
176
|
err.write(`deadset-ts: ${verb.name} is not implemented\n`);
|
|
@@ -177,15 +187,18 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
177
187
|
* time the verb asks for what they name, and a refusal the verb throws is written
|
|
178
188
|
* and turned into its exit code once the client the verb opened is closed.
|
|
179
189
|
*/
|
|
180
|
-
function invoke(verb, args, streams, openClient) {
|
|
190
|
+
function invoke(verb, own, args, streams, openClient) {
|
|
181
191
|
let options;
|
|
182
|
-
const read = () => (options ??= readOptions(args));
|
|
192
|
+
const read = () => (options ??= readOptions(args, own));
|
|
183
193
|
let opened;
|
|
184
194
|
try {
|
|
185
195
|
return verb({
|
|
186
196
|
...streams,
|
|
197
|
+
args,
|
|
187
198
|
inputs: () => inputsOf(streams.host, read()),
|
|
188
199
|
scope: () => scopeOf(streams.host, read()),
|
|
200
|
+
option: (name) => read().named.get(name),
|
|
201
|
+
repeated: (name) => read().repeated.get(name) ?? [],
|
|
189
202
|
openClient: (collectTiming) => {
|
|
190
203
|
opened = openClient(collectTiming);
|
|
191
204
|
return opened;
|