@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
|
@@ -0,0 +1,110 @@
|
|
|
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 type { Diagnostic } from './name-resolver.ts';
|
|
24
|
+
import type { SupertypeGroupDeclaration, SupertypeGroupMember, XDbmlDocument } from './ast.ts';
|
|
25
|
+
export type Completeness = 'total' | 'partial';
|
|
26
|
+
export type Exclusivity = 'disjoint' | 'overlapping';
|
|
27
|
+
export type MaterializationStrategy = 'preserved_hierarchy' | 'roll_up' | 'roll_down';
|
|
28
|
+
export type MergeOption = 'flat' | 'nested';
|
|
29
|
+
/**
|
|
30
|
+
* Accepted spellings, keyed by lowercase spelling, mapped to the canonical
|
|
31
|
+
* value. Canonical values map to themselves.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SUPERTYPE_GROUP_VALUES: {
|
|
34
|
+
readonly completeness: {
|
|
35
|
+
readonly total: "total";
|
|
36
|
+
readonly complete: "total";
|
|
37
|
+
readonly partial: "partial";
|
|
38
|
+
readonly incomplete: "partial";
|
|
39
|
+
};
|
|
40
|
+
readonly exclusivity: {
|
|
41
|
+
readonly disjoint: "disjoint";
|
|
42
|
+
readonly exclusive: "disjoint";
|
|
43
|
+
readonly overlapping: "overlapping";
|
|
44
|
+
readonly non_exclusive: "overlapping";
|
|
45
|
+
};
|
|
46
|
+
readonly strategy: {
|
|
47
|
+
readonly preserved_hierarchy: "preserved_hierarchy";
|
|
48
|
+
readonly class_table: "preserved_hierarchy";
|
|
49
|
+
readonly joined: "preserved_hierarchy";
|
|
50
|
+
readonly roll_up: "roll_up";
|
|
51
|
+
readonly single_table: "roll_up";
|
|
52
|
+
readonly roll_down: "roll_down";
|
|
53
|
+
readonly concrete_table: "roll_down";
|
|
54
|
+
readonly table_per_class: "roll_down";
|
|
55
|
+
};
|
|
56
|
+
readonly merge: {
|
|
57
|
+
readonly flat: "flat";
|
|
58
|
+
readonly flat_with_discriminator: "flat";
|
|
59
|
+
readonly nested: "nested";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export type SupertypeGroupValueSetting = keyof typeof SUPERTYPE_GROUP_VALUES;
|
|
63
|
+
/** The canonical value for a spelling, or undefined when it is not recognized. */
|
|
64
|
+
export declare function canonicalSupertypeGroupValue(setting: SupertypeGroupValueSetting, raw: string): string | undefined;
|
|
65
|
+
/** A group's settings with canonical values. Unrecognized values are left out. */
|
|
66
|
+
export interface SupertypeGroupSettings {
|
|
67
|
+
supertype?: string;
|
|
68
|
+
completeness?: Completeness;
|
|
69
|
+
exclusivity?: Exclusivity;
|
|
70
|
+
strategy?: MaterializationStrategy;
|
|
71
|
+
merge?: MergeOption;
|
|
72
|
+
discriminator?: string;
|
|
73
|
+
note?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function supertypeGroupSettings(decl: SupertypeGroupDeclaration): SupertypeGroupSettings;
|
|
76
|
+
/** A subtype member's own `strategy` (spec §12.7.4), canonical, or undefined. */
|
|
77
|
+
export declare function subtypeStrategy(member: SupertypeGroupMember): MaterializationStrategy | undefined;
|
|
78
|
+
/** One group with its members resolved to entity ids where they resolve. */
|
|
79
|
+
export interface ResolvedSupertypeGroup {
|
|
80
|
+
declaration: SupertypeGroupDeclaration;
|
|
81
|
+
settings: SupertypeGroupSettings;
|
|
82
|
+
/** Entity id of the supertype, when it resolves. */
|
|
83
|
+
supertype?: string;
|
|
84
|
+
subtypes: Array<{
|
|
85
|
+
member: SupertypeGroupMember;
|
|
86
|
+
/** Entity id of the subtype, when it resolves. */
|
|
87
|
+
entity?: string;
|
|
88
|
+
/** The member's own strategy, canonical, when stated and recognized. */
|
|
89
|
+
strategy?: MaterializationStrategy;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every SupertypeGroup of a document with its members resolved. Pass the
|
|
94
|
+
* flattened document (see `flatten()`) so imported and cloned declarations
|
|
95
|
+
* are visible. Entity ids are container-qualified for entities declared in
|
|
96
|
+
* a Container, bare otherwise.
|
|
97
|
+
*/
|
|
98
|
+
export declare function resolveSupertypeGroups(doc: XDbmlDocument): ResolvedSupertypeGroup[];
|
|
99
|
+
/**
|
|
100
|
+
* For each entity that is a subtype, its chain of supertypes, nearest
|
|
101
|
+
* first: `Employee -> [Person, Party]`. When an entity is listed as a
|
|
102
|
+
* subtype in several groups (an error), the first group wins; a chain that
|
|
103
|
+
* loops stops before repeating an entity.
|
|
104
|
+
*/
|
|
105
|
+
export declare function supertypeChains(doc: XDbmlDocument): Map<string, string[]>;
|
|
106
|
+
/**
|
|
107
|
+
* The rules of spec §12.8. Run by `resolveNames()` on the flattened
|
|
108
|
+
* document; exported for callers that check groups on their own.
|
|
109
|
+
*/
|
|
110
|
+
export declare function checkSupertypeGroups(doc: XDbmlDocument): Diagnostic[];
|
|
@@ -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.1",
|
|
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",
|