@vertekum/schema-builder 0.1.6 → 0.1.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/CHANGELOG.md +6 -0
- package/package.json +3 -3
- package/src/build.ts +47 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @vertekum/schema-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`880b288`](https://github.com/moros-oxus/vertekum/commit/880b288630455eaf629f0d458fb38b3eaf0222a6) Thanks [@tschemmer](https://github.com/tschemmer)! - Grammar errors are attributed to the module that contains them — a failure inside an imported module names that file, not the one being linted or built. Referencing a fragment by module name (`<@t-shirt>`) now explains that a fragment has no root and lists its productions to reference instead; a qualified miss lists what the import declares.
|
|
8
|
+
|
|
3
9
|
## 0.1.6
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertekum/schema-builder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Declare token vocabularies as grammar; build them into JSON Schema files",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"zod": "^3.25.76"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@vertekum/
|
|
47
|
-
"@vertekum/
|
|
46
|
+
"@vertekum/schema-vocabulary": "0.1.0",
|
|
47
|
+
"@vertekum/core": "0.1.2"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/build.ts
CHANGED
|
@@ -33,8 +33,19 @@ interface Scope {
|
|
|
33
33
|
expanding: Set<string>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** The `<@key/name>, …` listing a miss suggests — an error should name the fix, not just the gap. */
|
|
37
|
+
function productionsOf(key: string, imported: ResolvedModule): string {
|
|
38
|
+
return [...imported.module.productions.keys()]
|
|
39
|
+
.map((p) => `<@${key}/${p}>`)
|
|
40
|
+
.join(', ');
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
/** Find what `<name>` / `<@name>` means in this scope. Imported roots go by module name. */
|
|
37
44
|
function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
45
|
+
// Every error here names the module whose TEXT holds the ref — `scope.module` — so a
|
|
46
|
+
// failure inside an imported production's body is attributed to that file, not to
|
|
47
|
+
// whichever module the walk started from.
|
|
48
|
+
const file = scope.module.path;
|
|
38
49
|
if (!ref.imported) {
|
|
39
50
|
const production = scope.module.module.productions.get(ref.name);
|
|
40
51
|
if (!production) {
|
|
@@ -42,6 +53,7 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
42
53
|
`unknown production '<${ref.name}>'`,
|
|
43
54
|
ref.line,
|
|
44
55
|
ref.column,
|
|
56
|
+
file,
|
|
45
57
|
);
|
|
46
58
|
}
|
|
47
59
|
return { node: production, scope };
|
|
@@ -51,14 +63,20 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
51
63
|
if (ref.from) {
|
|
52
64
|
const imported = scope.module.imports.get(ref.from);
|
|
53
65
|
if (!imported) {
|
|
54
|
-
throw new DfnError(
|
|
66
|
+
throw new DfnError(
|
|
67
|
+
`no import named '${ref.from}'`,
|
|
68
|
+
ref.line,
|
|
69
|
+
ref.column,
|
|
70
|
+
file,
|
|
71
|
+
);
|
|
55
72
|
}
|
|
56
73
|
const production = imported.module.productions.get(ref.name);
|
|
57
74
|
if (!production) {
|
|
58
75
|
throw new DfnError(
|
|
59
|
-
`'${ref.from}' has no production '${ref.name}'`,
|
|
76
|
+
`'${ref.from}' has no production '${ref.name}' — it declares: ${productionsOf(ref.from, imported)}`,
|
|
60
77
|
ref.line,
|
|
61
78
|
ref.column,
|
|
79
|
+
file,
|
|
62
80
|
);
|
|
63
81
|
}
|
|
64
82
|
return {
|
|
@@ -77,10 +95,22 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
77
95
|
}
|
|
78
96
|
}
|
|
79
97
|
if (hits.length === 0) {
|
|
98
|
+
// The near-miss that reads as a missing `use`: the import EXISTS under this key, but
|
|
99
|
+
// `<@key>` means its root and a fragment has none. Say so, and name the real forms.
|
|
100
|
+
const fragment = scope.module.imports.get(ref.name);
|
|
101
|
+
if (fragment) {
|
|
102
|
+
throw new DfnError(
|
|
103
|
+
`'${ref.name}' is imported, but it declares no root (a fragment) — reference one of its productions: ${productionsOf(ref.name, fragment)}`,
|
|
104
|
+
ref.line,
|
|
105
|
+
ref.column,
|
|
106
|
+
file,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
80
109
|
throw new DfnError(
|
|
81
110
|
`no import provides '<@${ref.name}>'`,
|
|
82
111
|
ref.line,
|
|
83
112
|
ref.column,
|
|
113
|
+
file,
|
|
84
114
|
);
|
|
85
115
|
}
|
|
86
116
|
if (hits.length > 1) {
|
|
@@ -88,6 +118,7 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
88
118
|
`'<@${ref.name}>' is ambiguous across imports — qualify it: <@module/${ref.name}>`,
|
|
89
119
|
ref.line,
|
|
90
120
|
ref.column,
|
|
121
|
+
file,
|
|
91
122
|
);
|
|
92
123
|
}
|
|
93
124
|
return hits[0];
|
|
@@ -132,6 +163,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
132
163
|
error instanceof Error ? error.message : String(error),
|
|
133
164
|
node.line,
|
|
134
165
|
node.column,
|
|
166
|
+
scope.module.path,
|
|
135
167
|
);
|
|
136
168
|
}
|
|
137
169
|
if (scale.collisions.length > 0) {
|
|
@@ -139,6 +171,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
139
171
|
`scale steps ${scale.collisions.join(', ')} quantize onto earlier names — the quantum is too coarse for the factor`,
|
|
140
172
|
node.line,
|
|
141
173
|
node.column,
|
|
174
|
+
scope.module.path,
|
|
142
175
|
);
|
|
143
176
|
}
|
|
144
177
|
const forest = leaf();
|
|
@@ -188,6 +221,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
188
221
|
`'<${node.name}>' expands through itself`,
|
|
189
222
|
node.line,
|
|
190
223
|
node.column,
|
|
224
|
+
scope.module.path,
|
|
191
225
|
);
|
|
192
226
|
}
|
|
193
227
|
scope.expanding.add(node.name);
|
|
@@ -200,6 +234,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
200
234
|
`'<${node.name}>' has no member '${omitted}' to omit`,
|
|
201
235
|
node.line,
|
|
202
236
|
node.column,
|
|
237
|
+
scope.module.path,
|
|
203
238
|
);
|
|
204
239
|
}
|
|
205
240
|
}
|
|
@@ -210,6 +245,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
210
245
|
`'<${node.name}>' has no member '${picked}' to pick`,
|
|
211
246
|
node.line,
|
|
212
247
|
node.column,
|
|
248
|
+
scope.module.path,
|
|
213
249
|
);
|
|
214
250
|
}
|
|
215
251
|
}
|
|
@@ -254,7 +290,12 @@ function isTerminal(node: TreeNode): boolean {
|
|
|
254
290
|
export function build(resolved: ResolvedModule): TreeNode {
|
|
255
291
|
const root = resolved.module.root;
|
|
256
292
|
if (!root) {
|
|
257
|
-
throw new DfnError(
|
|
293
|
+
throw new DfnError(
|
|
294
|
+
`${resolved.name}.dfn declares no root`,
|
|
295
|
+
1,
|
|
296
|
+
1,
|
|
297
|
+
resolved.path,
|
|
298
|
+
);
|
|
258
299
|
}
|
|
259
300
|
const scope: Scope = { module: resolved, expanding: new Set() };
|
|
260
301
|
|
|
@@ -274,6 +315,7 @@ export function build(resolved: ResolvedModule): TreeNode {
|
|
|
274
315
|
`top-level '${name}' comes from both '${owner}' and '${option.name}'`,
|
|
275
316
|
option.line,
|
|
276
317
|
option.column,
|
|
318
|
+
resolved.path,
|
|
277
319
|
);
|
|
278
320
|
}
|
|
279
321
|
seen.set(name, option.name);
|
|
@@ -332,6 +374,7 @@ export function assertOpenSetsAreNameSets(
|
|
|
332
374
|
'* opens a set of names, not sub-paths',
|
|
333
375
|
node.line,
|
|
334
376
|
node.column,
|
|
377
|
+
scope.module.path,
|
|
335
378
|
);
|
|
336
379
|
}
|
|
337
380
|
assertOpenSetsAreNameSets(resolved, node.node, scope);
|
|
@@ -344,6 +387,7 @@ export function assertOpenSetsAreNameSets(
|
|
|
344
387
|
`'<${node.name}*>' opens a set, but the production is not name-only`,
|
|
345
388
|
node.line,
|
|
346
389
|
node.column,
|
|
390
|
+
scope.module.path,
|
|
347
391
|
);
|
|
348
392
|
}
|
|
349
393
|
}
|