@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/analysis.js +61 -10
- package/dist/src/augmentations.js +63 -1
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +98 -1
- package/dist/src/discover.js +14 -82
- package/dist/src/entry-points.js +9 -18
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +66 -28
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/query.js +1 -0
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +197 -42
- package/dist/src/serialization-contract.js +590 -12
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -3
- package/dist/src/workflow-steps.js +32 -15
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +109 -12
- package/src/augmentations.ts +77 -2
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +110 -1
- package/src/discover.ts +14 -94
- package/src/entry-points.ts +10 -25
- package/src/exempt.ts +16 -0
- package/src/file-facts.ts +3 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +76 -30
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/query.ts +11 -0
- package/src/report.ts +56 -16
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +226 -46
- package/src/serialization-contract.ts +758 -12
- package/src/sweep.ts +23 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -3
- package/src/workflow-steps.ts +35 -17
- package/src/yaml-document.ts +377 -0
package/src/manifest.ts
CHANGED
|
@@ -35,8 +35,8 @@ export interface ManifestEntry {
|
|
|
35
35
|
readonly path: string;
|
|
36
36
|
readonly role: EntryRole;
|
|
37
37
|
/**
|
|
38
|
-
* Whether the member declares what a consumer may import, which is `exports`
|
|
39
|
-
* the three members that preceded
|
|
38
|
+
* Whether the member declares what a consumer may import, which is `exports`, `browser`
|
|
39
|
+
* and the three members that preceded `exports`. A command is published too, and is not this:
|
|
40
40
|
* `exports` is the surface a library's published API is read from.
|
|
41
41
|
*/
|
|
42
42
|
readonly published: boolean;
|
|
@@ -136,7 +136,7 @@ function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | u
|
|
|
136
136
|
export interface Manifest {
|
|
137
137
|
/**
|
|
138
138
|
* Every entry point the manifest names, in the order the members are read: `main`,
|
|
139
|
-
* `module`, `types`, then each `bin` command, then every target of `exports`, then
|
|
139
|
+
* `module`, `types`, then `browser` and each file it maps a module to, then each `bin` command, then every target of `exports`, then
|
|
140
140
|
* every token of each script, which names a file only where one is at its path. Each
|
|
141
141
|
* path is absolute.
|
|
142
142
|
*/
|
|
@@ -171,6 +171,24 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// A `browser` string replaces `main` in a browser bundle, and an object replaces each
|
|
175
|
+
// module it maps with the file it names: https://github.com/defunctzombie/package-browser-field-spec
|
|
176
|
+
const browser = manifest["browser"];
|
|
177
|
+
if (typeof browser === "string" && isTarget(browser, false)) {
|
|
178
|
+
found.push({ member: "browser", path: browser, role: "import", published: true });
|
|
179
|
+
} else if (isRecord(browser)) {
|
|
180
|
+
for (const [replaced, value] of Object.entries(browser)) {
|
|
181
|
+
if (typeof value === "string" && isTarget(value, true)) {
|
|
182
|
+
found.push({
|
|
183
|
+
member: below("browser", replaced),
|
|
184
|
+
path: value,
|
|
185
|
+
role: "import",
|
|
186
|
+
published: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
const bin = manifest["bin"];
|
|
175
193
|
if (typeof bin === "string" && isTarget(bin, false)) {
|
|
176
194
|
found.push({ member: "bin", path: bin, role: "run", published: false });
|
package/src/matrix.ts
CHANGED
|
@@ -223,18 +223,32 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
223
223
|
|
|
224
224
|
const union = matrix.union;
|
|
225
225
|
const heldBy: (string | undefined)[] = union.symbols.map(() => undefined);
|
|
226
|
-
const
|
|
226
|
+
const held = [
|
|
227
227
|
...input.marked,
|
|
228
228
|
...(input.exempt ?? []).map((record) => record.id),
|
|
229
229
|
...(input.unanswered ?? []),
|
|
230
|
-
]
|
|
231
|
-
const
|
|
230
|
+
];
|
|
231
|
+
const testReached = reachedWithTheTests(union, held);
|
|
232
|
+
// A test uses what it names: a file a test reads through a star re-export or a
|
|
233
|
+
// namespace keeps the exports nothing names at their own class.
|
|
234
|
+
const testNamed = reachedWithTheTests(union, held, false);
|
|
235
|
+
const everywhere = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
|
|
232
236
|
const held = matrix.heldIn[at] ?? [];
|
|
233
237
|
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
234
238
|
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
235
239
|
? undefined
|
|
236
240
|
: found.filter((candidate): candidate is Candidate => candidate !== undefined);
|
|
237
241
|
});
|
|
242
|
+
const compiled = compiledReferences(
|
|
243
|
+
matrix,
|
|
244
|
+
new Set(
|
|
245
|
+
union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id])),
|
|
246
|
+
),
|
|
247
|
+
input.mode,
|
|
248
|
+
);
|
|
249
|
+
const judged = everywhere.map((found, at) =>
|
|
250
|
+
compiled.has(union.symbols[at]?.id ?? "") ? undefined : found,
|
|
251
|
+
);
|
|
238
252
|
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
239
253
|
// so a test is of dead code only where every production subject it names is dead in
|
|
240
254
|
// every configuration that holds it.
|
|
@@ -252,7 +266,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
252
266
|
const kept = keptByTests(union, testReached, admittedAt);
|
|
253
267
|
const testUsed = testUsedReach(
|
|
254
268
|
union,
|
|
255
|
-
union.symbols.map((_symbol, at) => matrix.testUsed[at] === true &&
|
|
269
|
+
union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true),
|
|
256
270
|
judged.map((one) => one !== undefined),
|
|
257
271
|
);
|
|
258
272
|
const candidates: MatrixCandidate[] = [];
|
|
@@ -296,6 +310,39 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
296
310
|
};
|
|
297
311
|
}
|
|
298
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Every declaration of `dead`, those dead in every configuration that holds them, that a
|
|
315
|
+
* configuration's graph references from a judged declaration outside `dead` or already
|
|
316
|
+
* kept, until none joins: deleting one would break that configuration's compile. A
|
|
317
|
+
* production run counts no reference a test file made.
|
|
318
|
+
*/
|
|
319
|
+
function compiledReferences(
|
|
320
|
+
matrix: Matrix,
|
|
321
|
+
dead: ReadonlySet<string>,
|
|
322
|
+
mode: SweepInput["mode"],
|
|
323
|
+
): ReadonlySet<string> {
|
|
324
|
+
const kept = new Set<string>();
|
|
325
|
+
for (let changed = true; changed;) {
|
|
326
|
+
changed = false;
|
|
327
|
+
for (const graph of matrix.graphs) {
|
|
328
|
+
graph.out.forEach((edges, from) => {
|
|
329
|
+
const id = graph.symbols[from]?.id ?? "";
|
|
330
|
+
if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const edge of edges) {
|
|
334
|
+
const to = graph.symbols[edge.to]?.id ?? "";
|
|
335
|
+
if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
|
|
336
|
+
kept.add(to);
|
|
337
|
+
changed = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return kept;
|
|
344
|
+
}
|
|
345
|
+
|
|
299
346
|
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
300
347
|
function testUsedReach(
|
|
301
348
|
union: Graph,
|
package/src/query.ts
CHANGED
|
@@ -211,6 +211,8 @@ export interface Queries {
|
|
|
211
211
|
symbolOfType(type: Type): Answer<TSSymbol | undefined>;
|
|
212
212
|
signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
|
|
213
213
|
resolvedSignature(node: Node): Answer<Signature | undefined>;
|
|
214
|
+
/** The signature each call-like node resolves to. */
|
|
215
|
+
resolvedSignatures(nodes: readonly Node[], cap?: number): Answer<Signature | undefined>[];
|
|
214
216
|
parameterType(signature: Signature, index: number): Answer<Type | undefined>;
|
|
215
217
|
/** The generic signature an instantiated one instantiates; none for another signature. */
|
|
216
218
|
genericOf(signature: Signature): Answer<Signature | undefined>;
|
|
@@ -544,6 +546,15 @@ export function queriesOf(
|
|
|
544
546
|
() => nodeLocation(node),
|
|
545
547
|
() => checker.getResolvedSignature(node),
|
|
546
548
|
),
|
|
549
|
+
resolvedSignatures: (nodes, cap = DEFAULT_BATCH_CAP) =>
|
|
550
|
+
each(
|
|
551
|
+
"getResolvedSignature",
|
|
552
|
+
nodes,
|
|
553
|
+
cap,
|
|
554
|
+
nodeLocation,
|
|
555
|
+
(node) => checker.getResolvedSignature.gen(node),
|
|
556
|
+
(node) => checker.getResolvedSignature(node),
|
|
557
|
+
),
|
|
547
558
|
parameterType: (signature, index) =>
|
|
548
559
|
one(
|
|
549
560
|
"getParameterType",
|
package/src/report.ts
CHANGED
|
@@ -81,11 +81,6 @@ export interface WireFinding {
|
|
|
81
81
|
readonly root: boolean;
|
|
82
82
|
readonly symbol_count: number;
|
|
83
83
|
readonly deletable_lines: number;
|
|
84
|
-
readonly members?: readonly {
|
|
85
|
-
readonly ref: string;
|
|
86
|
-
readonly name: string;
|
|
87
|
-
readonly position: WirePosition;
|
|
88
|
-
}[];
|
|
89
84
|
};
|
|
90
85
|
readonly retained_by: readonly string[];
|
|
91
86
|
readonly configurations: readonly string[];
|
|
@@ -96,6 +91,16 @@ export interface WireFinding {
|
|
|
96
91
|
readonly details: WireDetails;
|
|
97
92
|
}
|
|
98
93
|
|
|
94
|
+
/** One dead component a finding names, with its members, as the report lists it once. */
|
|
95
|
+
export interface WireComponent {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
readonly members: readonly {
|
|
98
|
+
readonly ref: string;
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly position: WirePosition;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
interface WireEdgeEvaluation {
|
|
100
105
|
readonly edge: string;
|
|
101
106
|
readonly side: string;
|
|
@@ -218,6 +223,8 @@ export interface Report {
|
|
|
218
223
|
}[];
|
|
219
224
|
};
|
|
220
225
|
readonly findings: readonly WireFinding[];
|
|
226
|
+
/** Present where the run lists each component in full. */
|
|
227
|
+
readonly components?: readonly WireComponent[];
|
|
221
228
|
readonly edge_evaluations: readonly WireEdgeEvaluation[];
|
|
222
229
|
readonly stale_suppressions: readonly WireStaleSuppression[];
|
|
223
230
|
readonly declared_gaps: readonly WireDeclaredGap[];
|
|
@@ -231,13 +238,20 @@ export interface Report {
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
/** One hint about the run's setup, as the report schema's `notes` spells it. */
|
|
234
|
-
export
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
export type ReportNote =
|
|
242
|
+
| {
|
|
243
|
+
readonly kind: "unmapped-entry";
|
|
244
|
+
/** The directory of the manifest the note is about, below the target root. */
|
|
245
|
+
readonly path: string;
|
|
246
|
+
readonly key: "ts.entry_files";
|
|
247
|
+
readonly message: string;
|
|
248
|
+
}
|
|
249
|
+
| {
|
|
250
|
+
readonly kind: "computed-load";
|
|
251
|
+
/** The directory of the file that writes the load, below the target root. */
|
|
252
|
+
readonly path: string;
|
|
253
|
+
readonly message: string;
|
|
254
|
+
};
|
|
241
255
|
|
|
242
256
|
/** What one run answered, from which its report is built. */
|
|
243
257
|
export interface ReportInput {
|
|
@@ -258,6 +272,26 @@ export interface ReportInput {
|
|
|
258
272
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
259
273
|
/** The notes the run states, in any order. */
|
|
260
274
|
readonly notes?: readonly ReportNote[];
|
|
275
|
+
/** Whether the run lists each component in full, which `reporters.cascade: full` asks for. */
|
|
276
|
+
readonly listsComponents?: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Each component the findings name, once, with the members the first finding naming it
|
|
281
|
+
* carries, in the order the findings are: `findings` paired with their wire forms and
|
|
282
|
+
* sorted, then the edge evaluations' findings.
|
|
283
|
+
*/
|
|
284
|
+
function componentsOf(
|
|
285
|
+
findings: readonly (readonly [WireFinding, CompletedFinding])[],
|
|
286
|
+
): readonly WireComponent[] {
|
|
287
|
+
const listed = new Map<string, WireComponent>();
|
|
288
|
+
for (const [wire, finding] of findings) {
|
|
289
|
+
const { members } = finding.component;
|
|
290
|
+
if (members !== undefined && !listed.has(wire.component.id)) {
|
|
291
|
+
listed.set(wire.component.id, { id: wire.component.id, members: members.map(wireSymbol) });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return [...listed.values()];
|
|
261
295
|
}
|
|
262
296
|
|
|
263
297
|
/** Two strings ordered bytewise. */
|
|
@@ -342,9 +376,6 @@ export function wireFinding(finding: CompletedFinding): WireFinding {
|
|
|
342
376
|
root: finding.component.root,
|
|
343
377
|
symbol_count: finding.component.symbolCount,
|
|
344
378
|
deletable_lines: finding.component.deletableLines,
|
|
345
|
-
...(finding.component.members === undefined
|
|
346
|
-
? {}
|
|
347
|
-
: { members: finding.component.members.map(wireSymbol) }),
|
|
348
379
|
},
|
|
349
380
|
retained_by: finding.retainedBy,
|
|
350
381
|
configurations: finding.configurations,
|
|
@@ -470,7 +501,13 @@ function totalsOf(findings: readonly WireFinding[], result: PassResult): WireTot
|
|
|
470
501
|
|
|
471
502
|
/** Builds the report of one run, every array in the order the Contract fixes for it. */
|
|
472
503
|
export function buildReport(input: ReportInput): Report {
|
|
473
|
-
const
|
|
504
|
+
const paired = input.result.findings
|
|
505
|
+
.map((finding) => [wireFinding(finding), finding] as const)
|
|
506
|
+
.sort(([a], [b]) => canonical(a, b));
|
|
507
|
+
const findings = paired.map(([wire]) => wire);
|
|
508
|
+
const evaluated = input.result.edgeEvaluations.flatMap((one) =>
|
|
509
|
+
one.finding === undefined ? [] : [[wireFinding(one.finding), one.finding] as const],
|
|
510
|
+
);
|
|
474
511
|
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
475
512
|
.sort((a, b) => compare(a.id, b.id))
|
|
476
513
|
.map((one) => ({ id: one.id, role: "consumer" as const, reason: one.reason }));
|
|
@@ -500,6 +537,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
500
537
|
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
501
538
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
502
539
|
findings,
|
|
540
|
+
...(input.listsComponents === true
|
|
541
|
+
? { components: componentsOf([...paired, ...evaluated]) }
|
|
542
|
+
: {}),
|
|
503
543
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
504
544
|
stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
|
|
505
545
|
// Every gap shares the one component of the canonical key a gap supplies, the analyzer's
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Severity } from "../config.ts";
|
|
2
2
|
import { coalesced } from "../json-chunks.ts";
|
|
3
3
|
import { KINDS } from "../kinds.ts";
|
|
4
4
|
import type { Report } from "../report.ts";
|
|
5
|
-
import type { RenderOptions } from "./reporter.ts";
|
|
6
5
|
|
|
7
6
|
/** The escapes a workflow command's message needs, so a line break does not end the command. */
|
|
8
7
|
const DATA_ESCAPES: Readonly<Record<string, string>> = { "%": "%25", "\r": "%0D", "\n": "%0A" };
|
|
@@ -27,8 +26,18 @@ function kindName(code: string): string {
|
|
|
27
26
|
return KINDS.get(code)?.name ?? code;
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
/** The workflow command a record is written as. */
|
|
30
|
+
type Level = "error" | "warning" | "notice";
|
|
31
|
+
|
|
32
|
+
/** The command each finding severity is annotated with, whatever the failing severity. */
|
|
33
|
+
const LEVELS: Readonly<Record<Severity, Level>> = {
|
|
34
|
+
deny: "error",
|
|
35
|
+
warn: "warning",
|
|
36
|
+
allow: "notice",
|
|
37
|
+
};
|
|
38
|
+
|
|
30
39
|
function command(
|
|
31
|
-
level:
|
|
40
|
+
level: Level,
|
|
32
41
|
at: { readonly path: string; readonly line: number; readonly column: number },
|
|
33
42
|
endLine: number,
|
|
34
43
|
title: string,
|
|
@@ -43,18 +52,17 @@ const SKIP_MESSAGE =
|
|
|
43
52
|
"the analysis did not evaluate the function or statement holding this type error: ";
|
|
44
53
|
|
|
45
54
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* is fixed on at the failing severity.
|
|
55
|
+
* The annotations `contract/grammar/annotations.md` states: one per finding, at the
|
|
56
|
+
* command its severity names, and one error per stale suppression, each naming its file,
|
|
57
|
+
* line, column and last line and titled by its code and kind; then one warning per
|
|
58
|
+
* type-error skip naming its file and line, and one per note naming its directory.
|
|
51
59
|
*/
|
|
52
|
-
export function annotations(report: Report
|
|
60
|
+
export function annotations(report: Report): Iterable<string> {
|
|
53
61
|
return coalesced(
|
|
54
62
|
(function* lines(): Generator<string, void, undefined> {
|
|
55
63
|
for (const found of report.findings) {
|
|
56
64
|
yield `${command(
|
|
57
|
-
|
|
65
|
+
LEVELS[found.severity],
|
|
58
66
|
found.position,
|
|
59
67
|
found.position.end_line,
|
|
60
68
|
`${found.code} ${found.kind}`,
|
|
@@ -73,6 +81,9 @@ export function annotations(report: Report, options: RenderOptions): Iterable<st
|
|
|
73
81
|
for (const skip of report.type_error_skips) {
|
|
74
82
|
yield `::warning file=${escapeProperty(skip.path)},line=${String(skip.line)},title=${escapeProperty(SKIP_TITLE)}::${escapeData(SKIP_MESSAGE + skip.message)}\n`;
|
|
75
83
|
}
|
|
84
|
+
for (const note of report.notes) {
|
|
85
|
+
yield `::warning file=${escapeProperty(note.path)},title=${escapeProperty(note.kind)}::${escapeData(note.message)}\n`;
|
|
86
|
+
}
|
|
76
87
|
})(),
|
|
77
88
|
);
|
|
78
89
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type { Format
|
|
1
|
+
import type { Format } from "../config.ts";
|
|
2
2
|
import type { Report } from "../report.ts";
|
|
3
3
|
import type { Template } from "./template.ts";
|
|
4
4
|
|
|
5
5
|
/** What a rendering reads beside the report. */
|
|
6
6
|
export interface RenderOptions {
|
|
7
|
-
/** The lowest severity at which a finding fails the run. */
|
|
8
|
-
readonly failOn: Severity;
|
|
9
7
|
/** The text of the file at one path below the target root, or a throw naming why not. */
|
|
10
8
|
readonly readSource: (path: string) => string;
|
|
11
9
|
/** The template the template rendering renders, where one was named. */
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Confidence, Severity } from "../config.ts";
|
|
|
2
2
|
import { jsonChunks } from "../json-chunks.ts";
|
|
3
3
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
4
4
|
import { declarationKey } from "../ref.ts";
|
|
5
|
-
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
5
|
+
import type { Report, WireComponent, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
6
6
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
7
7
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
8
8
|
import { utf8Bytes } from "./sha256.ts";
|
|
@@ -151,8 +151,11 @@ interface Related {
|
|
|
151
151
|
readonly endLine: number;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** The members the report lists for one component, none where it lists none. */
|
|
155
|
+
type MembersOf = (id: string) => WireComponent["members"];
|
|
156
|
+
|
|
154
157
|
/** Every position a finding names beyond its own, in the mapping's order, the first hundred. */
|
|
155
|
-
function relatedOf(found: WireFinding): readonly Related[] {
|
|
158
|
+
function relatedOf(found: WireFinding, members: MembersOf): readonly Related[] {
|
|
156
159
|
const related: Related[] = [];
|
|
157
160
|
for (const one of found.details.implementations ?? []) {
|
|
158
161
|
related.push({ label: "implementation", at: one.position, endLine: one.position.end_line });
|
|
@@ -164,7 +167,7 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
164
167
|
const at = found.details.name_literal;
|
|
165
168
|
related.push({ label: "name", at, endLine: at.end_line });
|
|
166
169
|
}
|
|
167
|
-
for (const one of found.component.
|
|
170
|
+
for (const one of members(found.component.id)) {
|
|
168
171
|
if (
|
|
169
172
|
declarationKey(one.ref, one.position.path) !==
|
|
170
173
|
declarationKey(found.symbol.ref, found.position.path)
|
|
@@ -188,7 +191,9 @@ function messageWithLinks(message: string, related: readonly Related[]): string
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/** Every member of the finding the mapping places nowhere else, under its schema name. */
|
|
191
|
-
|
|
194
|
+
/** A result's member bag, its component holding the members the report lists for it once. */
|
|
195
|
+
function propertiesOf(found: WireFinding, members: MembersOf): Record<string, unknown> {
|
|
196
|
+
const listed = members(found.component.id);
|
|
192
197
|
return {
|
|
193
198
|
language: found.language,
|
|
194
199
|
symbol: found.symbol,
|
|
@@ -199,7 +204,7 @@ function propertiesOf(found: WireFinding): Record<string, unknown> {
|
|
|
199
204
|
: { liveness_relation: found.liveness_relation }),
|
|
200
205
|
test_only: found.test_only,
|
|
201
206
|
generated: found.generated,
|
|
202
|
-
component: found.component,
|
|
207
|
+
component: listed.length === 0 ? found.component : { ...found.component, members: listed },
|
|
203
208
|
retained_by: found.retained_by,
|
|
204
209
|
configurations: found.configurations,
|
|
205
210
|
consumers_loaded: found.consumers_loaded,
|
|
@@ -220,8 +225,9 @@ function findingResult(
|
|
|
220
225
|
found: WireFinding,
|
|
221
226
|
index: ReadonlyMap<string, number>,
|
|
222
227
|
hashOf: (at: Located) => string,
|
|
228
|
+
members: MembersOf,
|
|
223
229
|
): Record<string, unknown> {
|
|
224
|
-
const related = relatedOf(found);
|
|
230
|
+
const related = relatedOf(found, members);
|
|
225
231
|
return {
|
|
226
232
|
ruleId: found.code,
|
|
227
233
|
ruleIndex: ruleIndex(index, found.code),
|
|
@@ -241,7 +247,7 @@ function findingResult(
|
|
|
241
247
|
primaryLocationLineHash: hashOf(found.position),
|
|
242
248
|
"deadsetSymbolRef/v1": symbolFingerprint(found.code, found.symbol.ref),
|
|
243
249
|
},
|
|
244
|
-
properties: propertiesOf(found),
|
|
250
|
+
properties: propertiesOf(found, members),
|
|
245
251
|
};
|
|
246
252
|
}
|
|
247
253
|
|
|
@@ -275,8 +281,10 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
|
|
|
275
281
|
const ruleList = rules(kinds);
|
|
276
282
|
const index = new Map(kinds.map((kind, at) => [kind.code, at]));
|
|
277
283
|
const hashOf = lineHashCache(options.readSource);
|
|
284
|
+
const listed = new Map((report.components ?? []).map((one) => [one.id, one.members]));
|
|
285
|
+
const members: MembersOf = (id) => listed.get(id) ?? [];
|
|
278
286
|
const results = [
|
|
279
|
-
...report.findings.map((found) => findingResult(found, index, hashOf)),
|
|
287
|
+
...report.findings.map((found) => findingResult(found, index, hashOf, members)),
|
|
280
288
|
...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
|
|
281
289
|
];
|
|
282
290
|
const languages = [...report.analyzer.languages].sort().join("+");
|