diff --git a/README.md b/README.md index 4102b98ff2..0af55c3c9b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,34 @@ format('SELECT * FROM tbl', { }); ``` +### Disabling the formatter + +You can disable the formatter for a section of SQL by surrounding it with disable/enable comments: + +```sql +/* sql-formatter-disable */ +SELECT * FROM tbl1; +/* sql-formatter-enable */ +SELECT * FROM tbl2; +``` + +which produces: + +```sql +/* sql-formatter-disable */ +SELECT * FROM tbl1; +/* sql-formatter-enable */ +SELECT + * +FROM + tbl2; +``` + +The formatter doesn't even parse the code between these comments. +So in case there's some SQL that happens to crash SQL Formatter, +you can at comment the culprit out (at least until the issue gets +fixed in SQL Formatter). + ### Placeholders replacement In addition to formatting, this library can also perform placeholder replacement in prepared SQL statements: diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 584416b4ef..8a18f7a4bc 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -30,6 +30,7 @@ import { CaseElseNode, DataTypeNode, ParameterizedDataTypeNode, + DisableCommentNode, } from '../parser/ast.js'; import Layout, { WS } from './Layout.js'; @@ -134,6 +135,8 @@ export default class ExpressionFormatter { return this.formatLineComment(node); case NodeType.block_comment: return this.formatBlockComment(node); + case NodeType.disable_comment: + return this.formatBlockComment(node); case NodeType.data_type: return this.formatDataType(node); case NodeType.keyword: @@ -367,8 +370,8 @@ export default class ExpressionFormatter { } } - private formatBlockComment(node: BlockCommentNode) { - if (this.isMultilineBlockComment(node)) { + private formatBlockComment(node: BlockCommentNode | DisableCommentNode) { + if (node.type === NodeType.block_comment && this.isMultilineBlockComment(node)) { this.splitBlockComment(node.text).forEach(line => { this.layout.add(WS.NEWLINE, WS.INDENT, line); }); diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts index 6edc64342a..713eb1ed1b 100644 --- a/src/lexer/Tokenizer.ts +++ b/src/lexer/Tokenizer.ts @@ -31,6 +31,11 @@ export default class Tokenizer { // the Tokenizer config options specified for each SQL dialect private buildRulesBeforeParams(cfg: TokenizerOptions): TokenRule[] { return this.validRules([ + { + type: TokenType.BLOCK_COMMENT, + regex: + /(\/\* *sql-formatter-disable *\*\/[\s\S]*?(?:\/\* *sql-formatter-enable *\*\/|$))/uy, + }, { type: TokenType.BLOCK_COMMENT, regex: cfg.nestedBlockComments ? new NestedComment() : /(\/\*[^]*?\*\/)/uy, diff --git a/src/lexer/token.ts b/src/lexer/token.ts index ee627ef242..2a6c14bb1d 100644 --- a/src/lexer/token.ts +++ b/src/lexer/token.ts @@ -33,6 +33,8 @@ export enum TokenType { CLOSE_PAREN = 'CLOSE_PAREN', LINE_COMMENT = 'LINE_COMMENT', BLOCK_COMMENT = 'BLOCK_COMMENT', + // Text between /* sql-formatter-disable */ and /* sql-formatter-enable */ + DISABLE_COMMENT = 'DISABLE_COMMENT', NUMBER = 'NUMBER', NAMED_PARAMETER = 'NAMED_PARAMETER', QUOTED_PARAMETER = 'QUOTED_PARAMETER', diff --git a/src/parser/ast.ts b/src/parser/ast.ts index ac7939c227..39616421a8 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -24,6 +24,7 @@ export enum NodeType { comma = 'comma', line_comment = 'line_comment', block_comment = 'block_comment', + disable_comment = 'disable_comment', } interface BaseNode { @@ -178,7 +179,13 @@ export interface BlockCommentNode extends BaseNode { precedingWhitespace: string; } -export type CommentNode = LineCommentNode | BlockCommentNode; +export interface DisableCommentNode extends BaseNode { + type: NodeType.disable_comment; + text: string; + precedingWhitespace: string; +} + +export type CommentNode = LineCommentNode | BlockCommentNode | DisableCommentNode; export type AstNode = | ClauseNode @@ -202,4 +209,5 @@ export type AstNode = | OperatorNode | CommaNode | LineCommentNode - | BlockCommentNode; + | BlockCommentNode + | DisableCommentNode; diff --git a/src/parser/grammar.ne b/src/parser/grammar.ne index f6b8f86881..6d6ae542b4 100644 --- a/src/parser/grammar.ne +++ b/src/parser/grammar.ne @@ -380,3 +380,10 @@ comment -> %BLOCK_COMMENT {% precedingWhitespace: token.precedingWhitespace, }) %} +comment -> %DISABLE_COMMENT {% + ([token]) => ({ + type: NodeType.disable_comment, + text: token.text, + precedingWhitespace: token.precedingWhitespace, + }) +%} diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts index bcd8da6537..d8c90f5e0d 100644 --- a/test/behavesLikeSqlFormatter.ts +++ b/test/behavesLikeSqlFormatter.ts @@ -17,11 +17,13 @@ import supportsLogicalOperatorNewline from './options/logicalOperatorNewline.js' import supportsParamTypes from './options/paramTypes.js'; import supportsWindowFunctions from './features/windowFunctions.js'; import supportsFunctionCase from './options/functionCase.js'; +import supportsDisableComment from './features/disableComment.js'; /** * Core tests for all SQL formatters */ export default function behavesLikeSqlFormatter(format: FormatFn) { + supportsDisableComment(format); supportsCase(format); supportsNumbers(format); supportsWith(format); diff --git a/test/features/disableComment.ts b/test/features/disableComment.ts new file mode 100644 index 0000000000..18ebe07e8a --- /dev/null +++ b/test/features/disableComment.ts @@ -0,0 +1,65 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsDisableComment(format: FormatFn) { + it('does not format text between /* sql-formatter-disable */ and /* sql-formatter-enable */', () => { + const result = format(dedent` + SELECT foo FROM bar; + /* sql-formatter-disable */ + SELECT foo FROM bar; + /* sql-formatter-enable */ + SELECT foo FROM bar; + `); + + expect(result).toBe(dedent` + SELECT + foo + FROM + bar; + + /* sql-formatter-disable */ + SELECT foo FROM bar; + /* sql-formatter-enable */ + SELECT + foo + FROM + bar; + `); + }); + + it('does not format text after /* sql-formatter-disable */ until end of file', () => { + const result = format(dedent` + SELECT foo FROM bar; + /* sql-formatter-disable */ + SELECT foo FROM bar; + + SELECT foo FROM bar; + `); + + expect(result).toBe(dedent` + SELECT + foo + FROM + bar; + + /* sql-formatter-disable */ + SELECT foo FROM bar; + + SELECT foo FROM bar; + `); + }); + + it('does not parse code between disable/enable comments', () => { + const result = format(dedent` + SELECT /*sql-formatter-disable*/ ?!{}[] /*sql-formatter-enable*/ FROM bar; + `); + + expect(result).toBe(dedent` + SELECT + /*sql-formatter-disable*/ ?!{}[] /*sql-formatter-enable*/ + FROM + bar; + `); + }); +}