@prisma-next/psl-parser 0.13.0 → 0.14.0-dev.10

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.
Files changed (54) hide show
  1. package/README.md +28 -10
  2. package/dist/declarations-D9h_ihD3.mjs +820 -0
  3. package/dist/declarations-D9h_ihD3.mjs.map +1 -0
  4. package/dist/format.d.mts +19 -0
  5. package/dist/format.d.mts.map +1 -0
  6. package/dist/format.mjs +470 -0
  7. package/dist/format.mjs.map +1 -0
  8. package/dist/index.d.mts +136 -3
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +472 -2
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/parse-BjZ1LPe6.d.mts +349 -0
  13. package/dist/parse-BjZ1LPe6.d.mts.map +1 -0
  14. package/dist/parse-DhEV6av6.mjs +605 -0
  15. package/dist/parse-DhEV6av6.mjs.map +1 -0
  16. package/dist/syntax.d.mts +3 -272
  17. package/dist/syntax.d.mts.map +1 -1
  18. package/dist/syntax.mjs +3 -708
  19. package/dist/tokenizer-1hAHZzmp.mjs +228 -0
  20. package/dist/tokenizer-1hAHZzmp.mjs.map +1 -0
  21. package/dist/tokenizer.mjs +1 -190
  22. package/package.json +6 -6
  23. package/src/block-reconstruction.ts +139 -0
  24. package/src/exports/format.ts +3 -0
  25. package/src/exports/index.ts +40 -5
  26. package/src/exports/syntax.ts +9 -4
  27. package/src/extension-block.ts +107 -0
  28. package/src/format/emit.ts +603 -0
  29. package/src/format/error.ts +13 -0
  30. package/src/format/format.ts +13 -0
  31. package/src/format/options.ts +28 -0
  32. package/src/parse.ts +751 -0
  33. package/src/resolve.ts +120 -0
  34. package/src/source-file.ts +89 -0
  35. package/src/symbol-table.ts +446 -0
  36. package/src/syntax/ast/attributes.ts +5 -24
  37. package/src/syntax/ast/declarations.ts +14 -67
  38. package/src/syntax/ast/expressions.ts +187 -19
  39. package/src/syntax/ast/identifier.ts +4 -0
  40. package/src/syntax/ast/qualified-name.ts +87 -0
  41. package/src/syntax/ast/type-annotation.ts +11 -41
  42. package/src/syntax/ast-helpers.ts +12 -0
  43. package/src/syntax/syntax-kind.ts +8 -4
  44. package/src/tokenizer.ts +47 -7
  45. package/dist/parser-Cw_zV0M5.mjs +0 -1176
  46. package/dist/parser-Cw_zV0M5.mjs.map +0 -1
  47. package/dist/parser-Dfi3Wfdq.d.mts +0 -7
  48. package/dist/parser-Dfi3Wfdq.d.mts.map +0 -1
  49. package/dist/parser.d.mts +0 -2
  50. package/dist/parser.mjs +0 -2
  51. package/dist/syntax.mjs.map +0 -1
  52. package/dist/tokenizer.mjs.map +0 -1
  53. package/src/exports/parser.ts +0 -1
  54. package/src/parser.ts +0 -1713
package/src/parser.ts DELETED
@@ -1,1713 +0,0 @@
1
- import type {
2
- AuthoringPslBlockDescriptor,
3
- AuthoringPslBlockDescriptorNamespace,
4
- } from '@prisma-next/framework-components/authoring';
5
- import { isAuthoringPslBlockDescriptor } from '@prisma-next/framework-components/authoring';
6
- import { emptyCodecLookup } from '@prisma-next/framework-components/codec';
7
- import type {
8
- ParsePslDocumentInput,
9
- ParsePslDocumentResult,
10
- PslAttribute,
11
- PslAttributeArgument,
12
- PslAttributeTarget,
13
- PslCompositeType,
14
- PslDiagnostic,
15
- PslDiagnosticCode,
16
- PslDocumentAst,
17
- PslEnum,
18
- PslEnumValue,
19
- PslExtensionBlock,
20
- PslExtensionBlockParamValue,
21
- PslField,
22
- PslFieldAttribute,
23
- PslModel,
24
- PslModelAttribute,
25
- PslNamedTypeDeclaration,
26
- PslNamespace,
27
- PslPosition,
28
- PslSpan,
29
- PslTypeConstructorCall,
30
- PslTypesBlock,
31
- } from '@prisma-next/framework-components/psl-ast';
32
- import {
33
- makePslNamespace,
34
- makePslNamespaceEntries,
35
- namespacePslExtensionBlocks,
36
- UNSPECIFIED_PSL_NAMESPACE_ID,
37
- validateExtensionBlock,
38
- } from '@prisma-next/framework-components/psl-ast';
39
- import { ifDefined } from '@prisma-next/utils/defined';
40
-
41
- const SCALAR_TYPES = new Set([
42
- 'String',
43
- 'Boolean',
44
- 'Int',
45
- 'BigInt',
46
- 'Float',
47
- 'Decimal',
48
- 'DateTime',
49
- 'Json',
50
- 'Bytes',
51
- ]);
52
-
53
- interface BlockBounds {
54
- readonly startLine: number;
55
- readonly endLine: number;
56
- readonly closed: boolean;
57
- }
58
-
59
- interface ParserContext {
60
- readonly schema: string;
61
- readonly sourceId: string;
62
- readonly lines: readonly string[];
63
- readonly lineOffsets: readonly number[];
64
- readonly diagnostics: PslDiagnostic[];
65
- }
66
-
67
- export function parsePslDocument(input: ParsePslDocumentInput): ParsePslDocumentResult {
68
- const normalizedSchema = input.schema.replaceAll('\r\n', '\n');
69
- const lines = normalizedSchema.split('\n');
70
- const lineOffsets = computeLineOffsets(normalizedSchema);
71
- const diagnostics: PslDiagnostic[] = [];
72
- const context: ParserContext = {
73
- schema: normalizedSchema,
74
- sourceId: input.sourceId,
75
- lines,
76
- lineOffsets,
77
- diagnostics,
78
- };
79
-
80
- interface NamespaceAccumulator {
81
- name: string;
82
- models: PslModel[];
83
- enums: PslEnum[];
84
- compositeTypes: PslCompositeType[];
85
- extensionBlocks: PslExtensionBlock[];
86
- span: PslSpan | undefined;
87
- }
88
-
89
- const namespaceOrder: string[] = [];
90
- const namespacesByName = new Map<string, NamespaceAccumulator>();
91
- const getOrCreateNamespace = (
92
- name: string,
93
- spanIfNew: PslSpan | undefined,
94
- ): NamespaceAccumulator => {
95
- let acc = namespacesByName.get(name);
96
- if (!acc) {
97
- acc = {
98
- name,
99
- models: [],
100
- enums: [],
101
- compositeTypes: [],
102
- extensionBlocks: [],
103
- span: spanIfNew,
104
- };
105
- namespacesByName.set(name, acc);
106
- namespaceOrder.push(name);
107
- }
108
- return acc;
109
- };
110
-
111
- let typesBlock: PslTypesBlock | undefined;
112
- const pslBlockNamespace: AuthoringPslBlockDescriptorNamespace = input.pslBlockDescriptors ?? {};
113
-
114
- // Walk a contiguous range of lines, routing top-level declarations into the
115
- // active namespace bucket. Called once for the whole document and once per
116
- // `namespace { … }` block body; nested `namespace { … }` or `types { … }`
117
- // blocks inside a namespace body are rejected with a diagnostic.
118
- const parseBody = (
119
- startLine: number,
120
- endLineExclusive: number,
121
- currentNamespaceName: string,
122
- isInsideNamespace: boolean,
123
- ): void => {
124
- let lineIndex = startLine;
125
- while (lineIndex < endLineExclusive) {
126
- const rawLine = context.lines[lineIndex] ?? '';
127
- const line = stripInlineComment(rawLine).trim();
128
- if (line.length === 0) {
129
- lineIndex += 1;
130
- continue;
131
- }
132
-
133
- const modelMatch = line.match(/^model\s+([A-Za-z_]\w*)\s*\{$/);
134
- if (modelMatch) {
135
- const bounds = findBlockBounds(context, lineIndex);
136
- const name = modelMatch[1] ?? '';
137
- if (name.length > 0) {
138
- const acc = getOrCreateNamespace(
139
- currentNamespaceName,
140
- createTrimmedLineSpan(context, lineIndex),
141
- );
142
- acc.models.push(parseModelBlock(context, name, bounds));
143
- }
144
- lineIndex = bounds.endLine + 1;
145
- continue;
146
- }
147
-
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
- const compositeTypeMatch = line.match(/^type\s+([A-Za-z_]\w*)\s*\{$/);
164
- if (compositeTypeMatch) {
165
- const bounds = findBlockBounds(context, lineIndex);
166
- const name = compositeTypeMatch[1] ?? '';
167
- if (name.length > 0) {
168
- const acc = getOrCreateNamespace(
169
- currentNamespaceName,
170
- createTrimmedLineSpan(context, lineIndex),
171
- );
172
- acc.compositeTypes.push(parseCompositeTypeBlock(context, name, bounds));
173
- }
174
- lineIndex = bounds.endLine + 1;
175
- continue;
176
- }
177
-
178
- const namespaceMatch = line.match(/^namespace\s+([A-Za-z_]\w*)\s*\{$/);
179
- if (namespaceMatch) {
180
- const bounds = findBlockBounds(context, lineIndex);
181
- const name = namespaceMatch[1] ?? '';
182
- if (isInsideNamespace) {
183
- pushDiagnostic(context, {
184
- code: 'PSL_INVALID_NAMESPACE_BLOCK',
185
- message: `Recursive "namespace ${name}" block is not allowed; namespace blocks may not nest`,
186
- span: createTrimmedLineSpan(context, lineIndex),
187
- });
188
- } else if (name === UNSPECIFIED_PSL_NAMESPACE_ID) {
189
- pushDiagnostic(context, {
190
- code: 'PSL_INVALID_NAMESPACE_BLOCK',
191
- message: `Namespace name "${UNSPECIFIED_PSL_NAMESPACE_ID}" is reserved for the parser-synthesised bucket for top-level declarations`,
192
- span: createTrimmedLineSpan(context, lineIndex),
193
- });
194
- } else if (name.length > 0) {
195
- getOrCreateNamespace(name, createTrimmedLineSpan(context, lineIndex));
196
- parseBody(bounds.startLine + 1, bounds.endLine, name, true);
197
- }
198
- lineIndex = bounds.endLine + 1;
199
- continue;
200
- }
201
-
202
- if (/^types\s*\{$/.test(line)) {
203
- const bounds = findBlockBounds(context, lineIndex);
204
- if (isInsideNamespace) {
205
- pushDiagnostic(context, {
206
- code: 'PSL_INVALID_NAMESPACE_BLOCK',
207
- message:
208
- '`types` blocks must be declared at the document top level, not inside a namespace block',
209
- span: createTrimmedLineSpan(context, lineIndex),
210
- });
211
- } else if (typesBlock !== undefined) {
212
- pushDiagnostic(context, {
213
- code: 'PSL_INVALID_TYPES_MEMBER',
214
- message: 'Only one top-level `types` block is allowed per document',
215
- span: createTrimmedLineSpan(context, lineIndex),
216
- });
217
- } else {
218
- typesBlock = parseTypesBlock(context, bounds);
219
- }
220
- lineIndex = bounds.endLine + 1;
221
- continue;
222
- }
223
-
224
- const extensionBlockMatch = line.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\{$/);
225
- if (extensionBlockMatch) {
226
- const keyword = extensionBlockMatch[1] ?? '';
227
- const blockName = extensionBlockMatch[2] ?? '';
228
- const descriptor = lookupExtensionBlockDescriptor(pslBlockNamespace, keyword);
229
- if (descriptor) {
230
- const bounds = findBlockBounds(context, lineIndex);
231
- if (blockName.length > 0) {
232
- const acc = getOrCreateNamespace(
233
- currentNamespaceName,
234
- createTrimmedLineSpan(context, lineIndex),
235
- );
236
- const node = parseExtensionBlock(context, descriptor, blockName, lineIndex, bounds);
237
- acc.extensionBlocks.push(node);
238
- }
239
- lineIndex = bounds.endLine + 1;
240
- continue;
241
- }
242
- }
243
-
244
- if (line.includes('{')) {
245
- const blockName = line.split(/\s+/)[0] ?? 'block';
246
- pushDiagnostic(context, {
247
- code: 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK',
248
- message: `Unsupported top-level block "${blockName}"`,
249
- span: createTrimmedLineSpan(context, lineIndex),
250
- });
251
- const bounds = findBlockBounds(context, lineIndex);
252
- lineIndex = bounds.endLine + 1;
253
- continue;
254
- }
255
-
256
- pushDiagnostic(context, {
257
- code: 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK',
258
- message: `Unsupported top-level declaration "${line}"`,
259
- span: createTrimmedLineSpan(context, lineIndex),
260
- });
261
- lineIndex += 1;
262
- }
263
- };
264
-
265
- parseBody(0, lines.length, UNSPECIFIED_PSL_NAMESPACE_ID, false);
266
-
267
- // Named-type validation: types are document-scoped (one block, outside any
268
- // namespace), so collision checks compare named-type names against every
269
- // model/enum/composite-type in every namespace.
270
- const allModels: PslModel[] = [];
271
- const allEnums: PslEnum[] = [];
272
- const allCompositeTypes: PslCompositeType[] = [];
273
- for (const name of namespaceOrder) {
274
- const acc = namespacesByName.get(name);
275
- if (!acc) continue;
276
- allModels.push(...acc.models);
277
- allEnums.push(...acc.enums);
278
- allCompositeTypes.push(...acc.compositeTypes);
279
- }
280
-
281
- const namedTypeNames = new Set(
282
- (typesBlock?.declarations ?? []).map((declaration) => declaration.name),
283
- );
284
- const modelNames = new Set(allModels.map((model) => model.name));
285
- const enumNames = new Set(allEnums.map((enumBlock) => enumBlock.name));
286
- const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
287
- for (const declaration of typesBlock?.declarations ?? []) {
288
- if (SCALAR_TYPES.has(declaration.name)) {
289
- pushDiagnostic(context, {
290
- code: 'PSL_INVALID_TYPES_MEMBER',
291
- message: `Named type "${declaration.name}" conflicts with scalar type "${declaration.name}"`,
292
- span: declaration.span,
293
- });
294
- continue;
295
- }
296
- if (modelNames.has(declaration.name)) {
297
- pushDiagnostic(context, {
298
- code: 'PSL_INVALID_TYPES_MEMBER',
299
- message: `Named type "${declaration.name}" conflicts with model name "${declaration.name}"`,
300
- span: declaration.span,
301
- });
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
- }
311
- }
312
-
313
- const documentSpan: PslSpan = {
314
- start: createPosition(context, 0, 0),
315
- end: createPosition(
316
- context,
317
- Math.max(lines.length - 1, 0),
318
- (lines[Math.max(lines.length - 1, 0)] ?? '').length,
319
- ),
320
- };
321
-
322
- const namespaces: PslNamespace[] = [];
323
- for (const name of namespaceOrder) {
324
- const acc = namespacesByName.get(name);
325
- if (!acc) continue;
326
- // Drop the synthesised __unspecified__ entry when it ended up empty (e.g.
327
- // every declaration in the document lived inside an explicit
328
- // `namespace { … }` block). Keeping a phantom bucket would force every
329
- // downstream consumer to special-case "namespace with no members".
330
- if (
331
- name === UNSPECIFIED_PSL_NAMESPACE_ID &&
332
- acc.models.length === 0 &&
333
- acc.enums.length === 0 &&
334
- acc.compositeTypes.length === 0 &&
335
- acc.extensionBlocks.length === 0
336
- ) {
337
- continue;
338
- }
339
- const normalizedModels = acc.models.map((model) => ({
340
- ...model,
341
- fields: model.fields.map((field) => {
342
- if (!namedTypeNames.has(field.typeName)) {
343
- return field;
344
- }
345
- const hasRelationAttribute = field.attributes.some(
346
- (attribute) => attribute.name === 'relation',
347
- );
348
- if (
349
- hasRelationAttribute ||
350
- modelNames.has(field.typeName) ||
351
- enumNames.has(field.typeName) ||
352
- compositeTypeNames.has(field.typeName) ||
353
- SCALAR_TYPES.has(field.typeName)
354
- ) {
355
- return field;
356
- }
357
- return {
358
- ...field,
359
- typeRef: field.typeName,
360
- };
361
- }),
362
- }));
363
- namespaces.push(
364
- makePslNamespace({
365
- kind: 'namespace',
366
- name,
367
- entries: makePslNamespaceEntries(
368
- normalizedModels,
369
- acc.enums,
370
- acc.compositeTypes,
371
- acc.extensionBlocks,
372
- ),
373
- span: acc.span ?? documentSpan,
374
- }),
375
- );
376
- }
377
-
378
- // Validate extension blocks with the generic validator after the full
379
- // AST is assembled. The validator needs all namespaces for ref resolution.
380
- // It runs whenever pslBlockDescriptors are registered — codecLookup falls
381
- // back to emptyCodecLookup when not provided, which rejects value-kind
382
- // parameters with unknown codecs. Callers with value parameters should
383
- // always supply a codecLookup.
384
- if (Object.keys(pslBlockNamespace).length > 0) {
385
- const codecLookup = input.codecLookup ?? emptyCodecLookup;
386
- // Build a discriminator → descriptor reverse map from the descriptor
387
- // namespace. The parser keyed by keyword; the validator resolves by
388
- // node.kind (= discriminator) so we need the reverse direction.
389
- const descriptorByDiscriminator = new Map<string, AuthoringPslBlockDescriptor>();
390
- for (const value of Object.values(pslBlockNamespace)) {
391
- if (isAuthoringPslBlockDescriptor(value)) {
392
- descriptorByDiscriminator.set(value.discriminator, value);
393
- }
394
- }
395
- for (const ns of namespaces) {
396
- // Collect extension blocks from entries using the canonical helper, then
397
- // filter to only those whose discriminator is registered in the namespace.
398
- for (const block of namespacePslExtensionBlocks(ns)) {
399
- const descriptor = descriptorByDiscriminator.get(block.kind);
400
- if (descriptor === undefined) {
401
- continue;
402
- }
403
- const blockDiagnostics = validateExtensionBlock(
404
- block,
405
- descriptor,
406
- input.sourceId,
407
- codecLookup,
408
- {
409
- ownerNamespace: ns,
410
- allNamespaces: namespaces,
411
- },
412
- );
413
- diagnostics.push(...blockDiagnostics);
414
- }
415
- }
416
- }
417
-
418
- const ast: PslDocumentAst = {
419
- kind: 'document',
420
- sourceId: input.sourceId,
421
- namespaces,
422
- ...ifDefined('types', typesBlock),
423
- span: documentSpan,
424
- };
425
-
426
- return {
427
- ast,
428
- diagnostics,
429
- ok: diagnostics.length === 0,
430
- };
431
- }
432
-
433
- function parseModelBlock(context: ParserContext, name: string, bounds: BlockBounds): PslModel {
434
- const fields: PslField[] = [];
435
- const attributes: PslModelAttribute[] = [];
436
-
437
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
438
- const raw = context.lines[lineIndex] ?? '';
439
- const line = stripInlineComment(raw).trim();
440
- if (line.length === 0) {
441
- continue;
442
- }
443
-
444
- if (line.startsWith('@@')) {
445
- const attribute = parseModelAttribute(context, line, lineIndex);
446
- if (attribute) {
447
- attributes.push(attribute);
448
- }
449
- continue;
450
- }
451
-
452
- const field = parseField(context, line, lineIndex);
453
- if (field) {
454
- fields.push(field);
455
- }
456
- }
457
-
458
- return {
459
- kind: 'model',
460
- name,
461
- fields,
462
- attributes,
463
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
464
- };
465
- }
466
-
467
- function parseCompositeTypeBlock(
468
- context: ParserContext,
469
- name: string,
470
- bounds: BlockBounds,
471
- ): PslCompositeType {
472
- const fields: PslField[] = [];
473
- const attributes: PslAttribute[] = [];
474
-
475
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
476
- const raw = context.lines[lineIndex] ?? '';
477
- const line = stripInlineComment(raw).trim();
478
- if (line.length === 0) {
479
- continue;
480
- }
481
-
482
- if (line.startsWith('@@')) {
483
- const attribute = parseModelAttribute(context, line, lineIndex);
484
- if (attribute) {
485
- attributes.push(attribute);
486
- }
487
- continue;
488
- }
489
-
490
- const field = parseField(context, line, lineIndex);
491
- if (field) {
492
- fields.push(field);
493
- }
494
- }
495
-
496
- return {
497
- kind: 'compositeType',
498
- name,
499
- fields,
500
- attributes,
501
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
502
- };
503
- }
504
-
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
- function parseTypesBlock(context: ParserContext, bounds: BlockBounds): PslTypesBlock {
589
- const declarations: PslNamedTypeDeclaration[] = [];
590
-
591
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
592
- const raw = context.lines[lineIndex] ?? '';
593
- const lineWithoutComment = stripInlineComment(raw);
594
- const line = lineWithoutComment.trim();
595
- if (line.length === 0) {
596
- continue;
597
- }
598
-
599
- const declarationMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
600
- if (!declarationMatch) {
601
- pushDiagnostic(context, {
602
- code: 'PSL_INVALID_TYPES_MEMBER',
603
- message: `Invalid types declaration "${line}"`,
604
- span: createTrimmedLineSpan(context, lineIndex),
605
- });
606
- continue;
607
- }
608
-
609
- const declarationName = declarationMatch[1] ?? '';
610
- const trimmedStartColumn = firstNonWhitespaceColumn(raw);
611
- const declarationValue = (declarationMatch[2] ?? '').trim();
612
- const valueOffset = line.indexOf(declarationValue);
613
- const declarationValueColumn = trimmedStartColumn + Math.max(valueOffset, 0);
614
-
615
- const typeAndAttributeSplit = splitTypeAndAttributes(declarationValue);
616
- const typeSource = typeAndAttributeSplit.typeSource.trim();
617
- const attributeSource = typeAndAttributeSplit.attributeSource.trimStart();
618
- const leadingAttributeWhitespace =
619
- typeAndAttributeSplit.attributeSource.length - attributeSource.length;
620
-
621
- const typeConstructor = parseTypeConstructorCall(context, {
622
- declarationValue: typeSource,
623
- lineIndex,
624
- startColumn: declarationValueColumn,
625
- invalidCode: 'PSL_INVALID_TYPES_MEMBER',
626
- invalidMessage: (value) => `Invalid types declaration "${value}"`,
627
- });
628
- if (typeConstructor === 'malformed') {
629
- continue;
630
- }
631
-
632
- const attributeParse = extractAttributeTokensWithSpans(
633
- context,
634
- lineIndex,
635
- attributeSource,
636
- declarationValueColumn + typeAndAttributeSplit.attributeOffset + leadingAttributeWhitespace,
637
- );
638
- if (!attributeParse.ok) {
639
- continue;
640
- }
641
- const attributes = attributeParse.tokens
642
- .map((token) =>
643
- parseAttributeToken(context, {
644
- token: token.text,
645
- target: 'namedType',
646
- lineIndex,
647
- span: token.span,
648
- }),
649
- )
650
- .filter((attribute): attribute is PslAttribute => Boolean(attribute));
651
-
652
- if (typeConstructor) {
653
- declarations.push({
654
- kind: 'namedType',
655
- name: declarationName,
656
- typeConstructor,
657
- attributes,
658
- span: createTrimmedLineSpan(context, lineIndex),
659
- });
660
- continue;
661
- }
662
-
663
- const baseTypeMatch = typeSource.match(/^([A-Za-z_]\w*)$/);
664
- if (!baseTypeMatch) {
665
- pushDiagnostic(context, {
666
- code: 'PSL_INVALID_TYPES_MEMBER',
667
- message: `Invalid types declaration "${line}"`,
668
- span: createTrimmedLineSpan(context, lineIndex),
669
- });
670
- continue;
671
- }
672
-
673
- const baseType = baseTypeMatch[1] ?? '';
674
-
675
- declarations.push({
676
- kind: 'namedType',
677
- name: declarationName,
678
- baseType,
679
- attributes,
680
- span: createTrimmedLineSpan(context, lineIndex),
681
- });
682
- }
683
-
684
- return {
685
- kind: 'types',
686
- declarations,
687
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine),
688
- };
689
- }
690
-
691
- function parseTypeConstructorCall(
692
- context: ParserContext,
693
- input: {
694
- readonly declarationValue: string;
695
- readonly lineIndex: number;
696
- readonly startColumn: number;
697
- readonly invalidCode: PslDiagnosticCode;
698
- readonly invalidMessage: (value: string) => string;
699
- },
700
- ): PslTypeConstructorCall | 'malformed' | undefined {
701
- const value = input.declarationValue.trim();
702
- const constructorMatch = value.match(
703
- /^([A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*)\s*\(/,
704
- );
705
- if (!constructorMatch) {
706
- return undefined;
707
- }
708
-
709
- // constructorMatch already required `(`; openParen is guaranteed ≥ 0.
710
- const openParen = value.indexOf('(');
711
- const closeParen = value.lastIndexOf(')');
712
-
713
- if (closeParen !== value.length - 1) {
714
- pushDiagnostic(context, {
715
- code: input.invalidCode,
716
- message: input.invalidMessage(value),
717
- span: createInlineSpan(
718
- context,
719
- input.lineIndex,
720
- input.startColumn,
721
- input.startColumn + value.length,
722
- ),
723
- });
724
- return 'malformed';
725
- }
726
-
727
- const constructorPath = constructorMatch[1] ?? '';
728
-
729
- const argsRaw = value.slice(openParen + 1, closeParen);
730
- const args = parseArgumentList(context, {
731
- argsRaw,
732
- argsOffset: input.startColumn + openParen + 1,
733
- lineIndex: input.lineIndex,
734
- token: value,
735
- span: createInlineSpan(
736
- context,
737
- input.lineIndex,
738
- input.startColumn,
739
- input.startColumn + value.length,
740
- ),
741
- invalidCode: input.invalidCode,
742
- invalidEmptyArgumentMessage: `Invalid empty argument in type constructor "${value}"`,
743
- invalidNamedArgumentMessage: (part) =>
744
- `Invalid named argument syntax "${part}" in type constructor "${value}"`,
745
- });
746
- if (!args) {
747
- return 'malformed';
748
- }
749
-
750
- return {
751
- kind: 'typeConstructor',
752
- path: constructorPath.split('.'),
753
- args,
754
- span: createInlineSpan(
755
- context,
756
- input.lineIndex,
757
- input.startColumn,
758
- input.startColumn + value.length,
759
- ),
760
- };
761
- }
762
-
763
- function parseModelAttribute(
764
- context: ParserContext,
765
- line: string,
766
- lineIndex: number,
767
- ): PslModelAttribute | undefined {
768
- const rawLine = context.lines[lineIndex] ?? '';
769
- const tokenParse = extractAttributeTokensWithSpans(
770
- context,
771
- lineIndex,
772
- line,
773
- firstNonWhitespaceColumn(rawLine),
774
- );
775
- if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
776
- pushDiagnostic(context, {
777
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
778
- message: `Invalid model attribute syntax "${line}"`,
779
- span: createTrimmedLineSpan(context, lineIndex),
780
- });
781
- return undefined;
782
- }
783
- const token = tokenParse.tokens[0];
784
- if (!token) {
785
- return undefined;
786
- }
787
- return parseAttributeToken(context, {
788
- token: token.text,
789
- target: 'model',
790
- lineIndex,
791
- span: token.span,
792
- });
793
- }
794
-
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
- function parseField(context: ParserContext, line: string, lineIndex: number): PslField | undefined {
840
- const fieldMatch = line.match(/^([A-Za-z_]\w*)(\s+)(.+)$/);
841
- if (!fieldMatch) {
842
- pushDiagnostic(context, {
843
- code: 'PSL_INVALID_MODEL_MEMBER',
844
- message: `Invalid model member declaration "${line}"`,
845
- span: createTrimmedLineSpan(context, lineIndex),
846
- });
847
- return undefined;
848
- }
849
-
850
- const fieldName = fieldMatch[1] ?? '';
851
- const separator = fieldMatch[2] ?? '';
852
- const remainder = fieldMatch[3] ?? '';
853
- const typeAndAttributeSplit = splitTypeAndAttributes(remainder);
854
- const rawTypeSource = typeAndAttributeSplit.typeSource.trim();
855
- const attributePart = typeAndAttributeSplit.attributeSource;
856
- const optional = rawTypeSource.endsWith('?');
857
- const typeSourceWithoutOptional = optional ? rawTypeSource.slice(0, -1).trimEnd() : rawTypeSource;
858
- const list = typeSourceWithoutOptional.endsWith('[]');
859
- const baseTypeSource = list
860
- ? typeSourceWithoutOptional.slice(0, -2).trimEnd()
861
- : typeSourceWithoutOptional;
862
- const rawLine = context.lines[lineIndex] ?? '';
863
- const trimmedStartColumn = firstNonWhitespaceColumn(rawLine);
864
- const typeStartColumn = trimmedStartColumn + fieldName.length + separator.length;
865
-
866
- const typeConstructor = parseTypeConstructorCall(context, {
867
- declarationValue: baseTypeSource,
868
- lineIndex,
869
- startColumn: typeStartColumn,
870
- invalidCode: 'PSL_INVALID_MODEL_MEMBER',
871
- invalidMessage: (value) => `Invalid field type constructor "${value}"`,
872
- });
873
- if (typeConstructor === 'malformed') {
874
- return undefined;
875
- }
876
-
877
- let typeName: string;
878
- let typeNamespaceId: string | undefined;
879
- let typeContractSpaceId: string | undefined;
880
-
881
- if (typeConstructor) {
882
- typeName = typeConstructor.path.join('.');
883
- } else {
884
- // Detect the colon-prefix cross-contract-space form: `<space>:<rest>` where
885
- // `<rest>` is the existing dot-qualified or bare form (`<ns>.<Name>` or `<Name>`).
886
- // Multiple colons (e.g. `a:b:c`) are invalid.
887
- const colonCount = (baseTypeSource.match(/:/g) ?? []).length;
888
- if (colonCount > 1) {
889
- pushDiagnostic(context, {
890
- code: 'PSL_INVALID_MODEL_MEMBER',
891
- message: `Invalid model member declaration "${line}"`,
892
- span: createTrimmedLineSpan(context, lineIndex),
893
- });
894
- return undefined;
895
- }
896
-
897
- let typeRefSource: string;
898
- if (colonCount === 1) {
899
- // Colon-prefix form: `<space>:<rest>`
900
- const colonIndex = baseTypeSource.indexOf(':');
901
- const spaceCandidate = baseTypeSource.slice(0, colonIndex);
902
- typeRefSource = baseTypeSource.slice(colonIndex + 1);
903
- // Validate space identifier
904
- if (!/^[A-Za-z_]\w*$/.test(spaceCandidate)) {
905
- pushDiagnostic(context, {
906
- code: 'PSL_INVALID_MODEL_MEMBER',
907
- message: `Invalid model member declaration "${line}"`,
908
- span: createTrimmedLineSpan(context, lineIndex),
909
- });
910
- return undefined;
911
- }
912
- typeContractSpaceId = spaceCandidate;
913
- } else {
914
- typeRefSource = baseTypeSource;
915
- }
916
-
917
- const dotCount = (typeRefSource.match(/\./g) ?? []).length;
918
- if (dotCount > 1) {
919
- pushDiagnostic(context, {
920
- code: 'PSL_INVALID_QUALIFIED_TYPE',
921
- message: `Nested dot-qualified type "${baseTypeSource}" is not supported; use exactly one qualifier segment (e.g. "ns.TypeName")`,
922
- span: createInlineSpan(
923
- context,
924
- lineIndex,
925
- typeStartColumn,
926
- typeStartColumn + baseTypeSource.length,
927
- ),
928
- });
929
- return undefined;
930
- }
931
- const singleMatch = typeRefSource.match(/^([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?$/);
932
- if (!singleMatch) {
933
- pushDiagnostic(context, {
934
- code: 'PSL_INVALID_MODEL_MEMBER',
935
- message: `Invalid model member declaration "${line}"`,
936
- span: createTrimmedLineSpan(context, lineIndex),
937
- });
938
- return undefined;
939
- }
940
- if (singleMatch[2] !== undefined) {
941
- typeNamespaceId = singleMatch[1];
942
- typeName = singleMatch[2];
943
- } else {
944
- typeName = singleMatch[1] ?? '';
945
- }
946
- }
947
-
948
- const attributes: PslFieldAttribute[] = [];
949
- const attributeSource = attributePart.trimStart();
950
- const leadingAttributeWhitespace = attributePart.length - attributeSource.length;
951
- const tokenParse = extractAttributeTokensWithSpans(
952
- context,
953
- lineIndex,
954
- attributeSource,
955
- trimmedStartColumn +
956
- fieldName.length +
957
- separator.length +
958
- typeAndAttributeSplit.attributeOffset +
959
- leadingAttributeWhitespace,
960
- );
961
- if (!tokenParse.ok) {
962
- return {
963
- kind: 'field',
964
- name: fieldName,
965
- typeName,
966
- ...ifDefined('typeNamespaceId', typeNamespaceId),
967
- ...ifDefined('typeContractSpaceId', typeContractSpaceId),
968
- ...ifDefined('typeConstructor', typeConstructor),
969
- optional,
970
- list,
971
- attributes,
972
- span: createTrimmedLineSpan(context, lineIndex),
973
- };
974
- }
975
-
976
- for (const token of tokenParse.tokens) {
977
- const parsed = parseAttributeToken(context, {
978
- token: token.text,
979
- target: 'field',
980
- lineIndex,
981
- span: token.span,
982
- });
983
- if (parsed) {
984
- attributes.push(parsed);
985
- }
986
- }
987
-
988
- return {
989
- kind: 'field',
990
- name: fieldName,
991
- typeName,
992
- ...ifDefined('typeNamespaceId', typeNamespaceId),
993
- ...ifDefined('typeContractSpaceId', typeContractSpaceId),
994
- ...ifDefined('typeConstructor', typeConstructor),
995
- optional,
996
- list,
997
- attributes,
998
- span: createTrimmedLineSpan(context, lineIndex),
999
- };
1000
- }
1001
-
1002
- function isQuoteEscaped(value: string, quoteIndex: number): boolean {
1003
- let backslashCount = 0;
1004
-
1005
- for (let index = quoteIndex - 1; index >= 0 && value[index] === '\\'; index -= 1) {
1006
- backslashCount += 1;
1007
- }
1008
-
1009
- return backslashCount % 2 === 1;
1010
- }
1011
-
1012
- function splitTypeAndAttributes(value: string): {
1013
- readonly typeSource: string;
1014
- readonly attributeSource: string;
1015
- readonly attributeOffset: number;
1016
- } {
1017
- let depthParen = 0;
1018
- let depthBracket = 0;
1019
- let depthBrace = 0;
1020
- let quote: '"' | "'" | null = null;
1021
-
1022
- for (let index = 0; index < value.length; index += 1) {
1023
- const character = value[index] ?? '';
1024
- if (quote) {
1025
- if (character === quote && !isQuoteEscaped(value, index)) {
1026
- quote = null;
1027
- }
1028
- continue;
1029
- }
1030
-
1031
- if (character === '"' || character === "'") {
1032
- quote = character;
1033
- continue;
1034
- }
1035
- if (character === '(') {
1036
- depthParen += 1;
1037
- continue;
1038
- }
1039
- if (character === ')') {
1040
- depthParen = Math.max(0, depthParen - 1);
1041
- continue;
1042
- }
1043
- if (character === '[') {
1044
- depthBracket += 1;
1045
- continue;
1046
- }
1047
- if (character === ']') {
1048
- depthBracket = Math.max(0, depthBracket - 1);
1049
- continue;
1050
- }
1051
- if (character === '{') {
1052
- depthBrace += 1;
1053
- continue;
1054
- }
1055
- if (character === '}') {
1056
- depthBrace = Math.max(0, depthBrace - 1);
1057
- continue;
1058
- }
1059
-
1060
- if (character === '@' && depthParen === 0 && depthBracket === 0 && depthBrace === 0) {
1061
- return {
1062
- typeSource: value.slice(0, index).trimEnd(),
1063
- attributeSource: value.slice(index),
1064
- attributeOffset: index,
1065
- };
1066
- }
1067
- }
1068
-
1069
- return {
1070
- typeSource: value.trimEnd(),
1071
- attributeSource: '',
1072
- attributeOffset: value.length,
1073
- };
1074
- }
1075
-
1076
- function parseAttributeToken(
1077
- context: ParserContext,
1078
- input: {
1079
- readonly token: string;
1080
- readonly target: PslAttributeTarget;
1081
- readonly lineIndex: number;
1082
- readonly span: PslSpan;
1083
- },
1084
- ): PslAttribute | undefined {
1085
- const expectsBlockPrefix = input.target === 'model' || input.target === 'enum';
1086
- const targetLabel = input.target === 'enum' ? 'Enum' : 'Model';
1087
- if (expectsBlockPrefix && !input.token.startsWith('@@')) {
1088
- pushDiagnostic(context, {
1089
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1090
- message: `${targetLabel} attribute "${input.token}" must use @@ prefix`,
1091
- span: input.span,
1092
- });
1093
- return undefined;
1094
- }
1095
- if (!expectsBlockPrefix && !input.token.startsWith('@')) {
1096
- pushDiagnostic(context, {
1097
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1098
- message: `Attribute "${input.token}" must use @ prefix`,
1099
- span: input.span,
1100
- });
1101
- return undefined;
1102
- }
1103
- if (!expectsBlockPrefix && input.token.startsWith('@@')) {
1104
- pushDiagnostic(context, {
1105
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1106
- message: `Attribute "${input.token}" is not valid in ${input.target} context`,
1107
- span: input.span,
1108
- });
1109
- return undefined;
1110
- }
1111
-
1112
- const rawBody = expectsBlockPrefix ? input.token.slice(2) : input.token.slice(1);
1113
- const openParen = rawBody.indexOf('(');
1114
- const closeParen = rawBody.lastIndexOf(')');
1115
- const hasArgs = openParen >= 0 || closeParen >= 0;
1116
- if ((openParen >= 0 && closeParen === -1) || (openParen === -1 && closeParen >= 0)) {
1117
- pushDiagnostic(context, {
1118
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1119
- message: `Invalid attribute syntax "${input.token}"`,
1120
- span: input.span,
1121
- });
1122
- return undefined;
1123
- }
1124
-
1125
- const name = (openParen >= 0 ? rawBody.slice(0, openParen) : rawBody).trim();
1126
- if (!/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(name)) {
1127
- pushDiagnostic(context, {
1128
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1129
- message: `Invalid attribute name "${name || input.token}"`,
1130
- span: input.span,
1131
- });
1132
- return undefined;
1133
- }
1134
-
1135
- let args: readonly PslAttributeArgument[] = [];
1136
- if (hasArgs && openParen >= 0 && closeParen >= openParen) {
1137
- if (closeParen !== rawBody.length - 1) {
1138
- pushDiagnostic(context, {
1139
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1140
- message: `Invalid trailing syntax in attribute "${input.token}"`,
1141
- span: input.span,
1142
- });
1143
- return undefined;
1144
- }
1145
- const argsRaw = rawBody.slice(openParen + 1, closeParen);
1146
- const parsedArgs = parseArgumentList(context, {
1147
- argsRaw,
1148
- argsOffset: input.span.start.column - 1 + (expectsBlockPrefix ? 2 : 1) + openParen + 1,
1149
- lineIndex: input.lineIndex,
1150
- token: input.token,
1151
- span: input.span,
1152
- invalidCode: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1153
- invalidEmptyArgumentMessage: `Invalid empty argument in attribute "${input.token}"`,
1154
- invalidNamedArgumentMessage: (part) => `Invalid named argument syntax "${part}"`,
1155
- });
1156
- if (!parsedArgs) {
1157
- return undefined;
1158
- }
1159
- args = parsedArgs;
1160
- }
1161
-
1162
- return {
1163
- kind: 'attribute',
1164
- target: input.target,
1165
- name,
1166
- args,
1167
- span: input.span,
1168
- };
1169
- }
1170
-
1171
- function parseArgumentList(
1172
- context: ParserContext,
1173
- input: {
1174
- readonly argsRaw: string;
1175
- readonly argsOffset: number;
1176
- readonly lineIndex: number;
1177
- readonly token: string;
1178
- readonly span: PslSpan;
1179
- readonly invalidCode: PslDiagnosticCode;
1180
- readonly invalidEmptyArgumentMessage: string;
1181
- readonly invalidNamedArgumentMessage: (part: string) => string;
1182
- },
1183
- ): readonly PslAttributeArgument[] | undefined {
1184
- const trimmed = input.argsRaw.trim();
1185
- if (trimmed.length === 0) {
1186
- return [];
1187
- }
1188
-
1189
- const parts = splitTopLevelSegments(input.argsRaw, ',');
1190
- const args: PslAttributeArgument[] = [];
1191
-
1192
- for (const part of parts) {
1193
- const original = part.value;
1194
- const trimmedPart = original.trim();
1195
- if (trimmedPart.length === 0) {
1196
- pushDiagnostic(context, {
1197
- code: input.invalidCode,
1198
- message: input.invalidEmptyArgumentMessage,
1199
- span: input.span,
1200
- });
1201
- return undefined;
1202
- }
1203
-
1204
- const leadingWhitespace = original.length - original.trimStart().length;
1205
- const partStart = input.argsOffset + part.start + leadingWhitespace;
1206
- const partEnd = partStart + trimmedPart.length;
1207
- const partSpan = createInlineSpan(context, input.lineIndex, partStart, partEnd);
1208
-
1209
- const namedSplit = splitTopLevelSegments(trimmedPart, ':');
1210
- if (namedSplit.length > 1) {
1211
- const first = namedSplit[0];
1212
- if (!first) {
1213
- pushDiagnostic(context, {
1214
- code: input.invalidCode,
1215
- message: input.invalidNamedArgumentMessage(trimmedPart),
1216
- span: partSpan,
1217
- });
1218
- return undefined;
1219
- }
1220
- const name = first.value.trim();
1221
- const rawValue = trimmedPart.slice(first.end + 1).trim();
1222
- if (!name || rawValue.length === 0) {
1223
- pushDiagnostic(context, {
1224
- code: input.invalidCode,
1225
- message: input.invalidNamedArgumentMessage(trimmedPart),
1226
- span: partSpan,
1227
- });
1228
- return undefined;
1229
- }
1230
- args.push({
1231
- kind: 'named',
1232
- name,
1233
- value: normalizeAttributeArgumentValue(rawValue),
1234
- span: partSpan,
1235
- });
1236
- continue;
1237
- }
1238
-
1239
- args.push({
1240
- kind: 'positional',
1241
- value: normalizeAttributeArgumentValue(trimmedPart),
1242
- span: partSpan,
1243
- });
1244
- }
1245
-
1246
- return args;
1247
- }
1248
-
1249
- function normalizeAttributeArgumentValue(value: string): string {
1250
- return value.trim();
1251
- }
1252
-
1253
- function findBlockBounds(context: ParserContext, startLine: number): BlockBounds {
1254
- let depth = 0;
1255
-
1256
- for (let lineIndex = startLine; lineIndex < context.lines.length; lineIndex += 1) {
1257
- const line = stripInlineComment(context.lines[lineIndex] ?? '');
1258
- let quote: '"' | "'" | null = null;
1259
- for (let index = 0; index < line.length; index += 1) {
1260
- const character = line[index] ?? '';
1261
- if (quote) {
1262
- if (character === quote && !isQuoteEscaped(line, index)) {
1263
- quote = null;
1264
- }
1265
- continue;
1266
- }
1267
-
1268
- if (character === '"' || character === "'") {
1269
- quote = character;
1270
- continue;
1271
- }
1272
-
1273
- if (character === '{') {
1274
- depth += 1;
1275
- }
1276
- if (character === '}') {
1277
- depth -= 1;
1278
- if (depth === 0) {
1279
- return { startLine, endLine: lineIndex, closed: true };
1280
- }
1281
- }
1282
- }
1283
- }
1284
-
1285
- pushDiagnostic(context, {
1286
- code: 'PSL_UNTERMINATED_BLOCK',
1287
- message: 'Unterminated block declaration',
1288
- span: createTrimmedLineSpan(context, startLine),
1289
- });
1290
- return {
1291
- startLine,
1292
- endLine: context.lines.length - 1,
1293
- closed: false,
1294
- };
1295
- }
1296
-
1297
- interface TopLevelSegment {
1298
- readonly value: string;
1299
- readonly start: number;
1300
- readonly end: number;
1301
- }
1302
-
1303
- function splitTopLevelSegments(value: string, separator: ',' | ':'): TopLevelSegment[] {
1304
- const parts: TopLevelSegment[] = [];
1305
- let depthParen = 0;
1306
- let depthBracket = 0;
1307
- let depthBrace = 0;
1308
- let quote: '"' | "'" | null = null;
1309
- let start = 0;
1310
-
1311
- for (let index = 0; index < value.length; index += 1) {
1312
- const character = value[index] ?? '';
1313
- if (quote) {
1314
- if (character === quote && !isQuoteEscaped(value, index)) {
1315
- quote = null;
1316
- }
1317
- continue;
1318
- }
1319
-
1320
- if (character === '"' || character === "'") {
1321
- quote = character;
1322
- continue;
1323
- }
1324
-
1325
- if (character === '(') {
1326
- depthParen += 1;
1327
- continue;
1328
- }
1329
- if (character === ')') {
1330
- depthParen = Math.max(0, depthParen - 1);
1331
- continue;
1332
- }
1333
- if (character === '[') {
1334
- depthBracket += 1;
1335
- continue;
1336
- }
1337
- if (character === ']') {
1338
- depthBracket = Math.max(0, depthBracket - 1);
1339
- continue;
1340
- }
1341
- if (character === '{') {
1342
- depthBrace += 1;
1343
- continue;
1344
- }
1345
- if (character === '}') {
1346
- depthBrace = Math.max(0, depthBrace - 1);
1347
- continue;
1348
- }
1349
-
1350
- if (character === separator && depthParen === 0 && depthBracket === 0 && depthBrace === 0) {
1351
- parts.push({
1352
- value: value.slice(start, index),
1353
- start,
1354
- end: index,
1355
- });
1356
- start = index + 1;
1357
- }
1358
- }
1359
-
1360
- parts.push({
1361
- value: value.slice(start),
1362
- start,
1363
- end: value.length,
1364
- });
1365
- return parts;
1366
- }
1367
-
1368
- function extractAttributeTokensWithSpans(
1369
- context: ParserContext,
1370
- lineIndex: number,
1371
- value: string,
1372
- startColumn: number,
1373
- ): { readonly ok: boolean; readonly tokens: readonly { text: string; span: PslSpan }[] } {
1374
- const tokens: { text: string; span: PslSpan }[] = [];
1375
- let index = 0;
1376
- while (index < value.length) {
1377
- while (index < value.length && /\s/.test(value[index] ?? '')) {
1378
- index += 1;
1379
- }
1380
- if (index >= value.length) {
1381
- break;
1382
- }
1383
-
1384
- if (value[index] !== '@') {
1385
- pushDiagnostic(context, {
1386
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1387
- message: `Invalid attribute syntax "${value.trim()}"`,
1388
- span: createInlineSpan(context, lineIndex, startColumn + index, startColumn + value.length),
1389
- });
1390
- return { ok: false, tokens };
1391
- }
1392
-
1393
- const start = index;
1394
- index += 1;
1395
- if (value[index] === '@') {
1396
- index += 1;
1397
- }
1398
-
1399
- const nameStart = index;
1400
- while (index < value.length && /[A-Za-z0-9_.-]/.test(value[index] ?? '')) {
1401
- index += 1;
1402
- }
1403
-
1404
- if (index === nameStart) {
1405
- pushDiagnostic(context, {
1406
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1407
- message: `Invalid attribute syntax "${value.slice(start).trim()}"`,
1408
- span: createInlineSpan(context, lineIndex, startColumn + start, startColumn + value.length),
1409
- });
1410
- return { ok: false, tokens };
1411
- }
1412
-
1413
- if (value[index] === '(') {
1414
- let depth = 0;
1415
- let quote: '"' | "'" | null = null;
1416
- while (index < value.length) {
1417
- const char = value[index] ?? '';
1418
- if (quote) {
1419
- if (char === quote && !isQuoteEscaped(value, index)) {
1420
- quote = null;
1421
- }
1422
- index += 1;
1423
- continue;
1424
- }
1425
-
1426
- if (char === '"' || char === "'") {
1427
- quote = char;
1428
- index += 1;
1429
- continue;
1430
- }
1431
-
1432
- if (char === '(') {
1433
- depth += 1;
1434
- } else if (char === ')') {
1435
- depth -= 1;
1436
- if (depth === 0) {
1437
- index += 1;
1438
- break;
1439
- }
1440
- }
1441
- index += 1;
1442
- }
1443
- if (depth !== 0) {
1444
- pushDiagnostic(context, {
1445
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1446
- message: `Unterminated attribute argument list in "${value.slice(start).trim()}"`,
1447
- span: createInlineSpan(
1448
- context,
1449
- lineIndex,
1450
- startColumn + start,
1451
- startColumn + value.length,
1452
- ),
1453
- });
1454
- return { ok: false, tokens };
1455
- }
1456
- }
1457
-
1458
- const tokenText = value.slice(start, index).trim();
1459
- tokens.push({
1460
- text: tokenText,
1461
- span: createInlineSpan(context, lineIndex, startColumn + start, startColumn + index),
1462
- });
1463
-
1464
- while (index < value.length && /\s/.test(value[index] ?? '')) {
1465
- index += 1;
1466
- }
1467
-
1468
- if (index < value.length && value[index] !== '@') {
1469
- break;
1470
- }
1471
- }
1472
-
1473
- if (index < value.length && value[index] !== '@') {
1474
- pushDiagnostic(context, {
1475
- code: 'PSL_INVALID_ATTRIBUTE_SYNTAX',
1476
- message: `Invalid attribute syntax "${value.trim()}"`,
1477
- span: createInlineSpan(context, lineIndex, startColumn + index, startColumn + value.length),
1478
- });
1479
- return { ok: false, tokens };
1480
- }
1481
-
1482
- return { ok: true, tokens };
1483
- }
1484
-
1485
- function stripInlineComment(line: string): string {
1486
- let quote: '"' | "'" | null = null;
1487
- for (let index = 0; index < line.length - 1; index += 1) {
1488
- const current = line[index] ?? '';
1489
- const next = line[index + 1] ?? '';
1490
-
1491
- if (quote) {
1492
- if (current === quote && !isQuoteEscaped(line, index)) {
1493
- quote = null;
1494
- }
1495
- continue;
1496
- }
1497
-
1498
- if (current === '"' || current === "'") {
1499
- quote = current;
1500
- continue;
1501
- }
1502
-
1503
- if (current === '/' && next === '/') {
1504
- return line.slice(0, index);
1505
- }
1506
- }
1507
-
1508
- return line;
1509
- }
1510
-
1511
- function computeLineOffsets(schema: string): number[] {
1512
- const offsets = [0];
1513
- for (let index = 0; index < schema.length; index += 1) {
1514
- if (schema[index] === '\n') {
1515
- offsets.push(index + 1);
1516
- }
1517
- }
1518
- return offsets;
1519
- }
1520
-
1521
- function firstNonWhitespaceColumn(line: string): number {
1522
- const first = line.search(/\S/);
1523
- return first === -1 ? 0 : first;
1524
- }
1525
-
1526
- function createInlineSpan(
1527
- context: ParserContext,
1528
- lineIndex: number,
1529
- startColumn: number,
1530
- endColumn: number,
1531
- ): PslSpan {
1532
- return {
1533
- start: createPosition(context, lineIndex, startColumn),
1534
- end: createPosition(context, lineIndex, endColumn),
1535
- };
1536
- }
1537
-
1538
- function createTrimmedLineSpan(context: ParserContext, lineIndex: number): PslSpan {
1539
- const line = context.lines[lineIndex] ?? '';
1540
- const startColumn = firstNonWhitespaceColumn(line);
1541
- return {
1542
- start: createPosition(context, lineIndex, startColumn),
1543
- end: createPosition(context, lineIndex, line.length),
1544
- };
1545
- }
1546
-
1547
- function createLineRangeSpan(context: ParserContext, startLine: number, endLine: number): PslSpan {
1548
- const startLineText = context.lines[startLine] ?? '';
1549
- const endLineText = context.lines[endLine] ?? '';
1550
- const startColumn = firstNonWhitespaceColumn(startLineText);
1551
- return {
1552
- start: createPosition(context, startLine, startColumn),
1553
- end: createPosition(context, endLine, endLineText.length),
1554
- };
1555
- }
1556
-
1557
- function createPosition(
1558
- context: ParserContext,
1559
- lineIndex: number,
1560
- columnIndex: number,
1561
- ): PslPosition {
1562
- const clampedLineIndex = Math.max(0, Math.min(lineIndex, context.lineOffsets.length - 1));
1563
- const lineText = context.lines[clampedLineIndex] ?? '';
1564
- const clampedColumnIndex = Math.max(0, Math.min(columnIndex, lineText.length));
1565
- return {
1566
- offset: (context.lineOffsets[clampedLineIndex] ?? 0) + clampedColumnIndex,
1567
- line: clampedLineIndex + 1,
1568
- column: clampedColumnIndex + 1,
1569
- };
1570
- }
1571
-
1572
- function pushDiagnostic(
1573
- context: ParserContext,
1574
- diagnostic: Omit<PslDiagnostic, 'sourceId'> & { readonly code: PslDiagnosticCode },
1575
- ): void {
1576
- context.diagnostics.push({
1577
- ...diagnostic,
1578
- sourceId: context.sourceId,
1579
- });
1580
- }
1581
-
1582
- function lookupExtensionBlockDescriptor(
1583
- namespace: AuthoringPslBlockDescriptorNamespace,
1584
- keyword: string,
1585
- ): AuthoringPslBlockDescriptor | undefined {
1586
- if (!Object.hasOwn(namespace, keyword)) {
1587
- return undefined;
1588
- }
1589
- const value = namespace[keyword];
1590
- return isAuthoringPslBlockDescriptor(value) ? value : undefined;
1591
- }
1592
-
1593
- /**
1594
- * 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:
1597
- *
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`)
1603
- *
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.
1607
- */
1608
- function parseExtensionBlock(
1609
- context: ParserContext,
1610
- descriptor: AuthoringPslBlockDescriptor,
1611
- blockName: string,
1612
- keywordLineIndex: number,
1613
- bounds: BlockBounds,
1614
- ): PslExtensionBlock {
1615
- const parameters: Record<string, PslExtensionBlockParamValue> = {};
1616
-
1617
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
1618
- const raw = context.lines[lineIndex] ?? '';
1619
- const line = stripInlineComment(raw).trim();
1620
- if (line.length === 0) {
1621
- continue;
1622
- }
1623
-
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
- });
1631
- continue;
1632
- }
1633
-
1634
- const key = assignMatch[1] ?? '';
1635
- const rhsRaw = (assignMatch[2] ?? '').trim();
1636
- const paramDescriptor = descriptor.parameters[key];
1637
-
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
- };
1645
- continue;
1646
- }
1647
-
1648
- const captured = captureParamRhs(context, lineIndex, rhsRaw, paramDescriptor);
1649
- if (captured !== undefined) {
1650
- parameters[key] = captured;
1651
- }
1652
- }
1653
-
1654
- return {
1655
- kind: descriptor.discriminator,
1656
- name: blockName,
1657
- parameters,
1658
- span: createLineRangeSpan(context, keywordLineIndex, bounds.endLine),
1659
- };
1660
- }
1661
-
1662
- /**
1663
- * Captures a single parameter RHS string according to the declared parameter
1664
- * kind. Returns `undefined` and pushes a diagnostic only if the syntax is
1665
- * genuinely malformed (e.g. a `list` with no opening bracket). Value
1666
- * acceptance (codec, option-in-set, ref resolution) is handled by the validator.
1667
- */
1668
- function captureParamRhs(
1669
- context: ParserContext,
1670
- lineIndex: number,
1671
- rhs: string,
1672
- param: AuthoringPslBlockDescriptor['parameters'][string],
1673
- ): PslExtensionBlockParamValue | undefined {
1674
- const span = createTrimmedLineSpan(context, lineIndex);
1675
-
1676
- switch (param.kind) {
1677
- case 'ref': {
1678
- return { kind: 'ref', identifier: rhs, span };
1679
- }
1680
- case 'value': {
1681
- return { kind: 'value', raw: rhs, span };
1682
- }
1683
- case 'option': {
1684
- return { kind: 'option', token: rhs, span };
1685
- }
1686
- case 'list': {
1687
- if (!rhs.startsWith('[') || !rhs.endsWith(']')) {
1688
- pushDiagnostic(context, {
1689
- code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
1690
- message: `Expected a bracketed list "[…]" for list parameter, got "${rhs}"`,
1691
- span,
1692
- });
1693
- return undefined;
1694
- }
1695
- const inner = rhs.slice(1, -1).trim();
1696
- const items: PslExtensionBlockParamValue[] = [];
1697
- if (inner.length > 0) {
1698
- const segments = splitTopLevelSegments(inner, ',');
1699
- for (const segment of segments) {
1700
- const itemRhs = segment.value.trim();
1701
- if (itemRhs.length === 0) {
1702
- continue;
1703
- }
1704
- const item = captureParamRhs(context, lineIndex, itemRhs, param.of);
1705
- if (item !== undefined) {
1706
- items.push(item);
1707
- }
1708
- }
1709
- }
1710
- return { kind: 'list', items, span };
1711
- }
1712
- }
1713
- }