@cplieger/deadset-ts 3.8.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 +28 -12
- package/dist/src/conformance.js +22 -0
- package/dist/src/findings/completion.js +9 -3
- package/dist/src/findings/self-check.js +14 -2
- 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/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/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 +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/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/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process;
|
|
11
|
+
**Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. `analyze` writes the report the Contract's report schema declares, with a text or JSON rendering beside it on request, and `describe` prints the analyzer's description, which states no conformance record, so an orchestrator that requires a conformance pass refuses this analyzer. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; `explain` exits with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -28,18 +28,18 @@ The npm package carries the command and the library as JavaScript that the compi
|
|
|
28
28
|
npx deadset-ts version
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze
|
|
32
|
-
|
|
33
|
-
| Verb | Job
|
|
34
|
-
| ---------------- |
|
|
35
|
-
| `analyze` | Analyze a project and
|
|
36
|
-
| `explain` | Explain one finding: the relation, the roots and the references that decided it
|
|
37
|
-
| `print-config` | Print the resolved configuration and where each setting came from
|
|
38
|
-
| `print-projects` | Print the identifier of every project a run analyzes
|
|
39
|
-
| `print-roots` | Print every declaration the analysis keeps live without a reference, and why
|
|
40
|
-
| `print-retained` | Print every declaration an exemption held back: each class, its site and detail
|
|
41
|
-
| `describe` | Describe the analyzer:
|
|
42
|
-
| `version` | Print the analyzer and Contract versions
|
|
31
|
+
`version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze --report=PATH` writes the JSON report to `PATH` through a temporary file renamed into place, each `--format=text` or `--format=json` rendering at `PATH` with `.txt` or `.json` appended, and with `--baseline-write=FILE` a baseline of every finding; it writes nothing to standard output and exits with the Contract's exit-code table: 0 clean, 1 for a finding at or above `reporters.fail_on` or a stale suppression, 2 for a usage error, 3 for a failed load, and 4 for a pending finding, which outranks 1. `--exit-code=off` exits 0 whatever the verdict and names the verdict on standard error. The report names the target relative to the directory the run is invoked from, so a target outside that directory fails with 3. `describe` writes its JSON document to standard output and exits 0. `explain`, any other invocation and no arguments print the usage line and exit 2.
|
|
32
|
+
|
|
33
|
+
| Verb | Job |
|
|
34
|
+
| ---------------- | -------------------------------------------------------------------------------- |
|
|
35
|
+
| `analyze` | Analyze a project and write the report, its renderings and a baseline |
|
|
36
|
+
| `explain` | Explain one finding: the relation, the roots and the references that decided it |
|
|
37
|
+
| `print-config` | Print the resolved configuration and where each setting came from |
|
|
38
|
+
| `print-projects` | Print the identifier of every project a run analyzes |
|
|
39
|
+
| `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
|
|
40
|
+
| `print-retained` | Print every declaration an exemption held back: each class, its site and detail |
|
|
41
|
+
| `describe` | Describe the analyzer: versions, accepted report schemas, languages, conformance |
|
|
42
|
+
| `version` | Print the analyzer and Contract versions |
|
|
43
43
|
|
|
44
44
|
## Requirements
|
|
45
45
|
|
package/dist/bin/node-host.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { closeSync, fchmodSync, fsyncSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
/** The name the manifest of this package declares. */
|
|
5
6
|
const PACKAGE_NAME = "@cplieger/deadset-ts";
|
|
@@ -43,6 +44,34 @@ function readVersion() {
|
|
|
43
44
|
at = up;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
/** The mode every document the command writes carries. */
|
|
48
|
+
const DOCUMENT_MODE = 0o644;
|
|
49
|
+
/**
|
|
50
|
+
* Writes one document through a temporary file in the same directory, flushed and then
|
|
51
|
+
* renamed into place, so a run that dies partway leaves no truncated document at the path.
|
|
52
|
+
*/
|
|
53
|
+
function writeAtomically(path, text) {
|
|
54
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}`);
|
|
55
|
+
const fd = openSync(temporary, "wx", DOCUMENT_MODE);
|
|
56
|
+
try {
|
|
57
|
+
try {
|
|
58
|
+
fchmodSync(fd, DOCUMENT_MODE);
|
|
59
|
+
const bytes = Buffer.from(text, "utf8");
|
|
60
|
+
for (let written = 0; written < bytes.length;) {
|
|
61
|
+
written += writeSync(fd, bytes, written);
|
|
62
|
+
}
|
|
63
|
+
fsyncSync(fd);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
renameSync(temporary, path);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
rmSync(temporary, { force: true });
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
46
75
|
/**
|
|
47
76
|
* The filesystem of the platform the command runs on, and the version of the
|
|
48
77
|
* package it was installed from.
|
|
@@ -70,5 +99,6 @@ export function nodeHost() {
|
|
|
70
99
|
found ??= readVersion();
|
|
71
100
|
return found;
|
|
72
101
|
},
|
|
102
|
+
writeFile: writeAtomically,
|
|
73
103
|
};
|
|
74
104
|
}
|
package/dist/src/analysis.js
CHANGED
|
@@ -27,7 +27,7 @@ import { matrixOf, sweepMatrix } from "./matrix.js";
|
|
|
27
27
|
import { relativePath, resolvePath } from "./paths.js";
|
|
28
28
|
import { byPosition } from "./position.js";
|
|
29
29
|
import { reflectiveLookup } from "./reflective-lookup.js";
|
|
30
|
-
import { references } from "./references.js";
|
|
30
|
+
import { references, testFileRulesOf } from "./references.js";
|
|
31
31
|
import { roots, unmatchedEverywhere } from "./roots.js";
|
|
32
32
|
import { serializationContract } from "./serialization-contract.js";
|
|
33
33
|
import { diagnosticsOf, runSession } from "./session.js";
|
|
@@ -85,7 +85,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
85
85
|
const held = inventory(project, host, targetRoot);
|
|
86
86
|
const rooted = roots(project, held, targetRoot, options);
|
|
87
87
|
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
88
|
-
return stage(project, { configuration, held, rooted });
|
|
88
|
+
return stage(project, { configuration, configFile: project.configFile, held, rooted });
|
|
89
89
|
});
|
|
90
90
|
if (failures.length > 0) {
|
|
91
91
|
throw new DiscoveryError(`${String(failures.length)} error(s); no answer was produced`, failures);
|
|
@@ -175,7 +175,12 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
175
175
|
mode,
|
|
176
176
|
testFiles: new Set(resolved.testFilePaths),
|
|
177
177
|
});
|
|
178
|
-
return {
|
|
178
|
+
return {
|
|
179
|
+
configFile: projectRead.configFile,
|
|
180
|
+
configured,
|
|
181
|
+
exempt,
|
|
182
|
+
extra: extra(project, projectRead),
|
|
183
|
+
};
|
|
179
184
|
});
|
|
180
185
|
return {
|
|
181
186
|
matrix: matrixOf(projects.map((one) => one.configured)),
|
|
@@ -265,8 +270,9 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
|
|
|
265
270
|
* any sweep, and the run is swept once under every bound record's mark and once under
|
|
266
271
|
* none, so a record is decided against the finding its code would have produced. The
|
|
267
272
|
* self-check family reports the refused suppressions and the roots that named nothing.
|
|
273
|
+
* Beside the inputs it answers the projects and test-file rules a report names.
|
|
268
274
|
*/
|
|
269
|
-
function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors
|
|
275
|
+
function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors) {
|
|
270
276
|
const targetRoot = scope.target.path;
|
|
271
277
|
const read = readEmitterRun(engine, host, scope, config, mode, detectors);
|
|
272
278
|
const symbols = read.matrix.union.symbols;
|
|
@@ -286,16 +292,26 @@ function runAnalysisInputs(engine, host, scope, config, provenance, mode, detect
|
|
|
286
292
|
};
|
|
287
293
|
const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter((id) => id !== "");
|
|
288
294
|
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
inputs: {
|
|
296
|
+
marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
|
|
297
|
+
unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
|
|
298
|
+
suppressions,
|
|
299
|
+
},
|
|
300
|
+
run: {
|
|
301
|
+
projects: read.projects.map((one) => ({
|
|
302
|
+
id: one.configured.configuration,
|
|
303
|
+
configFile: one.configFile,
|
|
304
|
+
})),
|
|
305
|
+
testFileRules: testFileRulesOf(config.ts.testFiles, read.projects.flatMap((one) => one.configured.testFiles)),
|
|
306
|
+
},
|
|
292
307
|
};
|
|
293
308
|
}
|
|
294
309
|
/**
|
|
295
|
-
* The
|
|
296
|
-
* suppressions bound,
|
|
297
|
-
* declared edges evaluated.
|
|
310
|
+
* The analysis of the run the scope and the configuration describe: the run read with its
|
|
311
|
+
* suppressions bound, every family's findings with the suppressions applied and the
|
|
312
|
+
* declared edges evaluated, and the projects and test-file rules the report names.
|
|
298
313
|
*/
|
|
299
|
-
export function
|
|
300
|
-
|
|
314
|
+
export function runAnalysis(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
|
|
315
|
+
const { inputs, run } = runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors);
|
|
316
|
+
return { result: findingsPass(inputs), run };
|
|
301
317
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** The version of the conformance corpus this analyzer is written against. */
|
|
6
|
+
const CORPUS_VERSION = "1.9.0";
|
|
7
|
+
/** The SHA-256 of zero bytes, which is the digest of a results document no run wrote. */
|
|
8
|
+
const EMPTY_DIGEST = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
9
|
+
/**
|
|
10
|
+
* The conformance run this analyzer records, or undefined where it records none. A
|
|
11
|
+
* description states exactly this, so a reader that requires a pass refuses an analyzer
|
|
12
|
+
* that has not answered the corpus.
|
|
13
|
+
*/
|
|
14
|
+
export const RECORDED_CONFORMANCE = undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The conformance a report states. A report requires the block, so an analyzer that
|
|
17
|
+
* records no run states a failed one over the results document no run wrote, which a merge
|
|
18
|
+
* refuses as it refuses any result other than a pass.
|
|
19
|
+
*/
|
|
20
|
+
export function reportedConformance(recorded) {
|
|
21
|
+
return recorded ?? { corpusVersion: CORPUS_VERSION, result: "fail", digest: EMPTY_DIGEST };
|
|
22
|
+
}
|
|
@@ -7,6 +7,7 @@ import { componentId } from "../components.js";
|
|
|
7
7
|
import { OUTSIDE } from "../graph.js";
|
|
8
8
|
import { KINDS } from "../kinds.js";
|
|
9
9
|
import { FAMILY_KEY_LENGTH } from "../resolve.js";
|
|
10
|
+
import { isRowSubject } from "../suppress.js";
|
|
10
11
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
11
12
|
const RANK = { certain: 3, probable: 2, possible: 1 };
|
|
12
13
|
/** The weaker of two classes, which is a class capped by a ceiling. */
|
|
@@ -59,7 +60,8 @@ function findingComponents(swept) {
|
|
|
59
60
|
* candidate's relation, counts, configurations and component; a live one carries no
|
|
60
61
|
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
61
62
|
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
62
|
-
* numbered past the computed ones in finding order, so no two families mint one
|
|
63
|
+
* numbered past the computed ones in finding order, so no two families mint one; nothing
|
|
64
|
+
* falls with it, so its deletion removes no line.
|
|
63
65
|
*/
|
|
64
66
|
export function completed(input, findings) {
|
|
65
67
|
const { swept } = input;
|
|
@@ -74,7 +76,11 @@ export function completed(input, findings) {
|
|
|
74
76
|
if (row === undefined) {
|
|
75
77
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
// A row's reference may spell a declaration's, as an ignore entry naming one does, and
|
|
80
|
+
// a row is still never the declaration.
|
|
81
|
+
const at = isRowSubject(finding.symbol.kind)
|
|
82
|
+
? OUTSIDE
|
|
83
|
+
: (positions.get(finding.symbol.ref) ?? OUTSIDE);
|
|
78
84
|
const symbol = union.symbols[at];
|
|
79
85
|
const judged = union.subject[at] === true ? symbol : undefined;
|
|
80
86
|
const candidate = judged === undefined ? undefined : candidates.get(judged.id);
|
|
@@ -89,7 +95,7 @@ export function completed(input, findings) {
|
|
|
89
95
|
id: componentId(minted),
|
|
90
96
|
root: true,
|
|
91
97
|
symbolCount: 1,
|
|
92
|
-
deletableLines:
|
|
98
|
+
deletableLines: 0,
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
const counts = candidate === undefined
|
|
@@ -35,12 +35,24 @@ function spelled(codes) {
|
|
|
35
35
|
return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
|
|
36
36
|
}
|
|
37
37
|
/** A finding about one suppression, at the suppression's own site. */
|
|
38
|
-
function aboutSuppression(code, ref, site, message) {
|
|
38
|
+
function aboutSuppression(code, ref, site, message, details) {
|
|
39
39
|
return {
|
|
40
40
|
code,
|
|
41
41
|
position: { ...site, endLine: site.line },
|
|
42
42
|
symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
|
|
43
43
|
message,
|
|
44
|
+
details,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Whether a reason holds a character other than whitespace. */
|
|
48
|
+
const STATED = /[^ \t\r\n]/u;
|
|
49
|
+
/** A refused suppression as written: a member it lacks is absent. */
|
|
50
|
+
function writtenEntry(refused) {
|
|
51
|
+
return {
|
|
52
|
+
code: refused.code,
|
|
53
|
+
...(refused.symbol === "" ? {} : { symbol: refused.symbol }),
|
|
54
|
+
...(refused.path === "" ? {} : { path: refused.path }),
|
|
55
|
+
...(STATED.test(refused.reason) ? { reason: refused.reason } : {}),
|
|
44
56
|
};
|
|
45
57
|
}
|
|
46
58
|
/**
|
|
@@ -54,7 +66,7 @@ function suppressionRef(symbol, path, files) {
|
|
|
54
66
|
export function refusedSuppressions(refusals, files) {
|
|
55
67
|
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
68
|
? "carries no reason"
|
|
57
|
-
: "names a symbol and no path"}
|
|
69
|
+
: "names a symbol and no path"}`, { mechanism: refused.mechanism, entry: writtenEntry(refused) }));
|
|
58
70
|
}
|
|
59
71
|
/**
|
|
60
72
|
* One record per site at which a suppression matched nothing, whatever the number of
|
package/dist/src/references.js
CHANGED
|
@@ -272,6 +272,22 @@ function sitesOf(file, declarations, fileId) {
|
|
|
272
272
|
function patternRule(index) {
|
|
273
273
|
return `test-file-pattern-${String(index + 1)}`;
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* The rules over the test files of several projects at once, ordered by rule: a file two
|
|
277
|
+
* projects hold is one file, so each rule counts the distinct paths it matches.
|
|
278
|
+
*/
|
|
279
|
+
export function testFileRulesOf(testFiles, paths) {
|
|
280
|
+
const distinct = [...new Set(paths)];
|
|
281
|
+
return testFiles
|
|
282
|
+
.map((pattern, index) => {
|
|
283
|
+
const expression = globExpression(pattern);
|
|
284
|
+
return {
|
|
285
|
+
rule: patternRule(index),
|
|
286
|
+
matched: distinct.filter((path) => expression.test(path)).length,
|
|
287
|
+
};
|
|
288
|
+
})
|
|
289
|
+
.sort((a, b) => compare(a.rule, b.rule));
|
|
290
|
+
}
|
|
275
291
|
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
276
292
|
function compare(a, b) {
|
|
277
293
|
if (a === b) {
|
|
@@ -281,7 +297,7 @@ function compare(a, b) {
|
|
|
281
297
|
}
|
|
282
298
|
/**
|
|
283
299
|
* Every reference one project's own files make to the project's own declarations, in
|
|
284
|
-
* position order, and the test-file rules
|
|
300
|
+
* position order, and the files the test-file rules classified.
|
|
285
301
|
*
|
|
286
302
|
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
287
303
|
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
@@ -297,7 +313,6 @@ export function references(project, held, targetRoot, options) {
|
|
|
297
313
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
298
314
|
const declarations = held.declarations;
|
|
299
315
|
const patterns = options.testFiles.map(globExpression);
|
|
300
|
-
const matched = options.testFiles.map(() => 0);
|
|
301
316
|
const found = [];
|
|
302
317
|
const tests = [];
|
|
303
318
|
const chains = aliasChains(project, held);
|
|
@@ -332,13 +347,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
332
347
|
};
|
|
333
348
|
for (const file of files) {
|
|
334
349
|
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);
|
|
350
|
+
const test = patterns.some((pattern) => pattern.test(path));
|
|
342
351
|
if (test) {
|
|
343
352
|
tests.push(path);
|
|
344
353
|
}
|
|
@@ -449,9 +458,6 @@ export function references(project, held, targetRoot, options) {
|
|
|
449
458
|
return {
|
|
450
459
|
configFile: project.configFile,
|
|
451
460
|
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
461
|
testFilePaths: tests.sort(compare),
|
|
456
462
|
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
|
|
457
463
|
};
|
|
@@ -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
|
+
};
|