@xdbml/parse 0.4.0 → 0.5.1
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/ast.d.ts +34 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/keywords.d.ts +3 -2
- package/dist/keywords.js +61 -3
- package/dist/module-resolver.d.ts +3 -3
- package/dist/module-resolver.js +32 -12
- package/dist/monarch.d.ts +1 -0
- package/dist/monarch.js +5 -1
- package/dist/name-resolver.d.ts +2 -2
- package/dist/name-resolver.js +131 -18
- package/dist/parser.d.ts +31 -7
- package/dist/parser.js +126 -19
- package/dist/supertypes.d.ts +110 -0
- package/dist/supertypes.js +526 -0
- package/package.json +1 -1
package/dist/name-resolver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Name resolution pass (spec §
|
|
2
|
+
* Name resolution pass (spec §27.10 / §27.15, parser batch P6).
|
|
3
3
|
*
|
|
4
4
|
* `resolveNames(doc)` walks an xDBML document (the flattened view; clone
|
|
5
5
|
* blocks have been merged) and produces:
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
42
42
|
import { flatten } from "./module-resolver.js";
|
|
43
43
|
import { checkRelationships } from "./relationships.js";
|
|
44
|
+
import { checkSupertypeGroups } from "./supertypes.js";
|
|
44
45
|
/**
|
|
45
46
|
* Read-only handle on the collected symbol table.
|
|
46
47
|
*
|
|
@@ -95,11 +96,18 @@ export class SymbolTable {
|
|
|
95
96
|
* Built-in type recognition
|
|
96
97
|
*
|
|
97
98
|
* Field type expressions can name a builtin scalar (`int`, `varchar`),
|
|
98
|
-
* a BSON type (`objectId`),
|
|
99
|
-
*
|
|
99
|
+
* a BSON type (`objectId`), a user-defined Named Type (`Email`) or Enum,
|
|
100
|
+
* or any other target-native type (`number`, `clob`, `serial`). The
|
|
101
|
+
* parser doesn't distinguish at parse time -- they all land as
|
|
100
102
|
* ScalarType nodes (or NamedTypeReference in some contexts). The
|
|
101
|
-
* resolver uses these sets
|
|
102
|
-
*
|
|
103
|
+
* resolver uses these sets only to skip the symbol-table lookup for
|
|
104
|
+
* names that can never be a Named Type reference.
|
|
105
|
+
*
|
|
106
|
+
* The sets are not a type vocabulary. Scalar type names pass through
|
|
107
|
+
* as written (spec §1.2, principle 4), so a name that is neither a
|
|
108
|
+
* builtin nor a declared Type or Enum is a target-native type, not an
|
|
109
|
+
* error. See `nearMissTypeName()` for the one diagnostic such a name
|
|
110
|
+
* can raise.
|
|
103
111
|
*
|
|
104
112
|
* Matching is case-insensitive: `Int`, `int`, `INT` all map to the same
|
|
105
113
|
* builtin per spec §3.8.
|
|
@@ -111,6 +119,87 @@ const BUILTIN_TYPES = new Set([
|
|
|
111
119
|
function isBuiltinType(name) {
|
|
112
120
|
return BUILTIN_TYPES.has(name.toLowerCase());
|
|
113
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The declared Types and Enums a bare type name can refer to. More than
|
|
124
|
+
* one entry means the name is declared in several containers (Enums can
|
|
125
|
+
* be container-scoped); the name still refers to a declaration, so it
|
|
126
|
+
* is neither a target-native type nor a near miss.
|
|
127
|
+
*/
|
|
128
|
+
function typeOrEnumDeclarations(name, symbols) {
|
|
129
|
+
const qualified = symbols.lookup(name);
|
|
130
|
+
if (qualified && (qualified.kind === 'type' || qualified.kind === 'enum'))
|
|
131
|
+
return [qualified];
|
|
132
|
+
return symbols.lookupAllBare(name).filter((e) => e.kind === 'type' || e.kind === 'enum');
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Near-miss detection for target-native type names.
|
|
136
|
+
*
|
|
137
|
+
* A type name that is neither a builtin nor a declared Type or Enum is
|
|
138
|
+
* accepted as a target-native type. The one case worth flagging is a
|
|
139
|
+
* name that differs only slightly from a declared Type or Enum -- most
|
|
140
|
+
* likely a misspelling (`Adress` for `Address`, `email` for `Email`).
|
|
141
|
+
* Returns the declared name to suggest, or undefined when nothing is
|
|
142
|
+
* close enough.
|
|
143
|
+
*
|
|
144
|
+
* Comparison is case-insensitive, so a difference of case alone counts
|
|
145
|
+
* as a near miss (identifiers are case-sensitive, spec §3.8). The edit
|
|
146
|
+
* distance allowed grows with the length of the name: none beyond case
|
|
147
|
+
* for three characters or fewer, one edit up to seven characters, two
|
|
148
|
+
* edits from eight. An adjacent transposition counts as one edit.
|
|
149
|
+
*/
|
|
150
|
+
function nearMissTypeName(name, symbols) {
|
|
151
|
+
const lower = name.toLowerCase();
|
|
152
|
+
const maxDistance = lower.length <= 3 ? 0 : lower.length <= 7 ? 1 : 2;
|
|
153
|
+
// A qualified name (`core.job_stauts`) compares against qualified
|
|
154
|
+
// declarations, a bare name against bare ones.
|
|
155
|
+
const qualified = name.includes('.');
|
|
156
|
+
let best;
|
|
157
|
+
for (const entry of symbols.entries()) {
|
|
158
|
+
if (entry.kind !== 'type' && entry.kind !== 'enum')
|
|
159
|
+
continue;
|
|
160
|
+
const declared = qualified ? entry.qualifiedName : entry.name;
|
|
161
|
+
const candidate = declared.toLowerCase();
|
|
162
|
+
if (Math.abs(candidate.length - lower.length) > maxDistance)
|
|
163
|
+
continue;
|
|
164
|
+
const distance = editDistance(lower, candidate, maxDistance);
|
|
165
|
+
if (distance <= maxDistance && (!best || distance < best.distance)) {
|
|
166
|
+
best = { name: declared, distance };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return best?.name;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Optimal string alignment distance (Levenshtein plus adjacent
|
|
173
|
+
* transposition). Returns `limit + 1` as soon as every alignment of a
|
|
174
|
+
* row exceeds `limit`, since callers only compare against the limit.
|
|
175
|
+
*/
|
|
176
|
+
function editDistance(a, b, limit) {
|
|
177
|
+
const rows = a.length + 1;
|
|
178
|
+
const cols = b.length + 1;
|
|
179
|
+
const d = [];
|
|
180
|
+
for (let i = 0; i < rows; i++) {
|
|
181
|
+
d.push(new Array(cols).fill(0));
|
|
182
|
+
d[i][0] = i;
|
|
183
|
+
}
|
|
184
|
+
for (let j = 0; j < cols; j++)
|
|
185
|
+
d[0][j] = j;
|
|
186
|
+
for (let i = 1; i < rows; i++) {
|
|
187
|
+
let rowMin = Infinity;
|
|
188
|
+
for (let j = 1; j < cols; j++) {
|
|
189
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
190
|
+
let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
191
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
192
|
+
v = Math.min(v, d[i - 2][j - 2] + 1);
|
|
193
|
+
}
|
|
194
|
+
d[i][j] = v;
|
|
195
|
+
if (v < rowMin)
|
|
196
|
+
rowMin = v;
|
|
197
|
+
}
|
|
198
|
+
if (rowMin > limit)
|
|
199
|
+
return limit + 1;
|
|
200
|
+
}
|
|
201
|
+
return d[rows - 1][cols - 1];
|
|
202
|
+
}
|
|
114
203
|
/* -------------------------------------------------------------------------
|
|
115
204
|
* Main entry point
|
|
116
205
|
* ----------------------------------------------------------------------- */
|
|
@@ -137,6 +226,8 @@ export function resolveNames(doc) {
|
|
|
137
226
|
resolveReferences(flat, symbols, diagnostics);
|
|
138
227
|
// Pass 3: relationship rules that the grammar cannot express (spec 11.11).
|
|
139
228
|
diagnostics.push(...checkRelationships(flat));
|
|
229
|
+
// Pass 4: supertype group rules (spec 12.8).
|
|
230
|
+
diagnostics.push(...checkSupertypeGroups(flat));
|
|
140
231
|
return { diagnostics, symbols };
|
|
141
232
|
}
|
|
142
233
|
/* -------------------------------------------------------------------------
|
|
@@ -156,9 +247,20 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
|
156
247
|
case 'TypeDeclaration':
|
|
157
248
|
addEntry(stmt.name, undefined, 'type', stmt, stmt.span, entries, diagnostics, seen);
|
|
158
249
|
return;
|
|
159
|
-
case 'EnumDeclaration':
|
|
160
|
-
|
|
250
|
+
case 'EnumDeclaration': {
|
|
251
|
+
// `enum core.job_status { ... }` (the DBML form) files the Enum
|
|
252
|
+
// under container `core`, exactly as `Container core { Enum
|
|
253
|
+
// job_status { ... } }` does: same qualified name, same bare name,
|
|
254
|
+
// and declaring both is a duplicate (spec §16).
|
|
255
|
+
const dot = stmt.name.lastIndexOf('.');
|
|
256
|
+
if (dot > 0) {
|
|
257
|
+
addEntry(stmt.name.slice(dot + 1), stmt.name.slice(0, dot), 'enum', stmt, stmt.span, entries, diagnostics, seen);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
|
|
261
|
+
}
|
|
161
262
|
return;
|
|
263
|
+
}
|
|
162
264
|
case 'EdgeDeclaration':
|
|
163
265
|
addEntry(stmt.name, undefined, 'edge', stmt, stmt.span, entries, diagnostics, seen);
|
|
164
266
|
return;
|
|
@@ -360,16 +462,18 @@ function resolveFieldDeclaration(field, symbols, diagnostics) {
|
|
|
360
462
|
function resolveTypeExpression(expr, symbols, diagnostics) {
|
|
361
463
|
switch (expr.kind) {
|
|
362
464
|
case 'ScalarType':
|
|
363
|
-
// A ScalarType is
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
465
|
+
// A ScalarType is a builtin, a reference to a declared Type or
|
|
466
|
+
// Enum, or a target-native type passed through as written (spec
|
|
467
|
+
// §1.2, principle 4) -- the parser doesn't distinguish at parse
|
|
468
|
+
// time. A target-native name is valid; the only diagnostic is a
|
|
469
|
+
// warning when it is a near miss of a declared Type or Enum.
|
|
470
|
+
if (!isBuiltinType(expr.name) && typeOrEnumDeclarations(expr.name, symbols).length === 0) {
|
|
471
|
+
const suggestion = nearMissTypeName(expr.name, symbols);
|
|
472
|
+
if (suggestion) {
|
|
369
473
|
diagnostics.push({
|
|
370
|
-
severity: '
|
|
371
|
-
code: '
|
|
372
|
-
message: `Type '${expr.name}' is not
|
|
474
|
+
severity: 'warning',
|
|
475
|
+
code: 'possible-type-typo',
|
|
476
|
+
message: `Type '${expr.name}' is not declared; did you mean '${suggestion}'? Otherwise it is kept as a target-native type.`,
|
|
373
477
|
span: expr.span,
|
|
374
478
|
});
|
|
375
479
|
}
|
|
@@ -821,9 +925,18 @@ function dereferenceNamedType(type, symbols, depth) {
|
|
|
821
925
|
return type;
|
|
822
926
|
}
|
|
823
927
|
const sym = symbols.lookup(name) ?? symbols.lookupBare(name);
|
|
928
|
+
if (type.kind === 'ScalarType' && (!sym || sym.kind !== 'type')) {
|
|
929
|
+
// An Enum or a target-native type: an opaque scalar, so a path that
|
|
930
|
+
// navigates into it gets the walker's shape diagnostic. A name
|
|
931
|
+
// declared as a Type or Enum in several containers stays unresolved.
|
|
932
|
+
if (typeOrEnumDeclarations(name, symbols).length > 1)
|
|
933
|
+
return undefined;
|
|
934
|
+
return type;
|
|
935
|
+
}
|
|
824
936
|
if (!sym || sym.kind !== 'type' || sym.declaration.kind !== 'TypeDeclaration') {
|
|
825
|
-
// Unresolved -- the field-type pass
|
|
826
|
-
// undefined so the walker bails without emitting a
|
|
937
|
+
// Unresolved Named Type reference -- the field-type pass diagnoses
|
|
938
|
+
// this. Return undefined so the walker bails without emitting a
|
|
939
|
+
// duplicate error.
|
|
827
940
|
return undefined;
|
|
828
941
|
}
|
|
829
942
|
const td = sym.declaration;
|
package/dist/parser.d.ts
CHANGED
|
@@ -13,8 +13,22 @@ import type { ParseOptions, Position, XDbmlDocument } from './ast.ts';
|
|
|
13
13
|
import type { Token } from './lexer.ts';
|
|
14
14
|
export declare class ParseError extends Error {
|
|
15
15
|
position: Position;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Optional machine-readable reason. Set for the errors a caller may want
|
|
18
|
+
* to tell apart from a plain syntax error: `unsupported-version` when a
|
|
19
|
+
* document declares a newer version than this parser supports (spec 4.1).
|
|
20
|
+
*/
|
|
21
|
+
code?: string;
|
|
22
|
+
constructor(message: string, position: Position, code?: string);
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The newest specification version this parser implements. A document
|
|
26
|
+
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
27
|
+
* semantics it does not have.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SUPPORTED_XDBML_VERSION = "0.5";
|
|
30
|
+
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
31
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
18
32
|
export declare class Parser {
|
|
19
33
|
private tokens;
|
|
20
34
|
private idx;
|
|
@@ -30,7 +44,7 @@ export declare class Parser {
|
|
|
30
44
|
* The set of file paths currently being parsed in the resolution chain.
|
|
31
45
|
* Used for cycle detection: when resolving a directive whose `from` path
|
|
32
46
|
* is already in this set, the parser produces an empty clone for that
|
|
33
|
-
* directive rather than recursing (matching spec §
|
|
47
|
+
* directive rather than recursing (matching spec §27.15: cycles are
|
|
34
48
|
* allowed; name resolution handles them). The set is passed by reference
|
|
35
49
|
* across recursive parse() calls so all transitive levels see it.
|
|
36
50
|
*
|
|
@@ -83,13 +97,13 @@ export declare class Parser {
|
|
|
83
97
|
private parseEntityBody;
|
|
84
98
|
private parsePartialInjection;
|
|
85
99
|
/**
|
|
86
|
-
* Parse a `records { ... }` block inside an entity body (§
|
|
100
|
+
* Parse a `records { ... }` block inside an entity body (§26.1, implicit
|
|
87
101
|
* column list). Values are stored as SettingValue cells; row boundaries
|
|
88
102
|
* are determined by source line (see `parseRecordRow`).
|
|
89
103
|
*/
|
|
90
104
|
private parseRecordsBlock;
|
|
91
105
|
/**
|
|
92
|
-
* Top-level records declaration (§
|
|
106
|
+
* Top-level records declaration (§26.2, new in v0.2):
|
|
93
107
|
*
|
|
94
108
|
* records users (id, name, email) { ... }
|
|
95
109
|
* records core.users (id, name, email) { ... }
|
|
@@ -151,12 +165,12 @@ export declare class Parser {
|
|
|
151
165
|
* that match the import items by name and element type (matching is
|
|
152
166
|
* downstream-consumer's job; the parser is permissive).
|
|
153
167
|
*
|
|
154
|
-
* Per spec §
|
|
168
|
+
* Per spec §27.6, clone content uses the importing file's vocabulary
|
|
155
169
|
* (aliases already applied) and is parsed under the importing file's
|
|
156
170
|
* xdbml version directive.
|
|
157
171
|
*
|
|
158
172
|
* Most clone-block content uses TopLevelStatement shapes (Entity, Type,
|
|
159
|
-
* Container, etc.). The exception is field imports (§
|
|
173
|
+
* Container, etc.). The exception is field imports (§27.8): when the
|
|
160
174
|
* directive imports one or more fields via `field <path>` items, the
|
|
161
175
|
* clone block holds each field as a bare FieldDeclaration with no entity
|
|
162
176
|
* wrapper. The dispatch below checks whether the next token starts a
|
|
@@ -278,7 +292,7 @@ export declare class Parser {
|
|
|
278
292
|
private parseCardinalityOperator;
|
|
279
293
|
private parseRefEndpoint;
|
|
280
294
|
/**
|
|
281
|
-
* Parse a dotted path with the §
|
|
295
|
+
* Parse a dotted path with the §20 segment vocabulary:
|
|
282
296
|
*
|
|
283
297
|
* IDENTIFIER -- a field segment
|
|
284
298
|
* .IDENTIFIER -- field
|
|
@@ -297,6 +311,16 @@ export declare class Parser {
|
|
|
297
311
|
private parsePathSegments;
|
|
298
312
|
private parseTablePartial;
|
|
299
313
|
private parseTableGroup;
|
|
314
|
+
/**
|
|
315
|
+
* `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
|
|
316
|
+
* Members are separated like TableGroup members: newline, comma or
|
|
317
|
+
* semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
|
|
318
|
+
* group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
|
|
319
|
+
* the parser never supplies one. Values are validated after parsing, by
|
|
320
|
+
* `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
|
|
321
|
+
* rather than stopping the parse.
|
|
322
|
+
*/
|
|
323
|
+
private parseSupertypeGroup;
|
|
300
324
|
private parseIndexes;
|
|
301
325
|
private parseIndexEntry;
|
|
302
326
|
private parseIndexComponent;
|
package/dist/parser.js
CHANGED
|
@@ -13,11 +13,37 @@ import { TokenKind, tokenize, } from "./lexer.js";
|
|
|
13
13
|
import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
|
|
14
14
|
export class ParseError extends Error {
|
|
15
15
|
position;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Optional machine-readable reason. Set for the errors a caller may want
|
|
18
|
+
* to tell apart from a plain syntax error: `unsupported-version` when a
|
|
19
|
+
* document declares a newer version than this parser supports (spec 4.1).
|
|
20
|
+
*/
|
|
21
|
+
code;
|
|
22
|
+
constructor(message, position, code) {
|
|
17
23
|
super(`${message} (line ${position.line}, column ${position.column})`);
|
|
18
24
|
this.position = position;
|
|
25
|
+
if (code)
|
|
26
|
+
this.code = code;
|
|
19
27
|
}
|
|
20
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The newest specification version this parser implements. A document
|
|
31
|
+
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
32
|
+
* semantics it does not have.
|
|
33
|
+
*/
|
|
34
|
+
export const SUPPORTED_XDBML_VERSION = '0.5';
|
|
35
|
+
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
36
|
+
export function compareVersions(a, b) {
|
|
37
|
+
const pa = a.split('.').map((n) => Number(n) || 0);
|
|
38
|
+
const pb = b.split('.').map((n) => Number(n) || 0);
|
|
39
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
40
|
+
const x = pa[i] ?? 0;
|
|
41
|
+
const y = pb[i] ?? 0;
|
|
42
|
+
if (x !== y)
|
|
43
|
+
return x < y ? -1 : 1;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
21
47
|
/* -------------------------------------------------------------------------
|
|
22
48
|
* Keyword recognition.
|
|
23
49
|
*
|
|
@@ -31,13 +57,13 @@ const CONTAINER_KEYWORDS = new Set([
|
|
|
31
57
|
const ENTITY_KEYWORDS = new Set(['table', 'entity', 'collection', 'record']);
|
|
32
58
|
/**
|
|
33
59
|
* Element-type keywords accepted in module-system import items
|
|
34
|
-
* (spec §
|
|
60
|
+
* (spec §27.3). Stored lowercased; matching is case-insensitive.
|
|
35
61
|
*
|
|
36
62
|
* `field` is recognized but explicitly rejected by parseImportItem in P4
|
|
37
63
|
* (field-level imports have special declaration-vs-placement semantics
|
|
38
64
|
* that will land in a later batch).
|
|
39
65
|
*
|
|
40
|
-
* `project` is intentionally excluded -- spec §
|
|
66
|
+
* `project` is intentionally excluded -- spec §27.1 forbids importing
|
|
41
67
|
* Project declarations.
|
|
42
68
|
*/
|
|
43
69
|
const IMPORT_ELEMENT_TYPES = new Set([
|
|
@@ -45,7 +71,7 @@ const IMPORT_ELEMENT_TYPES = new Set([
|
|
|
45
71
|
'enum', 'tablepartial', 'note',
|
|
46
72
|
'schema', 'container', 'tablegroup',
|
|
47
73
|
'type', 'edge', 'view', 'diagramview',
|
|
48
|
-
'field',
|
|
74
|
+
'field', 'supertypegroup',
|
|
49
75
|
]);
|
|
50
76
|
const STRUCTURAL_TYPE_KEYWORDS = new Set([
|
|
51
77
|
'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
|
|
@@ -98,7 +124,7 @@ export class Parser {
|
|
|
98
124
|
* The set of file paths currently being parsed in the resolution chain.
|
|
99
125
|
* Used for cycle detection: when resolving a directive whose `from` path
|
|
100
126
|
* is already in this set, the parser produces an empty clone for that
|
|
101
|
-
* directive rather than recursing (matching spec §
|
|
127
|
+
* directive rather than recursing (matching spec §27.15: cycles are
|
|
102
128
|
* allowed; name resolution handles them). The set is passed by reference
|
|
103
129
|
* across recursive parse() calls so all transitive levels see it.
|
|
104
130
|
*
|
|
@@ -179,6 +205,11 @@ export class Parser {
|
|
|
179
205
|
this.advance(); // xdbml
|
|
180
206
|
this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
|
|
181
207
|
const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
|
|
208
|
+
if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
|
|
209
|
+
throw new ParseError(`This document declares 'xdbml: ${numTok.text}', which is newer than the ` +
|
|
210
|
+
`latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
|
|
211
|
+
'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
|
|
212
|
+
}
|
|
182
213
|
return {
|
|
183
214
|
kind: 'VersionDeclaration',
|
|
184
215
|
version: numTok.text,
|
|
@@ -231,6 +262,8 @@ export class Parser {
|
|
|
231
262
|
return this.parseTablePartial();
|
|
232
263
|
if (k === 'tablegroup')
|
|
233
264
|
return this.parseTableGroup();
|
|
265
|
+
if (k === 'supertypegroup')
|
|
266
|
+
return this.parseSupertypeGroup();
|
|
234
267
|
if (k === 'note')
|
|
235
268
|
return this.parseNoteDeclaration();
|
|
236
269
|
if (k === 'records')
|
|
@@ -477,7 +510,7 @@ export class Parser {
|
|
|
477
510
|
};
|
|
478
511
|
}
|
|
479
512
|
/**
|
|
480
|
-
* Parse a `records { ... }` block inside an entity body (§
|
|
513
|
+
* Parse a `records { ... }` block inside an entity body (§26.1, implicit
|
|
481
514
|
* column list). Values are stored as SettingValue cells; row boundaries
|
|
482
515
|
* are determined by source line (see `parseRecordRow`).
|
|
483
516
|
*/
|
|
@@ -497,7 +530,7 @@ export class Parser {
|
|
|
497
530
|
};
|
|
498
531
|
}
|
|
499
532
|
/**
|
|
500
|
-
* Top-level records declaration (§
|
|
533
|
+
* Top-level records declaration (§26.2, new in v0.2):
|
|
501
534
|
*
|
|
502
535
|
* records users (id, name, email) { ... }
|
|
503
536
|
* records core.users (id, name, email) { ... }
|
|
@@ -587,7 +620,7 @@ export class Parser {
|
|
|
587
620
|
span: this.spanFrom(start),
|
|
588
621
|
};
|
|
589
622
|
}
|
|
590
|
-
/* ----- Module-system directives (spec §
|
|
623
|
+
/* ----- Module-system directives (spec §27, new in v0.2) ----- */
|
|
591
624
|
/**
|
|
592
625
|
* Parse a `use` or `reuse` directive. Called from both the top-level
|
|
593
626
|
* dispatcher and the Container body dispatcher; the caller indicates
|
|
@@ -701,7 +734,7 @@ export class Parser {
|
|
|
701
734
|
resolvedPath = result.resolvedPath;
|
|
702
735
|
break;
|
|
703
736
|
case 'cycle':
|
|
704
|
-
// Per spec §
|
|
737
|
+
// Per spec §27.15, cycles are allowed; the parser produces a
|
|
705
738
|
// directive with no clone, and name resolution (P6+) is
|
|
706
739
|
// expected to bridge the cycle. We leave clone undefined.
|
|
707
740
|
resolvedPath = result.resolvedPath;
|
|
@@ -759,12 +792,12 @@ export class Parser {
|
|
|
759
792
|
`Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
|
|
760
793
|
}
|
|
761
794
|
if (elementType === 'field' && context !== 'file-scope') {
|
|
762
|
-
// Spec §
|
|
795
|
+
// Spec §27.8: field imports must appear at file scope. Inside a
|
|
763
796
|
// Container body, the field's eventual placement (as a Named Type)
|
|
764
797
|
// would have no meaningful container scope -- field imports are
|
|
765
798
|
// always lifted to file scope by flatten(), regardless of where
|
|
766
799
|
// the directive sits.
|
|
767
|
-
throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §
|
|
800
|
+
throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §27.8).`, elemTok.start);
|
|
768
801
|
}
|
|
769
802
|
this.advance(); // consume element type keyword
|
|
770
803
|
// Dotted source path.
|
|
@@ -795,12 +828,12 @@ export class Parser {
|
|
|
795
828
|
* that match the import items by name and element type (matching is
|
|
796
829
|
* downstream-consumer's job; the parser is permissive).
|
|
797
830
|
*
|
|
798
|
-
* Per spec §
|
|
831
|
+
* Per spec §27.6, clone content uses the importing file's vocabulary
|
|
799
832
|
* (aliases already applied) and is parsed under the importing file's
|
|
800
833
|
* xdbml version directive.
|
|
801
834
|
*
|
|
802
835
|
* Most clone-block content uses TopLevelStatement shapes (Entity, Type,
|
|
803
|
-
* Container, etc.). The exception is field imports (§
|
|
836
|
+
* Container, etc.). The exception is field imports (§27.8): when the
|
|
804
837
|
* directive imports one or more fields via `field <path>` items, the
|
|
805
838
|
* clone block holds each field as a bare FieldDeclaration with no entity
|
|
806
839
|
* wrapper. The dispatch below checks whether the next token starts a
|
|
@@ -816,7 +849,7 @@ export class Parser {
|
|
|
816
849
|
statements.push(this.parseTopLevelStatement());
|
|
817
850
|
}
|
|
818
851
|
else {
|
|
819
|
-
// Bare field declaration -- the field-import case. Per spec §
|
|
852
|
+
// Bare field declaration -- the field-import case. Per spec §27.6
|
|
820
853
|
// the field appears without an entity wrapper.
|
|
821
854
|
statements.push(this.parseFieldDeclaration());
|
|
822
855
|
}
|
|
@@ -1256,7 +1289,14 @@ export class Parser {
|
|
|
1256
1289
|
throw new ParseError(`Expected type name, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
|
|
1257
1290
|
}
|
|
1258
1291
|
this.advance();
|
|
1259
|
-
|
|
1292
|
+
let name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
|
|
1293
|
+
// A qualified type name, `core.job_status`, names an Enum declared in
|
|
1294
|
+
// a container (spec §16), or a target-native type such as
|
|
1295
|
+
// `public.geometry` that passes through as written.
|
|
1296
|
+
while (this.check(TokenKind.Dot)) {
|
|
1297
|
+
this.advance();
|
|
1298
|
+
name += `.${this.parseIdentLikeName('type name after dot')}`;
|
|
1299
|
+
}
|
|
1260
1300
|
let params;
|
|
1261
1301
|
if (this.check(TokenKind.LParen)) {
|
|
1262
1302
|
this.advance();
|
|
@@ -1284,7 +1324,7 @@ export class Parser {
|
|
|
1284
1324
|
}
|
|
1285
1325
|
throw new ParseError(`Expected type parameter, got ${t.kind}`, t.start);
|
|
1286
1326
|
}
|
|
1287
|
-
/* ----- Type declaration (§
|
|
1327
|
+
/* ----- Type declaration (§15) ----- */
|
|
1288
1328
|
parseTypeDecl() {
|
|
1289
1329
|
const start = this.peek().start;
|
|
1290
1330
|
this.advance(); // Type
|
|
@@ -1293,7 +1333,7 @@ export class Parser {
|
|
|
1293
1333
|
//
|
|
1294
1334
|
// { ... } v0.1 object form, no pre-body settings
|
|
1295
1335
|
// [ settings ] { ... } v0.1 object form, pre-body settings (permissive)
|
|
1296
|
-
// typeExpression v0.2 scalar form (spec §
|
|
1336
|
+
// typeExpression v0.2 scalar form (spec §15.7)
|
|
1297
1337
|
// typeExpression [ settings ] v0.2 scalar form with field-level settings
|
|
1298
1338
|
//
|
|
1299
1339
|
// Note that LBrace and LBracket are distinct from any start-of-type-expression
|
|
@@ -1422,7 +1462,13 @@ export class Parser {
|
|
|
1422
1462
|
parseEnum() {
|
|
1423
1463
|
const start = this.peek().start;
|
|
1424
1464
|
const kwTok = this.advance(); // enum
|
|
1425
|
-
|
|
1465
|
+
// DBML form `enum core.job_status { ... }`: the qualifier names the
|
|
1466
|
+
// container, as for a schema-qualified Table (spec §7.3, §16).
|
|
1467
|
+
let name = this.parseIdentLikeName('enum name');
|
|
1468
|
+
while (this.check(TokenKind.Dot)) {
|
|
1469
|
+
this.advance();
|
|
1470
|
+
name += `.${this.parseIdentLikeName('enum name after dot')}`;
|
|
1471
|
+
}
|
|
1426
1472
|
this.expect(TokenKind.LBrace, "Expected '{' after enum name");
|
|
1427
1473
|
const values = [];
|
|
1428
1474
|
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
@@ -1572,7 +1618,7 @@ export class Parser {
|
|
|
1572
1618
|
};
|
|
1573
1619
|
}
|
|
1574
1620
|
/**
|
|
1575
|
-
* Parse a dotted path with the §
|
|
1621
|
+
* Parse a dotted path with the §20 segment vocabulary:
|
|
1576
1622
|
*
|
|
1577
1623
|
* IDENTIFIER -- a field segment
|
|
1578
1624
|
* .IDENTIFIER -- field
|
|
@@ -1746,6 +1792,67 @@ export class Parser {
|
|
|
1746
1792
|
span: this.spanFrom(start),
|
|
1747
1793
|
};
|
|
1748
1794
|
}
|
|
1795
|
+
/* ----- SupertypeGroup (spec §12, new in v0.5) ----- */
|
|
1796
|
+
/**
|
|
1797
|
+
* `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
|
|
1798
|
+
* Members are separated like TableGroup members: newline, comma or
|
|
1799
|
+
* semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
|
|
1800
|
+
* group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
|
|
1801
|
+
* the parser never supplies one. Values are validated after parsing, by
|
|
1802
|
+
* `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
|
|
1803
|
+
* rather than stopping the parse.
|
|
1804
|
+
*/
|
|
1805
|
+
parseSupertypeGroup() {
|
|
1806
|
+
const start = this.peek().start;
|
|
1807
|
+
this.advance(); // SupertypeGroup
|
|
1808
|
+
const nameTok = this.peek();
|
|
1809
|
+
if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
|
|
1810
|
+
throw new ParseError('Expected a name after SupertypeGroup. Every group has a name (spec §12.1); ' +
|
|
1811
|
+
'a group exported without one is written undefinedGroup1, undefinedGroup2, ...', nameTok.start);
|
|
1812
|
+
}
|
|
1813
|
+
const name = this.parseIdentLikeName('SupertypeGroup name');
|
|
1814
|
+
const settings = this.maybeSettingsBlock();
|
|
1815
|
+
this.expect(TokenKind.LBrace, "Expected '{' after SupertypeGroup name and settings");
|
|
1816
|
+
const members = [];
|
|
1817
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
1818
|
+
const t = this.peek();
|
|
1819
|
+
if (t.kind === TokenKind.Identifier || t.kind === TokenKind.QuotedIdentifier) {
|
|
1820
|
+
const memberStart = t.start;
|
|
1821
|
+
this.advance();
|
|
1822
|
+
let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
|
|
1823
|
+
while (this.check(TokenKind.Dot)) {
|
|
1824
|
+
this.advance();
|
|
1825
|
+
const next = this.peek();
|
|
1826
|
+
if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
|
|
1827
|
+
throw new ParseError('Expected identifier after dot in subtype path', next.start);
|
|
1828
|
+
}
|
|
1829
|
+
this.advance();
|
|
1830
|
+
n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
|
|
1831
|
+
}
|
|
1832
|
+
const memberSettings = this.maybeSettingsBlock();
|
|
1833
|
+
members.push({
|
|
1834
|
+
kind: 'SupertypeGroupMember',
|
|
1835
|
+
name: n,
|
|
1836
|
+
settings: memberSettings,
|
|
1837
|
+
span: this.spanFrom(memberStart),
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
else if (t.kind === TokenKind.Semicolon || t.kind === TokenKind.Comma) {
|
|
1841
|
+
this.advance();
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
throw new ParseError(`Unexpected ${t.kind} in SupertypeGroup body; expected a subtype entity name`, t.start);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
this.expect(TokenKind.RBrace, "Expected '}' closing SupertypeGroup");
|
|
1848
|
+
return {
|
|
1849
|
+
kind: 'SupertypeGroupDeclaration',
|
|
1850
|
+
name,
|
|
1851
|
+
settings,
|
|
1852
|
+
members,
|
|
1853
|
+
span: this.spanFrom(start),
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1749
1856
|
/* ----- Indexes ----- */
|
|
1750
1857
|
parseIndexes() {
|
|
1751
1858
|
const start = this.peek().start;
|