001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp2.layout.LayoutContext; 005import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 006import gudusoft.gsqlparser.pp2.layout.LayoutRule; 007import gudusoft.gsqlparser.pp2.token.Pp2Token; 008import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 009 010/** 011 * The base spacing rule: requests the number of blank columns before each token 012 * so adjacent tokens are separated by a single space, with the usual tight 013 * exceptions around parentheses, commas, periods, and semicolons. 014 * 015 * <h2>Spacing model (per adjacent token pair)</h2> 016 * 017 * <ul> 018 * <li>0 after an open paren {@code (} and before a close paren {@code )};</li> 019 * <li>0 before a comma or semicolon;</li> 020 * <li>0 around a period (qualified names {@code a.b});</li> 021 * <li>0 before an open paren {@code (} (function calls and groups stay tight — 022 * keyword-introduced groups are re-spaced by the higher-priority S26 rule);</li> 023 * <li>1 otherwise (operators, keywords, identifiers, {@code *}).</li> 024 * </ul> 025 * 026 * <p>This is the lowest-priority layout rule ({@link LayoutPriorities#SPACING}), 027 * so later rules (clause linebreaks, indent, alignment) override its decisions 028 * where they apply. It only sets {@code blanksBefore}; linebreaks/indent are 029 * other rules' concern. Spacing around comment tokens is left to the original 030 * whitespace (untouched) to avoid disturbing comment placement. 031 * 032 * <p>Iterative single pass; read-only over tokens. Plan reference: §7.3/S24, §7.4/S24. 033 */ 034public final class SpacingRules implements LayoutRule { 035 036 @Override 037 public int priority() { return LayoutPriorities.SPACING; } 038 039 @Override 040 public String name() { return "SpacingRules"; } 041 042 @Override 043 public void apply(LayoutContext context) { 044 Pp2TokenStream stream = context.getStream(); 045 int n = stream.size(); 046 for (int i = 1; i < n; i++) { 047 Pp2Token prev = stream.get(i - 1); 048 Pp2Token cur = stream.get(i); 049 ETokenType prevType = prev.getSourceToken().tokentype; 050 ETokenType curType = cur.getSourceToken().tokentype; 051 // Always keep at least one space adjacent to a comment so a comment 052 // can never glue to a token (e.g. SELECT/*c*/a -> SELECT /*c*/ a). 053 // Linebreaks around line comments are preserved separately (this rule 054 // does not touch linebreaksBefore). 055 if (isComment(prevType) || isComment(curType)) { 056 context.requestBlanksBefore(i, 1); 057 continue; 058 } 059 // A unary sign ( -1, +col, =-1, (-1) ) stays tight to its operand. 060 if (isSign(prev) && isUnaryPosition(stream, i - 1)) { 061 context.requestBlanksBefore(i, 0); 062 continue; 063 } 064 context.requestBlanksBefore(i, blanksBetween(prevType, curType)); 065 } 066 } 067 068 /** Whether the token is a {@code +} or {@code -} sign. */ 069 private static boolean isSign(Pp2Token t) { 070 String text = t.getText(); 071 return "+".equals(text) || "-".equals(text); 072 } 073 074 /** 075 * Whether the sign token at {@code signIndex} is in a unary position: it is 076 * the first token, or the token before it starts an expression (an open 077 * paren, comma, keyword, or another operator) rather than ending an operand. 078 */ 079 private static boolean isUnaryPosition(Pp2TokenStream stream, int signIndex) { 080 if (signIndex == 0) return true; 081 Pp2Token before = stream.get(signIndex - 1); 082 ETokenType type = before.getSourceToken().tokentype; 083 if (type == ETokenType.ttleftparenthesis 084 || type == ETokenType.ttcomma 085 || type == ETokenType.ttkeyword) { 086 return true; 087 } 088 return isOperator(before); 089 } 090 091 /** Heuristic operator detection by token type or symbol text. */ 092 private static boolean isOperator(Pp2Token t) { 093 if (t.getSourceToken().tokentype == ETokenType.ttsinglecharoperator) return true; 094 String s = t.getText(); 095 if (s == null || s.isEmpty()) return false; 096 return "=".equals(s) || "<".equals(s) || ">".equals(s) || "<=".equals(s) 097 || ">=".equals(s) || "<>".equals(s) || "!=".equals(s) || "+".equals(s) 098 || "-".equals(s) || "*".equals(s) || "/".equals(s) || "%".equals(s) 099 || "||".equals(s); 100 } 101 102 private static int blanksBetween(ETokenType prev, ETokenType cur) { 103 if (prev == ETokenType.ttleftparenthesis) return 0; // no space after "(" 104 if (cur == ETokenType.ttrightparenthesis) return 0; // no space before ")" 105 if (cur == ETokenType.ttcomma) return 0; // no space before "," 106 if (cur == ETokenType.ttsemicolon) return 0; // no space before ";" 107 if (cur == ETokenType.ttperiod || prev == ETokenType.ttperiod) return 0; // a.b 108 if (cur == ETokenType.ttleftparenthesis) return 0; // f( / group( stays tight 109 return 1; 110 } 111 112 private static boolean isComment(ETokenType type) { 113 return type == ETokenType.ttsimplecomment 114 || type == ETokenType.ttbracketedcomment 115 || type == ETokenType.ttCPPComment; 116 } 117}