@prisma-next/psl-parser 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{parser-Cw_zV0M5.mjs → parser-CaplKvRs.mjs} +110 -141
- package/dist/parser-CaplKvRs.mjs.map +1 -0
- package/dist/parser.mjs +1 -1
- package/dist/syntax.d.mts +115 -40
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +815 -105
- package/dist/syntax.mjs.map +1 -1
- package/dist/tokenizer-1hAHZzmp.mjs +228 -0
- package/dist/tokenizer-1hAHZzmp.mjs.map +1 -0
- package/dist/tokenizer.mjs +1 -190
- package/package.json +5 -5
- package/src/exports/index.ts +13 -2
- package/src/exports/syntax.ts +9 -4
- package/src/parse.ts +742 -0
- package/src/parser.ts +125 -196
- package/src/source-file.ts +89 -0
- package/src/syntax/ast/attributes.ts +5 -24
- package/src/syntax/ast/declarations.ts +14 -67
- package/src/syntax/ast/expressions.ts +187 -19
- package/src/syntax/ast/identifier.ts +4 -0
- package/src/syntax/ast/qualified-name.ts +87 -0
- package/src/syntax/ast/type-annotation.ts +11 -41
- package/src/syntax/ast-helpers.ts +12 -0
- package/src/syntax/syntax-kind.ts +8 -4
- package/src/tokenizer.ts +47 -7
- package/dist/parser-Cw_zV0M5.mjs.map +0 -1
- package/dist/tokenizer.mjs.map +0 -1
|
@@ -9,23 +9,20 @@ import { IdentifierAst } from './identifier';
|
|
|
9
9
|
import { TypeAnnotationAst } from './type-annotation';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* What may appear inside a `namespace` block: models,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* document-only, so they are not namespace members.
|
|
12
|
+
* What may appear inside a `namespace` block: models, composite types, and
|
|
13
|
+
* extension (block) declarations. `types {}` blocks and nested `namespace`
|
|
14
|
+
* blocks are document-only, so they are not namespace members.
|
|
16
15
|
*/
|
|
17
16
|
export type NamespaceMemberAst =
|
|
18
17
|
| ModelDeclarationAst
|
|
19
|
-
| EnumDeclarationAst
|
|
20
18
|
| CompositeTypeDeclarationAst
|
|
21
|
-
|
|
|
19
|
+
| GenericBlockDeclarationAst;
|
|
22
20
|
|
|
23
21
|
function castNamespaceMember(node: SyntaxNode): NamespaceMemberAst | undefined {
|
|
24
22
|
return (
|
|
25
23
|
ModelDeclarationAst.cast(node) ??
|
|
26
|
-
EnumDeclarationAst.cast(node) ??
|
|
27
24
|
CompositeTypeDeclarationAst.cast(node) ??
|
|
28
|
-
|
|
25
|
+
GenericBlockDeclarationAst.cast(node)
|
|
29
26
|
);
|
|
30
27
|
}
|
|
31
28
|
|
|
@@ -85,42 +82,6 @@ export class ModelDeclarationAst implements AstNode {
|
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
export class EnumDeclarationAst implements AstNode {
|
|
89
|
-
readonly syntax: SyntaxNode;
|
|
90
|
-
|
|
91
|
-
constructor(syntax: SyntaxNode) {
|
|
92
|
-
this.syntax = syntax;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
keyword(): Token | undefined {
|
|
96
|
-
return findChildToken(this.syntax, 'Ident');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
name(): IdentifierAst | undefined {
|
|
100
|
-
return findFirstChild(this.syntax, IdentifierAst.cast);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
lbrace(): Token | undefined {
|
|
104
|
-
return findChildToken(this.syntax, 'LBrace');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
rbrace(): Token | undefined {
|
|
108
|
-
return findChildToken(this.syntax, 'RBrace');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
*values(): Iterable<EnumValueDeclarationAst> {
|
|
112
|
-
yield* filterChildren(this.syntax, EnumValueDeclarationAst.cast);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
*attributes(): Iterable<ModelAttributeAst> {
|
|
116
|
-
yield* filterChildren(this.syntax, ModelAttributeAst.cast);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static cast(node: SyntaxNode): EnumDeclarationAst | undefined {
|
|
120
|
-
return node.kind === 'EnumDeclaration' ? new EnumDeclarationAst(node) : undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
85
|
export class CompositeTypeDeclarationAst implements AstNode {
|
|
125
86
|
readonly syntax: SyntaxNode;
|
|
126
87
|
|
|
@@ -219,7 +180,7 @@ export class TypesBlockAst implements AstNode {
|
|
|
219
180
|
}
|
|
220
181
|
}
|
|
221
182
|
|
|
222
|
-
export class
|
|
183
|
+
export class GenericBlockDeclarationAst implements AstNode {
|
|
223
184
|
readonly syntax: SyntaxNode;
|
|
224
185
|
|
|
225
186
|
constructor(syntax: SyntaxNode) {
|
|
@@ -246,8 +207,14 @@ export class BlockDeclarationAst implements AstNode {
|
|
|
246
207
|
yield* filterChildren(this.syntax, KeyValuePairAst.cast);
|
|
247
208
|
}
|
|
248
209
|
|
|
249
|
-
|
|
250
|
-
|
|
210
|
+
*attributes(): Iterable<ModelAttributeAst> {
|
|
211
|
+
yield* filterChildren(this.syntax, ModelAttributeAst.cast);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static cast(node: SyntaxNode): GenericBlockDeclarationAst | undefined {
|
|
215
|
+
return node.kind === 'GenericBlockDeclaration'
|
|
216
|
+
? new GenericBlockDeclarationAst(node)
|
|
217
|
+
: undefined;
|
|
251
218
|
}
|
|
252
219
|
}
|
|
253
220
|
|
|
@@ -310,26 +277,6 @@ export class FieldDeclarationAst implements AstNode {
|
|
|
310
277
|
}
|
|
311
278
|
}
|
|
312
279
|
|
|
313
|
-
export class EnumValueDeclarationAst implements AstNode {
|
|
314
|
-
readonly syntax: SyntaxNode;
|
|
315
|
-
|
|
316
|
-
constructor(syntax: SyntaxNode) {
|
|
317
|
-
this.syntax = syntax;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
name(): IdentifierAst | undefined {
|
|
321
|
-
return findFirstChild(this.syntax, IdentifierAst.cast);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
*attributes(): Iterable<FieldAttributeAst> {
|
|
325
|
-
yield* filterChildren(this.syntax, FieldAttributeAst.cast);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
static cast(node: SyntaxNode): EnumValueDeclarationAst | undefined {
|
|
329
|
-
return node.kind === 'EnumValueDeclaration' ? new EnumValueDeclarationAst(node) : undefined;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
280
|
export class NamedTypeDeclarationAst implements AstNode {
|
|
334
281
|
readonly syntax: SyntaxNode;
|
|
335
282
|
|
|
@@ -3,6 +3,7 @@ import type { AstNode } from '../ast-helpers';
|
|
|
3
3
|
import { filterChildren, findChildToken, findFirstChild } from '../ast-helpers';
|
|
4
4
|
import { SyntaxNode } from '../red';
|
|
5
5
|
import { IdentifierAst } from './identifier';
|
|
6
|
+
import { QualifiedNameAst } from './qualified-name';
|
|
6
7
|
|
|
7
8
|
export class FunctionCallAst implements AstNode {
|
|
8
9
|
readonly syntax: SyntaxNode;
|
|
@@ -11,8 +12,24 @@ export class FunctionCallAst implements AstNode {
|
|
|
11
12
|
this.syntax = syntax;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
name
|
|
15
|
-
|
|
15
|
+
/** The qualified-name callee, or `undefined` when identifier segments sit directly under the node. */
|
|
16
|
+
name(): QualifiedNameAst | undefined {
|
|
17
|
+
return findFirstChild(this.syntax, QualifiedNameAst.cast);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The dotted call path, in source order. A bare `Vector(…)` yields
|
|
22
|
+
* `['Vector']`; a namespace-qualified `pgvector.Vector(…)` yields
|
|
23
|
+
* `['pgvector', 'Vector']`. Empty when the call carries no identifier.
|
|
24
|
+
*/
|
|
25
|
+
path(): readonly string[] {
|
|
26
|
+
const qualified = this.name();
|
|
27
|
+
const segments: string[] = [];
|
|
28
|
+
for (const segment of filterChildren(qualified?.syntax ?? this.syntax, IdentifierAst.cast)) {
|
|
29
|
+
const text = segment.token()?.text;
|
|
30
|
+
if (text !== undefined) segments.push(text);
|
|
31
|
+
}
|
|
32
|
+
return segments;
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
lparen(): Token | undefined {
|
|
@@ -56,6 +73,82 @@ export class ArrayLiteralAst implements AstNode {
|
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
76
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
77
|
+
|
|
78
|
+
function decodeFixedHex(raw: string, start: number, width: number): string | undefined {
|
|
79
|
+
if (start + width > raw.length) return undefined;
|
|
80
|
+
const hex = raw.slice(start, start + width);
|
|
81
|
+
if (!HEX.test(hex)) return undefined;
|
|
82
|
+
return String.fromCharCode(Number.parseInt(hex, 16));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function decodeStringLiteral(raw: string): string {
|
|
86
|
+
let out = '';
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < raw.length) {
|
|
89
|
+
const ch = raw.charAt(i);
|
|
90
|
+
if (ch !== '\\' || i + 1 >= raw.length) {
|
|
91
|
+
out += ch;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const next = raw.charAt(i + 1);
|
|
96
|
+
switch (next) {
|
|
97
|
+
case 'n':
|
|
98
|
+
out += '\n';
|
|
99
|
+
i += 2;
|
|
100
|
+
continue;
|
|
101
|
+
case 'r':
|
|
102
|
+
out += '\r';
|
|
103
|
+
i += 2;
|
|
104
|
+
continue;
|
|
105
|
+
case 't':
|
|
106
|
+
out += '\t';
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
case '"':
|
|
110
|
+
out += '"';
|
|
111
|
+
i += 2;
|
|
112
|
+
continue;
|
|
113
|
+
case "'":
|
|
114
|
+
out += "'";
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
case '\\':
|
|
118
|
+
out += '\\';
|
|
119
|
+
i += 2;
|
|
120
|
+
continue;
|
|
121
|
+
case 'x': {
|
|
122
|
+
const decoded = decodeFixedHex(raw, i + 2, 2);
|
|
123
|
+
if (decoded === undefined) {
|
|
124
|
+
out += '\\x';
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
out += decoded;
|
|
129
|
+
i += 4;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
case 'u': {
|
|
133
|
+
const decoded = decodeFixedHex(raw, i + 2, 4);
|
|
134
|
+
if (decoded === undefined) {
|
|
135
|
+
out += '\\u';
|
|
136
|
+
i += 2;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
out += decoded;
|
|
140
|
+
i += 6;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
out += `\\${next}`;
|
|
145
|
+
i += 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
59
152
|
export class StringLiteralExprAst implements AstNode {
|
|
60
153
|
readonly syntax: SyntaxNode;
|
|
61
154
|
|
|
@@ -70,23 +163,7 @@ export class StringLiteralExprAst implements AstNode {
|
|
|
70
163
|
value(): string | undefined {
|
|
71
164
|
const tok = this.token();
|
|
72
165
|
if (!tok) return undefined;
|
|
73
|
-
|
|
74
|
-
return raw.replace(/\\(.)/g, (_match, char: string) => {
|
|
75
|
-
switch (char) {
|
|
76
|
-
case 'n':
|
|
77
|
-
return '\n';
|
|
78
|
-
case 'r':
|
|
79
|
-
return '\r';
|
|
80
|
-
case 't':
|
|
81
|
-
return '\t';
|
|
82
|
-
case '"':
|
|
83
|
-
return '"';
|
|
84
|
-
case '\\':
|
|
85
|
-
return '\\';
|
|
86
|
-
default:
|
|
87
|
-
return `\\${char}`;
|
|
88
|
-
}
|
|
89
|
-
});
|
|
166
|
+
return decodeStringLiteral(tok.text.slice(1, -1));
|
|
90
167
|
}
|
|
91
168
|
|
|
92
169
|
static cast(node: SyntaxNode): StringLiteralExprAst | undefined {
|
|
@@ -140,12 +217,102 @@ export class BooleanLiteralExprAst implements AstNode {
|
|
|
140
217
|
}
|
|
141
218
|
}
|
|
142
219
|
|
|
220
|
+
export class ObjectLiteralExprAst implements AstNode {
|
|
221
|
+
readonly syntax: SyntaxNode;
|
|
222
|
+
|
|
223
|
+
constructor(syntax: SyntaxNode) {
|
|
224
|
+
this.syntax = syntax;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lbrace(): Token | undefined {
|
|
228
|
+
return findChildToken(this.syntax, 'LBrace');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
rbrace(): Token | undefined {
|
|
232
|
+
return findChildToken(this.syntax, 'RBrace');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
*fields(): Iterable<ObjectFieldAst> {
|
|
236
|
+
yield* filterChildren(this.syntax, ObjectFieldAst.cast);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static cast(node: SyntaxNode): ObjectLiteralExprAst | undefined {
|
|
240
|
+
return node.kind === 'ObjectLiteralExpr' ? new ObjectLiteralExprAst(node) : undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class ObjectFieldAst implements AstNode {
|
|
245
|
+
readonly syntax: SyntaxNode;
|
|
246
|
+
|
|
247
|
+
constructor(syntax: SyntaxNode) {
|
|
248
|
+
this.syntax = syntax;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
key(): IdentifierAst | undefined {
|
|
252
|
+
for (const child of this.syntax.children()) {
|
|
253
|
+
if (!(child instanceof SyntaxNode)) {
|
|
254
|
+
if (child.kind === 'Colon') break;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
return IdentifierAst.cast(child);
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The field's logical key name, unquoted. An identifier key (`length:`) yields
|
|
264
|
+
* its text; a string-literal key (`"length":`) yields the decoded string.
|
|
265
|
+
* `undefined` when the field carries no key node.
|
|
266
|
+
*/
|
|
267
|
+
keyName(): string | undefined {
|
|
268
|
+
for (const child of this.syntax.children()) {
|
|
269
|
+
if (!(child instanceof SyntaxNode)) {
|
|
270
|
+
if (child.kind === 'Colon') break;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const identifier = IdentifierAst.cast(child);
|
|
274
|
+
if (identifier) return identifier.token()?.text;
|
|
275
|
+
const stringKey = StringLiteralExprAst.cast(child);
|
|
276
|
+
if (stringKey) return stringKey.value();
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
colon(): Token | undefined {
|
|
283
|
+
return findChildToken(this.syntax, 'Colon');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
value(): ExpressionAst | undefined {
|
|
287
|
+
if (this.colon()) {
|
|
288
|
+
let pastColon = false;
|
|
289
|
+
for (const child of this.syntax.children()) {
|
|
290
|
+
if (!(child instanceof SyntaxNode)) {
|
|
291
|
+
if (child.kind === 'Colon') pastColon = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (pastColon) {
|
|
295
|
+
const expr = castExpression(child);
|
|
296
|
+
if (expr) return expr;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
return findFirstChild(this.syntax, castExpression);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static cast(node: SyntaxNode): ObjectFieldAst | undefined {
|
|
305
|
+
return node.kind === 'ObjectField' ? new ObjectFieldAst(node) : undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
143
309
|
export type ExpressionAst =
|
|
144
310
|
| FunctionCallAst
|
|
145
311
|
| ArrayLiteralAst
|
|
146
312
|
| StringLiteralExprAst
|
|
147
313
|
| NumberLiteralExprAst
|
|
148
314
|
| BooleanLiteralExprAst
|
|
315
|
+
| ObjectLiteralExprAst
|
|
149
316
|
| IdentifierAst;
|
|
150
317
|
|
|
151
318
|
export function castExpression(node: SyntaxNode): ExpressionAst | undefined {
|
|
@@ -155,6 +322,7 @@ export function castExpression(node: SyntaxNode): ExpressionAst | undefined {
|
|
|
155
322
|
StringLiteralExprAst.cast(node) ??
|
|
156
323
|
NumberLiteralExprAst.cast(node) ??
|
|
157
324
|
BooleanLiteralExprAst.cast(node) ??
|
|
325
|
+
ObjectLiteralExprAst.cast(node) ??
|
|
158
326
|
IdentifierAst.cast(node)
|
|
159
327
|
);
|
|
160
328
|
}
|
|
@@ -14,6 +14,10 @@ export class IdentifierAst implements AstNode {
|
|
|
14
14
|
return findChildToken(this.syntax, 'Ident');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
name(): string | undefined {
|
|
18
|
+
return this.token()?.text;
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
static cast(node: SyntaxNode): IdentifierAst | undefined {
|
|
18
22
|
return node.kind === 'Identifier' ? new IdentifierAst(node) : undefined;
|
|
19
23
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Token } from '../../tokenizer';
|
|
2
|
+
import type { AstNode } from '../ast-helpers';
|
|
3
|
+
import { filterChildren, findChildToken, findFirstChild } from '../ast-helpers';
|
|
4
|
+
import { SyntaxNode } from '../red';
|
|
5
|
+
import { IdentifierAst } from './identifier';
|
|
6
|
+
|
|
7
|
+
/** A namespace-qualified name, e.g. `pgvector.Vector` or `supabase:auth.User`. */
|
|
8
|
+
export class QualifiedNameAst implements AstNode {
|
|
9
|
+
readonly syntax: SyntaxNode;
|
|
10
|
+
|
|
11
|
+
constructor(syntax: SyntaxNode) {
|
|
12
|
+
this.syntax = syntax;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#lastSegment(): IdentifierAst | undefined {
|
|
16
|
+
let last: IdentifierAst | undefined;
|
|
17
|
+
for (const segment of filterChildren(this.syntax, IdentifierAst.cast)) {
|
|
18
|
+
last = segment;
|
|
19
|
+
}
|
|
20
|
+
return last;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#penultimateSegment(): IdentifierAst | undefined {
|
|
24
|
+
let last: IdentifierAst | undefined;
|
|
25
|
+
let penultimate: IdentifierAst | undefined;
|
|
26
|
+
for (const segment of filterChildren(this.syntax, IdentifierAst.cast)) {
|
|
27
|
+
penultimate = last;
|
|
28
|
+
last = segment;
|
|
29
|
+
}
|
|
30
|
+
return penultimate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#separatorCount(kind: 'Dot' | 'Colon'): number {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const child of this.syntax.children()) {
|
|
36
|
+
if (!(child instanceof SyntaxNode) && child.kind === kind) count++;
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
colon(): Token | undefined {
|
|
42
|
+
return findChildToken(this.syntax, 'Colon');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dot(): Token | undefined {
|
|
46
|
+
return findChildToken(this.syntax, 'Dot');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
space(): IdentifierAst | undefined {
|
|
50
|
+
if (!this.colon()) return undefined;
|
|
51
|
+
return findFirstChild(this.syntax, IdentifierAst.cast);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
namespace(): IdentifierAst | undefined {
|
|
55
|
+
if (!this.dot()) return undefined;
|
|
56
|
+
return this.#penultimateSegment();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
identifier(): IdentifierAst | undefined {
|
|
60
|
+
return this.#lastSegment();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every identifier segment, in source order. A bare `Vector` yields
|
|
65
|
+
* `['Vector']`; a qualified `pgvector.Vector` yields `['pgvector', 'Vector']`.
|
|
66
|
+
*/
|
|
67
|
+
path(): readonly string[] {
|
|
68
|
+
const segments: string[] = [];
|
|
69
|
+
for (const segment of filterChildren(this.syntax, IdentifierAst.cast)) {
|
|
70
|
+
const text = segment.token()?.text;
|
|
71
|
+
if (text !== undefined) segments.push(text);
|
|
72
|
+
}
|
|
73
|
+
return segments;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Flags a malformed name with more qualifier segments than allowed (a second
|
|
78
|
+
* `:`-space or a second `.`-namespace).
|
|
79
|
+
*/
|
|
80
|
+
isOverQualified(): boolean {
|
|
81
|
+
return this.#separatorCount('Dot') > 1 || this.#separatorCount('Colon') > 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static cast(node: SyntaxNode): QualifiedNameAst | undefined {
|
|
85
|
+
return node.kind === 'QualifiedName' ? new QualifiedNameAst(node) : undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Token } from '../../tokenizer';
|
|
2
2
|
import type { AstNode } from '../ast-helpers';
|
|
3
|
-
import {
|
|
3
|
+
import { findChildToken, findFirstChild } from '../ast-helpers';
|
|
4
4
|
import type { SyntaxNode } from '../red';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { AttributeArgListAst } from './attributes';
|
|
6
|
+
import { QualifiedNameAst } from './qualified-name';
|
|
7
7
|
|
|
8
8
|
export class TypeAnnotationAst implements AstNode {
|
|
9
9
|
readonly syntax: SyntaxNode;
|
|
@@ -12,48 +12,18 @@ export class TypeAnnotationAst implements AstNode {
|
|
|
12
12
|
this.syntax = syntax;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
last = segment;
|
|
19
|
-
}
|
|
20
|
-
return last;
|
|
15
|
+
/** The annotation's reference, doubling as the constructor callee when an {@link argList} follows. */
|
|
16
|
+
name(): QualifiedNameAst | undefined {
|
|
17
|
+
return findFirstChild(this.syntax, QualifiedNameAst.cast);
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const segment of filterChildren(this.syntax, IdentifierAst.cast)) {
|
|
27
|
-
penultimate = last;
|
|
28
|
-
last = segment;
|
|
29
|
-
}
|
|
30
|
-
return penultimate;
|
|
20
|
+
/** Present when the annotation is a constructor (`Vector(1536)`) rather than a plain reference. */
|
|
21
|
+
argList(): AttributeArgListAst | undefined {
|
|
22
|
+
return findFirstChild(this.syntax, AttributeArgListAst.cast);
|
|
31
23
|
}
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
return this
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
colon(): Token | undefined {
|
|
38
|
-
return findChildToken(this.syntax, 'Colon');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
dot(): Token | undefined {
|
|
42
|
-
return findChildToken(this.syntax, 'Dot');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
spaceName(): IdentifierAst | undefined {
|
|
46
|
-
if (!this.colon()) return undefined;
|
|
47
|
-
return findFirstChild(this.syntax, IdentifierAst.cast);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
namespaceName(): IdentifierAst | undefined {
|
|
51
|
-
if (!this.dot()) return undefined;
|
|
52
|
-
return this.#penultimateSegment();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
constructorCall(): FunctionCallAst | undefined {
|
|
56
|
-
return findFirstChild(this.syntax, FunctionCallAst.cast);
|
|
25
|
+
isConstructor(): boolean {
|
|
26
|
+
return this.argList() !== undefined;
|
|
57
27
|
}
|
|
58
28
|
|
|
59
29
|
lbracket(): Token | undefined {
|
|
@@ -34,3 +34,15 @@ export function* filterChildren<T>(
|
|
|
34
34
|
if (result !== undefined) yield result;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Raw source text of a CST node, verbatim (quotes and brackets preserved). For
|
|
40
|
+
* the decoded value of a string literal, decode it instead.
|
|
41
|
+
*/
|
|
42
|
+
export function printSyntax(node: SyntaxNode): string {
|
|
43
|
+
let text = '';
|
|
44
|
+
for (const token of node.tokens()) {
|
|
45
|
+
text += token.text;
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export type SyntaxKind =
|
|
2
2
|
| 'Document'
|
|
3
3
|
| 'ModelDeclaration'
|
|
4
|
-
| 'EnumDeclaration'
|
|
5
4
|
| 'CompositeTypeDeclaration'
|
|
6
5
|
| 'Namespace'
|
|
7
6
|
| 'TypesBlock'
|
|
8
|
-
|
|
7
|
+
// The generic/extension block node: the `kw [name] { key = value }` form,
|
|
8
|
+
// distinct from the reserved `model`/`namespace`/`type`/`types` declarations.
|
|
9
|
+
| 'GenericBlockDeclaration'
|
|
9
10
|
| 'FieldDeclaration'
|
|
10
|
-
| 'EnumValueDeclaration'
|
|
11
11
|
| 'NamedTypeDeclaration'
|
|
12
12
|
| 'KeyValuePair'
|
|
13
13
|
| 'FieldAttribute'
|
|
@@ -16,8 +16,12 @@ export type SyntaxKind =
|
|
|
16
16
|
| 'AttributeArg'
|
|
17
17
|
| 'TypeAnnotation'
|
|
18
18
|
| 'Identifier'
|
|
19
|
+
// A namespace-qualified name `[space ':']? Ident ('.' Ident)*`.
|
|
20
|
+
| 'QualifiedName'
|
|
19
21
|
| 'FunctionCall'
|
|
20
22
|
| 'ArrayLiteral'
|
|
21
23
|
| 'StringLiteralExpr'
|
|
22
24
|
| 'NumberLiteralExpr'
|
|
23
|
-
| 'BooleanLiteralExpr'
|
|
25
|
+
| 'BooleanLiteralExpr'
|
|
26
|
+
| 'ObjectLiteralExpr'
|
|
27
|
+
| 'ObjectField';
|
package/src/tokenizer.ts
CHANGED
|
@@ -86,6 +86,7 @@ function scan(source: string, pos: number): Token {
|
|
|
86
86
|
scanWhitespace(source, pos) ??
|
|
87
87
|
scanComment(source, pos) ??
|
|
88
88
|
scanAt(source, pos) ??
|
|
89
|
+
scanKeywordNumber(source, pos) ??
|
|
89
90
|
scanIdent(source, pos) ??
|
|
90
91
|
scanNumber(source, pos) ??
|
|
91
92
|
scanString(source, pos) ??
|
|
@@ -151,6 +152,20 @@ function scanIdent(source: string, pos: number): Token | undefined {
|
|
|
151
152
|
return { kind: 'Ident', text: source.slice(pos, end) };
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
const KEYWORD_NUMBERS = ['-Infinity', 'Infinity', 'NaN'] as const;
|
|
156
|
+
|
|
157
|
+
// `NaN`, `Infinity`, and `-Infinity` are numeric literals; they must match
|
|
158
|
+
// before `scanIdent` and are word-bounded, so `Infinityx` stays an `Ident`.
|
|
159
|
+
function scanKeywordNumber(source: string, pos: number): Token | undefined {
|
|
160
|
+
for (const word of KEYWORD_NUMBERS) {
|
|
161
|
+
if (!source.startsWith(word, pos)) continue;
|
|
162
|
+
const after = readChar(source, pos + word.length);
|
|
163
|
+
if (after !== '' && isIdentPart(after)) return undefined;
|
|
164
|
+
return { kind: 'NumberLiteral', text: word };
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
154
169
|
function scanNumber(source: string, pos: number): Token | undefined {
|
|
155
170
|
let end = pos;
|
|
156
171
|
if (source.charAt(end) === '-') {
|
|
@@ -164,7 +179,7 @@ function scanNumber(source: string, pos: number): Token | undefined {
|
|
|
164
179
|
end++;
|
|
165
180
|
}
|
|
166
181
|
if (source.charAt(end) === '.' && end + 1 < source.length && isDigit(source.charAt(end + 1))) {
|
|
167
|
-
end++;
|
|
182
|
+
end++;
|
|
168
183
|
while (end < source.length && isDigit(source.charAt(end))) {
|
|
169
184
|
end++;
|
|
170
185
|
}
|
|
@@ -173,28 +188,53 @@ function scanNumber(source: string, pos: number): Token | undefined {
|
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
function scanString(source: string, pos: number): Token | undefined {
|
|
176
|
-
|
|
191
|
+
const quote = source.charAt(pos);
|
|
192
|
+
if (quote !== '"' && quote !== "'") return undefined;
|
|
177
193
|
let end = pos + 1;
|
|
178
194
|
while (end < source.length) {
|
|
179
195
|
const c = source.charAt(end);
|
|
180
196
|
if (c === '\\' && end + 1 < source.length) {
|
|
181
|
-
end += 2;
|
|
197
|
+
end += 2;
|
|
182
198
|
continue;
|
|
183
199
|
}
|
|
184
|
-
if (c ===
|
|
185
|
-
end++;
|
|
200
|
+
if (c === quote) {
|
|
201
|
+
end++;
|
|
186
202
|
return { kind: 'StringLiteral', text: source.slice(pos, end) };
|
|
187
203
|
}
|
|
188
204
|
if (c === '\n' || c === '\r') {
|
|
189
|
-
// Unterminated: stop before newline
|
|
205
|
+
// Unterminated string: stop before the newline.
|
|
190
206
|
return { kind: 'StringLiteral', text: source.slice(pos, end) };
|
|
191
207
|
}
|
|
192
208
|
end++;
|
|
193
209
|
}
|
|
194
|
-
// Unterminated at EOF
|
|
195
210
|
return { kind: 'StringLiteral', text: source.slice(pos, end) };
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/**
|
|
214
|
+
* `scanString` emits the same `StringLiteral` kind for well-formed and
|
|
215
|
+
* unterminated strings, so callers ask here to tell them apart.
|
|
216
|
+
*
|
|
217
|
+
* The closing quote is unescaped iff an **even** number of backslashes precede
|
|
218
|
+
* it (each `\\` pair cancels; an odd run leaves the final `\` escaping the
|
|
219
|
+
* quote), so counting the trailing backslash run suffices — no full re-scan:
|
|
220
|
+
*
|
|
221
|
+
* - `"ok"` → 0 backslashes (even) → terminated
|
|
222
|
+
* - `'a\'` → 1 backslash (odd) → the quote is escaped → unterminated
|
|
223
|
+
* - `"a\\"` → 2 backslashes (even) → escaped `\`, real `"` → terminated
|
|
224
|
+
*/
|
|
225
|
+
export function isTerminatedStringLiteral(text: string): boolean {
|
|
226
|
+
const quote = text.charAt(0);
|
|
227
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
228
|
+
if (text.length < 2 || text.charAt(text.length - 1) !== quote) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
let backslashes = 0;
|
|
232
|
+
for (let i = text.length - 2; i >= 1 && text.charAt(i) === '\\'; i--) {
|
|
233
|
+
backslashes++;
|
|
234
|
+
}
|
|
235
|
+
return backslashes % 2 === 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
198
238
|
function scanPunctuation(source: string, pos: number): Token | undefined {
|
|
199
239
|
const kind = PUNCTUATION[source.charAt(pos)];
|
|
200
240
|
if (kind === undefined) return undefined;
|