@prisma-next/psl-parser 0.14.0-dev.5 → 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.
- package/README.md +28 -10
- package/dist/declarations-D9h_ihD3.mjs +820 -0
- package/dist/declarations-D9h_ihD3.mjs.map +1 -0
- package/dist/format.mjs +2 -1
- package/dist/format.mjs.map +1 -1
- package/dist/index.d.mts +136 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +472 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{parse-B_3gIEFd.mjs → parse-DhEV6av6.mjs} +3 -819
- package/dist/parse-DhEV6av6.mjs.map +1 -0
- package/dist/syntax.mjs +2 -1
- package/package.json +9 -25
- package/src/block-reconstruction.ts +139 -0
- package/src/exports/index.ts +27 -3
- package/src/extension-block.ts +107 -0
- package/src/resolve.ts +120 -0
- package/src/symbol-table.ts +446 -0
- package/dist/parse-B_3gIEFd.mjs.map +0 -1
- package/dist/parser-CaplKvRs.mjs +0 -1145
- package/dist/parser-CaplKvRs.mjs.map +0 -1
- package/dist/parser-Dfi3Wfdq.d.mts +0 -7
- package/dist/parser-Dfi3Wfdq.d.mts.map +0 -1
- package/dist/parser.d.mts +0 -3
- package/dist/parser.mjs +0 -3
- package/src/exports/parser.ts +0 -4
- package/src/parser.ts +0 -1642
package/dist/parser-CaplKvRs.mjs
DELETED
|
@@ -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
|