001package gudusoft.gsqlparser.demos.prettyscore;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.ETokenType;
005import gudusoft.gsqlparser.TGSqlParser;
006import gudusoft.gsqlparser.TSourceToken;
007import gudusoft.gsqlparser.TSourceTokenList;
008
009import java.util.ArrayList;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Random;
013import java.util.Set;
014
015/**
016 * Produces layout-degraded variants of a SQL text that keep every token
017 * (so the fidelity gate still passes) but destroy the layout in a known way.
018 * Used by the monotonicity tests: a sane scorer must rank
019 * {@code formatted > randomIndent > minified} and {@code formatted > oneTokenPerLine}.
020 *
021 * <p>Whitespace only: no token text is ever changed. Lines that lie inside a
022 * multi-line token (block comment, string) are left untouched by
023 * {@link #randomIndent} so comments stay byte-exact.
024 */
025public final class LayoutDegrader {
026
027    private LayoutDegrader() {
028    }
029
030    /** Every token separated by one space on one line (a line comment forces a line break after it). */
031    public static String minify(String sql, EDbVendor vendor) {
032        StringBuilder sb = new StringBuilder(sql.length());
033        boolean needBreak = false;
034        for (TSourceToken t : solidAndCommentTokens(sql, vendor)) {
035            if (sb.length() > 0) sb.append(needBreak ? '\n' : ' ');
036            sb.append(t.toString());
037            needBreak = t.tokentype == ETokenType.ttsimplecomment || t.tokentype == ETokenType.ttCPPComment;
038        }
039        return sb.toString();
040    }
041
042    /** Every token on its own line, no indentation. */
043    public static String oneTokenPerLine(String sql, EDbVendor vendor) {
044        StringBuilder sb = new StringBuilder(sql.length() * 2);
045        for (TSourceToken t : solidAndCommentTokens(sql, vendor)) {
046            if (sb.length() > 0) sb.append('\n');
047            sb.append(t.toString());
048        }
049        return sb.toString();
050    }
051
052    /** Keep the line structure, replace every line's indent by a random 0..{@code maxIndent} spaces. */
053    public static String randomIndent(String sql, EDbVendor vendor, long seed, int maxIndent) {
054        Set<Integer> protectedLines = linesInsideMultiLineTokens(sql, vendor);
055        String[] lines = sql.split("\n", -1);
056        Random rnd = new Random(seed);
057        StringBuilder sb = new StringBuilder(sql.length() + lines.length * 4);
058        for (int i = 0; i < lines.length; i++) {
059            String l = lines[i];
060            if (i > 0) sb.append('\n');
061            if (protectedLines.contains(i) || l.trim().isEmpty()) {
062                sb.append(l);
063                continue;
064            }
065            int k = 0;
066            while (k < l.length() && (l.charAt(k) == ' ' || l.charAt(k) == '\t')) k++;
067            int indent = rnd.nextInt(maxIndent + 1);
068            for (int s = 0; s < indent; s++) sb.append(' ');
069            sb.append(l.substring(k));
070        }
071        return sb.toString();
072    }
073
074    static List<TSourceToken> solidAndCommentTokens(String sql, EDbVendor vendor) {
075        TGSqlParser p = new TGSqlParser(vendor);
076        p.sqltext = sql;
077        p.tokenizeSqltext();
078        TSourceTokenList list = p.getSourcetokenlist();
079        List<TSourceToken> out = new ArrayList<TSourceToken>();
080        long lastEnd = -1;
081        for (int i = 0; i < list.size(); i++) {
082            TSourceToken t = list.get(i);
083            if (t == null) continue;
084            String text = t.toString();
085            if (text == null || text.isEmpty()) continue;
086            if (t.tokentype == ETokenType.ttwhitespace || t.tokentype == ETokenType.ttreturn) continue;
087            if (t.offset < lastEnd) continue; // phantom inner token (see pp2 TokenCoverage)
088            out.add(t);
089            lastEnd = t.offset + text.length();
090        }
091        return out;
092    }
093
094    private static Set<Integer> linesInsideMultiLineTokens(String sql, EDbVendor vendor) {
095        Set<Integer> lines = new HashSet<Integer>();
096        for (TSourceToken t : solidAndCommentTokens(sql, vendor)) {
097            String text = t.toString();
098            int nl = 0;
099            for (int i = 0; i < text.length(); i++) if (text.charAt(i) == '\n') nl++;
100            if (nl == 0) continue;
101            int line = 0;
102            for (int i = 0; i < t.offset && i < sql.length(); i++) if (sql.charAt(i) == '\n') line++;
103            for (int k = 1; k <= nl; k++) lines.add(line + k);
104        }
105        return lines;
106    }
107}