@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.
Files changed (56) hide show
  1. package/README.md +79 -32
  2. package/dist/cli.js +40 -17
  3. package/dist/emit-queries-dart.js +168 -55
  4. package/dist/emit-queries-kotlin.js +167 -55
  5. package/dist/emit-queries-swift.js +183 -57
  6. package/dist/emit-queries.js +286 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +348 -316
  9. package/dist/generate.d.ts +1 -1
  10. package/dist/generate.js +28 -9
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +3 -1
  13. package/dist/lsp.d.ts +1 -3
  14. package/dist/lsp.js +349 -187
  15. package/dist/manifest.d.ts +1 -3
  16. package/dist/manifest.js +1 -1
  17. package/dist/query-ir.js +53 -42
  18. package/dist/query.d.ts +110 -63
  19. package/dist/query.js +89 -11
  20. package/dist/syql-ast.d.ts +12 -13
  21. package/dist/syql-ast.js +26 -20
  22. package/dist/syql-lexer.d.ts +2 -0
  23. package/dist/syql-lexer.js +9 -2
  24. package/dist/syql-lowering.d.ts +22 -0
  25. package/dist/syql-lowering.js +434 -0
  26. package/dist/syql-parser.d.ts +19 -18
  27. package/dist/syql-parser.js +341 -93
  28. package/dist/syql-semantics.d.ts +73 -0
  29. package/dist/syql-semantics.js +607 -0
  30. package/dist/syql-template-parser.d.ts +5 -20
  31. package/dist/syql-template-parser.js +116 -109
  32. package/dist/syql-validator.d.ts +35 -0
  33. package/dist/syql-validator.js +993 -0
  34. package/package.json +3 -3
  35. package/src/cli.ts +49 -21
  36. package/src/emit-queries-dart.ts +195 -69
  37. package/src/emit-queries-kotlin.ts +197 -69
  38. package/src/emit-queries-swift.ts +224 -68
  39. package/src/emit-queries.ts +410 -165
  40. package/src/fmt.ts +405 -303
  41. package/src/generate.ts +43 -9
  42. package/src/index.ts +3 -1
  43. package/src/lsp.ts +414 -216
  44. package/src/manifest.ts +2 -4
  45. package/src/query-ir.ts +52 -42
  46. package/src/query.ts +229 -79
  47. package/src/syql-ast.ts +38 -34
  48. package/src/syql-lexer.ts +12 -1
  49. package/src/syql-lowering.ts +664 -0
  50. package/src/syql-parser.ts +472 -134
  51. package/src/syql-semantics.ts +930 -0
  52. package/src/syql-template-parser.ts +119 -163
  53. package/src/syql-validator.ts +1486 -0
  54. package/dist/syql.d.ts +0 -102
  55. package/dist/syql.js +0 -1141
  56. 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
- * - SQL keywords lowercase; identifier casing preserved (SQL-truth).
6
- * - one space after commas; collapsed whitespace elsewhere.
7
- * - one clause per line in the body; WHERE conjuncts one per line,
8
- * `and`-prefixed and indented under the `where`.
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 { parseSyqlFile } from './syql.js';
19
- /** SQL keywords the canon lowercases (a pragmatic core set — identifiers
20
- * that collide with these would need quoting anyway). */
21
- const SQL_KEYWORDS = new Set(('select from where group by having order limit offset window and or not ' +
22
- 'in is null like glob between exists case when then else end as on using ' +
23
- 'join left right full outer inner cross natural union all except ' +
24
- 'intersect distinct values with recursive asc desc collate cast').split(' '));
25
- /** Tokenize a body: strings/comments verbatim, words, `:params`,
26
- * `@fragment` refs, single punctuation chars. */
27
- function tokenize(body) {
28
- const tokens = [];
29
- let i = 0;
30
- while (i < body.length) {
31
- const ch = body[i];
32
- if (/\s/.test(ch)) {
33
- i += 1;
34
- continue;
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
- /** Format a body: one clause per line; WHERE conjuncts one per line. */
142
- function formatBody(body) {
143
- const tokens = tokenize(body);
144
- const lines = [];
145
- let current = [];
146
- let depth = 0;
147
- let braceDepth = 0;
148
- let inWhere = false;
149
- let pendingBetween = 0;
150
- const flush = () => {
151
- if (current.length > 0)
152
- lines.push(render(current));
153
- current = [];
154
- };
155
- for (const token of tokens) {
156
- if (token.kind === 'comment') {
157
- flush();
158
- lines.push(token.text);
159
- continue;
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
- if (token.kind === 'punct') {
162
- if (token.text === '(')
163
- depth += 1;
164
- else if (token.text === ')')
165
- depth -= 1;
166
- else if (token.text === '{')
167
- braceDepth += 1;
168
- else if (token.text === '}')
169
- braceDepth -= 1;
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 (token.kind === 'word' && depth === 0 && braceDepth === 0) {
172
- const word = token.text.toLowerCase();
173
- if (CLAUSE_STARTERS.has(word)) {
174
- flush();
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
- current.push(token);
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 formatSignature(decl) {
196
- const parts = [];
197
- const seenGroups = new Set();
198
- for (const param of decl.params) {
199
- if (param.group !== undefined) {
200
- if (seenGroups.has(param.group))
201
- continue;
202
- seenGroups.add(param.group);
203
- const members = decl.params.filter((p) => p.group === param.group);
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 (param.flag)
208
- parts.push(`${param.name}?: flag`);
209
- else
210
- parts.push(`${param.name}${param.optional ? '?' : ''}`);
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 parts.join(', ');
193
+ return false;
213
194
  }
214
- /** The leading comment block (verbatim lines) before each declaration. */
215
- function leadingComments(source) {
216
- // Map from declaration index (0-based, in order of appearance) to its
217
- // preceding comment lines.
218
- const out = new Map();
219
- let pending = [];
220
- let declIndex = 0;
221
- let i = 0;
222
- while (i < source.length) {
223
- const ch = source[i];
224
- if (/\s/.test(ch)) {
225
- i += 1;
226
- continue;
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 (source.startsWith('--', i)) {
229
- const nl = source.indexOf('\n', i);
230
- const end = nl === -1 ? source.length : nl;
231
- pending.push(source.slice(i, end).trimEnd());
232
- i = end;
233
- continue;
243
+ else if (braceDepth === 0 &&
244
+ lower === 'query' &&
245
+ previous?.text === 'sync') {
246
+ awaitsDeclarationParameters = true;
234
247
  }
235
- if (source.startsWith('/*', i)) {
236
- const close = source.indexOf('*/', i + 2);
237
- const end = close === -1 ? source.length : close + 2;
238
- pending.push(source.slice(i, end));
239
- i = end;
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
- // A declaration keyword: attach pending comments, then skip through its
243
- // brace block.
244
- if (pending.length > 0) {
245
- out.set(declIndex, pending);
246
- pending = [];
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
- declIndex += 1;
249
- const brace = source.indexOf('{', i);
250
- if (brace === -1)
251
- break;
252
- let depth = 0;
253
- let j = brace;
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
- else if (source.startsWith('--', j)) {
262
- const nl = source.indexOf('\n', j);
263
- j = nl === -1 ? source.length : nl;
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 (c === '{')
266
- depth += 1;
267
- else if (c === '}') {
268
- depth -= 1;
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
- j += 1;
291
+ else {
292
+ writer.openBlock();
293
+ braceDepth += 1;
294
+ }
295
+ previous = token;
296
+ continue;
275
297
  }
276
- i = j;
277
- }
278
- return out;
279
- }
280
- /** Format one `.syql` source file into its canonical form. Throws
281
- * {@link TypegenError} when the source does not parse. */
282
- export function formatSyql(file, source) {
283
- const parsed = parseSyqlFile(file, source);
284
- const comments = leadingComments(source);
285
- const decls = [];
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
- void lastIndex;
314
- sequence.forEach((entry, index) => {
315
- const lines = [];
316
- const leading = comments.get(index);
317
- if (leading !== undefined)
318
- lines.push(...leading);
319
- if (entry.kind === 'fragment') {
320
- const decl = parsed.fragments.find((f) => f.name === entry.name);
321
- if (decl === undefined)
322
- return;
323
- lines.push(`fragment ${decl.name}(${formatSignature(decl)}) {`);
324
- for (const line of formatBody(decl.body))
325
- lines.push(` ${line}`);
326
- lines.push('}');
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
- else {
329
- const decl = parsed.queries.find((q) => q.name === entry.name);
330
- if (decl === undefined)
331
- return;
332
- const knobs = [];
333
- if (decl.orderBy !== undefined) {
334
- const dir = decl.orderBy.defaultDir === 'desc' ? ' desc' : '';
335
- knobs.push(` orderBy ${decl.orderBy.allowed.join(' | ')} default ${decl.orderBy.defaultColumn}${dir}`);
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
- if (decl.limit !== undefined) {
338
- const parts = [];
339
- if (decl.limit.max !== undefined)
340
- parts.push(`max ${decl.limit.max}`);
341
- if (decl.limit.default !== undefined) {
342
- parts.push(`default ${decl.limit.default}`);
343
- }
344
- knobs.push(` limit ${parts.join(' ')}`);
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
- if (decl.variants === true)
347
- knobs.push(' variants');
348
- lines.push(`query ${decl.name}(${formatSignature(decl)})${knobs.length > 0 ? `\n${knobs.join('\n')}\n{` : ' {'}`);
349
- for (const line of formatBody(decl.body))
350
- lines.push(` ${line}`);
351
- lines.push('}');
354
+ writer.write(')', false);
355
+ declarationParameters = undefined;
356
+ parenDepth -= 1;
357
+ previous = token;
358
+ continue;
352
359
  }
353
- decls.push({ order: index, text: lines.join('\n') });
354
- });
355
- return `${decls.map((d) => d.text).join('\n\n')}\n`;
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
  }