Repository: sql-formatter-org/sql-formatter
Pull request #258 (2022-06-28 13:37:22)

8.0: Detect SELECT * in Parser
Detection of the `*` operator when used in `SELECT *`. Now detected inside Parser instead having to do a lookbehind while formatting.


Patch:
diff --git a/src/core/ExpressionFormatter.ts b/src/core/ExpressionFormatter.ts
index 17f2e414..063c78e9 100644
--- a/src/core/ExpressionFormatter.ts
+++ b/src/core/ExpressionFormatter.ts
@@ -5,7 +5,14 @@ import Indentation from './Indentation';
 import InlineBlock from './InlineBlock';
 import Params from './Params';
 import { isReserved, type Token, TokenType, EOF_TOKEN } from './token';
-import { AstNode, BetweenPredicate, isTokenNode, LimitClause, Parenthesis } from './ast';
+import {
+  AllColumnsAsterisk,
+  AstNode,
+  BetweenPredicate,
+  isTokenNode,
+  LimitClause,
+  Parenthesis,
+} from './ast';
 import { indentString } from './config';
 import WhitespaceBuilder, { WS } from './WhitespaceBuilder';
 
@@ -45,6 +52,9 @@ export default class ExpressionFormatter {
         case 'limit_clause':
           this.formatLimitClause(node);
           break;
+        case 'all_columns_asterisk':
+          this.formatAllColumnsAsterisk(node);
+          break;
         case 'token':
           this.formatToken(node.token);
           break;
@@ -53,6 +63,77 @@ export default class ExpressionFormatter {
     return this.query.toString();
   }
 
+  private formatParenthesis(node: Parenthesis) {
+    const inline = this.inlineBlock.isInlineBlock(node);
+
+    const formattedSql = new ExpressionFormatter(this.cfg, this.params, {
+      inline,
+    })
+      .format(node.children)
+      .trimEnd();
+
+    if (inline) {
+      if (!this.isSpaceBeforeParenthesis(node)) {
+        this.query.add(WS.NO_SPACE);
+      }
+      this.query.add(node.openParen, formattedSql, node.closeParen, WS.SPACE);
+    } else {
+      if (!this.isSpaceBeforeParenthesis(node)) {
+        this.query.add(WS.NO_SPACE);
+      }
+      this.query.add(node.openParen);
+
+      formattedSql.split(/\n/).forEach(line => {
+        this.query.add(WS.NEWLINE, WS.INDENT, WS.SINGLE_INDENT, line);
+      });
+
+      this.query.add(WS.NEWLINE, WS.INDENT, node.closeParen, WS.SPACE);
+    }
+  }
+
+  // We add space before parenthesis when:
+  // - there's space in original SQL or
+  // - there's operator or line comment before the parenthesis
+  private isSpaceBeforeParenthesis(node: Parenthesis): boolean {
+    return (
+      node.hasWhitespaceBefore ||
+      [TokenType.LINE_COMMENT, TokenType.OPERATOR].includes(this.tokenLookBehind().type)
+    );
+  }
+
+  private formatBetweenPredicate(node: BetweenPredicate) {
+    this.query.add(
+      this.show(node.betweenToken),
+      WS.SPACE,
+      this.show(node.expr1),
+      WS.SPACE,
+      this.show(node.andToken),
+      WS.SPACE,
+      this.show(node.expr2),
+      WS.SPACE
+    );
+  }
+
+  private formatLimitClause(node: LimitClause) {
+    this.formatCommand(node.limitToken);
+    if (node.offsetToken) {
+      this.query.add(
+        this.show(node.offsetToken),
+        ',',
+        WS.SPACE,
+        this.show(node.countToken),
+        WS.SPACE
+      );
+    } else {
+      this.query.add(this.show(node.countToken), WS.SPACE);
+    }
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  private formatAllColumnsAsterisk(node: AllColumnsAsterisk) {
+    this.query.add('*', WS.SPACE);
+  }
+
   private formatToken(token: Token): void {
     switch (token.type) {
       case TokenType.LINE_COMMENT:
@@ -124,21 +205,6 @@ export default class ExpressionFormatter {
     this.query.add(this.show(token), WS.NEWLINE, WS.INDENT);
   }
 
-  private formatLimitClause(node: LimitClause) {
-    this.formatCommand(node.limitToken);
-    if (node.offsetToken) {
-      this.query.add(
-        this.show(node.offsetToken),
-        ',',
-        WS.SPACE,
-        this.show(node.countToken),
-        WS.SPACE
-      );
-    } else {
-      this.query.add(this.show(node.countToken), WS.SPACE);
-    }
-  }
-
   /**
    * Formats a Reserved Binary Command onto query, joining neighbouring tokens
    */
@@ -197,27 +263,13 @@ export default class ExpressionFormatter {
     }
 
     // other operators
-    // in dense operators mode do not trim whitespace if SELECT *
-    if (this.cfg.denseOperators && this.tokenLookBehind().type !== TokenType.RESERVED_COMMAND) {
+    if (this.cfg.denseOperators) {
       this.query.add(WS.NO_SPACE, this.show(token));
     } else {
       this.query.add(this.show(token), WS.SPACE);
     }
   }
 
-  private formatBetweenPredicate(node: BetweenPredicate) {
-    this.query.add(
-      this.show(node.betweenToken),
-      WS.SPACE,
-      this.show(node.expr1),
-      WS.SPACE,
-      this.show(node.andToken),
-      WS.SPACE,
-      this.show(node.expr2),
-      WS.SPACE
-    );
-  }
-
   /**
    * Formats a Logical Operator onto query, joining boolean conditions
    */
@@ -229,44 +281,6 @@ export default class ExpressionFormatter {
     }
   }
 
-  private formatParenthesis(node: Parenthesis) {
-    const inline = this.inlineBlock.isInlineBlock(node);
-
-    const formattedSql = new ExpressionFormatter(this.cfg, this.params, {
-      inline,
-    })
-      .format(node.children)
-      .trimEnd();
-
-    if (inline) {
-      if (!this.isSpaceBeforeParenthesis(node)) {
-        this.query.add(WS.NO_SPACE);
-      }
-      this.query.add(node.openParen, formattedSql, node.closeParen, WS.SPACE);
-    } else {
-      if (!this.isSpaceBeforeParenthesis(node)) {
-        this.query.add(WS.NO_SPACE);
-      }
-      this.query.add(node.openParen);
-
-      formattedSql.split(/\n/).forEach(line => {
-        this.query.add(WS.NEWLINE, WS.INDENT, WS.SINGLE_INDENT, line);
-      });
-
-      this.query.add(WS.NEWLINE, WS.INDENT, node.closeParen, WS.SPACE);
-    }
-  }
-
-  // We add space before parenthesis when:
-  // - there's space in original SQL or
-  // - there's operator or line comment before the parenthesis
-  private isSpaceBeforeParenthesis(node: Parenthesis): boolean {
-    return (
-      node.hasWhitespaceBefore ||
-      [TokenType.LINE_COMMENT, TokenType.OPERATOR].includes(this.tokenLookBehind().type)
-    );
-  }
-
   private formatCaseStart(token: Token) {
     this.indentation.increaseBlockLevel();
     this.query.add(this.show(token), WS.NEWLINE, WS.INDENT);
diff --git a/src/core/Parser.ts b/src/core/Parser.ts
index 6ced496f..f823cfa4 100644
--- a/src/core/Parser.ts
+++ b/src/core/Parser.ts
@@ -1,5 +1,13 @@
 /* eslint-disable no-cond-assign */
-import { AstNode, BetweenPredicate, LimitClause, Parenthesis, Statement, TokenNode } from './ast';
+import {
+  AllColumnsAsterisk,
+  AstNode,
+  BetweenPredicate,
+  LimitClause,
+  Parenthesis,
+  Statement,
+  TokenNode,
+} from './ast';
 import { EOF_TOKEN, type Token, TokenType, isToken } from './token';
 
 /**
@@ -40,7 +48,11 @@ export default class Parser {
 
   private expression(): AstNode {
     return (
-      this.parenthesis() || this.betweenPredicate() || this.limitClause() || this.nextTokenNode()
+      this.parenthesis() ||
+      this.betweenPredicate() ||
+      this.limitClause() ||
+      this.allColumnsAsterisk() ||
+      this.nextTokenNode()
     );
   }
 
@@ -94,6 +106,14 @@ export default class Parser {
     return undefined;
   }
 
+  private allColumnsAsterisk(): AllColumnsAsterisk | undefined {
+    if (this.look().value === '*' && isToken.SELECT(this.look(-1))) {
+      this.next();
+      return { type: 'all_columns_asterisk' };
+    }
+    return undefined;
+  }
+
   // Returns current token without advancing the pointer
   private look(ahead = 0): Token {
     return this.tokens[this.index + ahead] || EOF_TOKEN;
diff --git a/src/core/ast.ts b/src/core/ast.ts
index 808aec91..b9b93bea 100644
--- a/src/core/ast.ts
+++ b/src/core/ast.ts
@@ -37,6 +37,11 @@ export type LimitClause = {
   offsetToken?: Token;
 };
 
-export type AstNode = Parenthesis | BetweenPredicate | LimitClause | TokenNode;
+// The "*" operator used in SELECT *
+export type AllColumnsAsterisk = {
+  type: 'all_columns_asterisk';
+};
+
+export type AstNode = Parenthesis | BetweenPredicate | LimitClause | AllColumnsAsterisk | TokenNode;
 
 export const isTokenNode = (node: AstNode): node is TokenNode => node.type === 'token';


Test patch:
diff --git a/test/unit/Parser.test.ts b/test/unit/Parser.test.ts
index a642dfba..42b30c0a 100644
--- a/test/unit/Parser.test.ts
+++ b/test/unit/Parser.test.ts
@@ -270,4 +270,28 @@ describe('Parser', () => {
       ]
     `);
   });
+
+  it('parses SELECT *', () => {
+    expect(parse('SELECT *')).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "children": Array [
+            Object {
+              "token": Object {
+                "text": "SELECT",
+                "type": "RESERVED_COMMAND",
+                "value": "SELECT",
+                "whitespaceBefore": "",
+              },
+              "type": "token",
+            },
+            Object {
+              "type": "all_columns_asterisk",
+            },
+          ],
+          "type": "statement",
+        },
+      ]
+    `);
+  });
 });