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, ExecutionContext> getFormattingVisitor(
+ Dialect dialect, FormatConfig formatConfig) {
+ TreeVisitor, ExecutionContext> 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\""";
+ }
+ """
+ )
+ );
+ }
+
+}