@prisma-next/psl-parser 0.14.0 → 0.15.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/dist/declarations-DR6To8_k.mjs +1060 -0
- package/dist/declarations-DR6To8_k.mjs.map +1 -0
- package/dist/format.d.mts +19 -0
- package/dist/format.d.mts.map +1 -0
- package/dist/format.mjs +470 -0
- package/dist/format.mjs.map +1 -0
- package/dist/index.d.mts +144 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +849 -3
- package/dist/index.mjs.map +1 -1
- package/dist/interpret.d.mts +39 -0
- package/dist/interpret.d.mts.map +1 -0
- package/dist/interpret.mjs +25 -0
- package/dist/interpret.mjs.map +1 -0
- package/dist/parse-BazJr7Ye.d.mts +426 -0
- package/dist/parse-BazJr7Ye.d.mts.map +1 -0
- package/dist/parse-CdeXr0T3.mjs +626 -0
- package/dist/parse-CdeXr0T3.mjs.map +1 -0
- package/dist/symbol-table-C-AH04Ug.d.mts +127 -0
- package/dist/symbol-table-C-AH04Ug.d.mts.map +1 -0
- package/dist/syntax.d.mts +21 -347
- package/dist/syntax.d.mts.map +1 -1
- package/dist/syntax.mjs +26 -1401
- package/dist/syntax.mjs.map +1 -1
- package/package.json +11 -8
- package/src/attribute-spec/combinators/bool.ts +19 -0
- package/src/attribute-spec/combinators/diagnostic.ts +15 -0
- package/src/attribute-spec/combinators/entity-ref.ts +24 -0
- package/src/attribute-spec/combinators/field-ref.ts +36 -0
- package/src/attribute-spec/combinators/func-call.ts +65 -0
- package/src/attribute-spec/combinators/identifier.ts +16 -0
- package/src/attribute-spec/combinators/int.ts +35 -0
- package/src/attribute-spec/combinators/list.ts +43 -0
- package/src/attribute-spec/combinators/num.ts +26 -0
- package/src/attribute-spec/combinators/one-of.ts +29 -0
- package/src/attribute-spec/combinators/record.ts +43 -0
- package/src/attribute-spec/combinators/str.ts +19 -0
- package/src/attribute-spec/field-attribute.ts +27 -0
- package/src/attribute-spec/interpret.ts +177 -0
- package/src/attribute-spec/model-attribute.ts +27 -0
- package/src/attribute-spec/optional.ts +8 -0
- package/src/attribute-spec/types.ts +72 -0
- package/src/block-reconstruction.ts +140 -0
- package/src/exports/format.ts +3 -0
- package/src/exports/index.ts +61 -3
- package/src/exports/interpret.ts +2 -0
- package/src/exports/syntax.ts +25 -5
- package/src/extension-block.ts +107 -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/src/interpret.ts +67 -0
- package/src/parse.ts +32 -5
- package/src/resolve.ts +123 -0
- package/src/source-file.ts +25 -0
- package/src/symbol-table.ts +446 -0
- package/src/syntax/ast/attributes.ts +5 -6
- package/src/syntax/ast/declarations.ts +51 -26
- package/src/syntax/ast/expressions.ts +12 -13
- package/src/syntax/ast/identifier.ts +2 -3
- package/src/syntax/ast/qualified-name.ts +28 -19
- package/src/syntax/ast/type-annotation.ts +4 -5
- package/src/syntax/ast-helpers.ts +27 -3
- package/src/syntax/navigation.ts +55 -0
- package/src/syntax/red.ts +317 -42
- 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 -2
- package/dist/parser.mjs +0 -2
- package/src/exports/parser.ts +0 -1
- package/src/parser.ts +0 -1642
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthoringPslBlockDescriptor,
|
|
3
|
+
type AuthoringPslBlockDescriptorNamespace,
|
|
4
|
+
isAuthoringPslBlockDescriptor,
|
|
5
|
+
} from '@prisma-next/framework-components/authoring';
|
|
6
|
+
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
7
|
+
import {
|
|
8
|
+
makePslNamespace,
|
|
9
|
+
makePslNamespaceEntries,
|
|
10
|
+
type PslDiagnostic,
|
|
11
|
+
type PslModel,
|
|
12
|
+
type PslSpan,
|
|
13
|
+
UNSPECIFIED_PSL_NAMESPACE_ID,
|
|
14
|
+
validateExtensionBlock,
|
|
15
|
+
} from '@prisma-next/framework-components/psl-ast';
|
|
16
|
+
import type { SourceFile } from './source-file';
|
|
17
|
+
import type { BlockSymbol, ModelSymbol, SymbolTable } from './symbol-table';
|
|
18
|
+
|
|
19
|
+
export function findBlockDescriptor(
|
|
20
|
+
descriptors: AuthoringPslBlockDescriptorNamespace | undefined,
|
|
21
|
+
keyword: string,
|
|
22
|
+
): AuthoringPslBlockDescriptor | undefined {
|
|
23
|
+
if (descriptors === undefined) return undefined;
|
|
24
|
+
for (const value of Object.values(descriptors)) {
|
|
25
|
+
if (value === undefined) continue;
|
|
26
|
+
if (isAuthoringPslBlockDescriptor(value)) {
|
|
27
|
+
if (value.keyword === keyword) return value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const nested = findBlockDescriptor(value, keyword);
|
|
31
|
+
if (nested !== undefined) return nested;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateExtensionBlockFromSymbol(input: {
|
|
37
|
+
readonly block: BlockSymbol;
|
|
38
|
+
readonly descriptor: AuthoringPslBlockDescriptor;
|
|
39
|
+
readonly symbolTable: SymbolTable;
|
|
40
|
+
readonly sourceFile: SourceFile;
|
|
41
|
+
readonly sourceId: string;
|
|
42
|
+
readonly codecLookup: CodecLookup;
|
|
43
|
+
}): readonly PslDiagnostic[] {
|
|
44
|
+
const refCtx = buildRefResolutionContext(input.symbolTable, input.block);
|
|
45
|
+
return validateExtensionBlock(
|
|
46
|
+
input.block.block,
|
|
47
|
+
input.descriptor,
|
|
48
|
+
input.sourceId,
|
|
49
|
+
input.codecLookup,
|
|
50
|
+
refCtx,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ZERO_SPAN: PslSpan = {
|
|
55
|
+
start: { offset: 0, line: 1, column: 1 },
|
|
56
|
+
end: { offset: 0, line: 1, column: 1 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function buildRefResolutionContext(
|
|
60
|
+
symbolTable: SymbolTable,
|
|
61
|
+
block: BlockSymbol,
|
|
62
|
+
): {
|
|
63
|
+
ownerNamespace: ReturnType<typeof makePslNamespace>;
|
|
64
|
+
allNamespaces: readonly ReturnType<typeof makePslNamespace>[];
|
|
65
|
+
} {
|
|
66
|
+
const unspecifiedNamespace = makeNamespace(
|
|
67
|
+
UNSPECIFIED_PSL_NAMESPACE_ID,
|
|
68
|
+
Object.values(symbolTable.topLevel.models),
|
|
69
|
+
);
|
|
70
|
+
const namedNamespaces = Object.values(symbolTable.topLevel.namespaces).map((namespace) =>
|
|
71
|
+
makeNamespace(namespace.name, Object.values(namespace.models)),
|
|
72
|
+
);
|
|
73
|
+
const allNamespaces = [unspecifiedNamespace, ...namedNamespaces];
|
|
74
|
+
const ownerNamespaceName = findOwnerNamespaceName(symbolTable, block);
|
|
75
|
+
const ownerNamespace =
|
|
76
|
+
allNamespaces.find((namespace) => namespace.name === ownerNamespaceName) ??
|
|
77
|
+
unspecifiedNamespace;
|
|
78
|
+
return { ownerNamespace, allNamespaces };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeNamespace(
|
|
82
|
+
name: string,
|
|
83
|
+
models: readonly ModelSymbol[],
|
|
84
|
+
): ReturnType<typeof makePslNamespace> {
|
|
85
|
+
const modelStubs: PslModel[] = models.map((model) => ({
|
|
86
|
+
kind: 'model',
|
|
87
|
+
name: model.name,
|
|
88
|
+
fields: [],
|
|
89
|
+
attributes: [],
|
|
90
|
+
span: ZERO_SPAN,
|
|
91
|
+
}));
|
|
92
|
+
return makePslNamespace({
|
|
93
|
+
kind: 'namespace',
|
|
94
|
+
name,
|
|
95
|
+
entries: makePslNamespaceEntries(modelStubs, [], []),
|
|
96
|
+
span: ZERO_SPAN,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findOwnerNamespaceName(symbolTable: SymbolTable, block: BlockSymbol): string {
|
|
101
|
+
for (const namespace of Object.values(symbolTable.topLevel.namespaces)) {
|
|
102
|
+
if (Object.values(namespace.blocks).some((candidate) => candidate === block)) {
|
|
103
|
+
return namespace.name;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return UNSPECIFIED_PSL_NAMESPACE_ID;
|
|
107
|
+
}
|
|
@@ -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
|
+
}
|