@prisma-next/psl-parser 0.14.0-dev.3 → 0.14.0-dev.5
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/format.d.mts +19 -0
- package/dist/format.d.mts.map +1 -0
- package/dist/format.mjs +469 -0
- package/dist/format.mjs.map +1 -0
- package/dist/parse-B_3gIEFd.mjs +1421 -0
- package/dist/parse-B_3gIEFd.mjs.map +1 -0
- package/dist/parse-BjZ1LPe6.d.mts +349 -0
- package/dist/parse-BjZ1LPe6.d.mts.map +1 -0
- package/dist/parser.d.mts +2 -1
- package/dist/parser.mjs +2 -1
- package/dist/syntax.d.mts +2 -346
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +1 -1420
- package/package.json +25 -9
- package/src/exports/format.ts +3 -0
- package/src/exports/parser.ts +3 -0
- package/src/format/emit.ts +603 -0
- package/src/format/error.ts +13 -0
- package/src/format/format.ts +13 -0
- package/src/format/options.ts +28 -0
- package/dist/syntax.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/psl-parser",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Reusable parser for Prisma Schema Language (PSL)",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/framework-components": "0.14.0-dev.
|
|
10
|
-
"@prisma-next/utils": "0.14.0-dev.
|
|
9
|
+
"@prisma-next/framework-components": "0.14.0-dev.5",
|
|
10
|
+
"@prisma-next/utils": "0.14.0-dev.5"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@prisma-next/tsconfig": "0.14.0-dev.
|
|
14
|
-
"@prisma-next/tsdown": "0.14.0-dev.
|
|
13
|
+
"@prisma-next/tsconfig": "0.14.0-dev.5",
|
|
14
|
+
"@prisma-next/tsdown": "0.14.0-dev.5",
|
|
15
15
|
"tsdown": "0.22.1",
|
|
16
16
|
"typescript": "5.9.3",
|
|
17
17
|
"vitest": "4.1.8"
|
|
@@ -30,10 +30,26 @@
|
|
|
30
30
|
],
|
|
31
31
|
"types": "./dist/index.d.mts",
|
|
32
32
|
"exports": {
|
|
33
|
-
".":
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./format": {
|
|
38
|
+
"types": "./dist/format.d.mts",
|
|
39
|
+
"import": "./dist/format.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./parser": {
|
|
42
|
+
"types": "./dist/parser.d.mts",
|
|
43
|
+
"import": "./dist/parser.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./syntax": {
|
|
46
|
+
"types": "./dist/syntax.d.mts",
|
|
47
|
+
"import": "./dist/syntax.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./tokenizer": {
|
|
50
|
+
"types": "./dist/tokenizer.d.mts",
|
|
51
|
+
"import": "./dist/tokenizer.mjs"
|
|
52
|
+
},
|
|
37
53
|
"./package.json": "./package.json"
|
|
38
54
|
},
|
|
39
55
|
"engines": {
|
package/src/exports/parser.ts
CHANGED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { ModelAttributeAst } from '../syntax/ast/attributes';
|
|
2
|
+
import {
|
|
3
|
+
CompositeTypeDeclarationAst,
|
|
4
|
+
type DocumentAst,
|
|
5
|
+
FieldDeclarationAst,
|
|
6
|
+
GenericBlockDeclarationAst,
|
|
7
|
+
KeyValuePairAst,
|
|
8
|
+
ModelDeclarationAst,
|
|
9
|
+
NamedTypeDeclarationAst,
|
|
10
|
+
NamespaceDeclarationAst,
|
|
11
|
+
TypesBlockAst,
|
|
12
|
+
} from '../syntax/ast/declarations';
|
|
13
|
+
import { type SyntaxElement, SyntaxNode, type SyntaxToken } from '../syntax/red';
|
|
14
|
+
import type { TokenKind } from '../tokenizer';
|
|
15
|
+
|
|
16
|
+
export function emitDocument(document: DocumentAst, indentUnit: string, newline: string): string {
|
|
17
|
+
const writer = new LineWriter(indentUnit, newline);
|
|
18
|
+
emitTopLevel(writer, document);
|
|
19
|
+
return writer.finish();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class LineWriter {
|
|
23
|
+
readonly #indentUnit: string;
|
|
24
|
+
readonly #newline: string;
|
|
25
|
+
readonly #out: string[] = [];
|
|
26
|
+
#depth = 0;
|
|
27
|
+
#line = '';
|
|
28
|
+
#lineOpen = false;
|
|
29
|
+
#prevKind: TokenKind | undefined;
|
|
30
|
+
#lastWasBlank = false;
|
|
31
|
+
#hasContent = false;
|
|
32
|
+
|
|
33
|
+
constructor(indentUnit: string, newline: string) {
|
|
34
|
+
this.#indentUnit = indentUnit;
|
|
35
|
+
this.#newline = newline;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
indent(): void {
|
|
39
|
+
this.#depth += 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
unindent(): void {
|
|
43
|
+
this.#depth = Math.max(0, this.#depth - 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
lastIsBlank(): boolean {
|
|
47
|
+
return this.#lastWasBlank;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lineOpen(): boolean {
|
|
51
|
+
return this.#lineOpen;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
prevKind(): TokenKind | undefined {
|
|
55
|
+
return this.#prevKind;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
newline(): void {
|
|
59
|
+
if (!this.#lineOpen) return;
|
|
60
|
+
this.#out.push(`${this.#indentUnit.repeat(this.#depth)}${this.#line}`);
|
|
61
|
+
this.#line = '';
|
|
62
|
+
this.#lineOpen = false;
|
|
63
|
+
this.#prevKind = undefined;
|
|
64
|
+
this.#lastWasBlank = false;
|
|
65
|
+
this.#hasContent = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
blank(): void {
|
|
69
|
+
this.newline();
|
|
70
|
+
if (!this.#hasContent || this.#lastWasBlank) return;
|
|
71
|
+
this.#out.push('');
|
|
72
|
+
this.#lastWasBlank = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
write(token: SyntaxToken, space: boolean, padTo?: number): void {
|
|
76
|
+
if (this.#lineOpen && padTo !== undefined) {
|
|
77
|
+
this.#line = this.#line.padEnd(padTo);
|
|
78
|
+
} else if (this.#lineOpen && space) {
|
|
79
|
+
this.#line += ' ';
|
|
80
|
+
}
|
|
81
|
+
this.#line += token.text;
|
|
82
|
+
this.#lineOpen = true;
|
|
83
|
+
this.#prevKind = token.kind;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writeRaw(text: string): void {
|
|
87
|
+
this.#line += text;
|
|
88
|
+
this.#lineOpen = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
comment(text: string): void {
|
|
92
|
+
if (this.#lineOpen) this.#line += ` ${text}`;
|
|
93
|
+
else this.#line = text;
|
|
94
|
+
this.#lineOpen = true;
|
|
95
|
+
this.newline();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
finish(): string {
|
|
99
|
+
this.newline();
|
|
100
|
+
const body = this.#out.join(this.#newline);
|
|
101
|
+
return body.length > 0 ? `${body}${this.#newline}` : '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Qualified-name separators hug; argument/object colons keep the usual value space.
|
|
106
|
+
function spaceBetween(
|
|
107
|
+
prev: TokenKind | undefined,
|
|
108
|
+
cur: TokenKind,
|
|
109
|
+
inQualifiedName: boolean,
|
|
110
|
+
): boolean {
|
|
111
|
+
if (prev === undefined) return false;
|
|
112
|
+
if (inQualifiedName) return false;
|
|
113
|
+
|
|
114
|
+
switch (cur) {
|
|
115
|
+
case 'LParen':
|
|
116
|
+
case 'LBracket':
|
|
117
|
+
case 'RParen':
|
|
118
|
+
case 'RBracket':
|
|
119
|
+
case 'Comma':
|
|
120
|
+
case 'Question':
|
|
121
|
+
case 'Dot':
|
|
122
|
+
case 'Colon':
|
|
123
|
+
return false;
|
|
124
|
+
case 'RBrace':
|
|
125
|
+
return prev !== 'LBrace';
|
|
126
|
+
default:
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
switch (prev) {
|
|
130
|
+
case 'LParen':
|
|
131
|
+
case 'LBracket':
|
|
132
|
+
case 'Dot':
|
|
133
|
+
case 'At':
|
|
134
|
+
case 'DoubleAt':
|
|
135
|
+
return false;
|
|
136
|
+
default:
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function streamNode(writer: LineWriter, node: SyntaxNode, padTo?: number): number {
|
|
142
|
+
let continuation = 0;
|
|
143
|
+
let first = true;
|
|
144
|
+
let prevQualified = false;
|
|
145
|
+
|
|
146
|
+
const walk = (parent: SyntaxNode, qualified: boolean): void => {
|
|
147
|
+
for (const child of parent.children()) {
|
|
148
|
+
if (child instanceof SyntaxNode) {
|
|
149
|
+
walk(child, qualified || child.kind === 'QualifiedName');
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (child.kind === 'Whitespace' || child.kind === 'Newline') continue;
|
|
153
|
+
if (child.kind === 'Comment') {
|
|
154
|
+
writer.comment(child.text);
|
|
155
|
+
writer.indent();
|
|
156
|
+
continuation += 1;
|
|
157
|
+
prevQualified = false;
|
|
158
|
+
first = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const pad = first ? padTo : undefined;
|
|
162
|
+
const space = spaceBetween(writer.prevKind(), child.kind, qualified && prevQualified);
|
|
163
|
+
writer.write(child, space, writer.lineOpen() ? pad : undefined);
|
|
164
|
+
prevQualified = qualified;
|
|
165
|
+
first = false;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
walk(node, false);
|
|
170
|
+
return continuation;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function closeContinuation(writer: LineWriter, count: number): void {
|
|
174
|
+
for (let i = 0; i < count; i++) writer.unindent();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function emitField(
|
|
178
|
+
writer: LineWriter,
|
|
179
|
+
field: FieldDeclarationAst,
|
|
180
|
+
columns: AlignmentColumns | undefined,
|
|
181
|
+
): number {
|
|
182
|
+
return streamRow(writer, field.syntax, columns);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function emitNamedType(writer: LineWriter, decl: NamedTypeDeclarationAst): number {
|
|
186
|
+
return streamRow(writer, decl.syntax, undefined);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function streamRow(
|
|
190
|
+
writer: LineWriter,
|
|
191
|
+
row: SyntaxNode,
|
|
192
|
+
columns: AlignmentColumns | undefined,
|
|
193
|
+
): number {
|
|
194
|
+
let continuation = 0;
|
|
195
|
+
let sawAttribute = false;
|
|
196
|
+
|
|
197
|
+
for (const child of row.children()) {
|
|
198
|
+
if (child instanceof SyntaxNode) {
|
|
199
|
+
let padTo: number | undefined;
|
|
200
|
+
if (child.kind === 'TypeAnnotation' && continuation === 0) {
|
|
201
|
+
padTo = columns?.typeColumn;
|
|
202
|
+
} else if (child.kind === 'FieldAttribute') {
|
|
203
|
+
if (continuation > 0) writer.newline();
|
|
204
|
+
else if (!sawAttribute) padTo = columns?.attributeColumn;
|
|
205
|
+
sawAttribute = true;
|
|
206
|
+
}
|
|
207
|
+
continuation += streamNode(writer, child, padTo);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (child.kind === 'Whitespace' || child.kind === 'Newline') continue;
|
|
211
|
+
if (child.kind === 'Comment') {
|
|
212
|
+
writer.comment(child.text);
|
|
213
|
+
writer.indent();
|
|
214
|
+
continuation += 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const space = spaceBetween(writer.prevKind(), child.kind, false);
|
|
218
|
+
writer.write(child, space);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return continuation;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function emitBlockAttribute(writer: LineWriter, attribute: ModelAttributeAst): number {
|
|
225
|
+
return streamNode(writer, attribute.syntax);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function emitKeyValue(writer: LineWriter, pair: KeyValuePairAst): number {
|
|
229
|
+
return streamNode(writer, pair.syntax);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
type MemberCategory = 'regular' | 'blockAttribute' | 'nestedBlock';
|
|
233
|
+
|
|
234
|
+
interface BlockMember {
|
|
235
|
+
readonly category: MemberCategory;
|
|
236
|
+
emit(trailing: string | undefined): number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function leafMember(
|
|
240
|
+
writer: LineWriter,
|
|
241
|
+
category: MemberCategory,
|
|
242
|
+
print: () => number,
|
|
243
|
+
): BlockMember {
|
|
244
|
+
return {
|
|
245
|
+
category,
|
|
246
|
+
emit(trailing) {
|
|
247
|
+
const continuation = print();
|
|
248
|
+
if (trailing !== undefined) writer.comment(trailing);
|
|
249
|
+
else writer.newline();
|
|
250
|
+
return continuation;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
type MemberClassifier = (node: SyntaxNode) => BlockMember | undefined;
|
|
256
|
+
|
|
257
|
+
function emitModel(
|
|
258
|
+
writer: LineWriter,
|
|
259
|
+
model: ModelDeclarationAst,
|
|
260
|
+
trailing: string | undefined,
|
|
261
|
+
): void {
|
|
262
|
+
const columns = alignmentMap(model.syntax);
|
|
263
|
+
emitBlockBody(writer, model.syntax, trailing, (node) => {
|
|
264
|
+
const field = FieldDeclarationAst.cast(node);
|
|
265
|
+
if (field) return leafMember(writer, 'regular', () => emitField(writer, field, columns));
|
|
266
|
+
const attribute = ModelAttributeAst.cast(node);
|
|
267
|
+
if (attribute)
|
|
268
|
+
return leafMember(writer, 'blockAttribute', () => emitBlockAttribute(writer, attribute));
|
|
269
|
+
return undefined;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function emitCompositeType(
|
|
274
|
+
writer: LineWriter,
|
|
275
|
+
composite: CompositeTypeDeclarationAst,
|
|
276
|
+
trailing: string | undefined,
|
|
277
|
+
): void {
|
|
278
|
+
const columns = alignmentMap(composite.syntax);
|
|
279
|
+
emitBlockBody(writer, composite.syntax, trailing, (node) => {
|
|
280
|
+
const field = FieldDeclarationAst.cast(node);
|
|
281
|
+
if (field) return leafMember(writer, 'regular', () => emitField(writer, field, columns));
|
|
282
|
+
const attribute = ModelAttributeAst.cast(node);
|
|
283
|
+
if (attribute)
|
|
284
|
+
return leafMember(writer, 'blockAttribute', () => emitBlockAttribute(writer, attribute));
|
|
285
|
+
return undefined;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function emitGenericBlock(
|
|
290
|
+
writer: LineWriter,
|
|
291
|
+
block: GenericBlockDeclarationAst,
|
|
292
|
+
trailing: string | undefined,
|
|
293
|
+
): void {
|
|
294
|
+
emitBlockBody(writer, block.syntax, trailing, (node) => {
|
|
295
|
+
const entry = KeyValuePairAst.cast(node);
|
|
296
|
+
if (entry) return leafMember(writer, 'regular', () => emitKeyValue(writer, entry));
|
|
297
|
+
const attribute = ModelAttributeAst.cast(node);
|
|
298
|
+
if (attribute)
|
|
299
|
+
return leafMember(writer, 'blockAttribute', () => emitBlockAttribute(writer, attribute));
|
|
300
|
+
return undefined;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function emitNamespace(
|
|
305
|
+
writer: LineWriter,
|
|
306
|
+
namespace: NamespaceDeclarationAst,
|
|
307
|
+
trailing: string | undefined,
|
|
308
|
+
): void {
|
|
309
|
+
emitBlockBody(writer, namespace.syntax, trailing, (node) => {
|
|
310
|
+
const declaration = castBlockDeclaration(node);
|
|
311
|
+
if (declaration) return nestedBlockMember(writer, declaration);
|
|
312
|
+
return undefined;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function emitTypesBlock(
|
|
317
|
+
writer: LineWriter,
|
|
318
|
+
block: TypesBlockAst,
|
|
319
|
+
trailing: string | undefined,
|
|
320
|
+
): void {
|
|
321
|
+
emitBlockBody(writer, block.syntax, trailing, (node) => {
|
|
322
|
+
const named = NamedTypeDeclarationAst.cast(node);
|
|
323
|
+
if (named) return leafMember(writer, 'regular', () => emitNamedType(writer, named));
|
|
324
|
+
return undefined;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function emitTopLevel(writer: LineWriter, document: DocumentAst): void {
|
|
329
|
+
walkRegion(writer, Array.from(document.syntax.children()), undefined, (node) => {
|
|
330
|
+
const declaration = castTopLevelDeclaration(node);
|
|
331
|
+
if (declaration) return nestedBlockMember(writer, declaration);
|
|
332
|
+
return undefined;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
type BlockEmitter = (writer: LineWriter, trailing: string | undefined) => void;
|
|
337
|
+
|
|
338
|
+
function nestedBlockMember(writer: LineWriter, block: BlockEmitter): BlockMember {
|
|
339
|
+
return {
|
|
340
|
+
category: 'nestedBlock',
|
|
341
|
+
emit(trailing) {
|
|
342
|
+
block(writer, trailing);
|
|
343
|
+
return 0;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function castBlockDeclaration(node: SyntaxNode): BlockEmitter | undefined {
|
|
349
|
+
const model = ModelDeclarationAst.cast(node);
|
|
350
|
+
if (model) return (writer, trailing) => emitModel(writer, model, trailing);
|
|
351
|
+
const composite = CompositeTypeDeclarationAst.cast(node);
|
|
352
|
+
if (composite) return (writer, trailing) => emitCompositeType(writer, composite, trailing);
|
|
353
|
+
const generic = GenericBlockDeclarationAst.cast(node);
|
|
354
|
+
if (generic) return (writer, trailing) => emitGenericBlock(writer, generic, trailing);
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function castTopLevelDeclaration(node: SyntaxNode): BlockEmitter | undefined {
|
|
359
|
+
const block = castBlockDeclaration(node);
|
|
360
|
+
if (block) return block;
|
|
361
|
+
const namespace = NamespaceDeclarationAst.cast(node);
|
|
362
|
+
if (namespace) return (writer, trailing) => emitNamespace(writer, namespace, trailing);
|
|
363
|
+
const types = TypesBlockAst.cast(node);
|
|
364
|
+
if (types) return (writer, trailing) => emitTypesBlock(writer, types, trailing);
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function emitBlockBody(
|
|
369
|
+
writer: LineWriter,
|
|
370
|
+
node: SyntaxNode,
|
|
371
|
+
closingTrailing: string | undefined,
|
|
372
|
+
classify: MemberClassifier,
|
|
373
|
+
): void {
|
|
374
|
+
const children = Array.from(node.children());
|
|
375
|
+
const openIndex = children.findIndex((el) => !(el instanceof SyntaxNode) && el.kind === 'LBrace');
|
|
376
|
+
|
|
377
|
+
streamHeader(writer, node);
|
|
378
|
+
const headerComment = sameLineCommentAfter(children, openIndex);
|
|
379
|
+
if (headerComment !== undefined) writer.comment(headerComment);
|
|
380
|
+
else writer.newline();
|
|
381
|
+
|
|
382
|
+
writer.indent();
|
|
383
|
+
walkRegion(writer, children, 'RBrace', classify);
|
|
384
|
+
writer.unindent();
|
|
385
|
+
|
|
386
|
+
writer.writeRaw('}');
|
|
387
|
+
if (closingTrailing !== undefined) writer.comment(closingTrailing);
|
|
388
|
+
else writer.newline();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function streamHeader(writer: LineWriter, node: SyntaxNode): void {
|
|
392
|
+
let done = false;
|
|
393
|
+
const walk = (parent: SyntaxNode): void => {
|
|
394
|
+
for (const child of parent.children()) {
|
|
395
|
+
if (done) return;
|
|
396
|
+
if (child instanceof SyntaxNode) {
|
|
397
|
+
walk(child);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (child.kind === 'Whitespace' || child.kind === 'Newline' || child.kind === 'Comment') {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const space = spaceBetween(writer.prevKind(), child.kind, false);
|
|
404
|
+
writer.write(child, space);
|
|
405
|
+
if (child.kind === 'LBrace') {
|
|
406
|
+
done = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
walk(node);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function walkRegion(
|
|
415
|
+
writer: LineWriter,
|
|
416
|
+
elements: readonly SyntaxElement[],
|
|
417
|
+
closeKind: 'RBrace' | undefined,
|
|
418
|
+
classify: MemberClassifier,
|
|
419
|
+
): void {
|
|
420
|
+
let sawOpenBrace = closeKind === undefined;
|
|
421
|
+
let sawContent = false;
|
|
422
|
+
let lastWasRegular = false;
|
|
423
|
+
let ledByComment = false;
|
|
424
|
+
let newlines = 0;
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < elements.length; i++) {
|
|
427
|
+
const element = elements[i];
|
|
428
|
+
if (element === undefined) continue;
|
|
429
|
+
|
|
430
|
+
if (element instanceof SyntaxNode) {
|
|
431
|
+
if (!sawOpenBrace) continue;
|
|
432
|
+
const member = classify(element);
|
|
433
|
+
if (member === undefined) continue;
|
|
434
|
+
if (!ledByComment) {
|
|
435
|
+
if (newlines >= 2 && sawContent && !writer.lastIsBlank()) writer.blank();
|
|
436
|
+
else if (separationBlankWanted(writer, member.category, sawContent, lastWasRegular)) {
|
|
437
|
+
writer.blank();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const trailing = sameLineTrailingComment(elements, i);
|
|
442
|
+
closeContinuation(writer, member.emit(trailing.text));
|
|
443
|
+
if (trailing.index !== undefined) i = trailing.index;
|
|
444
|
+
sawContent = true;
|
|
445
|
+
lastWasRegular = member.category !== 'blockAttribute';
|
|
446
|
+
ledByComment = false;
|
|
447
|
+
newlines = 0;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (element.kind === 'LBrace' && closeKind === 'RBrace' && !sawOpenBrace) {
|
|
452
|
+
sawOpenBrace = true;
|
|
453
|
+
newlines = 0;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (!sawOpenBrace) continue;
|
|
457
|
+
if (closeKind === 'RBrace' && element.kind === 'RBrace') break;
|
|
458
|
+
if (element.kind === 'Whitespace') continue;
|
|
459
|
+
if (element.kind === 'Newline') {
|
|
460
|
+
newlines += 1;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (element.kind === 'Comment') {
|
|
464
|
+
if (closeKind === 'RBrace' && newlines === 0 && !sawContent) {
|
|
465
|
+
// Same-line comment trailing the opening `{`: owned by the block header.
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (newlines >= 2 && sawContent && !writer.lastIsBlank()) writer.blank();
|
|
469
|
+
else if (!ledByComment) {
|
|
470
|
+
const led = leadingMemberAfter(elements, i, classify);
|
|
471
|
+
if (led && separationBlankWanted(writer, led, sawContent, lastWasRegular)) writer.blank();
|
|
472
|
+
}
|
|
473
|
+
writer.writeRaw(element.text);
|
|
474
|
+
writer.newline();
|
|
475
|
+
sawContent = true;
|
|
476
|
+
ledByComment = true;
|
|
477
|
+
newlines = 0;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function separationBlankWanted(
|
|
483
|
+
writer: LineWriter,
|
|
484
|
+
category: MemberCategory,
|
|
485
|
+
sawContent: boolean,
|
|
486
|
+
lastWasRegular: boolean,
|
|
487
|
+
): boolean {
|
|
488
|
+
if (!sawContent || writer.lastIsBlank()) return false;
|
|
489
|
+
if (category === 'nestedBlock') return true;
|
|
490
|
+
return category === 'blockAttribute' && lastWasRegular;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function leadingMemberAfter(
|
|
494
|
+
elements: readonly SyntaxElement[],
|
|
495
|
+
commentIndex: number,
|
|
496
|
+
classify: MemberClassifier,
|
|
497
|
+
): MemberCategory | undefined {
|
|
498
|
+
for (let i = commentIndex + 1; i < elements.length; i++) {
|
|
499
|
+
const element = elements[i];
|
|
500
|
+
if (element === undefined) continue;
|
|
501
|
+
if (element instanceof SyntaxNode) return classify(element)?.category;
|
|
502
|
+
if (element.kind === 'RBrace') return undefined;
|
|
503
|
+
}
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function sameLineTrailingComment(
|
|
508
|
+
elements: readonly SyntaxElement[],
|
|
509
|
+
memberIndex: number,
|
|
510
|
+
): { text: string | undefined; index: number | undefined } {
|
|
511
|
+
for (let i = memberIndex + 1; i < elements.length; i++) {
|
|
512
|
+
const element = elements[i];
|
|
513
|
+
if (element === undefined) continue;
|
|
514
|
+
if (element instanceof SyntaxNode) break;
|
|
515
|
+
if (element.kind === 'Whitespace') continue;
|
|
516
|
+
if (element.kind === 'Comment') return { text: element.text, index: i };
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
return { text: undefined, index: undefined };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function sameLineCommentAfter(
|
|
523
|
+
children: readonly SyntaxElement[],
|
|
524
|
+
openIndex: number,
|
|
525
|
+
): string | undefined {
|
|
526
|
+
for (let i = openIndex + 1; i < children.length; i++) {
|
|
527
|
+
const child = children[i];
|
|
528
|
+
if (child === undefined) continue;
|
|
529
|
+
if (child instanceof SyntaxNode) return undefined;
|
|
530
|
+
if (child.kind === 'Whitespace') continue;
|
|
531
|
+
if (child.kind === 'Comment') return child.text;
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
interface AlignmentColumns {
|
|
538
|
+
readonly typeColumn: number;
|
|
539
|
+
readonly attributeColumn: number;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function alignmentMap(block: SyntaxNode): AlignmentColumns | undefined {
|
|
543
|
+
const fields: SyntaxNode[] = [];
|
|
544
|
+
for (const element of block.children()) {
|
|
545
|
+
if (!(element instanceof SyntaxNode)) continue;
|
|
546
|
+
if (FieldDeclarationAst.cast(element) === undefined) continue;
|
|
547
|
+
// Interior comments split rows into continuation lines, so those rows opt out of alignment.
|
|
548
|
+
if (hasInteriorComment(element)) continue;
|
|
549
|
+
fields.push(element);
|
|
550
|
+
}
|
|
551
|
+
if (fields.length === 0) return undefined;
|
|
552
|
+
return alignmentColumns(fields);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function alignmentColumns(rows: readonly SyntaxNode[]): AlignmentColumns {
|
|
556
|
+
let nameWidth = 0;
|
|
557
|
+
for (const row of rows) {
|
|
558
|
+
const field = FieldDeclarationAst.cast(row);
|
|
559
|
+
if (!field) continue;
|
|
560
|
+
nameWidth = Math.max(nameWidth, renderTokens(field.name()?.syntax).length);
|
|
561
|
+
}
|
|
562
|
+
const typeColumn = nameWidth + 1;
|
|
563
|
+
let cellEnd = 0;
|
|
564
|
+
for (const row of rows) {
|
|
565
|
+
const field = FieldDeclarationAst.cast(row);
|
|
566
|
+
if (!field) continue;
|
|
567
|
+
const name = renderTokens(field.name()?.syntax);
|
|
568
|
+
const type = renderTokens(field.typeAnnotation()?.syntax);
|
|
569
|
+
cellEnd = Math.max(cellEnd, type.length > 0 ? typeColumn + type.length : name.length);
|
|
570
|
+
}
|
|
571
|
+
return { typeColumn, attributeColumn: cellEnd + 1 };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function hasInteriorComment(node: SyntaxNode): boolean {
|
|
575
|
+
for (const token of node.tokens()) {
|
|
576
|
+
if (token.kind === 'Comment') return true;
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderTokens(node: SyntaxNode | undefined): string {
|
|
582
|
+
if (!node) return '';
|
|
583
|
+
let out = '';
|
|
584
|
+
let prev: TokenKind | undefined;
|
|
585
|
+
let prevQualified = false;
|
|
586
|
+
const walk = (parent: SyntaxNode, qualified: boolean): void => {
|
|
587
|
+
for (const child of parent.children()) {
|
|
588
|
+
if (child instanceof SyntaxNode) {
|
|
589
|
+
walk(child, qualified || child.kind === 'QualifiedName');
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (child.kind === 'Whitespace' || child.kind === 'Newline' || child.kind === 'Comment') {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (spaceBetween(prev, child.kind, qualified && prevQualified)) out += ' ';
|
|
596
|
+
out += child.text;
|
|
597
|
+
prev = child.kind;
|
|
598
|
+
prevQualified = qualified;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
walk(node, false);
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParseDiagnostic } from '../parse';
|
|
2
|
+
|
|
3
|
+
export class PslFormatError extends Error {
|
|
4
|
+
readonly diagnostics: readonly ParseDiagnostic[];
|
|
5
|
+
|
|
6
|
+
constructor(diagnostics: readonly ParseDiagnostic[]) {
|
|
7
|
+
const summary = diagnostics[0]?.message ?? 'unknown parse error';
|
|
8
|
+
const more = diagnostics.length > 1 ? ` (and ${diagnostics.length - 1} more)` : '';
|
|
9
|
+
super(`Cannot format PSL with parse errors: ${summary}${more}`);
|
|
10
|
+
this.name = 'PslFormatError';
|
|
11
|
+
this.diagnostics = diagnostics;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parse } from '../parse';
|
|
2
|
+
import { emitDocument } from './emit';
|
|
3
|
+
import { PslFormatError } from './error';
|
|
4
|
+
import { type FormatOptions, resolveFormatOptions } from './options';
|
|
5
|
+
|
|
6
|
+
export function format(source: string, options?: FormatOptions): string {
|
|
7
|
+
const resolved = resolveFormatOptions(options);
|
|
8
|
+
const { document, diagnostics } = parse(source);
|
|
9
|
+
if (diagnostics.length > 0) {
|
|
10
|
+
throw new PslFormatError(diagnostics);
|
|
11
|
+
}
|
|
12
|
+
return emitDocument(document, resolved.indentUnit, resolved.newline);
|
|
13
|
+
}
|