@cplieger/deadset-ts 8.0.0-dev.5 → 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.
Files changed (88) hide show
  1. package/dist/src/analysis.js +3 -3
  2. package/dist/src/config.js +1 -1
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/contract/contract.generated.js +1071 -0
  5. package/dist/src/contract/report.generated.js +796 -0
  6. package/dist/src/exempt-classes.js +1 -59
  7. package/dist/src/exempt.js +8 -19
  8. package/dist/src/findings/completion.js +11 -20
  9. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  10. package/dist/src/findings/emitters.js +4 -4
  11. package/dist/src/findings/interfaces.js +4 -5
  12. package/dist/src/findings/intra-function.js +8 -7
  13. package/dist/src/findings/non-code-artifacts.js +5 -6
  14. package/dist/src/findings/reads-and-writes.js +9 -8
  15. package/dist/src/findings/self-check.js +7 -10
  16. package/dist/src/findings/unused-declarations.js +28 -27
  17. package/dist/src/findings/visibility-narrowing.js +9 -11
  18. package/dist/src/findings-pass.js +1 -3
  19. package/dist/src/index.js +1 -1
  20. package/dist/src/intra-function-parts.js +8 -13
  21. package/dist/src/kinds.js +4 -360
  22. package/dist/src/report-schema.js +273 -0
  23. package/dist/src/report.js +17 -11
  24. package/dist/src/resolve.js +1 -6
  25. package/dist/src/run.js +2 -1
  26. package/dist/src/setup-failure.js +2 -6
  27. package/dist/src/suppress-file.js +4 -3
  28. package/dist/src/suppress-inline.js +3 -2
  29. package/dist/src/suppress.js +29 -48
  30. package/dist/src/verbs/analyze.js +7 -2
  31. package/dist/src/verbs/describe.js +4 -4
  32. package/dist/src/verbs/explain.js +2 -1
  33. package/dist/src/verbs/print-config.js +2 -1
  34. package/dist/src/verbs/print-projects.js +2 -1
  35. package/dist/src/verbs/print-retained.js +2 -1
  36. package/dist/src/verbs/print-roots.js +2 -1
  37. package/dist/src/verbs/verb.js +1 -5
  38. package/dist/src/verbs/version.js +2 -2
  39. package/jsr.json +1 -1
  40. package/package.json +2 -1
  41. package/src/analysis.ts +3 -3
  42. package/src/config.ts +7 -13
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contract/contract.generated.ts +1123 -0
  45. package/src/contract/report.generated.ts +1157 -0
  46. package/src/exempt-classes.ts +1 -70
  47. package/src/exempt.ts +12 -20
  48. package/src/finding.ts +9 -9
  49. package/src/findings/completion.ts +22 -23
  50. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  51. package/src/findings/emitter.ts +6 -7
  52. package/src/findings/emitters.ts +6 -6
  53. package/src/findings/interfaces.ts +4 -6
  54. package/src/findings/intra-function.ts +8 -8
  55. package/src/findings/non-code-artifacts.ts +5 -7
  56. package/src/findings/reads-and-writes.ts +13 -11
  57. package/src/findings/self-check.ts +8 -15
  58. package/src/findings/unused-declarations.ts +28 -27
  59. package/src/findings/visibility-narrowing.ts +9 -12
  60. package/src/findings-pass.ts +1 -4
  61. package/src/index.ts +1 -1
  62. package/src/intra-function-parts.ts +8 -14
  63. package/src/kinds.ts +6 -406
  64. package/src/report-schema.ts +366 -0
  65. package/src/report.ts +42 -195
  66. package/src/reporters/annotations.ts +2 -2
  67. package/src/reporters/json.ts +1 -1
  68. package/src/reporters/reporter.ts +2 -2
  69. package/src/reporters/reporters.ts +1 -1
  70. package/src/reporters/sarif.ts +13 -8
  71. package/src/reporters/text.ts +3 -3
  72. package/src/resolve.ts +11 -11
  73. package/src/run.ts +2 -1
  74. package/src/setup-failure.ts +5 -7
  75. package/src/suppress-file.ts +3 -4
  76. package/src/suppress-inline.ts +2 -2
  77. package/src/suppress.ts +39 -55
  78. package/src/verbs/analyze.ts +16 -4
  79. package/src/verbs/describe.ts +10 -4
  80. package/src/verbs/explain.ts +2 -1
  81. package/src/verbs/print-config.ts +2 -1
  82. package/src/verbs/print-projects.ts +2 -1
  83. package/src/verbs/print-retained.ts +2 -1
  84. package/src/verbs/print-roots.ts +2 -1
  85. package/src/verbs/verb.ts +0 -6
  86. package/src/verbs/version.ts +2 -2
  87. package/dist/src/version.js +0 -7
  88. 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
+ }
@@ -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
- /** The version of the report schema this analyzer writes a report to. */
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 [wire, finding] of findings) {
18
+ for (const [written, finding] of findings) {
22
19
  const { members } = finding.component;
23
- if (members !== undefined && !listed.has(wire.component.id)) {
24
- listed.set(wire.component.id, { id: wire.component.id, members: members.map(wireSymbol) });
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: 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
- * Every other total keeps describing the whole set. Zero keeps every finding.
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: report.findings.slice(0, most),
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
  }
@@ -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 { EXIT_FAILURE, EXIT_USAGE } from "./verbs/verb.js";
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 `setup failure: ${failure.setupClass}: ${failure.detail}`;
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, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isPartKind, isSymbolRef, siteText, } from "./suppress.js";
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: SUPPRESSION_WITHOUT_REASON });
103
+ refusals.push({ ...written, reported: CODE.suppressionWithoutReason });
103
104
  }
104
105
  if (written.path === "") {
105
- refusals.push({ ...written, reported: UNSCOPED_ENTRY });
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, SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
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: SUPPRESSION_WITHOUT_REASON,
278
+ reported: CODE.suppressionWithoutReason,
278
279
  code,
279
280
  symbol: "",
280
281
  path: site.path,
@@ -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
- * The subject kinds that are a dependency row, each with the code that reports one. An
76
- * ignore entry or a baseline row naming the row's reference and file withholds its finding
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 ROW_SUBJECTS.has(kind);
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 [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
90
+ return rowOf(SUBJECT_OF_CODE.get(code));
103
91
  }
104
- /** Whether a finding's subject is a dependency row. */
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 DEPENDENCY_SUBJECTS.has(kind);
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 [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
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
- * The subject kinds that are a part of a declaration rather than a declaration: a part
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 PART_SUBJECTS.has(kind);
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 code.startsWith(PART_KIND_PREFIX);
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 (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
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 (ROW_SUBJECTS.has(finding.symbol.kind)) {
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 { CONTRACT_VERSION } from "../version.js";
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, SCHEMA_VERSIONS_ACCEPTED } from "../report.js";
3
- import { CONTRACT_VERSION } from "../version.js";
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: 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 { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
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 { EXIT_CLEAN } from "./verb.js";
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());