-
Notifications
You must be signed in to change notification settings - Fork 391
/
Formatter.ts
111 lines (95 loc) · 3.52 KB
/
Formatter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { FormatOptions } from '../FormatOptions.js';
import { indentString } from './config.js';
import Params from './Params.js';
import Tokenizer from '../lexer/Tokenizer.js';
import { createParser } from '../parser/createParser.js';
import { StatementNode } from '../parser/ast.js';
import { cacheInClassField } from '../utils.js';
import formatCommaPositions from './formatCommaPositions.js';
import formatAliasPositions from './formatAliasPositions.js';
import ExpressionFormatter, {
DialectFormatOptions,
ProcessedDialectFormatOptions,
} from './ExpressionFormatter.js';
import Layout, { WS } from './Layout.js';
import Indentation from './Indentation.js';
/** Main formatter class that produces a final output string from list of tokens */
export default class Formatter {
private cfg: FormatOptions;
private params: Params;
constructor(cfg: FormatOptions) {
this.cfg = cfg;
this.params = new Params(this.cfg.params);
}
/**
* SQL Tokenizer for this formatter, provided by subclasses.
*/
protected tokenizer(): Tokenizer {
throw new Error('tokenizer() not implemented by subclass');
}
// Cache the tokenizer for each class (each SQL dialect)
// So we wouldn't need to recreate the tokenizer, which is kinda expensive,
// for each call to format() function.
private cachedTokenizer(): Tokenizer {
return cacheInClassField(this.constructor, 'cachedTokenizer', () => this.tokenizer());
}
/**
* Dialect-specific formatting configuration, provided by subclass.
*/
protected formatOptions(): DialectFormatOptions {
throw new Error('formatOptions() not implemented by sybclass');
}
private cachedFormatOptions(): ProcessedDialectFormatOptions {
return cacheInClassField(this.constructor, 'cachedFormatOptions', () => {
const opts = this.formatOptions();
return {
alwaysDenseOperators: opts.alwaysDenseOperators || [],
onelineClauses: Object.fromEntries(opts.onelineClauses.map(name => [name, true])),
};
});
}
/**
* Formats an SQL query.
* @param {string} query - The SQL query string to be formatted
* @return {string} The formatter query
*/
public format(query: string): string {
const ast = this.parse(query);
const formattedQuery = this.formatAst(ast);
const finalQuery = this.postFormat(formattedQuery);
return finalQuery.trimEnd();
}
private parse(query: string): StatementNode[] {
return createParser(this.cachedTokenizer()).parse(query, this.cfg.paramTypes || {});
}
private formatAst(statements: StatementNode[]): string {
return statements
.map(stat => this.formatStatement(stat))
.join('\n'.repeat(this.cfg.linesBetweenQueries + 1));
}
private formatStatement(statement: StatementNode): string {
const layout = new ExpressionFormatter({
cfg: this.cfg,
dialectCfg: this.cachedFormatOptions(),
params: this.params,
layout: new Layout(new Indentation(indentString(this.cfg))),
}).format(statement.children);
if (!statement.hasSemicolon) {
// do nothing
} else if (this.cfg.newlineBeforeSemicolon) {
layout.add(WS.NEWLINE, ';');
} else {
layout.add(WS.NO_NEWLINE, ';');
}
return layout.toString();
}
private postFormat(query: string): string {
if (this.cfg.tabulateAlias) {
query = formatAliasPositions(query);
}
if (this.cfg.commaPosition === 'before' || this.cfg.commaPosition === 'tabular') {
query = formatCommaPositions(query, this.cfg.commaPosition, indentString(this.cfg));
}
return query;
}
}