@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8
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/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +37 -56
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/build-scripts.js +17 -29
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +2 -3
- package/dist/src/config.js +6 -82
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +2 -2
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/contract.generated.js +13 -0
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/conventions.js +20 -60
- package/dist/src/dependencies.js +25 -43
- package/dist/src/discover.js +14 -40
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +2 -8
- package/dist/src/entry-points.js +3 -10
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +0 -4
- package/dist/src/exempt.js +6 -22
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/reads-and-writes.js +2 -1
- package/dist/src/findings/self-check.js +3 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +7 -12
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +59 -0
- package/dist/src/fs/manifest.js +43 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/git-ignore.js +34 -25
- package/dist/src/html-entries.js +6 -33
- package/dist/src/in-operands.js +2 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/intra-function-parts.js +10 -13
- package/dist/src/inventory.js +2 -21
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +0 -7
- package/dist/src/manifest.js +5 -43
- package/dist/src/mdx-entries.js +3 -26
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/references.js +14 -27
- package/dist/src/report-schema.js +3 -225
- package/dist/src/report.js +22 -31
- package/dist/src/reporters/sarif.js +4 -2
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -724
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +55 -61
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +34 -103
- package/dist/src/serialization-contract.js +5 -9
- package/dist/src/session.js +2 -1
- package/dist/src/suppress-file.js +21 -25
- package/dist/src/suppress.js +3 -3
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-globals.js +6 -28
- package/dist/src/test-joins.js +6 -6
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
- package/dist/src/type-errors.js +14 -38
- package/dist/src/unseen-loads.js +2 -9
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +25 -26
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +8 -6
- package/dist/src/verbs/print-projects.js +4 -3
- package/dist/src/verbs/print-retained.js +2 -1
- package/dist/src/verbs/print-roots.js +2 -1
- package/dist/src/verbs/version.js +2 -1
- package/dist/src/workflow-steps.js +9 -46
- package/dist/src/workspace-manifests.js +2 -2
- package/dist/src/workspace-programs.js +5 -3
- package/dist/src/workspace-resolution.js +13 -17
- package/dist/src/workspace.js +28 -116
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +55 -76
- package/src/automatic-types.ts +11 -29
- package/src/build-scripts.ts +17 -38
- package/src/component-layer.ts +4 -2
- package/src/components.ts +2 -4
- package/src/config.ts +31 -296
- package/src/configuration-files.ts +27 -170
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +4 -4
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/contract.generated.ts +25 -0
- package/src/contract/report.generated.ts +3 -2
- package/src/conventions.ts +26 -74
- package/src/dependencies.ts +32 -50
- package/src/discover.ts +15 -40
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +3 -9
- package/src/entry-points.ts +2 -13
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +8 -30
- package/src/file-facts.ts +6 -24
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/reads-and-writes.ts +2 -1
- package/src/findings/self-check.ts +3 -2
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +9 -15
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +74 -0
- package/src/fs/manifest.ts +52 -0
- package/src/fs/tree.ts +110 -0
- package/src/git-ignore.ts +38 -25
- package/src/host.ts +12 -5
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/injection-container.ts +1 -1
- package/src/intra-function-parts.ts +10 -14
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +0 -8
- package/src/manifest.ts +5 -46
- package/src/mdx-entries.ts +3 -26
- package/src/partial.ts +2 -1
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -92
- package/src/product.ts +18 -0
- package/src/references.ts +14 -29
- package/src/report-schema.ts +3 -301
- package/src/report.ts +24 -34
- package/src/reporters/sarif.ts +4 -4
- package/src/reporters/template.ts +4 -20
- package/src/resolve.ts +283 -1114
- package/src/roots.ts +7 -20
- package/src/run.ts +57 -75
- package/src/schema.ts +391 -388
- package/src/scope.ts +40 -126
- package/src/serialization-contract.ts +5 -10
- package/src/session.ts +2 -1
- package/src/suppress-file.ts +21 -29
- package/src/suppress.ts +3 -3
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +15 -25
- package/src/test-globals.ts +9 -27
- package/src/test-joins.ts +6 -8
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/shell.ts +161 -0
- package/src/{yaml-document.ts → text/yaml.ts} +46 -0
- package/src/type-errors.ts +25 -36
- package/src/unseen-loads.ts +1 -12
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +32 -42
- package/src/verbs/describe.ts +4 -4
- package/src/verbs/explain.ts +8 -10
- package/src/verbs/print-projects.ts +4 -3
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/verb.ts +2 -2
- package/src/verbs/version.ts +2 -1
- package/src/workflow-steps.ts +9 -44
- package/src/workspace-manifests.ts +2 -2
- package/src/workspace-programs.ts +5 -3
- package/src/workspace-resolution.ts +21 -17
- package/src/workspace.ts +35 -121
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
package/src/report-schema.ts
CHANGED
|
@@ -1,309 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Holds a report to `contract/report.schema.json` and `contract/finding.schema.json`
|
|
3
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.
|
|
5
|
-
* Contract's schemas use; the schemas' patterns are written to match alike in RE2 and in
|
|
6
|
-
* ECMAScript.
|
|
4
|
+
* this analyzer never writes a document a reader refuses.
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
7
|
import { REPORT_SCHEMAS, type Report } from "./contract/report.generated.ts";
|
|
10
|
-
|
|
11
|
-
/** One value a schema refuses: its JSON Pointer, the keyword, and why. */
|
|
12
|
-
interface SchemaRefusal {
|
|
13
|
-
readonly pointer: string;
|
|
14
|
-
readonly keyword: string;
|
|
15
|
-
readonly reason: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type Schema = Readonly<Record<string, unknown>>;
|
|
19
|
-
type Documents = Readonly<Record<string, Schema>>;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
23
|
-
* another fails every check rather than passing values the keyword would refuse.
|
|
24
|
-
*/
|
|
25
|
-
const KEYWORDS: ReadonlySet<string> = new Set([
|
|
26
|
-
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
27
|
-
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
28
|
-
...["properties", "additionalProperties", "required", "minProperties"],
|
|
29
|
-
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
30
|
-
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const patterns = new Map<string, RegExp>();
|
|
34
|
-
|
|
35
|
-
function pattern(source: string): RegExp {
|
|
36
|
-
let held = patterns.get(source);
|
|
37
|
-
if (held === undefined) {
|
|
38
|
-
held = new RegExp(source, "u");
|
|
39
|
-
patterns.set(source, held);
|
|
40
|
-
}
|
|
41
|
-
return held;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
45
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function typeOf(value: unknown): string {
|
|
49
|
-
if (value === null) {
|
|
50
|
-
return "null";
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
return "array";
|
|
54
|
-
}
|
|
55
|
-
if (typeof value === "number") {
|
|
56
|
-
return Number.isInteger(value) ? "integer" : "number";
|
|
57
|
-
}
|
|
58
|
-
return typeof value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function hasType(value: unknown, name: string): boolean {
|
|
62
|
-
const held = typeOf(value);
|
|
63
|
-
return held === name || (name === "number" && held === "integer");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
67
|
-
function canonical(value: unknown): string {
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
return `[${value.map(canonical).join(",")}]`;
|
|
70
|
-
}
|
|
71
|
-
if (isObject(value)) {
|
|
72
|
-
return `{${Object.keys(value)
|
|
73
|
-
.sort()
|
|
74
|
-
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
75
|
-
.join(",")}}`;
|
|
76
|
-
}
|
|
77
|
-
return JSON.stringify(value);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
81
|
-
function codePoints(value: string): number {
|
|
82
|
-
let counted = 0;
|
|
83
|
-
for (const _point of value) {
|
|
84
|
-
counted += 1;
|
|
85
|
-
}
|
|
86
|
-
return counted;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const escape = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
90
|
-
|
|
91
|
-
/** A schema evaluated in the document it is read from, so a relative `$ref` resolves there. */
|
|
92
|
-
class Checker {
|
|
93
|
-
private readonly documents: Documents;
|
|
94
|
-
|
|
95
|
-
constructor(documents: Documents) {
|
|
96
|
-
this.documents = documents;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
resolve(doc: string, ref: string): readonly [string, Schema] {
|
|
100
|
-
const [file = "", pointer = ""] = ref.split("#");
|
|
101
|
-
const target = file === "" ? doc : file;
|
|
102
|
-
let schema: unknown = this.documents[target];
|
|
103
|
-
for (const token of pointer.split("/").filter((one) => one !== "")) {
|
|
104
|
-
schema = isObject(schema)
|
|
105
|
-
? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
106
|
-
: undefined;
|
|
107
|
-
}
|
|
108
|
-
if (!isObject(schema)) {
|
|
109
|
-
throw new Error(`report schema: ${ref} names no schema`);
|
|
110
|
-
}
|
|
111
|
-
return [target, schema];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
matches(doc: string, schema: Schema, value: unknown, at: string): boolean {
|
|
115
|
-
const found: SchemaRefusal[] = [];
|
|
116
|
-
this.check(doc, schema, value, at, found);
|
|
117
|
-
return found.length === 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
check(doc: string, schema: Schema, value: unknown, at: string, found: SchemaRefusal[]): void {
|
|
121
|
-
for (const keyword of Object.keys(schema)) {
|
|
122
|
-
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`report schema: the keyword ${keyword} is not one this validator evaluates`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const refuse = (keyword: string, reason: string, pointer = at): void => {
|
|
129
|
-
found.push({ pointer, keyword, reason });
|
|
130
|
-
};
|
|
131
|
-
if (typeof schema["$ref"] === "string") {
|
|
132
|
-
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
133
|
-
this.check(target, referenced, value, at, found);
|
|
134
|
-
}
|
|
135
|
-
const types = schema["type"];
|
|
136
|
-
const named = types === undefined ? [] : Array.isArray(types) ? types : [types];
|
|
137
|
-
if (named.length > 0 && !named.some((one) => hasType(value, String(one)))) {
|
|
138
|
-
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.checkValue(schema, value, refuse);
|
|
142
|
-
if (Array.isArray(value)) {
|
|
143
|
-
this.checkArray(doc, schema, value, at, found, refuse);
|
|
144
|
-
} else if (isObject(value)) {
|
|
145
|
-
this.checkObject(doc, schema, value, at, found, refuse);
|
|
146
|
-
}
|
|
147
|
-
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private checkValue(
|
|
151
|
-
schema: Schema,
|
|
152
|
-
value: unknown,
|
|
153
|
-
refuse: (keyword: string, reason: string) => void,
|
|
154
|
-
): void {
|
|
155
|
-
const values = schema["enum"];
|
|
156
|
-
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
157
|
-
refuse("enum", `${canonical(value)} is not one of the values the schema lists`);
|
|
158
|
-
}
|
|
159
|
-
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
160
|
-
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
161
|
-
}
|
|
162
|
-
if (typeof value === "string") {
|
|
163
|
-
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
164
|
-
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
165
|
-
}
|
|
166
|
-
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
167
|
-
refuse(
|
|
168
|
-
"minLength",
|
|
169
|
-
`${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (typeof value === "number") {
|
|
174
|
-
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
175
|
-
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
176
|
-
}
|
|
177
|
-
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
178
|
-
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private checkObject(
|
|
184
|
-
doc: string,
|
|
185
|
-
schema: Schema,
|
|
186
|
-
value: Readonly<Record<string, unknown>>,
|
|
187
|
-
at: string,
|
|
188
|
-
found: SchemaRefusal[],
|
|
189
|
-
refuse: (keyword: string, reason: string) => void,
|
|
190
|
-
): void {
|
|
191
|
-
const properties = isObject(schema["properties"]) ? schema["properties"] : {};
|
|
192
|
-
const additional = schema["additionalProperties"];
|
|
193
|
-
for (const name of Object.keys(value).sort()) {
|
|
194
|
-
const member = `${at}/${escape(name)}`;
|
|
195
|
-
const declared = properties[name];
|
|
196
|
-
if (isObject(declared)) {
|
|
197
|
-
this.check(doc, declared, value[name], member, found);
|
|
198
|
-
} else if (additional === false) {
|
|
199
|
-
refuse(
|
|
200
|
-
"additionalProperties",
|
|
201
|
-
`the member ${JSON.stringify(name)} is not one the schema declares`,
|
|
202
|
-
);
|
|
203
|
-
} else if (isObject(additional)) {
|
|
204
|
-
this.check(doc, additional, value[name], member, found);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
for (const name of Array.isArray(schema["required"]) ? (schema["required"] as string[]) : []) {
|
|
208
|
-
if (!Object.hasOwn(value, name)) {
|
|
209
|
-
refuse("required", `the member ${JSON.stringify(name)} is absent`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const fewest = schema["minProperties"];
|
|
213
|
-
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
214
|
-
refuse(
|
|
215
|
-
"minProperties",
|
|
216
|
-
`the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private checkArray(
|
|
222
|
-
doc: string,
|
|
223
|
-
schema: Schema,
|
|
224
|
-
value: readonly unknown[],
|
|
225
|
-
at: string,
|
|
226
|
-
found: SchemaRefusal[],
|
|
227
|
-
refuse: (keyword: string, reason: string) => void,
|
|
228
|
-
): void {
|
|
229
|
-
const items = schema["items"];
|
|
230
|
-
if (isObject(items)) {
|
|
231
|
-
value.forEach((item, i) => {
|
|
232
|
-
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
236
|
-
refuse(
|
|
237
|
-
"minItems",
|
|
238
|
-
`the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
242
|
-
refuse(
|
|
243
|
-
"maxItems",
|
|
244
|
-
`the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
248
|
-
refuse("uniqueItems", "the array lists one value twice");
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private checkApplicators(
|
|
253
|
-
doc: string,
|
|
254
|
-
schema: Schema,
|
|
255
|
-
value: unknown,
|
|
256
|
-
at: string,
|
|
257
|
-
found: SchemaRefusal[],
|
|
258
|
-
refuse: (keyword: string, reason: string) => void,
|
|
259
|
-
): void {
|
|
260
|
-
const list = (keyword: string): readonly Schema[] =>
|
|
261
|
-
Array.isArray(schema[keyword]) ? (schema[keyword] as Schema[]) : [];
|
|
262
|
-
for (const each of list("allOf")) {
|
|
263
|
-
this.check(doc, each, value, at, found);
|
|
264
|
-
}
|
|
265
|
-
const matching = (keyword: string): number =>
|
|
266
|
-
list(keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
267
|
-
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
268
|
-
refuse("anyOf", "the value matches none of the alternatives");
|
|
269
|
-
}
|
|
270
|
-
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
271
|
-
refuse(
|
|
272
|
-
"oneOf",
|
|
273
|
-
`the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
if (isObject(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
277
|
-
refuse("not", "the value matches a schema it must not match");
|
|
278
|
-
}
|
|
279
|
-
if (isObject(schema["if"])) {
|
|
280
|
-
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
281
|
-
if (isObject(branch)) {
|
|
282
|
-
this.check(doc, branch, value, at, found);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
290
|
-
* at one of them: what it refuses in a value, none for an instance.
|
|
291
|
-
*/
|
|
292
|
-
export function schemaRefusals(
|
|
293
|
-
documents: Documents,
|
|
294
|
-
root: string,
|
|
295
|
-
): (value: unknown) => readonly SchemaRefusal[] {
|
|
296
|
-
const checker = new Checker(documents);
|
|
297
|
-
const top = documents[root];
|
|
298
|
-
if (top === undefined) {
|
|
299
|
-
throw new Error(`the schema set holds no document ${root}`);
|
|
300
|
-
}
|
|
301
|
-
return (value) => {
|
|
302
|
-
const found: SchemaRefusal[] = [];
|
|
303
|
-
checker.check(root, top, value, "", found);
|
|
304
|
-
return found;
|
|
305
|
-
};
|
|
306
|
-
}
|
|
8
|
+
import { schemaRefusals, type SchemaRefusal } from "./schema.ts";
|
|
307
9
|
|
|
308
10
|
const reportSchema = schemaRefusals(REPORT_SCHEMAS, "report.schema.json");
|
|
309
11
|
|
|
@@ -323,7 +25,7 @@ function listingRefusals(report: Report): readonly SchemaRefusal[] {
|
|
|
323
25
|
return [];
|
|
324
26
|
}
|
|
325
27
|
const refuse = (pointer: string, reason: string): SchemaRefusal[] => [
|
|
326
|
-
{ pointer, keyword: "components", reason },
|
|
28
|
+
{ pointer, keyword: "components", reason, member: "" },
|
|
327
29
|
];
|
|
328
30
|
const listed = new Set<string>();
|
|
329
31
|
for (const [i, entry] of report.components.entries()) {
|
package/src/report.ts
CHANGED
|
@@ -19,12 +19,8 @@ import type { AppliedConvention } from "./conventions.ts";
|
|
|
19
19
|
import type { PassResult } from "./findings-pass.ts";
|
|
20
20
|
import type { StaleSuppression } from "./findings/self-check.ts";
|
|
21
21
|
import type { TestFileRule } from "./references.ts";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export const ANALYZER_NAME = "deadset-ts";
|
|
25
|
-
|
|
26
|
-
/** The language this analyzer claims. */
|
|
27
|
-
export const LANGUAGE = "ts";
|
|
22
|
+
import { bytewise } from "./text/compare.ts";
|
|
23
|
+
import { LANGUAGE, PRODUCT } from "./product.ts";
|
|
28
24
|
|
|
29
25
|
/** One project configuration of the matrix a run analyzed. */
|
|
30
26
|
export interface ReportConfiguration {
|
|
@@ -131,14 +127,6 @@ function componentsOf(
|
|
|
131
127
|
return [...listed.values()];
|
|
132
128
|
}
|
|
133
129
|
|
|
134
|
-
/** Two strings ordered bytewise. */
|
|
135
|
-
function compare(a: string, b: string): number {
|
|
136
|
-
if (a === b) {
|
|
137
|
-
return 0;
|
|
138
|
-
}
|
|
139
|
-
return a < b ? -1 : 1;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
130
|
function wirePosition(position: FindingPosition): wire.Position {
|
|
143
131
|
return {
|
|
144
132
|
path: position.path,
|
|
@@ -257,22 +245,22 @@ function wireStale(stale: StaleSuppression): wire.StaleSuppressionsEntry {
|
|
|
257
245
|
/** The canonical key over two findings: path, line, column, code, then the symbol reference. */
|
|
258
246
|
function canonical(a: wire.Finding, b: wire.Finding): number {
|
|
259
247
|
return (
|
|
260
|
-
|
|
248
|
+
bytewise(a.position.path, b.position.path) ||
|
|
261
249
|
a.position.line - b.position.line ||
|
|
262
250
|
a.position.column - b.position.column ||
|
|
263
|
-
|
|
264
|
-
|
|
251
|
+
bytewise(a.code, b.code) ||
|
|
252
|
+
bytewise(a.symbol.ref, b.symbol.ref)
|
|
265
253
|
);
|
|
266
254
|
}
|
|
267
255
|
|
|
268
256
|
/** The canonical key over two stale suppressions, each component from the record's own member. */
|
|
269
257
|
function canonicalStale(a: wire.StaleSuppressionsEntry, b: wire.StaleSuppressionsEntry): number {
|
|
270
258
|
return (
|
|
271
|
-
|
|
259
|
+
bytewise(a.position.path, b.position.path) ||
|
|
272
260
|
a.position.line - b.position.line ||
|
|
273
261
|
a.position.column - b.position.column ||
|
|
274
|
-
|
|
275
|
-
|
|
262
|
+
bytewise(a.code, b.code) ||
|
|
263
|
+
bytewise(a.symbol, b.symbol)
|
|
276
264
|
);
|
|
277
265
|
}
|
|
278
266
|
|
|
@@ -289,9 +277,9 @@ function wireGap(gap: DeclaredGap): wire.DeclaredGapsEntry {
|
|
|
289
277
|
/** Edge evaluations by edge, then side, then their compact encodings. */
|
|
290
278
|
function byEdge(a: wire.EdgeEvaluationsEntry, b: wire.EdgeEvaluationsEntry): number {
|
|
291
279
|
return (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
280
|
+
bytewise(a.edge, b.edge) ||
|
|
281
|
+
bytewise(a.side, b.side) ||
|
|
282
|
+
bytewise(JSON.stringify(a), JSON.stringify(b))
|
|
295
283
|
);
|
|
296
284
|
}
|
|
297
285
|
|
|
@@ -346,16 +334,16 @@ export function buildReport(input: ReportInput): wire.Report {
|
|
|
346
334
|
one.finding === undefined ? [] : [[wireFinding(one.finding), one.finding] as const],
|
|
347
335
|
);
|
|
348
336
|
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
349
|
-
.sort((a, b) =>
|
|
337
|
+
.sort((a, b) => bytewise(a.id, b.id))
|
|
350
338
|
.map((one) => ({ id: one.id, role: "consumer" as const, reason: one.reason }));
|
|
351
339
|
const loaded = [...input.loaded]
|
|
352
|
-
.sort((a, b) =>
|
|
340
|
+
.sort((a, b) => bytewise(a.id, b.id) || bytewise(a.path, b.path))
|
|
353
341
|
.map((one) => ({ id: one.id, role: "consumer" as const, path: one.path }));
|
|
354
342
|
return {
|
|
355
343
|
schema_version: SCHEMA_VERSION,
|
|
356
344
|
contract_version: input.contractVersion,
|
|
357
345
|
analyzer: {
|
|
358
|
-
name:
|
|
346
|
+
name: PRODUCT,
|
|
359
347
|
version: input.version,
|
|
360
348
|
languages: [LANGUAGE],
|
|
361
349
|
schema_versions_accepted: SCHEMA_VERSIONS,
|
|
@@ -367,10 +355,10 @@ export function buildReport(input: ReportInput): wire.Report {
|
|
|
367
355
|
},
|
|
368
356
|
target: { kind: input.target.kind, root: input.target.root, identity: input.target.identity },
|
|
369
357
|
configurations: [...input.configurations]
|
|
370
|
-
.sort((a, b) =>
|
|
358
|
+
.sort((a, b) => bytewise(a.id, b.id))
|
|
371
359
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
372
360
|
configurations_not_built: [...input.notBuilt]
|
|
373
|
-
.sort((a, b) =>
|
|
361
|
+
.sort((a, b) => bytewise(a.id, b.id))
|
|
374
362
|
.map((one) => ({ id: one.id, project: one.project, error: one.error })),
|
|
375
363
|
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
376
364
|
findings,
|
|
@@ -383,10 +371,10 @@ export function buildReport(input: ReportInput): wire.Report {
|
|
|
383
371
|
// name, so the compact encodings alone order them.
|
|
384
372
|
declared_gaps: input.declaredGaps
|
|
385
373
|
.map(wireGap)
|
|
386
|
-
.sort((a, b) =>
|
|
374
|
+
.sort((a, b) => bytewise(JSON.stringify(a), JSON.stringify(b))),
|
|
387
375
|
excluded_by_cgo: [],
|
|
388
376
|
test_file_rules: [...input.testFileRules]
|
|
389
|
-
.sort((a, b) =>
|
|
377
|
+
.sort((a, b) => bytewise(a.rule, b.rule) || a.matched - b.matched)
|
|
390
378
|
.map((one) => ({ rule: one.rule, matched: one.matched })),
|
|
391
379
|
type_error_skips: [
|
|
392
380
|
...new Map(
|
|
@@ -396,16 +384,18 @@ export function buildReport(input: ReportInput): wire.Report {
|
|
|
396
384
|
}),
|
|
397
385
|
),
|
|
398
386
|
]
|
|
399
|
-
.sort(([a, x], [b, y]) =>
|
|
387
|
+
.sort(([a, x], [b, y]) => bytewise(x.path, y.path) || x.line - y.line || bytewise(a, b))
|
|
400
388
|
.map(([, skip]) => skip),
|
|
401
389
|
notes: [...new Map((input.notes ?? []).map((one) => [JSON.stringify(one), one])).entries()]
|
|
402
|
-
.sort(
|
|
390
|
+
.sort(
|
|
391
|
+
([a, x], [b, y]) => bytewise(x.kind, y.kind) || bytewise(x.path, y.path) || bytewise(a, b),
|
|
392
|
+
)
|
|
403
393
|
.map(([, note]) => note),
|
|
404
394
|
unanswered_questions: [...input.unanswered]
|
|
405
395
|
.sort(
|
|
406
396
|
(a, b) =>
|
|
407
|
-
|
|
408
|
-
|
|
397
|
+
bytewise(a.configuration, b.configuration) ||
|
|
398
|
+
bytewise(JSON.stringify(a), JSON.stringify(b)),
|
|
409
399
|
)
|
|
410
400
|
.map((one) => ({
|
|
411
401
|
configuration: one.configuration,
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
|
12
12
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
13
13
|
import { utf8Bytes } from "./sha256.ts";
|
|
14
14
|
import { withheldLine } from "./text.ts";
|
|
15
|
+
import { bytewise } from "../text/compare.ts";
|
|
16
|
+
import { message } from "../product.ts";
|
|
15
17
|
|
|
16
18
|
const SCHEMA =
|
|
17
19
|
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
@@ -133,9 +135,7 @@ function lineHashCache(read: (path: string) => string): (at: Located) => string
|
|
|
133
135
|
try {
|
|
134
136
|
text = read(at.path);
|
|
135
137
|
} catch (error: unknown) {
|
|
136
|
-
throw new RenderError(
|
|
137
|
-
`read ${at.path} for its line fingerprint: ${error instanceof Error ? error.message : String(error)}`,
|
|
138
|
-
);
|
|
138
|
+
throw new RenderError(`read ${at.path} for its line fingerprint: ${message(error)}`);
|
|
139
139
|
}
|
|
140
140
|
hashes = lineHashes(text);
|
|
141
141
|
held.set(at.path, hashes);
|
|
@@ -292,7 +292,7 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
|
|
|
292
292
|
...report.findings.map((found) => findingResult(found, index, hashOf, members)),
|
|
293
293
|
...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
|
|
294
294
|
];
|
|
295
|
-
const languages = [...report.analyzer.languages].sort().join("+");
|
|
295
|
+
const languages = [...report.analyzer.languages].sort(bytewise).join("+");
|
|
296
296
|
const withheld = withheldLine(report.totals);
|
|
297
297
|
const log = {
|
|
298
298
|
$schema: SCHEMA,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { CHUNK_LENGTH } from "../json-chunks.ts";
|
|
11
11
|
import { RenderError } from "./reporter.ts";
|
|
12
12
|
import { utf8Bytes } from "./sha256.ts";
|
|
13
|
+
import { bytewise } from "../text/compare.ts";
|
|
13
14
|
|
|
14
15
|
/** A template that does not parse, which refuses the invocation before any analysis. */
|
|
15
16
|
export class TemplateError extends Error {
|
|
@@ -618,7 +619,7 @@ function formatted(value: Value, nested: boolean): string {
|
|
|
618
619
|
}
|
|
619
620
|
const object: ValueObject = value;
|
|
620
621
|
const entries = Object.keys(object)
|
|
621
|
-
.sort()
|
|
622
|
+
.sort(bytewise)
|
|
622
623
|
.map((key) => `${key}:${formatted(object[key] ?? null, true)}`);
|
|
623
624
|
return `map[${entries.join(" ")}]`;
|
|
624
625
|
}
|
|
@@ -647,28 +648,11 @@ function compareBasic(name: string, a: Value, b: Value): number {
|
|
|
647
648
|
return Number.NaN;
|
|
648
649
|
}
|
|
649
650
|
if (kind === "string") {
|
|
650
|
-
return
|
|
651
|
+
return bytewise(a as string, b as string);
|
|
651
652
|
}
|
|
652
653
|
return (a as number) < (b as number) ? -1 : 1;
|
|
653
654
|
}
|
|
654
655
|
|
|
655
|
-
/**
|
|
656
|
-
* Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
|
|
657
|
-
* code points: the order of their UTF-16 code units differs above U+FFFF.
|
|
658
|
-
*/
|
|
659
|
-
function byCodePoints(a: string, b: string): number {
|
|
660
|
-
let at = 0;
|
|
661
|
-
while (at < a.length && at < b.length) {
|
|
662
|
-
const x = a.codePointAt(at) ?? 0;
|
|
663
|
-
const y = b.codePointAt(at) ?? 0;
|
|
664
|
-
if (x !== y) {
|
|
665
|
-
return x < y ? -1 : 1;
|
|
666
|
-
}
|
|
667
|
-
at += x > 0xffff ? 2 : 1;
|
|
668
|
-
}
|
|
669
|
-
return a.length - b.length;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
656
|
/** The escapes `%q` writes for the control characters that have one of their own. */
|
|
673
657
|
const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
|
|
674
658
|
'"': '\\"',
|
|
@@ -1022,7 +1006,7 @@ class Execution {
|
|
|
1022
1006
|
entries = value.map((one, i) => [i, one]);
|
|
1023
1007
|
} else if (isObject(value)) {
|
|
1024
1008
|
entries = Object.keys(value)
|
|
1025
|
-
.sort()
|
|
1009
|
+
.sort(bytewise)
|
|
1026
1010
|
.map((key) => [key, value[key] ?? null]);
|
|
1027
1011
|
} else {
|
|
1028
1012
|
throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
|