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