@prisma-next/psl-parser 0.3.0-dev.98 → 0.3.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/README.md +2 -3
- package/dist/index.d.mts +9 -3
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +14 -2
- package/dist/index.mjs.map +1 -0
- package/dist/{parser-BgdIJNqs.d.mts → parser-B_U8b1dE.d.mts} +2 -2
- package/dist/parser-B_U8b1dE.d.mts.map +1 -0
- package/dist/{parser-Br4vqzd-.mjs → parser-Cr64fw1E.mjs} +268 -45
- package/dist/parser-Cr64fw1E.mjs.map +1 -0
- package/dist/parser.d.mts +1 -1
- package/dist/parser.mjs +1 -1
- package/dist/{types-CyfspkvF.d.mts → types-CYb3hCxS.d.mts} +26 -4
- package/dist/types-CYb3hCxS.d.mts.map +1 -0
- package/dist/types.d.mts +2 -2
- package/package.json +4 -4
- package/src/attribute-helpers.ts +13 -0
- package/src/exports/index.ts +3 -0
- package/src/exports/types.ts +1 -0
- package/src/parser.ts +385 -43
- package/src/types.ts +26 -2
- package/dist/parser-BgdIJNqs.d.mts.map +0 -1
- package/dist/parser-Br4vqzd-.mjs.map +0 -1
- package/dist/types-CyfspkvF.d.mts.map +0 -1
package/src/parser.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PslAttribute,
|
|
6
6
|
PslAttributeArgument,
|
|
7
7
|
PslAttributeTarget,
|
|
8
|
+
PslCompositeType,
|
|
8
9
|
PslDiagnostic,
|
|
9
10
|
PslDiagnosticCode,
|
|
10
11
|
PslDocumentAst,
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
PslNamedTypeDeclaration,
|
|
18
19
|
PslPosition,
|
|
19
20
|
PslSpan,
|
|
21
|
+
PslTypeConstructorCall,
|
|
20
22
|
PslTypesBlock,
|
|
21
23
|
} from './types';
|
|
22
24
|
|
|
@@ -61,6 +63,7 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
|
|
|
61
63
|
|
|
62
64
|
const models: PslModel[] = [];
|
|
63
65
|
const enums: PslEnum[] = [];
|
|
66
|
+
const compositeTypes: PslCompositeType[] = [];
|
|
64
67
|
let typesBlock: PslTypesBlock | undefined;
|
|
65
68
|
|
|
66
69
|
let lineIndex = 0;
|
|
@@ -98,6 +101,19 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
|
|
|
98
101
|
continue;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
const compositeTypeMatch = line.match(/^type\s+([A-Za-z_]\w*)\s*\{$/);
|
|
105
|
+
if (compositeTypeMatch) {
|
|
106
|
+
const bounds = findBlockBounds(context, lineIndex);
|
|
107
|
+
const name = compositeTypeMatch[1] ?? '';
|
|
108
|
+
if (name.length === 0) {
|
|
109
|
+
lineIndex = bounds.endLine + 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
compositeTypes.push(parseCompositeTypeBlock(context, name, bounds));
|
|
113
|
+
lineIndex = bounds.endLine + 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
101
117
|
if (/^types\s*\{$/.test(line)) {
|
|
102
118
|
const bounds = findBlockBounds(context, lineIndex);
|
|
103
119
|
typesBlock = parseTypesBlock(context, bounds);
|
|
@@ -130,6 +146,7 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
|
|
|
130
146
|
);
|
|
131
147
|
const modelNames = new Set(models.map((model) => model.name));
|
|
132
148
|
const enumNames = new Set(enums.map((enumBlock) => enumBlock.name));
|
|
149
|
+
const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
|
|
133
150
|
for (const declaration of typesBlock?.declarations ?? []) {
|
|
134
151
|
if (SCALAR_TYPES.has(declaration.name)) {
|
|
135
152
|
pushDiagnostic(context, {
|
|
@@ -168,6 +185,7 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
|
|
|
168
185
|
hasRelationAttribute ||
|
|
169
186
|
modelNames.has(field.typeName) ||
|
|
170
187
|
enumNames.has(field.typeName) ||
|
|
188
|
+
compositeTypeNames.has(field.typeName) ||
|
|
171
189
|
SCALAR_TYPES.has(field.typeName)
|
|
172
190
|
) {
|
|
173
191
|
return field;
|
|
@@ -184,6 +202,7 @@ export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocument
|
|
|
184
202
|
sourceId: input.sourceId,
|
|
185
203
|
models: normalizedModels,
|
|
186
204
|
enums,
|
|
205
|
+
compositeTypes,
|
|
187
206
|
...ifDefined('types', typesBlock),
|
|
188
207
|
span: {
|
|
189
208
|
start: createPosition(context, 0, 0),
|
|
@@ -236,8 +255,47 @@ function parseModelBlock(context: ParserContext, name: string, bounds: BlockBoun
|
|
|
236
255
|
};
|
|
237
256
|
}
|
|
238
257
|
|
|
258
|
+
function parseCompositeTypeBlock(
|
|
259
|
+
context: ParserContext,
|
|
260
|
+
name: string,
|
|
261
|
+
bounds: BlockBounds,
|
|
262
|
+
): PslCompositeType {
|
|
263
|
+
const fields: PslField[] = [];
|
|
264
|
+
const attributes: PslAttribute[] = [];
|
|
265
|
+
|
|
266
|
+
for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
|
|
267
|
+
const raw = context.lines[lineIndex] ?? '';
|
|
268
|
+
const line = stripInlineComment(raw).trim();
|
|
269
|
+
if (line.length === 0) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (line.startsWith('@@')) {
|
|
274
|
+
const attribute = parseModelAttribute(context, line, lineIndex);
|
|
275
|
+
if (attribute) {
|
|
276
|
+
attributes.push(attribute);
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const field = parseField(context, line, lineIndex);
|
|
282
|
+
if (field) {
|
|
283
|
+
fields.push(field);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
kind: 'compositeType',
|
|
289
|
+
name,
|
|
290
|
+
fields,
|
|
291
|
+
attributes,
|
|
292
|
+
span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
239
296
|
function parseEnumBlock(context: ParserContext, name: string, bounds: BlockBounds): PslEnum {
|
|
240
297
|
const values: PslEnumValue[] = [];
|
|
298
|
+
const attributes: PslAttribute[] = [];
|
|
241
299
|
|
|
242
300
|
for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
|
|
243
301
|
const raw = context.lines[lineIndex] ?? '';
|
|
@@ -246,6 +304,14 @@ function parseEnumBlock(context: ParserContext, name: string, bounds: BlockBound
|
|
|
246
304
|
continue;
|
|
247
305
|
}
|
|
248
306
|
|
|
307
|
+
if (line.startsWith('@@')) {
|
|
308
|
+
const attribute = parseEnumAttribute(context, line, lineIndex);
|
|
309
|
+
if (attribute) {
|
|
310
|
+
attributes.push(attribute);
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
249
315
|
const valueMatch = line.match(/^([A-Za-z_]\w*)$/);
|
|
250
316
|
if (!valueMatch) {
|
|
251
317
|
pushDiagnostic(context, {
|
|
@@ -267,6 +333,7 @@ function parseEnumBlock(context: ParserContext, name: string, bounds: BlockBound
|
|
|
267
333
|
kind: 'enum',
|
|
268
334
|
name,
|
|
269
335
|
values,
|
|
336
|
+
attributes,
|
|
270
337
|
span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
|
|
271
338
|
};
|
|
272
339
|
}
|
|
@@ -282,7 +349,7 @@ function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesB
|
|
|
282
349
|
continue;
|
|
283
350
|
}
|
|
284
351
|
|
|
285
|
-
const declarationMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(
|
|
352
|
+
const declarationMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
|
|
286
353
|
if (!declarationMatch) {
|
|
287
354
|
pushDiagnostic(context, {
|
|
288
355
|
code: 'PSL_INVALID_TYPES_MEMBER',
|
|
@@ -293,17 +360,33 @@ function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesB
|
|
|
293
360
|
}
|
|
294
361
|
|
|
295
362
|
const declarationName = declarationMatch[1] ?? '';
|
|
296
|
-
const baseType = declarationMatch[2] ?? '';
|
|
297
|
-
const attributePart = declarationMatch[3] ?? '';
|
|
298
363
|
const trimmedStartColumn = firstNonWhitespaceColumn(raw);
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
364
|
+
const declarationValue = (declarationMatch[2] ?? '').trim();
|
|
365
|
+
const valueOffset = line.indexOf(declarationValue);
|
|
366
|
+
const declarationValueColumn = trimmedStartColumn + Math.max(valueOffset, 0);
|
|
367
|
+
|
|
368
|
+
const typeAndAttributeSplit = splitTypeAndAttributes(declarationValue);
|
|
369
|
+
const typeSource = typeAndAttributeSplit.typeSource.trim();
|
|
370
|
+
const attributeSource = typeAndAttributeSplit.attributeSource.trimStart();
|
|
371
|
+
const leadingAttributeWhitespace =
|
|
372
|
+
typeAndAttributeSplit.attributeSource.length - attributeSource.length;
|
|
373
|
+
|
|
374
|
+
const typeConstructor = parseTypeConstructorCall(context, {
|
|
375
|
+
declarationValue: typeSource,
|
|
376
|
+
lineIndex,
|
|
377
|
+
startColumn: declarationValueColumn,
|
|
378
|
+
invalidCode: 'PSL_INVALID_TYPES_MEMBER',
|
|
379
|
+
invalidMessage: (value) => `Invalid types declaration "${value}"`,
|
|
380
|
+
});
|
|
381
|
+
if (typeConstructor === 'malformed') {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
302
385
|
const attributeParse = extractAttributeTokensWithSpans(
|
|
303
386
|
context,
|
|
304
387
|
lineIndex,
|
|
305
388
|
attributeSource,
|
|
306
|
-
|
|
389
|
+
declarationValueColumn + typeAndAttributeSplit.attributeOffset + leadingAttributeWhitespace,
|
|
307
390
|
);
|
|
308
391
|
if (!attributeParse.ok) {
|
|
309
392
|
continue;
|
|
@@ -319,6 +402,29 @@ function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesB
|
|
|
319
402
|
)
|
|
320
403
|
.filter((attribute): attribute is PslAttribute => Boolean(attribute));
|
|
321
404
|
|
|
405
|
+
if (typeConstructor) {
|
|
406
|
+
declarations.push({
|
|
407
|
+
kind: 'namedType',
|
|
408
|
+
name: declarationName,
|
|
409
|
+
typeConstructor,
|
|
410
|
+
attributes,
|
|
411
|
+
span: createTrimmedLineSpan(context, lineIndex),
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const baseTypeMatch = typeSource.match(/^([A-Za-z_]\w*)$/);
|
|
417
|
+
if (!baseTypeMatch) {
|
|
418
|
+
pushDiagnostic(context, {
|
|
419
|
+
code: 'PSL_INVALID_TYPES_MEMBER',
|
|
420
|
+
message: `Invalid types declaration "${line}"`,
|
|
421
|
+
span: createTrimmedLineSpan(context, lineIndex),
|
|
422
|
+
});
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const baseType = baseTypeMatch[1] ?? '';
|
|
427
|
+
|
|
322
428
|
declarations.push({
|
|
323
429
|
kind: 'namedType',
|
|
324
430
|
name: declarationName,
|
|
@@ -335,6 +441,78 @@ function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesB
|
|
|
335
441
|
};
|
|
336
442
|
}
|
|
337
443
|
|
|
444
|
+
function parseTypeConstructorCall(
|
|
445
|
+
context: ParserContext,
|
|
446
|
+
input: {
|
|
447
|
+
readonly declarationValue: string;
|
|
448
|
+
readonly lineIndex: number;
|
|
449
|
+
readonly startColumn: number;
|
|
450
|
+
readonly invalidCode: PslDiagnosticCode;
|
|
451
|
+
readonly invalidMessage: (value: string) => string;
|
|
452
|
+
},
|
|
453
|
+
): PslTypeConstructorCall | 'malformed' | undefined {
|
|
454
|
+
const value = input.declarationValue.trim();
|
|
455
|
+
const constructorMatch = value.match(
|
|
456
|
+
/^([A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*)\s*\(/,
|
|
457
|
+
);
|
|
458
|
+
if (!constructorMatch) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// constructorMatch already required `(`; openParen is guaranteed ≥ 0.
|
|
463
|
+
const openParen = value.indexOf('(');
|
|
464
|
+
const closeParen = value.lastIndexOf(')');
|
|
465
|
+
|
|
466
|
+
if (closeParen !== value.length - 1) {
|
|
467
|
+
pushDiagnostic(context, {
|
|
468
|
+
code: input.invalidCode,
|
|
469
|
+
message: input.invalidMessage(value),
|
|
470
|
+
span: createInlineSpan(
|
|
471
|
+
context,
|
|
472
|
+
input.lineIndex,
|
|
473
|
+
input.startColumn,
|
|
474
|
+
input.startColumn + value.length,
|
|
475
|
+
),
|
|
476
|
+
});
|
|
477
|
+
return 'malformed';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const constructorPath = constructorMatch[1] ?? '';
|
|
481
|
+
|
|
482
|
+
const argsRaw = value.slice(openParen + 1, closeParen);
|
|
483
|
+
const args = parseArgumentList(context, {
|
|
484
|
+
argsRaw,
|
|
485
|
+
argsOffset: input.startColumn + openParen + 1,
|
|
486
|
+
lineIndex: input.lineIndex,
|
|
487
|
+
token: value,
|
|
488
|
+
span: createInlineSpan(
|
|
489
|
+
context,
|
|
490
|
+
input.lineIndex,
|
|
491
|
+
input.startColumn,
|
|
492
|
+
input.startColumn + value.length,
|
|
493
|
+
),
|
|
494
|
+
invalidCode: input.invalidCode,
|
|
495
|
+
invalidEmptyArgumentMessage: `Invalid empty argument in type constructor "${value}"`,
|
|
496
|
+
invalidNamedArgumentMessage: (part) =>
|
|
497
|
+
`Invalid named argument syntax "${part}" in type constructor "${value}"`,
|
|
498
|
+
});
|
|
499
|
+
if (!args) {
|
|
500
|
+
return 'malformed';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
kind: 'typeConstructor',
|
|
505
|
+
path: constructorPath.split('.'),
|
|
506
|
+
args,
|
|
507
|
+
span: createInlineSpan(
|
|
508
|
+
context,
|
|
509
|
+
input.lineIndex,
|
|
510
|
+
input.startColumn,
|
|
511
|
+
input.startColumn + value.length,
|
|
512
|
+
),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
338
516
|
function parseModelAttribute(
|
|
339
517
|
context: ParserContext,
|
|
340
518
|
line: string,
|
|
@@ -367,8 +545,52 @@ function parseModelAttribute(
|
|
|
367
545
|
});
|
|
368
546
|
}
|
|
369
547
|
|
|
548
|
+
function parseEnumAttribute(
|
|
549
|
+
context: ParserContext,
|
|
550
|
+
line: string,
|
|
551
|
+
lineIndex: number,
|
|
552
|
+
): PslAttribute | undefined {
|
|
553
|
+
const rawLine = context.lines[lineIndex] ?? '';
|
|
554
|
+
const tokenParse = extractAttributeTokensWithSpans(
|
|
555
|
+
context,
|
|
556
|
+
lineIndex,
|
|
557
|
+
line,
|
|
558
|
+
firstNonWhitespaceColumn(rawLine),
|
|
559
|
+
);
|
|
560
|
+
if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
|
|
561
|
+
pushDiagnostic(context, {
|
|
562
|
+
code: 'PSL_INVALID_ENUM_MEMBER',
|
|
563
|
+
message: `Invalid enum value declaration "${line}"`,
|
|
564
|
+
span: createTrimmedLineSpan(context, lineIndex),
|
|
565
|
+
});
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
const token = tokenParse.tokens[0];
|
|
569
|
+
if (!token) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const parsed = parseAttributeToken(context, {
|
|
573
|
+
token: token.text,
|
|
574
|
+
target: 'enum',
|
|
575
|
+
lineIndex,
|
|
576
|
+
span: token.span,
|
|
577
|
+
});
|
|
578
|
+
if (!parsed) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
if (parsed.name !== 'map') {
|
|
582
|
+
pushDiagnostic(context, {
|
|
583
|
+
code: 'PSL_INVALID_ENUM_MEMBER',
|
|
584
|
+
message: `Invalid enum value declaration "${line}"`,
|
|
585
|
+
span: createTrimmedLineSpan(context, lineIndex),
|
|
586
|
+
});
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
return parsed;
|
|
590
|
+
}
|
|
591
|
+
|
|
370
592
|
function parseField(context: ParserContext, line: string, lineIndex: number): PslField | undefined {
|
|
371
|
-
const fieldMatch = line.match(/^([A-Za-z_]\w*)\s+
|
|
593
|
+
const fieldMatch = line.match(/^([A-Za-z_]\w*)(\s+)(.+)$/);
|
|
372
594
|
if (!fieldMatch) {
|
|
373
595
|
pushDiagnostic(context, {
|
|
374
596
|
code: 'PSL_INVALID_MODEL_MEMBER',
|
|
@@ -379,30 +601,62 @@ function parseField(context: ParserContext, line: string, lineIndex: number): Ps
|
|
|
379
601
|
}
|
|
380
602
|
|
|
381
603
|
const fieldName = fieldMatch[1] ?? '';
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const optional =
|
|
388
|
-
|
|
389
|
-
const
|
|
604
|
+
const separator = fieldMatch[2] ?? '';
|
|
605
|
+
const remainder = fieldMatch[3] ?? '';
|
|
606
|
+
const typeAndAttributeSplit = splitTypeAndAttributes(remainder);
|
|
607
|
+
const rawTypeSource = typeAndAttributeSplit.typeSource.trim();
|
|
608
|
+
const attributePart = typeAndAttributeSplit.attributeSource;
|
|
609
|
+
const optional = rawTypeSource.endsWith('?');
|
|
610
|
+
const typeSourceWithoutOptional = optional ? rawTypeSource.slice(0, -1).trimEnd() : rawTypeSource;
|
|
611
|
+
const list = typeSourceWithoutOptional.endsWith('[]');
|
|
612
|
+
const baseTypeSource = list
|
|
613
|
+
? typeSourceWithoutOptional.slice(0, -2).trimEnd()
|
|
614
|
+
: typeSourceWithoutOptional;
|
|
390
615
|
const rawLine = context.lines[lineIndex] ?? '';
|
|
391
616
|
const trimmedStartColumn = firstNonWhitespaceColumn(rawLine);
|
|
392
|
-
const
|
|
617
|
+
const typeStartColumn = trimmedStartColumn + fieldName.length + separator.length;
|
|
618
|
+
|
|
619
|
+
const typeConstructor = parseTypeConstructorCall(context, {
|
|
620
|
+
declarationValue: baseTypeSource,
|
|
621
|
+
lineIndex,
|
|
622
|
+
startColumn: typeStartColumn,
|
|
623
|
+
invalidCode: 'PSL_INVALID_MODEL_MEMBER',
|
|
624
|
+
invalidMessage: (value) => `Invalid field type constructor "${value}"`,
|
|
625
|
+
});
|
|
626
|
+
if (typeConstructor === 'malformed') {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const simpleTypeMatch = baseTypeSource.match(/^([A-Za-z_]\w*)$/);
|
|
631
|
+
const typeName = typeConstructor?.path.join('.') ?? simpleTypeMatch?.[1];
|
|
632
|
+
if (!typeName) {
|
|
633
|
+
pushDiagnostic(context, {
|
|
634
|
+
code: 'PSL_INVALID_MODEL_MEMBER',
|
|
635
|
+
message: `Invalid model member declaration "${line}"`,
|
|
636
|
+
span: createTrimmedLineSpan(context, lineIndex),
|
|
637
|
+
});
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const attributes: PslFieldAttribute[] = [];
|
|
393
642
|
const attributeSource = attributePart.trimStart();
|
|
394
643
|
const leadingAttributeWhitespace = attributePart.length - attributeSource.length;
|
|
395
644
|
const tokenParse = extractAttributeTokensWithSpans(
|
|
396
645
|
context,
|
|
397
646
|
lineIndex,
|
|
398
647
|
attributeSource,
|
|
399
|
-
trimmedStartColumn +
|
|
648
|
+
trimmedStartColumn +
|
|
649
|
+
fieldName.length +
|
|
650
|
+
separator.length +
|
|
651
|
+
typeAndAttributeSplit.attributeOffset +
|
|
652
|
+
leadingAttributeWhitespace,
|
|
400
653
|
);
|
|
401
654
|
if (!tokenParse.ok) {
|
|
402
655
|
return {
|
|
403
656
|
kind: 'field',
|
|
404
657
|
name: fieldName,
|
|
405
658
|
typeName,
|
|
659
|
+
...ifDefined('typeConstructor', typeConstructor),
|
|
406
660
|
optional,
|
|
407
661
|
list,
|
|
408
662
|
attributes,
|
|
@@ -426,6 +680,7 @@ function parseField(context: ParserContext, line: string, lineIndex: number): Ps
|
|
|
426
680
|
kind: 'field',
|
|
427
681
|
name: fieldName,
|
|
428
682
|
typeName,
|
|
683
|
+
...ifDefined('typeConstructor', typeConstructor),
|
|
429
684
|
optional,
|
|
430
685
|
list,
|
|
431
686
|
attributes,
|
|
@@ -433,6 +688,80 @@ function parseField(context: ParserContext, line: string, lineIndex: number): Ps
|
|
|
433
688
|
};
|
|
434
689
|
}
|
|
435
690
|
|
|
691
|
+
function isQuoteEscaped(value: string, quoteIndex: number): boolean {
|
|
692
|
+
let backslashCount = 0;
|
|
693
|
+
|
|
694
|
+
for (let index = quoteIndex - 1; index >= 0 && value[index] === '\\'; index -= 1) {
|
|
695
|
+
backslashCount += 1;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return backslashCount % 2 === 1;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function splitTypeAndAttributes(value: string): {
|
|
702
|
+
readonly typeSource: string;
|
|
703
|
+
readonly attributeSource: string;
|
|
704
|
+
readonly attributeOffset: number;
|
|
705
|
+
} {
|
|
706
|
+
let depthParen = 0;
|
|
707
|
+
let depthBracket = 0;
|
|
708
|
+
let depthBrace = 0;
|
|
709
|
+
let quote: '"' | "'" | null = null;
|
|
710
|
+
|
|
711
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
712
|
+
const character = value[index] ?? '';
|
|
713
|
+
if (quote) {
|
|
714
|
+
if (character === quote && !isQuoteEscaped(value, index)) {
|
|
715
|
+
quote = null;
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (character === '"' || character === "'") {
|
|
721
|
+
quote = character;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (character === '(') {
|
|
725
|
+
depthParen += 1;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (character === ')') {
|
|
729
|
+
depthParen = Math.max(0, depthParen - 1);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (character === '[') {
|
|
733
|
+
depthBracket += 1;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (character === ']') {
|
|
737
|
+
depthBracket = Math.max(0, depthBracket - 1);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (character === '{') {
|
|
741
|
+
depthBrace += 1;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (character === '}') {
|
|
745
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (character === '@' && depthParen === 0 && depthBracket === 0 && depthBrace === 0) {
|
|
750
|
+
return {
|
|
751
|
+
typeSource: value.slice(0, index).trimEnd(),
|
|
752
|
+
attributeSource: value.slice(index),
|
|
753
|
+
attributeOffset: index,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
typeSource: value.trimEnd(),
|
|
760
|
+
attributeSource: '',
|
|
761
|
+
attributeOffset: value.length,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
436
765
|
function parseAttributeToken(
|
|
437
766
|
context: ParserContext,
|
|
438
767
|
input: {
|
|
@@ -442,16 +771,17 @@ function parseAttributeToken(
|
|
|
442
771
|
readonly span: PslSpan;
|
|
443
772
|
},
|
|
444
773
|
): PslAttribute | undefined {
|
|
445
|
-
const
|
|
446
|
-
|
|
774
|
+
const expectsBlockPrefix = input.target === 'model' || input.target === 'enum';
|
|
775
|
+
const targetLabel = input.target === 'enum' ? 'Enum' : 'Model';
|
|
776
|
+
if (expectsBlockPrefix && !input.token.startsWith('@@')) {
|
|
447
777
|
pushDiagnostic(context, {
|
|
448
778
|
code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
|
|
449
|
-
message:
|
|
779
|
+
message: `${targetLabel} attribute "${input.token}" must use @@ prefix`,
|
|
450
780
|
span: input.span,
|
|
451
781
|
});
|
|
452
782
|
return undefined;
|
|
453
783
|
}
|
|
454
|
-
if (!
|
|
784
|
+
if (!expectsBlockPrefix && !input.token.startsWith('@')) {
|
|
455
785
|
pushDiagnostic(context, {
|
|
456
786
|
code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
|
|
457
787
|
message: `Attribute "${input.token}" must use @ prefix`,
|
|
@@ -459,7 +789,7 @@ function parseAttributeToken(
|
|
|
459
789
|
});
|
|
460
790
|
return undefined;
|
|
461
791
|
}
|
|
462
|
-
if (!
|
|
792
|
+
if (!expectsBlockPrefix && input.token.startsWith('@@')) {
|
|
463
793
|
pushDiagnostic(context, {
|
|
464
794
|
code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
|
|
465
795
|
message: `Attribute "${input.token}" is not valid in ${input.target} context`,
|
|
@@ -468,7 +798,7 @@ function parseAttributeToken(
|
|
|
468
798
|
return undefined;
|
|
469
799
|
}
|
|
470
800
|
|
|
471
|
-
const rawBody =
|
|
801
|
+
const rawBody = expectsBlockPrefix ? input.token.slice(2) : input.token.slice(1);
|
|
472
802
|
const openParen = rawBody.indexOf('(');
|
|
473
803
|
const closeParen = rawBody.lastIndexOf(')');
|
|
474
804
|
const hasArgs = openParen >= 0 || closeParen >= 0;
|
|
@@ -502,12 +832,15 @@ function parseAttributeToken(
|
|
|
502
832
|
return undefined;
|
|
503
833
|
}
|
|
504
834
|
const argsRaw = rawBody.slice(openParen + 1, closeParen);
|
|
505
|
-
const parsedArgs =
|
|
835
|
+
const parsedArgs = parseArgumentList(context, {
|
|
506
836
|
argsRaw,
|
|
507
|
-
argsOffset: input.span.start.column - 1 + (
|
|
837
|
+
argsOffset: input.span.start.column - 1 + (expectsBlockPrefix ? 2 : 1) + openParen + 1,
|
|
508
838
|
lineIndex: input.lineIndex,
|
|
509
839
|
token: input.token,
|
|
510
840
|
span: input.span,
|
|
841
|
+
invalidCode: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
|
|
842
|
+
invalidEmptyArgumentMessage: `Invalid empty argument in attribute "${input.token}"`,
|
|
843
|
+
invalidNamedArgumentMessage: (part) => `Invalid named argument syntax "${part}"`,
|
|
511
844
|
});
|
|
512
845
|
if (!parsedArgs) {
|
|
513
846
|
return undefined;
|
|
@@ -524,7 +857,7 @@ function parseAttributeToken(
|
|
|
524
857
|
};
|
|
525
858
|
}
|
|
526
859
|
|
|
527
|
-
function
|
|
860
|
+
function parseArgumentList(
|
|
528
861
|
context: ParserContext,
|
|
529
862
|
input: {
|
|
530
863
|
readonly argsRaw: string;
|
|
@@ -532,6 +865,9 @@ function parseAttributeArguments(
|
|
|
532
865
|
readonly lineIndex: number;
|
|
533
866
|
readonly token: string;
|
|
534
867
|
readonly span: PslSpan;
|
|
868
|
+
readonly invalidCode: PslDiagnosticCode;
|
|
869
|
+
readonly invalidEmptyArgumentMessage: string;
|
|
870
|
+
readonly invalidNamedArgumentMessage: (part: string) => string;
|
|
535
871
|
},
|
|
536
872
|
): readonly PslAttributeArgument[] | undefined {
|
|
537
873
|
const trimmed = input.argsRaw.trim();
|
|
@@ -547,8 +883,8 @@ function parseAttributeArguments(
|
|
|
547
883
|
const trimmedPart = original.trim();
|
|
548
884
|
if (trimmedPart.length === 0) {
|
|
549
885
|
pushDiagnostic(context, {
|
|
550
|
-
code:
|
|
551
|
-
message:
|
|
886
|
+
code: input.invalidCode,
|
|
887
|
+
message: input.invalidEmptyArgumentMessage,
|
|
552
888
|
span: input.span,
|
|
553
889
|
});
|
|
554
890
|
return undefined;
|
|
@@ -564,8 +900,8 @@ function parseAttributeArguments(
|
|
|
564
900
|
const first = namedSplit[0];
|
|
565
901
|
if (!first) {
|
|
566
902
|
pushDiagnostic(context, {
|
|
567
|
-
code:
|
|
568
|
-
message:
|
|
903
|
+
code: input.invalidCode,
|
|
904
|
+
message: input.invalidNamedArgumentMessage(trimmedPart),
|
|
569
905
|
span: partSpan,
|
|
570
906
|
});
|
|
571
907
|
return undefined;
|
|
@@ -574,8 +910,8 @@ function parseAttributeArguments(
|
|
|
574
910
|
const rawValue = trimmedPart.slice(first.end + 1).trim();
|
|
575
911
|
if (!name || rawValue.length === 0) {
|
|
576
912
|
pushDiagnostic(context, {
|
|
577
|
-
code:
|
|
578
|
-
message:
|
|
913
|
+
code: input.invalidCode,
|
|
914
|
+
message: input.invalidNamedArgumentMessage(trimmedPart),
|
|
579
915
|
span: partSpan,
|
|
580
916
|
});
|
|
581
917
|
return undefined;
|
|
@@ -609,19 +945,17 @@ function findBlockBounds(context: ParserContext, startLine: number): BlockBounds
|
|
|
609
945
|
for (let lineIndex = startLine; lineIndex < context.lines.length; lineIndex += 1) {
|
|
610
946
|
const line = stripInlineComment(context.lines[lineIndex] ?? '');
|
|
611
947
|
let quote: '"' | "'" | null = null;
|
|
612
|
-
let
|
|
613
|
-
|
|
948
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
949
|
+
const character = line[index] ?? '';
|
|
614
950
|
if (quote) {
|
|
615
|
-
if (character === quote &&
|
|
951
|
+
if (character === quote && !isQuoteEscaped(line, index)) {
|
|
616
952
|
quote = null;
|
|
617
953
|
}
|
|
618
|
-
previousCharacter = character;
|
|
619
954
|
continue;
|
|
620
955
|
}
|
|
621
956
|
|
|
622
957
|
if (character === '"' || character === "'") {
|
|
623
958
|
quote = character;
|
|
624
|
-
previousCharacter = character;
|
|
625
959
|
continue;
|
|
626
960
|
}
|
|
627
961
|
|
|
@@ -634,7 +968,6 @@ function findBlockBounds(context: ParserContext, startLine: number): BlockBounds
|
|
|
634
968
|
return { startLine, endLine: lineIndex, closed: true };
|
|
635
969
|
}
|
|
636
970
|
}
|
|
637
|
-
previousCharacter = character;
|
|
638
971
|
}
|
|
639
972
|
}
|
|
640
973
|
|
|
@@ -660,13 +993,14 @@ function splitTopLevelSegments(value: string, separator: ',' | ':'): TopLevelSeg
|
|
|
660
993
|
const parts: TopLevelSegment[] = [];
|
|
661
994
|
let depthParen = 0;
|
|
662
995
|
let depthBracket = 0;
|
|
996
|
+
let depthBrace = 0;
|
|
663
997
|
let quote: '"' | "'" | null = null;
|
|
664
998
|
let start = 0;
|
|
665
999
|
|
|
666
1000
|
for (let index = 0; index < value.length; index += 1) {
|
|
667
1001
|
const character = value[index] ?? '';
|
|
668
1002
|
if (quote) {
|
|
669
|
-
if (character === quote && value
|
|
1003
|
+
if (character === quote && !isQuoteEscaped(value, index)) {
|
|
670
1004
|
quote = null;
|
|
671
1005
|
}
|
|
672
1006
|
continue;
|
|
@@ -693,8 +1027,16 @@ function splitTopLevelSegments(value: string, separator: ',' | ':'): TopLevelSeg
|
|
|
693
1027
|
depthBracket = Math.max(0, depthBracket - 1);
|
|
694
1028
|
continue;
|
|
695
1029
|
}
|
|
1030
|
+
if (character === '{') {
|
|
1031
|
+
depthBrace += 1;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (character === '}') {
|
|
1035
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
696
1038
|
|
|
697
|
-
if (character === separator && depthParen === 0 && depthBracket === 0) {
|
|
1039
|
+
if (character === separator && depthParen === 0 && depthBracket === 0 && depthBrace === 0) {
|
|
698
1040
|
parts.push({
|
|
699
1041
|
value: value.slice(start, index),
|
|
700
1042
|
start,
|
|
@@ -763,7 +1105,7 @@ function extractAttributeTokensWithSpans(
|
|
|
763
1105
|
while (index < value.length) {
|
|
764
1106
|
const char = value[index] ?? '';
|
|
765
1107
|
if (quote) {
|
|
766
|
-
if (char === quote && value
|
|
1108
|
+
if (char === quote && !isQuoteEscaped(value, index)) {
|
|
767
1109
|
quote = null;
|
|
768
1110
|
}
|
|
769
1111
|
index += 1;
|
|
@@ -836,7 +1178,7 @@ function stripInlineComment(line: string): string {
|
|
|
836
1178
|
const next = line[index + 1] ?? '';
|
|
837
1179
|
|
|
838
1180
|
if (quote) {
|
|
839
|
-
if (current === quote && line
|
|
1181
|
+
if (current === quote && !isQuoteEscaped(line, index)) {
|
|
840
1182
|
quote = null;
|
|
841
1183
|
}
|
|
842
1184
|
continue;
|