@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.10

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 (288) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +245 -309
  5. package/dist/src/augmentations.js +124 -18
  6. package/dist/src/automatic-types.js +170 -0
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +9 -9
  10. package/dist/src/config.js +6 -81
  11. package/dist/src/configuration-files.js +78 -188
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +2 -2
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +146 -1
  20. package/dist/src/conventions.js +136 -70
  21. package/dist/src/decorator.js +21 -18
  22. package/dist/src/dependencies.js +280 -62
  23. package/dist/src/edges.js +38 -122
  24. package/dist/src/element-access.js +4 -6
  25. package/dist/src/entry-point-gaps.js +9 -18
  26. package/dist/src/entry-points.js +106 -21
  27. package/dist/src/enum-group.js +3 -10
  28. package/dist/src/erased-stores.js +4 -6
  29. package/dist/src/exempt-classes.js +1 -63
  30. package/dist/src/exempt.js +21 -40
  31. package/dist/src/failure.js +36 -0
  32. package/dist/src/file-facts.js +2 -25
  33. package/dist/src/findings/completion.js +225 -63
  34. package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
  35. package/dist/src/findings/emitters.js +6 -6
  36. package/dist/src/findings/interfaces.js +4 -5
  37. package/dist/src/findings/intra-function.js +53 -19
  38. package/dist/src/findings/non-code-artifacts.js +71 -39
  39. package/dist/src/findings/reads-and-writes.js +16 -13
  40. package/dist/src/findings/self-check.js +10 -12
  41. package/dist/src/findings/unused-declarations.js +57 -28
  42. package/dist/src/findings/visibility-narrowing.js +40 -18
  43. package/dist/src/findings-pass.js +8 -15
  44. package/dist/src/framework-lifecycle.js +7 -7
  45. package/dist/src/fs/document.js +52 -0
  46. package/dist/src/fs/manifest.js +72 -0
  47. package/dist/src/fs/tree.js +88 -0
  48. package/dist/src/generated-file.js +18 -4
  49. package/dist/src/git-ignore.js +99 -0
  50. package/dist/src/graph.js +9 -4
  51. package/dist/src/html-entries.js +6 -33
  52. package/dist/src/import-bindings.js +97 -0
  53. package/dist/src/in-operands.js +2 -1
  54. package/dist/src/index.js +1 -1
  55. package/dist/src/indexed-access.js +130 -28
  56. package/dist/src/injection-container.js +1 -1
  57. package/dist/src/interface-satisfaction.js +30 -12
  58. package/dist/src/intra-function-parts.js +341 -69
  59. package/dist/src/inventory.js +54 -29
  60. package/dist/src/javascript-file.js +33 -0
  61. package/dist/src/json-document.js +198 -67
  62. package/dist/src/keyof-comparisons.js +4 -6
  63. package/dist/src/kinds.js +3 -366
  64. package/dist/src/load/classify.js +270 -0
  65. package/dist/src/load/diagnostics.js +80 -0
  66. package/dist/src/load/discover.js +277 -0
  67. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  68. package/dist/src/load/expected-module.js +136 -0
  69. package/dist/src/load/load.js +27 -0
  70. package/dist/src/load/module-lookup.js +37 -0
  71. package/dist/src/load/plan.js +313 -0
  72. package/dist/src/load/specifiers.js +115 -0
  73. package/dist/src/load/synthesize.js +140 -0
  74. package/dist/src/load/test-globals.js +207 -0
  75. package/dist/src/load/test-joins.js +82 -0
  76. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  77. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
  78. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
  79. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  80. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  81. package/dist/src/manifest.js +49 -54
  82. package/dist/src/matrix.js +94 -13
  83. package/dist/src/mdx-entries.js +174 -0
  84. package/dist/src/module-declarations.js +9 -13
  85. package/dist/src/outside-tests.js +1 -1
  86. package/dist/src/overrides.js +41 -10
  87. package/dist/src/partial.js +2 -1
  88. package/dist/src/paths.js +26 -15
  89. package/dist/src/position.js +2 -1
  90. package/dist/src/print.js +11 -78
  91. package/dist/src/product.js +13 -0
  92. package/dist/src/query.js +1 -0
  93. package/dist/src/ref.js +11 -0
  94. package/dist/src/references.js +52 -31
  95. package/dist/src/report-schema.js +51 -0
  96. package/dist/src/report.js +59 -41
  97. package/dist/src/reporters/annotations.js +15 -8
  98. package/dist/src/reporters/sarif.js +16 -10
  99. package/dist/src/reporters/template.js +4 -19
  100. package/dist/src/resolve.js +245 -727
  101. package/dist/src/roots.js +374 -116
  102. package/dist/src/run.js +66 -77
  103. package/dist/src/schema.js +320 -342
  104. package/dist/src/scope.js +38 -115
  105. package/dist/src/serialization-contract.js +649 -29
  106. package/dist/src/session.js +41 -29
  107. package/dist/src/source-files.js +40 -15
  108. package/dist/src/suppress-file.js +34 -29
  109. package/dist/src/suppress-inline.js +21 -4
  110. package/dist/src/suppress.js +40 -56
  111. package/dist/src/sweep.js +32 -17
  112. package/dist/src/symbol-keys.js +105 -6
  113. package/dist/src/template-field.js +9 -19
  114. package/dist/src/test-support.js +119 -18
  115. package/dist/src/text/compare.js +26 -0
  116. package/dist/src/text/glob.js +194 -0
  117. package/dist/src/text/named-files.js +54 -0
  118. package/dist/src/text/shell.js +159 -0
  119. package/dist/src/text/yaml.js +393 -0
  120. package/dist/src/type-errors.js +27 -221
  121. package/dist/src/type-parameter-reads.js +172 -0
  122. package/dist/src/unseen-loads.js +100 -0
  123. package/dist/src/unwrap.js +23 -0
  124. package/dist/src/usage.js +10 -0
  125. package/dist/src/verbs/analyze.js +181 -125
  126. package/dist/src/verbs/describe.js +9 -16
  127. package/dist/src/verbs/explain.js +14 -9
  128. package/dist/src/verbs/print-config.js +2 -1
  129. package/dist/src/verbs/print-projects.js +6 -4
  130. package/dist/src/verbs/print-retained.js +4 -2
  131. package/dist/src/verbs/print-roots.js +4 -2
  132. package/dist/src/verbs/verb.js +1 -5
  133. package/dist/src/verbs/version.js +14 -3
  134. package/dist/src/workflow-steps.js +128 -33
  135. package/jsr.json +1 -1
  136. package/package.json +5 -4
  137. package/src/analysis.ts +333 -457
  138. package/src/augmentations.ts +152 -23
  139. package/src/automatic-types.ts +228 -0
  140. package/src/build-scripts.ts +543 -0
  141. package/src/component-layer.ts +4 -2
  142. package/src/components.ts +9 -10
  143. package/src/config.ts +31 -299
  144. package/src/configuration-files.ts +100 -191
  145. package/src/configured-declarations.ts +18 -23
  146. package/src/conformance-record.ts +2 -2
  147. package/src/consumers.ts +6 -57
  148. package/src/contextual-uses.ts +64 -12
  149. package/src/contract/config.generated.ts +818 -0
  150. package/src/contract/contract.generated.ts +1308 -0
  151. package/src/contract/report.generated.ts +1237 -0
  152. package/src/convention-rows.ts +186 -1
  153. package/src/conventions.ts +166 -90
  154. package/src/decorator.ts +28 -18
  155. package/src/dependencies.ts +356 -78
  156. package/src/edges.ts +41 -140
  157. package/src/element-access.ts +4 -7
  158. package/src/entry-point-gaps.ts +11 -20
  159. package/src/entry-points.ts +124 -28
  160. package/src/enum-group.ts +2 -11
  161. package/src/erased-stores.ts +3 -7
  162. package/src/exempt-classes.ts +1 -75
  163. package/src/exempt.ts +36 -50
  164. package/src/failure.ts +63 -0
  165. package/src/file-facts.ts +9 -24
  166. package/src/finding.ts +24 -9
  167. package/src/findings/boundary.ts +2 -0
  168. package/src/findings/completion.ts +297 -77
  169. package/src/findings/dependencies-and-module-machinery.ts +90 -31
  170. package/src/findings/emitter.ts +8 -9
  171. package/src/findings/emitters.ts +8 -8
  172. package/src/findings/interfaces.ts +4 -6
  173. package/src/findings/intra-function.ts +64 -20
  174. package/src/findings/non-code-artifacts.ts +88 -50
  175. package/src/findings/reads-and-writes.ts +20 -16
  176. package/src/findings/self-check.ts +11 -17
  177. package/src/findings/unused-declarations.ts +65 -28
  178. package/src/findings/visibility-narrowing.ts +51 -19
  179. package/src/findings-pass.ts +10 -19
  180. package/src/framework-lifecycle.ts +7 -7
  181. package/src/fs/document.ts +70 -0
  182. package/src/fs/manifest.ts +92 -0
  183. package/src/fs/tree.ts +110 -0
  184. package/src/generated-file.ts +23 -4
  185. package/src/git-ignore.ts +122 -0
  186. package/src/graph.ts +11 -3
  187. package/src/host.ts +17 -14
  188. package/src/html-entries.ts +6 -33
  189. package/src/import-bindings.ts +128 -0
  190. package/src/in-operands.ts +2 -1
  191. package/src/index.ts +1 -1
  192. package/src/indexed-access.ts +152 -30
  193. package/src/injection-container.ts +1 -1
  194. package/src/interface-satisfaction.ts +33 -13
  195. package/src/intra-function-parts.ts +449 -85
  196. package/src/inventory.ts +73 -27
  197. package/src/javascript-file.ts +37 -0
  198. package/src/json-document.ts +241 -76
  199. package/src/keyof-comparisons.ts +3 -7
  200. package/src/kinds.ts +5 -413
  201. package/src/load/classify.ts +397 -0
  202. package/src/load/diagnostics.ts +144 -0
  203. package/src/load/discover.ts +455 -0
  204. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  205. package/src/load/expected-module.ts +188 -0
  206. package/src/load/load.ts +73 -0
  207. package/src/load/module-lookup.ts +50 -0
  208. package/src/load/plan.ts +504 -0
  209. package/src/load/specifiers.ts +168 -0
  210. package/src/load/synthesize.ts +211 -0
  211. package/src/load/test-globals.ts +251 -0
  212. package/src/load/test-joins.ts +98 -0
  213. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  214. package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
  215. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
  216. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  217. package/src/{workspace.ts → load/workspace.ts} +90 -134
  218. package/src/manifest.ts +81 -75
  219. package/src/matrix.ts +154 -18
  220. package/src/mdx-entries.ts +218 -0
  221. package/src/module-declarations.ts +11 -13
  222. package/src/outside-tests.ts +1 -1
  223. package/src/overrides.ts +51 -12
  224. package/src/partial.ts +3 -2
  225. package/src/paths.ts +27 -15
  226. package/src/position.ts +2 -1
  227. package/src/print.ts +12 -91
  228. package/src/product.ts +18 -0
  229. package/src/query.ts +11 -0
  230. package/src/ref.ts +13 -0
  231. package/src/references.ts +62 -32
  232. package/src/report-schema.ts +68 -0
  233. package/src/report.ts +105 -215
  234. package/src/reporters/annotations.ts +22 -11
  235. package/src/reporters/json.ts +1 -1
  236. package/src/reporters/reporter.ts +2 -4
  237. package/src/reporters/reporters.ts +1 -1
  238. package/src/reporters/sarif.ts +29 -16
  239. package/src/reporters/template.ts +4 -20
  240. package/src/reporters/text.ts +3 -3
  241. package/src/resolve.ts +283 -1111
  242. package/src/roots.ts +490 -137
  243. package/src/run.ts +75 -91
  244. package/src/schema.ts +391 -387
  245. package/src/scope.ts +46 -138
  246. package/src/serialization-contract.ts +818 -26
  247. package/src/session.ts +77 -54
  248. package/src/source-files.ts +67 -17
  249. package/src/suppress-file.ts +36 -34
  250. package/src/suppress-inline.ts +20 -3
  251. package/src/suppress.ts +52 -63
  252. package/src/sweep.ts +53 -18
  253. package/src/symbol-keys.ts +131 -7
  254. package/src/template-field.ts +15 -25
  255. package/src/test-support.ts +155 -18
  256. package/src/text/compare.ts +27 -0
  257. package/src/text/glob.ts +210 -0
  258. package/src/text/named-files.inputs.json +1522 -0
  259. package/src/text/named-files.ts +68 -0
  260. package/src/text/shell.ts +161 -0
  261. package/src/text/yaml.ts +423 -0
  262. package/src/type-errors.ts +35 -273
  263. package/src/type-parameter-reads.ts +227 -0
  264. package/src/unseen-loads.ts +134 -0
  265. package/src/unwrap.ts +34 -0
  266. package/src/usage.ts +10 -0
  267. package/src/verbs/analyze.ts +245 -161
  268. package/src/verbs/describe.ts +14 -15
  269. package/src/verbs/explain.ts +15 -13
  270. package/src/verbs/print-config.ts +2 -1
  271. package/src/verbs/print-projects.ts +6 -4
  272. package/src/verbs/print-retained.ts +4 -2
  273. package/src/verbs/print-roots.ts +4 -2
  274. package/src/verbs/verb.ts +2 -8
  275. package/src/verbs/version.ts +15 -3
  276. package/src/workflow-steps.ts +149 -34
  277. package/dist/src/discover.js +0 -347
  278. package/dist/src/glob.js +0 -65
  279. package/dist/src/pnpm-workspace.js +0 -120
  280. package/dist/src/setup-failure.js +0 -23
  281. package/dist/src/test-joins.js +0 -101
  282. package/dist/src/version.js +0 -7
  283. package/src/discover.ts +0 -510
  284. package/src/glob.ts +0 -67
  285. package/src/pnpm-workspace.ts +0 -135
  286. package/src/setup-failure.ts +0 -39
  287. package/src/test-joins.ts +0 -120
  288. package/src/version.ts +0 -7
@@ -1,393 +1,371 @@
1
- import { ConfigError } from "./config.js";
2
- const leaf = { kind: "leaf" };
3
- const integer = { kind: "leaf", integer: true };
4
- /** A list of declarations, each entry in one of the three shapes the `ts` section states. */
5
- const declarations = {
6
- kind: "list",
7
- members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
8
- };
9
1
  /**
10
- * The closed key list the Contract's configuration schema declares, derived from
11
- * that schema key by key. A test pins the two equal, so a Contract change that
12
- * adds a key fails there rather than being silently unimplemented.
2
+ * Holds a value to a JSON Schema the generator copies from the Contract. It evaluates the
3
+ * keywords the Contract's schemas use; the schemas' patterns are written to match alike in
4
+ * RE2 and in ECMAScript.
13
5
  */
14
- export const SCHEMA_ROOT = {
15
- kind: "section",
16
- members: {
17
- contract_version: leaf,
18
- target: {
19
- kind: "section",
20
- members: { kind: { kind: "object", members: { go: leaf, ts: leaf } } },
21
- },
22
- analysis: {
23
- kind: "section",
24
- members: {
25
- languages: leaf,
26
- min_confidence: leaf,
27
- generated_files: leaf,
28
- consumer_tests: leaf,
29
- configurations: {
30
- kind: "list",
31
- members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
32
- },
33
- matrix: { kind: "section", members: { complete: leaf } },
34
- template_dirs: leaf,
35
- template_delimiters: {
36
- kind: "object",
37
- members: { left: leaf, right: leaf },
38
- },
39
- },
40
- },
41
- consumers: { kind: "section", members: { complete: leaf } },
42
- roots: { kind: "section", members: { patterns: leaf } },
43
- severity: { kind: "map" },
44
- exemptions: { kind: "section", members: { disabled: leaf } },
45
- reporters: {
46
- kind: "section",
47
- members: {
48
- formats: leaf,
49
- sort: leaf,
50
- cascade: leaf,
51
- max_findings: integer,
52
- fail_on: leaf,
53
- },
54
- },
55
- providers: {
56
- kind: "section",
57
- members: {
58
- analyzers: {
59
- kind: "list",
60
- members: {
61
- name: leaf,
62
- languages: leaf,
63
- command: leaf,
64
- source: leaf,
65
- version: leaf,
66
- digest: leaf,
67
- },
68
- },
69
- },
70
- },
71
- go: { kind: "section", members: {} },
72
- ts: {
73
- kind: "section",
74
- members: {
75
- test_files: leaf,
76
- entry_files: leaf,
77
- component_extensions: leaf,
78
- disabled_conventions: leaf,
79
- injection_registrations: declarations,
80
- lifecycle_contracts: {
81
- kind: "list",
82
- members: { components: declarations, bases: declarations, members: leaf },
83
- },
84
- serializers: declarations,
85
- },
86
- },
87
- provenance: { kind: "map" },
88
- },
89
- };
90
- /** The name of the severity object, whose member names the key list leaves open. */
91
- export const SEVERITY_SECTION = "severity";
6
+ import { isRecord, pointerOf, pointerSegments } from "./json-document.js";
7
+ import { bytewise } from "./text/compare.js";
92
8
  /**
93
- * The node one member name resolves to, and whether the key list declares it. A
94
- * member of an open object, and any member below a setting, resolves to a leaf,
95
- * so the walk descends far enough to find a repeated member at any depth.
9
+ * Every keyword the validator evaluates or knows to carry no assertion. A schema naming
10
+ * another fails every check rather than passing values the keyword would refuse.
96
11
  */
97
- function memberOf(node, name) {
98
- if (node.kind === "section" || node.kind === "list" || node.kind === "object") {
99
- const members = node.members ?? {};
100
- return Object.hasOwn(members, name) ? members[name] : undefined;
12
+ const KEYWORDS = new Set([
13
+ ...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
14
+ ...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
15
+ ...["properties", "additionalProperties", "propertyNames", "required", "minProperties"],
16
+ ...["items", "minItems", "maxItems", "uniqueItems"],
17
+ ...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
18
+ ]);
19
+ const patterns = new Map();
20
+ function pattern(source) {
21
+ let held = patterns.get(source);
22
+ if (held === undefined) {
23
+ held = new RegExp(source, "u");
24
+ patterns.set(source, held);
101
25
  }
102
- return leaf;
26
+ return held;
103
27
  }
104
- function joinKey(at, name) {
105
- return at === "" ? name : `${at}.${name}`;
106
- }
107
- function collect(node, at, keys, settingsOnly) {
108
- for (const [name, child] of Object.entries(node.members ?? {})) {
109
- const path = joinKey(at, name);
110
- if (!settingsOnly) {
111
- keys.push(path);
112
- collect(child, child.kind === "list" ? `${path}[]` : path, keys, settingsOnly);
113
- continue;
114
- }
115
- if (child.kind === "leaf" || child.kind === "list" || child.kind === "object") {
116
- keys.push(path);
117
- continue;
118
- }
119
- collect(child, path, keys, settingsOnly);
28
+ function typeOf(value) {
29
+ if (value === null) {
30
+ return "null";
120
31
  }
121
- }
122
- /**
123
- * Every key the closed key list declares, as a dotted path, in ascending order:
124
- * the sections, the settings, and the two open objects. A list's entry members
125
- * are spelled under the list's own path followed by empty brackets.
126
- */
127
- export function declaredKeys() {
128
- const keys = [];
129
- collect(SCHEMA_ROOT, "", keys, false);
130
- return keys.sort();
131
- }
132
- /**
133
- * The dotted path of every setting that holds one value, in ascending order. A
134
- * list and a setting written as an object are each one setting, their members not
135
- * addressable on their own, and the severity object is not among them: it
136
- * resolves per code.
137
- */
138
- export function settingPaths() {
139
- const paths = [];
140
- collect(SCHEMA_ROOT, "", paths, true);
141
- return paths.sort();
142
- }
143
- /**
144
- * Whether one dotted path names a setting a source may supply: a setting of the
145
- * closed key list, or one code of the severity object, whose member names the key
146
- * list leaves open. It is the predicate the command maps its own flag names
147
- * through, so a flag that would be refused as an unimplemented key is a failing
148
- * test rather than a flag nothing reads.
149
- *
150
- * A section holds no value of its own, so `analysis` is false. A setting written
151
- * as an object is supplied whole, so `analysis.template_delimiters` is true and
152
- * `analysis.template_delimiters.left` is false. A severity code is a declared
153
- * setting whatever it names, so `severity.DS9999` is true here and refused by
154
- * resolution: this answers the shape of a path, and resolution answers the
155
- * vocabulary.
156
- */
157
- export function declaresSetting(path) {
158
- if (settingPaths().includes(path)) {
159
- return true;
32
+ if (Array.isArray(value)) {
33
+ return "array";
160
34
  }
161
- const dot = path.indexOf(".");
162
- if (dot < 0) {
163
- return false;
35
+ if (typeof value === "number") {
36
+ return Number.isInteger(value) ? "integer" : "number";
164
37
  }
165
- const code = path.slice(dot + 1);
166
- return path.slice(0, dot) === SEVERITY_SECTION && code !== "" && !code.includes(".");
38
+ return typeof value;
167
39
  }
168
- /** The Levenshtein distance between two strings, counted in code points. */
169
- function editDistance(a, b) {
170
- const from = Array.from(a);
171
- const to = Array.from(b);
172
- let previous = Array.from({ length: to.length + 1 }, (_unused, index) => index);
173
- let current = new Array(to.length + 1).fill(0);
174
- for (let i = 1; i <= from.length; i += 1) {
175
- current[0] = i;
176
- for (let j = 1; j <= to.length; j += 1) {
177
- const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
178
- current[j] = Math.min(substitution, (previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1);
179
- }
180
- [previous, current] = [current, previous];
40
+ function hasType(value, name) {
41
+ const held = typeOf(value);
42
+ return held === name || (name === "number" && held === "integer");
43
+ }
44
+ /** The types a schema names, none where it names no type. */
45
+ function typesOf(schema) {
46
+ const types = schema["type"];
47
+ if (types === undefined) {
48
+ return [];
181
49
  }
182
- return previous[to.length] ?? 0;
50
+ return (Array.isArray(types) ? types : [types]).filter((one) => typeof one === "string");
183
51
  }
184
- function sharedSegments(a, b) {
185
- let shared = 0;
186
- while (shared < a.length && shared < b.length && a[shared] === b[shared]) {
187
- shared += 1;
52
+ /** One value's encoding with object members sorted, so two equal values encode alike. */
53
+ function canonical(value) {
54
+ if (Array.isArray(value)) {
55
+ return `[${value.map(canonical).join(",")}]`;
56
+ }
57
+ if (isRecord(value)) {
58
+ return `{${Object.keys(value)
59
+ .sort(bytewise)
60
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
61
+ .join(",")}}`;
188
62
  }
189
- return shared;
63
+ return JSON.stringify(value);
190
64
  }
191
- /**
192
- * The implemented key closest to `key`: the one sharing the most leading dotted
193
- * segments, and among those the smallest edit distance over the whole path, ties
194
- * broken in ascending order.
195
- */
196
- export function nearestKey(key) {
197
- const segments = key.split(".");
198
- let nearest = "";
199
- let bestShared = -1;
200
- let bestDistance = 0;
201
- for (const candidate of declaredKeys()) {
202
- const shared = sharedSegments(segments, candidate.split("."));
203
- const distance = editDistance(key, candidate);
204
- if (shared < bestShared || (shared === bestShared && distance >= bestDistance)) {
205
- continue;
206
- }
207
- nearest = candidate;
208
- bestShared = shared;
209
- bestDistance = distance;
65
+ /** A string's length in code points, which JSON Schema's minLength counts. */
66
+ function codePoints(value) {
67
+ let counted = 0;
68
+ for (const _point of value) {
69
+ counted += 1;
210
70
  }
211
- return nearest;
71
+ return counted;
212
72
  }
213
- /** A refusal of a document that is not one instance of the closed key list. */
214
- export function malformed(label, path, detail) {
215
- const message = path === "" ? `${label}: ${detail}` : `${label}: ${path}: ${detail}`;
216
- return new ConfigError("malformed", path, message);
73
+ /** The schemas of a list keyword, none where the schema does not write it. */
74
+ function listOf(schema, keyword) {
75
+ const held = schema[keyword];
76
+ return Array.isArray(held) ? held : [];
217
77
  }
218
- /**
219
- * A refusal of a key the closed key list does not declare, naming the nearest key
220
- * it does.
221
- */
222
- function unimplementedKey(label, path) {
223
- const nearest = nearestKey(path);
224
- let hint = "";
225
- if (nearest === path) {
226
- hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
78
+ /** Whether a reader fixes refusal `a` before `b`: an object's before one inside it, then by kind. */
79
+ function before(a, b) {
80
+ if (a.pointer === b.pointer) {
81
+ return rank(a) < rank(b);
227
82
  }
228
- else if (nearest !== "") {
229
- hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
83
+ return b.pointer.startsWith(`${a.pointer}/`);
84
+ }
85
+ /** The order of the refusals of one place: an undeclared member, a missing one, any other. */
86
+ function rank(refusal) {
87
+ if (refusal.keyword === "additionalProperties" || refusal.keyword === "propertyNames") {
88
+ return 0;
230
89
  }
231
- return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented${hint}`);
90
+ return refusal.keyword === "required" ? 1 : 2;
232
91
  }
233
- const SPACE = new Set([0x20, 0x09, 0x0a, 0x0d]);
234
- function skipSpace(cursor) {
235
- while (cursor.at < cursor.text.length && SPACE.has(cursor.text.charCodeAt(cursor.at))) {
236
- cursor.at += 1;
92
+ /** The value one JSON Pointer segment names inside `value`, or undefined. */
93
+ function valueAt(value, segment) {
94
+ if (Array.isArray(value)) {
95
+ return /^[0-9]+$/u.test(segment) ? value[Number(segment)] : undefined;
237
96
  }
97
+ return isRecord(value) && Object.hasOwn(value, segment) ? value[segment] : undefined;
238
98
  }
239
99
  /**
240
- * The string literal at the cursor, decoded by the standard library: the walk
241
- * finds the literal's extent and the parse reads it, so an escaped member name
242
- * compares equal to the name it spells.
100
+ * The schema `properties` declares for the member `name`, or undefined: a member named as an
101
+ * `Object.prototype` property, such as `__proto__`, is declared only where it is written.
243
102
  */
244
- function readName(cursor, at, label) {
245
- const start = cursor.at;
246
- cursor.at += 1;
247
- while (cursor.at < cursor.text.length) {
248
- const ch = cursor.text.charCodeAt(cursor.at);
249
- if (ch === 0x5c) {
250
- cursor.at += 2;
251
- continue;
252
- }
253
- cursor.at += 1;
254
- if (ch === 0x22) {
255
- return JSON.parse(cursor.text.slice(start, cursor.at));
103
+ function declaredMember(properties, name) {
104
+ return Object.hasOwn(properties, name) ? properties[name] : undefined;
105
+ }
106
+ /** The keyword checks over one set of schema documents, each named as a `$ref` names it. */
107
+ class Checker {
108
+ documents;
109
+ constructor(documents) {
110
+ this.documents = documents;
111
+ }
112
+ resolve(doc, ref) {
113
+ const [file = "", pointer = ""] = ref.split("#");
114
+ const target = file === "" ? doc : file;
115
+ let schema = this.documents[target];
116
+ for (const token of pointerSegments(pointer)) {
117
+ schema = isRecord(schema) ? schema[token] : undefined;
256
118
  }
119
+ if (!isRecord(schema)) {
120
+ throw new Error(`schema: ${ref} names no schema`);
121
+ }
122
+ return [target, schema];
257
123
  }
258
- throw malformed(label, at, "a member name is not terminated");
259
- }
260
- function skipScalar(cursor) {
261
- while (cursor.at < cursor.text.length) {
262
- const ch = cursor.text.charCodeAt(cursor.at);
263
- if (ch === 0x2c || ch === 0x7d || ch === 0x5d || SPACE.has(ch)) {
124
+ refusals(doc, schema, value, at) {
125
+ const found = [];
126
+ this.check(doc, schema, value, at, found);
127
+ return found;
128
+ }
129
+ matches(doc, schema, value, at) {
130
+ return this.refusals(doc, schema, value, at).length === 0;
131
+ }
132
+ check(doc, schema, value, at, found) {
133
+ for (const keyword of Object.keys(schema)) {
134
+ if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
135
+ throw new Error(`schema: the keyword ${keyword} is not one this validator evaluates`);
136
+ }
137
+ }
138
+ const refuse = (keyword, reason, member = "") => {
139
+ found.push({ pointer: at, keyword, reason, member });
140
+ };
141
+ if (typeof schema["$ref"] === "string") {
142
+ const [target, referenced] = this.resolve(doc, schema["$ref"]);
143
+ this.check(target, referenced, value, at, found);
144
+ }
145
+ const named = typesOf(schema);
146
+ if (named.length > 0 && !named.some((one) => hasType(value, one))) {
147
+ refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
264
148
  return;
265
149
  }
266
- cursor.at += 1;
267
- }
268
- }
269
- /** A number the document writes as an optional minus sign and digits alone. */
270
- const INTEGER_TEXT = /^-?[0-9]+$/u;
271
- /** The start of a JSON number: a minus sign or a digit. */
272
- const NUMBER_START = /^[-0-9]/u;
273
- /**
274
- * Refuses a number written with a fraction or an exponent where the key list
275
- * declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
276
- * the text can tell them apart. A scalar that is no number is the decoder's to
277
- * refuse by its type.
278
- */
279
- function checkInteger(text, at, label) {
280
- if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
281
- throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
150
+ this.checkValue(schema, value, refuse);
151
+ if (Array.isArray(value)) {
152
+ this.checkArray(doc, schema, value, at, found, refuse);
153
+ }
154
+ else if (isRecord(value)) {
155
+ this.checkObject(doc, schema, value, at, found, refuse);
156
+ }
157
+ this.checkApplicators(doc, schema, value, at, found, refuse);
282
158
  }
283
- }
284
- function walkValue(cursor, at, node, label) {
285
- skipSpace(cursor);
286
- const ch = cursor.text.charCodeAt(cursor.at);
287
- if (ch === 0x7b) {
288
- cursor.at += 1;
289
- walkObject(cursor, at, node, label);
290
- return;
159
+ checkValue(schema, value, refuse) {
160
+ const values = schema["enum"];
161
+ if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
162
+ refuse("enum", `${canonical(value)} is not one of ${values.map(canonical).join(", ")}`);
163
+ }
164
+ if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
165
+ refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
166
+ }
167
+ if (typeof value === "string") {
168
+ if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
169
+ refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
170
+ }
171
+ if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
172
+ refuse("minLength", `${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`);
173
+ }
174
+ }
175
+ if (typeof value === "number") {
176
+ if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
177
+ refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
178
+ }
179
+ if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
180
+ refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
181
+ }
182
+ }
291
183
  }
292
- if (ch === 0x5b) {
293
- cursor.at += 1;
294
- walkArray(cursor, at, node, label);
295
- return;
184
+ checkObject(doc, schema, value, at, found, refuse) {
185
+ const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
186
+ const additional = schema["additionalProperties"];
187
+ const names = schema["propertyNames"];
188
+ for (const name of Object.keys(value).sort(bytewise)) {
189
+ const [refusedName] = isRecord(names) ? this.refusals(doc, names, name, at) : [];
190
+ if (refusedName !== undefined) {
191
+ refuse("propertyNames", `the member name ${JSON.stringify(name)}: ${refusedName.reason}`, name);
192
+ }
193
+ const member = `${at}${pointerOf([name])}`;
194
+ const declared = declaredMember(properties, name);
195
+ if (isRecord(declared)) {
196
+ this.check(doc, declared, value[name], member, found);
197
+ }
198
+ else if (additional === false) {
199
+ refuse("additionalProperties", `the member ${JSON.stringify(name)} is not one the schema declares`, name);
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"] : []) {
206
+ if (!Object.hasOwn(value, name)) {
207
+ refuse("required", `the member ${JSON.stringify(name)} is absent`, name);
208
+ }
209
+ }
210
+ const fewest = schema["minProperties"];
211
+ if (typeof fewest === "number" && Object.keys(value).length < fewest) {
212
+ refuse("minProperties", `the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`);
213
+ }
296
214
  }
297
- if (ch === 0x22) {
298
- readName(cursor, at, label);
299
- return;
215
+ checkArray(doc, schema, value, at, found, refuse) {
216
+ const items = schema["items"];
217
+ if (isRecord(items)) {
218
+ value.forEach((item, i) => {
219
+ this.check(doc, items, item, `${at}/${String(i)}`, found);
220
+ });
221
+ }
222
+ if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
223
+ refuse("minItems", `the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`);
224
+ }
225
+ if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
226
+ refuse("maxItems", `the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`);
227
+ }
228
+ if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
229
+ refuse("uniqueItems", "the array lists one value twice");
230
+ }
300
231
  }
301
- const start = cursor.at;
302
- skipScalar(cursor);
303
- if (node.integer === true) {
304
- checkInteger(cursor.text.slice(start, cursor.at), at, label);
232
+ checkApplicators(doc, schema, value, at, found, refuse) {
233
+ for (const each of listOf(schema, "allOf")) {
234
+ this.check(doc, each, value, at, found);
235
+ }
236
+ const matching = (keyword) => listOf(schema, keyword).filter((each) => this.matches(doc, each, value, at)).length;
237
+ if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
238
+ refuse("anyOf", "the value matches none of the alternatives");
239
+ }
240
+ if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
241
+ refuse("oneOf", `the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`);
242
+ }
243
+ if (isRecord(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
244
+ refuse("not", "the value matches a schema it must not match");
245
+ }
246
+ if (isRecord(schema["if"])) {
247
+ const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
248
+ if (isRecord(branch)) {
249
+ this.check(doc, branch, value, at, found);
250
+ }
251
+ }
305
252
  }
306
- }
307
- function walkObject(cursor, at, node, label) {
308
- const seen = new Set();
309
- for (;;) {
310
- skipSpace(cursor);
311
- const ch = cursor.text.charCodeAt(cursor.at);
312
- if (ch === 0x7d) {
313
- cursor.at += 1;
314
- return;
253
+ /**
254
+ * The refusal of `value` a reader fixes first, none for an instance: an object's
255
+ * refusal before any inside it, at one place an undeclared member, then a missing one,
256
+ * then any other, else the order the check finds them in. A oneOf no alternative
257
+ * matches is explained by the first refusal of the alternative the value was written
258
+ * for: the one whose type admits it, or of several, the one declaring the most of an
259
+ * object's members, then the fewest. Where two remain, the refusal is the oneOf's own.
260
+ */
261
+ first(doc, schema, value, at) {
262
+ const found = [];
263
+ this.check(doc, schema, value, at, found);
264
+ let first = found[0];
265
+ for (const other of found.slice(1)) {
266
+ if (first !== undefined && before(other, first)) {
267
+ first = other;
268
+ }
315
269
  }
316
- if (ch === 0x2c) {
317
- cursor.at += 1;
318
- continue;
270
+ if (first?.keyword !== "oneOf") {
271
+ return first;
319
272
  }
320
- if (ch !== 0x22) {
321
- throw malformed(label, at, "a member name is expected");
273
+ let held = value;
274
+ let holders = [[doc, schema]];
275
+ for (const segment of pointerSegments(first.pointer.slice(at.length))) {
276
+ holders = holders.flatMap(([d, s]) => this.applied(d, s, held).flatMap(([d2, s2]) => this.member(d2, s2, held, segment)));
277
+ held = valueAt(held, segment);
322
278
  }
323
- const name = readName(cursor, at, label);
324
- const path = joinKey(at, name);
325
- if (seen.has(name)) {
326
- throw malformed(label, path, `member ${JSON.stringify(name)} is written twice, so neither value is chosen`);
279
+ for (const [d, holder] of holders) {
280
+ const alternatives = listOf(holder, "oneOf");
281
+ if (alternatives.some((each) => this.matches(d, each, held, first.pointer))) {
282
+ continue;
283
+ }
284
+ const alternative = closest(alternatives, held);
285
+ const inner = alternative && this.first(d, alternative, held, first.pointer);
286
+ if (inner !== undefined) {
287
+ return inner;
288
+ }
327
289
  }
328
- seen.add(name);
329
- const child = memberOf(node, name);
330
- if (child === undefined) {
331
- throw unimplementedKey(label, path);
290
+ return first;
291
+ }
292
+ /** The schemas checked at one place when `schema` is: it, its `$ref`, its allOf and its if branch. */
293
+ applied(doc, schema, value) {
294
+ const held = [[doc, schema]];
295
+ if (typeof schema["$ref"] === "string") {
296
+ const [target, referenced] = this.resolve(doc, schema["$ref"]);
297
+ held.push(...this.applied(target, referenced, value));
332
298
  }
333
- skipSpace(cursor);
334
- if (cursor.text.charCodeAt(cursor.at) !== 0x3a) {
335
- throw malformed(label, path, "a member name is followed by a colon");
299
+ for (const each of listOf(schema, "allOf")) {
300
+ held.push(...this.applied(doc, each, value));
301
+ }
302
+ if (isRecord(schema["if"])) {
303
+ const branch = this.matches(doc, schema["if"], value, "") ? schema["then"] : schema["else"];
304
+ if (isRecord(branch)) {
305
+ held.push(...this.applied(doc, branch, value));
306
+ }
336
307
  }
337
- cursor.at += 1;
338
- walkValue(cursor, path, child, label);
308
+ return held;
309
+ }
310
+ /** The schemas `schema` checks the member or the entry `segment` names in `value` with. */
311
+ member(doc, schema, value, segment) {
312
+ if (Array.isArray(value)) {
313
+ return isRecord(schema["items"]) ? [[doc, schema["items"]]] : [];
314
+ }
315
+ const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
316
+ const declared = declaredMember(properties, segment);
317
+ if (isRecord(declared)) {
318
+ return [[doc, declared]];
319
+ }
320
+ const additional = schema["additionalProperties"];
321
+ return isRecord(additional) ? [[doc, additional]] : [];
339
322
  }
340
323
  }
341
- function walkArray(cursor, at, node, label) {
342
- const entry = node.kind === "list" ? { kind: "section", members: node.members ?? {} } : leaf;
343
- let index = 0;
344
- for (;;) {
345
- skipSpace(cursor);
346
- const ch = cursor.text.charCodeAt(cursor.at);
347
- if (ch === 0x5d) {
348
- cursor.at += 1;
349
- return;
324
+ /** The alternative a value matching none of them was written for, or none where none or two remain. */
325
+ function closest(alternatives, value) {
326
+ const typed = alternatives.filter((each) => {
327
+ const types = typesOf(each);
328
+ return types.length === 0 || types.some((one) => hasType(value, one));
329
+ });
330
+ if (typed.length === 1 || !isRecord(value)) {
331
+ return typed.length === 1 ? typed[0] : undefined;
332
+ }
333
+ let best = [];
334
+ let most = -1;
335
+ let fewest = 0;
336
+ for (const alternative of typed) {
337
+ const properties = isRecord(alternative["properties"]) ? alternative["properties"] : {};
338
+ const named = Object.keys(value).filter((name) => Object.hasOwn(properties, name)).length;
339
+ const declared = Object.keys(properties).length;
340
+ if (named > most || (named === most && declared < fewest)) {
341
+ [best, most, fewest] = [[alternative], named, declared];
350
342
  }
351
- if (ch === 0x2c) {
352
- cursor.at += 1;
353
- continue;
343
+ else if (named === most && declared === fewest) {
344
+ best.push(alternative);
354
345
  }
355
- walkValue(cursor, `${at}[${index}]`, entry, label);
356
- index += 1;
357
346
  }
347
+ return best.length === 1 ? best[0] : undefined;
358
348
  }
359
349
  /**
360
- * Walks one document's text against the closed key list, refusing a member the
361
- * key list does not declare and a member one object writes twice, each naming its
362
- * dotted path. The document is already known to parse.
350
+ * A validator over one set of schema documents, each named as a `$ref` names it, rooted
351
+ * at one of them: what it refuses in a value, none for an instance.
363
352
  */
364
- export function checkDocument(text, node, label) {
365
- const cursor = { text, at: 0 };
366
- walkValue(cursor, "", node, label);
353
+ export function schemaRefusals(documents, root) {
354
+ const checker = new Checker(documents);
355
+ const top = documents[root];
356
+ if (top === undefined) {
357
+ throw new Error(`the schema set holds no document ${root}`);
358
+ }
359
+ return (value) => {
360
+ const found = [];
361
+ checker.check(root, top, value, "", found);
362
+ return found;
363
+ };
367
364
  }
368
365
  /**
369
- * The flag document, whose keys are dotted setting paths, rewritten as the nested
370
- * object the closed key list declares, so one decoder reads every source. A key
371
- * naming no setting is refused.
366
+ * The refusal of `value` a reader fixes first under one schema, none for an instance. A
367
+ * `$ref` the schema holds names a place in the schema itself.
372
368
  */
373
- export function nestFlags(flat, label) {
374
- const nested = {};
375
- for (const path of Object.keys(flat).sort()) {
376
- if (!declaresSetting(path)) {
377
- throw unimplementedKey(label, path);
378
- }
379
- const segments = path.split(".");
380
- let into = nested;
381
- for (const segment of segments.slice(0, -1)) {
382
- const child = into[segment] ?? {};
383
- if (typeof child !== "object" || Array.isArray(child)) {
384
- throw malformed(label, path, `the flag document sets ${JSON.stringify(segment)} twice`);
385
- }
386
- into[segment] = child;
387
- into = child;
388
- }
389
- const last = segments[segments.length - 1] ?? "";
390
- into[last] = flat[path];
391
- }
392
- return nested;
369
+ export function firstRefusal(schema, value) {
370
+ return new Checker({ "": schema }).first("", schema, value, "");
393
371
  }