diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java index 63a42b2a07..6c1db5845d 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java @@ -207,6 +207,7 @@ public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { return super.visit(tree, ctx); } + //select.doSomething(arg1, arg2); @Override public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation m = super.visitMethodInvocation(method, ctx); diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/trait/GradleDependencies.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/trait/GradleDependencies.java index c3f37d7862..6d0bfb24d7 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/trait/GradleDependencies.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/trait/GradleDependencies.java @@ -26,22 +26,18 @@ import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaVisitor; import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.marker.ImplicitReturn; -import org.openrewrite.java.tree.*; -import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.java.trait.Block; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; import org.openrewrite.trait.Trait; import org.openrewrite.trait.VisitFunction2; -import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.Optional; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; -import static java.util.Collections.emptyList; - @RequiredArgsConstructor public class GradleDependencies implements Trait { private static final MethodMatcher DEPENDENCY_DSL_MATCHER = new MethodMatcher("DependencyHandlerSpec *(..)"); @@ -68,7 +64,20 @@ public class GradleDependencies implements Trait { J.Lambda lambda = (J.Lambda) expression; if (lambda.getBody() instanceof J.Block) { return new Block.Matcher().get(lambda.getBody(), new Cursor(cursor, lambda)) - .map(block -> block.mapStatements(mapper)) + .map(block -> block.mapStatements(statement -> { + if (statement instanceof J.Return && ((J.Return) statement).getExpression() instanceof Statement) { + Statement originalStatement = (Statement) ((J.Return) statement).getExpression(); + Statement newStatement = mapper.apply(originalStatement); + if (originalStatement == newStatement) { + return statement; + } else if (newStatement instanceof Expression) { + return ((J.Return) statement).withExpression((Expression) newStatement); + } else if (newStatement == null) { + return null; + } + } + return mapper.apply(statement); + })) .map(Trait::getTree) .filter(block -> !(block.getStatements().isEmpty() && block.getComments().isEmpty() && block.getEnd().getComments().isEmpty())) .map(lambda::withBody) @@ -132,243 +141,4 @@ public J visitMethodInvocation(J.MethodInvocation method, P p) { return null; } } - - @RequiredArgsConstructor - private static class Block implements Trait { - @Getter - private final Cursor cursor; - - public Block mapStatements(Function mapper) { - return mapStatements(mapper, (J.Return::withExpression)); - } - - public Block mapStatements(Function mapper, BiFunction returnMapper) { - J.Block block = getTree(); - List statements = block.getStatements(); - List newStatements = new LinkedList<>(); - LinkedList comments = new LinkedList<>(); - String whitespace = null; - boolean lastStatementWasKept = false; - for (Statement statement : statements) { - if (statement instanceof J.Return && ((J.Return) statement).getExpression() instanceof Statement) { - statement = ((J.Return) statement).getExpression().withPrefix(statement.getPrefix()); - } - Statement mappedStatement = mapper.apply(statement); - if (mappedStatement != null) { - // We want to keep the element (it might have been changed though). - List statementComments = mappedStatement.getComments(); - if (!comments.isEmpty() && whitespace != null) { - //A previous statement was removed, it had comments and we now need to add these comments to the current element. - Comment last = comments.removeLast(); - if (mappedStatement.getPrefix().getWhitespace().contains("\n")) { - // We need to add the prefix of the current statement as the suffix of the last comment of the previous block. - comments.addLast(last.withSuffix(mappedStatement.getPrefix().getWhitespace())); - comments.addAll(statementComments); - } else { - // Add the prefix of the current statement as the suffix of the last comment which is on the same line. (multiple multiline comments could be present) - int j = 0; - while (j < statementComments.size() && !statementComments.get(j).getSuffix().contains("\n")) { - j++; - } - comments.add(last.withSuffix(statementComments.get(j).getSuffix())); - // Now that we have suffixed the correct comment, we can add remaining comments of the current statement - if (j + 1 < statementComments.size()) { - comments.addAll(statementComments.subList(j + 1, statementComments.size())); - } - } - //make sure that the previous removed element's whitespace get used and the newly calculated list of comments containing also the previous removed element's comments. - statement = mappedStatement.withPrefix(mappedStatement.getPrefix().withWhitespace(whitespace).withComments(comments)); - } else if (!lastStatementWasKept && !statement.getPrefix().getWhitespace().contains("\n") && whitespace != null) { - int startIndex = 0; - int j = 0; - while (j < statementComments.size() && !statementComments.get(j).getSuffix().contains("\n")) { - j++; - } - if (j + 1 <= statementComments.size()) { - startIndex = j + 1; - } - if (startIndex < statementComments.size()) { - if (startIndex > 0) { - statement = statement.withPrefix(statement.getPrefix().withWhitespace(statementComments.get(startIndex - 1).getSuffix()).withComments(statementComments.subList(startIndex, statementComments.size()))); - } - } else { - statement = statement.withPrefix(statement.getPrefix().withWhitespace(whitespace).withComments(emptyList())); - } - } - comments = new LinkedList<>(); - whitespace = null; - newStatements.add(statement); - lastStatementWasKept = true; - } else { - List statementComments = statement.getComments(); - if (!comments.isEmpty() && whitespace != null) { - int startIndex = 0; - int endIndex = findIndexOfLastCommentOnNewLine(statementComments) + 1; - if (!statement.getPrefix().getWhitespace().contains("\n")) { - int j = 0; - while (j < statementComments.size() && !statementComments.get(j).getSuffix().contains("\n")) { - j++; - } - Comment last = comments.removeLast(); - comments.add(last.withSuffix(statementComments.get(j).getSuffix())); - if (j + 1 <= statementComments.size()) { - startIndex = j + 1; - } - } - if (startIndex < endIndex) { - comments.addAll(statementComments.subList(startIndex, endIndex)); - } - } else { - if (lastStatementWasKept) { - comments.addAll(statementComments.subList(0, findIndexOfLastCommentOnNewLine(statementComments) + 1)); - whitespace = statement.getPrefix().getWhitespace(); - } else { - int startIndex = findIndexOfFirstCommentOnNewLine(statementComments, statement.getPrefix()); - comments.addAll(statementComments.subList(startIndex, findIndexOfLastCommentOnNewLine(statementComments) + 1)); - if (startIndex == 0) { - whitespace = statement.getPrefix().getWhitespace(); - } else { - whitespace = statementComments.get(startIndex - 1).getSuffix(); - } - } - } - lastStatementWasKept = false; - } - } - - newStatements = ListUtils.mapLast(newStatements, (Function) newLast -> { - Statement currentLast = statements.get(statements.size() - 1); - if (currentLast instanceof J.Return && !(newLast instanceof J.Return)) { - if (newLast instanceof Expression) { - J.Return currentReturn = (J.Return) currentLast; - Space statementPrefix = currentReturn.getMarkers().findFirst(ImplicitReturn.class).isPresent() ? Space.EMPTY : Space.SINGLE_SPACE; - J.Return mappedReturn = returnMapper.apply(currentReturn, newLast.withPrefix(statementPrefix)); - if (mappedReturn.getExpression() instanceof Statement) { - if (mapper.apply((Statement) mappedReturn.getExpression()) == null) { - throw new IllegalArgumentException("The return statement replacement result should not be one that gets filtered out to avoid cyclic changes that result in the entire block being cleared. Did you return something from the old return that still return null when the mapper would be applied?"); - } - } else if (mapper.apply(mappedReturn) == null) { - throw new IllegalArgumentException("The return statement replacement result should not be one that gets filtered out to avoid cyclic changes that result in the entire block being cleared. Did you return something from the old return that still return null when the mapper would be applied?"); - } - return mappedReturn - .withPrefix(newLast.getPrefix()); - } - } - return newLast; - }); - if (statements.equals(newStatements) && comments.isEmpty()) { - return this; - } - - if (!lastStatementWasKept) { - block = block.withEnd(buildEnd(block.getEnd(), comments, whitespace)); - } - - block = block.withStatements(newStatements); - - return new Block(new Cursor(this.cursor.getParent(), block)); - } - - private Space buildEnd(Space end, List comments, @Nullable String endWhitespace) { - List newComments = new ArrayList<>(); - String endBlockWhitespace = null; - if (!comments.isEmpty()) { - for (Comment comment : comments) { - if (comment instanceof TextComment) { - newComments.add(comment); - if (endBlockWhitespace == null) { - endBlockWhitespace = endWhitespace; - } - } - } - } - - List existingEndComments = new ArrayList<>(); - boolean eolComments = !end.getWhitespace().contains("\n"); - String whitespace = null; - if (!eolComments) { - whitespace = end.getWhitespace(); - } - if (!end.getComments().isEmpty()) { - for (Comment comment : end.getComments()) { - if (comment instanceof TextComment) { - if (!eolComments) { - existingEndComments.add(comment); - } else if (comment.getSuffix().contains("\n")) { - eolComments = false; - } - if (whitespace == null) { - whitespace = comment.getSuffix(); - } - } - } - } - if (!newComments.isEmpty()) { - newComments.set(newComments.size() - 1, newComments.get(newComments.size() - 1).withSuffix(whitespace == null ? "" : whitespace)); - } else { - endBlockWhitespace = whitespace; - } - - newComments.addAll(existingEndComments); - - return end.withWhitespace(endBlockWhitespace == null ? end.getWhitespace() : endBlockWhitespace).withComments(newComments); - } - - private static int findIndexOfFirstCommentOnNewLine(List comments, Space prefix) { - if (prefix.getWhitespace().contains("\n")) { - return 0; - } - int index = 0; - while (index < comments.size()) { - Comment comment = comments.get(index); - index++; - if (comment.getSuffix().contains("\n")) { - break; - } - } - return index; - } - - private static int findIndexOfLastCommentOnNewLine(List comments) { - int index = comments.size() - 1; - while (index >= 0) { - Comment comment = comments.get(index); - if (comment.isMultiline()) { - if (comment.getSuffix().contains("\n")) { - return index; - } else { - index--; - } - } else { - return index; - } - } - return index; - } - - private static class Matcher extends SimpleTraitMatcher { - @Override - public

TreeVisitor asVisitor(VisitFunction2 visitor) { - return new JavaVisitor

() { - @Override - public J visitBlock(J.Block block, P p) { - Block trait = test(getCursor()); - return trait != null ? - (J) visitor.visit(trait, p) : - super.visitBlock(block, p); - } - }; - } - - @Override - protected @Nullable Block test(Cursor cursor) { - Object object = cursor.getValue(); - if (object instanceof J.Block) { - return new Block(cursor); - } - - return null; - } - } - } } diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/trait/GradleDependenciesTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/trait/GradleDependenciesTest.java index dbafb34fd7..bc2847b644 100644 --- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/trait/GradleDependenciesTest.java +++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/trait/GradleDependenciesTest.java @@ -38,10 +38,10 @@ public void defaults(RecipeSpec spec) { @Override public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); - Optional meybeDependenciesBlock = new GradleDependencies.Matcher().get(mi, getCursor().getParent()); + Optional maybeDependenciesBlock = new GradleDependencies.Matcher().get(mi, getCursor().getParent()); - if (meybeDependenciesBlock.isPresent()) { - return meybeDependenciesBlock + if (maybeDependenciesBlock.isPresent()) { + return maybeDependenciesBlock .map(deps -> deps.removeDependency("javax.validation", "validation-api")) .map(Trait::getTree) .orElse(null); diff --git a/rewrite-java/src/main/java/org/openrewrite/java/trait/Block.java b/rewrite-java/src/main/java/org/openrewrite/java/trait/Block.java new file mode 100644 index 0000000000..ab4d1d05f4 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/trait/Block.java @@ -0,0 +1,361 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.trait; + +import lombok.*; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.Comment; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.trait.Trait; +import org.openrewrite.trait.VisitFunction2; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import static java.util.Collections.emptyList; + +@RequiredArgsConstructor +public class Block implements Trait { + @Getter + private final Cursor cursor; + + private final List lines; + + public Block(Cursor cursor) { + Object object = cursor.getValue(); + if (!(object instanceof J.Block)) { + throw new IllegalArgumentException("Expected a J.Block, got " + object); + } + this.cursor = cursor; + this.lines = extractLines((J.Block) object); + } + + public Block filterStatements(Predicate predicate) { + return mapStatements(statement -> predicate.test(statement) ? statement : null); + } + + public Block mapStatements(Function mapper) { + return mapLines(line -> line.mapStatement(mapper)); + } + + public Block filterLines(Predicate predicate) { + return mapLines(line -> predicate.test(line) ? line : null); + } + + public Block mapLines(Function mapper) { + J.Block block = getTree(); + List newLines = ListUtils.map(lines, mapper); + if (lines == newLines) { + return this; + } + + return new Block( + new Cursor(this.cursor.getParent(), + block.withEnd(buildEnd(block.getEnd(), newLines)) + .withStatements(buildStatements(newLines))), + newLines); + } + + public static class Matcher extends SimpleTraitMatcher { + @Override + public

TreeVisitor asVisitor(VisitFunction2 visitor) { + return new JavaVisitor

() { + @Override + public J visitBlock(J.Block block, P p) { + Block trait = test(getCursor()); + return trait != null ? + (J) visitor.visit(trait, p) : + super.visitBlock(block, p); + } + }; + } + + @Override + protected @Nullable Block test(Cursor cursor) { + Object object = cursor.getValue(); + if (object instanceof J.Block) { + return new Block(cursor); + } + + return null; + } + } + + @EqualsAndHashCode + public static abstract class Line { + + public @Nullable Line remove() { + return null; + } + + public @Nullable Line mapStatement(Function mapper) { + return this; + } + + public abstract String getWhitespace(); + } + + @Getter + @EqualsAndHashCode(callSuper = false) + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class StatementLine extends Line { + private final Space beforeLine; + private final Statement statement; + @Setter(AccessLevel.PRIVATE) + private Space endOfLine; + + private StatementLine(Space beforeLine, Statement statement) { + this.beforeLine = beforeLine; + this.statement = statement; + this.endOfLine = Space.EMPTY; + } + + @Override + public @Nullable Line mapStatement(Function mapper) { + Statement newStatement = mapper.apply(statement); + if (newStatement == null) { + return null; + } + if (statement != newStatement) { + return new StatementLine(getBeforeLine(), newStatement, getEndOfLine()); + } + return this; + } + + @Override + public String getWhitespace() { + return getBeforeLine().getWhitespace(); + } + } + + @Getter + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + @EqualsAndHashCode(callSuper = false) + public static class CommentLine extends Line { + private final String whitespace; + private final Comment comment; + } + + private static List extractLines(J.Block block) { + List lines = new ArrayList<>(); + int indexOfFirstNewLineComment; + int indexOfLastNewLineComment; + Comment lastComment; + String whitespace; + for (Statement statement : block.getStatements()) { + if (!statement.getComments().isEmpty()) { + indexOfFirstNewLineComment = findIndexOfFirstCommentOnNewLine(statement.getComments(), statement.getPrefix()); + indexOfLastNewLineComment = findIndexOfLastCommentOnNewLine(statement.getComments()); + whitespace = statement.getPrefix().getWhitespace(); + if (!lines.isEmpty()) { + Line lastLine = lines.get(lines.size() - 1); + if (lastLine instanceof StatementLine) { + StatementLine statementLine = (StatementLine) lastLine; + ArrayList comments = new ArrayList<>(statement.getComments().subList(0, indexOfFirstNewLineComment)); + statementLine.setEndOfLine(statement.getPrefix().withComments(comments)); + if (!comments.isEmpty()) { + whitespace = comments.get(comments.size() - 1).getSuffix(); + } + } + } + if (indexOfFirstNewLineComment <= indexOfLastNewLineComment) { + for (Comment comment : statement.getComments().subList(indexOfFirstNewLineComment, indexOfLastNewLineComment)) { + lines.add(new CommentLine(whitespace, comment)); + whitespace = comment.getSuffix(); + } + lastComment = statement.getComments().get(indexOfLastNewLineComment); + if (lastComment.getSuffix().contains("\n")) { + lines.add(new CommentLine(whitespace, lastComment)); + whitespace = lastComment.getSuffix(); + } + } + if (indexOfLastNewLineComment + 1 < statement.getComments().size()) { + lines.add(new StatementLine(Space.build(whitespace, new ArrayList<>(statement.getComments().subList(indexOfLastNewLineComment + 1, statement.getComments().size()))), statement)); + } else { + lines.add(new StatementLine(Space.build(whitespace, emptyList()), statement)); + } + } else { + lines.add(new StatementLine(statement.getPrefix(), statement)); + } + } + if (!block.getEnd().getComments().isEmpty()) { + indexOfFirstNewLineComment = findIndexOfFirstCommentOnNewLine(block.getEnd().getComments(), block.getEnd()); + whitespace = block.getEnd().getWhitespace(); + if (!lines.isEmpty()) { + Line lastLine = lines.get(lines.size() - 1); + if (lastLine instanceof StatementLine) { + ArrayList comments = new ArrayList<>(block.getEnd().getComments().subList(0, indexOfFirstNewLineComment)); + ((StatementLine) lastLine).setEndOfLine(block.getEnd().withComments(comments)); + if (!comments.isEmpty()) { + whitespace = comments.get(comments.size() - 1).getSuffix(); + } + } + } + if (indexOfFirstNewLineComment == 0 || indexOfFirstNewLineComment <= block.getEnd().getComments().size() - 1) { + for (Comment comment : block.getEnd().getComments().subList(indexOfFirstNewLineComment, block.getEnd().getComments().size())) { + lines.add(new CommentLine(whitespace, comment)); + whitespace = comment.getSuffix(); + } + } + } + + return lines; + } + + private static List buildStatements(List lines) { + List statements = new ArrayList<>(); + List commentLines = new ArrayList<>(); + Space endOfLine = Space.EMPTY; + for (Line line : lines) { + if (line instanceof CommentLine) { + commentLines.add((CommentLine) line); + } else if (line instanceof StatementLine) { + StatementLine statementLine = (StatementLine) line; + Statement statement = statementLine.getStatement(); + String whitespace = statementLine.getBeforeLine().getWhitespace(); + List comments = new ArrayList<>(); + if (!commentLines.isEmpty()) { + whitespace = commentLines.get(0).getWhitespace(); + } + if (!endOfLine.getComments().isEmpty()) { + if (!commentLines.isEmpty()) { + String suffix = commentLines.get(0).getWhitespace(); + endOfLine = endOfLine.withComments(ListUtils.mapLast(endOfLine.getComments(), (Function) comment -> comment.withSuffix(suffix))); + } else { + endOfLine = endOfLine.withComments(ListUtils.mapLast(endOfLine.getComments(), (Function) comment -> comment.withSuffix(statementLine.getBeforeLine().getWhitespace()))); + } + comments.addAll(endOfLine.getComments()); + whitespace = endOfLine.getWhitespace(); + } + + if (!commentLines.isEmpty()) { + int i = 1; + for (; i < commentLines.size(); i++) { + comments.add(commentLines.get(i - 1).getComment().withSuffix(commentLines.get(i).getWhitespace())); + } + comments.add(commentLines.get(i - 1).getComment().withSuffix(statementLine.getBeforeLine().getWhitespace())); + } + if (!statementLine.getBeforeLine().getComments().isEmpty()) { + comments.addAll(statementLine.getBeforeLine().getComments()); + } + + endOfLine = statementLine.getEndOfLine(); + + Space prefix = statement.getPrefix().withWhitespace(whitespace).withComments(comments); + statements.add(statement.withPrefix(prefix)); + commentLines = new ArrayList<>(); + } + } + + return statements; + } + + private static Space buildEnd(Space end, List lines) { + if (lines.isEmpty()) { + return end.withComments(emptyList()); + } + int i = lines.size() - 1; + while (i >= 0 && lines.get(i) instanceof CommentLine) { + i--; + } + if (i == -1) { + i = 0; + } else if (lines.get(i) instanceof StatementLine) { + i++; + } + + List comments = new ArrayList<>(); + String suffix; + String prefix; + if (i < lines.size()) { + prefix = lines.get(i).getWhitespace(); + } else { + if (end.getComments().isEmpty()) { + prefix = end.getWhitespace(); + } else { + prefix = end.getComments().get(end.getComments().size() - 1).getSuffix(); + } + } + if (i > 0) { + Line line = lines.get(i - 1); + if (line instanceof StatementLine) { + Space endOfLine = ((StatementLine) line).getEndOfLine(); + List endOfLineComments = endOfLine.getComments(); + if (!endOfLineComments.isEmpty()) { + String lastPrefix = prefix; + endOfLineComments = ListUtils.mapLast(endOfLineComments, (Function) comment -> comment.withSuffix(lastPrefix)); + comments.addAll(endOfLineComments); + prefix = endOfLine.getWhitespace(); + } + } + } + for (; i < lines.size(); i++) { + if (i + 1 < lines.size()) { + suffix = lines.get(i + 1).getWhitespace(); + } else { + suffix = end.getWhitespace(); + if (!end.getComments().isEmpty()) { + suffix = end.getComments().get(end.getComments().size() - 1).getSuffix(); + } + } + comments.add(((CommentLine) lines.get(i)).getComment().withSuffix(suffix)); + } + + return end.withWhitespace(prefix).withComments(comments); + } + + private static int findIndexOfFirstCommentOnNewLine(List comments, Space prefix) { + if (prefix.getWhitespace().contains("\n")) { + return 0; + } + int index = 0; + while (index < comments.size()) { + Comment comment = comments.get(index); + index++; + if (comment.getSuffix().contains("\n")) { + break; + } + } + return index; + } + + private static int findIndexOfLastCommentOnNewLine(List comments) { + int index = comments.size() - 1; + while (index >= 0) { + Comment comment = comments.get(index); + if (comment.isMultiline()) { + if (comment.getSuffix().contains("\n")) { + return index; + } else { + index--; + } + } else { + return index; + } + } + return index; + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/trait/BlockTest.java b/rewrite-java/src/test/java/org/openrewrite/java/trait/BlockTest.java new file mode 100644 index 0000000000..a35fe05118 --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/trait/BlockTest.java @@ -0,0 +1,1118 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.trait; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Tree; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JLeftPadded; +import org.openrewrite.java.tree.MethodCall; +import org.openrewrite.java.tree.Space; +import org.openrewrite.marker.Markers; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class BlockTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().dependsOn( + """ + package testing; + public class TestMethods { + public static void remove() {} + public static void keep() {} + public static void initialize() {} + public static void processItems() {} + public static void doSomething() {} + public static boolean condition() { return true; } + public static boolean anotherCondition() { return true; } + public static void rename() {} + public static void renamed() {} + } + """ + )) + .recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + Block trait = new Block(getCursor()) + .filterStatements(statement -> { + if (statement instanceof J.MethodInvocation) { + return !new MethodMatcher("testing.TestMethods remove(..)").matches((MethodCall) statement); + } + return true; + }) + .mapStatements(statement -> { + if (statement instanceof J.MethodInvocation && new MethodMatcher("testing.TestMethods rename(..)").matches((J.MethodInvocation) statement)) { + return ((J.MethodInvocation) statement).withName(((J.MethodInvocation) statement).getName().withSimpleName("renamed")); + } + return statement; + }); + return super.visitBlock(trait.getTree(), ctx); + } + })); + + + } + + @DocumentExample + @Test + void blockWithStatements() { + rewriteRun( + java( + """ + import testing.TestMethods; + class Test { + void method() { + //First comment remains + TestMethods.remove(); + TestMethods.keep(); //Comment at the end of a statement remains + TestMethods.keep(); //Even if they are indented with more than a single space + TestMethods.remove();//Comment at end of removed ones not put at end of previous one. + //Comment in between 2 removed ones also remains + TestMethods.remove(); //Comment at end of removed ones removed + TestMethods.keep(); + //Comment before removed ones also remain + TestMethods.remove(); + TestMethods.keep(); /* + Multiline comments belong to the line where they where added + */ + TestMethods.remove(); /* + So this one gets removed + */ + TestMethods.remove(); + /* + But this one does not + */ + + //Section 1 + TestMethods.remove(); + TestMethods.keep(); + + //Section 2 + TestMethods.keep(); + + + //Section3 + TestMethods.keep(); + //comment will also be at end + TestMethods.remove(); + //comment at end + } + } + """, + """ + import testing.TestMethods; + class Test { + void method() { + //First comment remains + TestMethods.keep(); //Comment at the end of a statement remains + TestMethods.keep(); //Even if they are indented with more than a single space + //Comment in between 2 removed ones also remains + TestMethods.keep(); + //Comment before removed ones also remain + TestMethods.keep(); /* + Multiline comments belong to the line where they where added + */ + /* + But this one does not + */ + + //Section 1 + TestMethods.keep(); + + //Section 2 + TestMethods.keep(); + + + //Section3 + TestMethods.keep(); + //comment will also be at end + //comment at end + } + } + """ + ) + ); + } + + @Test + void emptyBlocksPreserved() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + if (true) { + remove(); + remove(); + } + System.out.println("kept"); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + if (true) { + } + System.out.println("kept"); + } + } + """ + ) + ); + } + + @Test + void emptyBlockWithCommentsIsPreserved() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + if (true) { + // This comment should preserve the block + remove(); + } + System.out.println("kept"); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + if (true) { + // This comment should preserve the block + } + System.out.println("kept"); + } + } + """ + ) + ); + } + + @Test + void nestedBlocksArePreserved() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); + + if (condition()) { + doSomething(); + } // end of if comment + // after if comment + + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + + if (condition()) { + doSomething(); + } // end of if comment + // after if comment + } + } + """ + ) + ); + } + + @Test + void differentCommentLocations() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); // end of line comment + /* inline block */ remove(); + /* + * Multiline block comment + * before a statement + */ + remove(); + keep(); + // End of line comment after last kept statement + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); // end of line comment + /* + * Multiline block comment + * before a statement + */ + keep(); + // End of line comment after last kept statement + } + } + """ + ) + ); + } + + @Test + void removeFirstStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + remove(); + + // Second kept statement + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + + // Second kept statement + keep(); + } + } + """ + ) + ); + } + + @Test + void removeMiddleStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + keep(); + + // Middle statement removed + remove(); + + // Another kept statement + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + keep(); + + // Middle statement removed + + // Another kept statement + keep(); + } + } + """ + ) + ); + } + + @Test + void removeLastStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment kept + keep(); + + // Second statement removed + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment kept + keep(); + + // Second statement removed + } + } + """ + ) + ); + } + + @Test + void removeFirstStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + remove(); // End of line comment + + // Second kept statement + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + + // Second kept statement + keep(); + } + } + """ + ) + ); + } + + @Test + void removeMiddleStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + keep(); + + // Middle statement removed + remove(); // End of line comment + + // Another kept statement + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment + keep(); + + // Middle statement removed + + // Another kept statement + keep(); + } + } + """ + ) + ); + } + + @Test + void removeLastStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment kept + keep(); + + // Second statement removed + remove(); // End of line comment + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // First statement comment kept + keep(); + + // Second statement removed + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedFirstStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedMiddleStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + remove(); + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + keep(); + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedLastStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedFirstStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); // End of line comment + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedMiddleStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + remove(); // End of line comment + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + keep(); + } + } + """ + ) + ); + } + + @Test + void removeUnseparatedLastStatementWithEOLComment() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + remove(); // End of line comment + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void removeAllButOneStatement() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); + remove(); + keep(); + remove(); + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void commentedOutStatements() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); + // keep(); // commented out + keep(); + /* keep(); // also commented */ + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // keep(); // commented out + keep(); + /* keep(); // also commented */ + } + } + """ + ) + ); + } + + @Test + void commentInBetweenRemovedOnesRemains() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + remove();//Comment at end of removed ones not put at end of previous one. + //Comment in between 2 removed ones remains + remove(); + keep(); + + keep(); //Also if the kept item has a end of line + remove();//Comment at end of removed ones not put at end of previous one. + //Comment in between 2 removed ones remains + remove(); + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + //Comment in between 2 removed ones remains + keep(); + + keep(); //Also if the kept item has a end of line + //Comment in between 2 removed ones remains + keep(); + } + } + """ + ) + ); + } + + @Test + void beforeLineComments() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + /* before line comment remains */ keep(); + remove(); + /* before line comment remains */ remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + /* before line comment remains */ keep(); + } + } + """ + ) + ); + } + + @Test + void multilineComments() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + /* First comment remains */ + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + /* + Comment at new line + */ + /* First comment remains */ + remove(); + /* before line comment gets removed also */ remove(); /* end of line gets removed */ + /* even gets removed if preceded by another comment not on a new line */ remove(); + remove(); /* Comment at the end of a statement is removed */ + remove(); /* Even removed if they are indented with more than a single space */ + remove(); /* + Comment at end over multiple lines is removed + */ + remove(); + /* + Comment at new line + */ + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + /* First comment remains */ + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + /* + Comment at new line + */ + /* First comment remains */ + /* + Comment at new line + */ + } + } + """ + ) + ); + } + + @Test + void filterCanRemoveLine() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + return super.visitBlock(new Block(getCursor()).filterLines(line -> !(line instanceof Block.CommentLine)).getTree(), ctx); + } + })), + java( + """ + import static testing.TestMethods.*; + class Test { + //This block has a line comment + void /* and a comment in the middle */ method() { + /* First comment remains */ + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + /* + Comment at new line + */ + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void /* and a comment in the middle */ method() { + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + } + } + """ + ) + ); + } + + @Test + void mapCanRemoveCommentLine() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + return super.visitBlock(new Block(getCursor()).mapLines(line -> line instanceof Block.CommentLine ? line.remove() : line).getTree(), ctx); + } + })), + java( + """ + import static testing.TestMethods.*; + class Test { + //This block has a line comment + void /* and a comment in the middle */ method() { + /* First comment remains */ + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + /* + Comment at new line + */ + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void /* and a comment in the middle */ method() { + keep(); + /* before line comment remains */ keep(); + keep(); /* Comment at the end of a statement remains */ + keep(); /* Even if they are indented with more than a single space */ + keep(); /* + Comment at end over multiple lines + */ + keep(); + } + } + """ + ) + ); + } + + @Test + void filterStatements() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + remove(); + keep(); + remove(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + keep(); + } + } + """ + ) + ); + } + + @Test + void returnStatementHasExpression() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + int method() { + remove(); + keep(); + return 42; + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + int method() { + keep(); + return 42; + } + } + """ + ) + ); + } + + @Test + void userShouldUnwrapReturnStatementIfRequired() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + Block trait = new Block(getCursor()).filterStatements( + statement -> { + if (statement instanceof J.MethodInvocation) { + return !new MethodMatcher("testing.TestMethods condition(..)").matches((MethodCall) statement); + } + return true; + } + ); + return super.visitBlock(trait.getTree(), ctx); + } + })), + java( + """ + import static testing.TestMethods.*; + class Test { + boolean method() { + keep(); + anotherCondition(); + return condition(); + } + } + """ + ) + ); + } + + @Test + void mapStatementsWithCustomReturnMapperReplacingStatement() { + rewriteRun( + spec -> spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + Block trait = new Block(getCursor()).mapStatements( + statement -> { + if (statement instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) statement; + if (new MethodMatcher("testing.TestMethods condition(..)").matches(mi)) { + return null; + } + } + if (statement instanceof J.Return && ((J.Return) statement).getExpression() instanceof J.MethodInvocation) { + return ((J.Return) statement).withExpression( + new J.Unary( + Tree.randomId(), + Space.SINGLE_SPACE, + Markers.EMPTY, + JLeftPadded.build(J.Unary.Type.Not), + ((J.Return) statement).getExpression().withPrefix(Space.EMPTY), + null + ) + ); + } + return statement; + }); + return super.visitBlock(trait.getTree(), ctx); + } + })), + java( + """ + import static testing.TestMethods.*; + class Test { + boolean method() { + keep(); + anotherCondition(); + condition(); + return condition(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + boolean method() { + keep(); + anotherCondition(); + return !condition(); + } + } + """ + ) + ); + } + + @Test + void complexNestedStructure() { + rewriteRun( + java( + """ + import static testing.TestMethods.*; + class Test { + void method() { + // Outer comment + if (true) { + // Inner comment before removed + remove(); + // Between statements + for (int i = 0; i < 10; i++) { + doSomething(); + } + // After for loop + remove(); + } + // After if + keep(); + } + } + """, + """ + import static testing.TestMethods.*; + class Test { + void method() { + // Outer comment + if (true) { + // Inner comment before removed + // Between statements + for (int i = 0; i < 10; i++) { + doSomething(); + } + // After for loop + } + // After if + keep(); + } + } + """ + ) + ); + } +}