@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,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name resolution pass (spec §26.10 / §26.15, parser batch P6).
|
|
3
|
+
*
|
|
4
|
+
* `resolveNames(doc)` walks an xDBML document (the flattened view; clone
|
|
5
|
+
* blocks have been merged) and produces:
|
|
6
|
+
*
|
|
7
|
+
* - A symbol table mapping qualified names to declarations
|
|
8
|
+
* - A list of diagnostics: unresolved references and name conflicts
|
|
9
|
+
*
|
|
10
|
+
* The resolver does NOT mutate the AST. It is a pure side computation
|
|
11
|
+
* that downstream consumers can run for validation, IDE support, code
|
|
12
|
+
* generation, etc. Spans on diagnostics point to the offending construct
|
|
13
|
+
* in the source, so callers can surface them as editor markers.
|
|
14
|
+
*
|
|
15
|
+
* Per the spec, name resolution is a two-pass process:
|
|
16
|
+
*
|
|
17
|
+
* Pass 1: Collect declarations.
|
|
18
|
+
* Walk all top-level + container-body declarations and add them to
|
|
19
|
+
* the symbol table. Duplicates (same qualified name + same kind)
|
|
20
|
+
* produce a `duplicate-declaration` diagnostic and the LATER
|
|
21
|
+
* declaration is silently dropped from the table.
|
|
22
|
+
*
|
|
23
|
+
* Pass 2: Resolve references.
|
|
24
|
+
* Walk all reference sites and look up targets in the symbol table.
|
|
25
|
+
* References that don't resolve produce diagnostics. Built-in scalar
|
|
26
|
+
* and BSON types are recognized via SCALAR_TYPES / BSON_TYPES and
|
|
27
|
+
* never produce unresolved-type diagnostics.
|
|
28
|
+
*
|
|
29
|
+
* Two passes handle forward references (a Type declared at end of file
|
|
30
|
+
* can be referenced from a field declared at the top) and circular
|
|
31
|
+
* imports (cycles are already collapsed by `flatten()` / cycles in P5
|
|
32
|
+
* resolution; the resolver just sees the merged namespace).
|
|
33
|
+
*
|
|
34
|
+
* The resolver flattens its input internally, so callers don't need to
|
|
35
|
+
* `flatten()` first. Callers that want to surface diagnostics tied to
|
|
36
|
+
* the original (provenance-preserving) AST can map positions back via
|
|
37
|
+
* span comparison; in practice the cloned declarations' spans point
|
|
38
|
+
* into the importing file's clone block, which is where the user can
|
|
39
|
+
* edit them, so the natural workflow works correctly.
|
|
40
|
+
*/
|
|
41
|
+
import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
42
|
+
import { flatten } from "./module-resolver.js";
|
|
43
|
+
/**
|
|
44
|
+
* Read-only handle on the collected symbol table.
|
|
45
|
+
*
|
|
46
|
+
* Lookup is by qualified name (e.g., `core.dim_customer`). The class
|
|
47
|
+
* also exposes a `lookupBare()` for the common case where the name has
|
|
48
|
+
* no container prefix and the caller wants to find the unique match
|
|
49
|
+
* (returns undefined if ambiguous or missing).
|
|
50
|
+
*/
|
|
51
|
+
export class SymbolTable {
|
|
52
|
+
byQualified;
|
|
53
|
+
byBare;
|
|
54
|
+
constructor(entries) {
|
|
55
|
+
this.byQualified = new Map();
|
|
56
|
+
this.byBare = new Map();
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
this.byQualified.set(e.qualifiedName, e);
|
|
59
|
+
const list = this.byBare.get(e.name) ?? [];
|
|
60
|
+
list.push(e);
|
|
61
|
+
this.byBare.set(e.name, list);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Look up by canonical qualified name. */
|
|
65
|
+
lookup(qualifiedName) {
|
|
66
|
+
return this.byQualified.get(qualifiedName);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Look up by bare name. Returns the unique entry if exactly one match,
|
|
70
|
+
* or undefined when missing or ambiguous (multiple containers contain
|
|
71
|
+
* an entry with this bare name). For ambiguous cases, callers should
|
|
72
|
+
* inspect `lookupAllBare()` if they want to disambiguate.
|
|
73
|
+
*/
|
|
74
|
+
lookupBare(name) {
|
|
75
|
+
const list = this.byBare.get(name);
|
|
76
|
+
if (!list || list.length !== 1)
|
|
77
|
+
return undefined;
|
|
78
|
+
return list[0];
|
|
79
|
+
}
|
|
80
|
+
/** Look up by bare name; returns all matches. */
|
|
81
|
+
lookupAllBare(name) {
|
|
82
|
+
return this.byBare.get(name) ?? [];
|
|
83
|
+
}
|
|
84
|
+
/** Iterate all entries in declaration order. */
|
|
85
|
+
entries() {
|
|
86
|
+
return this.byQualified.values();
|
|
87
|
+
}
|
|
88
|
+
/** Total number of entries. */
|
|
89
|
+
get size() {
|
|
90
|
+
return this.byQualified.size;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/* -------------------------------------------------------------------------
|
|
94
|
+
* Built-in type recognition
|
|
95
|
+
*
|
|
96
|
+
* Field type expressions can name a builtin scalar (`int`, `varchar`),
|
|
97
|
+
* a BSON type (`objectId`), or a user-defined Named Type (`Email`).
|
|
98
|
+
* The parser doesn't distinguish at parse time -- they all land as
|
|
99
|
+
* ScalarType nodes (or NamedTypeReference in some contexts). The
|
|
100
|
+
* resolver uses these sets to decide whether to look up a name in the
|
|
101
|
+
* symbol table.
|
|
102
|
+
*
|
|
103
|
+
* Matching is case-insensitive: `Int`, `int`, `INT` all map to the same
|
|
104
|
+
* builtin per spec §3.8.
|
|
105
|
+
* ----------------------------------------------------------------------- */
|
|
106
|
+
const BUILTIN_TYPES = new Set([
|
|
107
|
+
...SCALAR_TYPES.map((t) => t.toLowerCase()),
|
|
108
|
+
...BSON_TYPES.map((t) => t.toLowerCase()),
|
|
109
|
+
]);
|
|
110
|
+
function isBuiltinType(name) {
|
|
111
|
+
return BUILTIN_TYPES.has(name.toLowerCase());
|
|
112
|
+
}
|
|
113
|
+
/* -------------------------------------------------------------------------
|
|
114
|
+
* Main entry point
|
|
115
|
+
* ----------------------------------------------------------------------- */
|
|
116
|
+
/**
|
|
117
|
+
* Resolve names in an xDBML document. Flattens the AST internally
|
|
118
|
+
* (so callers don't need to call `flatten()` first), then runs the
|
|
119
|
+
* two-pass resolution algorithm. Returns diagnostics and the symbol
|
|
120
|
+
* table.
|
|
121
|
+
*
|
|
122
|
+
* Cost is roughly linear in (declarations + reference sites). For
|
|
123
|
+
* typical schemas (10s-100s of entities) this is fast enough to run
|
|
124
|
+
* on every keystroke in an interactive editor.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveNames(doc) {
|
|
127
|
+
// Always work on the flattened view so cloned declarations are visible.
|
|
128
|
+
// The flatten operation is shallow-cheap relative to the resolver pass.
|
|
129
|
+
const flat = flatten(doc);
|
|
130
|
+
const diagnostics = [];
|
|
131
|
+
const entries = [];
|
|
132
|
+
// Pass 1: collect declarations.
|
|
133
|
+
collectDeclarations(flat, entries, diagnostics);
|
|
134
|
+
const symbols = new SymbolTable(entries);
|
|
135
|
+
// Pass 2: resolve references.
|
|
136
|
+
resolveReferences(flat, symbols, diagnostics);
|
|
137
|
+
return { diagnostics, symbols };
|
|
138
|
+
}
|
|
139
|
+
/* -------------------------------------------------------------------------
|
|
140
|
+
* Pass 1: collect declarations
|
|
141
|
+
* ----------------------------------------------------------------------- */
|
|
142
|
+
function collectDeclarations(doc, entries, diagnostics) {
|
|
143
|
+
const seen = new Set(); // qualified-name keys to detect duplicates
|
|
144
|
+
for (const stmt of doc.statements) {
|
|
145
|
+
addTopLevelDeclaration(stmt, entries, diagnostics, seen);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
149
|
+
switch (stmt.kind) {
|
|
150
|
+
case 'EntityDeclaration':
|
|
151
|
+
addEntry(stmt.name, undefined, 'entity', stmt, stmt.span, entries, diagnostics, seen);
|
|
152
|
+
return;
|
|
153
|
+
case 'TypeDeclaration':
|
|
154
|
+
addEntry(stmt.name, undefined, 'type', stmt, stmt.span, entries, diagnostics, seen);
|
|
155
|
+
return;
|
|
156
|
+
case 'EnumDeclaration':
|
|
157
|
+
addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
|
|
158
|
+
return;
|
|
159
|
+
case 'EdgeDeclaration':
|
|
160
|
+
addEntry(stmt.name, undefined, 'edge', stmt, stmt.span, entries, diagnostics, seen);
|
|
161
|
+
return;
|
|
162
|
+
case 'ViewDeclaration':
|
|
163
|
+
addEntry(stmt.name, undefined, 'view', stmt, stmt.span, entries, diagnostics, seen);
|
|
164
|
+
return;
|
|
165
|
+
case 'ContainerDeclaration': {
|
|
166
|
+
addEntry(stmt.name, undefined, 'container', stmt, stmt.span, entries, diagnostics, seen);
|
|
167
|
+
// Walk container body for nested entities, edges, views, enums.
|
|
168
|
+
for (const body of stmt.body) {
|
|
169
|
+
switch (body.kind) {
|
|
170
|
+
case 'EntityDeclaration':
|
|
171
|
+
addEntry(body.name, stmt.name, 'entity', body, body.span, entries, diagnostics, seen);
|
|
172
|
+
break;
|
|
173
|
+
case 'EdgeDeclaration':
|
|
174
|
+
addEntry(body.name, stmt.name, 'edge', body, body.span, entries, diagnostics, seen);
|
|
175
|
+
break;
|
|
176
|
+
case 'ViewDeclaration':
|
|
177
|
+
addEntry(body.name, stmt.name, 'view', body, body.span, entries, diagnostics, seen);
|
|
178
|
+
break;
|
|
179
|
+
case 'EnumDeclaration':
|
|
180
|
+
addEntry(body.name, stmt.name, 'enum', body, body.span, entries, diagnostics, seen);
|
|
181
|
+
break;
|
|
182
|
+
// NoteBlock and ModuleImportDirective contribute no symbols.
|
|
183
|
+
default:
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
case 'TableGroupDeclaration':
|
|
190
|
+
addEntry(stmt.name, undefined, 'tablegroup', stmt, stmt.span, entries, diagnostics, seen);
|
|
191
|
+
return;
|
|
192
|
+
case 'TablePartialDeclaration':
|
|
193
|
+
addEntry(stmt.name, undefined, 'tablepartial', stmt, stmt.span, entries, diagnostics, seen);
|
|
194
|
+
return;
|
|
195
|
+
case 'NoteDeclaration':
|
|
196
|
+
if (stmt.name) {
|
|
197
|
+
addEntry(stmt.name, undefined, 'note', stmt, stmt.span, entries, diagnostics, seen);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
// No symbols contributed by Project, Ref, top-level Records, ModuleImportDirective.
|
|
201
|
+
default:
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function addEntry(name, containerName, kind, declaration, span, entries, diagnostics, seen) {
|
|
206
|
+
const qualifiedName = containerName ? `${containerName}.${name}` : name;
|
|
207
|
+
const key = `${kind}:${qualifiedName}`;
|
|
208
|
+
if (seen.has(key)) {
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
severity: 'error',
|
|
211
|
+
code: 'duplicate-declaration',
|
|
212
|
+
message: `Duplicate ${kind} declaration '${qualifiedName}'. ` +
|
|
213
|
+
`Each ${kind} must have a unique qualified name within its scope.`,
|
|
214
|
+
span,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
seen.add(key);
|
|
219
|
+
entries.push({
|
|
220
|
+
qualifiedName,
|
|
221
|
+
name,
|
|
222
|
+
containerName,
|
|
223
|
+
kind,
|
|
224
|
+
declaration,
|
|
225
|
+
position: span.start,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/* -------------------------------------------------------------------------
|
|
229
|
+
* Pass 2: resolve references
|
|
230
|
+
* ----------------------------------------------------------------------- */
|
|
231
|
+
function resolveReferences(doc, symbols, diagnostics) {
|
|
232
|
+
for (const stmt of doc.statements) {
|
|
233
|
+
resolveTopLevel(stmt, symbols, diagnostics);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function resolveTopLevel(stmt, symbols, diagnostics) {
|
|
237
|
+
switch (stmt.kind) {
|
|
238
|
+
case 'EntityDeclaration':
|
|
239
|
+
resolveEntityBody(stmt, undefined, symbols, diagnostics);
|
|
240
|
+
return;
|
|
241
|
+
case 'ContainerDeclaration':
|
|
242
|
+
for (const body of stmt.body) {
|
|
243
|
+
if (body.kind === 'EntityDeclaration') {
|
|
244
|
+
resolveEntityBody(body, stmt.name, symbols, diagnostics);
|
|
245
|
+
}
|
|
246
|
+
else if (body.kind === 'ViewDeclaration') {
|
|
247
|
+
// Views have field declarations too -- walk them as if they
|
|
248
|
+
// were an entity.
|
|
249
|
+
resolveViewBody(body, stmt.name, symbols, diagnostics);
|
|
250
|
+
}
|
|
251
|
+
// EdgeDeclaration: similar shape but rare; we walk its fields
|
|
252
|
+
// when present. Skip for now to keep scope tight.
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
case 'ViewDeclaration':
|
|
256
|
+
resolveViewBody(stmt, undefined, symbols, diagnostics);
|
|
257
|
+
return;
|
|
258
|
+
case 'RefDeclaration':
|
|
259
|
+
resolveRefSpec(stmt.spec.source, symbols, diagnostics);
|
|
260
|
+
resolveRefSpec(stmt.spec.target, symbols, diagnostics);
|
|
261
|
+
return;
|
|
262
|
+
case 'TableGroupDeclaration':
|
|
263
|
+
for (const member of stmt.members) {
|
|
264
|
+
resolveTableGroupMember(member, stmt.span, symbols, diagnostics);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
case 'TopLevelRecordsDeclaration': {
|
|
268
|
+
const entity = resolveEntityRef(stmt.entityRef, symbols);
|
|
269
|
+
if (!entity) {
|
|
270
|
+
diagnostics.push({
|
|
271
|
+
severity: 'error',
|
|
272
|
+
code: 'unresolved-records-entity',
|
|
273
|
+
message: `Top-level records declaration refers to unknown entity '${stmt.entityRef}'.`,
|
|
274
|
+
span: stmt.span,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Validate each column is a field of the entity.
|
|
279
|
+
const fieldNames = new Set();
|
|
280
|
+
for (const item of entity.declaration.kind === 'EntityDeclaration' ? entity.declaration.body : []) {
|
|
281
|
+
if (item.kind === 'FieldDeclaration')
|
|
282
|
+
fieldNames.add(item.name);
|
|
283
|
+
}
|
|
284
|
+
for (const col of stmt.columns) {
|
|
285
|
+
if (!fieldNames.has(col)) {
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
severity: 'error',
|
|
288
|
+
code: 'unresolved-records-column',
|
|
289
|
+
message: `Records column '${col}' is not a field of entity '${stmt.entityRef}'.`,
|
|
290
|
+
span: stmt.span,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// No reference sites in TypeDeclaration headers (their settings are
|
|
298
|
+
// open-vocabulary). Field-type references inside a Type's object form
|
|
299
|
+
// are handled when we walk that Type as a "pseudo-entity" body --
|
|
300
|
+
// but for P6 we keep the scope tight and don't recurse into Type
|
|
301
|
+
// bodies. The cost is missed unresolved-type diagnostics for fields
|
|
302
|
+
// INSIDE composite Named Types, which is acceptable for v0.2.
|
|
303
|
+
default:
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function resolveEntityBody(entity, containerName, symbols, diagnostics) {
|
|
308
|
+
for (const item of entity.body) {
|
|
309
|
+
switch (item.kind) {
|
|
310
|
+
case 'FieldDeclaration':
|
|
311
|
+
resolveFieldDeclaration(item, symbols, diagnostics);
|
|
312
|
+
break;
|
|
313
|
+
case 'PartialInjection': {
|
|
314
|
+
const target = symbols.lookup(item.partialName);
|
|
315
|
+
if (!target || target.kind !== 'tablepartial') {
|
|
316
|
+
diagnostics.push({
|
|
317
|
+
severity: 'error',
|
|
318
|
+
code: 'unresolved-partial',
|
|
319
|
+
message: `Partial injection '~${item.partialName}' does not resolve to a TablePartial declaration.`,
|
|
320
|
+
span: item.span,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
// ChecksBlock, IndexesBlock, RecordsBlock, NoteBlock: contain
|
|
326
|
+
// opaque expressions or content that the resolver doesn't try to
|
|
327
|
+
// type-check. Skip.
|
|
328
|
+
default:
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
void containerName; // currently unused; reserved for future "field in nested scope" diagnostics
|
|
333
|
+
}
|
|
334
|
+
function resolveViewBody(view, containerName, symbols, diagnostics) {
|
|
335
|
+
// Views can contain field declarations; walk them the same way.
|
|
336
|
+
for (const item of view.body) {
|
|
337
|
+
if (item.kind === 'FieldDeclaration') {
|
|
338
|
+
resolveFieldDeclaration(item, symbols, diagnostics);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
void containerName;
|
|
342
|
+
}
|
|
343
|
+
function resolveFieldDeclaration(field, symbols, diagnostics) {
|
|
344
|
+
// Resolve the field's type expression. This walks into nested object
|
|
345
|
+
// types, arrays, unions, etc. recursively -- callers don't need to
|
|
346
|
+
// do their own recursion.
|
|
347
|
+
resolveTypeExpression(field.type, symbols, diagnostics);
|
|
348
|
+
// Resolve any inline `ref:` setting on the field. Other settings
|
|
349
|
+
// (notes, defaults, etc.) carry no name references that the
|
|
350
|
+
// resolver tracks.
|
|
351
|
+
for (const setting of field.settings) {
|
|
352
|
+
if (setting.value && setting.value.kind === 'RefValue') {
|
|
353
|
+
resolveRefSpec(setting.value.target, symbols, diagnostics);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function resolveTypeExpression(expr, symbols, diagnostics) {
|
|
358
|
+
switch (expr.kind) {
|
|
359
|
+
case 'ScalarType':
|
|
360
|
+
// A ScalarType is either a builtin or a reference to a Named Type
|
|
361
|
+
// (the parser doesn't distinguish at parse time). Look up only
|
|
362
|
+
// when the name isn't a builtin.
|
|
363
|
+
if (!isBuiltinType(expr.name)) {
|
|
364
|
+
const found = symbols.lookup(expr.name) ?? symbols.lookupBare(expr.name);
|
|
365
|
+
if (!found || found.kind !== 'type') {
|
|
366
|
+
diagnostics.push({
|
|
367
|
+
severity: 'error',
|
|
368
|
+
code: 'unresolved-type',
|
|
369
|
+
message: `Type '${expr.name}' is not a built-in type or declared Named Type.`,
|
|
370
|
+
span: expr.span,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
case 'NamedTypeReference': {
|
|
376
|
+
const found = symbols.lookup(expr.name) ?? symbols.lookupBare(expr.name);
|
|
377
|
+
if (!found || found.kind !== 'type') {
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
severity: 'error',
|
|
380
|
+
code: 'unresolved-type',
|
|
381
|
+
message: `Named type '${expr.name}' is not declared.`,
|
|
382
|
+
span: expr.span,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
case 'ObjectType':
|
|
388
|
+
for (const field of expr.fields) {
|
|
389
|
+
// ObjectType.fields is `(FieldDeclaration | NoteBlock | PartialInjection)[]`.
|
|
390
|
+
// Only FieldDeclaration carries a type expression to resolve;
|
|
391
|
+
// PartialInjection has a name (resolved as a partial reference),
|
|
392
|
+
// NoteBlock has no name resolution surface.
|
|
393
|
+
if (field.kind === 'FieldDeclaration') {
|
|
394
|
+
resolveTypeExpression(field.type, symbols, diagnostics);
|
|
395
|
+
}
|
|
396
|
+
else if (field.kind === 'PartialInjection') {
|
|
397
|
+
const target = symbols.lookup(field.partialName);
|
|
398
|
+
if (!target || target.kind !== 'tablepartial') {
|
|
399
|
+
diagnostics.push({
|
|
400
|
+
severity: 'error',
|
|
401
|
+
code: 'unresolved-partial',
|
|
402
|
+
message: `Partial injection '~${field.partialName}' does not resolve to a TablePartial declaration.`,
|
|
403
|
+
span: field.span,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
case 'ArrayType':
|
|
410
|
+
// `elementType` is optional (when the array body uses the `name type`
|
|
411
|
+
// alias form, the element type is reachable via a nested structure
|
|
412
|
+
// that's covered elsewhere; here we only recurse when present).
|
|
413
|
+
if (expr.elementType) {
|
|
414
|
+
resolveTypeExpression(expr.elementType, symbols, diagnostics);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
case 'MapType':
|
|
418
|
+
resolveTypeExpression(expr.keyType, symbols, diagnostics);
|
|
419
|
+
resolveTypeExpression(expr.valueType, symbols, diagnostics);
|
|
420
|
+
return;
|
|
421
|
+
case 'SetType':
|
|
422
|
+
resolveTypeExpression(expr.elementType, symbols, diagnostics);
|
|
423
|
+
return;
|
|
424
|
+
case 'TupleType':
|
|
425
|
+
for (const elem of expr.elements) {
|
|
426
|
+
resolveTypeExpression(elem.type, symbols, diagnostics);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
case 'JsonType':
|
|
430
|
+
// No nested type expressions.
|
|
431
|
+
return;
|
|
432
|
+
case 'OneOfType':
|
|
433
|
+
case 'AnyOfType':
|
|
434
|
+
case 'AllOfType':
|
|
435
|
+
for (const alt of expr.alternatives) {
|
|
436
|
+
resolveTypeExpression(alt.type, symbols, diagnostics);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
case 'UnionType':
|
|
440
|
+
// UnionType uses `members` (not `alternatives`), and each member is
|
|
441
|
+
// a (ScalarType | NamedTypeReference | NullTypeLiteral). The first
|
|
442
|
+
// two have name fields that may need resolution; NullTypeLiteral is
|
|
443
|
+
// a built-in placeholder for the `null` literal and has nothing to
|
|
444
|
+
// resolve, so we skip it.
|
|
445
|
+
for (const member of expr.members) {
|
|
446
|
+
if (member.kind !== 'NullTypeLiteral') {
|
|
447
|
+
resolveTypeExpression(member, symbols, diagnostics);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
default:
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function resolveRefSpec(endpoint, symbols, diagnostics) {
|
|
456
|
+
// Foreign-key endpoint resolution proceeds in four phases:
|
|
457
|
+
//
|
|
458
|
+
// PHASE 1 Find the entity. The entity is the longest leading run of
|
|
459
|
+
// PathField segments that resolves to a declared entity.
|
|
460
|
+
//
|
|
461
|
+
// PHASE 2 Compute the post-entity path -- the segments that name a
|
|
462
|
+
// field on the entity and (optionally) navigate into the
|
|
463
|
+
// field's type. Includes any non-PathField segments (array
|
|
464
|
+
// wildcards, indices, map keys) that follow.
|
|
465
|
+
//
|
|
466
|
+
// PHASE 3 Validate the top-level field exists on the entity.
|
|
467
|
+
//
|
|
468
|
+
// PHASE 4 If more segments remain, walk the field's type expression
|
|
469
|
+
// consuming one segment at a time. Composite endpoints
|
|
470
|
+
// (`(f1, f2)`) get validated against whichever type the walk
|
|
471
|
+
// lands on (the entity itself, or a nested object).
|
|
472
|
+
//
|
|
473
|
+
// The walker handles ObjectType field access, Array/Set wildcards and
|
|
474
|
+
// indices, Map key access, Tuple positional access, and Named Type
|
|
475
|
+
// dereferencing (with a depth limit to break cycles).
|
|
476
|
+
// PHASE 1: collect leading PathField run.
|
|
477
|
+
const leadingFields = [];
|
|
478
|
+
for (const seg of endpoint.path) {
|
|
479
|
+
if (seg.kind === 'PathField') {
|
|
480
|
+
leadingFields.push(seg.name);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Stop at the first non-field segment -- nothing past it can be
|
|
484
|
+
// part of the entity name.
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (leadingFields.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
const hasComposite = !!(endpoint.compositeFields && endpoint.compositeFields.length > 0);
|
|
491
|
+
const hasNonFieldTail = endpoint.path.length > leadingFields.length;
|
|
492
|
+
// Decide how many leading PathFields could form the entity. If there
|
|
493
|
+
// are non-field segments after the leading run (e.g., `[*]`), the
|
|
494
|
+
// entity must end at least one PathField before, because navigation
|
|
495
|
+
// through array/map/etc. can only begin AFTER a field has been
|
|
496
|
+
// selected. If there are no non-field segments and no composite
|
|
497
|
+
// (the simple `a.b.c` case), the entity is everything except the last
|
|
498
|
+
// segment. With composite and no nested tail, the entity can be the
|
|
499
|
+
// ENTIRE leading run -- composite fields are listed separately.
|
|
500
|
+
let maxEntityLen;
|
|
501
|
+
if (hasNonFieldTail) {
|
|
502
|
+
maxEntityLen = leadingFields.length - 1;
|
|
503
|
+
}
|
|
504
|
+
else if (hasComposite) {
|
|
505
|
+
maxEntityLen = leadingFields.length;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
maxEntityLen = leadingFields.length - 1;
|
|
509
|
+
}
|
|
510
|
+
if (maxEntityLen < 1)
|
|
511
|
+
return;
|
|
512
|
+
// PHASE 1 (cont'd): longest-prefix entity match.
|
|
513
|
+
let entity;
|
|
514
|
+
let entityPrefixLen = 0;
|
|
515
|
+
for (let len = maxEntityLen; len >= 1; len -= 1) {
|
|
516
|
+
const candidate = leadingFields.slice(0, len).join('.');
|
|
517
|
+
const found = resolveEntityRef(candidate, symbols);
|
|
518
|
+
if (found) {
|
|
519
|
+
entity = found;
|
|
520
|
+
entityPrefixLen = len;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!entity) {
|
|
525
|
+
const guess = leadingFields.slice(0, maxEntityLen).join('.');
|
|
526
|
+
diagnostics.push({
|
|
527
|
+
severity: 'error',
|
|
528
|
+
code: 'unresolved-entity',
|
|
529
|
+
message: `Foreign-key endpoint references unknown entity '${guess}'.`,
|
|
530
|
+
span: endpoint.span,
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (entity.declaration.kind !== 'EntityDeclaration')
|
|
535
|
+
return;
|
|
536
|
+
// PHASE 2: compute the post-entity portion of endpoint.path.
|
|
537
|
+
const remaining = endpoint.path.slice(entityPrefixLen);
|
|
538
|
+
// Special case: path is JUST the entity (no remaining segments). With
|
|
539
|
+
// composite, validate composite fields against the entity body.
|
|
540
|
+
if (remaining.length === 0) {
|
|
541
|
+
if (hasComposite) {
|
|
542
|
+
const fieldNameSet = collectFieldNames(entity.declaration);
|
|
543
|
+
for (const fname of endpoint.compositeFields) {
|
|
544
|
+
if (!fieldNameSet.has(fname)) {
|
|
545
|
+
diagnostics.push({
|
|
546
|
+
severity: 'error',
|
|
547
|
+
code: 'unresolved-field',
|
|
548
|
+
message: `Field '${fname}' is not declared on entity '${entity.qualifiedName}'.`,
|
|
549
|
+
span: endpoint.span,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// PHASE 3: the first remaining segment is the top-level field name.
|
|
557
|
+
const topSeg = remaining[0];
|
|
558
|
+
if (topSeg.kind !== 'PathField') {
|
|
559
|
+
// E.g., entity followed immediately by `[*]`. Structurally invalid
|
|
560
|
+
// because navigation can only begin after a field selection.
|
|
561
|
+
diagnostics.push({
|
|
562
|
+
severity: 'error',
|
|
563
|
+
code: 'invalid-nested-path',
|
|
564
|
+
message: `Foreign-key path on entity '${entity.qualifiedName}' starts with a non-field segment; expected a field name first.`,
|
|
565
|
+
span: topSeg.span,
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const topField = entity.declaration.body.find((item) => item.kind === 'FieldDeclaration' && item.name === topSeg.name);
|
|
570
|
+
if (!topField) {
|
|
571
|
+
diagnostics.push({
|
|
572
|
+
severity: 'error',
|
|
573
|
+
code: 'unresolved-field',
|
|
574
|
+
message: `Field '${topSeg.name}' is not declared on entity '${entity.qualifiedName}'.`,
|
|
575
|
+
span: topSeg.span,
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// PHASE 4: walk the field's type for any further segments.
|
|
580
|
+
let finalType = topField.type;
|
|
581
|
+
if (remaining.length > 1) {
|
|
582
|
+
const nested = remaining.slice(1);
|
|
583
|
+
finalType = walkTypePath(topField.type, nested, symbols, diagnostics, 0);
|
|
584
|
+
// walkTypePath returns undefined on error (and has already emitted
|
|
585
|
+
// a diagnostic). Continue to composite validation only when the walk
|
|
586
|
+
// succeeded.
|
|
587
|
+
if (!finalType)
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Composite endpoint: validate composite fields against the final type.
|
|
591
|
+
if (hasComposite) {
|
|
592
|
+
const compositeFieldNames = extractFieldNamesFromType(finalType, symbols);
|
|
593
|
+
if (!compositeFieldNames) {
|
|
594
|
+
// The final type isn't an object-shaped type, so composite-field
|
|
595
|
+
// validation can't apply. Emit a structural diagnostic.
|
|
596
|
+
diagnostics.push({
|
|
597
|
+
severity: 'error',
|
|
598
|
+
code: 'invalid-nested-path',
|
|
599
|
+
message: `Cannot apply composite fields (${endpoint.compositeFields.join(', ')}) at this point in the path; the navigated type is not an object.`,
|
|
600
|
+
span: endpoint.span,
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
for (const fname of endpoint.compositeFields) {
|
|
605
|
+
if (!compositeFieldNames.has(fname)) {
|
|
606
|
+
diagnostics.push({
|
|
607
|
+
severity: 'error',
|
|
608
|
+
code: 'unresolved-field',
|
|
609
|
+
message: `Field '${fname}' is not declared at the FK endpoint's nested object type.`,
|
|
610
|
+
span: endpoint.span,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function collectFieldNames(entity) {
|
|
617
|
+
const names = new Set();
|
|
618
|
+
for (const item of entity.body) {
|
|
619
|
+
if (item.kind === 'FieldDeclaration')
|
|
620
|
+
names.add(item.name);
|
|
621
|
+
}
|
|
622
|
+
return names;
|
|
623
|
+
}
|
|
624
|
+
/* -------------------------------------------------------------------------
|
|
625
|
+
* Type-path navigation (nested-field FK validation)
|
|
626
|
+
*
|
|
627
|
+
* Given a starting `TypeExpression` and a sequence of `PathSegment`s,
|
|
628
|
+
* walk the type structure consuming one segment at a time. Each segment
|
|
629
|
+
* shapes how we advance:
|
|
630
|
+
*
|
|
631
|
+
* PathField -> requires an ObjectType; selects the named field
|
|
632
|
+
* PathArrayWildcard [*] -> requires Array/Set; advances to the element type
|
|
633
|
+
* PathArrayIndex [N] -> Array (-> element), Tuple (-> position N's type)
|
|
634
|
+
* PathMapKey [k] -> requires MapType; advances to the value type
|
|
635
|
+
*
|
|
636
|
+
* Named Types are dereferenced on entry (a `ScalarType` whose name isn't
|
|
637
|
+
* a built-in or a `NamedTypeReference` -> look up in the symbol table;
|
|
638
|
+
* if a scalar Named Type, deref to its base; if an object Named Type,
|
|
639
|
+
* synthesize an ObjectType wrapping its body). A depth limit breaks
|
|
640
|
+
* pathological cycles (`Type A B; Type B A`).
|
|
641
|
+
*
|
|
642
|
+
* On structural failure (wrong shape for the segment) or unresolved
|
|
643
|
+
* names, the walker emits a diagnostic and returns undefined. On success
|
|
644
|
+
* it returns the type after consuming all segments.
|
|
645
|
+
* ----------------------------------------------------------------------- */
|
|
646
|
+
const MAX_TYPE_WALK_DEPTH = 16;
|
|
647
|
+
function walkTypePath(startType, segments, symbols, diagnostics, depth) {
|
|
648
|
+
let current = startType;
|
|
649
|
+
for (const seg of segments) {
|
|
650
|
+
const next = stepIntoType(current, seg, symbols, diagnostics, depth);
|
|
651
|
+
if (!next)
|
|
652
|
+
return undefined;
|
|
653
|
+
current = next;
|
|
654
|
+
}
|
|
655
|
+
return current;
|
|
656
|
+
}
|
|
657
|
+
function stepIntoType(current, seg, symbols, diagnostics, depth) {
|
|
658
|
+
// Dereference Named Types up front so the segment-kind switch below
|
|
659
|
+
// operates on the structural form.
|
|
660
|
+
const resolved = dereferenceNamedType(current, symbols, depth);
|
|
661
|
+
if (!resolved)
|
|
662
|
+
return undefined; // depth limit hit (or unresolved Named Type)
|
|
663
|
+
current = resolved;
|
|
664
|
+
switch (seg.kind) {
|
|
665
|
+
case 'PathField': {
|
|
666
|
+
// Field access is only meaningful on ObjectType. Other shapes
|
|
667
|
+
// (array/map/set/tuple) require an explicit element-access segment
|
|
668
|
+
// first ([*], [N], [key]).
|
|
669
|
+
if (current.kind !== 'ObjectType') {
|
|
670
|
+
diagnostics.push({
|
|
671
|
+
severity: 'error',
|
|
672
|
+
code: 'invalid-nested-path',
|
|
673
|
+
message: `Cannot navigate field '${seg.name}' through ${current.kind}; use [*] (array/set), [N] (tuple), or [key] (map) before naming a field.`,
|
|
674
|
+
span: seg.span,
|
|
675
|
+
});
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const field = current.fields.find((f) => f.kind === 'FieldDeclaration' && f.name === seg.name);
|
|
679
|
+
if (!field) {
|
|
680
|
+
diagnostics.push({
|
|
681
|
+
severity: 'error',
|
|
682
|
+
code: 'unresolved-field',
|
|
683
|
+
message: `Field '${seg.name}' is not declared on the nested object type.`,
|
|
684
|
+
span: seg.span,
|
|
685
|
+
});
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
return field.type;
|
|
689
|
+
}
|
|
690
|
+
case 'PathArrayWildcard': {
|
|
691
|
+
if (current.kind === 'ArrayType') {
|
|
692
|
+
if (!current.elementType) {
|
|
693
|
+
// `array [name type]` form -- the element type lives elsewhere
|
|
694
|
+
// (elementName + elementSettings). For walker purposes, treat
|
|
695
|
+
// the array as having an opaque element and stop walking
|
|
696
|
+
// further by returning undefined silently (no diagnostic; this
|
|
697
|
+
// is a v0.2 shape the spec calls out as alias-only).
|
|
698
|
+
diagnostics.push({
|
|
699
|
+
severity: 'error',
|
|
700
|
+
code: 'invalid-nested-path',
|
|
701
|
+
message: `Array uses the alias 'name type' form which does not expose a navigable element type for further path traversal.`,
|
|
702
|
+
span: seg.span,
|
|
703
|
+
});
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
return current.elementType;
|
|
707
|
+
}
|
|
708
|
+
if (current.kind === 'SetType') {
|
|
709
|
+
return current.elementType;
|
|
710
|
+
}
|
|
711
|
+
diagnostics.push({
|
|
712
|
+
severity: 'error',
|
|
713
|
+
code: 'invalid-nested-path',
|
|
714
|
+
message: `Array wildcard [*] is only valid on array or set types, not ${current.kind}.`,
|
|
715
|
+
span: seg.span,
|
|
716
|
+
});
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
case 'PathArrayIndex': {
|
|
720
|
+
if (current.kind === 'ArrayType') {
|
|
721
|
+
return current.elementType;
|
|
722
|
+
}
|
|
723
|
+
if (current.kind === 'TupleType') {
|
|
724
|
+
const elem = current.elements.find((e) => e.position === seg.index);
|
|
725
|
+
if (!elem) {
|
|
726
|
+
diagnostics.push({
|
|
727
|
+
severity: 'error',
|
|
728
|
+
code: 'invalid-nested-path',
|
|
729
|
+
message: `Tuple has no element at position [${seg.index}].`,
|
|
730
|
+
span: seg.span,
|
|
731
|
+
});
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
return elem.type;
|
|
735
|
+
}
|
|
736
|
+
diagnostics.push({
|
|
737
|
+
severity: 'error',
|
|
738
|
+
code: 'invalid-nested-path',
|
|
739
|
+
message: `Numeric index [${seg.index}] is only valid on array or tuple types, not ${current.kind}.`,
|
|
740
|
+
span: seg.span,
|
|
741
|
+
});
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
case 'PathMapKey': {
|
|
745
|
+
if (current.kind === 'MapType') {
|
|
746
|
+
return current.valueType;
|
|
747
|
+
}
|
|
748
|
+
diagnostics.push({
|
|
749
|
+
severity: 'error',
|
|
750
|
+
code: 'invalid-nested-path',
|
|
751
|
+
message: `Map key [${seg.key}] is only valid on map types, not ${current.kind}.`,
|
|
752
|
+
span: seg.span,
|
|
753
|
+
});
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
default:
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Resolve a TypeExpression to its structural form by chasing through
|
|
762
|
+
* Named Type references. Returns the resolved type, or undefined when
|
|
763
|
+
* the depth limit is hit (cycle) or the Named Type doesn't exist (in
|
|
764
|
+
* which case the field-type resolver has already emitted a diagnostic
|
|
765
|
+
* elsewhere -- we silently fail here to avoid duplicate errors).
|
|
766
|
+
*
|
|
767
|
+
* For object-form Named Types, synthesizes an ObjectType wrapping the
|
|
768
|
+
* Type's body so callers can navigate via PathField uniformly.
|
|
769
|
+
*/
|
|
770
|
+
function dereferenceNamedType(type, symbols, depth) {
|
|
771
|
+
if (depth > MAX_TYPE_WALK_DEPTH)
|
|
772
|
+
return undefined;
|
|
773
|
+
// ScalarType might be a Named Type reference (parser ambiguity).
|
|
774
|
+
// NamedTypeReference always is.
|
|
775
|
+
let name;
|
|
776
|
+
if (type.kind === 'ScalarType' && !isBuiltinType(type.name)) {
|
|
777
|
+
name = type.name;
|
|
778
|
+
}
|
|
779
|
+
else if (type.kind === 'NamedTypeReference') {
|
|
780
|
+
name = type.name;
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// Already a structural form.
|
|
784
|
+
return type;
|
|
785
|
+
}
|
|
786
|
+
const sym = symbols.lookup(name) ?? symbols.lookupBare(name);
|
|
787
|
+
if (!sym || sym.kind !== 'type' || sym.declaration.kind !== 'TypeDeclaration') {
|
|
788
|
+
// Unresolved -- the field-type pass will diagnose this. Return
|
|
789
|
+
// undefined so the walker bails without emitting a duplicate error.
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
792
|
+
const td = sym.declaration;
|
|
793
|
+
if (td.scalarBase) {
|
|
794
|
+
// Scalar Named Type -- recurse through to the base.
|
|
795
|
+
return dereferenceNamedType(td.scalarBase, symbols, depth + 1);
|
|
796
|
+
}
|
|
797
|
+
// Object-form Named Type. Synthesize an ObjectType so the walker can
|
|
798
|
+
// navigate its fields uniformly. The span points at the Type
|
|
799
|
+
// declaration; segment-level positions remain correct because we don't
|
|
800
|
+
// use the synthetic ObjectType's span for diagnostics.
|
|
801
|
+
const synthesized = {
|
|
802
|
+
kind: 'ObjectType',
|
|
803
|
+
keyword: 'object',
|
|
804
|
+
fields: td.body,
|
|
805
|
+
span: td.span,
|
|
806
|
+
};
|
|
807
|
+
return synthesized;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Get the set of declared field names on a type, dereferencing Named
|
|
811
|
+
* Types as needed. Returns undefined when the type isn't an object-shaped
|
|
812
|
+
* type at all (composite-field validation can't apply).
|
|
813
|
+
*/
|
|
814
|
+
function extractFieldNamesFromType(type, symbols) {
|
|
815
|
+
const resolved = dereferenceNamedType(type, symbols, 0);
|
|
816
|
+
if (!resolved || resolved.kind !== 'ObjectType')
|
|
817
|
+
return undefined;
|
|
818
|
+
const names = new Set();
|
|
819
|
+
for (const f of resolved.fields) {
|
|
820
|
+
if (f.kind === 'FieldDeclaration')
|
|
821
|
+
names.add(f.name);
|
|
822
|
+
}
|
|
823
|
+
return names;
|
|
824
|
+
}
|
|
825
|
+
function resolveTableGroupMember(member, span, symbols, diagnostics) {
|
|
826
|
+
const found = resolveEntityRef(member, symbols);
|
|
827
|
+
if (!found) {
|
|
828
|
+
diagnostics.push({
|
|
829
|
+
severity: 'error',
|
|
830
|
+
code: 'unresolved-tablegroup-member',
|
|
831
|
+
message: `TableGroup member '${member}' does not resolve to an entity.`,
|
|
832
|
+
span,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Resolve an entity reference by name. Accepts bare (`dim_customer`) or
|
|
838
|
+
* qualified (`core.dim_customer`) form. Bare references are resolved
|
|
839
|
+
* through the SymbolTable's bare-name lookup; ambiguous bare references
|
|
840
|
+
* (matching multiple containers) return undefined.
|
|
841
|
+
*/
|
|
842
|
+
function resolveEntityRef(ref, symbols) {
|
|
843
|
+
// Try qualified first.
|
|
844
|
+
const qualified = symbols.lookup(ref);
|
|
845
|
+
if (qualified && qualified.kind === 'entity')
|
|
846
|
+
return qualified;
|
|
847
|
+
// Try bare.
|
|
848
|
+
if (!ref.includes('.')) {
|
|
849
|
+
const bare = symbols.lookupBare(ref);
|
|
850
|
+
if (bare && bare.kind === 'entity')
|
|
851
|
+
return bare;
|
|
852
|
+
}
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|