@cplieger/deadset-ts 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/dist/bin/node-host.js +32 -2
- package/dist/src/analysis.js +111 -26
- package/dist/src/conformance.js +22 -0
- package/dist/src/edges.js +190 -0
- package/dist/src/findings/completion.js +39 -12
- package/dist/src/findings/emitters.js +42 -6
- package/dist/src/findings/self-check.js +139 -2
- package/dist/src/findings/visibility-narrowing.js +9 -40
- package/dist/src/findings-pass.js +97 -0
- package/dist/src/json-document.js +113 -0
- package/dist/src/references.js +18 -12
- 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/roots.js +0 -34
- package/dist/src/run.js +23 -10
- package/dist/src/suppress-file.js +196 -0
- package/dist/src/suppress-inline.js +259 -0
- package/dist/src/suppress.js +148 -0
- 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 +230 -35
- package/src/conformance.ts +34 -0
- package/src/edges.ts +241 -0
- package/src/finding.ts +13 -0
- package/src/findings/boundary.ts +0 -11
- package/src/findings/completion.ts +54 -15
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +81 -7
- package/src/findings/self-check.ts +226 -2
- package/src/findings/visibility-narrowing.ts +10 -52
- package/src/findings-pass.ts +145 -0
- package/src/host.ts +5 -0
- package/src/json-document.ts +130 -0
- package/src/references.ts +23 -15
- 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/roots.ts +0 -39
- package/src/run.ts +40 -12
- package/src/suppress-file.ts +283 -0
- package/src/suppress-inline.ts +318 -0
- package/src/suppress.ts +262 -0
- package/src/verbs/analyze.ts +234 -0
- package/src/verbs/describe.ts +46 -0
- package/src/verbs/verb.ts +13 -0
package/src/analysis.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { decorator } from "./decorator.ts";
|
|
|
14
14
|
import { dependenciesOf, projectNeeds } from "./dependencies.ts";
|
|
15
15
|
import { deprecatedDeclarations } from "./deprecation.ts";
|
|
16
16
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
|
|
17
|
+
import { readEdgeSides } from "./edges.ts";
|
|
17
18
|
import { enumGroup } from "./enum-group.ts";
|
|
18
19
|
import {
|
|
19
20
|
computeExemptions,
|
|
@@ -26,7 +27,13 @@ import {
|
|
|
26
27
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
27
28
|
import { heldByInclusion, sourceTree } from "./file-facts.ts";
|
|
28
29
|
import type { Finding } from "./finding.ts";
|
|
30
|
+
import { findingsPass, type PassResult } from "./findings-pass.ts";
|
|
29
31
|
import type { EmitterInput } from "./findings/emitter.ts";
|
|
32
|
+
import {
|
|
33
|
+
NO_SELF_CHECK,
|
|
34
|
+
unmatchedRootFindings,
|
|
35
|
+
type SelfCheckFacts,
|
|
36
|
+
} from "./findings/self-check.ts";
|
|
30
37
|
import { frameworkLifecycle } from "./framework-lifecycle.ts";
|
|
31
38
|
import type { Host } from "./host.ts";
|
|
32
39
|
import { implementations, mergeImplementations } from "./implementations.ts";
|
|
@@ -38,13 +45,16 @@ import { matrixOf, sweepMatrix, type Configured, type Matrix, type SweepResult }
|
|
|
38
45
|
import { relativePath, resolvePath } from "./paths.ts";
|
|
39
46
|
import { byPosition, type Position } from "./position.ts";
|
|
40
47
|
import { reflectiveLookup } from "./reflective-lookup.ts";
|
|
41
|
-
import { references } from "./references.ts";
|
|
42
|
-
import { roots, unmatchedEverywhere,
|
|
48
|
+
import { references, testFileRulesOf, type TestFileRule } from "./references.ts";
|
|
49
|
+
import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
|
|
43
50
|
import type { Scope } from "./scope.ts";
|
|
44
51
|
import { serializationContract } from "./serialization-contract.ts";
|
|
45
52
|
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
46
53
|
import { accessorsOf, storesOf } from "./stores.ts";
|
|
47
|
-
import type {
|
|
54
|
+
import type { Suppressions } from "./suppress.ts";
|
|
55
|
+
import { readBaseline, readIgnoreFile } from "./suppress-file.ts";
|
|
56
|
+
import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
|
|
57
|
+
import type { Exemption, Mode, SweepInput } from "./sweep.ts";
|
|
48
58
|
import { readTemplates, templateField } from "./template-field.ts";
|
|
49
59
|
|
|
50
60
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
@@ -109,6 +119,8 @@ function compare(a: string, b: string): number {
|
|
|
109
119
|
/** What every stage after the root set reads of one project. */
|
|
110
120
|
interface ProjectRead {
|
|
111
121
|
readonly configuration: string;
|
|
122
|
+
/** The compiler configuration file the project was opened from, absolute. */
|
|
123
|
+
readonly configFile: string;
|
|
112
124
|
readonly held: Inventory;
|
|
113
125
|
readonly rooted: Roots;
|
|
114
126
|
}
|
|
@@ -146,7 +158,7 @@ function readProjects<Answer>(
|
|
|
146
158
|
const held = inventory(project, host, targetRoot);
|
|
147
159
|
const rooted = roots(project, held, targetRoot, options);
|
|
148
160
|
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
149
|
-
return stage(project, { configuration, held, rooted });
|
|
161
|
+
return stage(project, { configuration, configFile: project.configFile, held, rooted });
|
|
150
162
|
});
|
|
151
163
|
if (failures.length > 0) {
|
|
152
164
|
throw new DiscoveryError(
|
|
@@ -211,7 +223,10 @@ export function runRoots(
|
|
|
211
223
|
return {
|
|
212
224
|
configurations,
|
|
213
225
|
roots: ordered,
|
|
214
|
-
findings:
|
|
226
|
+
findings: unmatchedRootFindings(
|
|
227
|
+
unmatched,
|
|
228
|
+
documentOf(host, provenance, ROOTS_SETTING, targetRoot),
|
|
229
|
+
),
|
|
215
230
|
};
|
|
216
231
|
}
|
|
217
232
|
|
|
@@ -240,31 +255,33 @@ const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
|
240
255
|
|
|
241
256
|
/** What the sweep of one run reads of each project, beside what a caller's stage adds. */
|
|
242
257
|
interface SweptProject<Extra> {
|
|
258
|
+
readonly configFile: string;
|
|
243
259
|
readonly configured: Configured;
|
|
244
260
|
readonly exempt: readonly Exemption[];
|
|
245
261
|
readonly extra: Extra;
|
|
246
262
|
}
|
|
247
263
|
|
|
248
|
-
/**
|
|
249
|
-
interface
|
|
250
|
-
readonly
|
|
264
|
+
/** What a run read of its projects before any sweep: the matrix, its exemptions, and each project's answers. */
|
|
265
|
+
interface ReadRun<Extra> {
|
|
266
|
+
readonly matrix: Matrix;
|
|
251
267
|
readonly exempt: readonly Exemption[];
|
|
252
268
|
readonly projects: readonly SweptProject<Extra>[];
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
272
|
+
* Reads every project of the run, its references and exemptions, and `extra`'s answer,
|
|
273
|
+
* while the project's view is open, and merges the projects into one matrix in the run's
|
|
274
|
+
* order.
|
|
258
275
|
*/
|
|
259
|
-
function
|
|
276
|
+
function readRun<Extra>(
|
|
260
277
|
engine: Engine,
|
|
261
278
|
host: Host,
|
|
262
279
|
scope: Scope,
|
|
263
280
|
config: Config,
|
|
264
|
-
|
|
281
|
+
mode: Mode,
|
|
265
282
|
detectors: Detectors,
|
|
266
283
|
extra: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Extra,
|
|
267
|
-
):
|
|
284
|
+
): ReadRun<Extra> {
|
|
268
285
|
const targetRoot = scope.target.path;
|
|
269
286
|
const disabled = disabledClasses(config);
|
|
270
287
|
const templates = disabled.has("template-field")
|
|
@@ -287,22 +304,37 @@ function sweepRun<Extra>(
|
|
|
287
304
|
detectors,
|
|
288
305
|
{
|
|
289
306
|
disabled,
|
|
290
|
-
mode
|
|
307
|
+
mode,
|
|
291
308
|
testFiles: new Set(resolved.testFilePaths),
|
|
292
309
|
},
|
|
293
310
|
);
|
|
294
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
configFile: projectRead.configFile,
|
|
313
|
+
configured,
|
|
314
|
+
exempt,
|
|
315
|
+
extra: extra(project, projectRead),
|
|
316
|
+
};
|
|
295
317
|
});
|
|
296
|
-
const matrix = matrixOf(projects.map((one) => one.configured));
|
|
297
|
-
const exempt = exemptionsOf(projects.flatMap((one) => one.exempt));
|
|
298
|
-
const swept: SweepInput = { ...input, exempt };
|
|
299
318
|
return {
|
|
300
|
-
|
|
301
|
-
exempt,
|
|
319
|
+
matrix: matrixOf(projects.map((one) => one.configured)),
|
|
320
|
+
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
302
321
|
projects,
|
|
303
322
|
};
|
|
304
323
|
}
|
|
305
324
|
|
|
325
|
+
/** The sweep of a run read once, under one set of marks. */
|
|
326
|
+
function sweptOf<Extra>(
|
|
327
|
+
read: ReadRun<Extra>,
|
|
328
|
+
input: Pick<SweepInput, "marked" | "mode">,
|
|
329
|
+
): RunSweep {
|
|
330
|
+
const swept: SweepInput = { ...input, exempt: read.exempt };
|
|
331
|
+
return {
|
|
332
|
+
matrix: read.matrix,
|
|
333
|
+
sweep: sweepMatrix(read.matrix, swept),
|
|
334
|
+
retained: retainedIn(read.matrix, swept),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
306
338
|
/**
|
|
307
339
|
* Sweeps the run the scope and the configuration describe. Each project's references
|
|
308
340
|
* and exemptions are read against its own inventory while its view is open; the
|
|
@@ -321,32 +353,59 @@ export function runSweep(
|
|
|
321
353
|
input: Pick<SweepInput, "marked" | "mode">,
|
|
322
354
|
detectors: Detectors = DETECTORS,
|
|
323
355
|
): RunSweep {
|
|
324
|
-
return
|
|
356
|
+
return sweptOf(
|
|
357
|
+
readRun(engine, host, scope, config, input.mode, detectors, () => undefined),
|
|
358
|
+
input,
|
|
359
|
+
);
|
|
325
360
|
}
|
|
326
361
|
|
|
327
|
-
/**
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
/** What each project contributes to the facts every emitter reads beside the sweep. */
|
|
363
|
+
interface EmitterExtra {
|
|
364
|
+
readonly deprecated: readonly string[];
|
|
365
|
+
readonly accessors: ReturnType<typeof accessorsOf>;
|
|
366
|
+
readonly needs: ReturnType<typeof projectNeeds>;
|
|
367
|
+
readonly implementations: ReturnType<typeof implementations>;
|
|
368
|
+
readonly heldByInclusion: readonly string[];
|
|
369
|
+
readonly rooted: Roots;
|
|
370
|
+
readonly directives: readonly InlineDirective[];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
374
|
+
function readEmitterRun(
|
|
334
375
|
engine: Engine,
|
|
335
376
|
host: Host,
|
|
336
377
|
scope: Scope,
|
|
337
378
|
config: Config,
|
|
338
|
-
|
|
339
|
-
detectors: Detectors
|
|
340
|
-
):
|
|
379
|
+
mode: Mode,
|
|
380
|
+
detectors: Detectors,
|
|
381
|
+
): ReadRun<EmitterExtra> {
|
|
341
382
|
const targetRoot = scope.target.path;
|
|
342
|
-
|
|
383
|
+
return readRun(engine, host, scope, config, mode, detectors, (project, read) => ({
|
|
343
384
|
deprecated: deprecatedDeclarations(project, read.held),
|
|
344
385
|
accessors: accessorsOf(project, read.held),
|
|
345
386
|
needs: projectNeeds(project, read.held, host),
|
|
346
387
|
implementations: implementations(project, read.held),
|
|
347
388
|
heldByInclusion: heldByInclusion(project, targetRoot),
|
|
389
|
+
rooted: read.rooted,
|
|
390
|
+
directives: inlineDirectives(project, read.held, targetRoot),
|
|
348
391
|
}));
|
|
349
|
-
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
396
|
+
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
397
|
+
*/
|
|
398
|
+
function emitterInputOver(
|
|
399
|
+
host: Host,
|
|
400
|
+
scope: Scope,
|
|
401
|
+
config: Config,
|
|
402
|
+
read: ReadRun<EmitterExtra>,
|
|
403
|
+
input: Pick<SweepInput, "marked" | "mode">,
|
|
404
|
+
selfCheck: SelfCheckFacts,
|
|
405
|
+
): EmitterInput {
|
|
406
|
+
const targetRoot = scope.target.path;
|
|
407
|
+
const swept = sweptOf(read, input);
|
|
408
|
+
const { projects } = read;
|
|
350
409
|
return {
|
|
351
410
|
config,
|
|
352
411
|
swept,
|
|
@@ -356,7 +415,7 @@ export function runEmitterInput(
|
|
|
356
415
|
references: one.configured.references,
|
|
357
416
|
accessors: one.extra.accessors,
|
|
358
417
|
})),
|
|
359
|
-
|
|
418
|
+
read.exempt,
|
|
360
419
|
input.mode,
|
|
361
420
|
),
|
|
362
421
|
dependencies: dependenciesOf(
|
|
@@ -373,7 +432,143 @@ export function runEmitterInput(
|
|
|
373
432
|
boundary: {
|
|
374
433
|
consumers: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
|
|
375
434
|
encapsulated: readManifest(host, targetRoot).declaresExports,
|
|
376
|
-
edges:
|
|
435
|
+
edges: readEdgeSides(host, targetRoot),
|
|
436
|
+
},
|
|
437
|
+
selfCheck,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* What every emitter reads of the run the scope and the configuration describe, swept
|
|
443
|
+
* under the caller's marks: the run's sweep, the facts beside it each kind family reads,
|
|
444
|
+
* and the sides of the target's declared cross-language edges that name a symbol of this
|
|
445
|
+
* language. The run loads no consumer, and it reads no suppression document, so the
|
|
446
|
+
* self-check family has nothing to report.
|
|
447
|
+
*/
|
|
448
|
+
export function runEmitterInput(
|
|
449
|
+
engine: Engine,
|
|
450
|
+
host: Host,
|
|
451
|
+
scope: Scope,
|
|
452
|
+
config: Config,
|
|
453
|
+
input: Pick<SweepInput, "marked" | "mode">,
|
|
454
|
+
detectors: Detectors = DETECTORS,
|
|
455
|
+
): EmitterInput {
|
|
456
|
+
const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
|
|
457
|
+
return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** One run read for its findings: the input swept with and without its suppressions' marks. */
|
|
461
|
+
export interface AnalysisInputs {
|
|
462
|
+
/** The run swept under the marks of every bound suppression record. */
|
|
463
|
+
readonly marked: EmitterInput;
|
|
464
|
+
/** The same run swept with no mark, which is what a record would have withheld. */
|
|
465
|
+
readonly unmarked: EmitterInput;
|
|
466
|
+
/** The run's suppression records and refusals, in reading order. */
|
|
467
|
+
readonly suppressions: Suppressions;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Reads the run the scope and the configuration describe for its findings. The inline
|
|
472
|
+
* directives, the ignore file's entries and the baseline's rows are read and bound before
|
|
473
|
+
* any sweep, and the run is swept once under every bound record's mark and once under
|
|
474
|
+
* none, so a record is decided against the finding its code would have produced. The
|
|
475
|
+
* self-check family reports the refused suppressions and the roots that named nothing.
|
|
476
|
+
* Beside the inputs it answers the projects and test-file rules a report names.
|
|
477
|
+
*/
|
|
478
|
+
function runAnalysisInputs(
|
|
479
|
+
engine: Engine,
|
|
480
|
+
host: Host,
|
|
481
|
+
scope: Scope,
|
|
482
|
+
config: Config,
|
|
483
|
+
provenance: Provenance,
|
|
484
|
+
mode: Mode,
|
|
485
|
+
detectors: Detectors,
|
|
486
|
+
): { readonly inputs: AnalysisInputs; readonly run: RunFacts } {
|
|
487
|
+
const targetRoot = scope.target.path;
|
|
488
|
+
const read = readEmitterRun(engine, host, scope, config, mode, detectors);
|
|
489
|
+
const symbols = read.matrix.union.symbols;
|
|
490
|
+
const documents = [
|
|
491
|
+
inlineSuppressions(read.projects.map((one) => one.extra.directives)),
|
|
492
|
+
readIgnoreFile(host, targetRoot, symbols),
|
|
493
|
+
readBaseline(host, targetRoot, symbols),
|
|
494
|
+
];
|
|
495
|
+
const suppressions: Suppressions = {
|
|
496
|
+
records: documents.flatMap((one) => one.records),
|
|
497
|
+
refusals: documents.flatMap((one) => one.refusals),
|
|
498
|
+
};
|
|
499
|
+
const facts: SelfCheckFacts = {
|
|
500
|
+
refusals: suppressions.refusals,
|
|
501
|
+
unmatchedRoots: unmatchedEverywhere(
|
|
502
|
+
config.rootPatterns,
|
|
503
|
+
read.projects.map((one) => one.extra.rooted),
|
|
504
|
+
),
|
|
505
|
+
rootsDocument: documentOf(host, provenance, ROOTS_SETTING, targetRoot),
|
|
506
|
+
};
|
|
507
|
+
const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter(
|
|
508
|
+
(id) => id !== "",
|
|
509
|
+
);
|
|
510
|
+
return {
|
|
511
|
+
inputs: {
|
|
512
|
+
marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
|
|
513
|
+
unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
|
|
514
|
+
suppressions,
|
|
515
|
+
},
|
|
516
|
+
run: {
|
|
517
|
+
projects: read.projects.map((one) => ({
|
|
518
|
+
id: one.configured.configuration,
|
|
519
|
+
configFile: one.configFile,
|
|
520
|
+
})),
|
|
521
|
+
testFileRules: testFileRulesOf(
|
|
522
|
+
config.ts.testFiles,
|
|
523
|
+
read.projects.flatMap((one) => one.configured.testFiles),
|
|
524
|
+
),
|
|
377
525
|
},
|
|
378
526
|
};
|
|
379
527
|
}
|
|
528
|
+
|
|
529
|
+
/** One project of a run: the name the run gives it and the file it was opened from. */
|
|
530
|
+
export interface RunProject {
|
|
531
|
+
readonly id: string;
|
|
532
|
+
/** The compiler configuration file, absolute. */
|
|
533
|
+
readonly configFile: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** What a report states about the run beside its findings. */
|
|
537
|
+
export interface RunFacts {
|
|
538
|
+
/** The projects the run analyzed, in the run's order. */
|
|
539
|
+
readonly projects: readonly RunProject[];
|
|
540
|
+
/** The rules that classified files as test files, each counted over every project at once. */
|
|
541
|
+
readonly testFileRules: readonly TestFileRule[];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The findings of one run, beside what a report states about the run itself. */
|
|
545
|
+
export interface RunAnalysis {
|
|
546
|
+
readonly result: PassResult;
|
|
547
|
+
readonly run: RunFacts;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The analysis of the run the scope and the configuration describe: the run read with its
|
|
552
|
+
* suppressions bound, every family's findings with the suppressions applied and the
|
|
553
|
+
* declared edges evaluated, and the projects and test-file rules the report names.
|
|
554
|
+
*/
|
|
555
|
+
export function runAnalysis(
|
|
556
|
+
engine: Engine,
|
|
557
|
+
host: Host,
|
|
558
|
+
scope: Scope,
|
|
559
|
+
config: Config,
|
|
560
|
+
provenance: Provenance,
|
|
561
|
+
mode: Mode,
|
|
562
|
+
detectors: Detectors = DETECTORS,
|
|
563
|
+
): RunAnalysis {
|
|
564
|
+
const { inputs, run } = runAnalysisInputs(
|
|
565
|
+
engine,
|
|
566
|
+
host,
|
|
567
|
+
scope,
|
|
568
|
+
config,
|
|
569
|
+
provenance,
|
|
570
|
+
mode,
|
|
571
|
+
detectors,
|
|
572
|
+
);
|
|
573
|
+
return { result: findingsPass(inputs), run };
|
|
574
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this analyzer states about its own conformance: the corpus version it is written
|
|
3
|
+
* against and its recorded result over that corpus, which `describe` and every report name.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The result one product reached over the conformance corpus. */
|
|
7
|
+
export interface Conformance {
|
|
8
|
+
readonly corpusVersion: string;
|
|
9
|
+
readonly result: "pass" | "fail";
|
|
10
|
+
/** `sha256:` and the lowercase hexadecimal SHA-256 of the results document the runner wrote. */
|
|
11
|
+
readonly digest: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The version of the conformance corpus this analyzer is written against. */
|
|
15
|
+
const CORPUS_VERSION = "1.9.0";
|
|
16
|
+
|
|
17
|
+
/** The SHA-256 of zero bytes, which is the digest of a results document no run wrote. */
|
|
18
|
+
const EMPTY_DIGEST = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The conformance run this analyzer records, or undefined where it records none. A
|
|
22
|
+
* description states exactly this, so a reader that requires a pass refuses an analyzer
|
|
23
|
+
* that has not answered the corpus.
|
|
24
|
+
*/
|
|
25
|
+
export const RECORDED_CONFORMANCE: Conformance | undefined = undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The conformance a report states. A report requires the block, so an analyzer that
|
|
29
|
+
* records no run states a failed one over the results document no run wrote, which a merge
|
|
30
|
+
* refuses as it refuses any result other than a pass.
|
|
31
|
+
*/
|
|
32
|
+
export function reportedConformance(recorded: Conformance | undefined): Conformance {
|
|
33
|
+
return recorded ?? { corpusVersion: CORPUS_VERSION, result: "fail", digest: EMPTY_DIGEST };
|
|
34
|
+
}
|
package/src/edges.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
|
|
8
|
+
import { ConfigError } from "./config.ts";
|
|
9
|
+
import type { Finding } from "./finding.ts";
|
|
10
|
+
import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
|
|
11
|
+
import type { Host } from "./host.ts";
|
|
12
|
+
import type { InventorySymbol } from "./inventory.ts";
|
|
13
|
+
import { positionAt, walkDocument } from "./json-document.ts";
|
|
14
|
+
import { joinPath } from "./paths.ts";
|
|
15
|
+
import { positionKey } from "./position.ts";
|
|
16
|
+
|
|
17
|
+
/** The name and the location the Contract fixes for the edges document. */
|
|
18
|
+
export const EDGES_FILE = "deadset-edges.json";
|
|
19
|
+
|
|
20
|
+
/** The language prefix of every reference this analyzer evaluates. */
|
|
21
|
+
const OWN_PREFIX = "ts://";
|
|
22
|
+
|
|
23
|
+
/** The closed key list of the document and of one edge. */
|
|
24
|
+
const DOCUMENT_KEYS: ReadonlySet<string> = new Set(["description", "edges"]);
|
|
25
|
+
const EDGE_KEYS: ReadonlySet<string> = new Set(["id", "because", "provides", "used_by"]);
|
|
26
|
+
|
|
27
|
+
/** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
|
|
28
|
+
const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
|
|
29
|
+
|
|
30
|
+
/** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
|
|
31
|
+
const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
|
|
32
|
+
|
|
33
|
+
const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
|
|
34
|
+
const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
|
|
35
|
+
const ID_WANT =
|
|
36
|
+
"one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
37
|
+
const REFERENCE_WANT =
|
|
38
|
+
"a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
39
|
+
|
|
40
|
+
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
41
|
+
export class EdgesError extends ConfigError {
|
|
42
|
+
constructor(at: string, text: string, want: string) {
|
|
43
|
+
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
44
|
+
this.name = "EdgesError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a reference carries a wildcard outside a quoted or a computed component, which
|
|
50
|
+
* would name a set of symbols rather than one.
|
|
51
|
+
*/
|
|
52
|
+
function carriesWildcard(ref: string): boolean {
|
|
53
|
+
let quoted = false;
|
|
54
|
+
let computed = false;
|
|
55
|
+
let escaped = false;
|
|
56
|
+
for (const char of ref) {
|
|
57
|
+
if (escaped) {
|
|
58
|
+
escaped = false;
|
|
59
|
+
} else if (quoted && char === "\\") {
|
|
60
|
+
escaped = true;
|
|
61
|
+
} else if (quoted) {
|
|
62
|
+
quoted = char !== "'";
|
|
63
|
+
} else if (computed) {
|
|
64
|
+
computed = char !== "]";
|
|
65
|
+
} else if (char === "'") {
|
|
66
|
+
quoted = true;
|
|
67
|
+
} else if (char === "[") {
|
|
68
|
+
computed = true;
|
|
69
|
+
} else if (char === "*") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One edge, checked: the closed key list, the value types and the value forms. */
|
|
81
|
+
function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
|
|
82
|
+
const refuse = (text: string, want: string): never => {
|
|
83
|
+
throw new EdgesError(at, text, want);
|
|
84
|
+
};
|
|
85
|
+
if (!isObject(value)) {
|
|
86
|
+
return refuse(JSON.stringify(value), EDGE_WANT);
|
|
87
|
+
}
|
|
88
|
+
for (const [key, held] of Object.entries(value)) {
|
|
89
|
+
if (!EDGE_KEYS.has(key) || typeof held !== "string") {
|
|
90
|
+
refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const edge = value as Readonly<Record<string, string | undefined>>;
|
|
94
|
+
const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
|
|
95
|
+
const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
|
|
96
|
+
const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
|
|
97
|
+
if (!ID_FORM.test(id)) {
|
|
98
|
+
refuse(JSON.stringify(id), ID_WANT);
|
|
99
|
+
}
|
|
100
|
+
for (const ref of [provides, usedBy]) {
|
|
101
|
+
if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
|
|
102
|
+
refuse(JSON.stringify(ref), REFERENCE_WANT);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { id, provides, usedBy };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every side of a declared edge that names a symbol of this language, in document order,
|
|
110
|
+
* `provides` before `used_by` within an edge. An absent document declares no edge. The
|
|
111
|
+
* document is strict JSON with a closed key list at both levels, and a defect in it is an
|
|
112
|
+
* {@link EdgesError}.
|
|
113
|
+
*/
|
|
114
|
+
export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide[] {
|
|
115
|
+
const file = joinPath(targetRoot, EDGES_FILE);
|
|
116
|
+
if (host.kindOf(file) === "absent") {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const text = host.readFile(file);
|
|
120
|
+
let parsed: unknown;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(text);
|
|
123
|
+
} catch (error: unknown) {
|
|
124
|
+
throw new EdgesError(
|
|
125
|
+
EDGES_FILE,
|
|
126
|
+
error instanceof Error ? error.message : String(error),
|
|
127
|
+
DOCUMENT_WANT,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!isObject(parsed)) {
|
|
131
|
+
throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
|
|
132
|
+
}
|
|
133
|
+
const walk = walkDocument(text, "edges");
|
|
134
|
+
if (walk.twice !== undefined) {
|
|
135
|
+
throw new EdgesError(
|
|
136
|
+
positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)),
|
|
137
|
+
`the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
|
|
138
|
+
DOCUMENT_WANT,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
|
|
142
|
+
const description = parsed["description"];
|
|
143
|
+
const edges = parsed["edges"];
|
|
144
|
+
if (
|
|
145
|
+
unknown !== undefined ||
|
|
146
|
+
(description !== undefined && typeof description !== "string") ||
|
|
147
|
+
!Array.isArray(edges)
|
|
148
|
+
) {
|
|
149
|
+
throw new EdgesError(
|
|
150
|
+
EDGES_FILE,
|
|
151
|
+
unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`,
|
|
152
|
+
DOCUMENT_WANT,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sides: EdgeSide[] = [];
|
|
157
|
+
(edges as readonly unknown[]).forEach((value, index) => {
|
|
158
|
+
const offset = walk.opened[index];
|
|
159
|
+
const at =
|
|
160
|
+
offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
|
|
161
|
+
const edge = readEdge(value, at);
|
|
162
|
+
const named: readonly [EdgeSideName, string][] = [
|
|
163
|
+
["provides", edge.provides],
|
|
164
|
+
["used_by", edge.usedBy],
|
|
165
|
+
];
|
|
166
|
+
for (const [side, symbol] of named) {
|
|
167
|
+
if (symbol.startsWith(OWN_PREFIX)) {
|
|
168
|
+
sides.push({ edge: edge.id, side, symbol });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return sides;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** One side's verdict, as an evaluation names it. */
|
|
176
|
+
export type EdgeState = "live" | "dead" | "absent";
|
|
177
|
+
|
|
178
|
+
/** One edge evaluation: one side of this language, its state, and the finding it holds. */
|
|
179
|
+
export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
|
|
180
|
+
readonly state: EdgeState;
|
|
181
|
+
/** Present exactly when the state is `dead`: the finding a live paired symbol cancels. */
|
|
182
|
+
readonly finding?: F;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The findings a report publishes, and one evaluation per declared side of this language. */
|
|
186
|
+
export interface Evaluated<F extends Finding> {
|
|
187
|
+
readonly findings: readonly F[];
|
|
188
|
+
/** Ordered by edge, then by side, each compared bytewise. */
|
|
189
|
+
readonly evaluations: readonly EdgeEvaluation<F>[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Two strings ordered bytewise. */
|
|
193
|
+
function compare(a: string, b: string): number {
|
|
194
|
+
if (a === b) {
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
return a < b ? -1 : 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Evaluates every declared side of this language over the run's findings. A side naming a
|
|
202
|
+
* symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
|
|
203
|
+
* finding about is `dead` and carries that finding, pending until a merge reads the other
|
|
204
|
+
* side and reported nowhere else, a narrowing finding included; any other is `live`.
|
|
205
|
+
*/
|
|
206
|
+
export function evaluateEdges<F extends Finding>(
|
|
207
|
+
findings: readonly F[],
|
|
208
|
+
sides: readonly EdgeSide[],
|
|
209
|
+
symbols: readonly InventorySymbol[],
|
|
210
|
+
): Evaluated<F> {
|
|
211
|
+
if (sides.length === 0) {
|
|
212
|
+
return { findings, evaluations: [] };
|
|
213
|
+
}
|
|
214
|
+
const byRef = new Map<string, string>();
|
|
215
|
+
for (const symbol of symbols) {
|
|
216
|
+
if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
|
|
217
|
+
byRef.set(symbol.ref, symbol.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const held = new Map<string, number>();
|
|
221
|
+
findings.forEach((finding, at) => {
|
|
222
|
+
held.set(positionKey(finding.position), at);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const pending = new Set<number>();
|
|
226
|
+
const evaluations = sides.map((side): EdgeEvaluation<F> => {
|
|
227
|
+
const id = byRef.get(side.symbol);
|
|
228
|
+
if (id === undefined) {
|
|
229
|
+
return { ...side, state: "absent" };
|
|
230
|
+
}
|
|
231
|
+
const at = held.get(id);
|
|
232
|
+
const finding = at === undefined ? undefined : findings[at];
|
|
233
|
+
if (at === undefined || finding === undefined) {
|
|
234
|
+
return { ...side, state: "live" };
|
|
235
|
+
}
|
|
236
|
+
pending.add(at);
|
|
237
|
+
return { ...side, state: "dead", finding };
|
|
238
|
+
});
|
|
239
|
+
evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
|
|
240
|
+
return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
|
|
241
|
+
}
|
package/src/finding.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "./config.ts";
|
|
2
2
|
import type { Fixability } from "./kinds.ts";
|
|
3
3
|
import type { DependencySection } from "./ref.ts";
|
|
4
|
+
import type { Mechanism } from "./suppress.ts";
|
|
4
5
|
import type { Relation } from "./sweep.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -58,6 +59,18 @@ export interface FindingDetails {
|
|
|
58
59
|
readonly writePositions?: readonly FindingPosition[];
|
|
59
60
|
/** On `DS1601`: the manifest section that declares the dependency. */
|
|
60
61
|
readonly dependencyClass?: DependencySection;
|
|
62
|
+
/** On `DS1701` and `DS1702`: the document that holds the refused suppression. */
|
|
63
|
+
readonly mechanism?: Mechanism;
|
|
64
|
+
/** On `DS1701` and `DS1702`: the refused suppression as written, a member absent where it lacks one. */
|
|
65
|
+
readonly entry?: SuppressionEntry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One suppression in the four-member form the ignore file and the baseline share. */
|
|
69
|
+
export interface SuppressionEntry {
|
|
70
|
+
readonly code: string;
|
|
71
|
+
readonly symbol?: string;
|
|
72
|
+
readonly path?: string;
|
|
73
|
+
readonly reason?: string;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/** One finding as its emitter states it. */
|