diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e6f00d..706a886 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,9 @@ "jakarta", "openrewrite", "plsql" - ] + ], + "sonarlint.connectedMode.project": { + "connectionId": "SonarQube for IDE - Visual Studio Code", + "projectKey": "mhagnumdw_rewrite-format-sql" + } } diff --git a/README.md b/README.md index 19f21b3..7b20227 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ A set of [OpenRewrite](https://docs.openrewrite.org/) recipes for formatting SQL - [Recipes](#recipes) - [FormatSqlBlockRecipe](#formatsqlblockrecipe) + - [FormatSqlTextBlockRecipe](#formatsqltextblockrecipe) - [FormatSqlFileRecipe](#formatsqlfilerecipe) - [Configurable Options](#configurable-options) - [Examples](#examples) - [FormatSqlBlockRecipe Example](#formatsqlblockrecipe-example) + - [FormatSqlTextBlockRecipe Example](#formatsqltextblockrecipe-example) - [FormatSqlFileRecipe Example](#formatsqlfilerecipe-example) - [Usage](#usage) - [Configuring in `pom.xml`](#configuring-in-pomxml) @@ -33,17 +35,23 @@ The `io.github.mhagnumdw.FormatSqlBlockRecipe` recipe automatically formats SQL > Future enhancements may allow configuration of custom annotations. Please open an issue. +### FormatSqlTextBlockRecipe + +The `io.github.mhagnumdw.FormatSqlTextBlockRecipe` recipe formats SQL code in Java [Text Blocks](https://docs.oracle.com/en/java/javase/13/text_blocks/index.html) that are preceded by a `// language=sql` comment (case-insensitive). + +This is the same [language injection comment](https://www.jetbrains.com/help/idea/language-injections.html) recognized by IntelliJ IDEA for SQL syntax highlighting. + ### FormatSqlFileRecipe The `io.github.mhagnumdw.FormatSqlFileRecipe` recipe automatically formats the content of SQL files. ## Configurable Options -The following options are applicable to both `FormatSqlBlockRecipe` and `FormatSqlFileRecipe`: +The following options are applicable to `FormatSqlBlockRecipe`, `FormatSqlTextBlockRecipe`, and `FormatSqlFileRecipe`: | Type | Name | Description | Example | Default Value | | :------ | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------- | -| String | `filePath` | Optional. The path to the files that the Recipe should process. Accepts a glob expression; multiple patterns can be specified, separated by a semicolon `;`. If omitted, processes all matching files. | `**/*DAO.java`
`**/*.sql` | FormatSqlBlockRecipe: `**/*.java`
FormatSqlFileRecipe: `**/*.sql` | +| String | `filePath` | Optional. The path to the files that the Recipe should process. Accepts a glob expression; multiple patterns can be specified, separated by a semicolon `;`. If omitted, processes all matching files. | `**/*DAO.java`
`**/*.sql` | FormatSqlBlockRecipe: `**/*.java`
FormatSqlTextBlockRecipe: `**/*.java`
FormatSqlFileRecipe: `**/*.sql` | | String | `sqlDialect` | Optional. The SQL dialect to be used for formatting. Valid options: `sql` (StandardSql), `mysql`, `postgresql`, `db2`, `plsql` (Oracle PL/SQL), `n1ql` (Couchbase N1QL), `redshift`, `spark`, `tsql` (SQL Server Transact-SQL). Details [here](https://github.com/vertical-blank/sql-formatter). | `plsql` | `sql` | | String | `indent` | Optional. The string to be used for indentation. | `" "` for 2 spaces
`"\t"` for a tab | 4 spaces `" "` | | Integer | `maxColumnLength` | Optional. The maximum length of a line before the formatter tries to break it. | `100` | `120` | @@ -91,6 +99,33 @@ public interface HolidayRepository { } ``` +### FormatSqlTextBlockRecipe Example + +Before + +```java +// language=sql +private static final String QUERY = """ + select * from users u inner join orders o on u.id = o.user_id where u.active = true order by u.name + """; +``` + +After + +```java +// language=sql +private static final String QUERY = """ + select + * + from + users u + inner join orders o on u.id = o.user_id + where + u.active = true + order by + u.name"""; +``` + ### FormatSqlFileRecipe Example Consider the following `example.sql` file: @@ -140,6 +175,7 @@ Inside the plugins section, add: io.github.mhagnumdw.FormatSqlBlockRecipe + io.github.mhagnumdw.FormatSqlTextBlockRecipe io.github.mhagnumdw.FormatSqlFileRecipe false @@ -170,6 +206,8 @@ recipeList: # Add the Recipes you want to use here - io.github.mhagnumdw.FormatSqlBlockRecipe: sqlDialect: "plsql" + - io.github.mhagnumdw.FormatSqlTextBlockRecipe: + sqlDialect: "plsql" - io.github.mhagnumdw.FormatSqlFileRecipe: sqlDialect: "mysql" ``` @@ -185,7 +223,7 @@ And change the `` tag in `pom.xml` to: io.github.mhagnumdw.FormatSqlCustomConfig ``` -> As in this example the `FormatSqlCustomConfig` recipe includes both `FormatSqlBlockRecipe` and `FormatSqlFileRecipe` recipes, in `pom.xml` it is only necessary to define the `FormatSqlCustomConfig` recipe. +> As in this example the `FormatSqlCustomConfig` recipe includes both `FormatSqlBlockRecipe`, `FormatSqlTextBlockRecipe` and `FormatSqlFileRecipe` recipes, in `pom.xml` it is only necessary to define the `FormatSqlCustomConfig` recipe. Then run: @@ -201,7 +239,7 @@ This mode is indicated if your intention is to run the recipe only once. ```bash ./mvnw org.openrewrite.maven:rewrite-maven-plugin:run \ - -Drewrite.activeRecipes=io.github.mhagnumdw.FormatSqlBlockRecipe,io.github.mhagnumdw.FormatSqlFileRecipe \ + -Drewrite.activeRecipes=io.github.mhagnumdw.FormatSqlBlockRecipe,io.github.mhagnumdw.FormatSqlTextBlockRecipe,io.github.mhagnumdw.FormatSqlFileRecipe \ -Drewrite.recipeArtifactCoordinates=io.github.mhagnumdw:rewrite-format-sql:1.0.0 ``` diff --git a/pom.xml b/pom.xml index 4c6a9a4..a097a62 100644 --- a/pom.xml +++ b/pom.xml @@ -94,11 +94,21 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-engine + test + org.junit.jupiter junit-jupiter-params test + + org.junit.platform + junit-platform-launcher + test + org.openrewrite rewrite-java-17 diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlBlockRecipe.java b/src/main/java/io/github/mhagnumdw/FormatSqlBlockRecipe.java index 3cde71c..6ccf2af 100644 --- a/src/main/java/io/github/mhagnumdw/FormatSqlBlockRecipe.java +++ b/src/main/java/io/github/mhagnumdw/FormatSqlBlockRecipe.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; import lombok.EqualsAndHashCode; import lombok.Value; import org.jspecify.annotations.Nullable; @@ -11,9 +13,6 @@ import org.openrewrite.TreeVisitor; import org.openrewrite.java.search.UsesJavaVersion; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - /** * A recipe that formats SQL/HQL in Text Blocks within Java source files. */ diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlBlockVisitor.java b/src/main/java/io/github/mhagnumdw/FormatSqlBlockVisitor.java index 2c299cd..05e3913 100644 --- a/src/main/java/io/github/mhagnumdw/FormatSqlBlockVisitor.java +++ b/src/main/java/io/github/mhagnumdw/FormatSqlBlockVisitor.java @@ -1,14 +1,13 @@ package io.github.mhagnumdw; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; import io.github.mhagnumdw.processors.Annotations; import org.openrewrite.ExecutionContext; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - public class FormatSqlBlockVisitor extends JavaIsoVisitor { private final Dialect dialect; diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlFileRecipe.java b/src/main/java/io/github/mhagnumdw/FormatSqlFileRecipe.java index 27c0207..82f9fea 100644 --- a/src/main/java/io/github/mhagnumdw/FormatSqlFileRecipe.java +++ b/src/main/java/io/github/mhagnumdw/FormatSqlFileRecipe.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; import lombok.EqualsAndHashCode; import lombok.Value; import org.jspecify.annotations.Nullable; @@ -10,9 +12,6 @@ import org.openrewrite.Preconditions; import org.openrewrite.TreeVisitor; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - /** * A recipe that formats SQL text files. */ diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlFileVisitor.java b/src/main/java/io/github/mhagnumdw/FormatSqlFileVisitor.java index ef3a2b5..716ddcc 100644 --- a/src/main/java/io/github/mhagnumdw/FormatSqlFileVisitor.java +++ b/src/main/java/io/github/mhagnumdw/FormatSqlFileVisitor.java @@ -1,5 +1,8 @@ package io.github.mhagnumdw; +import com.github.vertical_blank.sqlformatter.SqlFormatter; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; import org.openrewrite.SourceFile; @@ -9,10 +12,6 @@ import java.util.Objects; -import com.github.vertical_blank.sqlformatter.SqlFormatter; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - public class FormatSqlFileVisitor extends TreeVisitor { private final Dialect dialect; diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlRecipeAbstract.java b/src/main/java/io/github/mhagnumdw/FormatSqlRecipeAbstract.java index 0130924..45878c8 100644 --- a/src/main/java/io/github/mhagnumdw/FormatSqlRecipeAbstract.java +++ b/src/main/java/io/github/mhagnumdw/FormatSqlRecipeAbstract.java @@ -1,6 +1,8 @@ package io.github.mhagnumdw; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; import lombok.EqualsAndHashCode; import lombok.Getter; import org.jspecify.annotations.Nullable; @@ -9,9 +11,6 @@ import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - /** * Abstract base class for SQL formatting recipes. * Provides common configuration options and methods for SQL formatting. diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockRecipe.java b/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockRecipe.java new file mode 100644 index 0000000..d50565f --- /dev/null +++ b/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockRecipe.java @@ -0,0 +1,46 @@ +package io.github.mhagnumdw; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.FindSourceFiles; +import org.openrewrite.Preconditions; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.search.UsesJavaVersion; + +@Value +@EqualsAndHashCode(callSuper = false) +public class FormatSqlTextBlockRecipe extends FormatSqlRecipeAbstract { + + private static final String DEFAULT_FILE_PATH = "**/*.java"; + + @JsonCreator + public FormatSqlTextBlockRecipe( + @Nullable @JsonProperty("filePath") String filePath, + @Nullable @JsonProperty("sqlDialect") String sqlDialect, + @Nullable @JsonProperty("indent") String indent, + @Nullable @JsonProperty("maxColumnLength") Integer maxColumnLength, + @Nullable @JsonProperty("uppercase") Boolean uppercase) { + super(filePath == null ? DEFAULT_FILE_PATH : filePath, sqlDialect, indent, maxColumnLength, uppercase); + } + + String displayName = "Format SQL Text Blocks marked with language injection comment"; + + String description = "Formats SQL code in Java Text Blocks that are preceded by " + + "a '// language=sql' comment (case-insensitive)."; + + @Override + TreeVisitor getFormattingVisitor( + Dialect dialect, FormatConfig formatConfig) { + TreeVisitor check = Preconditions.and( + new FindSourceFiles(getFilePath()).getVisitor(), + new UsesJavaVersion<>(13) // Text blocks were introduced as a preview feature in Java 13 and became a standard feature in Java 15 + ); + return Preconditions.check(check, new FormatSqlTextBlockVisitor(dialect, formatConfig)); + } +} diff --git a/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockVisitor.java b/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockVisitor.java new file mode 100644 index 0000000..11008bb --- /dev/null +++ b/src/main/java/io/github/mhagnumdw/FormatSqlTextBlockVisitor.java @@ -0,0 +1,89 @@ +package io.github.mhagnumdw; + +import static org.openrewrite.java.tree.J.Literal; + +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; +import org.openrewrite.ExecutionContext; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.Comment; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TextComment; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class FormatSqlTextBlockVisitor extends JavaIsoVisitor { + + private static final Pattern LANGUAGE_SQL_PATTERN = Pattern.compile( + "\\s*language\\s*=\\s*sql\\s*", Pattern.CASE_INSENSITIVE + ); + + private final Dialect dialect; + private final FormatConfig formatConfig; + + FormatSqlTextBlockVisitor(Dialect dialect, FormatConfig formatConfig) { + this.dialect = dialect; + this.formatConfig = formatConfig; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations varDecls, ExecutionContext ctx) { + + if (!hasLanguageSqlComment(varDecls)) { + return varDecls; + } + + List variables = varDecls.getVariables(); + boolean changed = false; + + for (int i = 0; i < variables.size(); i++) { + J.VariableDeclarations.NamedVariable variable = variables.get(i); + Expression initializer = variable.getInitializer(); + + if (initializer == null || !TextBlockUtil.isTextBlock(initializer)) { + continue; + } + + String indentation = getParentIndentation(varDecls) + + TextBlockUtil.getFileIndent(getCursor()); + + Literal newLiteral = TextBlockUtil.formatTextBlock( + (Literal) initializer, indentation, dialect, formatConfig); + + if (newLiteral != null) { + variable = variable.withInitializer(newLiteral); + List newVariables = new ArrayList<>(variables); + newVariables.set(i, variable); + variables = newVariables; + changed = true; + } + } + + if (!changed) { + return varDecls; + } + + return varDecls.withVariables(variables); + } + + private static boolean hasLanguageSqlComment(J.VariableDeclarations varDecls) { + List comments = varDecls.getPrefix().getComments(); + + for (Comment comment : comments) { + if (comment instanceof TextComment) { + TextComment tc = (TextComment) comment; + if (!tc.isMultiline() && LANGUAGE_SQL_PATTERN.matcher(tc.getText()).matches()) { + return true; + } + } + } + return false; + } + + private static String getParentIndentation(J.VariableDeclarations varDecls) { + return varDecls.getPrefix().getIndent(); + } +} diff --git a/src/main/java/io/github/mhagnumdw/TextBlockUtil.java b/src/main/java/io/github/mhagnumdw/TextBlockUtil.java new file mode 100644 index 0000000..5e7ed78 --- /dev/null +++ b/src/main/java/io/github/mhagnumdw/TextBlockUtil.java @@ -0,0 +1,135 @@ +package io.github.mhagnumdw; + +import static org.openrewrite.Tree.randomId; + +import com.github.vertical_blank.sqlformatter.SqlFormatter; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.java.style.IntelliJ; +import org.openrewrite.java.style.TabsAndIndentsStyle; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Markers; +import org.openrewrite.style.Style; + +/** + * Utility class for formatting SQL in Java Text Blocks. + * Provides shared methods used by both annotation processors and the Text Block visitor. + */ +public final class TextBlockUtil { + + private TextBlockUtil() {} + + /** + * Checks if the given expression is a Java Text Block ("""..."""). + * + * @param expr The expression to check + * @return true if the expression is a text block, false otherwise + * @see UseTextBlocks.java + */ + public static boolean isTextBlock(Expression expr) { + if (expr instanceof J.Literal) { + J.Literal l = (J.Literal) expr; + return TypeUtils.isString(l.getType()) && + l.getValueSource() != null && + l.getValueSource().startsWith("\"\"\""); + } + return false; + } + + /** + * Formats SQL in a text block literal and returns the new literal, + * or null if nothing changed. + * + * @param literal The text block literal containing SQL + * @param indentation The indentation string to apply to formatted SQL + * @param dialect The SQL dialect to use for formatting + * @param formatConfig The format configuration + * @return The new formatted literal, or null if nothing changed + */ + public static J.@Nullable Literal formatTextBlock( + J.Literal literal, + String indentation, + Dialect dialect, + FormatConfig formatConfig) { + + String sql = (String) literal.getValue(); + if (sql == null) { + return null; + } + + String sqlFormattedRaw = SqlFormatter.of(dialect).format(sql, formatConfig); + + if (sqlFormattedRaw.equals(sql)) { + return null; + } + + String sqlFormatted = sqlFormattedRaw.replace("\n", "\n" + indentation); + sqlFormatted = "\n" + indentation + sqlFormatted; + + if (sqlFormatted.equals(literal.getValue())) { + return null; + } + + return new J.Literal( + randomId(), + literal.getPrefix(), + Markers.EMPTY, + sqlFormatted, + String.format("\"\"\"%s\"\"\"", sqlFormatted), + null, + JavaType.Primitive.String + ); + } + + /** + * Retrieves the file indentation from the cursor context. + * + * @param cursor The cursor to extract indentation from + * @return The indentation string (spaces or tab) + */ + public static String getFileIndent(Cursor cursor) { + JavaSourceFile sf = cursor.firstEnclosingOrThrow(JavaSourceFile.class); + TabsAndIndentsStyle style = Style.from(TabsAndIndentsStyle.class, sf); + if (style == null) { + style = IntelliJ.tabsAndIndents(); + } + + boolean useTab = style.getUseTabCharacter(); + int tabSize = style.getTabSize(); + + if (useTab) { + return "\t"; + } + return StringUtils.repeat(" ", tabSize); + } + + /** + * Retrieves the indentation of the parent node from the cursor context. + * + * @param cursor The cursor to extract indentation from + * @return The indentation string (spaces or tab) + */ + public static String getParentIndentation(Cursor cursor) { + Cursor parentCursor = cursor.getParent(); + + while (parentCursor != null) { + Object parent = parentCursor.getValue(); + + if (parent instanceof J.MethodDeclaration) { + J.MethodDeclaration lstNode = (J.MethodDeclaration) parent; + return lstNode.getPrefix().getIndent(); + } + + parentCursor = parentCursor.getParent(); + } + + return ""; + } +} diff --git a/src/main/java/io/github/mhagnumdw/processors/AnnotationOnlyOneArgumentProcessor.java b/src/main/java/io/github/mhagnumdw/processors/AnnotationOnlyOneArgumentProcessor.java index 5f4bc02..c4cef40 100644 --- a/src/main/java/io/github/mhagnumdw/processors/AnnotationOnlyOneArgumentProcessor.java +++ b/src/main/java/io/github/mhagnumdw/processors/AnnotationOnlyOneArgumentProcessor.java @@ -1,27 +1,16 @@ package io.github.mhagnumdw.processors; import static java.util.Collections.singletonList; -import static org.openrewrite.Tree.randomId; +import com.github.vertical_blank.sqlformatter.core.FormatConfig; +import com.github.vertical_blank.sqlformatter.languages.Dialect; +import io.github.mhagnumdw.TextBlockUtil; import org.openrewrite.Cursor; -import org.openrewrite.internal.StringUtils; -import org.openrewrite.java.style.IntelliJ; -import org.openrewrite.java.style.TabsAndIndentsStyle; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.J.Literal; -import org.openrewrite.java.tree.JavaSourceFile; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.TypeUtils; -import org.openrewrite.marker.Markers; -import org.openrewrite.style.Style; import java.util.List; -import com.github.vertical_blank.sqlformatter.SqlFormatter; -import com.github.vertical_blank.sqlformatter.core.FormatConfig; -import com.github.vertical_blank.sqlformatter.languages.Dialect; - /** * This abstract class provides a common tasks for processing annotations that contain an annotation with single SQL/HQL * string as their argument. It handles the extraction of the query, formatting it, and updating the annotation with @@ -34,12 +23,11 @@ abstract class AnnotationOnlyOneArgumentProcessor implements AnnotationProcessor @Override public final J.Annotation process(J.Annotation annotation, Cursor cursor, Dialect dialect, FormatConfig formatConfig) { - JavaType type = annotation.getType(); - if (type == null) { + if (annotation.getType() == null) { return annotation; } - if (!getFQN().equals(type.toString())) { + if (!getFQN().equals(annotation.getType().toString())) { return annotation; } @@ -51,78 +39,21 @@ public final J.Annotation process(J.Annotation annotation, Cursor cursor, Dialec Expression arg = args.get(0); - if (!isTextBlock(arg)) { + if (!TextBlockUtil.isTextBlock(arg)) { return annotation; } - J.Literal literal = (Literal) arg; - String sql = (String) literal.getValue(); - - String sqlFormatted = SqlFormatter.of(dialect).format(sql, formatConfig); - - String indentation = getParentIndentation(cursor) + getFileIndent(cursor); + J.Literal literal = (J.Literal) arg; + String indentation = TextBlockUtil.getParentIndentation(cursor) + TextBlockUtil.getFileIndent(cursor); - // handle preceding indentation - sqlFormatted = sqlFormatted.replace("\n", "\n" + indentation); + J.Literal newLiteral = TextBlockUtil.formatTextBlock(literal, indentation, dialect, formatConfig); - // add first line - sqlFormatted = "\n" + indentation + sqlFormatted; - - if (sqlFormatted.equals(sql)) { + if (newLiteral == null) { // nothing has changed return annotation; } - J.Literal newLiteral = new J.Literal(randomId(), literal.getPrefix(), Markers.EMPTY, sqlFormatted, - String.format("\"\"\"%s\"\"\"", sqlFormatted), null, JavaType.Primitive.String); - return annotation.withArguments(singletonList(newLiteral)); } - // Retrieve the file indentation based on the style of the file - private String getFileIndent(Cursor cursor) { - JavaSourceFile sf = cursor.firstEnclosingOrThrow(JavaSourceFile.class); - TabsAndIndentsStyle style = Style.from(TabsAndIndentsStyle.class, sf); - if (style == null) { - style = IntelliJ.tabsAndIndents(); - } - - boolean useTab = style.getUseTabCharacter(); - int tabSize = style.getTabSize(); - - if (useTab) { - return "\t"; - } - return StringUtils.repeat(" ", tabSize); - } - - // From: https://github.com/openrewrite/rewrite-migrate-java/blob/main/src/main/java/org/openrewrite/java/migrate/lang/UseTextBlocks.java - private static boolean isTextBlock(Expression expr) { - if (expr instanceof J.Literal) { - J.Literal l = (J.Literal) expr; - return TypeUtils.isString(l.getType()) && - l.getValueSource() != null && - l.getValueSource().startsWith("\"\"\""); - } - return false; - } - - // Retrieve the indentation of the parent method declaration - private static String getParentIndentation(Cursor cursor) { - Cursor parentCursor = cursor.getParent(); - - while (parentCursor != null) { - Object parent = parentCursor.getValue(); - - if (parent instanceof J.MethodDeclaration) { - J.MethodDeclaration lstNode = (J.MethodDeclaration) parent; - return lstNode.getPrefix().getIndent(); - } - - parentCursor = parentCursor.getParent(); - } - - return ""; - } - } diff --git a/src/main/java/io/github/mhagnumdw/processors/AnnotationProcessor.java b/src/main/java/io/github/mhagnumdw/processors/AnnotationProcessor.java index 17d3c40..d2e5efc 100644 --- a/src/main/java/io/github/mhagnumdw/processors/AnnotationProcessor.java +++ b/src/main/java/io/github/mhagnumdw/processors/AnnotationProcessor.java @@ -1,10 +1,9 @@ package io.github.mhagnumdw.processors; -import org.openrewrite.Cursor; -import org.openrewrite.java.tree.J; - import com.github.vertical_blank.sqlformatter.core.FormatConfig; import com.github.vertical_blank.sqlformatter.languages.Dialect; +import org.openrewrite.Cursor; +import org.openrewrite.java.tree.J; /** * This interface defines the contract for processing annotations that contain an annotation with SQL/HQL. diff --git a/src/main/resources/META-INF/rewrite/examples.yml b/src/main/resources/META-INF/rewrite/examples.yml index 849af9c..215aa43 100644 --- a/src/main/resources/META-INF/rewrite/examples.yml +++ b/src/main/resources/META-INF/rewrite/examples.yml @@ -43,3 +43,40 @@ examples: void select(); } language: java +--- +type: specs.openrewrite.org/v1beta/example +recipeName: io.github.mhagnumdw.FormatSqlTextBlockRecipe +examples: +- description: '`FormatSqlTextBlockRecipeTest#shouldFormatFieldWithLanguageSqlComment`' + parameters: + - io/github/mhagnumdw/test/*.java + - sql + - 'null' + - 'null' + - 'null' + sources: + - before: | + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = """ + select * from users u inner join orders o on u.id = o.user_id where u.active = true order by u.name"""; + } + after: | + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = """ + select + * + from + users u + inner join orders o on u.id = o.user_id + where + u.active = true + order by + u.name"""; + } + language: java diff --git a/src/test/java/io/github/mhagnumdw/FormatSqlTextBlockRecipeTest.java b/src/test/java/io/github/mhagnumdw/FormatSqlTextBlockRecipeTest.java new file mode 100644 index 0000000..1b117b7 --- /dev/null +++ b/src/test/java/io/github/mhagnumdw/FormatSqlTextBlockRecipeTest.java @@ -0,0 +1,318 @@ +package io.github.mhagnumdw; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +// SonarQube doesn't recognize internal assertions in rewriteRun() +@SuppressWarnings("java:S2699") +class FormatSqlTextBlockRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe( + new FormatSqlTextBlockRecipe( + "io/github/mhagnumdw/test/*.java", + "sql", + null, + null, + null + ) + ) + .allSources(s -> s.markers( + // https://docs.openrewrite.org/authoring-recipes/recipe-testing#specifying-java-versions + javaVersion(13) // Text blocks were introduced as a preview feature in Java 13 and became a standard feature in Java 15 + )); + } + + @DocumentExample + @Test + void shouldFormatFieldWithLanguageSqlComment() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = \""" + select * from users u inner join orders o on u.id = o.user_id where u.active = true order by u.name\"""; + } + """, + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = \""" + select + * + from + users u + inner join orders o on u.id = o.user_id + where + u.active = true + order by + u.name\"""; + } + """ + ) + ); + } + + @Test + void shouldFormatLocalVariableWithLanguageSqlComment() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + public void test() { + // language=sql + String query = \""" + select * from users where active = true\"""; + } + } + """, + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + public void test() { + // language=sql + String query = \""" + select + * + from + users + where + active = true\"""; + } + } + """ + ) + ); + } + + @Test + void shouldBeCaseInsensitive() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // LANGUAGE=SQL + private static final String Q1 = \""" + select * from users\"""; + // Language=Sql + private static final String Q2 = \""" + select * from orders\"""; + //language=sql + private static final String Q3 = \""" + select * from products\"""; + } + """, + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // LANGUAGE=SQL + private static final String Q1 = \""" + select + * + from + users\"""; + // Language=Sql + private static final String Q2 = \""" + select + * + from + orders\"""; + //language=sql + private static final String Q3 = \""" + select + * + from + products\"""; + } + """ + ) + ); + } + + // Text Block without comment — does not change + @Test + void shouldNotFormatWithoutComment() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + private static final String QUERY = \""" + select * from users\"""; + } + """ + ) + ); + } + + // Normal string with // language=sql — does not change + @Test + void shouldNotFormatNonTextBlockWithComment() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = "select * from users"; + } + """ + ) + ); + } + + // Because we only support `language=sql` comment for both sql and hql, + // shouldn't do anything with `language=hql` comment + @Test + void shouldNotFormatWithLanguageHqlComment() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=hql + private static final String QUERY = \""" + select * from users\"""; + } + """ + ) + ); + } + + @Test + void shouldPreserveTextBlockIndentation() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + public class Inner { + // language=sql + private static final String QUERY = \""" + select * from users\"""; + } + } + """, + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + public class Inner { + // language=sql + private static final String QUERY = \""" + select + * + from + users\"""; + } + } + """ + ) + ); + } + + // Block already formatted — no diff + @Test + void shouldNotChangeAlreadyFormatted() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String QUERY = \""" + select + * + from + users\"""; + } + """ + ) + ); + } + + // Block with multiple variables in the same declaration + @Test + void shouldFormatMultipleVariablesInSameDeclaration() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String Q1 = \""" + select * from users\""", + Q2 = \""" + select * from orders\""", + Q3 = \""" + select * from products\"""; + } + """, + """ + package io.github.mhagnumdw.test; + + public class MyQuery { + // language=sql + private static final String Q1 = \""" + select + * + from + users\""", + Q2 = \""" + select + * + from + orders\""", + Q3 = \""" + select + * + from + products\"""; + } + """ + ) + ); + } + + // Class outside the filePath — does not change + @Test + void shouldNotChangeUnrelatedClasses() { + rewriteRun( + java( + """ + package io.github.mhagnumdw.other; + + public class OtherQuery { + // language=sql + private static final String QUERY = \""" + select * from users ORDER by name\"""; + } + """ + ) + ); + } + +}