Skip to content

Commit 7a94ac3

Browse files
committed
feat: Add matchIndentation option
1 parent 4537d7f commit 7a94ac3

File tree

7 files changed

+307
-15
lines changed

7 files changed

+307
-15
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module.exports = {
22
extends: ['canonical/auto', 'canonical/node'],
3-
ignorePatterns: ['dist', 'package-lock.json'],
3+
ignorePatterns: ['dist', 'package-lock.json', 'node_modules'],
44
root: true,
55
rules: {
6+
'complexity': 0,
67
'no-template-curly-in-string': 0,
78
'node/no-sync': 0,
89
},

src/rules/format.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { indentString, stripIndent } from '../utilities/indent';
12
import isSqlQuery from '../utilities/isSqlQuery';
23
import { generate } from 'astring';
34
import { format } from 'pg-formatter';
@@ -11,6 +12,7 @@ const create = (context) => {
1112
const ignoreInline = pluginOptions.ignoreInline !== false;
1213
const ignoreTagless = pluginOptions.ignoreTagless !== false;
1314
const ignoreStartWithNewLine = pluginOptions.ignoreStartWithNewLine !== false;
15+
const matchIndentation = pluginOptions.matchIndentation !== false;
1416

1517
return {
1618
TemplateLiteral(node) {
@@ -41,18 +43,49 @@ const create = (context) => {
4143
return;
4244
}
4345

44-
if (ignoreInline && !literal.includes('\n')) {
46+
const eolMatch = literal.match(/\r?\n/u);
47+
if (ignoreInline && !eolMatch) {
4548
return;
4649
}
4750

48-
let formatted = format(literal, context.options[1]);
51+
const [eol = '\n'] = eolMatch || [];
52+
53+
const sourceCode = context.getSourceCode();
54+
const startLine = sourceCode.lines[node.loc.start.line - 1];
55+
const marginMatch = startLine.match(/^(\s*)\S/u);
56+
const parentMargin = marginMatch ? marginMatch[1] : '';
57+
58+
const pgFormatterOptions = { ...context.options[1] };
59+
const { tabs, spaces = 4 } = pgFormatterOptions;
60+
61+
const indent = tabs ? `\t` : ' '.repeat(spaces || 4);
62+
let formatted = format(literal, pgFormatterOptions).trim();
63+
64+
if (matchIndentation) {
65+
const stripped = stripIndent(formatted);
66+
const trimmed = stripped.replaceAll(
67+
new RegExp(`^${eol}|${eol}[ \t]*$`, 'gu'),
68+
'',
69+
);
70+
const formattedLines = trimmed.split(eol);
71+
const indented =
72+
formattedLines
73+
.map((line) => {
74+
return parentMargin + indentString(line, 1, { indent });
75+
})
76+
.join(eol) +
77+
// The last line has an eol to make the backtick line up
78+
eol +
79+
parentMargin;
80+
81+
formatted = indented;
82+
}
83+
84+
const shouldPrependEol =
85+
ignoreStartWithNewLine && literal.startsWith(eol);
4986

50-
if (
51-
ignoreStartWithNewLine &&
52-
literal.startsWith('\n') &&
53-
!formatted.startsWith('\n')
54-
) {
55-
formatted = '\n' + formatted;
87+
if (shouldPrependEol && !formatted.startsWith(eol)) {
88+
formatted = eol + formatted;
5689
}
5790

5891
if (formatted !== literal) {
@@ -77,7 +110,9 @@ const create = (context) => {
77110
node.quasis[0].range[0],
78111
node.quasis[node.quasis.length - 1].range[1],
79112
],
80-
'`\n' + final + '`',
113+
`\`${shouldPrependEol && formatted.startsWith(eol) ? '' : '\n'}` +
114+
final +
115+
'`',
81116
);
82117
},
83118
message: 'Format the query',
@@ -117,6 +152,10 @@ export = {
117152
default: true,
118153
type: 'boolean',
119154
},
155+
matchIndentation: {
156+
default: true,
157+
type: 'boolean',
158+
},
120159
},
121160
type: 'object',
122161
},

src/utilities/indent.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
type IndentOptions = {
2+
includeEmptyLines?: boolean;
3+
indent?: string;
4+
};
5+
6+
const minIndent = function (string: string): number {
7+
const match = string.match(/^[\t ]*(?=\S)/gmu);
8+
9+
if (!match) {
10+
return 0;
11+
}
12+
13+
let min = Number.POSITIVE_INFINITY;
14+
15+
for (const indent of match) {
16+
min = Math.min(min, indent.length);
17+
}
18+
19+
return min;
20+
};
21+
22+
export const stripIndent = function (string: string): string {
23+
const indent = minIndent(string);
24+
25+
if (indent === 0) {
26+
return string;
27+
}
28+
29+
const regex = new RegExp(`^[ \\t]{${indent}}`, 'gmu');
30+
31+
return string.replace(regex, '');
32+
};
33+
34+
export const indentString = function (
35+
string: string,
36+
count: number = 1,
37+
options: IndentOptions = {},
38+
): string {
39+
const { indent = ' ', includeEmptyLines = false } = options;
40+
41+
if (count === 0) {
42+
return string;
43+
}
44+
45+
const regex = includeEmptyLines ? /^/gmu : /^(?!\s*$)/gmu;
46+
47+
return string.replace(regex, indent.repeat(count));
48+
};

test/rules/assertions/format.ts

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default {
1313
ignoreTagless: false,
1414
},
1515
],
16-
output: '`\nSELECT\n 1\n`',
16+
output: '`\n SELECT\n 1\n`',
1717
},
1818
{
1919
code: '`SELECT 2`',
@@ -31,7 +31,7 @@ export default {
3131
spaces: 2,
3232
},
3333
],
34-
output: '`\nSELECT\n 2\n`',
34+
output: '`\n SELECT\n 2\n`',
3535
},
3636
{
3737
code: 'sql.unsafe`SELECT 3`',
@@ -45,7 +45,7 @@ export default {
4545
ignoreInline: false,
4646
},
4747
],
48-
output: 'sql.unsafe`\nSELECT\n 3\n`',
48+
output: 'sql.unsafe`\n SELECT\n 3\n`',
4949
},
5050
{
5151
code: 'sql.type()`SELECT 3`',
@@ -59,7 +59,7 @@ export default {
5959
ignoreInline: false,
6060
},
6161
],
62-
output: 'sql.type()`\nSELECT\n 3\n`',
62+
output: 'sql.type()`\n SELECT\n 3\n`',
6363
},
6464
{
6565
code: "`SELECT ${'foo'} FROM ${'bar'}`",
@@ -74,10 +74,132 @@ export default {
7474
ignoreTagless: false,
7575
},
7676
],
77-
output: "`\nSELECT\n ${'foo'}\nFROM\n ${'bar'}\n`",
77+
output: "`\n SELECT\n ${'foo'}\n FROM\n ${'bar'}\n`",
78+
},
79+
{
80+
code: "\t\t`SELECT ${'foo'} FROM ${'bar'}`",
81+
errors: [
82+
{
83+
message: 'Format the query',
84+
},
85+
],
86+
options: [
87+
{
88+
ignoreInline: false,
89+
ignoreTagless: false,
90+
},
91+
],
92+
output:
93+
"\t\t`\n\t\t SELECT\n\t\t ${'foo'}\n\t\t FROM\n\t\t ${'bar'}\n\t\t`",
94+
},
95+
{
96+
code: '\tconst s = sql`SELECT\n1\nFROM\ntable`',
97+
errors: [
98+
{
99+
message: 'Format the query',
100+
},
101+
],
102+
options: [
103+
{},
104+
{
105+
spaces: 2,
106+
},
107+
],
108+
output:
109+
'\tconst s = sql`\n\t SELECT\n\t 1\n\t FROM\n\t table\n\t`',
110+
},
111+
{
112+
code: '\tconst s = sql`SELECT 1 FROM table`',
113+
errors: [
114+
{
115+
message: 'Format the query',
116+
},
117+
],
118+
options: [
119+
{
120+
ignoreInline: false,
121+
},
122+
{
123+
tabs: true,
124+
},
125+
],
126+
output:
127+
'\tconst s = sql`\n\t\tSELECT\n\t\t\t1\n\t\tFROM\n\t\t\ttable\n\t`',
128+
},
129+
{
130+
code: '\tconst s = sql`SELECT 1 FROM table`',
131+
errors: [
132+
{
133+
message: 'Format the query',
134+
},
135+
],
136+
options: [
137+
{
138+
ignoreInline: false,
139+
},
140+
{
141+
spaces: 0,
142+
tabs: false,
143+
},
144+
],
145+
output:
146+
'\tconst s = sql`\n\t SELECT\n\t 1\n\t FROM\n\t table\n\t`',
147+
},
148+
{
149+
code: ' const s = sql`SELECT 1 FROM table`',
150+
errors: [
151+
{
152+
message: 'Format the query',
153+
},
154+
],
155+
options: [
156+
{
157+
ignoreInline: false,
158+
},
159+
{
160+
spaces: 0,
161+
tabs: false,
162+
},
163+
],
164+
output:
165+
' const s = sql`\n SELECT\n 1\n FROM\n table\n `',
166+
},
167+
{
168+
code: '\tsql`SELECT 1 FROM table`',
169+
errors: [
170+
{
171+
message: 'Format the query',
172+
},
173+
],
174+
options: [
175+
{
176+
ignoreInline: false,
177+
matchIndentation: false,
178+
},
179+
],
180+
output: '\tsql`\nSELECT\n 1\nFROM\n table`',
181+
},
182+
{
183+
code: '\tconst query = sql`\nDELETE FROM table AS t\nWHERE t.id = ${foo}AND t.type = ${type};`',
184+
errors: [
185+
{
186+
message: 'Format the query',
187+
},
188+
],
189+
options: [
190+
{
191+
ignoreInline: false,
192+
matchIndentation: true,
193+
},
194+
],
195+
output:
196+
'\tconst query = sql`\n\t DELETE FROM table AS t\n\t WHERE t.id = ${foo}\n\t AND t.type = ${type};\n\t`',
78197
},
79198
],
80199
valid: [
200+
{
201+
code: 'sql`SELECT 1`',
202+
},
81203
{
82204
code: 'sql`SELECT 1`',
83205
options: [
@@ -104,5 +226,23 @@ export default {
104226
},
105227
],
106228
},
229+
{
230+
code: '\tconst s = sql`\n\t SELECT\n\t 1\n\t FROM\n\t table\n\t`',
231+
options: [
232+
{},
233+
{
234+
spaces: 2,
235+
},
236+
],
237+
},
238+
{
239+
code: '\tconst query = sql`\n\t DELETE FROM table AS t\n\t WHERE t.id = ${foo}\n\t AND t.type = ${type};\n\t`',
240+
options: [
241+
{},
242+
{
243+
spaces: 2,
244+
},
245+
],
246+
},
107247
],
108248
};

test/rules/assertions/noUnsafeQuery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,8 @@ export default {
5353
{
5454
code: "sql`SELECT ${'foo'}`",
5555
},
56+
{
57+
code: 'sql``',
58+
},
5659
],
5760
};

0 commit comments

Comments
 (0)