@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/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{parser-Cw_zV0M5.mjs → parser-CaplKvRs.mjs} +110 -141
- package/dist/parser-CaplKvRs.mjs.map +1 -0
- package/dist/parser.mjs +1 -1
- package/dist/syntax.d.mts +115 -40
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +815 -105
- package/dist/syntax.mjs.map +1 -1
- package/dist/tokenizer-1hAHZzmp.mjs +228 -0
- package/dist/tokenizer-1hAHZzmp.mjs.map +1 -0
- package/dist/tokenizer.mjs +1 -190
- package/package.json +5 -5
- package/src/exports/index.ts +13 -2
- package/src/exports/syntax.ts +9 -4
- package/src/parse.ts +742 -0
- package/src/parser.ts +125 -196
- package/src/source-file.ts +89 -0
- package/src/syntax/ast/attributes.ts +5 -24
- package/src/syntax/ast/declarations.ts +14 -67
- package/src/syntax/ast/expressions.ts +187 -19
- package/src/syntax/ast/identifier.ts +4 -0
- package/src/syntax/ast/qualified-name.ts +87 -0
- package/src/syntax/ast/type-annotation.ts +11 -41
- package/src/syntax/ast-helpers.ts +12 -0
- package/src/syntax/syntax-kind.ts +8 -4
- package/src/tokenizer.ts +47 -7
- package/dist/parser-Cw_zV0M5.mjs.map +0 -1
- package/dist/tokenizer.mjs.map +0 -1
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/
|
|
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
|
|
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
|
-
* - `
|
|
1599
|
-
* - `
|
|
1600
|
-
* - `
|
|
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
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
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
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
|
1649
|
-
if (
|
|
1650
|
-
|
|
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 {
|
|
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():
|
|
44
|
-
|
|
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():
|
|
87
|
-
return findFirstChild(this.syntax,
|
|
67
|
+
name(): QualifiedNameAst | undefined {
|
|
68
|
+
return findFirstChild(this.syntax, QualifiedNameAst.cast);
|
|
88
69
|
}
|
|
89
70
|
|
|
90
71
|
argList(): AttributeArgListAst | undefined {
|