@cplieger/deadset-ts 3.7.0 → 3.8.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.
@@ -11,9 +11,12 @@ import { decorator } from "./decorator.js";
11
11
  import { dependenciesOf, projectNeeds } from "./dependencies.js";
12
12
  import { deprecatedDeclarations } from "./deprecation.js";
13
13
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
14
+ import { readEdgeSides } from "./edges.js";
14
15
  import { enumGroup } from "./enum-group.js";
15
16
  import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
16
17
  import { heldByInclusion, sourceTree } from "./file-facts.js";
18
+ import { findingsPass } from "./findings-pass.js";
19
+ import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
17
20
  import { frameworkLifecycle } from "./framework-lifecycle.js";
18
21
  import { implementations, mergeImplementations } from "./implementations.js";
19
22
  import { injectionContainer } from "./injection-container.js";
@@ -25,10 +28,12 @@ import { relativePath, resolvePath } from "./paths.js";
25
28
  import { byPosition } from "./position.js";
26
29
  import { reflectiveLookup } from "./reflective-lookup.js";
27
30
  import { references } from "./references.js";
28
- import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
31
+ import { roots, unmatchedEverywhere } from "./roots.js";
29
32
  import { serializationContract } from "./serialization-contract.js";
30
33
  import { diagnosticsOf, runSession } from "./session.js";
31
34
  import { accessorsOf, storesOf } from "./stores.js";
35
+ import { readBaseline, readIgnoreFile } from "./suppress-file.js";
36
+ import { inlineDirectives, inlineSuppressions } from "./suppress-inline.js";
32
37
  import { readTemplates, templateField } from "./template-field.js";
33
38
  /** The setting whose source decides where a finding about a configured root sits. */
34
39
  const ROOTS_SETTING = "roots.patterns";
@@ -128,7 +133,7 @@ export function runRoots(engine, host, scope, config, provenance) {
128
133
  return {
129
134
  configurations,
130
135
  roots: ordered,
131
- findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
136
+ findings: unmatchedRootFindings(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
132
137
  };
133
138
  }
134
139
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
@@ -143,10 +148,11 @@ const DETECTORS = new Map([
143
148
  ["serialization-contract", serializationContract],
144
149
  ]);
145
150
  /**
146
- * Sweeps the run, reading each project's references and exemptions, and `extra`'s
147
- * answer, while the project's view is open.
151
+ * Reads every project of the run, its references and exemptions, and `extra`'s answer,
152
+ * while the project's view is open, and merges the projects into one matrix in the run's
153
+ * order.
148
154
  */
149
- function sweepRun(engine, host, scope, config, input, detectors, extra) {
155
+ function readRun(engine, host, scope, config, mode, detectors, extra) {
150
156
  const targetRoot = scope.target.path;
151
157
  const disabled = disabledClasses(config);
152
158
  const templates = disabled.has("template-field")
@@ -166,20 +172,26 @@ function sweepRun(engine, host, scope, config, input, detectors, extra) {
166
172
  };
167
173
  const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers }, detectors, {
168
174
  disabled,
169
- mode: input.mode,
175
+ mode,
170
176
  testFiles: new Set(resolved.testFilePaths),
171
177
  });
172
178
  return { configured, exempt, extra: extra(project, projectRead) };
173
179
  });
174
- const matrix = matrixOf(projects.map((one) => one.configured));
175
- const exempt = exemptionsOf(projects.flatMap((one) => one.exempt));
176
- const swept = { ...input, exempt };
177
180
  return {
178
- swept: { matrix, sweep: sweepMatrix(matrix, swept), retained: retainedIn(matrix, swept) },
179
- exempt,
181
+ matrix: matrixOf(projects.map((one) => one.configured)),
182
+ exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
180
183
  projects,
181
184
  };
182
185
  }
186
+ /** The sweep of a run read once, under one set of marks. */
187
+ function sweptOf(read, input) {
188
+ const swept = { ...input, exempt: read.exempt };
189
+ return {
190
+ matrix: read.matrix,
191
+ sweep: sweepMatrix(read.matrix, swept),
192
+ retained: retainedIn(read.matrix, swept),
193
+ };
194
+ }
183
195
  /**
184
196
  * Sweeps the run the scope and the configuration describe. Each project's references
185
197
  * and exemptions are read against its own inventory while its view is open; the
@@ -191,24 +203,29 @@ function sweepRun(engine, host, scope, config, input, detectors, extra) {
191
203
  * project is swept.
192
204
  */
193
205
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
194
- return sweepRun(engine, host, scope, config, input, detectors, () => undefined).swept;
206
+ return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
195
207
  }
196
- /**
197
- * What every emitter reads of the run the scope and the configuration describe: the
198
- * run's sweep, and the facts beside it each kind family reads, every per-project one
199
- * read in the same session as the sweep. The run loads no consumer and reads no
200
- * declared cross-language edge, so its boundary names neither.
201
- */
202
- export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
208
+ /** Reads a run's projects with every per-project fact an emitter reads. */
209
+ function readEmitterRun(engine, host, scope, config, mode, detectors) {
203
210
  const targetRoot = scope.target.path;
204
- const run = sweepRun(engine, host, scope, config, input, detectors, (project, read) => ({
211
+ return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
205
212
  deprecated: deprecatedDeclarations(project, read.held),
206
213
  accessors: accessorsOf(project, read.held),
207
214
  needs: projectNeeds(project, read.held, host),
208
215
  implementations: implementations(project, read.held),
209
216
  heldByInclusion: heldByInclusion(project, targetRoot),
217
+ rooted: read.rooted,
218
+ directives: inlineDirectives(project, read.held, targetRoot),
210
219
  }));
211
- const { swept, projects } = run;
220
+ }
221
+ /**
222
+ * The input every emitter reads over one sweep of a read run: the facts beside the sweep
223
+ * each kind family reads, every per-project one read in the same session as the sweep.
224
+ */
225
+ function emitterInputOver(host, scope, config, read, input, selfCheck) {
226
+ const targetRoot = scope.target.path;
227
+ const swept = sweptOf(read, input);
228
+ const { projects } = read;
212
229
  return {
213
230
  config,
214
231
  swept,
@@ -216,7 +233,7 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
216
233
  stores: storesOf(projects.map((one) => ({
217
234
  references: one.configured.references,
218
235
  accessors: one.extra.accessors,
219
- })), run.exempt, input.mode),
236
+ })), read.exempt, input.mode),
220
237
  dependencies: dependenciesOf(host, targetRoot, projects.map((one) => one.extra.needs), swept.sweep.candidates.map((candidate) => candidate.id)),
221
238
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
222
239
  files: {
@@ -226,7 +243,59 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
226
243
  boundary: {
227
244
  consumers: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
228
245
  encapsulated: readManifest(host, targetRoot).declaresExports,
229
- edges: [],
246
+ edges: readEdgeSides(host, targetRoot),
230
247
  },
248
+ selfCheck,
231
249
  };
232
250
  }
251
+ /**
252
+ * What every emitter reads of the run the scope and the configuration describe, swept
253
+ * under the caller's marks: the run's sweep, the facts beside it each kind family reads,
254
+ * and the sides of the target's declared cross-language edges that name a symbol of this
255
+ * language. The run loads no consumer, and it reads no suppression document, so the
256
+ * self-check family has nothing to report.
257
+ */
258
+ export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
259
+ const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
260
+ return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
261
+ }
262
+ /**
263
+ * Reads the run the scope and the configuration describe for its findings. The inline
264
+ * directives, the ignore file's entries and the baseline's rows are read and bound before
265
+ * any sweep, and the run is swept once under every bound record's mark and once under
266
+ * none, so a record is decided against the finding its code would have produced. The
267
+ * self-check family reports the refused suppressions and the roots that named nothing.
268
+ */
269
+ function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
270
+ const targetRoot = scope.target.path;
271
+ const read = readEmitterRun(engine, host, scope, config, mode, detectors);
272
+ const symbols = read.matrix.union.symbols;
273
+ const documents = [
274
+ inlineSuppressions(read.projects.map((one) => one.extra.directives)),
275
+ readIgnoreFile(host, targetRoot, symbols),
276
+ readBaseline(host, targetRoot, symbols),
277
+ ];
278
+ const suppressions = {
279
+ records: documents.flatMap((one) => one.records),
280
+ refusals: documents.flatMap((one) => one.refusals),
281
+ };
282
+ const facts = {
283
+ refusals: suppressions.refusals,
284
+ unmatchedRoots: unmatchedEverywhere(config.rootPatterns, read.projects.map((one) => one.extra.rooted)),
285
+ rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
286
+ };
287
+ const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter((id) => id !== "");
288
+ return {
289
+ marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
290
+ unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
291
+ suppressions,
292
+ };
293
+ }
294
+ /**
295
+ * The findings of the run the scope and the configuration describe: the run read with its
296
+ * suppressions bound, then every family's findings with the suppressions applied and the
297
+ * declared edges evaluated.
298
+ */
299
+ export function runFindings(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
300
+ return findingsPass(runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors));
301
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Declared cross-language edges: the sides of the target's edges document that name a
3
+ * symbol of this language, and the evaluation the report publishes for each. This analyzer
4
+ * never resolves the paired symbol: the other language's analyzer decides whether the
5
+ * pairing is a use, and the merge reads the two evaluations together.
6
+ */
7
+ import { ConfigError } from "./config.js";
8
+ import { positionAt, walkDocument } from "./json-document.js";
9
+ import { joinPath } from "./paths.js";
10
+ import { positionKey } from "./position.js";
11
+ /** The name and the location the Contract fixes for the edges document. */
12
+ export const EDGES_FILE = "deadset-edges.json";
13
+ /** The language prefix of every reference this analyzer evaluates. */
14
+ const OWN_PREFIX = "ts://";
15
+ /** The closed key list of the document and of one edge. */
16
+ const DOCUMENT_KEYS = new Set(["description", "edges"]);
17
+ const EDGE_KEYS = new Set(["id", "because", "provides", "used_by"]);
18
+ /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
19
+ const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
20
+ /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
21
+ const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
22
+ const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
23
+ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
24
+ const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
25
+ const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
26
+ /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
27
+ export class EdgesError extends ConfigError {
28
+ constructor(at, text, want) {
29
+ super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
30
+ this.name = "EdgesError";
31
+ }
32
+ }
33
+ /**
34
+ * Whether a reference carries a wildcard outside a quoted or a computed component, which
35
+ * would name a set of symbols rather than one.
36
+ */
37
+ function carriesWildcard(ref) {
38
+ let quoted = false;
39
+ let computed = false;
40
+ let escaped = false;
41
+ for (const char of ref) {
42
+ if (escaped) {
43
+ escaped = false;
44
+ }
45
+ else if (quoted && char === "\\") {
46
+ escaped = true;
47
+ }
48
+ else if (quoted) {
49
+ quoted = char !== "'";
50
+ }
51
+ else if (computed) {
52
+ computed = char !== "]";
53
+ }
54
+ else if (char === "'") {
55
+ quoted = true;
56
+ }
57
+ else if (char === "[") {
58
+ computed = true;
59
+ }
60
+ else if (char === "*") {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ function isObject(value) {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }
69
+ /** One edge, checked: the closed key list, the value types and the value forms. */
70
+ function readEdge(value, at) {
71
+ const refuse = (text, want) => {
72
+ throw new EdgesError(at, text, want);
73
+ };
74
+ if (!isObject(value)) {
75
+ return refuse(JSON.stringify(value), EDGE_WANT);
76
+ }
77
+ for (const [key, held] of Object.entries(value)) {
78
+ if (!EDGE_KEYS.has(key) || typeof held !== "string") {
79
+ refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
80
+ }
81
+ }
82
+ const edge = value;
83
+ const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
84
+ const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
85
+ const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
86
+ if (!ID_FORM.test(id)) {
87
+ refuse(JSON.stringify(id), ID_WANT);
88
+ }
89
+ for (const ref of [provides, usedBy]) {
90
+ if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
91
+ refuse(JSON.stringify(ref), REFERENCE_WANT);
92
+ }
93
+ }
94
+ return { id, provides, usedBy };
95
+ }
96
+ /**
97
+ * Every side of a declared edge that names a symbol of this language, in document order,
98
+ * `provides` before `used_by` within an edge. An absent document declares no edge. The
99
+ * document is strict JSON with a closed key list at both levels, and a defect in it is an
100
+ * {@link EdgesError}.
101
+ */
102
+ export function readEdgeSides(host, targetRoot) {
103
+ const file = joinPath(targetRoot, EDGES_FILE);
104
+ if (host.kindOf(file) === "absent") {
105
+ return [];
106
+ }
107
+ const text = host.readFile(file);
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(text);
111
+ }
112
+ catch (error) {
113
+ throw new EdgesError(EDGES_FILE, error instanceof Error ? error.message : String(error), DOCUMENT_WANT);
114
+ }
115
+ if (!isObject(parsed)) {
116
+ throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
117
+ }
118
+ const walk = walkDocument(text, "edges");
119
+ if (walk.twice !== undefined) {
120
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)), `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`, DOCUMENT_WANT);
121
+ }
122
+ const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
123
+ const description = parsed["description"];
124
+ const edges = parsed["edges"];
125
+ if (unknown !== undefined ||
126
+ (description !== undefined && typeof description !== "string") ||
127
+ !Array.isArray(edges)) {
128
+ throw new EdgesError(EDGES_FILE, unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`, DOCUMENT_WANT);
129
+ }
130
+ const sides = [];
131
+ edges.forEach((value, index) => {
132
+ const offset = walk.opened[index];
133
+ const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
134
+ const edge = readEdge(value, at);
135
+ const named = [
136
+ ["provides", edge.provides],
137
+ ["used_by", edge.usedBy],
138
+ ];
139
+ for (const [side, symbol] of named) {
140
+ if (symbol.startsWith(OWN_PREFIX)) {
141
+ sides.push({ edge: edge.id, side, symbol });
142
+ }
143
+ }
144
+ });
145
+ return sides;
146
+ }
147
+ /** Two strings ordered bytewise. */
148
+ function compare(a, b) {
149
+ if (a === b) {
150
+ return 0;
151
+ }
152
+ return a < b ? -1 : 1;
153
+ }
154
+ /**
155
+ * Evaluates every declared side of this language over the run's findings. A side naming a
156
+ * symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
157
+ * finding about is `dead` and carries that finding, pending until a merge reads the other
158
+ * side and reported nowhere else, a narrowing finding included; any other is `live`.
159
+ */
160
+ export function evaluateEdges(findings, sides, symbols) {
161
+ if (sides.length === 0) {
162
+ return { findings, evaluations: [] };
163
+ }
164
+ const byRef = new Map();
165
+ for (const symbol of symbols) {
166
+ if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
167
+ byRef.set(symbol.ref, symbol.id);
168
+ }
169
+ }
170
+ const held = new Map();
171
+ findings.forEach((finding, at) => {
172
+ held.set(positionKey(finding.position), at);
173
+ });
174
+ const pending = new Set();
175
+ const evaluations = sides.map((side) => {
176
+ const id = byRef.get(side.symbol);
177
+ if (id === undefined) {
178
+ return { ...side, state: "absent" };
179
+ }
180
+ const at = held.get(id);
181
+ const finding = at === undefined ? undefined : findings[at];
182
+ if (at === undefined || finding === undefined) {
183
+ return { ...side, state: "live" };
184
+ }
185
+ pending.add(at);
186
+ return { ...side, state: "dead", finding };
187
+ });
188
+ evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
189
+ return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
190
+ }
@@ -119,17 +119,38 @@ export function completed(input, findings) {
119
119
  };
120
120
  });
121
121
  }
122
+ /** Whether the dials withhold a finding by its own severity or confidence. */
123
+ function dialed(finding, minConfidence) {
124
+ return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
125
+ }
122
126
  /**
123
- * The findings no dial withholds. A finding at `allow` or below the minimum confidence
124
- * is withheld, and so is every finding of the component of a root the dials withhold,
125
- * because a symbol that falls with a root is dead only through it and its deletion
126
- * does not compile while the root stays. A withheld finding that is no root withholds
127
- * itself alone.
127
+ * The configuration's dials over one run's completed findings. A finding at `allow` or
128
+ * below the minimum confidence is withheld, and so is every finding of the component of a
129
+ * root the dials withhold, because a symbol that falls with a root is dead only through it
130
+ * and its deletion does not compile while the root stays. A withheld finding that is no
131
+ * root withholds itself alone. A code is withheld from a declaration when the finding it
132
+ * would report there, completed, is withheld by its own severity or confidence.
128
133
  */
129
- export function reportable(findings, minConfidence) {
130
- const dialed = (finding) => finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
134
+ export function dialsOf(input, findings) {
135
+ const minConfidence = input.config.analysis.minConfidence;
136
+ const classOf = reachabilityClasses(input.swept);
131
137
  const components = new Set(findings
132
- .filter((finding) => finding.component.root && dialed(finding))
138
+ .filter((finding) => finding.component.root && dialed(finding, minConfidence))
133
139
  .map((finding) => finding.component.id));
134
- return findings.filter((finding) => !dialed(finding) && !components.has(finding.component.id));
140
+ return {
141
+ withholdsCode: (code, id) => {
142
+ const row = KINDS.get(code);
143
+ return (row !== undefined &&
144
+ dialed({
145
+ severity: severityOf(input.config, code, row.defaultSeverity),
146
+ confidence: capped(classOf(id), row.maxClass),
147
+ }, minConfidence));
148
+ },
149
+ withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
150
+ };
151
+ }
152
+ /** The findings of one run's completed findings that no dial withholds, in the order given. */
153
+ export function reportable(input, findings) {
154
+ const dials = dialsOf(input, findings);
155
+ return findings.filter((finding) => !dials.withholds(finding));
135
156
  }
@@ -1,5 +1,7 @@
1
+ import { evaluateEdges } from "../edges.js";
2
+ import { ledgerOf } from "../suppress.js";
3
+ import { completed, dialsOf, reportable } from "./completion.js";
1
4
  import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
2
- import { completed, reportable } from "./completion.js";
3
5
  import { interfaces } from "./interfaces.js";
4
6
  import { intraFunction } from "./intra-function.js";
5
7
  import { nonCodeArtifacts } from "./non-code-artifacts.js";
@@ -18,12 +20,46 @@ export const EMITTERS = new Map([
18
20
  ["self-check", selfCheck],
19
21
  ["intra-function", intraFunction],
20
22
  ]);
23
+ /** Every family's findings over one sweep, completed by one step. */
24
+ function completedOver(input) {
25
+ return completed(input, [...EMITTERS.values()].flatMap((emit) => emit(input)));
26
+ }
21
27
  /**
22
- * Every finding of the run the dials do not withhold, each family's in table order: every
23
- * family's findings completed by one step, then withheld by one step, so a root one
24
- * family reports withholds what another family reports in its component.
28
+ * The dials over the findings a record could name, each read against the sweep that
29
+ * produced it, so a component one sweep numbers withholds nothing of the other's.
25
30
  */
31
+ function dialsOver(input, found, suppressed, unmarked) {
32
+ const marked = dialsOf(input, found);
33
+ const before = dialsOf(suppressed.unmarked, unmarked);
34
+ const fromUnmarked = new Set(unmarked);
35
+ return {
36
+ withholdsCode: marked.withholdsCode,
37
+ withholds: (finding) => fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
38
+ };
39
+ }
40
+ /**
41
+ * Every finding of one run swept under its suppressions' marks, decided in one pass: every
42
+ * family's findings completed by one step, so every component number is minted once, a
43
+ * pending finding's among them; the finding each record claims withheld, whatever its
44
+ * kind; the rest withheld by the dials, so a root one family reports withholds what
45
+ * another family reports in its component; and every declared edge evaluated over what
46
+ * remains, so a finding it holds pending is in its evaluation and nowhere else.
47
+ */
48
+ export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
49
+ const found = completedOver(input);
50
+ const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
51
+ const would = [...unmarked, ...found];
52
+ const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
53
+ const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding)));
54
+ const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
55
+ return {
56
+ findings: evaluated.findings,
57
+ evaluations: evaluated.evaluations,
58
+ ledger,
59
+ would,
60
+ };
61
+ }
62
+ /** Every finding the run reports, each family's in table order. */
26
63
  export function findingsOf(input) {
27
- const found = [...EMITTERS.values()].flatMap((emit) => emit(input));
28
- return reportable(completed(input, found), input.config.analysis.minConfidence);
64
+ return decidedFindings(input).findings;
29
65
  }
@@ -1,2 +1,127 @@
1
- /** The findings of the self-check family, `DS1700` to `DS1799`. */
2
- export const selfCheck = () => [];
1
+ import { REPOSITORY_DOCUMENT } from "../config.js";
2
+ import { STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
3
+ /** The code a configured root that matches nothing is reported under. */
4
+ const UNMATCHED_ROOT = "DS1704";
5
+ /** The subject kind of a finding about a suppression record. */
6
+ const SUPPRESSION_SUBJECT = "suppression";
7
+ /** The subject kind of a finding about a configured root. */
8
+ const ROOT_SUBJECT = "root";
9
+ /** The fixed position of a finding about a document rather than about a line of one. */
10
+ const DOCUMENT_POSITION = 1;
11
+ /** A run with nothing for the self-check family to report. */
12
+ export const NO_SELF_CHECK = {
13
+ refusals: [],
14
+ unmatchedRoots: [],
15
+ rootsDocument: "",
16
+ };
17
+ /** The words a message names a mechanism by. */
18
+ const WRITTEN = {
19
+ inline: "inline directive",
20
+ ignore: "ignore entry",
21
+ baseline: "baseline row",
22
+ };
23
+ /** The file references of a run's declarations, by path. */
24
+ export function fileRefsOf(symbols) {
25
+ const byPath = new Map(symbols
26
+ .filter((symbol) => symbol.kind === "file")
27
+ .map((symbol) => [symbol.position.path, symbol.ref]));
28
+ return (path) => byPath.get(path);
29
+ }
30
+ /** A list of codes as a sentence names them. */
31
+ function spelled(codes) {
32
+ if (codes.length < 2) {
33
+ return codes.join("");
34
+ }
35
+ return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
36
+ }
37
+ /** A finding about one suppression, at the suppression's own site. */
38
+ function aboutSuppression(code, ref, site, message) {
39
+ return {
40
+ code,
41
+ position: { ...site, endLine: site.line },
42
+ symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
43
+ message,
44
+ };
45
+ }
46
+ /**
47
+ * The reference a finding about a suppression names: the symbol it names or bound, else
48
+ * the file form of the document that holds it, else that document's path.
49
+ */
50
+ function suppressionRef(symbol, path, files) {
51
+ return symbol === "" ? (files(path) ?? path) : symbol;
52
+ }
53
+ /** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
54
+ export function refusedSuppressions(refusals, files) {
55
+ return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === SUPPRESSION_WITHOUT_REASON
56
+ ? "carries no reason"
57
+ : "names a symbol and no path"}`));
58
+ }
59
+ /**
60
+ * One record per site at which a suppression matched nothing, whatever the number of
61
+ * codes or declarations written there: the first record names the entry, and the message
62
+ * names every code the site's records named and every code the declaration reports
63
+ * instead, so the maintainer corrects the code rather than hunting for it.
64
+ */
65
+ export function staleSuppressions(stale, files) {
66
+ const bySite = new Map();
67
+ for (const one of stale) {
68
+ const { site, mechanism } = one.record;
69
+ const key = `${mechanism}\u0000${site.path}\u0000${String(site.line)}\u0000${String(site.column)}`;
70
+ const held = bySite.get(key) ?? { first: one.record, records: [] };
71
+ held.records.push(one);
72
+ bySite.set(key, held);
73
+ }
74
+ return [...bySite.values()].map(({ first, records }) => {
75
+ const codes = [...new Set(records.map((one) => one.record.code))].sort();
76
+ const reports = [...new Set(records.flatMap((one) => one.reports))]
77
+ .filter((code) => !codes.includes(code))
78
+ .sort();
79
+ const instead = reports.length === 0 ? "" : `, and its declaration reports ${spelled(reports)}`;
80
+ return {
81
+ code: STALE_SUPPRESSION,
82
+ mechanism: first.mechanism,
83
+ entry: {
84
+ code: first.code,
85
+ ...(first.symbol === "" ? {} : { symbol: first.symbol }),
86
+ path: first.path,
87
+ reason: first.reason,
88
+ },
89
+ position: first.site,
90
+ symbol: suppressionRef(first.symbol, first.path, files),
91
+ message: `${WRITTEN[first.mechanism]} for ${spelled(codes)} matches no current finding${instead}`,
92
+ };
93
+ });
94
+ }
95
+ /**
96
+ * One finding per configured root or pattern that named nothing in any project. A finding
97
+ * names the document that declared the roots, or the conventional repository configuration
98
+ * where no document of the target did, and every finding of one run sits at that
99
+ * document's first position: the roots are an array whose members carry no line of their
100
+ * own, so the string each finding names tells one from another.
101
+ */
102
+ export function unmatchedRootFindings(sources, document) {
103
+ const position = {
104
+ path: document === "" ? REPOSITORY_DOCUMENT : document,
105
+ line: DOCUMENT_POSITION,
106
+ column: DOCUMENT_POSITION,
107
+ endLine: DOCUMENT_POSITION,
108
+ };
109
+ return sources.map((source) => ({
110
+ code: UNMATCHED_ROOT,
111
+ position,
112
+ symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
113
+ message: source.includes("*") || source.includes("?")
114
+ ? "configured root pattern matches no symbol of the inventory"
115
+ : "configured root matches no symbol of the inventory",
116
+ }));
117
+ }
118
+ /**
119
+ * The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
120
+ * carrying no reason or naming no path, and a configured root that named nothing. A
121
+ * suppression that matched nothing is a record of the report's stale suppressions, which
122
+ * {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
123
+ */
124
+ export const selfCheck = ({ selfCheck: facts, swept }) => [
125
+ ...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
126
+ ...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
127
+ ];