@syncular/typegen 0.5.0 → 0.6.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/README.md +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +359 -185
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +115 -63
- package/dist/query.js +60 -11
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
package/src/fmt.ts
CHANGED
|
@@ -1,137 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `syncular fmt` — the canonical `.syql` formatter (DESIGN-queries.md §10,
|
|
3
|
-
* §12). One style, no options:
|
|
1
|
+
/** Canonical revision-1 SYQL formatter (§19).
|
|
4
2
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - declarations separated by one blank line; knobs on their own lines.
|
|
10
|
-
* - comments are preserved: a declaration's leading comment block stays
|
|
11
|
-
* above it; comments inside bodies stay on their own lines.
|
|
12
|
-
*
|
|
13
|
-
* The formatter is built on the same parser as the generator (`.syql` that
|
|
14
|
-
* does not parse does not format), and formatting is semantics-preserving
|
|
15
|
-
* by construction — `fmt` output re-parses to the same declarations.
|
|
3
|
+
* Formatting is a lossless-token rewrite: semantic/atomic tokens and comments
|
|
4
|
+
* keep their order and exact spelling, while trivia is regenerated. The pass
|
|
5
|
+
* parses both sides and refuses output unless their normalized semantic ASTs
|
|
6
|
+
* agree. This keeps the formatter on the same lexer/parser contract as codegen.
|
|
16
7
|
*/
|
|
17
8
|
import { TypegenError } from './errors';
|
|
18
|
-
import {
|
|
9
|
+
import { type SyqlSemanticFile, toSyqlSemanticAst } from './syql-ast';
|
|
10
|
+
import type { SyqlToken } from './syql-lexer';
|
|
11
|
+
import {
|
|
12
|
+
parseSyqlSyntaxFile,
|
|
13
|
+
type SyqlSyntaxFile,
|
|
14
|
+
type SyqlTemplate,
|
|
15
|
+
} from './syql-parser';
|
|
19
16
|
|
|
20
|
-
/** SQL keywords the canon lowercases (a pragmatic core set — identifiers
|
|
21
|
-
* that collide with these would need quoting anyway). */
|
|
22
17
|
const SQL_KEYWORDS = new Set(
|
|
23
18
|
(
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
|
|
19
|
+
'abort action add after all alter analyze and as asc attach autoincrement ' +
|
|
20
|
+
'before begin between by cascade case cast check collate column commit ' +
|
|
21
|
+
'conflict constraint create cross current current_date current_time ' +
|
|
22
|
+
'current_timestamp database default deferrable deferred delete desc ' +
|
|
23
|
+
'detach distinct do drop each else end escape except exclude exclusive ' +
|
|
24
|
+
'exists explain fail filter first following for foreign from full ' +
|
|
25
|
+
'generated glob group groups having if ignore immediate in index indexed ' +
|
|
26
|
+
'initially inner insert instead intersect into is isnull join key last ' +
|
|
27
|
+
'left like limit match materialized natural no not nothing notnull null ' +
|
|
28
|
+
'nulls of offset on or order others outer over partition plan pragma ' +
|
|
29
|
+
'preceding primary query raise range recursive references regexp reindex ' +
|
|
30
|
+
'release rename replace restrict returning right rollback row rows savepoint ' +
|
|
31
|
+
'select set table temp temporary then ties to transaction trigger ' +
|
|
32
|
+
'unbounded union unique update using vacuum values view virtual when where ' +
|
|
33
|
+
'window with without asc desc'
|
|
34
|
+
).split(/\s+/),
|
|
29
35
|
);
|
|
30
36
|
|
|
31
|
-
interface Token {
|
|
32
|
-
readonly kind: 'word' | 'string' | 'comment' | 'punct' | 'param' | 'fragment';
|
|
33
|
-
readonly text: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Tokenize a body: strings/comments verbatim, words, `:params`,
|
|
37
|
-
* `@fragment` refs, single punctuation chars. */
|
|
38
|
-
function tokenize(body: string): Token[] {
|
|
39
|
-
const tokens: Token[] = [];
|
|
40
|
-
let i = 0;
|
|
41
|
-
while (i < body.length) {
|
|
42
|
-
const ch = body[i] as string;
|
|
43
|
-
if (/\s/.test(ch)) {
|
|
44
|
-
i += 1;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (body.startsWith('--', i)) {
|
|
48
|
-
const nl = body.indexOf('\n', i);
|
|
49
|
-
const end = nl === -1 ? body.length : nl;
|
|
50
|
-
tokens.push({ kind: 'comment', text: body.slice(i, end).trimEnd() });
|
|
51
|
-
i = end;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (body.startsWith('/*', i)) {
|
|
55
|
-
const close = body.indexOf('*/', i + 2);
|
|
56
|
-
const end = close === -1 ? body.length : close + 2;
|
|
57
|
-
tokens.push({ kind: 'comment', text: body.slice(i, end) });
|
|
58
|
-
i = end;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (ch === "'") {
|
|
62
|
-
let j = i + 1;
|
|
63
|
-
for (;;) {
|
|
64
|
-
if (j >= body.length) break;
|
|
65
|
-
if (body[j] === "'") {
|
|
66
|
-
if (body[j + 1] === "'") j += 2;
|
|
67
|
-
else {
|
|
68
|
-
j += 1;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
} else j += 1;
|
|
72
|
-
}
|
|
73
|
-
tokens.push({ kind: 'string', text: body.slice(i, j) });
|
|
74
|
-
i = j;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (ch === ':' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
78
|
-
let j = i + 1;
|
|
79
|
-
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
80
|
-
tokens.push({ kind: 'param', text: body.slice(i, j) });
|
|
81
|
-
i = j;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (ch === '@' && /[A-Za-z_]/.test(body[i + 1] ?? '')) {
|
|
85
|
-
let j = i + 1;
|
|
86
|
-
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
87
|
-
tokens.push({ kind: 'fragment', text: body.slice(i, j) });
|
|
88
|
-
i = j;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (/[A-Za-z_]/.test(ch)) {
|
|
92
|
-
let j = i + 1;
|
|
93
|
-
while (j < body.length && /[A-Za-z0-9_]/.test(body[j] as string)) j += 1;
|
|
94
|
-
tokens.push({ kind: 'word', text: body.slice(i, j) });
|
|
95
|
-
i = j;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
// Multi-char operators stay one token (`||`, `>=`, `<=`, `<>`, `!=`, `==`).
|
|
99
|
-
const two = body.slice(i, i + 2);
|
|
100
|
-
if (['||', '>=', '<=', '<>', '!=', '=='].includes(two)) {
|
|
101
|
-
tokens.push({ kind: 'punct', text: two });
|
|
102
|
-
i += 2;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
tokens.push({ kind: 'punct', text: ch });
|
|
106
|
-
i += 1;
|
|
107
|
-
}
|
|
108
|
-
return tokens;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Render tokens back to one line with canonical spacing + keyword case. */
|
|
112
|
-
function render(tokens: readonly Token[]): string {
|
|
113
|
-
let out = '';
|
|
114
|
-
let prev: Token | undefined;
|
|
115
|
-
for (const token of tokens) {
|
|
116
|
-
let text = token.text;
|
|
117
|
-
if (token.kind === 'word' && SQL_KEYWORDS.has(text.toLowerCase())) {
|
|
118
|
-
text = text.toLowerCase();
|
|
119
|
-
}
|
|
120
|
-
const glueLeft =
|
|
121
|
-
token.kind === 'punct' &&
|
|
122
|
-
(text === ',' || text === ')' || text === ';' || text === '.');
|
|
123
|
-
const glueRight =
|
|
124
|
-
prev !== undefined &&
|
|
125
|
-
((prev.kind === 'punct' && (prev.text === '(' || prev.text === '.')) ||
|
|
126
|
-
prev.kind === 'fragment');
|
|
127
|
-
if (out.length > 0 && !glueLeft && !glueRight) out += ' ';
|
|
128
|
-
out += text;
|
|
129
|
-
prev = token;
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Clause keywords that start a new line at paren depth 0. */
|
|
135
37
|
const CLAUSE_STARTERS = new Set([
|
|
136
38
|
'select',
|
|
137
39
|
'from',
|
|
@@ -140,212 +42,384 @@ const CLAUSE_STARTERS = new Set([
|
|
|
140
42
|
'having',
|
|
141
43
|
'order',
|
|
142
44
|
'limit',
|
|
45
|
+
'offset',
|
|
143
46
|
'window',
|
|
144
47
|
'union',
|
|
145
48
|
'except',
|
|
146
49
|
'intersect',
|
|
147
50
|
]);
|
|
148
51
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
52
|
+
function templates(file: SyqlSyntaxFile): readonly SyqlTemplate[] {
|
|
53
|
+
return file.declarations.flatMap((declaration) => {
|
|
54
|
+
if (declaration.kind === 'predicate') return [declaration.body];
|
|
55
|
+
return [
|
|
56
|
+
declaration.sql.body,
|
|
57
|
+
...(declaration.sort?.profiles.map((profile) => profile.order) ?? []),
|
|
58
|
+
];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function inTemplate(
|
|
63
|
+
ranges: readonly SyqlTemplate[],
|
|
64
|
+
token: SyqlToken,
|
|
65
|
+
): boolean {
|
|
66
|
+
return ranges.some(
|
|
67
|
+
(template) =>
|
|
68
|
+
token.span.start.offset >= template.span.start.offset &&
|
|
69
|
+
token.span.end.offset <= template.span.end.offset,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizedAst(ast: SyqlSemanticFile): unknown {
|
|
74
|
+
return JSON.parse(
|
|
75
|
+
JSON.stringify(ast, (_key, value: unknown) => {
|
|
76
|
+
if (
|
|
77
|
+
value !== null &&
|
|
78
|
+
typeof value === 'object' &&
|
|
79
|
+
'kind' in value &&
|
|
80
|
+
'text' in value &&
|
|
81
|
+
(value as { kind?: unknown }).kind === 'identifier' &&
|
|
82
|
+
typeof (value as { text?: unknown }).text === 'string'
|
|
83
|
+
) {
|
|
84
|
+
const token = value as { readonly kind: string; readonly text: string };
|
|
85
|
+
const lower = token.text.toLowerCase();
|
|
86
|
+
return {
|
|
87
|
+
kind: token.kind,
|
|
88
|
+
text: SQL_KEYWORDS.has(lower) ? lower : token.text,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function comments(file: SyqlSyntaxFile): readonly string[] {
|
|
97
|
+
return file.tokens
|
|
98
|
+
.filter(
|
|
99
|
+
(token) =>
|
|
100
|
+
token.kind === 'line-comment' || token.kind === 'block-comment',
|
|
101
|
+
)
|
|
102
|
+
.map((token) => token.text);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class TokenWriter {
|
|
106
|
+
readonly #lines: string[] = [''];
|
|
107
|
+
readonly #blockIndents: {
|
|
108
|
+
readonly close: number;
|
|
109
|
+
readonly parent: number;
|
|
110
|
+
}[] = [];
|
|
111
|
+
#indent = 0;
|
|
112
|
+
#nextIndent = 0;
|
|
113
|
+
|
|
114
|
+
get line(): string {
|
|
115
|
+
return this.#lines[this.#lines.length - 1] as string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#setLine(value: string): void {
|
|
119
|
+
this.#lines[this.#lines.length - 1] = value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
write(text: string, spaceBefore: boolean): void {
|
|
123
|
+
if (this.line.length === 0) {
|
|
124
|
+
this.#setLine(' '.repeat(this.#indent + this.#nextIndent));
|
|
125
|
+
this.#nextIndent = 0;
|
|
167
126
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
127
|
+
if (
|
|
128
|
+
spaceBefore &&
|
|
129
|
+
this.line.trim().length > 0 &&
|
|
130
|
+
!this.line.endsWith(' ')
|
|
131
|
+
) {
|
|
132
|
+
this.#setLine(`${this.line} `);
|
|
173
133
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
134
|
+
this.#setLine(`${this.line}${text}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
comment(text: string): void {
|
|
138
|
+
if (this.line.trim().length > 0) this.write('', true);
|
|
139
|
+
const pieces = text.split('\n');
|
|
140
|
+
this.write(pieces[0] ?? '', false);
|
|
141
|
+
for (const piece of pieces.slice(1)) {
|
|
142
|
+
this.newline();
|
|
143
|
+
// Internal block-comment indentation is part of the token. Do not add
|
|
144
|
+
// formatter indentation before its continuation text.
|
|
145
|
+
this.#setLine(piece);
|
|
187
146
|
}
|
|
188
|
-
|
|
147
|
+
this.newline();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
newline(extraIndent = 0): void {
|
|
151
|
+
this.#setLine(this.line.trimEnd());
|
|
152
|
+
if (this.line.length > 0 || this.#lines.length === 1) this.#lines.push('');
|
|
153
|
+
this.#nextIndent = extraIndent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
blankline(): void {
|
|
157
|
+
this.newline();
|
|
158
|
+
while (
|
|
159
|
+
this.#lines.length >= 2 &&
|
|
160
|
+
this.#lines[this.#lines.length - 2] === ''
|
|
161
|
+
) {
|
|
162
|
+
this.#lines.pop();
|
|
163
|
+
}
|
|
164
|
+
if (this.#lines[this.#lines.length - 1] !== '') this.#lines.push('');
|
|
165
|
+
this.#lines.push('');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
openBlock(): void {
|
|
169
|
+
const parentIndent = this.#indent;
|
|
170
|
+
this.write('{', true);
|
|
171
|
+
const leadingSpaces = this.line.length - this.line.trimStart().length;
|
|
172
|
+
this.#blockIndents.push({
|
|
173
|
+
close: leadingSpaces / 2,
|
|
174
|
+
parent: parentIndent,
|
|
175
|
+
});
|
|
176
|
+
this.#indent = leadingSpaces / 2 + 1;
|
|
177
|
+
this.newline();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
indent(): void {
|
|
181
|
+
this.#indent += 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dedent(): void {
|
|
185
|
+
this.#indent -= 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
closeBlock(): void {
|
|
189
|
+
const block = this.#blockIndents.pop();
|
|
190
|
+
if (block === undefined) {
|
|
191
|
+
throw new Error('formatter block indentation stack underflow');
|
|
192
|
+
}
|
|
193
|
+
if (this.line.trim().length > 0) this.newline();
|
|
194
|
+
this.#indent = block.close;
|
|
195
|
+
this.write('}', false);
|
|
196
|
+
this.#indent = block.parent;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
finish(): string {
|
|
200
|
+
while (this.#lines.length > 0 && this.#lines.at(-1)?.trim() === '') {
|
|
201
|
+
this.#lines.pop();
|
|
202
|
+
}
|
|
203
|
+
return `${this.#lines.join('\n')}\n`;
|
|
189
204
|
}
|
|
190
|
-
flush();
|
|
191
|
-
// Indent: clauses flush-left; WHERE continuation (`and …`) indented 2.
|
|
192
|
-
return lines.map((line) =>
|
|
193
|
-
/^(and|or)\b/.test(line) || line.startsWith('--') ? ` ${line}` : line,
|
|
194
|
-
);
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
|
|
198
|
-
readonly
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
interface DeclarationParameters {
|
|
208
|
+
readonly depth: number;
|
|
209
|
+
readonly multiline: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function declarationParametersMultiline(
|
|
213
|
+
tokens: readonly SyqlToken[],
|
|
214
|
+
openIndex: number,
|
|
215
|
+
prefixLength: number,
|
|
216
|
+
): boolean {
|
|
217
|
+
let depth = 0;
|
|
218
|
+
let parameters = 0;
|
|
219
|
+
let hasContent = false;
|
|
220
|
+
let length = prefixLength + 1;
|
|
221
|
+
for (let index = openIndex + 1; index < tokens.length; index += 1) {
|
|
222
|
+
const token = tokens[index] as SyqlToken;
|
|
223
|
+
if (token.kind === 'line-comment' || token.kind === 'block-comment') {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (token.text === '(') depth += 1;
|
|
227
|
+
if (token.text === ')') {
|
|
228
|
+
if (depth === 0) {
|
|
229
|
+
if (hasContent) parameters += 1;
|
|
230
|
+
return parameters >= 4 || length + 1 > 88;
|
|
231
|
+
}
|
|
232
|
+
depth -= 1;
|
|
209
233
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
234
|
+
if (depth === 0 && token.text === ',') {
|
|
235
|
+
parameters += 1;
|
|
236
|
+
hasContent = false;
|
|
237
|
+
} else if (depth === 0) {
|
|
238
|
+
hasContent = true;
|
|
239
|
+
}
|
|
240
|
+
length += token.text.length + 1;
|
|
212
241
|
}
|
|
213
|
-
return
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function needsSpace(
|
|
246
|
+
previous: SyqlToken | undefined,
|
|
247
|
+
token: SyqlToken,
|
|
248
|
+
): boolean {
|
|
249
|
+
if (previous === undefined) return false;
|
|
250
|
+
if ([',', ')', ']', ';', '.', '?', ':'].includes(token.text)) return false;
|
|
251
|
+
if (['(', '[', '.', '@'].includes(previous.text)) return false;
|
|
252
|
+
if (token.text === '(') return false;
|
|
253
|
+
if (previous.text === ':') return true;
|
|
254
|
+
if (token.kind === 'operator' || previous.kind === 'operator') return true;
|
|
255
|
+
return true;
|
|
214
256
|
}
|
|
215
257
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
let
|
|
223
|
-
let
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
259
|
+
const writer = new TokenWriter();
|
|
260
|
+
const ranges = templates(parsed);
|
|
261
|
+
const tokens = parsed.tokens.filter(
|
|
262
|
+
(token) => token.kind !== 'whitespace' && token.kind !== 'eof',
|
|
263
|
+
);
|
|
264
|
+
let previous: SyqlToken | undefined;
|
|
265
|
+
let braceDepth = 0;
|
|
266
|
+
let parenDepth = 0;
|
|
267
|
+
let importList = false;
|
|
268
|
+
let justClosedImportList = false;
|
|
269
|
+
let seenTopLevel = false;
|
|
270
|
+
let pendingBetween = 0;
|
|
271
|
+
|
|
272
|
+
let declarationParameters: DeclarationParameters | undefined;
|
|
273
|
+
let awaitsDeclarationParameters = false;
|
|
274
|
+
|
|
275
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
276
|
+
const token = tokens[index] as SyqlToken;
|
|
277
|
+
const lower = token.text.toLowerCase();
|
|
278
|
+
const template = inTemplate(ranges, token);
|
|
279
|
+
const topLevelDeclaration =
|
|
280
|
+
braceDepth === 0 &&
|
|
281
|
+
token.kind === 'identifier' &&
|
|
282
|
+
(lower === 'import' || lower === 'predicate' || lower === 'query');
|
|
283
|
+
if (topLevelDeclaration) {
|
|
284
|
+
if (seenTopLevel) writer.blankline();
|
|
285
|
+
seenTopLevel = true;
|
|
286
|
+
previous = undefined;
|
|
287
|
+
awaitsDeclarationParameters = lower === 'predicate' || lower === 'query';
|
|
288
|
+
} else if (
|
|
289
|
+
previous?.text === '}' &&
|
|
290
|
+
!justClosedImportList &&
|
|
291
|
+
token.text !== ';' &&
|
|
292
|
+
token.text !== ','
|
|
293
|
+
) {
|
|
294
|
+
writer.newline();
|
|
295
|
+
previous = undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (token.kind === 'line-comment' || token.kind === 'block-comment') {
|
|
299
|
+
writer.comment(token.text);
|
|
300
|
+
previous = undefined;
|
|
228
301
|
continue;
|
|
229
302
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
template &&
|
|
306
|
+
parenDepth === 0 &&
|
|
307
|
+
token.kind === 'identifier' &&
|
|
308
|
+
CLAUSE_STARTERS.has(lower) &&
|
|
309
|
+
writer.line.trim().length > 0
|
|
310
|
+
) {
|
|
311
|
+
writer.newline();
|
|
312
|
+
previous = undefined;
|
|
313
|
+
pendingBetween = 0;
|
|
314
|
+
}
|
|
315
|
+
if (template && parenDepth === 0 && lower === 'and') {
|
|
316
|
+
if (pendingBetween > 0) pendingBetween -= 1;
|
|
317
|
+
else {
|
|
318
|
+
writer.newline(1);
|
|
319
|
+
previous = undefined;
|
|
320
|
+
}
|
|
321
|
+
} else if (template && lower === 'between') {
|
|
322
|
+
pendingBetween += 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (token.text === '{') {
|
|
326
|
+
if (previous?.kind === 'identifier' && previous.text === 'import') {
|
|
327
|
+
writer.write('{', true);
|
|
328
|
+
importList = true;
|
|
329
|
+
} else {
|
|
330
|
+
writer.openBlock();
|
|
331
|
+
braceDepth += 1;
|
|
332
|
+
}
|
|
333
|
+
previous = token;
|
|
235
334
|
continue;
|
|
236
335
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
336
|
+
if (token.text === '}') {
|
|
337
|
+
if (importList && braceDepth === 0) {
|
|
338
|
+
writer.write('}', true);
|
|
339
|
+
importList = false;
|
|
340
|
+
justClosedImportList = true;
|
|
341
|
+
} else {
|
|
342
|
+
writer.closeBlock();
|
|
343
|
+
braceDepth -= 1;
|
|
344
|
+
}
|
|
345
|
+
previous = token;
|
|
242
346
|
continue;
|
|
243
347
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
348
|
+
justClosedImportList = false;
|
|
349
|
+
|
|
350
|
+
if (token.text === '(') {
|
|
351
|
+
parenDepth += 1;
|
|
352
|
+
const multiline =
|
|
353
|
+
awaitsDeclarationParameters &&
|
|
354
|
+
declarationParametersMultiline(tokens, index, writer.line.length);
|
|
355
|
+
if (awaitsDeclarationParameters) {
|
|
356
|
+
declarationParameters = { depth: parenDepth, multiline };
|
|
357
|
+
awaitsDeclarationParameters = false;
|
|
358
|
+
}
|
|
359
|
+
writer.write(token.text, needsSpace(previous, token));
|
|
360
|
+
if (multiline) {
|
|
361
|
+
writer.indent();
|
|
362
|
+
writer.newline();
|
|
363
|
+
}
|
|
364
|
+
previous = token;
|
|
365
|
+
continue;
|
|
249
366
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const c = source[j] as string;
|
|
257
|
-
if (c === "'") {
|
|
258
|
-
j += 1;
|
|
259
|
-
while (j < source.length && source[j] !== "'") j += 1;
|
|
260
|
-
} else if (source.startsWith('--', j)) {
|
|
261
|
-
const nl = source.indexOf('\n', j);
|
|
262
|
-
j = nl === -1 ? source.length : nl;
|
|
263
|
-
} else if (c === '{') depth += 1;
|
|
264
|
-
else if (c === '}') {
|
|
265
|
-
depth -= 1;
|
|
266
|
-
if (depth === 0) {
|
|
267
|
-
j += 1;
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
367
|
+
|
|
368
|
+
if (token.text === ',' && declarationParameters?.depth === parenDepth) {
|
|
369
|
+
const next = tokens[index + 1];
|
|
370
|
+
if (!(declarationParameters.multiline === false && next?.text === ')')) {
|
|
371
|
+
writer.write(',', false);
|
|
372
|
+
if (declarationParameters.multiline) writer.newline();
|
|
270
373
|
}
|
|
271
|
-
|
|
374
|
+
previous = token;
|
|
375
|
+
continue;
|
|
272
376
|
}
|
|
273
|
-
i = j;
|
|
274
|
-
}
|
|
275
|
-
return out;
|
|
276
|
-
}
|
|
277
377
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const decls: { order: number; text: string }[] = [];
|
|
284
|
-
|
|
285
|
-
// Re-derive declaration order (fragments/queries interleave in-source);
|
|
286
|
-
// parseSyqlFile keeps per-kind order, so re-scan the source for kind
|
|
287
|
-
// keywords to interleave faithfully.
|
|
288
|
-
const orderRe = /\b(query|fragment)\s+([A-Za-z][A-Za-z0-9]*)/g;
|
|
289
|
-
const blanked = source
|
|
290
|
-
.replace(/--[^\n]*/g, (m) => ' '.repeat(m.length))
|
|
291
|
-
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
292
|
-
.replace(/'(?:[^']|'')*'/g, (m) => ' '.repeat(m.length));
|
|
293
|
-
const sequence: { kind: string; name: string }[] = [];
|
|
294
|
-
let braceDepth = 0;
|
|
295
|
-
let lastIndex = 0;
|
|
296
|
-
for (let i = 0; i < blanked.length; i++) {
|
|
297
|
-
const c = blanked[i];
|
|
298
|
-
if (c === '{') braceDepth += 1;
|
|
299
|
-
else if (c === '}') braceDepth -= 1;
|
|
300
|
-
else if (braceDepth === 0) {
|
|
301
|
-
orderRe.lastIndex = i;
|
|
302
|
-
const m = orderRe.exec(blanked);
|
|
303
|
-
if (m !== null && m.index === i) {
|
|
304
|
-
sequence.push({ kind: m[1] as string, name: m[2] as string });
|
|
305
|
-
i = orderRe.lastIndex - 1;
|
|
306
|
-
lastIndex = orderRe.lastIndex;
|
|
378
|
+
if (token.text === ')' && declarationParameters?.depth === parenDepth) {
|
|
379
|
+
if (declarationParameters.multiline) {
|
|
380
|
+
if (previous?.text !== ',') writer.write(',', false);
|
|
381
|
+
writer.dedent();
|
|
382
|
+
if (writer.line.trim().length > 0) writer.newline();
|
|
307
383
|
}
|
|
384
|
+
writer.write(')', false);
|
|
385
|
+
declarationParameters = undefined;
|
|
386
|
+
parenDepth -= 1;
|
|
387
|
+
previous = token;
|
|
388
|
+
continue;
|
|
308
389
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
for (const line of formatBody(decl.body)) lines.push(` ${line}`);
|
|
321
|
-
lines.push('}');
|
|
390
|
+
|
|
391
|
+
if (token.text === ')') parenDepth -= 1;
|
|
392
|
+
|
|
393
|
+
const text =
|
|
394
|
+
template && token.kind === 'identifier' && SQL_KEYWORDS.has(lower)
|
|
395
|
+
? lower
|
|
396
|
+
: token.text;
|
|
397
|
+
writer.write(text, needsSpace(previous, token));
|
|
398
|
+
if (token.text === ';') {
|
|
399
|
+
writer.newline();
|
|
400
|
+
previous = undefined;
|
|
322
401
|
} else {
|
|
323
|
-
|
|
324
|
-
if (decl === undefined) return;
|
|
325
|
-
const knobs: string[] = [];
|
|
326
|
-
if (decl.orderBy !== undefined) {
|
|
327
|
-
const dir = decl.orderBy.defaultDir === 'desc' ? ' desc' : '';
|
|
328
|
-
knobs.push(
|
|
329
|
-
` orderBy ${decl.orderBy.allowed.join(' | ')} default ${decl.orderBy.defaultColumn}${dir}`,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
if (decl.limit !== undefined) {
|
|
333
|
-
const parts: string[] = [];
|
|
334
|
-
if (decl.limit.max !== undefined) parts.push(`max ${decl.limit.max}`);
|
|
335
|
-
if (decl.limit.default !== undefined) {
|
|
336
|
-
parts.push(`default ${decl.limit.default}`);
|
|
337
|
-
}
|
|
338
|
-
knobs.push(` limit ${parts.join(' ')}`);
|
|
339
|
-
}
|
|
340
|
-
if (decl.variants === true) knobs.push(' variants');
|
|
341
|
-
lines.push(
|
|
342
|
-
`query ${decl.name}(${formatSignature(decl)})${knobs.length > 0 ? `\n${knobs.join('\n')}\n{` : ' {'}`,
|
|
343
|
-
);
|
|
344
|
-
for (const line of formatBody(decl.body)) lines.push(` ${line}`);
|
|
345
|
-
lines.push('}');
|
|
402
|
+
previous = token;
|
|
346
403
|
}
|
|
347
|
-
|
|
348
|
-
|
|
404
|
+
}
|
|
405
|
+
return writer.finish();
|
|
406
|
+
}
|
|
349
407
|
|
|
350
|
-
|
|
408
|
+
/** Format one `.syql` source. Invalid or non-equivalent output is rejected so
|
|
409
|
+
* the CLI never writes a partially understood file. */
|
|
410
|
+
export function formatSyql(file: string, source: string): string {
|
|
411
|
+
const before = parseSyqlSyntaxFile(file, source);
|
|
412
|
+
const formatted = formatTokens(before);
|
|
413
|
+
const after = parseSyqlSyntaxFile(file, formatted);
|
|
414
|
+
if (
|
|
415
|
+
JSON.stringify(normalizedAst(toSyqlSemanticAst(before))) !==
|
|
416
|
+
JSON.stringify(normalizedAst(toSyqlSemanticAst(after))) ||
|
|
417
|
+
JSON.stringify(comments(before)) !== JSON.stringify(comments(after))
|
|
418
|
+
) {
|
|
419
|
+
throw new TypegenError(
|
|
420
|
+
file,
|
|
421
|
+
'SYQL8001_FORMATTER_EQUIVALENCE: formatter output changed the revision-1 semantic AST or comment order',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return formatted;
|
|
351
425
|
}
|