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