@prisma-next/psl-parser 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parser.ts CHANGED
@@ -14,9 +14,9 @@ import type {
14
14
  PslDiagnostic,
15
15
  PslDiagnosticCode,
16
16
  PslDocumentAst,
17
- PslEnum,
18
- PslEnumValue,
19
17
  PslExtensionBlock,
18
+ PslExtensionBlockAttribute,
19
+ PslExtensionBlockAttributeArg,
20
20
  PslExtensionBlockParamValue,
21
21
  PslField,
22
22
  PslFieldAttribute,
@@ -80,7 +80,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
80
80
  interface NamespaceAccumulator {
81
81
  name: string;
82
82
  models: PslModel[];
83
- enums: PslEnum[];
84
83
  compositeTypes: PslCompositeType[];
85
84
  extensionBlocks: PslExtensionBlock[];
86
85
  span: PslSpan | undefined;
@@ -97,7 +96,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
97
96
  acc = {
98
97
  name,
99
98
  models: [],
100
- enums: [],
101
99
  compositeTypes: [],
102
100
  extensionBlocks: [],
103
101
  span: spanIfNew,
@@ -145,21 +143,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
145
143
  continue;
146
144
  }
147
145
 
148
- const enumMatch = line.match(/^enum\s+([A-Za-z_]\w*)\s*\{$/);
149
- if (enumMatch) {
150
- const bounds = findBlockBounds(context, lineIndex);
151
- const name = enumMatch[1] ?? '';
152
- if (name.length > 0) {
153
- const acc = getOrCreateNamespace(
154
- currentNamespaceName,
155
- createTrimmedLineSpan(context, lineIndex),
156
- );
157
- acc.enums.push(parseEnumBlock(context, name, bounds));
158
- }
159
- lineIndex = bounds.endLine + 1;
160
- continue;
161
- }
162
-
163
146
  const compositeTypeMatch = line.match(/^type\s+([A-Za-z_]\w*)\s*\{$/);
164
147
  if (compositeTypeMatch) {
165
148
  const bounds = findBlockBounds(context, lineIndex);
@@ -266,15 +249,13 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
266
249
 
267
250
  // Named-type validation: types are document-scoped (one block, outside any
268
251
  // namespace), so collision checks compare named-type names against every
269
- // model/enum/composite-type in every namespace.
252
+ // model/composite-type in every namespace.
270
253
  const allModels: PslModel[] = [];
271
- const allEnums: PslEnum[] = [];
272
254
  const allCompositeTypes: PslCompositeType[] = [];
273
255
  for (const name of namespaceOrder) {
274
256
  const acc = namespacesByName.get(name);
275
257
  if (!acc) continue;
276
258
  allModels.push(...acc.models);
277
- allEnums.push(...acc.enums);
278
259
  allCompositeTypes.push(...acc.compositeTypes);
279
260
  }
280
261
 
@@ -282,7 +263,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
282
263
  (typesBlock?.declarations ?? []).map((declaration) => declaration.name),
283
264
  );
284
265
  const modelNames = new Set(allModels.map((model) => model.name));
285
- const enumNames = new Set(allEnums.map((enumBlock) => enumBlock.name));
286
266
  const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
287
267
  for (const declaration of typesBlock?.declarations ?? []) {
288
268
  if (SCALAR_TYPES.has(declaration.name)) {
@@ -299,14 +279,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
299
279
  message: `Named type "${declaration.name}" conflicts with model name "${declaration.name}"`,
300
280
  span: declaration.span,
301
281
  });
302
- continue;
303
- }
304
- if (enumNames.has(declaration.name)) {
305
- pushDiagnostic(context, {
306
- code: 'PSL_INVALID_TYPES_MEMBER',
307
- message: `Named type "${declaration.name}" conflicts with enum name "${declaration.name}"`,
308
- span: declaration.span,
309
- });
310
282
  }
311
283
  }
312
284
 
@@ -330,7 +302,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
330
302
  if (
331
303
  name === UNSPECIFIED_PSL_NAMESPACE_ID &&
332
304
  acc.models.length === 0 &&
333
- acc.enums.length === 0 &&
334
305
  acc.compositeTypes.length === 0 &&
335
306
  acc.extensionBlocks.length === 0
336
307
  ) {
@@ -348,7 +319,6 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
348
319
  if (
349
320
  hasRelationAttribute ||
350
321
  modelNames.has(field.typeName) ||
351
- enumNames.has(field.typeName) ||
352
322
  compositeTypeNames.has(field.typeName) ||
353
323
  SCALAR_TYPES.has(field.typeName)
354
324
  ) {
@@ -364,12 +334,7 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
364
334
  makePslNamespace({
365
335
  kind: 'namespace',
366
336
  name,
367
- entries: makePslNamespaceEntries(
368
- normalizedModels,
369
- acc.enums,
370
- acc.compositeTypes,
371
- acc.extensionBlocks,
372
- ),
337
+ entries: makePslNamespaceEntries(normalizedModels, acc.compositeTypes, acc.extensionBlocks),
373
338
  span: acc.span ?? documentSpan,
374
339
  }),
375
340
  );
@@ -502,89 +467,6 @@ function parseCompositeTypeBlock(
502
467
  };
503
468
  }
504
469
 
505
- function parseEnumBlock(context: ParserContext, name: string, bounds: BlockBounds): PslEnum {
506
- const values: PslEnumValue[] = [];
507
- const attributes: PslAttribute[] = [];
508
-
509
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
510
- const raw = context.lines[lineIndex] ?? '';
511
- const line = stripInlineComment(raw).trim();
512
- if (line.length === 0) {
513
- continue;
514
- }
515
-
516
- if (line.startsWith('@@')) {
517
- const attribute = parseEnumAttribute(context, line, lineIndex);
518
- if (attribute) {
519
- attributes.push(attribute);
520
- }
521
- continue;
522
- }
523
-
524
- // An enum member line is the bare member identifier, optionally followed
525
- // by a `@map("storage-label")` attribute. The map attribute lets the
526
- // printer round-trip enum values whose original storage label is not a
527
- // valid PSL identifier (e.g. PostgreSQL enum labels with hyphens).
528
- const valueMatch = line.match(/^([A-Za-z_]\w*)(?:\s+@map\(\s*"((?:[^"\\]|\\.)*)"\s*\))?$/);
529
- if (!valueMatch) {
530
- pushDiagnostic(context, {
531
- code: 'PSL_INVALID_ENUM_MEMBER',
532
- message: `Invalid enum value declaration "${line}"`,
533
- span: createTrimmedLineSpan(context, lineIndex),
534
- });
535
- continue;
536
- }
537
-
538
- const mapName = valueMatch[2] !== undefined ? unescapePslString(valueMatch[2]) : undefined;
539
-
540
- values.push({
541
- kind: 'enumValue',
542
- name: valueMatch[1] ?? '',
543
- ...(mapName !== undefined ? { mapName } : {}),
544
- span: createTrimmedLineSpan(context, lineIndex),
545
- });
546
- }
547
-
548
- return {
549
- kind: 'enum',
550
- name,
551
- values,
552
- attributes,
553
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
554
- };
555
- }
556
-
557
- /**
558
- * Decode PSL escape sequences (`\\`, `\"`, `\'`, `\n`, `\r`) inside a
559
- * quoted-literal body. The argument is the body of the literal with the
560
- * surrounding quotes already stripped by the caller. Mirrors the inverse
561
- * helper in `@prisma-next/psl-printer`'s `escapePslString` so a string
562
- * round-trips parser → printer → parser unchanged.
563
- */
564
- function unescapePslString(value: string): string {
565
- let result = '';
566
- for (let i = 0; i < value.length; i++) {
567
- const ch = value.charCodeAt(i);
568
- if (ch !== 0x5c /* '\\' */ || i + 1 >= value.length) {
569
- result += value[i];
570
- continue;
571
- }
572
- const next = value[i + 1];
573
- if (next === '\\' || next === '"' || next === "'") {
574
- result += next;
575
- } else if (next === 'n') {
576
- result += '\n';
577
- } else if (next === 'r') {
578
- result += '\r';
579
- } else {
580
- result += '\\';
581
- result += next;
582
- }
583
- i++;
584
- }
585
- return result;
586
- }
587
-
588
470
  function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesBlock {
589
471
  const declarations: PslNamedTypeDeclaration[] = [];
590
472
 
@@ -792,50 +674,6 @@ function parseModelAttribute(
792
674
  });
793
675
  }
794
676
 
795
- function parseEnumAttribute(
796
- context: ParserContext,
797
- line: string,
798
- lineIndex: number,
799
- ): PslAttribute | undefined {
800
- const rawLine = context.lines[lineIndex] ?? '';
801
- const tokenParse = extractAttributeTokensWithSpans(
802
- context,
803
- lineIndex,
804
- line,
805
- firstNonWhitespaceColumn(rawLine),
806
- );
807
- if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
808
- pushDiagnostic(context, {
809
- code: 'PSL_INVALID_ENUM_MEMBER',
810
- message: `Invalid enum value declaration "${line}"`,
811
- span: createTrimmedLineSpan(context, lineIndex),
812
- });
813
- return undefined;
814
- }
815
- const token = tokenParse.tokens[0];
816
- if (!token) {
817
- return undefined;
818
- }
819
- const parsed = parseAttributeToken(context, {
820
- token: token.text,
821
- target: 'enum',
822
- lineIndex,
823
- span: token.span,
824
- });
825
- if (!parsed) {
826
- return undefined;
827
- }
828
- if (parsed.name !== 'map') {
829
- pushDiagnostic(context, {
830
- code: 'PSL_INVALID_ENUM_MEMBER',
831
- message: `Invalid enum value declaration "${line}"`,
832
- span: createTrimmedLineSpan(context, lineIndex),
833
- });
834
- return undefined;
835
- }
836
- return parsed;
837
- }
838
-
839
677
  function parseField(context: ParserContext, line: string, lineIndex: number): PslField | undefined {
840
678
  const fieldMatch = line.match(/^([A-Za-z_]\w*)(\s+)(.+)$/);
841
679
  if (!fieldMatch) {
@@ -1592,18 +1430,15 @@ function lookupExtensionBlockDescriptor(
1592
1430
 
1593
1431
  /**
1594
1432
  * Reads an extension block body generically, driven by the descriptor's
1595
- * `parameters` map. Each `key = <rhs>` line in the body is matched against
1596
- * a declared parameter and the RHS is captured per the parameter's kind:
1433
+ * `parameters` map. Each body line is one of:
1597
1434
  *
1598
- * - `ref` → the bareword identifier token (`PslExtensionBlockParamRef`)
1599
- * - `value` → the raw RHS text verbatim (`PslExtensionBlockParamScalarValue`)
1600
- * - `option` the chosen bareword token (`PslExtensionBlockParamOption`)
1601
- * - `list` → bracketed `[a, b, …]` list, each element captured per `of`
1602
- * (`PslExtensionBlockParamList`)
1435
+ * - `@@name(args…)` a block attribute, captured in `blockAttributes`
1436
+ * - `key = <rhs>` a parameter entry, captured in `parameters` per descriptor
1437
+ * - `key` a bare-identifier entry (no `= value`), captured as `kind:'bare'`
1603
1438
  *
1604
- * No validation runs here (unknown key, missing required, codec acceptance,
1605
- * option-in-set, ref resolution). A body line that is not `key = value`
1606
- * shaped emits a `PSL_INVALID_EXTENSION_BLOCK_MEMBER` parse diagnostic.
1439
+ * Duplicate parameter keys emit `PSL_EXTENSION_DUPLICATE_PARAMETER`; the first
1440
+ * occurrence wins. `@@` attribute lines are not validated here (name or args);
1441
+ * that is a concern of the consuming interpreter.
1607
1442
  */
1608
1443
  function parseExtensionBlock(
1609
1444
  context: ParserContext,
@@ -1613,6 +1448,7 @@ function parseExtensionBlock(
1613
1448
  bounds: BlockBounds,
1614
1449
  ): PslExtensionBlock {
1615
1450
  const parameters: Record<string, PslExtensionBlockParamValue> = {};
1451
+ const blockAttributes: PslExtensionBlockAttribute[] = [];
1616
1452
 
1617
1453
  for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
1618
1454
  const raw = context.lines[lineIndex] ?? '';
@@ -1621,44 +1457,137 @@ function parseExtensionBlock(
1621
1457
  continue;
1622
1458
  }
1623
1459
 
1624
- const assignMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
1625
- if (!assignMatch) {
1626
- pushDiagnostic(context, {
1627
- code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
1628
- message: `Invalid extension block body line "${line}"; expected "key = value"`,
1629
- span: createTrimmedLineSpan(context, lineIndex),
1630
- });
1460
+ if (line.startsWith('@@')) {
1461
+ const attr = parseExtensionBlockAttribute(context, line, lineIndex);
1462
+ if (attr !== undefined) {
1463
+ blockAttributes.push(attr);
1464
+ }
1631
1465
  continue;
1632
1466
  }
1633
1467
 
1634
- const key = assignMatch[1] ?? '';
1635
- const rhsRaw = (assignMatch[2] ?? '').trim();
1636
- const paramDescriptor = descriptor.parameters[key];
1468
+ const assignMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
1469
+ if (assignMatch) {
1470
+ const key = assignMatch[1] ?? '';
1471
+ const rhsRaw = (assignMatch[2] ?? '').trim();
1637
1472
 
1638
- if (paramDescriptor === undefined) {
1639
- // Unknown parameter — captured as a raw value; the generic validator reports this.
1640
- parameters[key] = {
1641
- kind: 'value',
1642
- raw: rhsRaw,
1643
- span: createTrimmedLineSpan(context, lineIndex),
1644
- };
1473
+ if (Object.hasOwn(parameters, key)) {
1474
+ pushDiagnostic(context, {
1475
+ code: 'PSL_EXTENSION_DUPLICATE_PARAMETER',
1476
+ message: `Duplicate parameter "${key}" in "${descriptor.keyword}" block "${blockName}"; first occurrence wins`,
1477
+ span: createTrimmedLineSpan(context, lineIndex),
1478
+ });
1479
+ continue;
1480
+ }
1481
+
1482
+ const paramDescriptor = descriptor.parameters[key];
1483
+ if (paramDescriptor === undefined) {
1484
+ parameters[key] = {
1485
+ kind: 'value',
1486
+ raw: rhsRaw,
1487
+ span: createTrimmedLineSpan(context, lineIndex),
1488
+ };
1489
+ } else {
1490
+ const captured = captureParamRhs(context, lineIndex, rhsRaw, paramDescriptor);
1491
+ if (captured !== undefined) {
1492
+ parameters[key] = captured;
1493
+ }
1494
+ }
1645
1495
  continue;
1646
1496
  }
1647
1497
 
1648
- const captured = captureParamRhs(context, lineIndex, rhsRaw, paramDescriptor);
1649
- if (captured !== undefined) {
1650
- parameters[key] = captured;
1498
+ const bareMatch = line.match(/^([A-Za-z_]\w*)$/);
1499
+ if (bareMatch) {
1500
+ const key = bareMatch[1] ?? '';
1501
+
1502
+ if (!descriptor.variadicParameters) {
1503
+ pushDiagnostic(context, {
1504
+ code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
1505
+ message: `Invalid extension block body line "${line}"; expected "key = value" or "@@attribute"`,
1506
+ span: createTrimmedLineSpan(context, lineIndex),
1507
+ });
1508
+ continue;
1509
+ }
1510
+
1511
+ if (Object.hasOwn(descriptor.parameters, key)) {
1512
+ pushDiagnostic(context, {
1513
+ code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
1514
+ message: `Parameter "${key}" in "${descriptor.keyword}" block "${blockName}" is declared in the descriptor and must be supplied as "${key} = value"`,
1515
+ span: createTrimmedLineSpan(context, lineIndex),
1516
+ });
1517
+ continue;
1518
+ }
1519
+
1520
+ if (Object.hasOwn(parameters, key)) {
1521
+ pushDiagnostic(context, {
1522
+ code: 'PSL_EXTENSION_DUPLICATE_PARAMETER',
1523
+ message: `Duplicate parameter "${key}" in "${descriptor.keyword}" block "${blockName}"; first occurrence wins`,
1524
+ span: createTrimmedLineSpan(context, lineIndex),
1525
+ });
1526
+ continue;
1527
+ }
1528
+
1529
+ parameters[key] = { kind: 'bare', span: createTrimmedLineSpan(context, lineIndex) };
1530
+ continue;
1651
1531
  }
1532
+
1533
+ pushDiagnostic(context, {
1534
+ code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
1535
+ message: `Invalid extension block body line "${line}"; expected "key = value", "key", or "@@attribute"`,
1536
+ span: createTrimmedLineSpan(context, lineIndex),
1537
+ });
1652
1538
  }
1653
1539
 
1654
1540
  return {
1655
1541
  kind: descriptor.discriminator,
1656
1542
  name: blockName,
1657
1543
  parameters,
1544
+ blockAttributes,
1658
1545
  span: createLineRangeSpan(context, keywordLineIndex, bounds.endLine),
1659
1546
  };
1660
1547
  }
1661
1548
 
1549
+ /**
1550
+ * Parses a `@@name(args…)` block-attribute line inside an extension block
1551
+ * using the shared block-attribute tokeniser (`extractAttributeTokensWithSpans`
1552
+ * + `parseAttributeToken`). Returns undefined (and emits a diagnostic) on
1553
+ * malformed syntax.
1554
+ */
1555
+ function parseExtensionBlockAttribute(
1556
+ context: ParserContext,
1557
+ line: string,
1558
+ lineIndex: number,
1559
+ ): PslExtensionBlockAttribute | undefined {
1560
+ const rawLine = context.lines[lineIndex] ?? '';
1561
+ const startCol = firstNonWhitespaceColumn(rawLine);
1562
+
1563
+ const tokenParse = extractAttributeTokensWithSpans(context, lineIndex, line, startCol);
1564
+ if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
1565
+ pushDiagnostic(context, {
1566
+ code: 'PSL_INVALID_EXTENSION_BLOCK_ATTRIBUTE',
1567
+ message: `Invalid extension block attribute "${line}"`,
1568
+ span: createTrimmedLineSpan(context, lineIndex),
1569
+ });
1570
+ return undefined;
1571
+ }
1572
+ const token = tokenParse.tokens[0];
1573
+ if (!token) {
1574
+ return undefined;
1575
+ }
1576
+ const parsed = parseAttributeToken(context, {
1577
+ token: token.text,
1578
+ target: 'model',
1579
+ lineIndex,
1580
+ span: token.span,
1581
+ });
1582
+ if (!parsed) {
1583
+ return undefined;
1584
+ }
1585
+ const args: PslExtensionBlockAttributeArg[] = parsed.args
1586
+ .filter((a): a is PslAttributeArgument & { kind: 'positional' } => a.kind === 'positional')
1587
+ .map((a) => ({ kind: 'positional' as const, value: a.value, span: a.span }));
1588
+ return { name: parsed.name, args, span: parsed.span };
1589
+ }
1590
+
1662
1591
  /**
1663
1592
  * Captures a single parameter RHS string according to the declared parameter
1664
1593
  * kind. Returns `undefined` and pushes a diagnostic only if the syntax is
@@ -0,0 +1,89 @@
1
+ const LINE_FEED = 10;
2
+
3
+ export interface Position {
4
+ readonly line: number;
5
+ readonly character: number;
6
+ }
7
+
8
+ export interface Range {
9
+ readonly start: Position;
10
+ readonly end: Position;
11
+ }
12
+
13
+ export class SourceFile {
14
+ readonly #text: string;
15
+ readonly #lineStarts: readonly number[];
16
+
17
+ constructor(text: string) {
18
+ this.#text = text;
19
+ const lineStarts: number[] = [0];
20
+ for (let offset = 0; offset < text.length; offset++) {
21
+ if (text.charCodeAt(offset) === LINE_FEED) {
22
+ lineStarts.push(offset + 1);
23
+ }
24
+ }
25
+ this.#lineStarts = lineStarts;
26
+ }
27
+
28
+ get text(): string {
29
+ return this.#text;
30
+ }
31
+
32
+ get length(): number {
33
+ return this.#text.length;
34
+ }
35
+
36
+ get lineCount(): number {
37
+ return this.#lineStarts.length;
38
+ }
39
+
40
+ lineStartOffsets(): readonly number[] {
41
+ return this.#lineStarts;
42
+ }
43
+
44
+ positionAt(offset: number): Position {
45
+ const clamped = clamp(offset, 0, this.#text.length);
46
+ const line = this.#lineIndexAt(clamped);
47
+ return { line, character: clamped - this.#lineStartAt(line) };
48
+ }
49
+
50
+ offsetAt(position: Position): number {
51
+ const line = clamp(position.line, 0, this.#lineStarts.length - 1);
52
+ const lineStart = this.#lineStartAt(line);
53
+ const lineEnd = this.#lineEndAt(line);
54
+ return clamp(lineStart + position.character, lineStart, lineEnd);
55
+ }
56
+
57
+ #lineStartAt(line: number): number {
58
+ return this.#lineStarts[line] ?? 0;
59
+ }
60
+
61
+ #lineEndAt(line: number): number {
62
+ return line + 1 < this.#lineStarts.length ? this.#lineStartAt(line + 1) - 1 : this.#text.length;
63
+ }
64
+
65
+ #lineIndexAt(offset: number): number {
66
+ const lineStarts = this.#lineStarts;
67
+ let low = 0;
68
+ let high = lineStarts.length - 1;
69
+ while (low < high) {
70
+ const mid = (low + high + 1) >>> 1;
71
+ if ((lineStarts[mid] ?? 0) <= offset) {
72
+ low = mid;
73
+ } else {
74
+ high = mid - 1;
75
+ }
76
+ }
77
+ return low;
78
+ }
79
+ }
80
+
81
+ function clamp(value: number, min: number, max: number): number {
82
+ if (value < min) {
83
+ return min;
84
+ }
85
+ if (value > max) {
86
+ return max;
87
+ }
88
+ return value;
89
+ }
@@ -3,7 +3,7 @@ import type { AstNode } from '../ast-helpers';
3
3
  import { filterChildren, findChildToken, findFirstChild } from '../ast-helpers';
4
4
  import type { SyntaxNode } from '../red';
5
5
  import { AttributeArgAst } from './expressions';
6
- import { IdentifierAst } from './identifier';
6
+ import { QualifiedNameAst } from './qualified-name';
7
7
 
8
8
  export class AttributeArgListAst implements AstNode {
9
9
  readonly syntax: SyntaxNode;
@@ -40,27 +40,8 @@ export class FieldAttributeAst implements AstNode {
40
40
  return findChildToken(this.syntax, 'At');
41
41
  }
42
42
 
43
- name(): IdentifierAst | undefined {
44
- if (this.dot()) {
45
- let count = 0;
46
- for (const child of this.syntax.childNodes()) {
47
- if (child.kind === 'Identifier') {
48
- count++;
49
- if (count === 2) return new IdentifierAst(child);
50
- }
51
- }
52
- return undefined;
53
- }
54
- return findFirstChild(this.syntax, IdentifierAst.cast);
55
- }
56
-
57
- dot(): Token | undefined {
58
- return findChildToken(this.syntax, 'Dot');
59
- }
60
-
61
- namespaceName(): IdentifierAst | undefined {
62
- if (!this.dot()) return undefined;
63
- return findFirstChild(this.syntax, IdentifierAst.cast);
43
+ name(): QualifiedNameAst | undefined {
44
+ return findFirstChild(this.syntax, QualifiedNameAst.cast);
64
45
  }
65
46
 
66
47
  argList(): AttributeArgListAst | undefined {
@@ -83,8 +64,8 @@ export class ModelAttributeAst implements AstNode {
83
64
  return findChildToken(this.syntax, 'DoubleAt');
84
65
  }
85
66
 
86
- name(): IdentifierAst | undefined {
87
- return findFirstChild(this.syntax, IdentifierAst.cast);
67
+ name(): QualifiedNameAst | undefined {
68
+ return findFirstChild(this.syntax, QualifiedNameAst.cast);
88
69
  }
89
70
 
90
71
  argList(): AttributeArgListAst | undefined {