@prisma-next/psl-parser 0.14.0-dev.4 → 0.14.0-dev.6

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.
@@ -1,1145 +0,0 @@
1
- import { UNSPECIFIED_PSL_NAMESPACE_ID, makePslNamespace, makePslNamespaceEntries, namespacePslExtensionBlocks, validateExtensionBlock } from "@prisma-next/framework-components/psl-ast";
2
- import { isAuthoringPslBlockDescriptor } from "@prisma-next/framework-components/authoring";
3
- import { emptyCodecLookup } from "@prisma-next/framework-components/codec";
4
- import { ifDefined } from "@prisma-next/utils/defined";
5
- //#region src/parser.ts
6
- const SCALAR_TYPES = new Set([
7
- "String",
8
- "Boolean",
9
- "Int",
10
- "BigInt",
11
- "Float",
12
- "Decimal",
13
- "DateTime",
14
- "Json",
15
- "Bytes"
16
- ]);
17
- function parsePslDocument(input) {
18
- const normalizedSchema = input.schema.replaceAll("\r\n", "\n");
19
- const lines = normalizedSchema.split("\n");
20
- const lineOffsets = computeLineOffsets(normalizedSchema);
21
- const diagnostics = [];
22
- const context = {
23
- schema: normalizedSchema,
24
- sourceId: input.sourceId,
25
- lines,
26
- lineOffsets,
27
- diagnostics
28
- };
29
- const namespaceOrder = [];
30
- const namespacesByName = /* @__PURE__ */ new Map();
31
- const getOrCreateNamespace = (name, spanIfNew) => {
32
- let acc = namespacesByName.get(name);
33
- if (!acc) {
34
- acc = {
35
- name,
36
- models: [],
37
- compositeTypes: [],
38
- extensionBlocks: [],
39
- span: spanIfNew
40
- };
41
- namespacesByName.set(name, acc);
42
- namespaceOrder.push(name);
43
- }
44
- return acc;
45
- };
46
- let typesBlock;
47
- const pslBlockNamespace = input.pslBlockDescriptors ?? {};
48
- const parseBody = (startLine, endLineExclusive, currentNamespaceName, isInsideNamespace) => {
49
- let lineIndex = startLine;
50
- while (lineIndex < endLineExclusive) {
51
- const line = stripInlineComment(context.lines[lineIndex] ?? "").trim();
52
- if (line.length === 0) {
53
- lineIndex += 1;
54
- continue;
55
- }
56
- const modelMatch = line.match(/^model\s+([A-Za-z_]\w*)\s*\{$/);
57
- if (modelMatch) {
58
- const bounds = findBlockBounds(context, lineIndex);
59
- const name = modelMatch[1] ?? "";
60
- if (name.length > 0) getOrCreateNamespace(currentNamespaceName, createTrimmedLineSpan(context, lineIndex)).models.push(parseModelBlock(context, name, bounds));
61
- lineIndex = bounds.endLine + 1;
62
- continue;
63
- }
64
- const compositeTypeMatch = line.match(/^type\s+([A-Za-z_]\w*)\s*\{$/);
65
- if (compositeTypeMatch) {
66
- const bounds = findBlockBounds(context, lineIndex);
67
- const name = compositeTypeMatch[1] ?? "";
68
- if (name.length > 0) getOrCreateNamespace(currentNamespaceName, createTrimmedLineSpan(context, lineIndex)).compositeTypes.push(parseCompositeTypeBlock(context, name, bounds));
69
- lineIndex = bounds.endLine + 1;
70
- continue;
71
- }
72
- const namespaceMatch = line.match(/^namespace\s+([A-Za-z_]\w*)\s*\{$/);
73
- if (namespaceMatch) {
74
- const bounds = findBlockBounds(context, lineIndex);
75
- const name = namespaceMatch[1] ?? "";
76
- if (isInsideNamespace) pushDiagnostic(context, {
77
- code: "PSL_INVALID_NAMESPACE_BLOCK",
78
- message: `Recursive "namespace ${name}" block is not allowed; namespace blocks may not nest`,
79
- span: createTrimmedLineSpan(context, lineIndex)
80
- });
81
- else if (name === UNSPECIFIED_PSL_NAMESPACE_ID) pushDiagnostic(context, {
82
- code: "PSL_INVALID_NAMESPACE_BLOCK",
83
- message: `Namespace name "${UNSPECIFIED_PSL_NAMESPACE_ID}" is reserved for the parser-synthesised bucket for top-level declarations`,
84
- span: createTrimmedLineSpan(context, lineIndex)
85
- });
86
- else if (name.length > 0) {
87
- getOrCreateNamespace(name, createTrimmedLineSpan(context, lineIndex));
88
- parseBody(bounds.startLine + 1, bounds.endLine, name, true);
89
- }
90
- lineIndex = bounds.endLine + 1;
91
- continue;
92
- }
93
- if (/^types\s*\{$/.test(line)) {
94
- const bounds = findBlockBounds(context, lineIndex);
95
- if (isInsideNamespace) pushDiagnostic(context, {
96
- code: "PSL_INVALID_NAMESPACE_BLOCK",
97
- message: "`types` blocks must be declared at the document top level, not inside a namespace block",
98
- span: createTrimmedLineSpan(context, lineIndex)
99
- });
100
- else if (typesBlock !== void 0) pushDiagnostic(context, {
101
- code: "PSL_INVALID_TYPES_MEMBER",
102
- message: "Only one top-level `types` block is allowed per document",
103
- span: createTrimmedLineSpan(context, lineIndex)
104
- });
105
- else typesBlock = parseTypesBlock(context, bounds);
106
- lineIndex = bounds.endLine + 1;
107
- continue;
108
- }
109
- const extensionBlockMatch = line.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\{$/);
110
- if (extensionBlockMatch) {
111
- const keyword = extensionBlockMatch[1] ?? "";
112
- const blockName = extensionBlockMatch[2] ?? "";
113
- const descriptor = lookupExtensionBlockDescriptor(pslBlockNamespace, keyword);
114
- if (descriptor) {
115
- const bounds = findBlockBounds(context, lineIndex);
116
- if (blockName.length > 0) {
117
- const acc = getOrCreateNamespace(currentNamespaceName, createTrimmedLineSpan(context, lineIndex));
118
- const node = parseExtensionBlock(context, descriptor, blockName, lineIndex, bounds);
119
- acc.extensionBlocks.push(node);
120
- }
121
- lineIndex = bounds.endLine + 1;
122
- continue;
123
- }
124
- }
125
- if (line.includes("{")) {
126
- pushDiagnostic(context, {
127
- code: "PSL_UNSUPPORTED_TOP_LEVEL_BLOCK",
128
- message: `Unsupported top-level block "${line.split(/\s+/)[0] ?? "block"}"`,
129
- span: createTrimmedLineSpan(context, lineIndex)
130
- });
131
- lineIndex = findBlockBounds(context, lineIndex).endLine + 1;
132
- continue;
133
- }
134
- pushDiagnostic(context, {
135
- code: "PSL_UNSUPPORTED_TOP_LEVEL_BLOCK",
136
- message: `Unsupported top-level declaration "${line}"`,
137
- span: createTrimmedLineSpan(context, lineIndex)
138
- });
139
- lineIndex += 1;
140
- }
141
- };
142
- parseBody(0, lines.length, UNSPECIFIED_PSL_NAMESPACE_ID, false);
143
- const allModels = [];
144
- const allCompositeTypes = [];
145
- for (const name of namespaceOrder) {
146
- const acc = namespacesByName.get(name);
147
- if (!acc) continue;
148
- allModels.push(...acc.models);
149
- allCompositeTypes.push(...acc.compositeTypes);
150
- }
151
- const namedTypeNames = new Set((typesBlock?.declarations ?? []).map((declaration) => declaration.name));
152
- const modelNames = new Set(allModels.map((model) => model.name));
153
- const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
154
- for (const declaration of typesBlock?.declarations ?? []) {
155
- if (SCALAR_TYPES.has(declaration.name)) {
156
- pushDiagnostic(context, {
157
- code: "PSL_INVALID_TYPES_MEMBER",
158
- message: `Named type "${declaration.name}" conflicts with scalar type "${declaration.name}"`,
159
- span: declaration.span
160
- });
161
- continue;
162
- }
163
- if (modelNames.has(declaration.name)) pushDiagnostic(context, {
164
- code: "PSL_INVALID_TYPES_MEMBER",
165
- message: `Named type "${declaration.name}" conflicts with model name "${declaration.name}"`,
166
- span: declaration.span
167
- });
168
- }
169
- const documentSpan = {
170
- start: createPosition(context, 0, 0),
171
- end: createPosition(context, Math.max(lines.length - 1, 0), (lines[Math.max(lines.length - 1, 0)] ?? "").length)
172
- };
173
- const namespaces = [];
174
- for (const name of namespaceOrder) {
175
- const acc = namespacesByName.get(name);
176
- if (!acc) continue;
177
- if (name === UNSPECIFIED_PSL_NAMESPACE_ID && acc.models.length === 0 && acc.compositeTypes.length === 0 && acc.extensionBlocks.length === 0) continue;
178
- const normalizedModels = acc.models.map((model) => ({
179
- ...model,
180
- fields: model.fields.map((field) => {
181
- if (!namedTypeNames.has(field.typeName)) return field;
182
- if (field.attributes.some((attribute) => attribute.name === "relation") || modelNames.has(field.typeName) || compositeTypeNames.has(field.typeName) || SCALAR_TYPES.has(field.typeName)) return field;
183
- return {
184
- ...field,
185
- typeRef: field.typeName
186
- };
187
- })
188
- }));
189
- namespaces.push(makePslNamespace({
190
- kind: "namespace",
191
- name,
192
- entries: makePslNamespaceEntries(normalizedModels, acc.compositeTypes, acc.extensionBlocks),
193
- span: acc.span ?? documentSpan
194
- }));
195
- }
196
- if (Object.keys(pslBlockNamespace).length > 0) {
197
- const codecLookup = input.codecLookup ?? emptyCodecLookup;
198
- const descriptorByDiscriminator = /* @__PURE__ */ new Map();
199
- for (const value of Object.values(pslBlockNamespace)) if (isAuthoringPslBlockDescriptor(value)) descriptorByDiscriminator.set(value.discriminator, value);
200
- for (const ns of namespaces) for (const block of namespacePslExtensionBlocks(ns)) {
201
- const descriptor = descriptorByDiscriminator.get(block.kind);
202
- if (descriptor === void 0) continue;
203
- const blockDiagnostics = validateExtensionBlock(block, descriptor, input.sourceId, codecLookup, {
204
- ownerNamespace: ns,
205
- allNamespaces: namespaces
206
- });
207
- diagnostics.push(...blockDiagnostics);
208
- }
209
- }
210
- return {
211
- ast: {
212
- kind: "document",
213
- sourceId: input.sourceId,
214
- namespaces,
215
- ...ifDefined("types", typesBlock),
216
- span: documentSpan
217
- },
218
- diagnostics,
219
- ok: diagnostics.length === 0
220
- };
221
- }
222
- function parseModelBlock(context, name, bounds) {
223
- const fields = [];
224
- const attributes = [];
225
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
226
- const line = stripInlineComment(context.lines[lineIndex] ?? "").trim();
227
- if (line.length === 0) continue;
228
- if (line.startsWith("@@")) {
229
- const attribute = parseModelAttribute(context, line, lineIndex);
230
- if (attribute) attributes.push(attribute);
231
- continue;
232
- }
233
- const field = parseField(context, line, lineIndex);
234
- if (field) fields.push(field);
235
- }
236
- return {
237
- kind: "model",
238
- name,
239
- fields,
240
- attributes,
241
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine)
242
- };
243
- }
244
- function parseCompositeTypeBlock(context, name, bounds) {
245
- const fields = [];
246
- const attributes = [];
247
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
248
- const line = stripInlineComment(context.lines[lineIndex] ?? "").trim();
249
- if (line.length === 0) continue;
250
- if (line.startsWith("@@")) {
251
- const attribute = parseModelAttribute(context, line, lineIndex);
252
- if (attribute) attributes.push(attribute);
253
- continue;
254
- }
255
- const field = parseField(context, line, lineIndex);
256
- if (field) fields.push(field);
257
- }
258
- return {
259
- kind: "compositeType",
260
- name,
261
- fields,
262
- attributes,
263
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine)
264
- };
265
- }
266
- function parseTypesBlock(context, bounds) {
267
- const declarations = [];
268
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
269
- const raw = context.lines[lineIndex] ?? "";
270
- const line = stripInlineComment(raw).trim();
271
- if (line.length === 0) continue;
272
- const declarationMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
273
- if (!declarationMatch) {
274
- pushDiagnostic(context, {
275
- code: "PSL_INVALID_TYPES_MEMBER",
276
- message: `Invalid types declaration "${line}"`,
277
- span: createTrimmedLineSpan(context, lineIndex)
278
- });
279
- continue;
280
- }
281
- const declarationName = declarationMatch[1] ?? "";
282
- const trimmedStartColumn = firstNonWhitespaceColumn(raw);
283
- const declarationValue = (declarationMatch[2] ?? "").trim();
284
- const valueOffset = line.indexOf(declarationValue);
285
- const declarationValueColumn = trimmedStartColumn + Math.max(valueOffset, 0);
286
- const typeAndAttributeSplit = splitTypeAndAttributes(declarationValue);
287
- const typeSource = typeAndAttributeSplit.typeSource.trim();
288
- const attributeSource = typeAndAttributeSplit.attributeSource.trimStart();
289
- const leadingAttributeWhitespace = typeAndAttributeSplit.attributeSource.length - attributeSource.length;
290
- const typeConstructor = parseTypeConstructorCall(context, {
291
- declarationValue: typeSource,
292
- lineIndex,
293
- startColumn: declarationValueColumn,
294
- invalidCode: "PSL_INVALID_TYPES_MEMBER",
295
- invalidMessage: (value) => `Invalid types declaration "${value}"`
296
- });
297
- if (typeConstructor === "malformed") continue;
298
- const attributeParse = extractAttributeTokensWithSpans(context, lineIndex, attributeSource, declarationValueColumn + typeAndAttributeSplit.attributeOffset + leadingAttributeWhitespace);
299
- if (!attributeParse.ok) continue;
300
- const attributes = attributeParse.tokens.map((token) => parseAttributeToken(context, {
301
- token: token.text,
302
- target: "namedType",
303
- lineIndex,
304
- span: token.span
305
- })).filter((attribute) => Boolean(attribute));
306
- if (typeConstructor) {
307
- declarations.push({
308
- kind: "namedType",
309
- name: declarationName,
310
- typeConstructor,
311
- attributes,
312
- span: createTrimmedLineSpan(context, lineIndex)
313
- });
314
- continue;
315
- }
316
- const baseTypeMatch = typeSource.match(/^([A-Za-z_]\w*)$/);
317
- if (!baseTypeMatch) {
318
- pushDiagnostic(context, {
319
- code: "PSL_INVALID_TYPES_MEMBER",
320
- message: `Invalid types declaration "${line}"`,
321
- span: createTrimmedLineSpan(context, lineIndex)
322
- });
323
- continue;
324
- }
325
- const baseType = baseTypeMatch[1] ?? "";
326
- declarations.push({
327
- kind: "namedType",
328
- name: declarationName,
329
- baseType,
330
- attributes,
331
- span: createTrimmedLineSpan(context, lineIndex)
332
- });
333
- }
334
- return {
335
- kind: "types",
336
- declarations,
337
- span: createLineRangeSpan(context, bounds.startLine, bounds.endLine)
338
- };
339
- }
340
- function parseTypeConstructorCall(context, input) {
341
- const value = input.declarationValue.trim();
342
- const constructorMatch = value.match(/^([A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*)\s*\(/);
343
- if (!constructorMatch) return;
344
- const openParen = value.indexOf("(");
345
- const closeParen = value.lastIndexOf(")");
346
- if (closeParen !== value.length - 1) {
347
- pushDiagnostic(context, {
348
- code: input.invalidCode,
349
- message: input.invalidMessage(value),
350
- span: createInlineSpan(context, input.lineIndex, input.startColumn, input.startColumn + value.length)
351
- });
352
- return "malformed";
353
- }
354
- const constructorPath = constructorMatch[1] ?? "";
355
- const args = parseArgumentList(context, {
356
- argsRaw: value.slice(openParen + 1, closeParen),
357
- argsOffset: input.startColumn + openParen + 1,
358
- lineIndex: input.lineIndex,
359
- token: value,
360
- span: createInlineSpan(context, input.lineIndex, input.startColumn, input.startColumn + value.length),
361
- invalidCode: input.invalidCode,
362
- invalidEmptyArgumentMessage: `Invalid empty argument in type constructor "${value}"`,
363
- invalidNamedArgumentMessage: (part) => `Invalid named argument syntax "${part}" in type constructor "${value}"`
364
- });
365
- if (!args) return "malformed";
366
- return {
367
- kind: "typeConstructor",
368
- path: constructorPath.split("."),
369
- args,
370
- span: createInlineSpan(context, input.lineIndex, input.startColumn, input.startColumn + value.length)
371
- };
372
- }
373
- function parseModelAttribute(context, line, lineIndex) {
374
- const tokenParse = extractAttributeTokensWithSpans(context, lineIndex, line, firstNonWhitespaceColumn(context.lines[lineIndex] ?? ""));
375
- if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
376
- pushDiagnostic(context, {
377
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
378
- message: `Invalid model attribute syntax "${line}"`,
379
- span: createTrimmedLineSpan(context, lineIndex)
380
- });
381
- return;
382
- }
383
- const token = tokenParse.tokens[0];
384
- if (!token) return;
385
- return parseAttributeToken(context, {
386
- token: token.text,
387
- target: "model",
388
- lineIndex,
389
- span: token.span
390
- });
391
- }
392
- function parseField(context, line, lineIndex) {
393
- const fieldMatch = line.match(/^([A-Za-z_]\w*)(\s+)(.+)$/);
394
- if (!fieldMatch) {
395
- pushDiagnostic(context, {
396
- code: "PSL_INVALID_MODEL_MEMBER",
397
- message: `Invalid model member declaration "${line}"`,
398
- span: createTrimmedLineSpan(context, lineIndex)
399
- });
400
- return;
401
- }
402
- const fieldName = fieldMatch[1] ?? "";
403
- const separator = fieldMatch[2] ?? "";
404
- const typeAndAttributeSplit = splitTypeAndAttributes(fieldMatch[3] ?? "");
405
- const rawTypeSource = typeAndAttributeSplit.typeSource.trim();
406
- const attributePart = typeAndAttributeSplit.attributeSource;
407
- const optional = rawTypeSource.endsWith("?");
408
- const typeSourceWithoutOptional = optional ? rawTypeSource.slice(0, -1).trimEnd() : rawTypeSource;
409
- const list = typeSourceWithoutOptional.endsWith("[]");
410
- const baseTypeSource = list ? typeSourceWithoutOptional.slice(0, -2).trimEnd() : typeSourceWithoutOptional;
411
- const trimmedStartColumn = firstNonWhitespaceColumn(context.lines[lineIndex] ?? "");
412
- const typeStartColumn = trimmedStartColumn + fieldName.length + separator.length;
413
- const typeConstructor = parseTypeConstructorCall(context, {
414
- declarationValue: baseTypeSource,
415
- lineIndex,
416
- startColumn: typeStartColumn,
417
- invalidCode: "PSL_INVALID_MODEL_MEMBER",
418
- invalidMessage: (value) => `Invalid field type constructor "${value}"`
419
- });
420
- if (typeConstructor === "malformed") return;
421
- let typeName;
422
- let typeNamespaceId;
423
- let typeContractSpaceId;
424
- if (typeConstructor) typeName = typeConstructor.path.join(".");
425
- else {
426
- const colonCount = (baseTypeSource.match(/:/g) ?? []).length;
427
- if (colonCount > 1) {
428
- pushDiagnostic(context, {
429
- code: "PSL_INVALID_MODEL_MEMBER",
430
- message: `Invalid model member declaration "${line}"`,
431
- span: createTrimmedLineSpan(context, lineIndex)
432
- });
433
- return;
434
- }
435
- let typeRefSource;
436
- if (colonCount === 1) {
437
- const colonIndex = baseTypeSource.indexOf(":");
438
- const spaceCandidate = baseTypeSource.slice(0, colonIndex);
439
- typeRefSource = baseTypeSource.slice(colonIndex + 1);
440
- if (!/^[A-Za-z_]\w*$/.test(spaceCandidate)) {
441
- pushDiagnostic(context, {
442
- code: "PSL_INVALID_MODEL_MEMBER",
443
- message: `Invalid model member declaration "${line}"`,
444
- span: createTrimmedLineSpan(context, lineIndex)
445
- });
446
- return;
447
- }
448
- typeContractSpaceId = spaceCandidate;
449
- } else typeRefSource = baseTypeSource;
450
- if ((typeRefSource.match(/\./g) ?? []).length > 1) {
451
- pushDiagnostic(context, {
452
- code: "PSL_INVALID_QUALIFIED_TYPE",
453
- message: `Nested dot-qualified type "${baseTypeSource}" is not supported; use exactly one qualifier segment (e.g. "ns.TypeName")`,
454
- span: createInlineSpan(context, lineIndex, typeStartColumn, typeStartColumn + baseTypeSource.length)
455
- });
456
- return;
457
- }
458
- const singleMatch = typeRefSource.match(/^([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?$/);
459
- if (!singleMatch) {
460
- pushDiagnostic(context, {
461
- code: "PSL_INVALID_MODEL_MEMBER",
462
- message: `Invalid model member declaration "${line}"`,
463
- span: createTrimmedLineSpan(context, lineIndex)
464
- });
465
- return;
466
- }
467
- if (singleMatch[2] !== void 0) {
468
- typeNamespaceId = singleMatch[1];
469
- typeName = singleMatch[2];
470
- } else typeName = singleMatch[1] ?? "";
471
- }
472
- const attributes = [];
473
- const attributeSource = attributePart.trimStart();
474
- const leadingAttributeWhitespace = attributePart.length - attributeSource.length;
475
- const tokenParse = extractAttributeTokensWithSpans(context, lineIndex, attributeSource, trimmedStartColumn + fieldName.length + separator.length + typeAndAttributeSplit.attributeOffset + leadingAttributeWhitespace);
476
- if (!tokenParse.ok) return {
477
- kind: "field",
478
- name: fieldName,
479
- typeName,
480
- ...ifDefined("typeNamespaceId", typeNamespaceId),
481
- ...ifDefined("typeContractSpaceId", typeContractSpaceId),
482
- ...ifDefined("typeConstructor", typeConstructor),
483
- optional,
484
- list,
485
- attributes,
486
- span: createTrimmedLineSpan(context, lineIndex)
487
- };
488
- for (const token of tokenParse.tokens) {
489
- const parsed = parseAttributeToken(context, {
490
- token: token.text,
491
- target: "field",
492
- lineIndex,
493
- span: token.span
494
- });
495
- if (parsed) attributes.push(parsed);
496
- }
497
- return {
498
- kind: "field",
499
- name: fieldName,
500
- typeName,
501
- ...ifDefined("typeNamespaceId", typeNamespaceId),
502
- ...ifDefined("typeContractSpaceId", typeContractSpaceId),
503
- ...ifDefined("typeConstructor", typeConstructor),
504
- optional,
505
- list,
506
- attributes,
507
- span: createTrimmedLineSpan(context, lineIndex)
508
- };
509
- }
510
- function isQuoteEscaped(value, quoteIndex) {
511
- let backslashCount = 0;
512
- for (let index = quoteIndex - 1; index >= 0 && value[index] === "\\"; index -= 1) backslashCount += 1;
513
- return backslashCount % 2 === 1;
514
- }
515
- function splitTypeAndAttributes(value) {
516
- let depthParen = 0;
517
- let depthBracket = 0;
518
- let depthBrace = 0;
519
- let quote = null;
520
- for (let index = 0; index < value.length; index += 1) {
521
- const character = value[index] ?? "";
522
- if (quote) {
523
- if (character === quote && !isQuoteEscaped(value, index)) quote = null;
524
- continue;
525
- }
526
- if (character === "\"" || character === "'") {
527
- quote = character;
528
- continue;
529
- }
530
- if (character === "(") {
531
- depthParen += 1;
532
- continue;
533
- }
534
- if (character === ")") {
535
- depthParen = Math.max(0, depthParen - 1);
536
- continue;
537
- }
538
- if (character === "[") {
539
- depthBracket += 1;
540
- continue;
541
- }
542
- if (character === "]") {
543
- depthBracket = Math.max(0, depthBracket - 1);
544
- continue;
545
- }
546
- if (character === "{") {
547
- depthBrace += 1;
548
- continue;
549
- }
550
- if (character === "}") {
551
- depthBrace = Math.max(0, depthBrace - 1);
552
- continue;
553
- }
554
- if (character === "@" && depthParen === 0 && depthBracket === 0 && depthBrace === 0) return {
555
- typeSource: value.slice(0, index).trimEnd(),
556
- attributeSource: value.slice(index),
557
- attributeOffset: index
558
- };
559
- }
560
- return {
561
- typeSource: value.trimEnd(),
562
- attributeSource: "",
563
- attributeOffset: value.length
564
- };
565
- }
566
- function parseAttributeToken(context, input) {
567
- const expectsBlockPrefix = input.target === "model" || input.target === "enum";
568
- const targetLabel = input.target === "enum" ? "Enum" : "Model";
569
- if (expectsBlockPrefix && !input.token.startsWith("@@")) {
570
- pushDiagnostic(context, {
571
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
572
- message: `${targetLabel} attribute "${input.token}" must use @@ prefix`,
573
- span: input.span
574
- });
575
- return;
576
- }
577
- if (!expectsBlockPrefix && !input.token.startsWith("@")) {
578
- pushDiagnostic(context, {
579
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
580
- message: `Attribute "${input.token}" must use @ prefix`,
581
- span: input.span
582
- });
583
- return;
584
- }
585
- if (!expectsBlockPrefix && input.token.startsWith("@@")) {
586
- pushDiagnostic(context, {
587
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
588
- message: `Attribute "${input.token}" is not valid in ${input.target} context`,
589
- span: input.span
590
- });
591
- return;
592
- }
593
- const rawBody = expectsBlockPrefix ? input.token.slice(2) : input.token.slice(1);
594
- const openParen = rawBody.indexOf("(");
595
- const closeParen = rawBody.lastIndexOf(")");
596
- const hasArgs = openParen >= 0 || closeParen >= 0;
597
- if (openParen >= 0 && closeParen === -1 || openParen === -1 && closeParen >= 0) {
598
- pushDiagnostic(context, {
599
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
600
- message: `Invalid attribute syntax "${input.token}"`,
601
- span: input.span
602
- });
603
- return;
604
- }
605
- const name = (openParen >= 0 ? rawBody.slice(0, openParen) : rawBody).trim();
606
- if (!/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(name)) {
607
- pushDiagnostic(context, {
608
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
609
- message: `Invalid attribute name "${name || input.token}"`,
610
- span: input.span
611
- });
612
- return;
613
- }
614
- let args = [];
615
- if (hasArgs && openParen >= 0 && closeParen >= openParen) {
616
- if (closeParen !== rawBody.length - 1) {
617
- pushDiagnostic(context, {
618
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
619
- message: `Invalid trailing syntax in attribute "${input.token}"`,
620
- span: input.span
621
- });
622
- return;
623
- }
624
- const parsedArgs = parseArgumentList(context, {
625
- argsRaw: rawBody.slice(openParen + 1, closeParen),
626
- argsOffset: input.span.start.column - 1 + (expectsBlockPrefix ? 2 : 1) + openParen + 1,
627
- lineIndex: input.lineIndex,
628
- token: input.token,
629
- span: input.span,
630
- invalidCode: "PSL_INVALID_ATTRIBUTE_SYNTAX",
631
- invalidEmptyArgumentMessage: `Invalid empty argument in attribute "${input.token}"`,
632
- invalidNamedArgumentMessage: (part) => `Invalid named argument syntax "${part}"`
633
- });
634
- if (!parsedArgs) return;
635
- args = parsedArgs;
636
- }
637
- return {
638
- kind: "attribute",
639
- target: input.target,
640
- name,
641
- args,
642
- span: input.span
643
- };
644
- }
645
- function parseArgumentList(context, input) {
646
- if (input.argsRaw.trim().length === 0) return [];
647
- const parts = splitTopLevelSegments(input.argsRaw, ",");
648
- const args = [];
649
- for (const part of parts) {
650
- const original = part.value;
651
- const trimmedPart = original.trim();
652
- if (trimmedPart.length === 0) {
653
- pushDiagnostic(context, {
654
- code: input.invalidCode,
655
- message: input.invalidEmptyArgumentMessage,
656
- span: input.span
657
- });
658
- return;
659
- }
660
- const leadingWhitespace = original.length - original.trimStart().length;
661
- const partStart = input.argsOffset + part.start + leadingWhitespace;
662
- const partEnd = partStart + trimmedPart.length;
663
- const partSpan = createInlineSpan(context, input.lineIndex, partStart, partEnd);
664
- const namedSplit = splitTopLevelSegments(trimmedPart, ":");
665
- if (namedSplit.length > 1) {
666
- const first = namedSplit[0];
667
- if (!first) {
668
- pushDiagnostic(context, {
669
- code: input.invalidCode,
670
- message: input.invalidNamedArgumentMessage(trimmedPart),
671
- span: partSpan
672
- });
673
- return;
674
- }
675
- const name = first.value.trim();
676
- const rawValue = trimmedPart.slice(first.end + 1).trim();
677
- if (!name || rawValue.length === 0) {
678
- pushDiagnostic(context, {
679
- code: input.invalidCode,
680
- message: input.invalidNamedArgumentMessage(trimmedPart),
681
- span: partSpan
682
- });
683
- return;
684
- }
685
- args.push({
686
- kind: "named",
687
- name,
688
- value: normalizeAttributeArgumentValue(rawValue),
689
- span: partSpan
690
- });
691
- continue;
692
- }
693
- args.push({
694
- kind: "positional",
695
- value: normalizeAttributeArgumentValue(trimmedPart),
696
- span: partSpan
697
- });
698
- }
699
- return args;
700
- }
701
- function normalizeAttributeArgumentValue(value) {
702
- return value.trim();
703
- }
704
- function findBlockBounds(context, startLine) {
705
- let depth = 0;
706
- for (let lineIndex = startLine; lineIndex < context.lines.length; lineIndex += 1) {
707
- const line = stripInlineComment(context.lines[lineIndex] ?? "");
708
- let quote = null;
709
- for (let index = 0; index < line.length; index += 1) {
710
- const character = line[index] ?? "";
711
- if (quote) {
712
- if (character === quote && !isQuoteEscaped(line, index)) quote = null;
713
- continue;
714
- }
715
- if (character === "\"" || character === "'") {
716
- quote = character;
717
- continue;
718
- }
719
- if (character === "{") depth += 1;
720
- if (character === "}") {
721
- depth -= 1;
722
- if (depth === 0) return {
723
- startLine,
724
- endLine: lineIndex,
725
- closed: true
726
- };
727
- }
728
- }
729
- }
730
- pushDiagnostic(context, {
731
- code: "PSL_UNTERMINATED_BLOCK",
732
- message: "Unterminated block declaration",
733
- span: createTrimmedLineSpan(context, startLine)
734
- });
735
- return {
736
- startLine,
737
- endLine: context.lines.length - 1,
738
- closed: false
739
- };
740
- }
741
- function splitTopLevelSegments(value, separator) {
742
- const parts = [];
743
- let depthParen = 0;
744
- let depthBracket = 0;
745
- let depthBrace = 0;
746
- let quote = null;
747
- let start = 0;
748
- for (let index = 0; index < value.length; index += 1) {
749
- const character = value[index] ?? "";
750
- if (quote) {
751
- if (character === quote && !isQuoteEscaped(value, index)) quote = null;
752
- continue;
753
- }
754
- if (character === "\"" || character === "'") {
755
- quote = character;
756
- continue;
757
- }
758
- if (character === "(") {
759
- depthParen += 1;
760
- continue;
761
- }
762
- if (character === ")") {
763
- depthParen = Math.max(0, depthParen - 1);
764
- continue;
765
- }
766
- if (character === "[") {
767
- depthBracket += 1;
768
- continue;
769
- }
770
- if (character === "]") {
771
- depthBracket = Math.max(0, depthBracket - 1);
772
- continue;
773
- }
774
- if (character === "{") {
775
- depthBrace += 1;
776
- continue;
777
- }
778
- if (character === "}") {
779
- depthBrace = Math.max(0, depthBrace - 1);
780
- continue;
781
- }
782
- if (character === separator && depthParen === 0 && depthBracket === 0 && depthBrace === 0) {
783
- parts.push({
784
- value: value.slice(start, index),
785
- start,
786
- end: index
787
- });
788
- start = index + 1;
789
- }
790
- }
791
- parts.push({
792
- value: value.slice(start),
793
- start,
794
- end: value.length
795
- });
796
- return parts;
797
- }
798
- function extractAttributeTokensWithSpans(context, lineIndex, value, startColumn) {
799
- const tokens = [];
800
- let index = 0;
801
- while (index < value.length) {
802
- while (index < value.length && /\s/.test(value[index] ?? "")) index += 1;
803
- if (index >= value.length) break;
804
- if (value[index] !== "@") {
805
- pushDiagnostic(context, {
806
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
807
- message: `Invalid attribute syntax "${value.trim()}"`,
808
- span: createInlineSpan(context, lineIndex, startColumn + index, startColumn + value.length)
809
- });
810
- return {
811
- ok: false,
812
- tokens
813
- };
814
- }
815
- const start = index;
816
- index += 1;
817
- if (value[index] === "@") index += 1;
818
- const nameStart = index;
819
- while (index < value.length && /[A-Za-z0-9_.-]/.test(value[index] ?? "")) index += 1;
820
- if (index === nameStart) {
821
- pushDiagnostic(context, {
822
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
823
- message: `Invalid attribute syntax "${value.slice(start).trim()}"`,
824
- span: createInlineSpan(context, lineIndex, startColumn + start, startColumn + value.length)
825
- });
826
- return {
827
- ok: false,
828
- tokens
829
- };
830
- }
831
- if (value[index] === "(") {
832
- let depth = 0;
833
- let quote = null;
834
- while (index < value.length) {
835
- const char = value[index] ?? "";
836
- if (quote) {
837
- if (char === quote && !isQuoteEscaped(value, index)) quote = null;
838
- index += 1;
839
- continue;
840
- }
841
- if (char === "\"" || char === "'") {
842
- quote = char;
843
- index += 1;
844
- continue;
845
- }
846
- if (char === "(") depth += 1;
847
- else if (char === ")") {
848
- depth -= 1;
849
- if (depth === 0) {
850
- index += 1;
851
- break;
852
- }
853
- }
854
- index += 1;
855
- }
856
- if (depth !== 0) {
857
- pushDiagnostic(context, {
858
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
859
- message: `Unterminated attribute argument list in "${value.slice(start).trim()}"`,
860
- span: createInlineSpan(context, lineIndex, startColumn + start, startColumn + value.length)
861
- });
862
- return {
863
- ok: false,
864
- tokens
865
- };
866
- }
867
- }
868
- const tokenText = value.slice(start, index).trim();
869
- tokens.push({
870
- text: tokenText,
871
- span: createInlineSpan(context, lineIndex, startColumn + start, startColumn + index)
872
- });
873
- while (index < value.length && /\s/.test(value[index] ?? "")) index += 1;
874
- if (index < value.length && value[index] !== "@") break;
875
- }
876
- if (index < value.length && value[index] !== "@") {
877
- pushDiagnostic(context, {
878
- code: "PSL_INVALID_ATTRIBUTE_SYNTAX",
879
- message: `Invalid attribute syntax "${value.trim()}"`,
880
- span: createInlineSpan(context, lineIndex, startColumn + index, startColumn + value.length)
881
- });
882
- return {
883
- ok: false,
884
- tokens
885
- };
886
- }
887
- return {
888
- ok: true,
889
- tokens
890
- };
891
- }
892
- function stripInlineComment(line) {
893
- let quote = null;
894
- for (let index = 0; index < line.length - 1; index += 1) {
895
- const current = line[index] ?? "";
896
- const next = line[index + 1] ?? "";
897
- if (quote) {
898
- if (current === quote && !isQuoteEscaped(line, index)) quote = null;
899
- continue;
900
- }
901
- if (current === "\"" || current === "'") {
902
- quote = current;
903
- continue;
904
- }
905
- if (current === "/" && next === "/") return line.slice(0, index);
906
- }
907
- return line;
908
- }
909
- function computeLineOffsets(schema) {
910
- const offsets = [0];
911
- for (let index = 0; index < schema.length; index += 1) if (schema[index] === "\n") offsets.push(index + 1);
912
- return offsets;
913
- }
914
- function firstNonWhitespaceColumn(line) {
915
- const first = line.search(/\S/);
916
- return first === -1 ? 0 : first;
917
- }
918
- function createInlineSpan(context, lineIndex, startColumn, endColumn) {
919
- return {
920
- start: createPosition(context, lineIndex, startColumn),
921
- end: createPosition(context, lineIndex, endColumn)
922
- };
923
- }
924
- function createTrimmedLineSpan(context, lineIndex) {
925
- const line = context.lines[lineIndex] ?? "";
926
- return {
927
- start: createPosition(context, lineIndex, firstNonWhitespaceColumn(line)),
928
- end: createPosition(context, lineIndex, line.length)
929
- };
930
- }
931
- function createLineRangeSpan(context, startLine, endLine) {
932
- const startLineText = context.lines[startLine] ?? "";
933
- const endLineText = context.lines[endLine] ?? "";
934
- return {
935
- start: createPosition(context, startLine, firstNonWhitespaceColumn(startLineText)),
936
- end: createPosition(context, endLine, endLineText.length)
937
- };
938
- }
939
- function createPosition(context, lineIndex, columnIndex) {
940
- const clampedLineIndex = Math.max(0, Math.min(lineIndex, context.lineOffsets.length - 1));
941
- const lineText = context.lines[clampedLineIndex] ?? "";
942
- const clampedColumnIndex = Math.max(0, Math.min(columnIndex, lineText.length));
943
- return {
944
- offset: (context.lineOffsets[clampedLineIndex] ?? 0) + clampedColumnIndex,
945
- line: clampedLineIndex + 1,
946
- column: clampedColumnIndex + 1
947
- };
948
- }
949
- function pushDiagnostic(context, diagnostic) {
950
- context.diagnostics.push({
951
- ...diagnostic,
952
- sourceId: context.sourceId
953
- });
954
- }
955
- function lookupExtensionBlockDescriptor(namespace, keyword) {
956
- if (!Object.hasOwn(namespace, keyword)) return;
957
- const value = namespace[keyword];
958
- return isAuthoringPslBlockDescriptor(value) ? value : void 0;
959
- }
960
- /**
961
- * Reads an extension block body generically, driven by the descriptor's
962
- * `parameters` map. Each body line is one of:
963
- *
964
- * - `@@name(args…)` — a block attribute, captured in `blockAttributes`
965
- * - `key = <rhs>` — a parameter entry, captured in `parameters` per descriptor
966
- * - `key` — a bare-identifier entry (no `= value`), captured as `kind:'bare'`
967
- *
968
- * Duplicate parameter keys emit `PSL_EXTENSION_DUPLICATE_PARAMETER`; the first
969
- * occurrence wins. `@@` attribute lines are not validated here (name or args);
970
- * that is a concern of the consuming interpreter.
971
- */
972
- function parseExtensionBlock(context, descriptor, blockName, keywordLineIndex, bounds) {
973
- const parameters = {};
974
- const blockAttributes = [];
975
- for (let lineIndex = bounds.startLine + 1; lineIndex < bounds.endLine; lineIndex += 1) {
976
- const line = stripInlineComment(context.lines[lineIndex] ?? "").trim();
977
- if (line.length === 0) continue;
978
- if (line.startsWith("@@")) {
979
- const attr = parseExtensionBlockAttribute(context, line, lineIndex);
980
- if (attr !== void 0) blockAttributes.push(attr);
981
- continue;
982
- }
983
- const assignMatch = line.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
984
- if (assignMatch) {
985
- const key = assignMatch[1] ?? "";
986
- const rhsRaw = (assignMatch[2] ?? "").trim();
987
- if (Object.hasOwn(parameters, key)) {
988
- pushDiagnostic(context, {
989
- code: "PSL_EXTENSION_DUPLICATE_PARAMETER",
990
- message: `Duplicate parameter "${key}" in "${descriptor.keyword}" block "${blockName}"; first occurrence wins`,
991
- span: createTrimmedLineSpan(context, lineIndex)
992
- });
993
- continue;
994
- }
995
- const paramDescriptor = descriptor.parameters[key];
996
- if (paramDescriptor === void 0) parameters[key] = {
997
- kind: "value",
998
- raw: rhsRaw,
999
- span: createTrimmedLineSpan(context, lineIndex)
1000
- };
1001
- else {
1002
- const captured = captureParamRhs(context, lineIndex, rhsRaw, paramDescriptor);
1003
- if (captured !== void 0) parameters[key] = captured;
1004
- }
1005
- continue;
1006
- }
1007
- const bareMatch = line.match(/^([A-Za-z_]\w*)$/);
1008
- if (bareMatch) {
1009
- const key = bareMatch[1] ?? "";
1010
- if (!descriptor.variadicParameters) {
1011
- pushDiagnostic(context, {
1012
- code: "PSL_INVALID_EXTENSION_BLOCK_MEMBER",
1013
- message: `Invalid extension block body line "${line}"; expected "key = value" or "@@attribute"`,
1014
- span: createTrimmedLineSpan(context, lineIndex)
1015
- });
1016
- continue;
1017
- }
1018
- if (Object.hasOwn(descriptor.parameters, key)) {
1019
- pushDiagnostic(context, {
1020
- code: "PSL_INVALID_EXTENSION_BLOCK_MEMBER",
1021
- message: `Parameter "${key}" in "${descriptor.keyword}" block "${blockName}" is declared in the descriptor and must be supplied as "${key} = value"`,
1022
- span: createTrimmedLineSpan(context, lineIndex)
1023
- });
1024
- continue;
1025
- }
1026
- if (Object.hasOwn(parameters, key)) {
1027
- pushDiagnostic(context, {
1028
- code: "PSL_EXTENSION_DUPLICATE_PARAMETER",
1029
- message: `Duplicate parameter "${key}" in "${descriptor.keyword}" block "${blockName}"; first occurrence wins`,
1030
- span: createTrimmedLineSpan(context, lineIndex)
1031
- });
1032
- continue;
1033
- }
1034
- parameters[key] = {
1035
- kind: "bare",
1036
- span: createTrimmedLineSpan(context, lineIndex)
1037
- };
1038
- continue;
1039
- }
1040
- pushDiagnostic(context, {
1041
- code: "PSL_INVALID_EXTENSION_BLOCK_MEMBER",
1042
- message: `Invalid extension block body line "${line}"; expected "key = value", "key", or "@@attribute"`,
1043
- span: createTrimmedLineSpan(context, lineIndex)
1044
- });
1045
- }
1046
- return {
1047
- kind: descriptor.discriminator,
1048
- name: blockName,
1049
- parameters,
1050
- blockAttributes,
1051
- span: createLineRangeSpan(context, keywordLineIndex, bounds.endLine)
1052
- };
1053
- }
1054
- /**
1055
- * Parses a `@@name(args…)` block-attribute line inside an extension block
1056
- * using the shared block-attribute tokeniser (`extractAttributeTokensWithSpans`
1057
- * + `parseAttributeToken`). Returns undefined (and emits a diagnostic) on
1058
- * malformed syntax.
1059
- */
1060
- function parseExtensionBlockAttribute(context, line, lineIndex) {
1061
- const tokenParse = extractAttributeTokensWithSpans(context, lineIndex, line, firstNonWhitespaceColumn(context.lines[lineIndex] ?? ""));
1062
- if (!tokenParse.ok || tokenParse.tokens.length !== 1) {
1063
- pushDiagnostic(context, {
1064
- code: "PSL_INVALID_EXTENSION_BLOCK_ATTRIBUTE",
1065
- message: `Invalid extension block attribute "${line}"`,
1066
- span: createTrimmedLineSpan(context, lineIndex)
1067
- });
1068
- return;
1069
- }
1070
- const token = tokenParse.tokens[0];
1071
- if (!token) return;
1072
- const parsed = parseAttributeToken(context, {
1073
- token: token.text,
1074
- target: "model",
1075
- lineIndex,
1076
- span: token.span
1077
- });
1078
- if (!parsed) return;
1079
- const args = parsed.args.filter((a) => a.kind === "positional").map((a) => ({
1080
- kind: "positional",
1081
- value: a.value,
1082
- span: a.span
1083
- }));
1084
- return {
1085
- name: parsed.name,
1086
- args,
1087
- span: parsed.span
1088
- };
1089
- }
1090
- /**
1091
- * Captures a single parameter RHS string according to the declared parameter
1092
- * kind. Returns `undefined` and pushes a diagnostic only if the syntax is
1093
- * genuinely malformed (e.g. a `list` with no opening bracket). Value
1094
- * acceptance (codec, option-in-set, ref resolution) is handled by the validator.
1095
- */
1096
- function captureParamRhs(context, lineIndex, rhs, param) {
1097
- const span = createTrimmedLineSpan(context, lineIndex);
1098
- switch (param.kind) {
1099
- case "ref": return {
1100
- kind: "ref",
1101
- identifier: rhs,
1102
- span
1103
- };
1104
- case "value": return {
1105
- kind: "value",
1106
- raw: rhs,
1107
- span
1108
- };
1109
- case "option": return {
1110
- kind: "option",
1111
- token: rhs,
1112
- span
1113
- };
1114
- case "list": {
1115
- if (!rhs.startsWith("[") || !rhs.endsWith("]")) {
1116
- pushDiagnostic(context, {
1117
- code: "PSL_INVALID_EXTENSION_BLOCK_MEMBER",
1118
- message: `Expected a bracketed list "[…]" for list parameter, got "${rhs}"`,
1119
- span
1120
- });
1121
- return;
1122
- }
1123
- const inner = rhs.slice(1, -1).trim();
1124
- const items = [];
1125
- if (inner.length > 0) {
1126
- const segments = splitTopLevelSegments(inner, ",");
1127
- for (const segment of segments) {
1128
- const itemRhs = segment.value.trim();
1129
- if (itemRhs.length === 0) continue;
1130
- const item = captureParamRhs(context, lineIndex, itemRhs, param.of);
1131
- if (item !== void 0) items.push(item);
1132
- }
1133
- }
1134
- return {
1135
- kind: "list",
1136
- items,
1137
- span
1138
- };
1139
- }
1140
- }
1141
- }
1142
- //#endregion
1143
- export { parsePslDocument as t };
1144
-
1145
- //# sourceMappingURL=parser-CaplKvRs.mjs.map