@cplieger/deadset-ts 8.0.0-dev.4 → 8.0.0-dev.6
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/dist/src/analysis.js +3 -3
- package/dist/src/config.js +1 -1
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1071 -0
- package/dist/src/contract/report.generated.js +796 -0
- package/dist/src/convention-rows.js +1 -1
- package/dist/src/exempt-classes.js +1 -59
- package/dist/src/exempt.js +8 -19
- package/dist/src/findings/completion.js +11 -20
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +4 -4
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +8 -7
- package/dist/src/findings/non-code-artifacts.js +5 -6
- package/dist/src/findings/reads-and-writes.js +9 -8
- package/dist/src/findings/self-check.js +7 -10
- package/dist/src/findings/unused-declarations.js +28 -27
- package/dist/src/findings/visibility-narrowing.js +9 -11
- package/dist/src/findings-pass.js +1 -3
- package/dist/src/index.js +1 -1
- package/dist/src/intra-function-parts.js +8 -13
- package/dist/src/kinds.js +4 -360
- package/dist/src/report-schema.js +273 -0
- package/dist/src/report.js +17 -11
- package/dist/src/resolve.js +1 -6
- package/dist/src/run.js +2 -1
- package/dist/src/setup-failure.js +2 -6
- package/dist/src/suppress-file.js +4 -3
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +29 -48
- package/dist/src/verbs/analyze.js +7 -2
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +2 -1
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +2 -1
- package/dist/src/verbs/print-retained.js +2 -1
- package/dist/src/verbs/print-roots.js +2 -1
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +2 -2
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +3 -3
- package/src/config.ts +7 -13
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1123 -0
- package/src/contract/report.generated.ts +1157 -0
- package/src/convention-rows.ts +1 -1
- package/src/exempt-classes.ts +1 -70
- package/src/exempt.ts +12 -20
- package/src/finding.ts +9 -9
- package/src/findings/completion.ts +22 -23
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +6 -7
- package/src/findings/emitters.ts +6 -6
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +8 -8
- package/src/findings/non-code-artifacts.ts +5 -7
- package/src/findings/reads-and-writes.ts +13 -11
- package/src/findings/self-check.ts +8 -15
- package/src/findings/unused-declarations.ts +28 -27
- package/src/findings/visibility-narrowing.ts +9 -12
- package/src/findings-pass.ts +1 -4
- package/src/index.ts +1 -1
- package/src/intra-function-parts.ts +8 -14
- package/src/kinds.ts +6 -406
- package/src/report-schema.ts +366 -0
- package/src/report.ts +42 -195
- package/src/reporters/annotations.ts +2 -2
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +13 -8
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +11 -11
- package/src/run.ts +2 -1
- package/src/setup-failure.ts +5 -7
- package/src/suppress-file.ts +3 -4
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +39 -55
- package/src/verbs/analyze.ts +16 -4
- package/src/verbs/describe.ts +10 -4
- package/src/verbs/explain.ts +2 -1
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +2 -1
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/verb.ts +0 -6
- package/src/verbs/version.ts +2 -2
- package/dist/src/version.js +0 -7
- package/src/version.ts +0 -7
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds a report to `contract/report.schema.json` and `contract/finding.schema.json`
|
|
3
|
+
* before it is written, reading the schemas the generator copies from the Contract, so
|
|
4
|
+
* this analyzer never writes a document a reader refuses. It evaluates the keywords the
|
|
5
|
+
* Contract's schemas use; the schemas' patterns are written to match alike in RE2 and in
|
|
6
|
+
* ECMAScript.
|
|
7
|
+
*/
|
|
8
|
+
import { REPORT_SCHEMAS } from "./contract/report.generated.js";
|
|
9
|
+
/**
|
|
10
|
+
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
11
|
+
* another fails every check rather than passing values the keyword would refuse.
|
|
12
|
+
*/
|
|
13
|
+
const KEYWORDS = new Set([
|
|
14
|
+
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
15
|
+
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
16
|
+
...["properties", "additionalProperties", "required", "minProperties"],
|
|
17
|
+
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
18
|
+
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
19
|
+
]);
|
|
20
|
+
const patterns = new Map();
|
|
21
|
+
function pattern(source) {
|
|
22
|
+
let held = patterns.get(source);
|
|
23
|
+
if (held === undefined) {
|
|
24
|
+
held = new RegExp(source, "u");
|
|
25
|
+
patterns.set(source, held);
|
|
26
|
+
}
|
|
27
|
+
return held;
|
|
28
|
+
}
|
|
29
|
+
function isObject(value) {
|
|
30
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function typeOf(value) {
|
|
33
|
+
if (value === null) {
|
|
34
|
+
return "null";
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return "array";
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
41
|
+
}
|
|
42
|
+
return typeof value;
|
|
43
|
+
}
|
|
44
|
+
function hasType(value, name) {
|
|
45
|
+
const held = typeOf(value);
|
|
46
|
+
return held === name || (name === "number" && held === "integer");
|
|
47
|
+
}
|
|
48
|
+
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
49
|
+
function canonical(value) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
52
|
+
}
|
|
53
|
+
if (isObject(value)) {
|
|
54
|
+
return `{${Object.keys(value)
|
|
55
|
+
.sort()
|
|
56
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
57
|
+
.join(",")}}`;
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
62
|
+
function codePoints(value) {
|
|
63
|
+
let counted = 0;
|
|
64
|
+
for (const _point of value) {
|
|
65
|
+
counted += 1;
|
|
66
|
+
}
|
|
67
|
+
return counted;
|
|
68
|
+
}
|
|
69
|
+
const escape = (name) => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
70
|
+
/** A schema evaluated in the document it is read from, so a relative `$ref` resolves there. */
|
|
71
|
+
class Checker {
|
|
72
|
+
documents;
|
|
73
|
+
constructor(documents) {
|
|
74
|
+
this.documents = documents;
|
|
75
|
+
}
|
|
76
|
+
resolve(doc, ref) {
|
|
77
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
78
|
+
const target = file === "" ? doc : file;
|
|
79
|
+
let schema = this.documents[target];
|
|
80
|
+
for (const token of pointer.split("/").filter((one) => one !== "")) {
|
|
81
|
+
schema = isObject(schema)
|
|
82
|
+
? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
85
|
+
if (!isObject(schema)) {
|
|
86
|
+
throw new Error(`report schema: ${ref} names no schema`);
|
|
87
|
+
}
|
|
88
|
+
return [target, schema];
|
|
89
|
+
}
|
|
90
|
+
matches(doc, schema, value, at) {
|
|
91
|
+
const found = [];
|
|
92
|
+
this.check(doc, schema, value, at, found);
|
|
93
|
+
return found.length === 0;
|
|
94
|
+
}
|
|
95
|
+
check(doc, schema, value, at, found) {
|
|
96
|
+
for (const keyword of Object.keys(schema)) {
|
|
97
|
+
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
98
|
+
throw new Error(`report schema: the keyword ${keyword} is not one this validator evaluates`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const refuse = (keyword, reason, pointer = at) => {
|
|
102
|
+
found.push({ pointer, keyword, reason });
|
|
103
|
+
};
|
|
104
|
+
if (typeof schema["$ref"] === "string") {
|
|
105
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
106
|
+
this.check(target, referenced, value, at, found);
|
|
107
|
+
}
|
|
108
|
+
const types = schema["type"];
|
|
109
|
+
const named = types === undefined ? [] : Array.isArray(types) ? types : [types];
|
|
110
|
+
if (named.length > 0 && !named.some((one) => hasType(value, String(one)))) {
|
|
111
|
+
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.checkValue(schema, value, refuse);
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
this.checkArray(doc, schema, value, at, found, refuse);
|
|
117
|
+
}
|
|
118
|
+
else if (isObject(value)) {
|
|
119
|
+
this.checkObject(doc, schema, value, at, found, refuse);
|
|
120
|
+
}
|
|
121
|
+
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
122
|
+
}
|
|
123
|
+
checkValue(schema, value, refuse) {
|
|
124
|
+
const values = schema["enum"];
|
|
125
|
+
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
126
|
+
refuse("enum", `${canonical(value)} is not one of the values the schema lists`);
|
|
127
|
+
}
|
|
128
|
+
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
129
|
+
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
130
|
+
}
|
|
131
|
+
if (typeof value === "string") {
|
|
132
|
+
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
133
|
+
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
136
|
+
refuse("minLength", `${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (typeof value === "number") {
|
|
140
|
+
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
141
|
+
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
142
|
+
}
|
|
143
|
+
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
144
|
+
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
checkObject(doc, schema, value, at, found, refuse) {
|
|
149
|
+
const properties = isObject(schema["properties"]) ? schema["properties"] : {};
|
|
150
|
+
const additional = schema["additionalProperties"];
|
|
151
|
+
for (const name of Object.keys(value).sort()) {
|
|
152
|
+
const member = `${at}/${escape(name)}`;
|
|
153
|
+
const declared = properties[name];
|
|
154
|
+
if (isObject(declared)) {
|
|
155
|
+
this.check(doc, declared, value[name], member, found);
|
|
156
|
+
}
|
|
157
|
+
else if (additional === false) {
|
|
158
|
+
refuse("additionalProperties", `the member ${JSON.stringify(name)} is not one the schema declares`);
|
|
159
|
+
}
|
|
160
|
+
else if (isObject(additional)) {
|
|
161
|
+
this.check(doc, additional, value[name], member, found);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const name of Array.isArray(schema["required"]) ? schema["required"] : []) {
|
|
165
|
+
if (!Object.hasOwn(value, name)) {
|
|
166
|
+
refuse("required", `the member ${JSON.stringify(name)} is absent`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const fewest = schema["minProperties"];
|
|
170
|
+
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
171
|
+
refuse("minProperties", `the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
checkArray(doc, schema, value, at, found, refuse) {
|
|
175
|
+
const items = schema["items"];
|
|
176
|
+
if (isObject(items)) {
|
|
177
|
+
value.forEach((item, i) => {
|
|
178
|
+
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
182
|
+
refuse("minItems", `the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
185
|
+
refuse("maxItems", `the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`);
|
|
186
|
+
}
|
|
187
|
+
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
188
|
+
refuse("uniqueItems", "the array lists one value twice");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
checkApplicators(doc, schema, value, at, found, refuse) {
|
|
192
|
+
const list = (keyword) => Array.isArray(schema[keyword]) ? schema[keyword] : [];
|
|
193
|
+
for (const each of list("allOf")) {
|
|
194
|
+
this.check(doc, each, value, at, found);
|
|
195
|
+
}
|
|
196
|
+
const matching = (keyword) => list(keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
197
|
+
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
198
|
+
refuse("anyOf", "the value matches none of the alternatives");
|
|
199
|
+
}
|
|
200
|
+
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
201
|
+
refuse("oneOf", `the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`);
|
|
202
|
+
}
|
|
203
|
+
if (isObject(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
204
|
+
refuse("not", "the value matches a schema it must not match");
|
|
205
|
+
}
|
|
206
|
+
if (isObject(schema["if"])) {
|
|
207
|
+
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
208
|
+
if (isObject(branch)) {
|
|
209
|
+
this.check(doc, branch, value, at, found);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
216
|
+
* at one of them: what it refuses in a value, none for an instance.
|
|
217
|
+
*/
|
|
218
|
+
export function schemaRefusals(documents, root) {
|
|
219
|
+
const checker = new Checker(documents);
|
|
220
|
+
const top = documents[root];
|
|
221
|
+
if (top === undefined) {
|
|
222
|
+
throw new Error(`the schema set holds no document ${root}`);
|
|
223
|
+
}
|
|
224
|
+
return (value) => {
|
|
225
|
+
const found = [];
|
|
226
|
+
checker.check(root, top, value, "", found);
|
|
227
|
+
return found;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const reportSchema = schemaRefusals(REPORT_SCHEMAS, "report.schema.json");
|
|
231
|
+
/**
|
|
232
|
+
* What the report schema refuses in one report document, none for an instance, and, as
|
|
233
|
+
* no schema keyword can state it, a components listing that is not one entry per
|
|
234
|
+
* component a finding or an edge evaluation's finding names.
|
|
235
|
+
*/
|
|
236
|
+
export function reportRefusals(document) {
|
|
237
|
+
const found = reportSchema(document);
|
|
238
|
+
return found.length > 0 ? found : listingRefusals(document);
|
|
239
|
+
}
|
|
240
|
+
/** The components listing held to the components the findings name, once each. */
|
|
241
|
+
function listingRefusals(report) {
|
|
242
|
+
if (report.components === undefined) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const refuse = (pointer, reason) => [
|
|
246
|
+
{ pointer, keyword: "components", reason },
|
|
247
|
+
];
|
|
248
|
+
const listed = new Set();
|
|
249
|
+
for (const [i, entry] of report.components.entries()) {
|
|
250
|
+
if (listed.has(entry.id)) {
|
|
251
|
+
return refuse(`/components/${String(i)}`, `the entry lists component ${entry.id} a second time`);
|
|
252
|
+
}
|
|
253
|
+
listed.add(entry.id);
|
|
254
|
+
}
|
|
255
|
+
const named = [
|
|
256
|
+
...report.findings.map((finding, i) => [`/findings/${String(i)}`, finding.component.id]),
|
|
257
|
+
...report.edge_evaluations.flatMap((one, i) => one.finding === undefined
|
|
258
|
+
? []
|
|
259
|
+
: [[`/edge_evaluations/${String(i)}/finding`, one.finding.component.id]]),
|
|
260
|
+
];
|
|
261
|
+
for (const [pointer, id] of named) {
|
|
262
|
+
if (!listed.has(id)) {
|
|
263
|
+
return refuse(pointer, `the finding names component ${id}, which the components listing lacks`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const ids = new Set(named.map(([, id]) => id));
|
|
267
|
+
for (const [i, entry] of report.components.entries()) {
|
|
268
|
+
if (!ids.has(entry.id)) {
|
|
269
|
+
return refuse(`/components/${String(i)}`, `the entry lists component ${entry.id}, which no finding names`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [];
|
|
273
|
+
}
|
package/dist/src/report.js
CHANGED
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
* in the order its schema lists them, built from what one run answered. Every reporter
|
|
4
4
|
* renders this document, so no two renderings disagree about which findings exist.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
const SCHEMA_VERSION = "9.0.0";
|
|
8
|
-
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
9
|
-
export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
|
|
6
|
+
import { SCHEMA_VERSION, SCHEMA_VERSIONS } from "./contract/contract.generated.js";
|
|
10
7
|
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
11
8
|
export const ANALYZER_NAME = "deadset-ts";
|
|
12
9
|
/** The language this analyzer claims. */
|
|
@@ -18,10 +15,13 @@ export const LANGUAGE = "ts";
|
|
|
18
15
|
*/
|
|
19
16
|
function componentsOf(findings) {
|
|
20
17
|
const listed = new Map();
|
|
21
|
-
for (const [
|
|
18
|
+
for (const [written, finding] of findings) {
|
|
22
19
|
const { members } = finding.component;
|
|
23
|
-
if (members !== undefined && !listed.has(
|
|
24
|
-
listed.set(
|
|
20
|
+
if (members !== undefined && !listed.has(written.component.id)) {
|
|
21
|
+
listed.set(written.component.id, {
|
|
22
|
+
id: written.component.id,
|
|
23
|
+
members: members.map(wireSymbol),
|
|
24
|
+
});
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
return [...listed.values()];
|
|
@@ -228,7 +228,7 @@ export function buildReport(input) {
|
|
|
228
228
|
name: ANALYZER_NAME,
|
|
229
229
|
version: input.version,
|
|
230
230
|
languages: [LANGUAGE],
|
|
231
|
-
schema_versions_accepted:
|
|
231
|
+
schema_versions_accepted: SCHEMA_VERSIONS,
|
|
232
232
|
conformance: {
|
|
233
233
|
corpus_version: input.conformance.corpusVersion,
|
|
234
234
|
result: input.conformance.result,
|
|
@@ -298,16 +298,22 @@ export function sortFindings(report, by) {
|
|
|
298
298
|
return { ...report, findings: [...report.findings].sort(by === "size" ? bySize : canonical) };
|
|
299
299
|
}
|
|
300
300
|
/**
|
|
301
|
-
* The report holding the first `most` findings of its order, the rest counted as omitted
|
|
302
|
-
*
|
|
301
|
+
* The report holding the first `most` findings of its order, the rest counted as omitted,
|
|
302
|
+
* and listing only the components those findings name. Every other total keeps describing
|
|
303
|
+
* the whole set. Zero keeps every finding.
|
|
303
304
|
*/
|
|
304
305
|
export function capFindings(report, most) {
|
|
305
306
|
if (most <= 0 || report.findings.length <= most) {
|
|
306
307
|
return report;
|
|
307
308
|
}
|
|
309
|
+
const findings = report.findings.slice(0, most);
|
|
310
|
+
const named = new Set([...findings, ...report.edge_evaluations.flatMap((one) => one.finding ?? [])].map((finding) => finding.component.id));
|
|
308
311
|
return {
|
|
309
312
|
...report,
|
|
310
|
-
findings
|
|
313
|
+
findings,
|
|
314
|
+
...(report.components === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { components: report.components.filter((listed) => named.has(listed.id)) }),
|
|
311
317
|
totals: { ...report.totals, omitted: report.findings.length - most },
|
|
312
318
|
};
|
|
313
319
|
}
|
package/dist/src/resolve.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { SEVERITIES, LANGUAGES, CONFIDENCES, FORMATS, FAMILY_KEY_LENGTH, } from "./contract/contract.generated.js";
|
|
1
2
|
import { ConfigError, defaultConfig, } from "./config.js";
|
|
2
3
|
import { CONVENTION_ROWS } from "./convention-rows.js";
|
|
3
4
|
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
|
|
4
5
|
import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
|
|
5
6
|
/** Names the command-line flags in a refusal, where a document is named by its path. */
|
|
6
7
|
const FLAGS_LABEL = "the command-line flags";
|
|
7
|
-
/** The length of a severity key naming a whole family: the prefix and two digits. */
|
|
8
|
-
export const FAMILY_KEY_LENGTH = 4;
|
|
9
8
|
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
10
9
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
11
10
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
@@ -30,12 +29,8 @@ const PROJECT_SEGMENT = String.raw `(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^
|
|
|
30
29
|
*/
|
|
31
30
|
const PROJECT_PATH = new RegExp(`^${PROJECT_SEGMENT}(?:/${PROJECT_SEGMENT})*$`, "u");
|
|
32
31
|
const TARGET_KINDS = ["application", "library"];
|
|
33
|
-
const CONFIDENCES = ["certain", "probable", "possible"];
|
|
34
32
|
const GENERATED_FILES = ["exclude", "include"];
|
|
35
33
|
const CONSUMER_TESTS = ["test", "production"];
|
|
36
|
-
const LANGUAGES = ["go", "ts"];
|
|
37
|
-
const SEVERITIES = ["allow", "warn", "deny"];
|
|
38
|
-
const FORMATS = ["text", "json", "github", "sarif", "template"];
|
|
39
34
|
const SORTS = ["position", "size"];
|
|
40
35
|
const CASCADES = ["roots", "full"];
|
|
41
36
|
/**
|
package/dist/src/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXIT_FAILURE, EXIT_USAGE } from "./contract/contract.generated.js";
|
|
1
2
|
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
2
3
|
import { DiscoveryError, renderDiagnostic } from "./discover.js";
|
|
3
4
|
import { joinPath, resolvePath } from "./paths.js";
|
|
@@ -11,7 +12,7 @@ import { printRootsVerb } from "./verbs/print-roots.js";
|
|
|
11
12
|
import { ANALYZE_OPTIONS, analyzeVerb } from "./verbs/analyze.js";
|
|
12
13
|
import { describeVerb } from "./verbs/describe.js";
|
|
13
14
|
import { EXPLAIN_OPTIONS, explainVerb } from "./verbs/explain.js";
|
|
14
|
-
import {
|
|
15
|
+
import {} from "./verbs/verb.js";
|
|
15
16
|
import { versionVerb } from "./verbs/version.js";
|
|
16
17
|
/** Every verb this command answers, in the order the usage text lists them. */
|
|
17
18
|
const VERBS = [
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* A setup failure: a file or a component the analysis needs and the project does not
|
|
3
|
-
* provide. Each one names what is missing and the fix, on one line of the error stream
|
|
4
|
-
* that starts with its class.
|
|
5
|
-
*/
|
|
1
|
+
import { SETUP_FAILURE_PREFIX } from "./contract/contract.generated.js";
|
|
6
2
|
/** The failure as the one line of the error stream that names it. */
|
|
7
3
|
export function setupLine(failure) {
|
|
8
|
-
return
|
|
4
|
+
return `${SETUP_FAILURE_PREFIX}${failure.setupClass}: ${failure.detail}`;
|
|
9
5
|
}
|
|
10
6
|
/**
|
|
11
7
|
* A run that met a setup failure where none may be dropped: in a configuration the
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* one record shape, so one decoder and one matcher serve both, and a row moves into the
|
|
5
5
|
* ignore file by gaining a maintainer's reason.
|
|
6
6
|
*/
|
|
7
|
+
import { CODE } from "./contract/contract.generated.js";
|
|
7
8
|
import { jsonChunks } from "./json-chunks.js";
|
|
8
9
|
import { joinPath } from "./paths.js";
|
|
9
10
|
import { positionAt, walkDocument } from "./json-document.js";
|
|
10
11
|
import { declarationKey } from "./ref.js";
|
|
11
|
-
import { CODE_FORM,
|
|
12
|
+
import { CODE_FORM, SuppressionError, blankReason, isEntryPath, isPartKind, isSymbolRef, siteText, } from "./suppress.js";
|
|
12
13
|
/** The name and the location the grammar fixes for the ignore file. */
|
|
13
14
|
export const IGNORE_FILE = "deadset-ignore.json";
|
|
14
15
|
/** The name and the location the grammar fixes for the baseline. */
|
|
@@ -99,10 +100,10 @@ function readRecord(value, site, byReference, shape) {
|
|
|
99
100
|
};
|
|
100
101
|
const refusals = [];
|
|
101
102
|
if (blankReason(reason)) {
|
|
102
|
-
refusals.push({ ...written, reported:
|
|
103
|
+
refusals.push({ ...written, reported: CODE.suppressionWithoutReason });
|
|
103
104
|
}
|
|
104
105
|
if (written.path === "") {
|
|
105
|
-
refusals.push({ ...written, reported:
|
|
106
|
+
refusals.push({ ...written, reported: CODE.unscopedIgnoreEntry });
|
|
106
107
|
}
|
|
107
108
|
if (refusals.length > 0) {
|
|
108
109
|
return { records: [], refusals };
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* one or more codes and a reason, and binding to every declaration that begins on the line
|
|
4
4
|
* below it, or on its own line where it trails a token there.
|
|
5
5
|
*/
|
|
6
|
+
import { CODE } from "./contract/contract.generated.js";
|
|
6
7
|
import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
|
|
7
8
|
import { importBindings } from "./import-bindings.js";
|
|
8
9
|
import { isParameterProperty, nodeKey } from "./inventory.js";
|
|
9
10
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
10
11
|
import { importRef } from "./ref.js";
|
|
11
12
|
import { signatureOf } from "./signatures.js";
|
|
12
|
-
import { isPartKind,
|
|
13
|
+
import { isPartKind, SuppressionError, siteText, } from "./suppress.js";
|
|
13
14
|
/**
|
|
14
15
|
* The three expressions the grammar applies to the text of each line comment, in this
|
|
15
16
|
* order: a comment in the namespace, a well-formed directive, and a directive that lacks
|
|
@@ -274,7 +275,7 @@ export function inlineSuppressions(perProject) {
|
|
|
274
275
|
case "no-reason":
|
|
275
276
|
for (const code of directive.codes) {
|
|
276
277
|
refusals.push({
|
|
277
|
-
reported:
|
|
278
|
+
reported: CODE.suppressionWithoutReason,
|
|
278
279
|
code,
|
|
279
280
|
symbol: "",
|
|
280
281
|
path: site.path,
|
package/dist/src/suppress.js
CHANGED
|
@@ -6,15 +6,10 @@
|
|
|
6
6
|
* withholds a finding its code would have produced there, dormant when the configuration
|
|
7
7
|
* withholds every such finding anyway, and stale otherwise.
|
|
8
8
|
*/
|
|
9
|
+
import { KIND_ROWS, SYMBOL_SUBJECTS, } from "./contract/contract.generated.js";
|
|
9
10
|
import { ConfigError } from "./config.js";
|
|
10
11
|
import { positionKey } from "./position.js";
|
|
11
12
|
import { isRef } from "./ref.js";
|
|
12
|
-
/** The code a suppression that carries no reason is reported under. */
|
|
13
|
-
export const SUPPRESSION_WITHOUT_REASON = "DS1701";
|
|
14
|
-
/** The code an ignore entry or a baseline row that names no path is reported under. */
|
|
15
|
-
export const UNSCOPED_ENTRY = "DS1702";
|
|
16
|
-
/** The code a suppression that withholds nothing is reported under. */
|
|
17
|
-
export const STALE_SUPPRESSION = "DS1703";
|
|
18
13
|
/**
|
|
19
14
|
* A directive, an entry or a document the grammar refuses before any finding exists: an
|
|
20
15
|
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
@@ -71,73 +66,59 @@ export function isSymbolRef(value) {
|
|
|
71
66
|
export function blankReason(reason) {
|
|
72
67
|
return reason === undefined || /^[ \t\r\n]*$/u.test(reason);
|
|
73
68
|
}
|
|
69
|
+
/** The subject of the kind each live code names. */
|
|
70
|
+
const SUBJECT_OF_CODE = new Map(KIND_ROWS.map((row) => [row.code, row.subject]));
|
|
71
|
+
/** The subject a finding about one symbol kind is about. */
|
|
72
|
+
function subjectOf(kind) {
|
|
73
|
+
return Object.hasOwn(SYMBOL_SUBJECTS, kind)
|
|
74
|
+
? SYMBOL_SUBJECTS[kind]
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Whether a subject is a row of a document rather than a declaration of the program. */
|
|
78
|
+
function rowOf(subject) {
|
|
79
|
+
return subject === "file" || subject === "dependency" || subject === "record";
|
|
80
|
+
}
|
|
74
81
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* and marks nothing, and an inline directive never binds one.
|
|
78
|
-
*/
|
|
79
|
-
const DEPENDENCY_SUBJECTS = new Map([
|
|
80
|
-
["dependency", ["DS1601"]],
|
|
81
|
-
["module-directive", ["DS1605"]],
|
|
82
|
-
]);
|
|
83
|
-
/**
|
|
84
|
-
* The subject kinds that are a row of a document rather than a declaration of the program,
|
|
85
|
-
* each with the codes that report one. Apart from a dependency row, nothing withholds a
|
|
86
|
-
* finding about one.
|
|
82
|
+
* Whether a finding's subject is a row of a document rather than a declaration. Apart
|
|
83
|
+
* from a dependency row, nothing withholds a finding about one.
|
|
87
84
|
*/
|
|
88
|
-
const ROW_SUBJECTS = new Map([
|
|
89
|
-
["file", ["DS1501", "DS1502"]],
|
|
90
|
-
...DEPENDENCY_SUBJECTS,
|
|
91
|
-
["root", ["DS1704"]],
|
|
92
|
-
["configured-declaration", ["DS1706"]],
|
|
93
|
-
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
94
|
-
["edge", ["DS1705"]],
|
|
95
|
-
]);
|
|
96
|
-
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
97
85
|
export function isRowSubject(kind) {
|
|
98
|
-
return
|
|
86
|
+
return rowOf(subjectOf(kind));
|
|
99
87
|
}
|
|
100
88
|
/** Whether a code reports a row of a document. */
|
|
101
89
|
export function isRowCode(code) {
|
|
102
|
-
return
|
|
90
|
+
return rowOf(SUBJECT_OF_CODE.get(code));
|
|
103
91
|
}
|
|
104
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Whether a finding's subject is a dependency row. An ignore entry or a baseline row
|
|
94
|
+
* naming the row's reference and file withholds its finding and marks nothing, and an
|
|
95
|
+
* inline directive never binds one.
|
|
96
|
+
*/
|
|
105
97
|
export function isDependencySubject(kind) {
|
|
106
|
-
return
|
|
98
|
+
return subjectOf(kind) === "dependency";
|
|
107
99
|
}
|
|
108
100
|
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
109
101
|
export function isDependencyCode(code) {
|
|
110
|
-
return
|
|
102
|
+
return SUBJECT_OF_CODE.get(code) === "dependency";
|
|
111
103
|
}
|
|
112
104
|
/** Whether a code reports a row of a document that no record of any mechanism can name. */
|
|
113
105
|
export function isUnsuppressibleCode(code) {
|
|
114
106
|
return isRowCode(code) && !isDependencyCode(code);
|
|
115
107
|
}
|
|
116
108
|
/**
|
|
117
|
-
*
|
|
109
|
+
* Whether a finding's subject is a part of the declaration its reference names: a part
|
|
118
110
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
119
111
|
* it, and the part is decided inside that declaration whatever uses it.
|
|
120
112
|
*/
|
|
121
|
-
const PART_SUBJECTS = new Set([
|
|
122
|
-
"parameter",
|
|
123
|
-
"receiver",
|
|
124
|
-
"result",
|
|
125
|
-
"statement",
|
|
126
|
-
"case",
|
|
127
|
-
"store",
|
|
128
|
-
]);
|
|
129
|
-
/** Whether a finding's subject is a part of the declaration its reference names. */
|
|
130
113
|
export function isPartSubject(kind) {
|
|
131
|
-
return
|
|
114
|
+
return subjectOf(kind) === "part";
|
|
132
115
|
}
|
|
133
|
-
/** The prefix of the codes of the part kinds, the intra-function range. */
|
|
134
|
-
const PART_KIND_PREFIX = "DS18";
|
|
135
116
|
/**
|
|
136
117
|
* Whether a code is a part kind, whose record withholds its finding and marks nothing:
|
|
137
118
|
* the record says the part is wanted, not that the declaration holding it is.
|
|
138
119
|
*/
|
|
139
120
|
export function isPartKind(code) {
|
|
140
|
-
return
|
|
121
|
+
return SUBJECT_OF_CODE.get(code) === "part";
|
|
141
122
|
}
|
|
142
123
|
/** One finding's identity: its code and the position of the thing it names. */
|
|
143
124
|
function claimKey(finding) {
|
|
@@ -155,12 +136,12 @@ export function pairs(record, finding) {
|
|
|
155
136
|
if (record.code !== finding.code) {
|
|
156
137
|
return false;
|
|
157
138
|
}
|
|
158
|
-
if (
|
|
139
|
+
if (isDependencySubject(finding.symbol.kind)) {
|
|
159
140
|
return (record.mechanism !== "inline" &&
|
|
160
141
|
record.symbol === finding.symbol.ref &&
|
|
161
142
|
record.path === finding.position.path);
|
|
162
143
|
}
|
|
163
|
-
if (
|
|
144
|
+
if (isRowSubject(finding.symbol.kind)) {
|
|
164
145
|
return false;
|
|
165
146
|
}
|
|
166
147
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, } from "../contract/contract.generated.js";
|
|
1
2
|
import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
|
|
2
3
|
import { ConfigError, kindOf } from "../config.js";
|
|
3
4
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
@@ -6,13 +7,13 @@ import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
|
6
7
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
8
|
import { partialNotes } from "../partial.js";
|
|
8
9
|
import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
|
|
10
|
+
import { reportRefusals } from "../report-schema.js";
|
|
9
11
|
import { json } from "../reporters/json.js";
|
|
10
12
|
import { RENDERINGS } from "../reporters/reporters.js";
|
|
11
13
|
import { parseTemplate, TemplateError } from "../reporters/template.js";
|
|
12
14
|
import { resolve } from "../resolve.js";
|
|
13
15
|
import { otherSchemeRows, rowReason, writeBaseline } from "../suppress-file.js";
|
|
14
|
-
import {
|
|
15
|
-
import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
|
|
16
|
+
import {} from "./verb.js";
|
|
16
17
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
17
18
|
export const ANALYZE_OPTIONS = {
|
|
18
19
|
plain: ["report", "baseline-write", "exit-code", "template"],
|
|
@@ -257,6 +258,10 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
257
258
|
return EXIT_FAILURE;
|
|
258
259
|
}
|
|
259
260
|
try {
|
|
261
|
+
const [refused] = reportRefusals(report);
|
|
262
|
+
if (refused !== undefined) {
|
|
263
|
+
throw new Error(`the report breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`);
|
|
264
|
+
}
|
|
260
265
|
host.writeDocument(documents.report, json(report));
|
|
261
266
|
const options = {
|
|
262
267
|
readSource: (path) => host.readFile(joinPath(targetRoot, path)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
|
|
1
2
|
import { CONFORMANCE } from "../conformance.js";
|
|
2
|
-
import { ANALYZER_NAME, LANGUAGE
|
|
3
|
-
import {
|
|
4
|
-
import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
|
|
3
|
+
import { ANALYZER_NAME, LANGUAGE } from "../report.js";
|
|
4
|
+
import {} from "./verb.js";
|
|
5
5
|
/**
|
|
6
6
|
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
7
7
|
* report schema versions it reads, the languages it claims, and its recorded result over
|
|
@@ -12,7 +12,7 @@ export function describeDocument(version, recorded) {
|
|
|
12
12
|
name: ANALYZER_NAME,
|
|
13
13
|
version,
|
|
14
14
|
contract_version: CONTRACT_VERSION,
|
|
15
|
-
schema_versions_accepted:
|
|
15
|
+
schema_versions_accepted: SCHEMA_VERSIONS,
|
|
16
16
|
languages: [LANGUAGE],
|
|
17
17
|
...(recorded === undefined
|
|
18
18
|
? {}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
|
|
1
2
|
import { runAnalysis } from "../analysis.js";
|
|
2
3
|
import { listing } from "../components.js";
|
|
3
4
|
import { ConfigError } from "../config.js";
|
|
@@ -7,7 +8,7 @@ import { partialNotes } from "../partial.js";
|
|
|
7
8
|
import { resolvePath } from "../paths.js";
|
|
8
9
|
import { positionKey } from "../position.js";
|
|
9
10
|
import { resolve } from "../resolve.js";
|
|
10
|
-
import {
|
|
11
|
+
import {} from "./verb.js";
|
|
11
12
|
/**
|
|
12
13
|
* The three spellings of an explanation request, each naming the symbol to explain. They are
|
|
13
14
|
* one request, because exactly one state holds for a symbol and the answer is that state.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { EXIT_CLEAN } from "../contract/contract.generated.js";
|
|
1
2
|
import { printConfig } from "../print.js";
|
|
2
3
|
import { resolve } from "../resolve.js";
|
|
3
|
-
import {
|
|
4
|
+
import {} from "./verb.js";
|
|
4
5
|
/** Writes the resolved configuration with the source of every setting. */
|
|
5
6
|
export const printConfigVerb = ({ out, inputs }) => {
|
|
6
7
|
const { config, provenance } = resolve(inputs());
|