001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp.para.GFmtOpt; 005import gudusoft.gsqlparser.pp.para.styleenums.TAlignStyle; 006import gudusoft.gsqlparser.pp.para.styleenums.TLinefeedsCommaOption; 007import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult; 008import gudusoft.gsqlparser.pp2.island.ClausePart; 009import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 010import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult; 011import gudusoft.gsqlparser.pp2.layout.LayoutContext; 012import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 013import gudusoft.gsqlparser.pp2.layout.LayoutRule; 014import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 015 016import java.util.EnumSet; 017import java.util.Set; 018 019/** 020 * Stacks comma-separated list items (SELECT list, GROUP BY, ORDER BY) one per 021 * line, placing the linebreak relative to the comma per 022 * {@link GFmtOpt#selectColumnlistComma}. 023 * 024 * <h2>Which commas</h2> 025 * 026 * <p>Only <i>top-level</i> list commas are stacked: a comma whose paren (block) 027 * depth equals the depth of its clause run's first token. This excludes commas 028 * inside function arguments ({@code count(a, b)}) or nested parens — the 029 * "nested paren handling" of S26. 030 * 031 * <h2>Style</h2> 032 * 033 * <p>Stacking only happens when {@link GFmtOpt#selectColumnlistStyle} is 034 * {@link TAlignStyle#AsStacked} (the default). {@link TAlignStyle#AsWrapped} 035 * (width-based wrapping) is deferred. Comma placement: 036 * <ul> 037 * <li>{@code LfAfterComma} — break before the next item (comma trails the line);</li> 038 * <li>{@code LfBeforeComma} — break before the comma (comma leads the next line);</li> 039 * <li>{@code LfbeforeCommaWithSpace} — break before the comma, with a leading space.</li> 040 * </ul> 041 * 042 * <p>Priority {@link LayoutPriorities#JOIN_PAREN_COMMA}. Needs the S19 block, 043 * S20 sql, and S21 clause analyses attached to the context. Iterative; read-only. 044 * 045 * <p>Plan reference: §7.3/S26, §7.4/S26. 046 */ 047public final class ParenAndCommaRules implements LayoutRule { 048 049 private static final Set<ClausePart> LIST_CLAUSES = EnumSet.of( 050 ClausePart.SELECT_LIST, ClausePart.GROUP_BY, ClausePart.ORDER_BY); 051 052 @Override 053 public int priority() { return LayoutPriorities.JOIN_PAREN_COMMA; } 054 055 @Override 056 public String name() { return "ParenAndCommaRules"; } 057 058 @Override 059 public void apply(LayoutContext context) { 060 ClauseScopeResult clause = context.getClauseScope(); 061 SqlScopeResult sql = context.getSqlScope(); 062 BlockScopeResult block = context.getBlockScope(); 063 if (clause == null || sql == null || block == null) return; 064 065 GFmtOpt opt = context.getOptions().toGFmtOpt(); 066 if (opt.selectColumnlistStyle != TAlignStyle.AsStacked) return; // AsWrapped deferred 067 TLinefeedsCommaOption commaOpt = opt.selectColumnlistComma; 068 069 Pp2TokenStream stream = context.getStream(); 070 int n = min(stream.size(), clause.size(), sql.size(), block.size()); 071 for (int i = 0; i < n; i++) { 072 if (stream.get(i).getSourceToken().tokentype != ETokenType.ttcomma) continue; 073 if (!LIST_CLAUSES.contains(clause.partAt(i))) continue; 074 int runStart = clauseRunStart(clause, sql, i); 075 // Only stack a comma that is a top-level separator of its clause run, 076 // not one nested inside a function call / paren group. 077 if (block.depthAt(i) != block.depthAt(runStart)) continue; 078 applyComma(context, stream, i, commaOpt); 079 } 080 } 081 082 private static void applyComma(LayoutContext ctx, Pp2TokenStream stream, int comma, 083 TLinefeedsCommaOption commaOpt) { 084 if (commaOpt == TLinefeedsCommaOption.LfAfterComma) { 085 int next = nextSolid(stream, comma); 086 if (next >= 0) { 087 ctx.requestLinebreaksBefore(next, 1); 088 ctx.requestBlanksBefore(next, 0); 089 } 090 } else if (commaOpt == TLinefeedsCommaOption.LfBeforeComma) { 091 ctx.requestLinebreaksBefore(comma, 1); 092 ctx.requestBlanksBefore(comma, 0); 093 } else { // LfbeforeCommaWithSpace 094 ctx.requestLinebreaksBefore(comma, 1); 095 ctx.requestBlanksBefore(comma, 1); 096 } 097 } 098 099 /** First index of the contiguous run with the same clause part and SQL level as i. */ 100 private static int clauseRunStart(ClauseScopeResult clause, SqlScopeResult sql, int i) { 101 ClausePart part = clause.partAt(i); 102 int level = sql.levelAt(i); 103 int s = i; 104 while (s - 1 >= 0 && clause.partAt(s - 1) == part && sql.levelAt(s - 1) == level) { 105 s--; 106 } 107 return s; 108 } 109 110 private static int nextSolid(Pp2TokenStream stream, int from) { 111 for (int j = from + 1; j < stream.size(); j++) { 112 ETokenType type = stream.get(j).getSourceToken().tokentype; 113 if (type == ETokenType.ttsimplecomment 114 || type == ETokenType.ttbracketedcomment 115 || type == ETokenType.ttCPPComment) { 116 continue; 117 } 118 return j; 119 } 120 return -1; 121 } 122 123 private static int min(int a, int b, int c, int d) { 124 return Math.min(Math.min(a, b), Math.min(c, d)); 125 } 126}