@xdbml/parse 0.3.2 → 0.5.0
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 +24 -8
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/keywords.d.ts +3 -3
- package/dist/keywords.js +14 -0
- package/dist/module-resolver.d.ts +3 -3
- package/dist/module-resolver.js +21 -11
- package/dist/name-resolver.d.ts +2 -2
- package/dist/name-resolver.js +43 -3
- package/dist/parser.d.ts +31 -7
- package/dist/parser.js +116 -18
- package/dist/relationships.d.ts +99 -0
- package/dist/relationships.js +436 -0
- package/dist/supertypes.d.ts +110 -0
- package/dist/supertypes.js +526 -0
- package/package.json +2 -1
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supertype groups (spec §12, new in v0.5).
|
|
3
|
+
*
|
|
4
|
+
* Three things live here:
|
|
5
|
+
*
|
|
6
|
+
* - Setting normalization. `completeness`, `exclusivity`, `strategy` and
|
|
7
|
+
* `merge` accept a canonical value and a set of aliases (spec §12.2).
|
|
8
|
+
* The AST keeps what was written; `supertypeGroupSettings()` returns
|
|
9
|
+
* the canonical values.
|
|
10
|
+
* - Member resolution. The supertype and every subtype name an entity by
|
|
11
|
+
* path, bare (`Person`) or container-qualified (`crm.Person`), with the
|
|
12
|
+
* same reading as an entity-level `Ref` endpoint (spec §11.16.3).
|
|
13
|
+
* - The validation rules summarized in spec §12.8, run by `resolveNames()`
|
|
14
|
+
* on the flattened document so imported and cloned groups are checked
|
|
15
|
+
* like local ones.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here computes the attributes or keys a subtype would hold in a
|
|
18
|
+
* physical model. A subtype declares its own attributes only; placing the
|
|
19
|
+
* supertype's attributes in stored structures is derivation output (spec
|
|
20
|
+
* §12.5, §12.7). Supertype chains are computed for the structural checks
|
|
21
|
+
* and for tools that display the hierarchy.
|
|
22
|
+
*/
|
|
23
|
+
import { versionAtLeast } from "./relationships.js";
|
|
24
|
+
/**
|
|
25
|
+
* Accepted spellings, keyed by lowercase spelling, mapped to the canonical
|
|
26
|
+
* value. Canonical values map to themselves.
|
|
27
|
+
*/
|
|
28
|
+
export const SUPERTYPE_GROUP_VALUES = {
|
|
29
|
+
completeness: {
|
|
30
|
+
total: 'total',
|
|
31
|
+
complete: 'total',
|
|
32
|
+
partial: 'partial',
|
|
33
|
+
incomplete: 'partial',
|
|
34
|
+
},
|
|
35
|
+
exclusivity: {
|
|
36
|
+
disjoint: 'disjoint',
|
|
37
|
+
exclusive: 'disjoint',
|
|
38
|
+
overlapping: 'overlapping',
|
|
39
|
+
non_exclusive: 'overlapping',
|
|
40
|
+
},
|
|
41
|
+
strategy: {
|
|
42
|
+
preserved_hierarchy: 'preserved_hierarchy',
|
|
43
|
+
class_table: 'preserved_hierarchy',
|
|
44
|
+
joined: 'preserved_hierarchy',
|
|
45
|
+
roll_up: 'roll_up',
|
|
46
|
+
single_table: 'roll_up',
|
|
47
|
+
roll_down: 'roll_down',
|
|
48
|
+
concrete_table: 'roll_down',
|
|
49
|
+
table_per_class: 'roll_down',
|
|
50
|
+
},
|
|
51
|
+
merge: {
|
|
52
|
+
flat: 'flat',
|
|
53
|
+
flat_with_discriminator: 'flat',
|
|
54
|
+
nested: 'nested',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const CANONICAL_VALUES = {
|
|
58
|
+
completeness: ['total', 'partial'],
|
|
59
|
+
exclusivity: ['disjoint', 'overlapping'],
|
|
60
|
+
strategy: ['preserved_hierarchy', 'roll_up', 'roll_down'],
|
|
61
|
+
merge: ['flat', 'nested'],
|
|
62
|
+
};
|
|
63
|
+
/** The canonical value for a spelling, or undefined when it is not recognized. */
|
|
64
|
+
export function canonicalSupertypeGroupValue(setting, raw) {
|
|
65
|
+
const table = SUPERTYPE_GROUP_VALUES[setting];
|
|
66
|
+
const key = raw.toLowerCase();
|
|
67
|
+
return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;
|
|
68
|
+
}
|
|
69
|
+
/** The text of an identifier or string setting value. */
|
|
70
|
+
function settingText(s) {
|
|
71
|
+
if (!s || !s.value)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (s.value.kind === 'IdentifierValue' || s.value.kind === 'StringValue')
|
|
74
|
+
return s.value.value;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function findSetting(settings, name) {
|
|
78
|
+
return settings.find((s) => s.name === name);
|
|
79
|
+
}
|
|
80
|
+
export function supertypeGroupSettings(decl) {
|
|
81
|
+
const out = {};
|
|
82
|
+
const supertype = settingText(findSetting(decl.settings, 'supertype'));
|
|
83
|
+
if (supertype)
|
|
84
|
+
out.supertype = supertype;
|
|
85
|
+
for (const key of ['completeness', 'exclusivity', 'strategy', 'merge']) {
|
|
86
|
+
const raw = settingText(findSetting(decl.settings, key));
|
|
87
|
+
const value = raw === undefined ? undefined : canonicalSupertypeGroupValue(key, raw);
|
|
88
|
+
if (value)
|
|
89
|
+
out[key] = value;
|
|
90
|
+
}
|
|
91
|
+
const discriminator = settingText(findSetting(decl.settings, 'discriminator'));
|
|
92
|
+
if (discriminator)
|
|
93
|
+
out.discriminator = discriminator;
|
|
94
|
+
const note = settingText(findSetting(decl.settings, 'note'));
|
|
95
|
+
if (note !== undefined)
|
|
96
|
+
out.note = note;
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** A subtype member's own `strategy` (spec §12.7.4), canonical, or undefined. */
|
|
100
|
+
export function subtypeStrategy(member) {
|
|
101
|
+
const raw = settingText(findSetting(member.settings, 'strategy'));
|
|
102
|
+
if (raw === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
return canonicalSupertypeGroupValue('strategy', raw);
|
|
105
|
+
}
|
|
106
|
+
function buildIndex(doc) {
|
|
107
|
+
const byId = new Map();
|
|
108
|
+
const byBare = new Map();
|
|
109
|
+
const otherKinds = new Map();
|
|
110
|
+
const partials = new Map();
|
|
111
|
+
const addEntity = (e, container) => {
|
|
112
|
+
const id = container ? `${container}.${e.name}` : e.name;
|
|
113
|
+
if (byId.has(id))
|
|
114
|
+
return;
|
|
115
|
+
byId.set(id, e);
|
|
116
|
+
const list = byBare.get(e.name) ?? [];
|
|
117
|
+
list.push(id);
|
|
118
|
+
byBare.set(e.name, list);
|
|
119
|
+
};
|
|
120
|
+
const addOther = (name, kind) => {
|
|
121
|
+
if (!otherKinds.has(name))
|
|
122
|
+
otherKinds.set(name, kind);
|
|
123
|
+
};
|
|
124
|
+
for (const stmt of doc.statements) {
|
|
125
|
+
switch (stmt.kind) {
|
|
126
|
+
case 'EntityDeclaration':
|
|
127
|
+
addEntity(stmt);
|
|
128
|
+
break;
|
|
129
|
+
case 'ContainerDeclaration': {
|
|
130
|
+
const c = stmt;
|
|
131
|
+
addOther(c.name, 'Container');
|
|
132
|
+
for (const item of c.body) {
|
|
133
|
+
if (item.kind === 'EntityDeclaration')
|
|
134
|
+
addEntity(item, c.name);
|
|
135
|
+
else if (item.kind === 'ViewDeclaration')
|
|
136
|
+
addOther(`${c.name}.${item.name}`, 'View');
|
|
137
|
+
else if (item.kind === 'EdgeDeclaration')
|
|
138
|
+
addOther(`${c.name}.${item.name}`, 'Edge');
|
|
139
|
+
else if (item.kind === 'EnumDeclaration')
|
|
140
|
+
addOther(`${c.name}.${item.name}`, 'Enum');
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'ViewDeclaration':
|
|
145
|
+
addOther(stmt.name, 'View');
|
|
146
|
+
break;
|
|
147
|
+
case 'EdgeDeclaration':
|
|
148
|
+
addOther(stmt.name, 'Edge');
|
|
149
|
+
break;
|
|
150
|
+
case 'TypeDeclaration':
|
|
151
|
+
addOther(stmt.name, 'Type');
|
|
152
|
+
break;
|
|
153
|
+
case 'EnumDeclaration':
|
|
154
|
+
addOther(stmt.name, 'Enum');
|
|
155
|
+
break;
|
|
156
|
+
case 'TableGroupDeclaration':
|
|
157
|
+
addOther(stmt.name, 'TableGroup');
|
|
158
|
+
break;
|
|
159
|
+
case 'TablePartialDeclaration':
|
|
160
|
+
addOther(stmt.name, 'TablePartial');
|
|
161
|
+
if (!partials.has(stmt.name))
|
|
162
|
+
partials.set(stmt.name, stmt);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { byId, byBare, otherKinds, partials };
|
|
169
|
+
}
|
|
170
|
+
function resolveEntityPath(path, index) {
|
|
171
|
+
if (index.byId.has(path))
|
|
172
|
+
return { ok: true, id: path };
|
|
173
|
+
if (!path.includes('.')) {
|
|
174
|
+
const ids = index.byBare.get(path) ?? [];
|
|
175
|
+
if (ids.length === 1)
|
|
176
|
+
return { ok: true, id: ids[0] };
|
|
177
|
+
if (ids.length > 1)
|
|
178
|
+
return { ok: false, reason: 'ambiguous', candidates: ids };
|
|
179
|
+
}
|
|
180
|
+
const kind = index.otherKinds.get(path);
|
|
181
|
+
if (kind)
|
|
182
|
+
return { ok: false, reason: 'not-entity', kind };
|
|
183
|
+
return { ok: false, reason: 'unresolved' };
|
|
184
|
+
}
|
|
185
|
+
function groupsOf(doc) {
|
|
186
|
+
return doc.statements.filter((s) => s.kind === 'SupertypeGroupDeclaration');
|
|
187
|
+
}
|
|
188
|
+
function resolveWith(doc, index) {
|
|
189
|
+
return groupsOf(doc).map((decl) => {
|
|
190
|
+
const settings = supertypeGroupSettings(decl);
|
|
191
|
+
const sup = settings.supertype ? resolveEntityPath(settings.supertype, index) : undefined;
|
|
192
|
+
return {
|
|
193
|
+
declaration: decl,
|
|
194
|
+
settings,
|
|
195
|
+
supertype: sup && sup.ok ? sup.id : undefined,
|
|
196
|
+
subtypes: decl.members.map((member) => {
|
|
197
|
+
const r = resolveEntityPath(member.name, index);
|
|
198
|
+
return {
|
|
199
|
+
member,
|
|
200
|
+
entity: r.ok ? r.id : undefined,
|
|
201
|
+
strategy: subtypeStrategy(member),
|
|
202
|
+
};
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Every SupertypeGroup of a document with its members resolved. Pass the
|
|
209
|
+
* flattened document (see `flatten()`) so imported and cloned declarations
|
|
210
|
+
* are visible. Entity ids are container-qualified for entities declared in
|
|
211
|
+
* a Container, bare otherwise.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveSupertypeGroups(doc) {
|
|
214
|
+
return resolveWith(doc, buildIndex(doc));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* For each entity that is a subtype, its chain of supertypes, nearest
|
|
218
|
+
* first: `Employee -> [Person, Party]`. When an entity is listed as a
|
|
219
|
+
* subtype in several groups (an error), the first group wins; a chain that
|
|
220
|
+
* loops stops before repeating an entity.
|
|
221
|
+
*/
|
|
222
|
+
export function supertypeChains(doc) {
|
|
223
|
+
return chainsFrom(parentMap(resolveSupertypeGroups(doc)));
|
|
224
|
+
}
|
|
225
|
+
function parentMap(groups) {
|
|
226
|
+
const parent = new Map();
|
|
227
|
+
for (const g of groups) {
|
|
228
|
+
if (!g.supertype)
|
|
229
|
+
continue;
|
|
230
|
+
for (const s of g.subtypes) {
|
|
231
|
+
if (!s.entity || s.entity === g.supertype)
|
|
232
|
+
continue;
|
|
233
|
+
if (!parent.has(s.entity))
|
|
234
|
+
parent.set(s.entity, g.supertype);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return parent;
|
|
238
|
+
}
|
|
239
|
+
function chainsFrom(parent) {
|
|
240
|
+
const chains = new Map();
|
|
241
|
+
for (const start of parent.keys()) {
|
|
242
|
+
const chain = [];
|
|
243
|
+
const seen = new Set([start]);
|
|
244
|
+
let cur = parent.get(start);
|
|
245
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
246
|
+
chain.push(cur);
|
|
247
|
+
seen.add(cur);
|
|
248
|
+
cur = parent.get(cur);
|
|
249
|
+
}
|
|
250
|
+
chains.set(start, chain);
|
|
251
|
+
}
|
|
252
|
+
return chains;
|
|
253
|
+
}
|
|
254
|
+
function declaredAttributes(entity, index) {
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const item of entity.body) {
|
|
257
|
+
if (item.kind === 'FieldDeclaration') {
|
|
258
|
+
out.push({ name: item.name, span: item.span });
|
|
259
|
+
}
|
|
260
|
+
else if (item.kind === 'PartialInjection') {
|
|
261
|
+
const partial = index.partials.get(item.partialName);
|
|
262
|
+
if (!partial)
|
|
263
|
+
continue;
|
|
264
|
+
for (const p of partial.body) {
|
|
265
|
+
if (p.kind === 'FieldDeclaration') {
|
|
266
|
+
out.push({ name: p.name, span: item.span, viaPartial: item.partialName });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
/* -------------------------------------------------------------------------
|
|
274
|
+
* Validation (spec §12.8)
|
|
275
|
+
* ----------------------------------------------------------------------- */
|
|
276
|
+
function listValues(setting) {
|
|
277
|
+
const canonical = CANONICAL_VALUES[setting];
|
|
278
|
+
const aliases = Object.keys(SUPERTYPE_GROUP_VALUES[setting]).filter((k) => !canonical.includes(k));
|
|
279
|
+
const main = canonical.length === 2 ? `${canonical[0]} or ${canonical[1]}` : `${canonical.slice(0, -1).join(', ')} or ${canonical[canonical.length - 1]}`;
|
|
280
|
+
return aliases.length > 0 ? `${main} (aliases: ${aliases.join(', ')})` : main;
|
|
281
|
+
}
|
|
282
|
+
function describeFailure(what, path, r) {
|
|
283
|
+
if (r.ok)
|
|
284
|
+
return '';
|
|
285
|
+
switch (r.reason) {
|
|
286
|
+
case 'ambiguous':
|
|
287
|
+
return `${what} '${path}' matches entities in several containers (${r.candidates.join(', ')}); qualify it with its container.`;
|
|
288
|
+
case 'not-entity':
|
|
289
|
+
return `${what} '${path}' names a ${r.kind}. A supertype group relates entities only (spec §12.1).`;
|
|
290
|
+
default:
|
|
291
|
+
return `${what} '${path}' does not resolve to an entity.`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The rules of spec §12.8. Run by `resolveNames()` on the flattened
|
|
296
|
+
* document; exported for callers that check groups on their own.
|
|
297
|
+
*/
|
|
298
|
+
export function checkSupertypeGroups(doc) {
|
|
299
|
+
const groups = groupsOf(doc);
|
|
300
|
+
if (groups.length === 0)
|
|
301
|
+
return [];
|
|
302
|
+
const diagnostics = [];
|
|
303
|
+
const index = buildIndex(doc);
|
|
304
|
+
const resolved = resolveWith(doc, index);
|
|
305
|
+
// Section 4: the construct requires a document declaring 0.5 or later.
|
|
306
|
+
if (!versionAtLeast(doc, '0.5')) {
|
|
307
|
+
diagnostics.push({
|
|
308
|
+
severity: 'error',
|
|
309
|
+
code: 'construct-requires-version',
|
|
310
|
+
message: "SupertypeGroup requires a document declaring 'xdbml: 0.5' or later.",
|
|
311
|
+
span: doc.version ? doc.version.span : groups[0].span,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// Group names are unique among supertype groups (spec §12.1).
|
|
315
|
+
const names = new Set();
|
|
316
|
+
for (const g of groups) {
|
|
317
|
+
if (names.has(g.name)) {
|
|
318
|
+
diagnostics.push({
|
|
319
|
+
severity: 'error',
|
|
320
|
+
code: 'duplicate-declaration',
|
|
321
|
+
message: `Duplicate SupertypeGroup declaration '${g.name}'. Each supertype group must have a unique name.`,
|
|
322
|
+
span: g.span,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
names.add(g.name);
|
|
326
|
+
}
|
|
327
|
+
for (const r of resolved) {
|
|
328
|
+
const g = r.declaration;
|
|
329
|
+
// Values of the group's settings (spec §12.2).
|
|
330
|
+
for (const key of ['completeness', 'exclusivity', 'strategy', 'merge']) {
|
|
331
|
+
const s = findSetting(g.settings, key);
|
|
332
|
+
if (!s)
|
|
333
|
+
continue;
|
|
334
|
+
const raw = settingText(s);
|
|
335
|
+
if (raw === undefined || !canonicalSupertypeGroupValue(key, raw)) {
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
severity: 'error',
|
|
338
|
+
code: 'invalid-supertype-group-value',
|
|
339
|
+
message: `Invalid ${key} ${raw === undefined ? 'value' : `'${raw}'`} on SupertypeGroup '${g.name}'. Expected ${listValues(key)}.`,
|
|
340
|
+
span: s.span,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// The supertype (spec §12.1).
|
|
345
|
+
const supSetting = findSetting(g.settings, 'supertype');
|
|
346
|
+
const supPath = settingText(supSetting);
|
|
347
|
+
if (!supSetting || supPath === undefined) {
|
|
348
|
+
diagnostics.push({
|
|
349
|
+
severity: 'error',
|
|
350
|
+
code: 'missing-supertype',
|
|
351
|
+
message: `SupertypeGroup '${g.name}' names no supertype. Add a 'supertype:' setting naming an entity.`,
|
|
352
|
+
span: supSetting ? supSetting.span : g.span,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
const res = resolveEntityPath(supPath, index);
|
|
357
|
+
if (!res.ok) {
|
|
358
|
+
diagnostics.push({
|
|
359
|
+
severity: 'error',
|
|
360
|
+
code: 'unresolved-supertype-group-member',
|
|
361
|
+
message: describeFailure('Supertype', supPath, res),
|
|
362
|
+
span: supSetting.span,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Discriminator on an overlapping group (spec §12.7.3).
|
|
367
|
+
const disc = findSetting(g.settings, 'discriminator');
|
|
368
|
+
if (disc && r.settings.exclusivity === 'overlapping') {
|
|
369
|
+
diagnostics.push({
|
|
370
|
+
severity: 'error',
|
|
371
|
+
code: 'discriminator-on-overlapping-group',
|
|
372
|
+
message: `SupertypeGroup '${g.name}' is overlapping, so one instance may belong to several subtypes and a single discriminator attribute cannot record its membership (spec §12.7.3).`,
|
|
373
|
+
span: disc.span,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
// Empty group (spec §12.1): valid, reported as a warning.
|
|
377
|
+
if (g.members.length === 0) {
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
severity: 'warning',
|
|
380
|
+
code: 'empty-supertype-group',
|
|
381
|
+
message: `SupertypeGroup '${g.name}' lists no subtype yet.`,
|
|
382
|
+
span: g.span,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// Members.
|
|
386
|
+
const seenInGroup = new Set();
|
|
387
|
+
for (const s of r.subtypes) {
|
|
388
|
+
const m = s.member;
|
|
389
|
+
const stratSetting = findSetting(m.settings, 'strategy');
|
|
390
|
+
if (stratSetting) {
|
|
391
|
+
const raw = settingText(stratSetting);
|
|
392
|
+
if (raw === undefined || !canonicalSupertypeGroupValue('strategy', raw)) {
|
|
393
|
+
diagnostics.push({
|
|
394
|
+
severity: 'error',
|
|
395
|
+
code: 'invalid-supertype-group-value',
|
|
396
|
+
message: `Invalid strategy ${raw === undefined ? 'value' : `'${raw}'`} on subtype '${m.name}' of SupertypeGroup '${g.name}'. Expected ${listValues('strategy')}.`,
|
|
397
|
+
span: stratSetting.span,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const res = resolveEntityPath(m.name, index);
|
|
402
|
+
if (!res.ok) {
|
|
403
|
+
diagnostics.push({
|
|
404
|
+
severity: 'error',
|
|
405
|
+
code: 'unresolved-supertype-group-member',
|
|
406
|
+
message: describeFailure('Subtype', m.name, res),
|
|
407
|
+
span: m.span,
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (seenInGroup.has(res.id)) {
|
|
412
|
+
diagnostics.push({
|
|
413
|
+
severity: 'error',
|
|
414
|
+
code: 'duplicate-subtype',
|
|
415
|
+
message: `'${m.name}' is listed twice in SupertypeGroup '${g.name}'.`,
|
|
416
|
+
span: m.span,
|
|
417
|
+
});
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
seenInGroup.add(res.id);
|
|
421
|
+
if (r.supertype && res.id === r.supertype) {
|
|
422
|
+
diagnostics.push({
|
|
423
|
+
severity: 'error',
|
|
424
|
+
code: 'supertype-is-subtype',
|
|
425
|
+
message: `'${m.name}' is the supertype of SupertypeGroup '${g.name}' and cannot also be one of its subtypes.`,
|
|
426
|
+
span: m.span,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Merge with nothing to apply to (spec §12.7.2).
|
|
431
|
+
const merge = findSetting(g.settings, 'merge');
|
|
432
|
+
if (merge && r.settings.merge) {
|
|
433
|
+
const rollUp = r.settings.strategy === 'roll_up' || r.subtypes.some((s) => s.strategy === 'roll_up');
|
|
434
|
+
if (!rollUp) {
|
|
435
|
+
diagnostics.push({
|
|
436
|
+
severity: 'warning',
|
|
437
|
+
code: 'merge-without-roll-up',
|
|
438
|
+
message: `'merge' on SupertypeGroup '${g.name}' applies to roll-up, but neither the group nor any subtype uses 'strategy: roll_up'.`,
|
|
439
|
+
span: merge.span,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Single inheritance: an entity is a subtype in one group only (spec §12.4.3).
|
|
445
|
+
const subtypeHome = new Map();
|
|
446
|
+
for (const r of resolved) {
|
|
447
|
+
for (const s of r.subtypes) {
|
|
448
|
+
if (!s.entity || s.entity === r.supertype)
|
|
449
|
+
continue;
|
|
450
|
+
const home = subtypeHome.get(s.entity);
|
|
451
|
+
if (home === undefined) {
|
|
452
|
+
subtypeHome.set(s.entity, r.declaration.name);
|
|
453
|
+
}
|
|
454
|
+
else if (home !== r.declaration.name) {
|
|
455
|
+
diagnostics.push({
|
|
456
|
+
severity: 'error',
|
|
457
|
+
code: 'subtype-in-multiple-groups',
|
|
458
|
+
message: `'${s.member.name}' is already a subtype in SupertypeGroup '${home}'. An entity is a subtype in one group only (spec §12.4.3).`,
|
|
459
|
+
span: s.member.span,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Cycles across levels (spec §12.4.4).
|
|
465
|
+
const parent = parentMap(resolved);
|
|
466
|
+
const memberSpan = new Map();
|
|
467
|
+
for (const r of resolved) {
|
|
468
|
+
for (const s of r.subtypes) {
|
|
469
|
+
if (s.entity && !memberSpan.has(s.entity))
|
|
470
|
+
memberSpan.set(s.entity, s.member.span);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const reported = new Set();
|
|
474
|
+
for (const start of parent.keys()) {
|
|
475
|
+
const path = [start];
|
|
476
|
+
const seen = new Set([start]);
|
|
477
|
+
let cur = parent.get(start);
|
|
478
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
479
|
+
path.push(cur);
|
|
480
|
+
seen.add(cur);
|
|
481
|
+
cur = parent.get(cur);
|
|
482
|
+
}
|
|
483
|
+
if (cur !== start)
|
|
484
|
+
continue;
|
|
485
|
+
const key = [...path].sort().join('|');
|
|
486
|
+
if (reported.has(key))
|
|
487
|
+
continue;
|
|
488
|
+
reported.add(key);
|
|
489
|
+
diagnostics.push({
|
|
490
|
+
severity: 'error',
|
|
491
|
+
code: 'supertype-cycle',
|
|
492
|
+
message: `Supertype groups form a cycle: ${[...path, start].join(' -> ')}. An entity cannot be its own supertype (spec §12.4.4).`,
|
|
493
|
+
span: memberSpan.get(start) ?? resolved[0].declaration.span,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// Redeclaration of a supertype's attribute (spec §12.5).
|
|
497
|
+
const chains = chainsFrom(parent);
|
|
498
|
+
for (const [entityId, chain] of chains) {
|
|
499
|
+
const entity = index.byId.get(entityId);
|
|
500
|
+
if (!entity || chain.length === 0)
|
|
501
|
+
continue;
|
|
502
|
+
const inherited = new Map();
|
|
503
|
+
for (const ancestorId of chain) {
|
|
504
|
+
const ancestor = index.byId.get(ancestorId);
|
|
505
|
+
if (!ancestor)
|
|
506
|
+
continue;
|
|
507
|
+
for (const a of declaredAttributes(ancestor, index)) {
|
|
508
|
+
if (!inherited.has(a.name))
|
|
509
|
+
inherited.set(a.name, ancestorId);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const a of declaredAttributes(entity, index)) {
|
|
513
|
+
const owner = inherited.get(a.name);
|
|
514
|
+
if (owner === undefined)
|
|
515
|
+
continue;
|
|
516
|
+
const via = a.viaPartial ? ` (through TablePartial '${a.viaPartial}')` : '';
|
|
517
|
+
diagnostics.push({
|
|
518
|
+
severity: 'error',
|
|
519
|
+
code: 'supertype-attribute-redeclared',
|
|
520
|
+
message: `'${entityId}' declares '${a.name}'${via}, which its supertype '${owner}' already declares. A subtype declares only its own attributes (spec §12.5).`,
|
|
521
|
+
span: a.span,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return diagnostics;
|
|
526
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdbml/parse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Parser for xDBML (eXtended Database Markup Language), a strict superset of DBML 3.13.6: tokenizer, parser, module resolver, and name resolver.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hackolade",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"node": ">=22"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@types/node": "^26.1.1",
|
|
45
46
|
"typescript": "^5.7.3"
|
|
46
47
|
},
|
|
47
48
|
"publishConfig": {
|