001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp2.dialect.DialectStrategy; 005import gudusoft.gsqlparser.pp2.island.ClausePart; 006import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 007import gudusoft.gsqlparser.pp2.layout.LayoutContext; 008import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 009import gudusoft.gsqlparser.pp2.layout.LayoutRule; 010import gudusoft.gsqlparser.pp2.token.Pp2Token; 011import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 012import gudusoft.gsqlparser.pp2.token.TokenRole; 013 014import java.util.Collections; 015import java.util.EnumSet; 016import java.util.Locale; 017import java.util.Set; 018 019/** 020 * Requests a linebreak before each major clause keyword so a query lays out one 021 * clause per line: {@code FROM}, {@code WHERE}, {@code GROUP BY}, {@code HAVING}, 022 * {@code ORDER BY}. 023 * 024 * <p>It breaks before a token that carries the {@link TokenRole#KEYWORD_CLAUSE} 025 * role (set by S21) whose {@link ClausePart} is one of the targeted clauses. It 026 * does <b>not</b> break before JOIN keywords (S26 owns JOIN layout) nor before a 027 * clause keyword that immediately follows a master keyword (so {@code DELETE FROM} 028 * / {@code INSERT INTO} stay on one line). {@code SELECT} master layout and set 029 * operators are handled by {@link SetOperatorRules}. 030 * 031 * <p>Priority {@link LayoutPriorities#CLAUSE_LINEBREAK}. Requires the 032 * {@link ClauseScopeResult} to be attached to the context. Iterative; read-only 033 * over tokens. 034 * 035 * <p>Plan reference: §7.3/S25, §7.4/S25. 036 */ 037public final class ClauseLinebreakRules implements LayoutRule { 038 039 private static final Set<ClausePart> BREAK_BEFORE = EnumSet.of( 040 ClausePart.FROM, ClausePart.WHERE, ClausePart.GROUP_BY, 041 ClausePart.HAVING, ClausePart.ORDER_BY); 042 043 @Override 044 public int priority() { return LayoutPriorities.CLAUSE_LINEBREAK; } 045 046 @Override 047 public String name() { return "ClauseLinebreakRules"; } 048 049 @Override 050 public void apply(LayoutContext context) { 051 ClauseScopeResult clause = context.getClauseScope(); 052 if (clause == null) return; // nothing to do without clause analysis 053 Set<String> dialectClauseKeywords = dialectClauseKeywords(context.getDialect()); 054 Pp2TokenStream stream = context.getStream(); 055 int n = Math.min(stream.size(), clause.size()); 056 for (int i = 1; i < n; i++) { 057 Pp2Token t = stream.get(i); 058 boolean universalClause = t.hasRole(TokenRole.KEYWORD_CLAUSE) 059 && BREAK_BEFORE.contains(clause.partAt(i)); 060 boolean dialectClause = startsDialectClausePhrase(stream, i, dialectClauseKeywords); 061 if (!universalClause && !dialectClause) continue; 062 // Keep "DELETE FROM" / "<master> <clause>" together, even with a 063 // comment between the master and the clause keyword. 064 int prev = prevSolid(stream, i); 065 if (prev >= 0 && stream.get(prev).hasRole(TokenRole.KEYWORD_MASTER)) continue; 066 context.requestLinebreaksBefore(i, 1); 067 // Start the clause keyword at column 0 (override the spacing rule's 068 // inter-word blank); indentation, if any, is added by S28. 069 context.requestBlanksBefore(i, 0); 070 } 071 } 072 073 private static Set<String> dialectClauseKeywords(DialectStrategy dialect) { 074 return dialect == null ? Collections.<String>emptySet() 075 : dialect.additionalClauseKeywords(); 076 } 077 078 /** True if a dialect clause-start phrase begins (matches consecutively) at token i. */ 079 private static boolean startsDialectClausePhrase(Pp2TokenStream stream, int i, 080 Set<String> phrases) { 081 if (phrases.isEmpty()) return false; 082 if (stream.get(i).getSourceToken().tokentype != ETokenType.ttkeyword) return false; 083 for (String phrase : phrases) { 084 if (matchesPhraseAt(stream, i, phrase.split("\\s+"))) return true; 085 } 086 return false; 087 } 088 089 /** True if {@code words} match consecutive solid tokens starting at {@code start}. */ 090 private static boolean matchesPhraseAt(Pp2TokenStream stream, int start, String[] words) { 091 int idx = start; 092 for (int k = 0; k < words.length; k++) { 093 if (k > 0) idx = nextSolid(stream, idx); 094 if (idx < 0 || idx >= stream.size()) return false; 095 String text = stream.get(idx).getText(); 096 if (text == null || !text.equalsIgnoreCase(words[k])) return false; 097 } 098 return true; 099 } 100 101 private static int nextSolid(Pp2TokenStream stream, int i) { 102 for (int j = i + 1; j < stream.size(); j++) { 103 ETokenType type = stream.get(j).getSourceToken().tokentype; 104 if (type == ETokenType.ttsimplecomment 105 || type == ETokenType.ttbracketedcomment 106 || type == ETokenType.ttCPPComment) { 107 continue; 108 } 109 return j; 110 } 111 return -1; 112 } 113 114 /** Index of the first non-comment token before {@code i}, or -1. */ 115 private static int prevSolid(Pp2TokenStream stream, int i) { 116 for (int j = i - 1; j >= 0; j--) { 117 ETokenType type = stream.get(j).getSourceToken().tokentype; 118 if (type == ETokenType.ttsimplecomment 119 || type == ETokenType.ttbracketedcomment 120 || type == ETokenType.ttCPPComment) { 121 continue; 122 } 123 return j; 124 } 125 return -1; 126 } 127} 128