diff --git a/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java b/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java index 91155b1a79..71b0e2499a 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java @@ -102,14 +102,10 @@ String format(String raw) { if (numSpaces > 0) { switch (state.type) { case SPACE: - for (int i = 0; i < numSpaces; ++i) { - builder.append(' '); - } + builder.append(" ".repeat(numSpaces)); break; case TAB: - for (int i = 0; i < numSpaces / state.numSpacesPerTab; ++i) { - builder.append('\t'); - } + builder.append("\t".repeat(Math.max(0, numSpaces / state.numSpacesPerTab))); if (mightBeMultiLineComment && (numSpaces % state.numSpacesPerTab == 1)) { builder.append(' '); } diff --git a/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java b/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java index 6dc3185ef6..0fef566f25 100644 --- a/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java @@ -22,6 +22,8 @@ import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; +import static java.lang.System.lineSeparator; + /** * Removes all semicolons from the end of lines. * @@ -51,7 +53,7 @@ FormatterFunc toFormatter() { String line; while ((line = reader.readLine()) != null) { result.append(removeSemicolon(line)); - result.append(System.lineSeparator()); + result.append(lineSeparator()); } return result.toString(); } diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java index 58a0119a7f..c546de2f17 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java @@ -67,9 +67,7 @@ public DBeaverSQLFormatterConfiguration(Properties properties) { private String getIndentString(String indentType, int indentSize) { char indentChar = indentType.equals("space") ? ' ' : '\t'; StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < indentSize; i++) { - stringBuilder.append(indentChar); - } + stringBuilder.append(String.valueOf(indentChar).repeat(Math.max(0, indentSize))); return stringBuilder.toString(); } diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java index 01f0fd759e..eefeba5f92 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java @@ -38,38 +38,24 @@ public class SQLTokenizedFormatter { private static final String[] JOIN_BEGIN = {"LEFT", "RIGHT", "INNER", "OUTER", "JOIN"}; private static final SQLDialect sqlDialect = SQLDialect.INSTANCE; - private DBeaverSQLFormatterConfiguration formatterCfg; - private List functionBracket = new ArrayList<>(); - private List statementDelimiters = new ArrayList<>(2); + private final DBeaverSQLFormatterConfiguration formatterCfg; + private final List functionBracket = new ArrayList<>(); + private final List statementDelimiters = new ArrayList<>(2); public SQLTokenizedFormatter(DBeaverSQLFormatterConfiguration formatterCfg) { this.formatterCfg = formatterCfg; } public String format(final String argSql) { - statementDelimiters.add(formatterCfg.getStatementDelimiter()); - SQLTokensParser fParser = new SQLTokensParser(); - functionBracket.clear(); - - boolean isSqlEndsWithNewLine = false; - if (argSql.endsWith("\n")) { - isSqlEndsWithNewLine = true; - } - - List list = fParser.parse(argSql); - list = format(list); - StringBuilder after = new StringBuilder(argSql.length() + 20); - for (FormatterToken token : list) { + for (FormatterToken token : format(new SQLTokensParser().parse(argSql))) { after.append(token.getString()); } - - if (isSqlEndsWithNewLine) { + if (argSql.endsWith("\n")) { after.append(getDefaultLineSeparator()); } - return after.toString(); } @@ -94,27 +80,31 @@ private List format(final List argList) { } } - final KeywordCase keywordCase = formatterCfg.getKeywordCase(); for (FormatterToken anArgList : argList) { token = anArgList; if (token.getType() == TokenType.KEYWORD) { - token.setString(keywordCase.transform(token.getString())); + token.setString(formatterCfg.getKeywordCase().transform(token.getString())); } } - // Remove extra tokens (spaces, etc) - for (int index = argList.size() - 1; index >= 1; index--) { - token = argList.get(index); - FormatterToken prevToken = argList.get(index - 1); - if (token.getType() == TokenType.SPACE && (prevToken.getType() == TokenType.SYMBOL || prevToken.getType() == TokenType.COMMENT)) { - argList.remove(index); - } else if ((token.getType() == TokenType.SYMBOL || token.getType() == TokenType.COMMENT) && prevToken.getType() == TokenType.SPACE) { - argList.remove(index - 1); - } else if (token.getType() == TokenType.SPACE) { - token.setString(" "); - } - } + Remove_extra_tokens(argList); + extracted2(argList); + + int indent = 0; + final List bracketIndent = new ArrayList<>(); + FormatterToken prev = new FormatterToken(TokenType.SPACE, " "); + boolean encounterBetween = false; + extracted(argList, prev, bracketIndent, indent, encounterBetween); + + extracted(argList); + + extracted1(argList); + + return argList; + } + + private static void extracted2(List argList) { for (int index = 0; index < argList.size() - 2; index++) { FormatterToken t0 = argList.get(index); FormatterToken t1 = argList.get(index + 1); @@ -146,11 +136,90 @@ private List format(final List argList) { argList.remove(index + 1); } } + } - int indent = 0; - final List bracketIndent = new ArrayList<>(); - FormatterToken prev = new FormatterToken(TokenType.SPACE, " "); - boolean encounterBetween = false; + private static void Remove_extra_tokens(List argList) { + FormatterToken token; + // Remove extra tokens (spaces, etc) + for (int index = argList.size() - 1; index >= 1; index--) { + token = argList.get(index); + FormatterToken prevToken = argList.get(index - 1); + if (token.getType() == TokenType.SPACE && (prevToken.getType() == TokenType.SYMBOL || prevToken.getType() == TokenType.COMMENT)) { + argList.remove(index); + } else if ((token.getType() == TokenType.SYMBOL || token.getType() == TokenType.COMMENT) && prevToken.getType() == TokenType.SPACE) { + argList.remove(index - 1); + } else if (token.getType() == TokenType.SPACE) { + token.setString(" "); + } + } + } + + private void extracted1(List argList) { + FormatterToken prev; + FormatterToken token; + for (int index = 1; index < argList.size(); index++) { + prev = argList.get(index - 1); + token = argList.get(index); + + if (prev.getType() != TokenType.SPACE && + token.getType() != TokenType.SPACE && + !token.getString().startsWith("(")) { + if (token.getString().equals(",") || statementDelimiters.contains(token.getString())) { + continue; + } + if (isFunction(prev.getString()) + && token.getString().equals("(")) { + continue; + } + if (token.getType() == TokenType.VALUE && prev.getType() == TokenType.NAME) { + // Do not add space between name and value [JDBC:MSSQL] + continue; + } + if (token.getType() == TokenType.SYMBOL && isEmbeddedToken(token) || + prev.getType() == TokenType.SYMBOL && isEmbeddedToken(prev)) { + // Do not insert spaces around colons + continue; + } + if (token.getType() == TokenType.SYMBOL && prev.getType() == TokenType.SYMBOL) { + // Do not add space between symbols + continue; + } + if (prev.getType() == TokenType.COMMENT) { + // Do not add spaces to comments + continue; + } + argList.add(index, new FormatterToken(TokenType.SPACE, " ")); + } + } + } + + private static void extracted(List argList) { + for (int index = argList.size() - 1; index >= 4; index--) { + if (index >= argList.size()) { + continue; + } + + FormatterToken t0 = argList.get(index); + FormatterToken t1 = argList.get(index - 1); + FormatterToken t2 = argList.get(index - 2); + FormatterToken t3 = argList.get(index - 3); + FormatterToken t4 = argList.get(index - 4); + + if (t4.getString().equals("(") + && t3.getString().trim().isEmpty() + && t1.getString().trim().isEmpty() + && t0.getString().equalsIgnoreCase(")")) { + t4.setString(t4.getString() + t2.getString() + t0.getString()); + argList.remove(index); + argList.remove(index - 1); + argList.remove(index - 2); + argList.remove(index - 3); + } + } + } + + private void extracted(List argList, FormatterToken prev, List bracketIndent, int indent, boolean encounterBetween) { + FormatterToken token; for (int index = 0; index < argList.size(); index++) { token = argList.get(index); String tokenString = token.getString().toUpperCase(Locale.ENGLISH); @@ -280,66 +349,6 @@ private List format(final List argList) { } prev = token; } - - for (int index = argList.size() - 1; index >= 4; index--) { - if (index >= argList.size()) { - continue; - } - - FormatterToken t0 = argList.get(index); - FormatterToken t1 = argList.get(index - 1); - FormatterToken t2 = argList.get(index - 2); - FormatterToken t3 = argList.get(index - 3); - FormatterToken t4 = argList.get(index - 4); - - if (t4.getString().equals("(") - && t3.getString().trim().isEmpty() - && t1.getString().trim().isEmpty() - && t0.getString().equalsIgnoreCase(")")) { - t4.setString(t4.getString() + t2.getString() + t0.getString()); - argList.remove(index); - argList.remove(index - 1); - argList.remove(index - 2); - argList.remove(index - 3); - } - } - - for (int index = 1; index < argList.size(); index++) { - prev = argList.get(index - 1); - token = argList.get(index); - - if (prev.getType() != TokenType.SPACE && - token.getType() != TokenType.SPACE && - !token.getString().startsWith("(")) { - if (token.getString().equals(",") || statementDelimiters.contains(token.getString())) { - continue; - } - if (isFunction(prev.getString()) - && token.getString().equals("(")) { - continue; - } - if (token.getType() == TokenType.VALUE && prev.getType() == TokenType.NAME) { - // Do not add space between name and value [JDBC:MSSQL] - continue; - } - if (token.getType() == TokenType.SYMBOL && isEmbeddedToken(token) || - prev.getType() == TokenType.SYMBOL && isEmbeddedToken(prev)) { - // Do not insert spaces around colons - continue; - } - if (token.getType() == TokenType.SYMBOL && prev.getType() == TokenType.SYMBOL) { - // Do not add space between symbols - continue; - } - if (prev.getType() == TokenType.COMMENT) { - // Do not add spaces to comments - continue; - } - argList.add(index, new FormatterToken(TokenType.SPACE, " ")); - } - } - - return argList; } private static boolean isEmbeddedToken(FormatterToken token) { @@ -351,7 +360,7 @@ private boolean isJoinStart(List argList, int index) { // And we must be in the beginning of sequence // check current token - if (!contains(JOIN_BEGIN, argList.get(index).getString())) { + if (!contains(argList.get(index).getString())) { return false; } // check previous token @@ -360,7 +369,7 @@ private boolean isJoinStart(List argList, int index) { if (token.getType() == TokenType.SPACE) { continue; } - if (contains(JOIN_BEGIN, token.getString())) { + if (contains(token.getString())) { // It is not the begin of sequence return false; } else { @@ -376,7 +385,7 @@ private boolean isJoinStart(List argList, int index) { if (token.getString().equals("JOIN")) { return true; } - if (!contains(JOIN_BEGIN, token.getString())) { + if (!contains(token.getString())) { // It is not the begin of sequence return false; } @@ -398,14 +407,12 @@ private int insertReturnAndIndent(final List argList, final int try { final String defaultLineSeparator = getDefaultLineSeparator(); StringBuilder s = new StringBuilder(defaultLineSeparator); - for (int index = 0; index < argIndent; index++) { - s.append(formatterCfg.getIndentString()); - } + s.append(String.valueOf(formatterCfg.getIndentString()).repeat(Math.max(0, argIndent))); if (argIndex > 0) { final FormatterToken token = argList.get(argIndex); final FormatterToken prevToken = argList.get(argIndex - 1); if (token.getType() == TokenType.COMMENT && - isCommentLine(sqlDialect, token.getString()) && + isCommentLine(token.getString()) && prevToken.getType() != TokenType.END) { s.setCharAt(0, ' '); s.setLength(1); @@ -446,8 +453,8 @@ private int insertReturnAndIndent(final List argList, final int } } - private static boolean isCommentLine(SQLDialect dialect, String line) { - for (String slc : dialect.getSingleLineComments()) { + private static boolean isCommentLine(String line) { + for (String slc : SQLTokenizedFormatter.sqlDialect.getSingleLineComments()) { if (line.startsWith(slc)) { return true; } @@ -455,10 +462,10 @@ private static boolean isCommentLine(SQLDialect dialect, String line) { return false; } - private static boolean contains(OBJECT_TYPE[] array, OBJECT_TYPE value) { - if (array == null || array.length == 0) + private static boolean contains(OBJECT_TYPE value) { + if (SQLTokenizedFormatter.JOIN_BEGIN == null) return false; - for (OBJECT_TYPE anArray : array) { + for (OBJECT_TYPE anArray : (OBJECT_TYPE[]) SQLTokenizedFormatter.JOIN_BEGIN) { if (Objects.equals(value, anArray)) return true; } diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java index b1931c3c4c..854bd56926 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java @@ -292,7 +292,7 @@ List parse(final String argSql) { } private static boolean contains(char[] array, char value) { - if (array == null || array.length == 0) + if (array == null) return false; for (char aChar : array) { if (aChar == value) diff --git a/lib/src/test/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatterTest.java b/lib/src/test/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatterTest.java new file mode 100644 index 0000000000..dda65fb265 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatterTest.java @@ -0,0 +1,216 @@ +package com.diffplug.spotless.sql.dbeaver; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Properties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SQLTokenizedFormatterTest { + private SQLTokenizedFormatter formatter; + + @BeforeEach + void setUp() { + Properties properties = new Properties(); + properties.setProperty("sql.formatter.keyword.case", "UPPER"); + properties.setProperty("sql.formatter.statement.delimiter", ";"); + properties.setProperty("sql.formatter.indent.type", "space"); + properties.setProperty("sql.formatter.indent.size", "4"); + + DBeaverSQLFormatterConfiguration config = new DBeaverSQLFormatterConfiguration(properties); + formatter = new SQLTokenizedFormatter(config); + } + + // Helper method for normalized comparison + private void assertNormalizedEquals(String input, String expectedFormatted) { + String formatted = formatter.format(input); + String normalizedFormatted = formatted.replaceAll("\\s+", " ").trim(); + String normalizedExpected = expectedFormatted.replaceAll("\\s+", " ").trim(); + assertThat(normalizedFormatted).isEqualTo(normalizedExpected); + } + + @Test + void testSimpleSelect() { + String input = "SELECT * FROM table"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM table"); + } + + @Test + void testSelectWithWhere() { + String input = "SELECT id, name FROM users WHERE age > 18"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT id, name\nFROM users\nWHERE age > 18"); + } + + @Test + void testNestedSelect() { + String input = "SELECT a FROM (SELECT b FROM table2) t"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT a\nFROM (\n SELECT b\n FROM table2\n) t"); + } + + @Test + void testDeeplyNestedQuery() { + String input = "SELECT * FROM (SELECT * FROM (SELECT * FROM table))"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM (\n SELECT *\n FROM (\n SELECT *\n FROM table\n )\n)"); + } + + @Test + void testInnerJoin() { + String input = "SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT u.name, o.amount\nFROM users u\nJOIN orders o\n ON u.id = o.user_id"); + } + + @Test + void testLeftOuterJoin() { + String input = "SELECT u.name, o.amount FROM users u LEFT OUTER JOIN orders o ON u.id = o.user_id"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT u.name, o.amount\nFROM users u\nLEFT OUTER JOIN orders o\n ON u.id = o.user_id"); + } + + @Test + void testFunctionCall() { + String input = "SELECT COUNT(*), MAX(age) FROM users"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT COUNT(*), MAX(age)\nFROM users"); + } + + @Test + void testCommaSeparatedList() { + String input = "SELECT id, name, age, email FROM users"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT id, name, age, email\nFROM users"); + } + + @Test + void testSingleLineComment() { + String input = "SELECT * FROM users -- This is a comment"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM users -- This is a comment"); + } + + @Test + void testMultiLineComment() { + String input = "SELECT * FROM /* Multi-line \n comment */ users"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM /* Multi-line \n comment */ users"); + } + + @Test + void testEmptyInput() { + assertThat(formatter.format("")) + .isEmpty(); + } + + @Test + void testUnclosedParenthesis() { + String input = "SELECT * FROM (SELECT id FROM users"; + assertThatCode(() -> formatter.format(input)) + .doesNotThrowAnyException(); + } + + @Test + void testKeywordUppercase() { + String input = "select * from users"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM users"); + } + + @Test + void testMixedCaseKeywords() { + String input = "SeLeCt * FrOm users"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM users"); + } + + @Test + void testComplexQuery() { + String input = "SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.age > 18 GROUP BY u.name HAVING COUNT(o.id) > 5 ORDER BY u.name"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT u.name, COUNT(o.id)\n" + + "FROM users u\n" + + "LEFT JOIN orders o\n" + + " ON u.id = o.user_id\n" + + "WHERE u.age > 18\n" + + "GROUP BY u.name\n" + + "HAVING COUNT(o.id) > 5\n" + + "ORDER BY u.name"); + } + + @Test + void testStatementDelimiter() { + String input = "SELECT * FROM users; SELECT * FROM orders;"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM users;\n\nSELECT *\nFROM orders;"); + } + + @Test + void testBetweenClause() { + String input = "SELECT * FROM products WHERE price BETWEEN 10 AND 100"; + assertThat(formatter.format(input)) + .isEqualTo("SELECT *\nFROM products\nWHERE price BETWEEN 10 AND 100"); + } + + @Test + void testGroupByOrderBy() { + String input = "SELECT department, COUNT(*) FROM employees GROUP BY department ORDER BY COUNT(*) DESC"; + assertNormalizedEquals(input, + "SELECT department, COUNT(*) FROM employees " + + "GROUP BY department ORDER BY COUNT(*) DESC"); + } + + @Test + void testSubqueryInWhere() { + String input = "SELECT name FROM products WHERE category_id IN (SELECT id FROM categories WHERE active = true)"; + assertNormalizedEquals(input, + "SELECT name FROM products WHERE category_id IN " + + "(SELECT id FROM categories WHERE active = true)"); + } + + @Test + void testCaseStatement() { + String input = "SELECT name, CASE WHEN age < 18 THEN 'minor' WHEN age < 65 THEN 'adult' ELSE 'senior' END AS age_group FROM users"; + assertNormalizedEquals(input, + "SELECT name, CASE WHEN age < 18 THEN 'minor' " + + "WHEN age < 65 THEN 'adult' ELSE 'senior' END AS age_group FROM users"); + } + + @Test + void testUnionQuery() { + String input = "SELECT name FROM active_users UNION SELECT name FROM inactive_users"; + assertNormalizedEquals(input, + "SELECT name FROM active_users UNION SELECT name FROM inactive_users"); + } + + @Test + void testCreateTable() { + String input = "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100), email VARCHAR(255))"; + assertNormalizedEquals(input, input); + } + + @Test + void testInsertStatement() { + String input = "INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@example.com')"; + assertNormalizedEquals(input, + "INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@example.com')"); + } + + @Test + void testWithClause() { + String input = "WITH temp AS (SELECT * FROM users) SELECT * FROM temp"; + assertNormalizedEquals(input, + "WITH temp AS (SELECT * FROM users) SELECT * FROM temp"); + } + + @Test + void testMultipleWithClauses() { + String input = "WITH users_18 AS (SELECT * FROM users WHERE age = 18), users_19 AS (SELECT * FROM users WHERE age = 19) SELECT * FROM users_18 UNION SELECT * FROM users_19"; + assertNormalizedEquals(input, + "WITH users_18 AS (SELECT * FROM users WHERE age = 18), " + + "users_19 AS (SELECT * FROM users WHERE age = 19) " + + "SELECT * FROM users_18 UNION SELECT * FROM users_19"); + } +}