Skip to content

Commit ac0a045

Browse files
Merge PR #4 from inferrinizzard/inline-tokens
2 parents 8887d7e + fcf2854 commit ac0a045

File tree

4 files changed

+128
-5
lines changed

4 files changed

+128
-5
lines changed

src/core/Formatter.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export default class Formatter {
102102
} else if (token.type === tokenTypes.BLOCK_COMMENT) {
103103
formattedQuery = this.formatBlockComment(token, formattedQuery);
104104
} else if (token.type === tokenTypes.RESERVED_TOP_LEVEL) {
105+
this.currentNewline = this.checkNewline(index);
105106
formattedQuery = this.formatTopLevelReservedWord(token, formattedQuery);
106107
this.previousReservedToken = token;
107108
this.withinSelect = isSelect(token);
@@ -170,6 +171,48 @@ export default class Formatter {
170171
return query;
171172
}
172173

174+
checkNewline = (index: number) => {
175+
if (
176+
this.newline.mode === 'always' ||
177+
this.tokens.some(({ type, value }) => type === tokenTypes.OPEN_PAREN && value.length > 1) // auto break on CASE statements
178+
)
179+
return true;
180+
if (this.newline.mode === 'never') return false;
181+
const tail = this.tokens.slice(index + 1);
182+
const nextTokens = tail.slice(
183+
0,
184+
tail.findIndex(
185+
({ type }) =>
186+
type === tokenTypes.RESERVED_TOP_LEVEL ||
187+
type === tokenTypes.RESERVED_TOP_LEVEL_NO_INDENT ||
188+
type === tokenTypes.RESERVED_NEWLINE
189+
)
190+
);
191+
192+
const numItems = nextTokens.reduce(
193+
(acc, { type, value }) => {
194+
if (value == ',' && !acc.inParen) return { ...acc, count: acc.count + 1 }; // count commas between items in clause
195+
if (type === tokenTypes.OPEN_PAREN) return { ...acc, inParen: true }; // don't count commas in functions
196+
if (type === tokenTypes.CLOSE_PAREN) return { ...acc, inParen: false };
197+
return acc;
198+
},
199+
{ count: 1, inParen: false } // start with 1 for first word
200+
).count;
201+
202+
if (this.newline.mode === 'itemCount') return numItems > this.newline.itemCount!;
203+
204+
// calculate length if it were all inline
205+
const inlineWidth = `${this.tokens[index].whitespaceBefore}${
206+
this.tokens[index].value
207+
} ${nextTokens.map(({ value }) => (value === ',' ? value + ' ' : value)).join('')}`.length;
208+
209+
if (this.newline.mode === 'lineWidth') return inlineWidth > this.lineWidth;
210+
else if (this.newline.mode == 'hybrid')
211+
return numItems > this.newline.itemCount! || inlineWidth > this.lineWidth;
212+
213+
return true;
214+
};
215+
173216
formatLineComment(token: Token, query: string) {
174217
return this.addNewline(query + this.show(token));
175218
}
@@ -196,7 +239,9 @@ export default class Formatter {
196239
this.indentation.increaseTopLevel();
197240

198241
query += this.equalizeWhitespace(this.show(token));
199-
return this.addNewline(query);
242+
if (this.currentNewline) query = this.addNewline(query);
243+
else query += ' ';
244+
return query;
200245
}
201246

202247
formatNewlineReservedWord(token: Token, query: string) {
@@ -261,7 +306,8 @@ export default class Formatter {
261306
} else if (isLimit(this.previousReservedToken)) {
262307
return query;
263308
} else {
264-
return this.addNewline(query);
309+
if (this.currentNewline) return this.addNewline(query);
310+
else return query;
265311
}
266312
}
267313

@@ -293,9 +339,7 @@ export default class Formatter {
293339

294340
addNewline(query: string) {
295341
query = trimSpacesEnd(query);
296-
if (!query.endsWith('\n')) {
297-
query += '\n';
298-
}
342+
if (!query.endsWith('\n')) query += '\n';
299343
return query + this.indentation.getIndent();
300344
}
301345

src/sqlFormatter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface FormatOptions {
5252
* @param {String} cfg.newline.mode always | never | lineWidth (break only when > line width) | itemCount (break when > itemCount) | hybrid (lineWidth OR itemCount)
5353
* @param {Integer} cfg.newline.itemCount Used when mode is itemCount or hybrid, must be >=0
5454
* @param {String} cfg.aliasAs Whether to use AS in column aliases in only SELECT clause, both SELECT and table aliases, or never
55+
* @param {Integer} cfg.lineWidth Number of characters in each line before breaking, default: 50
5556
* @param {Integer} cfg.linesBetweenQueries How many line breaks between queries
5657
* @param {ParamItems} cfg.params Collection of params for placeholder replacement
5758
* @return {String}

test/behavesLikeSqlFormatter.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import supportsComments from './features/comments';
33
import supportsConfigOptions from './features/configOptions';
44
import supportsOperators from './features/operators';
55
import supportsAliases from './features/alias';
6+
import supportsNewlineOptions from './features/newline';
67

78
/**
89
* Core tests for all SQL formatters
910
* @param {Function} format
1011
*/
1112
export default function behavesLikeSqlFormatter(format) {
1213
supportsAliases(format);
14+
supportsNewlineOptions(format);
1315
supportsComments(format);
1416
supportsConfigOptions(format);
1517
supportsOperators(['=', '+', '-', '*', '/', '<>', '>', '<', '>=', '<=']);

test/features/newline.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import dedent from 'dedent-js';
2+
3+
/**
4+
* Tests support for all newline options
5+
* @param {Function} format
6+
*/
7+
export default function supportsNewlineOptions(format) {
8+
it('supports always mode', () => {
9+
const result = format('SELECT foo, bar, baz FROM qux;', {
10+
newline: { mode: 'always' },
11+
});
12+
expect(result).toBe(dedent`
13+
SELECT
14+
foo,
15+
bar,
16+
baz
17+
FROM
18+
qux;
19+
`);
20+
});
21+
22+
it('supports never mode', () => {
23+
const result = format('SELECT foo, bar, baz, qux FROM corge;', { newline: { mode: 'never' } });
24+
expect(result).toBe(dedent`
25+
SELECT foo, bar, baz, qux
26+
FROM corge;
27+
`);
28+
});
29+
30+
it('supports itemCount mode', () => {
31+
const result = format('SELECT foo, bar, baz, qux FROM corge;', {
32+
newline: { mode: 'itemCount', itemCount: 3 },
33+
});
34+
expect(result).toBe(dedent`
35+
SELECT
36+
foo,
37+
bar,
38+
baz,
39+
qux
40+
FROM corge;
41+
`);
42+
});
43+
44+
it('supports lineWidth mode', () => {
45+
const result = format('SELECT foo, bar, baz, qux FROM corge;', {
46+
newline: { mode: 'lineWidth' },
47+
lineWidth: 20,
48+
});
49+
expect(result).toBe(dedent`
50+
SELECT
51+
foo,
52+
bar,
53+
baz,
54+
qux
55+
FROM corge;
56+
`);
57+
});
58+
59+
it('supports hybrid mode', () => {
60+
const result = format('SELECT verylongfoo, verylongbar FROM baz GROUP BY foo, bar, baz, qux;', {
61+
newline: { mode: 'hybrid', itemCount: 2 },
62+
lineWidth: 30,
63+
});
64+
expect(result).toBe(dedent`
65+
SELECT
66+
verylongfoo,
67+
verylongbar
68+
FROM baz
69+
GROUP BY
70+
foo,
71+
bar,
72+
baz,
73+
qux;
74+
`);
75+
});
76+
}

0 commit comments

Comments
 (0)