@xdbml/parse 0.1.0-poc.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 +565 -0
- package/dist/ast.js +10 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +33 -0
- package/dist/keywords.d.ts +45 -0
- package/dist/keywords.js +277 -0
- package/dist/lexer.d.ts +91 -0
- package/dist/lexer.js +549 -0
- package/dist/module-resolver.d.ts +115 -0
- package/dist/module-resolver.js +771 -0
- package/dist/monarch.d.ts +64 -0
- package/dist/monarch.js +205 -0
- package/dist/name-resolver.d.ts +135 -0
- package/dist/name-resolver.js +854 -0
- package/dist/parser.d.ts +331 -0
- package/dist/parser.js +2083 -0
- package/package.json +33 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-system support utilities.
|
|
3
|
+
*
|
|
4
|
+
* Per parser-design v2, the parser produces a provenance-preserving
|
|
5
|
+
* "Shape B" AST: `ModuleImportDirective` nodes keep imported declarations
|
|
6
|
+
* inside their `clone.statements` field rather than splicing them into the
|
|
7
|
+
* parent's statement list. This preserves the information about which file
|
|
8
|
+
* each declaration came from, useful for navigation, inspector panels,
|
|
9
|
+
* round-tripping back to source text, etc.
|
|
10
|
+
*
|
|
11
|
+
* Many downstream consumers (code generators, diagram renderers, simple
|
|
12
|
+
* walkers) just want a flat list of declarations and don't care about
|
|
13
|
+
* provenance. The `flatten()` helper produces a new XDbmlDocument where
|
|
14
|
+
* `ModuleImportDirective` nodes have been replaced by their `clone.statements`,
|
|
15
|
+
* recursively.
|
|
16
|
+
*
|
|
17
|
+
* Currently P4-only: handles clone blocks. Reference-only directives are
|
|
18
|
+
* rejected at parse time, so `flatten()` doesn't need to do file resolution.
|
|
19
|
+
* In P5, a separate `resolveModules()` function will populate clone blocks
|
|
20
|
+
* from referenced files before `flatten()` runs.
|
|
21
|
+
*/
|
|
22
|
+
import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
23
|
+
/**
|
|
24
|
+
* Produce a new XDbmlDocument with all module-system directives replaced
|
|
25
|
+
* by their clone-block content, recursively.
|
|
26
|
+
*
|
|
27
|
+
* At the top level, each `ModuleImportDirective` is replaced by its
|
|
28
|
+
* `clone.statements` (each statement appears at the same position the
|
|
29
|
+
* directive used to occupy).
|
|
30
|
+
*
|
|
31
|
+
* Inside a Container body, each `ModuleImportDirective` is replaced by its
|
|
32
|
+
* `clone.statements` as `ContainerBodyItem`s. Note that the spec table in
|
|
33
|
+
* §26.6 guarantees that clone-block content for entity/edge/view/enum
|
|
34
|
+
* imports is shape-compatible with `ContainerBodyItem`; other shapes
|
|
35
|
+
* (e.g., a TablePartial clone inside a Container directive) would be
|
|
36
|
+
* semantically invalid per the spec and would surface as a downstream
|
|
37
|
+
* type error rather than being caught here.
|
|
38
|
+
*
|
|
39
|
+
* Field-level imports (spec §26.8) get a special transform: the clone
|
|
40
|
+
* block holds a bare `FieldDeclaration`, which `flatten()` lifts into a
|
|
41
|
+
* synthetic `TypeDeclaration` at file scope. Downstream consumers see
|
|
42
|
+
* a normal Named Type and can use it as a field type without learning
|
|
43
|
+
* about the field-import construct.
|
|
44
|
+
*
|
|
45
|
+
* Provenance information is lost in the flattened view. Consumers that
|
|
46
|
+
* want to know where each declaration came from should walk the original
|
|
47
|
+
* (non-flattened) AST instead.
|
|
48
|
+
*/
|
|
49
|
+
export function flatten(doc) {
|
|
50
|
+
const statements = [];
|
|
51
|
+
for (const stmt of doc.statements) {
|
|
52
|
+
flattenTopLevel(stmt, statements);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...doc,
|
|
56
|
+
statements,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function flattenTopLevel(stmt, out) {
|
|
60
|
+
if (stmt.kind === 'FieldDeclaration') {
|
|
61
|
+
// A bare FieldDeclaration only appears at the top level via the field-
|
|
62
|
+
// import path -- the parser only accepts it inside a clone block. Lift
|
|
63
|
+
// it to a synthetic TypeDeclaration so the name behaves like a Named
|
|
64
|
+
// Type for any downstream consumer (resolver, code generator, etc).
|
|
65
|
+
out.push(synthesizeTypeFromField(stmt));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (stmt.kind === 'ModuleImportDirective') {
|
|
69
|
+
// Replace the directive with its clone-block content. Each inner
|
|
70
|
+
// statement is itself flattened (a clone may itself contain Containers
|
|
71
|
+
// with nested directives, although the spec doesn't currently expect
|
|
72
|
+
// multi-level nesting in v0.2 phase 1).
|
|
73
|
+
if (stmt.clone) {
|
|
74
|
+
for (const inner of stmt.clone.statements) {
|
|
75
|
+
flattenTopLevel(inner, out);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (stmt.kind === 'ContainerDeclaration') {
|
|
81
|
+
out.push(flattenContainer(stmt));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// All other top-level statement kinds pass through unchanged.
|
|
85
|
+
out.push(stmt);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Lift a bare FieldDeclaration (from a field-import clone block) into a
|
|
89
|
+
* synthetic TypeDeclaration. The synthesized Type takes the field's name
|
|
90
|
+
* (or alias, since the clone block already has the post-alias name) and
|
|
91
|
+
* its full settings array.
|
|
92
|
+
*
|
|
93
|
+
* - For SCALAR fields (any TypeExpression that isn't ObjectType): the
|
|
94
|
+
* Type is in scalar form with `scalarBase = field.type`. Equivalent
|
|
95
|
+
* to the v0.2 scalar-Named-Type form (spec §14.7).
|
|
96
|
+
*
|
|
97
|
+
* - For OBJECT-typed fields (`field foo object { ... }`): the Type is
|
|
98
|
+
* in object form with `body = field.type.fields`. Equivalent to the
|
|
99
|
+
* v0.1 object-Named-Type form (spec §14).
|
|
100
|
+
*
|
|
101
|
+
* Other type shapes (Array, Map, Set, Tuple, Union, polymorphic types)
|
|
102
|
+
* are valid as `scalarBase` of a Type and pass through unchanged. The
|
|
103
|
+
* "scalar" in `scalarBase` is a historical name; functionally it means
|
|
104
|
+
* "the base TypeExpression this Named Type aliases."
|
|
105
|
+
*/
|
|
106
|
+
function synthesizeTypeFromField(field) {
|
|
107
|
+
if (field.type.kind === 'ObjectType') {
|
|
108
|
+
return {
|
|
109
|
+
kind: 'TypeDeclaration',
|
|
110
|
+
name: field.name,
|
|
111
|
+
scalarBase: undefined,
|
|
112
|
+
settings: field.settings,
|
|
113
|
+
body: field.type.fields,
|
|
114
|
+
span: field.span,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
kind: 'TypeDeclaration',
|
|
119
|
+
name: field.name,
|
|
120
|
+
scalarBase: field.type,
|
|
121
|
+
settings: field.settings,
|
|
122
|
+
body: [],
|
|
123
|
+
span: field.span,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function flattenContainer(c) {
|
|
127
|
+
const body = [];
|
|
128
|
+
for (const item of c.body) {
|
|
129
|
+
flattenContainerBodyItem(item, body);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...c,
|
|
133
|
+
body,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function flattenContainerBodyItem(item, out) {
|
|
137
|
+
if (item.kind === 'ModuleImportDirective') {
|
|
138
|
+
if (item.clone) {
|
|
139
|
+
// Each cloned statement becomes a ContainerBodyItem in the parent's
|
|
140
|
+
// body. The type narrowing is technically unsafe -- the spec implies
|
|
141
|
+
// that only EntityDeclaration, EdgeDeclaration, ViewDeclaration,
|
|
142
|
+
// EnumDeclaration, and NoteBlock can validly appear in a Container's
|
|
143
|
+
// clone block, but the parser is permissive about what was put there.
|
|
144
|
+
// Downstream consumers handle the type-mismatch case.
|
|
145
|
+
for (const inner of item.clone.statements) {
|
|
146
|
+
// `inner` is TopLevelStatement; we re-narrow at use site.
|
|
147
|
+
out.push(inner);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
out.push(item);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a directive's reference. Returns an `ImportResolution` indicating
|
|
156
|
+
* what happened. The caller decides how to act (set `clone`, fail, etc).
|
|
157
|
+
*
|
|
158
|
+
* Path resolution: the directive's `from` is relative to the importer's
|
|
159
|
+
* `filePath`. If `from` doesn't end with `.xdbml`, that extension is
|
|
160
|
+
* appended. Returns the absolute resolved path on success.
|
|
161
|
+
*
|
|
162
|
+
* Cycles: if the resolved path is already in `resolutionStack`, returns
|
|
163
|
+
* `kind: 'cycle'` without recursing.
|
|
164
|
+
*
|
|
165
|
+
* Depth: if `depth + 1 > maxDepth`, throws.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveImport(directive, options, resolutionStack, depth, parseFn) {
|
|
168
|
+
if (!options.readFile) {
|
|
169
|
+
return { kind: 'no-resolver' };
|
|
170
|
+
}
|
|
171
|
+
const resolvedPath = resolveModulePath(directive.from, options.filePath);
|
|
172
|
+
if (resolutionStack.has(resolvedPath)) {
|
|
173
|
+
return { kind: 'cycle', resolvedPath };
|
|
174
|
+
}
|
|
175
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
176
|
+
if (depth + 1 > maxDepth) {
|
|
177
|
+
throw new Error(`Module resolution depth limit (${maxDepth}) exceeded while resolving '${directive.from}' ` +
|
|
178
|
+
`from '${options.filePath ?? '<no filePath>'}'. ` +
|
|
179
|
+
`Increase maxDepth in ParseOptions if your module graph is genuinely this deep.`);
|
|
180
|
+
}
|
|
181
|
+
let referencedSource;
|
|
182
|
+
try {
|
|
183
|
+
referencedSource = options.readFile(resolvedPath);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
const inner = e.message ?? String(e);
|
|
187
|
+
throw new Error(`Failed to read referenced module file '${resolvedPath}' ` +
|
|
188
|
+
`(directive '${directive.mode} { ... } from \"${directive.from}\"' in '${options.filePath ?? '<no filePath>'}'). ` +
|
|
189
|
+
`Cause: ${inner}`);
|
|
190
|
+
}
|
|
191
|
+
// Recursively parse the referenced file. We pass the SAME options, but
|
|
192
|
+
// with `filePath` updated to the resolved path so any directives inside
|
|
193
|
+
// that file resolve relative to it. The resolution stack is widened to
|
|
194
|
+
// include this file, so cycles get caught.
|
|
195
|
+
const nextStack = new Set(resolutionStack);
|
|
196
|
+
nextStack.add(resolvedPath);
|
|
197
|
+
const nextOptions = { ...options, filePath: resolvedPath };
|
|
198
|
+
let referencedDoc;
|
|
199
|
+
try {
|
|
200
|
+
referencedDoc = parseFn(referencedSource, nextOptions, nextStack, depth + 1);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
const inner = e.message ?? String(e);
|
|
204
|
+
throw new Error(`Error while parsing referenced module file '${resolvedPath}' ` +
|
|
205
|
+
`(via directive '${directive.mode} { ... } from \"${directive.from}\"' in '${options.filePath ?? '<no filePath>'}'). ` +
|
|
206
|
+
`Cause: ${inner}`);
|
|
207
|
+
}
|
|
208
|
+
// Extract the declarations matching the directive's import spec.
|
|
209
|
+
const statements = extractImports(directive.spec, referencedDoc);
|
|
210
|
+
// Synthesize the clone block. The span uses the directive's own span as
|
|
211
|
+
// a placeholder; we don't have precise source positions for synthesized
|
|
212
|
+
// content, and downstream consumers that care about spans (Monaco
|
|
213
|
+
// markers, "go to definition") look at the inner declarations' own
|
|
214
|
+
// spans, which point into the referenced file. Spans on the wrapper
|
|
215
|
+
// CloneBlock node itself are rarely consulted.
|
|
216
|
+
const clone = {
|
|
217
|
+
kind: 'CloneBlock',
|
|
218
|
+
statements,
|
|
219
|
+
span: directive.span,
|
|
220
|
+
};
|
|
221
|
+
return { kind: 'resolved', clone, resolvedPath };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Walk the parsed source document and pick out declarations matching the
|
|
225
|
+
* import spec. Applies aliases by renaming the extracted declaration.
|
|
226
|
+
*
|
|
227
|
+
* For `ImportAll`: every TopLevelStatement except ProjectDeclaration
|
|
228
|
+
* (per spec §26.4, Project declarations cannot be imported).
|
|
229
|
+
*
|
|
230
|
+
* For `ImportList`: each item is matched by element-type + source path.
|
|
231
|
+
* Items not found in the source produce nothing (silent for P5; a
|
|
232
|
+
* future name-resolution pass should diagnose unresolved imports).
|
|
233
|
+
*/
|
|
234
|
+
function extractImports(spec, sourceDoc) {
|
|
235
|
+
if (spec.kind === 'ImportAll') {
|
|
236
|
+
// ImportAll never matches field imports (the `*` form is for top-level
|
|
237
|
+
// declarations only per spec §26.2). Return the source's top-level
|
|
238
|
+
// statements minus Project.
|
|
239
|
+
return sourceDoc.statements
|
|
240
|
+
.filter((s) => s.kind !== 'ProjectDeclaration')
|
|
241
|
+
.map((s) => s); // shallow-clone-able; we don't mutate them
|
|
242
|
+
}
|
|
243
|
+
// ImportList: process each item.
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const item of spec.items) {
|
|
246
|
+
const found = findImportTarget(item, sourceDoc);
|
|
247
|
+
if (found) {
|
|
248
|
+
out.push(applyAlias(found, item));
|
|
249
|
+
}
|
|
250
|
+
// Silent skip on not-found. The spec leaves diagnostics to name
|
|
251
|
+
// resolution (§26.13). In practice, users notice missing entities
|
|
252
|
+
// because their references downstream become unresolved.
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Find the declaration matching an import item in the source document.
|
|
258
|
+
* Returns undefined when no match is found.
|
|
259
|
+
*
|
|
260
|
+
* Element-type to lookup:
|
|
261
|
+
* - 'entity' / 'table' / 'collection' / 'record':
|
|
262
|
+
* EntityDeclaration with matching name; supports dotted path
|
|
263
|
+
* like 'core.dim_customer' (Container 'core', Entity 'dim_customer').
|
|
264
|
+
* - 'type':
|
|
265
|
+
* TypeDeclaration with matching name (top-level only; spec doesn't
|
|
266
|
+
* support importing nested Types).
|
|
267
|
+
* - 'enum':
|
|
268
|
+
* EnumDeclaration with matching name; supports dotted path for
|
|
269
|
+
* container-scoped enums.
|
|
270
|
+
* - 'container' / 'schema':
|
|
271
|
+
* ContainerDeclaration with matching name (the whole container,
|
|
272
|
+
* including all its body items).
|
|
273
|
+
* - 'edge':
|
|
274
|
+
* EdgeDeclaration with matching name; supports dotted path.
|
|
275
|
+
* - 'view' / 'diagramview':
|
|
276
|
+
* ViewDeclaration with matching name; supports dotted path.
|
|
277
|
+
* - 'tablegroup':
|
|
278
|
+
* TableGroupDeclaration with matching name (top-level only).
|
|
279
|
+
* - 'tablepartial':
|
|
280
|
+
* TablePartialDeclaration with matching name (top-level only).
|
|
281
|
+
* - 'note':
|
|
282
|
+
* NoteDeclaration with matching name (top-level only).
|
|
283
|
+
* - 'field':
|
|
284
|
+
* FieldDeclaration found by walking a dotted path through
|
|
285
|
+
* containers, entities, and nested type expressions (object types,
|
|
286
|
+
* arrays via [*]/[N], maps via ['key'], tuples via [N]). Returned
|
|
287
|
+
* as a bare FieldDeclaration; the caller wraps it in a CloneBlock
|
|
288
|
+
* and `flatten()` later lifts it to a synthetic TypeDeclaration.
|
|
289
|
+
*
|
|
290
|
+
* The return type is widened to include `FieldDeclaration` solely for
|
|
291
|
+
* the field-import case; for every other element type the returned shape
|
|
292
|
+
* is a `TopLevelStatement`.
|
|
293
|
+
*/
|
|
294
|
+
function findImportTarget(item, doc) {
|
|
295
|
+
const path = item.sourcePath;
|
|
296
|
+
const segments = path.split('.');
|
|
297
|
+
// Field imports go through their own walker. The path can be deeper
|
|
298
|
+
// than `container.entity.field` -- the walker handles nested objects,
|
|
299
|
+
// array wildcards, etc.
|
|
300
|
+
if (item.elementType === 'field') {
|
|
301
|
+
return findFieldTarget(segments, doc);
|
|
302
|
+
}
|
|
303
|
+
// Entity-shaped items: support container.entity dotted form.
|
|
304
|
+
if (item.elementType === 'entity' ||
|
|
305
|
+
item.elementType === 'table' ||
|
|
306
|
+
item.elementType === 'collection' ||
|
|
307
|
+
item.elementType === 'record') {
|
|
308
|
+
if (segments.length === 1) {
|
|
309
|
+
// Bare name -- match a top-level entity OR a container-scoped entity
|
|
310
|
+
// whose bare name is unique.
|
|
311
|
+
const topLevel = doc.statements.find((s) => s.kind === 'EntityDeclaration' && s.name === segments[0]);
|
|
312
|
+
if (topLevel)
|
|
313
|
+
return topLevel;
|
|
314
|
+
// Look inside containers.
|
|
315
|
+
const matches = [];
|
|
316
|
+
for (const stmt of doc.statements) {
|
|
317
|
+
if (stmt.kind === 'ContainerDeclaration') {
|
|
318
|
+
for (const body of stmt.body) {
|
|
319
|
+
if (body.kind === 'EntityDeclaration' && body.name === segments[0]) {
|
|
320
|
+
matches.push(body);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (matches.length === 1)
|
|
326
|
+
return matches[0];
|
|
327
|
+
// Ambiguous or missing -- silent skip.
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
if (segments.length === 2) {
|
|
331
|
+
// container.entity
|
|
332
|
+
const container = doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === segments[0]);
|
|
333
|
+
if (!container || container.kind !== 'ContainerDeclaration')
|
|
334
|
+
return undefined;
|
|
335
|
+
const entity = container.body.find((b) => b.kind === 'EntityDeclaration' && b.name === segments[1]);
|
|
336
|
+
return entity && entity.kind === 'EntityDeclaration' ? entity : undefined;
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
// Type, TableGroup, TablePartial, Note: top-level only, bare name.
|
|
341
|
+
if (item.elementType === 'type') {
|
|
342
|
+
return doc.statements.find((s) => s.kind === 'TypeDeclaration' && s.name === path);
|
|
343
|
+
}
|
|
344
|
+
if (item.elementType === 'tablegroup') {
|
|
345
|
+
return doc.statements.find((s) => s.kind === 'TableGroupDeclaration' && s.name === path);
|
|
346
|
+
}
|
|
347
|
+
if (item.elementType === 'tablepartial') {
|
|
348
|
+
return doc.statements.find((s) => s.kind === 'TablePartialDeclaration' && s.name === path);
|
|
349
|
+
}
|
|
350
|
+
if (item.elementType === 'note') {
|
|
351
|
+
return doc.statements.find((s) => s.kind === 'NoteDeclaration' && s.name === path);
|
|
352
|
+
}
|
|
353
|
+
// Container / Schema: top-level, bare name.
|
|
354
|
+
if (item.elementType === 'container' || item.elementType === 'schema') {
|
|
355
|
+
return doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === path);
|
|
356
|
+
}
|
|
357
|
+
// Edge: top-level OR container-scoped.
|
|
358
|
+
if (item.elementType === 'edge') {
|
|
359
|
+
if (segments.length === 1) {
|
|
360
|
+
return doc.statements.find((s) => s.kind === 'EdgeDeclaration' && s.name === segments[0]);
|
|
361
|
+
}
|
|
362
|
+
if (segments.length === 2) {
|
|
363
|
+
const container = doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === segments[0]);
|
|
364
|
+
if (!container || container.kind !== 'ContainerDeclaration')
|
|
365
|
+
return undefined;
|
|
366
|
+
const edge = container.body.find((b) => b.kind === 'EdgeDeclaration' && b.name === segments[1]);
|
|
367
|
+
return edge && edge.kind === 'EdgeDeclaration' ? edge : undefined;
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
// View / DiagramView: top-level OR container-scoped.
|
|
372
|
+
if (item.elementType === 'view' || item.elementType === 'diagramview') {
|
|
373
|
+
if (segments.length === 1) {
|
|
374
|
+
return doc.statements.find((s) => s.kind === 'ViewDeclaration' && s.name === segments[0]);
|
|
375
|
+
}
|
|
376
|
+
if (segments.length === 2) {
|
|
377
|
+
const container = doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === segments[0]);
|
|
378
|
+
if (!container || container.kind !== 'ContainerDeclaration')
|
|
379
|
+
return undefined;
|
|
380
|
+
const view = container.body.find((b) => b.kind === 'ViewDeclaration' && b.name === segments[1]);
|
|
381
|
+
return view && view.kind === 'ViewDeclaration' ? view : undefined;
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
// Enum: top-level OR container-scoped.
|
|
386
|
+
if (item.elementType === 'enum') {
|
|
387
|
+
if (segments.length === 1) {
|
|
388
|
+
return doc.statements.find((s) => s.kind === 'EnumDeclaration' && s.name === segments[0]);
|
|
389
|
+
}
|
|
390
|
+
if (segments.length === 2) {
|
|
391
|
+
const container = doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === segments[0]);
|
|
392
|
+
if (!container || container.kind !== 'ContainerDeclaration')
|
|
393
|
+
return undefined;
|
|
394
|
+
const enm = container.body.find((b) => b.kind === 'EnumDeclaration' && b.name === segments[1]);
|
|
395
|
+
return enm && enm.kind === 'EnumDeclaration' ? enm : undefined;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Walk a dotted path through containers, entities, and (optionally) nested
|
|
403
|
+
* object types to find a FieldDeclaration in a referenced source document.
|
|
404
|
+
* Returns undefined when any segment fails to resolve.
|
|
405
|
+
*
|
|
406
|
+
* Path shapes accepted (per spec §26.8):
|
|
407
|
+
* - `entity.field` -- top-level entity
|
|
408
|
+
* - `container.entity.field` -- container-qualified entity
|
|
409
|
+
* - `entity.field.sub` -- nested via ObjectType
|
|
410
|
+
* - `container.entity.field.sub.leaf` -- container + nested
|
|
411
|
+
*
|
|
412
|
+
* Bracketed segments (`[*]`, `[N]`, `['key']`) are not supported because
|
|
413
|
+
* the parser's ImportItem path grammar accepts only dotted identifiers.
|
|
414
|
+
* A field whose source is reached only through an array or map element
|
|
415
|
+
* needs to be re-declared as a Named Type at the source side instead.
|
|
416
|
+
*
|
|
417
|
+
* Named Type dereferencing: when walking a path like
|
|
418
|
+
* `entity.field.subfield` where `field`'s type is a Named Type defined
|
|
419
|
+
* in the source document, the walker looks up the Type and continues
|
|
420
|
+
* the walk through its body. Cycle detection caps recursion at depth 8
|
|
421
|
+
* (deep enough for realistic nesting, shallow enough that a malformed
|
|
422
|
+
* cyclic type declaration can't hang the parser).
|
|
423
|
+
*/
|
|
424
|
+
function findFieldTarget(segments, doc) {
|
|
425
|
+
if (segments.length < 2)
|
|
426
|
+
return undefined;
|
|
427
|
+
// -- Step 1: identify the entity and how many leading segments it consumed.
|
|
428
|
+
let entity;
|
|
429
|
+
let entityPrefixLen = 0;
|
|
430
|
+
// Try 2-segment: container.entity (only if we have room for at least
|
|
431
|
+
// one field segment after).
|
|
432
|
+
if (segments.length >= 3) {
|
|
433
|
+
const container = doc.statements.find((s) => s.kind === 'ContainerDeclaration' && s.name === segments[0]);
|
|
434
|
+
if (container && container.kind === 'ContainerDeclaration') {
|
|
435
|
+
const ent = container.body.find((b) => b.kind === 'EntityDeclaration' && b.name === segments[1]);
|
|
436
|
+
if (ent && ent.kind === 'EntityDeclaration') {
|
|
437
|
+
entity = ent;
|
|
438
|
+
entityPrefixLen = 2;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Try 1-segment: top-level entity (declared outside any Container).
|
|
443
|
+
if (!entity) {
|
|
444
|
+
const topLevel = doc.statements.find((s) => s.kind === 'EntityDeclaration' && s.name === segments[0]);
|
|
445
|
+
if (topLevel && topLevel.kind === 'EntityDeclaration') {
|
|
446
|
+
entity = topLevel;
|
|
447
|
+
entityPrefixLen = 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Try 1-segment: bare entity name with unique match across containers.
|
|
451
|
+
// (Ambiguous bare names -- entity X exists in multiple containers --
|
|
452
|
+
// return undefined to force the importer to qualify the path.)
|
|
453
|
+
if (!entity) {
|
|
454
|
+
const matches = [];
|
|
455
|
+
for (const stmt of doc.statements) {
|
|
456
|
+
if (stmt.kind === 'ContainerDeclaration') {
|
|
457
|
+
for (const item of stmt.body) {
|
|
458
|
+
if (item.kind === 'EntityDeclaration' && item.name === segments[0]) {
|
|
459
|
+
matches.push(item);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (matches.length === 1) {
|
|
465
|
+
entity = matches[0];
|
|
466
|
+
entityPrefixLen = 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!entity)
|
|
470
|
+
return undefined;
|
|
471
|
+
// -- Step 2: find the top-level field on the entity.
|
|
472
|
+
const fieldSegments = segments.slice(entityPrefixLen);
|
|
473
|
+
if (fieldSegments.length === 0)
|
|
474
|
+
return undefined;
|
|
475
|
+
let currentField;
|
|
476
|
+
for (const item of entity.body) {
|
|
477
|
+
if (item.kind === 'FieldDeclaration' && item.name === fieldSegments[0]) {
|
|
478
|
+
currentField = item;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!currentField)
|
|
483
|
+
return undefined;
|
|
484
|
+
// If no more segments, we're done.
|
|
485
|
+
if (fieldSegments.length === 1)
|
|
486
|
+
return currentField;
|
|
487
|
+
// -- Step 3: walk nested segments through object-typed fields.
|
|
488
|
+
// Build a local Named-Type table from the source doc so the walker
|
|
489
|
+
// can dereference scalar-Named-Type and object-form Named Types when
|
|
490
|
+
// a path crosses a Named Type boundary.
|
|
491
|
+
const typeTable = new Map();
|
|
492
|
+
for (const s of doc.statements) {
|
|
493
|
+
if (s.kind === 'TypeDeclaration')
|
|
494
|
+
typeTable.set(s.name, s);
|
|
495
|
+
}
|
|
496
|
+
return walkObjectFieldPath(currentField, fieldSegments.slice(1), typeTable);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Walk through remaining path segments, each step expecting the current
|
|
500
|
+
* field's type to be an ObjectType (directly or after Named Type deref)
|
|
501
|
+
* and looking up the next segment as a field name in that object.
|
|
502
|
+
*/
|
|
503
|
+
function walkObjectFieldPath(startField, remaining, typeTable) {
|
|
504
|
+
let current = startField;
|
|
505
|
+
for (const seg of remaining) {
|
|
506
|
+
const objType = derefToObject(current.type, typeTable, 0);
|
|
507
|
+
if (!objType)
|
|
508
|
+
return undefined;
|
|
509
|
+
let next;
|
|
510
|
+
for (const item of objType.fields) {
|
|
511
|
+
if (item.kind === 'FieldDeclaration' && item.name === seg) {
|
|
512
|
+
next = item;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!next)
|
|
517
|
+
return undefined;
|
|
518
|
+
current = next;
|
|
519
|
+
}
|
|
520
|
+
return current;
|
|
521
|
+
}
|
|
522
|
+
const BUILTIN_TYPE_NAMES = new Set([
|
|
523
|
+
...SCALAR_TYPES.map((t) => t.toLowerCase()),
|
|
524
|
+
...BSON_TYPES.map((t) => t.toLowerCase()),
|
|
525
|
+
]);
|
|
526
|
+
/**
|
|
527
|
+
* Dereference a TypeExpression to an ObjectType when possible. Returns
|
|
528
|
+
* undefined when the expression is a builtin scalar, an array/map/tuple
|
|
529
|
+
* (which the ImportItem path grammar can't navigate into), or a Named
|
|
530
|
+
* Type that the source document doesn't declare.
|
|
531
|
+
*
|
|
532
|
+
* `depth` guards against cyclic Named-Type chains (e.g., `Type A B`,
|
|
533
|
+
* `Type B A`). Realistic schemas won't approach the limit; the cap is
|
|
534
|
+
* defensive.
|
|
535
|
+
*/
|
|
536
|
+
function derefToObject(type, typeTable, depth) {
|
|
537
|
+
if (depth > 8)
|
|
538
|
+
return undefined;
|
|
539
|
+
if (type.kind === 'ObjectType')
|
|
540
|
+
return type;
|
|
541
|
+
if (type.kind !== 'ScalarType')
|
|
542
|
+
return undefined;
|
|
543
|
+
// ScalarType might be a builtin or a reference to a declared Named
|
|
544
|
+
// Type -- the parser doesn't distinguish them at parse time. If the
|
|
545
|
+
// name matches a builtin, no Named-Type deref is possible.
|
|
546
|
+
if (BUILTIN_TYPE_NAMES.has(type.name.toLowerCase()))
|
|
547
|
+
return undefined;
|
|
548
|
+
const td = typeTable.get(type.name);
|
|
549
|
+
if (!td)
|
|
550
|
+
return undefined;
|
|
551
|
+
// Object-form Named Type: body holds the fields directly.
|
|
552
|
+
if (!td.scalarBase && td.body.length > 0) {
|
|
553
|
+
return {
|
|
554
|
+
kind: 'ObjectType',
|
|
555
|
+
keyword: 'object',
|
|
556
|
+
fields: td.body,
|
|
557
|
+
span: td.span,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
// Scalar-form Named Type: recurse into the base.
|
|
561
|
+
if (td.scalarBase) {
|
|
562
|
+
return derefToObject(td.scalarBase, typeTable, depth + 1);
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Apply the alias from an import item by renaming the extracted declaration.
|
|
568
|
+
* If no alias is present, returns the declaration unchanged.
|
|
569
|
+
*
|
|
570
|
+
* The rename is shallow: we update the top-level `name` field and leave
|
|
571
|
+
* everything else intact. References inside the declaration (e.g., field
|
|
572
|
+
* type expressions that name other Types) keep their original names --
|
|
573
|
+
* the user is expected to ensure aliases don't break internal references.
|
|
574
|
+
*
|
|
575
|
+
* Field imports are aliased the same way: the bare FieldDeclaration's
|
|
576
|
+
* `name` field becomes the alias. The synthesized TypeDeclaration that
|
|
577
|
+
* `flatten()` produces will then carry the alias as its Named Type name.
|
|
578
|
+
*/
|
|
579
|
+
function applyAlias(stmt, item) {
|
|
580
|
+
if (!item.alias)
|
|
581
|
+
return stmt;
|
|
582
|
+
switch (stmt.kind) {
|
|
583
|
+
case 'EntityDeclaration':
|
|
584
|
+
return { ...stmt, name: item.alias };
|
|
585
|
+
case 'TypeDeclaration':
|
|
586
|
+
return { ...stmt, name: item.alias };
|
|
587
|
+
case 'EnumDeclaration':
|
|
588
|
+
return { ...stmt, name: item.alias };
|
|
589
|
+
case 'EdgeDeclaration':
|
|
590
|
+
return { ...stmt, name: item.alias };
|
|
591
|
+
case 'ViewDeclaration':
|
|
592
|
+
return { ...stmt, name: item.alias };
|
|
593
|
+
case 'ContainerDeclaration':
|
|
594
|
+
return { ...stmt, name: item.alias };
|
|
595
|
+
case 'TableGroupDeclaration':
|
|
596
|
+
return { ...stmt, name: item.alias };
|
|
597
|
+
case 'TablePartialDeclaration':
|
|
598
|
+
return { ...stmt, name: item.alias };
|
|
599
|
+
case 'NoteDeclaration':
|
|
600
|
+
return { ...stmt, name: item.alias };
|
|
601
|
+
case 'FieldDeclaration':
|
|
602
|
+
return { ...stmt, name: item.alias };
|
|
603
|
+
default:
|
|
604
|
+
return stmt;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/* -------------------------------------------------------------------------
|
|
608
|
+
* Module source classification (spec §26.14, "Remote module sources")
|
|
609
|
+
*
|
|
610
|
+
* A `from` source is recognized purely by its scheme. A source beginning
|
|
611
|
+
* with 'https://' is a remote (URL) source; anything else is a relative
|
|
612
|
+
* path, resolved exactly as in v0.2. Disallowed forms -- a non-https
|
|
613
|
+
* scheme, a protocol-relative '//host/...' source, embedded credentials,
|
|
614
|
+
* or a bare host such as 'github.com/owner/repo/...' -- are rejected here
|
|
615
|
+
* so the parser can surface a located error at the source string.
|
|
616
|
+
*
|
|
617
|
+
* This is pure, synchronous classification. The actual network fetch is
|
|
618
|
+
* delegated to ParseOptions.readFile (the host's resolver). The obligations
|
|
619
|
+
* on a fetcher (SSRF defenses, https-only redirects, size and time limits)
|
|
620
|
+
* live with that resolver, not here; see spec §26.14.5.
|
|
621
|
+
* ----------------------------------------------------------------------- */
|
|
622
|
+
/** Raised when a `from` source string is structurally disallowed. */
|
|
623
|
+
export class ModuleSourceError extends Error {
|
|
624
|
+
constructor(message) {
|
|
625
|
+
super(message);
|
|
626
|
+
this.name = 'ModuleSourceError';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
|
|
630
|
+
// A scheme-less first path segment that looks like a public host. Used only
|
|
631
|
+
// to give a clearer error than "file not found" when someone pastes a URL
|
|
632
|
+
// without its scheme. Deliberately conservative: it requires a dot-separated
|
|
633
|
+
// label ending in an alphabetic TLD, so version directories like 'v1.2/...'
|
|
634
|
+
// and ordinary relative roots like 'lib/...' are NOT treated as hosts.
|
|
635
|
+
const BARE_HOST_RE = /^([a-z0-9-]+\.)+[a-z]{2,}$/i;
|
|
636
|
+
/** True when a resolved key is a remote (https) URL rather than a path. */
|
|
637
|
+
export function isUrlKey(s) {
|
|
638
|
+
return /^https:\/\//i.test(s);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Classify a directive's `from` source string. Returns a discriminated
|
|
642
|
+
* union; throws ModuleSourceError for disallowed forms. Pure and sync.
|
|
643
|
+
*/
|
|
644
|
+
export function classifyModuleSource(from) {
|
|
645
|
+
// Protocol-relative: ambiguous (no scheme to resolve against). Rejected.
|
|
646
|
+
if (from.startsWith('//')) {
|
|
647
|
+
throw new ModuleSourceError(`Protocol-relative module source ${JSON.stringify(from)} is not allowed; ` +
|
|
648
|
+
`use an explicit 'https://' URL or a relative path.`);
|
|
649
|
+
}
|
|
650
|
+
const schemeMatch = SCHEME_RE.exec(from);
|
|
651
|
+
if (schemeMatch) {
|
|
652
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
653
|
+
if (scheme !== 'https') {
|
|
654
|
+
throw new ModuleSourceError(`Module source scheme '${scheme}:' is not allowed; remote sources must use 'https://' ` +
|
|
655
|
+
`(got ${JSON.stringify(from)}).`);
|
|
656
|
+
}
|
|
657
|
+
let url;
|
|
658
|
+
try {
|
|
659
|
+
url = new URL(from);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
throw new ModuleSourceError(`Module source ${JSON.stringify(from)} is not a valid 'https://' URL.`);
|
|
663
|
+
}
|
|
664
|
+
if (url.username !== '' || url.password !== '') {
|
|
665
|
+
throw new ModuleSourceError(`Module source ${JSON.stringify(from)} embeds credentials in the URL, which is not allowed; ` +
|
|
666
|
+
`supply authentication through the resolver's configuration instead.`);
|
|
667
|
+
}
|
|
668
|
+
return { kind: 'url', href: url.href };
|
|
669
|
+
}
|
|
670
|
+
// No scheme. Reject an obvious bare host (domain-like first segment
|
|
671
|
+
// followed by a path) rather than silently treating it as a relative file.
|
|
672
|
+
const slash = from.indexOf('/');
|
|
673
|
+
if (slash > 0) {
|
|
674
|
+
const firstSegment = from.slice(0, slash);
|
|
675
|
+
if (BARE_HOST_RE.test(firstSegment)) {
|
|
676
|
+
throw new ModuleSourceError(`Module source ${JSON.stringify(from)} looks like a bare host; ` +
|
|
677
|
+
`prefix it with 'https://' to use it as a remote source, ` +
|
|
678
|
+
`or write './${from}' if you really mean a relative path.`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return { kind: 'relative', from };
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Resolve a `from` source against the importer's `filePath`, returning a
|
|
685
|
+
* stable key for `readFile` and for cycle detection.
|
|
686
|
+
*
|
|
687
|
+
* - A remote (https) source resolves to its normalized href. No '.xdbml'
|
|
688
|
+
* is appended (a raw-content URL may carry a query string).
|
|
689
|
+
* - A relative source whose importer is itself a remote module resolves
|
|
690
|
+
* against the importer's base URL per RFC 3986 (spec §26.14.1). A remote
|
|
691
|
+
* module therefore can never reach the local filesystem.
|
|
692
|
+
* - A relative source with a local importer resolves on the filesystem,
|
|
693
|
+
* exactly as in v0.2.
|
|
694
|
+
*
|
|
695
|
+
* Uses pure string / WHATWG-URL manipulation (no node:path) so the same
|
|
696
|
+
* code runs in Node and the browser. Forward slashes only; Windows paths
|
|
697
|
+
* with backslashes should be normalized before reaching the parser.
|
|
698
|
+
*/
|
|
699
|
+
function resolveModulePath(fromClause, importerPath) {
|
|
700
|
+
const source = classifyModuleSource(fromClause);
|
|
701
|
+
// Remote (URL) source: the normalized href IS the resolution key.
|
|
702
|
+
if (source.kind === 'url') {
|
|
703
|
+
return source.href;
|
|
704
|
+
}
|
|
705
|
+
// Relative source under a remote importer: resolve against its base URL.
|
|
706
|
+
if (importerPath && isUrlKey(importerPath)) {
|
|
707
|
+
let rel = source.from;
|
|
708
|
+
if (!rel.endsWith('.xdbml'))
|
|
709
|
+
rel = `${rel}.xdbml`;
|
|
710
|
+
// WHATWG URL resolution normalizes host case and dot-segments, which is
|
|
711
|
+
// also what we want for the cycle-detection / de-duplication key.
|
|
712
|
+
return new URL(rel, importerPath).href;
|
|
713
|
+
}
|
|
714
|
+
// Local relative source (unchanged v0.2 behavior).
|
|
715
|
+
let withExt = source.from;
|
|
716
|
+
if (!withExt.endsWith('.xdbml')) {
|
|
717
|
+
withExt = `${withExt}.xdbml`;
|
|
718
|
+
}
|
|
719
|
+
// Absolute path: return as-is.
|
|
720
|
+
if (withExt.startsWith('/'))
|
|
721
|
+
return withExt;
|
|
722
|
+
// No importer context: return as-is (resolver handles it).
|
|
723
|
+
if (!importerPath)
|
|
724
|
+
return withExt;
|
|
725
|
+
// Resolve relative to importer's directory.
|
|
726
|
+
const importerDir = posixDirname(importerPath);
|
|
727
|
+
return posixJoin(importerDir, withExt);
|
|
728
|
+
}
|
|
729
|
+
function posixDirname(p) {
|
|
730
|
+
const idx = p.lastIndexOf('/');
|
|
731
|
+
if (idx < 0)
|
|
732
|
+
return '.';
|
|
733
|
+
if (idx === 0)
|
|
734
|
+
return '/';
|
|
735
|
+
return p.slice(0, idx);
|
|
736
|
+
}
|
|
737
|
+
function posixJoin(base, rel) {
|
|
738
|
+
// Split base into segments; consume `.`/`..` from rel.
|
|
739
|
+
const baseSegs = base === '/' ? [''] : base.split('/');
|
|
740
|
+
const relSegs = rel.split('/');
|
|
741
|
+
const out = baseSegs.slice();
|
|
742
|
+
for (const seg of relSegs) {
|
|
743
|
+
if (seg === '' || seg === '.')
|
|
744
|
+
continue;
|
|
745
|
+
if (seg === '..') {
|
|
746
|
+
// Pop unless we'd cross above root.
|
|
747
|
+
if (out.length > 0 && out[out.length - 1] !== '..' && out[out.length - 1] !== '') {
|
|
748
|
+
out.pop();
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
out.push('..');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
out.push(seg);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Re-join. Leading empty (from absolute base) preserves the leading slash.
|
|
759
|
+
let joined = out.join('/');
|
|
760
|
+
if (base.startsWith('/') && !joined.startsWith('/'))
|
|
761
|
+
joined = `/${joined}`;
|
|
762
|
+
if (joined === '')
|
|
763
|
+
joined = '.';
|
|
764
|
+
return joined;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Default recursion depth limit for module resolution. Enough for any
|
|
768
|
+
* realistic module graph; small enough to bound stack usage on
|
|
769
|
+
* pathological inputs.
|
|
770
|
+
*/
|
|
771
|
+
export const DEFAULT_MAX_DEPTH = 8;
|