@cplieger/deadset-ts 8.0.0-dev.5 → 8.0.0-dev.7
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 +3 -2
- package/dist/src/analysis.js +28 -46
- 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 +3 -2
- package/dist/src/configuration-files.js +27 -168
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1084 -0
- package/dist/src/contract/report.generated.js +796 -0
- package/dist/src/conventions.js +20 -60
- package/dist/src/dependencies.js +25 -43
- package/dist/src/discover.js +13 -39
- package/dist/src/edges.js +7 -15
- 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 +3 -63
- package/dist/src/exempt.js +13 -40
- package/dist/src/file-facts.js +2 -25
- 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 +11 -9
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +28 -27
- package/dist/src/findings/visibility-narrowing.js +9 -11
- package/dist/src/findings-pass.js +7 -14
- 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/index.js +1 -1
- package/dist/src/intra-function-parts.js +18 -26
- package/dist/src/inventory.js +2 -21
- package/dist/src/json-document.js +4 -0
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +4 -360
- 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 +3 -2
- package/dist/src/product.js +13 -0
- package/dist/src/references.js +14 -27
- package/dist/src/report-schema.js +272 -0
- package/dist/src/report.js +39 -42
- package/dist/src/reporters/sarif.js +4 -2
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +12 -26
- package/dist/src/roots.js +7 -17
- package/dist/src/run.js +10 -8
- package/dist/src/schema.js +4 -3
- package/dist/src/scope.js +4 -5
- package/dist/src/serialization-contract.js +4 -8
- package/dist/src/session.js +2 -1
- package/dist/src/setup-failure.js +2 -6
- package/dist/src/suppress-file.js +12 -13
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +29 -48
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +5 -15
- 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/verbs/analyze.js +22 -15
- package/dist/src/verbs/describe.js +7 -7
- package/dist/src/verbs/explain.js +7 -4
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +4 -2
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +4 -3
- 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 +2 -1
- package/src/analysis.ts +29 -54
- 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 +11 -15
- package/src/configuration-files.ts +27 -170
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1148 -0
- package/src/contract/report.generated.ts +1157 -0
- package/src/conventions.ts +26 -74
- package/src/dependencies.ts +32 -50
- package/src/discover.ts +14 -39
- package/src/edges.ts +7 -21
- 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 +5 -74
- package/src/exempt.ts +17 -47
- package/src/file-facts.ts +6 -24
- 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 +15 -12
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +28 -27
- package/src/findings/visibility-narrowing.ts +9 -12
- package/src/findings-pass.ts +7 -16
- package/src/fs/manifest.ts +52 -0
- package/src/fs/tree.ts +110 -0
- package/src/git-ignore.ts +38 -25
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/intra-function-parts.ts +18 -28
- package/src/inventory.ts +2 -20
- package/src/json-document.ts +5 -0
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +6 -406
- 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 +3 -2
- package/src/product.ts +18 -0
- package/src/references.ts +14 -29
- package/src/report-schema.ts +364 -0
- package/src/report.ts +66 -229
- 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 +17 -12
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +31 -38
- package/src/roots.ts +7 -20
- package/src/run.ts +10 -14
- package/src/schema.ts +4 -3
- package/src/scope.ts +4 -10
- package/src/serialization-contract.ts +4 -9
- package/src/session.ts +2 -1
- package/src/setup-failure.ts +5 -7
- package/src/suppress-file.ts +11 -15
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +39 -55
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +5 -18
- 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/verbs/analyze.ts +38 -25
- package/src/verbs/describe.ts +13 -7
- package/src/verbs/explain.ts +7 -4
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +4 -2
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +0 -6
- package/src/verbs/version.ts +4 -3
- 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/dist/src/version.js +0 -7
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/version.ts +0 -7
package/src/paths.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Filesystem paths
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* They are written here rather than taken from the platform's own library because
|
|
6
|
-
* every module below the command line stays free of the platform, and because the
|
|
7
|
-
* supported platform set is one, so a second spelling would be a rule nothing
|
|
8
|
-
* exercises.
|
|
2
|
+
* Filesystem paths as the supported platform spells them: a solidus between segments, a
|
|
3
|
+
* leading one for an absolute path. They are written here rather than taken from the
|
|
4
|
+
* platform's library so every module below the command line stays free of the platform.
|
|
9
5
|
*/
|
|
10
6
|
|
|
11
7
|
const SEPARATOR = "/";
|
|
@@ -67,6 +63,25 @@ export function dirnamePath(path: string): string {
|
|
|
67
63
|
return cut === 0 ? SEPARATOR : normalized.slice(0, cut);
|
|
68
64
|
}
|
|
69
65
|
|
|
66
|
+
/**
|
|
67
|
+
* `dir` and every directory above it, nearest first, normalized. With `top`, the list ends
|
|
68
|
+
* at `top` and is empty where `dir` is neither `top` nor below it; without, it ends at the
|
|
69
|
+
* root of the path.
|
|
70
|
+
*/
|
|
71
|
+
export function ancestors(dir: string, top?: string): readonly string[] {
|
|
72
|
+
const end = top === undefined ? undefined : normalizePath(top);
|
|
73
|
+
const found: string[] = [];
|
|
74
|
+
for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
|
|
75
|
+
if (end !== undefined && at !== end && relativePath(end, at) === undefined) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
found.push(at);
|
|
79
|
+
if (at === end || dirnamePath(at) === at) {
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
/**
|
|
71
86
|
* The extension of one path's last segment, from its last full stop, or the empty string
|
|
72
87
|
* where it has none. A segment that only starts with a full stop has none.
|
|
@@ -101,14 +116,11 @@ export function queriedModulePath(specifier: string): string | undefined {
|
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
/**
|
|
104
|
-
* One path as it reads below a root, or undefined when it is not below the root
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* The caller resolves both against the same directory before calling: a relative
|
|
111
|
-
* root and an absolute path are two spellings this cannot reconcile.
|
|
119
|
+
* One path as it reads below a root, or undefined when it is not strictly below the root:
|
|
120
|
+
* the root itself is not below itself. Both are normalized, then compared lexically, so a
|
|
121
|
+
* root and a path spelled through different symbolic links name nothing in common. The
|
|
122
|
+
* caller resolves both against the same directory first: a relative root and an absolute
|
|
123
|
+
* path cannot be reconciled.
|
|
112
124
|
*/
|
|
113
125
|
export function relativePath(root: string, path: string): string | undefined {
|
|
114
126
|
const at = normalizePath(root);
|
package/src/position.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SourceFile } from "@typescript/native/ast";
|
|
2
2
|
import { relativePath } from "./paths.ts";
|
|
3
|
+
import { bytewise } from "./text/compare.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Where one declaration is written, as every report renders it.
|
|
@@ -87,7 +88,7 @@ export function positionKey(position: Position): string {
|
|
|
87
88
|
*/
|
|
88
89
|
export function byPosition(a: Position, b: Position): number {
|
|
89
90
|
if (a.path !== b.path) {
|
|
90
|
-
return a.path
|
|
91
|
+
return bytewise(a.path, b.path);
|
|
91
92
|
}
|
|
92
93
|
if (a.line !== b.line) {
|
|
93
94
|
return a.line - b.line;
|
package/src/print.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type Provenance,
|
|
6
6
|
type Provider,
|
|
7
7
|
} from "./config.ts";
|
|
8
|
+
import { bytewise } from "./text/compare.ts";
|
|
8
9
|
|
|
9
10
|
/** One declaration entry as a document writes it: the members of its shape alone. */
|
|
10
11
|
function declarationOf(entry: DeclarationEntry): Record<string, string> {
|
|
@@ -61,7 +62,7 @@ export function printConfig(config: Config, provenance: Provenance): string {
|
|
|
61
62
|
},
|
|
62
63
|
consumers: { complete: config.consumersComplete },
|
|
63
64
|
roots: { patterns: config.rootPatterns },
|
|
64
|
-
severity: Object.fromEntries([...config.severity].sort(([a], [b]) => (a
|
|
65
|
+
severity: Object.fromEntries([...config.severity].sort(([a], [b]) => bytewise(a, b))),
|
|
65
66
|
exemptions: { disabled: config.exemptionsDisabled },
|
|
66
67
|
reporters: {
|
|
67
68
|
formats: config.reporters.formats,
|
|
@@ -88,7 +89,7 @@ export function printConfig(config: Config, provenance: Provenance): string {
|
|
|
88
89
|
},
|
|
89
90
|
provenance: Object.fromEntries(
|
|
90
91
|
[...provenance]
|
|
91
|
-
.sort(([a], [b]) => (a
|
|
92
|
+
.sort(([a], [b]) => bytewise(a, b))
|
|
92
93
|
.map(([path, origin]) => [path, renderOrigin(origin)]),
|
|
93
94
|
),
|
|
94
95
|
};
|
package/src/product.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LANGUAGE_ROWS } from "./contract/contract.generated.ts";
|
|
2
|
+
import type { Writer } from "./run.ts";
|
|
3
|
+
|
|
4
|
+
/** The language this analyzer reads. */
|
|
5
|
+
export const LANGUAGE = "ts";
|
|
6
|
+
|
|
7
|
+
/** The analyzer's name: the one its language's row names, which every output spells it with. */
|
|
8
|
+
export const PRODUCT = LANGUAGE_ROWS[LANGUAGE].analyzer;
|
|
9
|
+
|
|
10
|
+
/** Writes one diagnostic line to the error stream, named for the analyzer. */
|
|
11
|
+
export function diagnose(err: Writer, text: string): void {
|
|
12
|
+
err.write(`${PRODUCT}: ${text}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The text one thrown value carries. */
|
|
16
|
+
export function message(error: unknown): string {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
package/src/references.ts
CHANGED
|
@@ -38,13 +38,13 @@ import {
|
|
|
38
38
|
type ParameterDeclaration,
|
|
39
39
|
type SourceFile,
|
|
40
40
|
} from "@typescript/native/ast";
|
|
41
|
+
import { parenthesesRemoved } from "./unwrap.ts";
|
|
41
42
|
import type { Symbol as TSSymbol } from "@typescript/native/sync";
|
|
42
43
|
import { aliasChains, type ChainTarget } from "./alias-chain.ts";
|
|
43
44
|
import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.ts";
|
|
44
45
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
45
46
|
import { specifierCandidates } from "./configuration-files.ts";
|
|
46
47
|
import { namespaceUses } from "./namespace-objects.ts";
|
|
47
|
-
import { globExpression } from "./glob.ts";
|
|
48
48
|
import {
|
|
49
49
|
declarationsByName,
|
|
50
50
|
isParameterProperty,
|
|
@@ -73,6 +73,8 @@ import {
|
|
|
73
73
|
type KeyedAccess,
|
|
74
74
|
} from "./symbol-keys.ts";
|
|
75
75
|
import { handedOn, valuesOf } from "./value-flow.ts";
|
|
76
|
+
import { bytewise } from "./text/compare.ts";
|
|
77
|
+
import { globExpression } from "./text/glob.ts";
|
|
76
78
|
|
|
77
79
|
/**
|
|
78
80
|
* Every use one project's own files make of the project's own declarations.
|
|
@@ -315,15 +317,6 @@ function isName(node: Node): boolean {
|
|
|
315
317
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
316
318
|
}
|
|
317
319
|
|
|
318
|
-
/** The expression one node is, through however many parentheses are written around it. */
|
|
319
|
-
function unparenthesized(node: Node): Node {
|
|
320
|
-
let held = node;
|
|
321
|
-
while (isParenthesizedExpression(held)) {
|
|
322
|
-
held = held.expression;
|
|
323
|
-
}
|
|
324
|
-
return held;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
320
|
/**
|
|
328
321
|
* Records the name one store writes into.
|
|
329
322
|
*
|
|
@@ -333,7 +326,7 @@ function unparenthesized(node: Node): Node {
|
|
|
333
326
|
* target writes each name it binds, through its spreads and nested patterns.
|
|
334
327
|
*/
|
|
335
328
|
function markWrite(target: Node, writes: Set<number>, accesses: Set<Node>): void {
|
|
336
|
-
const held =
|
|
329
|
+
const held = parenthesesRemoved(target);
|
|
337
330
|
if (isName(held)) {
|
|
338
331
|
writes.add(held.pos);
|
|
339
332
|
return;
|
|
@@ -348,7 +341,7 @@ function markWrite(target: Node, writes: Set<number>, accesses: Set<Node>): void
|
|
|
348
341
|
accesses.add(held);
|
|
349
342
|
// A store through a member's value lands in storage every holder of the value
|
|
350
343
|
// reads, so it reads the member; a variable only ever stored into stays written.
|
|
351
|
-
if (!isPropertyAccessExpression(
|
|
344
|
+
if (!isPropertyAccessExpression(parenthesesRemoved(held.expression))) {
|
|
352
345
|
markWrite(held.expression, writes, accesses);
|
|
353
346
|
}
|
|
354
347
|
return;
|
|
@@ -524,7 +517,7 @@ interface FileSites {
|
|
|
524
517
|
|
|
525
518
|
/** Whether one node is `import.meta.url`. */
|
|
526
519
|
function isImportMetaURL(node: Node): boolean {
|
|
527
|
-
const at =
|
|
520
|
+
const at = parenthesesRemoved(node);
|
|
528
521
|
return (
|
|
529
522
|
isPropertyAccessExpression(at) &&
|
|
530
523
|
isMetaProperty(at.expression) &&
|
|
@@ -536,7 +529,7 @@ function isImportMetaURL(node: Node): boolean {
|
|
|
536
529
|
/** The module URL one node builds, where it builds one. */
|
|
537
530
|
function moduleURLOf(node: Node): ModuleURL | undefined {
|
|
538
531
|
const relative = (argument: Node | undefined): string | undefined => {
|
|
539
|
-
const at = argument === undefined ? undefined :
|
|
532
|
+
const at = argument === undefined ? undefined : parenthesesRemoved(argument);
|
|
540
533
|
return at !== undefined &&
|
|
541
534
|
(isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at)) &&
|
|
542
535
|
/^\.\.?\//u.test(at.text)
|
|
@@ -551,7 +544,7 @@ function moduleURLOf(node: Node): ModuleURL | undefined {
|
|
|
551
544
|
: undefined;
|
|
552
545
|
}
|
|
553
546
|
if (isCallExpression(node)) {
|
|
554
|
-
const callee =
|
|
547
|
+
const callee = parenthesesRemoved(node.expression);
|
|
555
548
|
const specifier = relative(node.arguments[0]);
|
|
556
549
|
if (
|
|
557
550
|
specifier !== undefined &&
|
|
@@ -869,7 +862,7 @@ export function testFileRulesOf(
|
|
|
869
862
|
};
|
|
870
863
|
}),
|
|
871
864
|
...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
|
|
872
|
-
].sort((a, b) =>
|
|
865
|
+
].sort((a, b) => bytewise(a.rule, b.rule));
|
|
873
866
|
}
|
|
874
867
|
|
|
875
868
|
const importedCache = new WeakMap<
|
|
@@ -1003,7 +996,7 @@ function aloneIn(
|
|
|
1003
996
|
nodes.every((node) => sameTarget.some((one) => inside(node, one.value))),
|
|
1004
997
|
)
|
|
1005
998
|
.map(([name]) => name)
|
|
1006
|
-
.sort(
|
|
999
|
+
.sort(bytewise);
|
|
1007
1000
|
if (names.length > 0) {
|
|
1008
1001
|
result.push({ index: write.index, names });
|
|
1009
1002
|
}
|
|
@@ -1011,14 +1004,6 @@ function aloneIn(
|
|
|
1011
1004
|
return result;
|
|
1012
1005
|
}
|
|
1013
1006
|
|
|
1014
|
-
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
1015
|
-
function compare(a: string, b: string): number {
|
|
1016
|
-
if (a === b) {
|
|
1017
|
-
return 0;
|
|
1018
|
-
}
|
|
1019
|
-
return a < b ? -1 : 1;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1007
|
/**
|
|
1023
1008
|
* Every reference one project's own files make to the project's own declarations, in
|
|
1024
1009
|
* position order, and the files the test-file rules classified.
|
|
@@ -1677,9 +1662,9 @@ export function references<Brand>(
|
|
|
1677
1662
|
found.sort(
|
|
1678
1663
|
(a, b) =>
|
|
1679
1664
|
byPosition(a.position, b.position) ||
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1665
|
+
bytewise(a.to, b.to) ||
|
|
1666
|
+
bytewise(a.from, b.from) ||
|
|
1667
|
+
bytewise(a.use, b.use),
|
|
1683
1668
|
);
|
|
1684
1669
|
|
|
1685
1670
|
return {
|
|
@@ -1687,7 +1672,7 @@ export function references<Brand>(
|
|
|
1687
1672
|
consumer === undefined
|
|
1688
1673
|
? found
|
|
1689
1674
|
: found.map((reference) => ({ ...reference, consumer: consumer.id })),
|
|
1690
|
-
testFilePaths: tests.sort(
|
|
1675
|
+
testFilePaths: tests.sort(bytewise),
|
|
1691
1676
|
cost: {
|
|
1692
1677
|
batched,
|
|
1693
1678
|
fileBatches,
|
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
|
|
9
|
+
import { REPORT_SCHEMAS, type Report } from "./contract/report.generated.ts";
|
|
10
|
+
import { bytewise } from "./text/compare.ts";
|
|
11
|
+
import { isRecord } from "./json-document.ts";
|
|
12
|
+
|
|
13
|
+
/** One value a schema refuses: its JSON Pointer, the keyword, and why. */
|
|
14
|
+
interface SchemaRefusal {
|
|
15
|
+
readonly pointer: string;
|
|
16
|
+
readonly keyword: string;
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Schema = Readonly<Record<string, unknown>>;
|
|
21
|
+
type Documents = Readonly<Record<string, Schema>>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
25
|
+
* another fails every check rather than passing values the keyword would refuse.
|
|
26
|
+
*/
|
|
27
|
+
const KEYWORDS: ReadonlySet<string> = new Set([
|
|
28
|
+
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
29
|
+
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
30
|
+
...["properties", "additionalProperties", "required", "minProperties"],
|
|
31
|
+
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
32
|
+
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const patterns = new Map<string, RegExp>();
|
|
36
|
+
|
|
37
|
+
function pattern(source: string): RegExp {
|
|
38
|
+
let held = patterns.get(source);
|
|
39
|
+
if (held === undefined) {
|
|
40
|
+
held = new RegExp(source, "u");
|
|
41
|
+
patterns.set(source, held);
|
|
42
|
+
}
|
|
43
|
+
return held;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function typeOf(value: unknown): string {
|
|
47
|
+
if (value === null) {
|
|
48
|
+
return "null";
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return "array";
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "number") {
|
|
54
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
55
|
+
}
|
|
56
|
+
return typeof value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasType(value: unknown, name: string): boolean {
|
|
60
|
+
const held = typeOf(value);
|
|
61
|
+
return held === name || (name === "number" && held === "integer");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
65
|
+
function canonical(value: unknown): string {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
68
|
+
}
|
|
69
|
+
if (isRecord(value)) {
|
|
70
|
+
return `{${Object.keys(value)
|
|
71
|
+
.sort(bytewise)
|
|
72
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
73
|
+
.join(",")}}`;
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
79
|
+
function codePoints(value: string): number {
|
|
80
|
+
let counted = 0;
|
|
81
|
+
for (const _point of value) {
|
|
82
|
+
counted += 1;
|
|
83
|
+
}
|
|
84
|
+
return counted;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const escape = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
88
|
+
|
|
89
|
+
/** A schema evaluated in the document it is read from, so a relative `$ref` resolves there. */
|
|
90
|
+
class Checker {
|
|
91
|
+
private readonly documents: Documents;
|
|
92
|
+
|
|
93
|
+
constructor(documents: Documents) {
|
|
94
|
+
this.documents = documents;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resolve(doc: string, ref: string): readonly [string, Schema] {
|
|
98
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
99
|
+
const target = file === "" ? doc : file;
|
|
100
|
+
let schema: unknown = this.documents[target];
|
|
101
|
+
for (const token of pointer.split("/").filter((one) => one !== "")) {
|
|
102
|
+
schema = isRecord(schema)
|
|
103
|
+
? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(schema)) {
|
|
107
|
+
throw new Error(`report schema: ${ref} names no schema`);
|
|
108
|
+
}
|
|
109
|
+
return [target, schema];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
matches(doc: string, schema: Schema, value: unknown, at: string): boolean {
|
|
113
|
+
const found: SchemaRefusal[] = [];
|
|
114
|
+
this.check(doc, schema, value, at, found);
|
|
115
|
+
return found.length === 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
check(doc: string, schema: Schema, value: unknown, at: string, found: SchemaRefusal[]): void {
|
|
119
|
+
for (const keyword of Object.keys(schema)) {
|
|
120
|
+
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`report schema: the keyword ${keyword} is not one this validator evaluates`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const refuse = (keyword: string, reason: string, pointer = at): void => {
|
|
127
|
+
found.push({ pointer, keyword, reason });
|
|
128
|
+
};
|
|
129
|
+
if (typeof schema["$ref"] === "string") {
|
|
130
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
131
|
+
this.check(target, referenced, value, at, found);
|
|
132
|
+
}
|
|
133
|
+
const types = schema["type"];
|
|
134
|
+
const named = types === undefined ? [] : Array.isArray(types) ? types : [types];
|
|
135
|
+
if (named.length > 0 && !named.some((one) => hasType(value, String(one)))) {
|
|
136
|
+
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.checkValue(schema, value, refuse);
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
this.checkArray(doc, schema, value, at, found, refuse);
|
|
142
|
+
} else if (isRecord(value)) {
|
|
143
|
+
this.checkObject(doc, schema, value, at, found, refuse);
|
|
144
|
+
}
|
|
145
|
+
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private checkValue(
|
|
149
|
+
schema: Schema,
|
|
150
|
+
value: unknown,
|
|
151
|
+
refuse: (keyword: string, reason: string) => void,
|
|
152
|
+
): void {
|
|
153
|
+
const values = schema["enum"];
|
|
154
|
+
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
155
|
+
refuse("enum", `${canonical(value)} is not one of the values the schema lists`);
|
|
156
|
+
}
|
|
157
|
+
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
158
|
+
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
159
|
+
}
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
162
|
+
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
165
|
+
refuse(
|
|
166
|
+
"minLength",
|
|
167
|
+
`${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === "number") {
|
|
172
|
+
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
173
|
+
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
176
|
+
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private checkObject(
|
|
182
|
+
doc: string,
|
|
183
|
+
schema: Schema,
|
|
184
|
+
value: Readonly<Record<string, unknown>>,
|
|
185
|
+
at: string,
|
|
186
|
+
found: SchemaRefusal[],
|
|
187
|
+
refuse: (keyword: string, reason: string) => void,
|
|
188
|
+
): void {
|
|
189
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
190
|
+
const additional = schema["additionalProperties"];
|
|
191
|
+
for (const name of Object.keys(value).sort(bytewise)) {
|
|
192
|
+
const member = `${at}/${escape(name)}`;
|
|
193
|
+
const declared = properties[name];
|
|
194
|
+
if (isRecord(declared)) {
|
|
195
|
+
this.check(doc, declared, value[name], member, found);
|
|
196
|
+
} else if (additional === false) {
|
|
197
|
+
refuse(
|
|
198
|
+
"additionalProperties",
|
|
199
|
+
`the member ${JSON.stringify(name)} is not one the schema declares`,
|
|
200
|
+
);
|
|
201
|
+
} else if (isRecord(additional)) {
|
|
202
|
+
this.check(doc, additional, value[name], member, found);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const name of Array.isArray(schema["required"]) ? (schema["required"] as string[]) : []) {
|
|
206
|
+
if (!Object.hasOwn(value, name)) {
|
|
207
|
+
refuse("required", `the member ${JSON.stringify(name)} is absent`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const fewest = schema["minProperties"];
|
|
211
|
+
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
212
|
+
refuse(
|
|
213
|
+
"minProperties",
|
|
214
|
+
`the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private checkArray(
|
|
220
|
+
doc: string,
|
|
221
|
+
schema: Schema,
|
|
222
|
+
value: readonly unknown[],
|
|
223
|
+
at: string,
|
|
224
|
+
found: SchemaRefusal[],
|
|
225
|
+
refuse: (keyword: string, reason: string) => void,
|
|
226
|
+
): void {
|
|
227
|
+
const items = schema["items"];
|
|
228
|
+
if (isRecord(items)) {
|
|
229
|
+
value.forEach((item, i) => {
|
|
230
|
+
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
234
|
+
refuse(
|
|
235
|
+
"minItems",
|
|
236
|
+
`the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
240
|
+
refuse(
|
|
241
|
+
"maxItems",
|
|
242
|
+
`the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
246
|
+
refuse("uniqueItems", "the array lists one value twice");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private checkApplicators(
|
|
251
|
+
doc: string,
|
|
252
|
+
schema: Schema,
|
|
253
|
+
value: unknown,
|
|
254
|
+
at: string,
|
|
255
|
+
found: SchemaRefusal[],
|
|
256
|
+
refuse: (keyword: string, reason: string) => void,
|
|
257
|
+
): void {
|
|
258
|
+
const list = (keyword: string): readonly Schema[] =>
|
|
259
|
+
Array.isArray(schema[keyword]) ? (schema[keyword] as Schema[]) : [];
|
|
260
|
+
for (const each of list("allOf")) {
|
|
261
|
+
this.check(doc, each, value, at, found);
|
|
262
|
+
}
|
|
263
|
+
const matching = (keyword: string): number =>
|
|
264
|
+
list(keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
265
|
+
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
266
|
+
refuse("anyOf", "the value matches none of the alternatives");
|
|
267
|
+
}
|
|
268
|
+
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
269
|
+
refuse(
|
|
270
|
+
"oneOf",
|
|
271
|
+
`the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (isRecord(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
275
|
+
refuse("not", "the value matches a schema it must not match");
|
|
276
|
+
}
|
|
277
|
+
if (isRecord(schema["if"])) {
|
|
278
|
+
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
279
|
+
if (isRecord(branch)) {
|
|
280
|
+
this.check(doc, branch, value, at, found);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
288
|
+
* at one of them: what it refuses in a value, none for an instance.
|
|
289
|
+
*/
|
|
290
|
+
export function schemaRefusals(
|
|
291
|
+
documents: Documents,
|
|
292
|
+
root: string,
|
|
293
|
+
): (value: unknown) => readonly SchemaRefusal[] {
|
|
294
|
+
const checker = new Checker(documents);
|
|
295
|
+
const top = documents[root];
|
|
296
|
+
if (top === undefined) {
|
|
297
|
+
throw new Error(`the schema set holds no document ${root}`);
|
|
298
|
+
}
|
|
299
|
+
return (value) => {
|
|
300
|
+
const found: SchemaRefusal[] = [];
|
|
301
|
+
checker.check(root, top, value, "", found);
|
|
302
|
+
return found;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const reportSchema = schemaRefusals(REPORT_SCHEMAS, "report.schema.json");
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* What the report schema refuses in one report document, none for an instance, and, as
|
|
310
|
+
* no schema keyword can state it, a components listing that is not one entry per
|
|
311
|
+
* component a finding or an edge evaluation's finding names.
|
|
312
|
+
*/
|
|
313
|
+
export function reportRefusals(document: unknown): readonly SchemaRefusal[] {
|
|
314
|
+
const found = reportSchema(document);
|
|
315
|
+
return found.length > 0 ? found : listingRefusals(document as Report);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The components listing held to the components the findings name, once each. */
|
|
319
|
+
function listingRefusals(report: Report): readonly SchemaRefusal[] {
|
|
320
|
+
if (report.components === undefined) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
const refuse = (pointer: string, reason: string): SchemaRefusal[] => [
|
|
324
|
+
{ pointer, keyword: "components", reason },
|
|
325
|
+
];
|
|
326
|
+
const listed = new Set<string>();
|
|
327
|
+
for (const [i, entry] of report.components.entries()) {
|
|
328
|
+
if (listed.has(entry.id)) {
|
|
329
|
+
return refuse(
|
|
330
|
+
`/components/${String(i)}`,
|
|
331
|
+
`the entry lists component ${entry.id} a second time`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
listed.add(entry.id);
|
|
335
|
+
}
|
|
336
|
+
const named = [
|
|
337
|
+
...report.findings.map(
|
|
338
|
+
(finding, i) => [`/findings/${String(i)}`, finding.component.id] as const,
|
|
339
|
+
),
|
|
340
|
+
...report.edge_evaluations.flatMap((one, i) =>
|
|
341
|
+
one.finding === undefined
|
|
342
|
+
? []
|
|
343
|
+
: [[`/edge_evaluations/${String(i)}/finding`, one.finding.component.id] as const],
|
|
344
|
+
),
|
|
345
|
+
];
|
|
346
|
+
for (const [pointer, id] of named) {
|
|
347
|
+
if (!listed.has(id)) {
|
|
348
|
+
return refuse(
|
|
349
|
+
pointer,
|
|
350
|
+
`the finding names component ${id}, which the components listing lacks`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const ids = new Set(named.map(([, id]) => id));
|
|
355
|
+
for (const [i, entry] of report.components.entries()) {
|
|
356
|
+
if (!ids.has(entry.id)) {
|
|
357
|
+
return refuse(
|
|
358
|
+
`/components/${String(i)}`,
|
|
359
|
+
`the entry lists component ${entry.id}, which no finding names`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return [];
|
|
364
|
+
}
|