From 1c642049ff3139021205c39f18d15ca9f76915df Mon Sep 17 00:00:00 2001 From: aboyko Date: Tue, 17 Dec 2024 10:20:16 -0500 Subject: [PATCH] No rewrite-static-analysis dependency. Adopt latest Apache license OR --- .../commons/commons-rewrite/pom.xml | 23 +- ...AddSerialAnnotationToSerialVersionUID.java | 93 ++ .../AddSerialVersionUidToSerializable.java | 149 ++ .../ChainStringBuilderAppendCalls.java | 199 +++ .../InstanceOfPatternMatch.java | 527 +++++++ .../staticanalysis/RemoveUnneededBlock.java | 100 ++ .../ReplaceDeprecatedRuntimeExecMethods.java | 138 ++ .../groovy/GroovyFileChecker.java | 35 + .../kotlin/KotlinFileChecker.java | 40 + .../commons/rewrite/java/JavaFileChecker.java | 34 + ...erialAnnotationToSerialVersionUIDTest.java | 192 +++ ...AddSerialVersionUidToSerializableTest.java | 275 ++++ .../ChainStringBuilderAppendCallsTest.java | 313 ++++ .../InstanceOfPatternMatchTest.java | 1296 +++++++++++++++++ .../RemoveUnneededBlockTest.java | 568 ++++++++ ...placeDeprecatedRuntimeExecMethodsTest.java | 297 ++++ headless-services/commons/pom.xml | 2 +- .../rewrite/RewriteRecipeRepositoryTest.java | 2 +- 18 files changed, 4269 insertions(+), 14 deletions(-) create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUID.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/RemoveUnneededBlockTest.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethodsTest.java diff --git a/headless-services/commons/commons-rewrite/pom.xml b/headless-services/commons/commons-rewrite/pom.xml index 56a2dcb3b0..66d64b67ef 100644 --- a/headless-services/commons/commons-rewrite/pom.xml +++ b/headless-services/commons/commons-rewrite/pom.xml @@ -28,14 +28,6 @@ ${project.version} - - - - de.danielbechler - java-object-diff - 0.95 - - io.github.classgraph classgraph @@ -58,10 +50,6 @@ org.openrewrite rewrite-groovy - - org.openrewrite - rewrite-kotlin - org.openrewrite rewrite-gradle @@ -113,6 +101,12 @@ org.openrewrite.recipe rewrite-spring + + + org.openrewrite.recipe + rewrite-static-analysis + + @@ -133,6 +127,11 @@ ${project.version} test + + org.openrewrite + rewrite-test + test + diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUID.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUID.java new file mode 100644 index 0000000000..746399b097 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUID.java @@ -0,0 +1,93 @@ +/* + * 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.staticanalysis; + +import org.jspecify.annotations.NonNull; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +import java.time.Duration; +import java.util.Comparator; + +public class AddSerialAnnotationToSerialVersionUID extends Recipe { + @Override + public String getDisplayName() { + return "Add `@Serial` annotation to `serialVersionUID`"; + } + + @Override + public String getDescription() { + return "Annotation any `serialVersionUID` fields with `@Serial` to indicate it's part of the serialization mechanism."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(1); + } + + @Override + @NonNull + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + new UsesJavaVersion<>(14), + new UsesType<>("java.io.Serializable", true) + ), + new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (TypeUtils.isAssignableTo("java.io.Serializable", classDecl.getType())) { + return super.visitClassDeclaration(classDecl, ctx); + } + return classDecl; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ctx); + if (isPrivateStaticFinalLongSerialVersionUID(vd) && + FindAnnotations.find(vd, "@java.io.Serial").isEmpty()) { + maybeAddImport("java.io.Serial"); + return JavaTemplate.builder("@Serial") + .imports("java.io.Serial") + .build() + .apply(getCursor(), vd.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))); + } + return vd; + } + + private boolean isPrivateStaticFinalLongSerialVersionUID(J.VariableDeclarations vd) { + return vd.hasModifier(J.Modifier.Type.Private) && + vd.hasModifier(J.Modifier.Type.Static) && + vd.hasModifier(J.Modifier.Type.Final) && + TypeUtils.asPrimitive(vd.getType()) == JavaType.Primitive.Long && + vd.getVariables().size() == 1 && + "serialVersionUID".equals(vd.getVariables().get(0).getSimpleName()); + } + } + ); + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java new file mode 100644 index 0000000000..67e25a0395 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java @@ -0,0 +1,149 @@ +/* + * Copyright 2021 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.staticanalysis; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Markers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AddSerialVersionUidToSerializable extends Recipe { + + @Override + public String getDisplayName() { + return "Add `serialVersionUID` to a `Serializable` class when missing"; + } + + @Override + public String getDescription() { + return "A `serialVersionUID` field is strongly recommended in all `Serializable` classes. If this is not " + + "defined on a `Serializable` class, the compiler will generate this value. If a change is later made " + + "to the class, the generated value will change and attempts to deserialize the class will fail."; + } + + @Override + public Set getTags() { + return Collections.singleton("RSPEC-S2057"); + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + final JavaTemplate template = JavaTemplate.builder("private static final long serialVersionUID = 1;").build(); + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + // Anonymous classes are not of interest + return method; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + // Anonymous classes are not of interest + return multiVariable; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration c = super.visitClassDeclaration(classDecl, ctx); + if (c.getKind() != J.ClassDeclaration.Kind.Type.Class || !requiresSerialVersionField(classDecl.getType())) { + return c; + } + AtomicBoolean needsSerialVersionId = new AtomicBoolean(true); + J.Block body = c.getBody(); + c = c.withBody(c.getBody().withStatements(ListUtils.map(c.getBody().getStatements(), s -> { + if (!(s instanceof J.VariableDeclarations)) { + return s; + } + J.VariableDeclarations varDecls = (J.VariableDeclarations) s; + for (J.VariableDeclarations.NamedVariable v : varDecls.getVariables()) { + if ("serialVersionUID".equals(v.getSimpleName())) { + needsSerialVersionId.set(false); + return maybeAutoFormat(varDecls, maybeFixVariableDeclarations(varDecls), ctx, new Cursor(getCursor(), body)); + } + } + return s; + }))); + if (needsSerialVersionId.get()) { + c = template.apply(updateCursor(c), c.getBody().getCoordinates().firstStatement()); + } + return c; + } + + private J.VariableDeclarations maybeFixVariableDeclarations(J.VariableDeclarations varDecls) { + List modifiers = varDecls.getModifiers(); + if (!J.Modifier.hasModifier(modifiers, J.Modifier.Type.Private) || + !J.Modifier.hasModifier(modifiers, J.Modifier.Type.Static) || + !J.Modifier.hasModifier(modifiers, J.Modifier.Type.Final)) { + varDecls = varDecls.withModifiers(Arrays.asList( + new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null, J.Modifier.Type.Private, Collections.emptyList()), + new J.Modifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, null, J.Modifier.Type.Static, Collections.emptyList()), + new J.Modifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, null, J.Modifier.Type.Final, Collections.emptyList()) + )); + } + if (TypeUtils.asPrimitive(varDecls.getType()) != JavaType.Primitive.Long) { + varDecls = varDecls.withTypeExpression(new J.Primitive(Tree.randomId(), Space.EMPTY, Markers.EMPTY, JavaType.Primitive.Long)); + } + return varDecls; + } + + private boolean requiresSerialVersionField(@Nullable JavaType type) { + if (type == null) { + return false; + } else if (type instanceof JavaType.Primitive) { + return true; + } else if (type instanceof JavaType.Array) { + return requiresSerialVersionField(((JavaType.Array) type).getElemType()); + } else if (type instanceof JavaType.Parameterized) { + JavaType.Parameterized parameterized = (JavaType.Parameterized) type; + if (parameterized.isAssignableTo("java.util.Collection") || parameterized.isAssignableTo("java.util.Map")) { + //If the type is either a collection or a map, make sure the type parameters are serializable. We + //force all type parameters to be checked to correctly scoop up all non-serializable candidates. + boolean typeParametersSerializable = true; + for (JavaType typeParameter : parameterized.getTypeParameters()) { + typeParametersSerializable = typeParametersSerializable && requiresSerialVersionField(typeParameter); + } + return typeParametersSerializable; + } + //All other parameterized types fall through + } else if (type instanceof JavaType.FullyQualified) { + JavaType.FullyQualified fq = (JavaType.FullyQualified) type; + if (fq.getKind() == JavaType.Class.Kind.Enum) { + return false; + } + + if (fq.getKind() != JavaType.Class.Kind.Interface && + !fq.isAssignableTo("java.lang.Throwable")) { + return fq.isAssignableTo("java.io.Serializable"); + } + } + return false; + } + }; + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java new file mode 100644 index 0000000000..911d39ffdd --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java @@ -0,0 +1,199 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.*; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +public class ChainStringBuilderAppendCalls extends Recipe { + private static final MethodMatcher STRING_BUILDER_APPEND = new MethodMatcher("java.lang.StringBuilder append(String)"); + + @SuppressWarnings("ALL") // Stop NoMutableStaticFieldsInRecipes from suggesting to remove this mutable static field + private static J.Binary additiveBinaryTemplate = null; + + @Override + public String getDisplayName() { + return "Chain `StringBuilder.append()` calls"; + } + + @Override + public String getDescription() { + return "String concatenation within calls to `StringBuilder.append()` causes unnecessary memory allocation. Except for concatenations of String literals, which are joined together at compile time. Replaces inefficient concatenations with chained calls to `StringBuilder.append()`."; + } + + @Override + public @Nullable Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(2); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesMethod<>(STRING_BUILDER_APPEND), Repeat.repeatUntilStable(new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation m = super.visitMethodInvocation(method, ctx); + + if (STRING_BUILDER_APPEND.matches(m)) { + List arguments = m.getArguments(); + if (arguments.size() != 1) { + return m; + } + + List flattenExpressions = new ArrayList<>(); + boolean flattenable = flatAdditiveExpressions(arguments.get(0).unwrap(), flattenExpressions); + if (!flattenable) { + return m; + } + + if (flattenExpressions.stream().allMatch(J.Literal.class::isInstance)) { + return m; + } + + // group expressions + List groups = new ArrayList<>(); + List group = new ArrayList<>(); + boolean appendToString = false; + for (Expression exp : flattenExpressions) { + if (appendToString) { + if (exp instanceof J.Literal && + (((J.Literal) exp).getType() == JavaType.Primitive.String) + ) { + group.add(exp); + } else { + addToGroups(group, groups); + groups.add(exp); + } + } else { + if (exp instanceof J.Literal && + (((J.Literal) exp).getType() == JavaType.Primitive.String)) { + addToGroups(group, groups); + appendToString = true; + } else if ((exp instanceof J.Identifier || exp instanceof J.MethodInvocation) && exp.getType() != null) { + JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(exp.getType()); + if (fullyQualified != null && fullyQualified.getFullyQualifiedName().equals("java.lang.String")) { + addToGroups(group, groups); + appendToString = true; + } + } + group.add(exp); + + } + } + addToGroups(group, groups); + + J.MethodInvocation chainedMethods = m.withArguments(singletonList(groups.get(0))); + for (int i = 1; i < groups.size(); i++) { + chainedMethods = chainedMethods.withSelect(chainedMethods) + .withArguments(singletonList(groups.get(i).unwrap())) + .withPrefix(Space.EMPTY); + } + + return chainedMethods; + } + + return m; + } + })); + } + + /** + * Concat two literals to an expression with '+' and surrounded with single space. + */ + public static J.Binary concatAdditionBinary(Expression left, Expression right) { + J.Binary b = getAdditiveBinaryTemplate(); + return b.withPrefix(b.getLeft().getPrefix()) + .withLeft(left) + .withRight(right.withPrefix(Space.build(" " + right.getPrefix().getWhitespace(), emptyList()))); + } + + /** + * Concat expressions to an expression with '+' connected. + */ + public static @Nullable Expression additiveExpression(Expression... expressions) { + Expression expression = null; + for (Expression element : expressions) { + if (element != null) { + expression = (expression == null) ? element : concatAdditionBinary(expression, element); + } + } + return expression; + } + + public static @Nullable Expression additiveExpression(List expressions) { + return additiveExpression(expressions.toArray(new Expression[0])); + } + + public static J.Binary getAdditiveBinaryTemplate() { + if (additiveBinaryTemplate == null) { + //noinspection OptionalGetWithoutIsPresent + J.CompilationUnit cu = JavaParser.fromJavaVersion() + .build() + .parse("class A { String s = \"A\" + \"B\";}") + .map(J.CompilationUnit.class::cast) + .findFirst() + .get(); + additiveBinaryTemplate = (J.Binary) ((J.VariableDeclarations) cu.getClasses().get(0) + .getBody() + .getStatements().get(0)) + .getVariables().get(0) + .getInitializer(); + assert additiveBinaryTemplate != null; + } + return additiveBinaryTemplate; + } + + /** + * Concat an additive expression in a group and add to groups + */ + private static void addToGroups(List group, List groups) { + if (!group.isEmpty()) { + groups.add(additiveExpression(group)); + group.clear(); + } + } + + public static boolean flatAdditiveExpressions(Expression expression, List expressionList) { + if (expression instanceof J.Binary) { + J.Binary b = (J.Binary) expression; + if (b.getOperator() != J.Binary.Type.Addition) { + return false; + } + + return flatAdditiveExpressions(b.getLeft(), expressionList) && + flatAdditiveExpressions(b.getRight(), expressionList); + } else if (expression instanceof J.Literal || + expression instanceof J.Identifier || + expression instanceof J.MethodInvocation || + expression instanceof J.Parentheses) { + expressionList.add(expression.withPrefix(Space.EMPTY)); + return true; + } + + return false; + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java new file mode 100644 index 0000000000..5306421f69 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java @@ -0,0 +1,527 @@ +/* + * Copyright 2021 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.staticanalysis; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.VariableNameUtils; +import org.openrewrite.java.search.SemanticallyEqual; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.*; +import org.openrewrite.java.tree.J.VariableDeclarations.NamedVariable; +import org.openrewrite.marker.Markers; +import org.openrewrite.staticanalysis.groovy.GroovyFileChecker; +import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker; + +import java.time.Duration; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; +import static org.openrewrite.Tree.randomId; +import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER; + +public class InstanceOfPatternMatch extends Recipe { + + @Override + public String getDisplayName() { + return "Changes code to use Java 17's `instanceof` pattern matching"; + } + + @Override + public String getDescription() { + return "Adds pattern variables to `instanceof` expressions wherever the same (side effect free) expression is referenced in a corresponding type cast expression within the flow scope of the `instanceof`." + + " Currently, this recipe supports `if` statements and ternary operator expressions."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(1); + } + + @Override + public TreeVisitor getVisitor() { + TreeVisitor preconditions = Preconditions.and( + new UsesJavaVersion<>(17), + Preconditions.not(new KotlinFileChecker<>()), + Preconditions.not(new GroovyFileChecker<>()) + ); + + return Preconditions.check(preconditions, new JavaVisitor() { + @Override + public @Nullable J postVisit(J tree, ExecutionContext ctx) { + J result = super.postVisit(tree, ctx); + InstanceOfPatternReplacements original = getCursor().getMessage("flowTypeScope"); + if (original != null && !original.isEmpty()) { + return UseInstanceOfPatternMatching.refactor(result, original, getCursor().getParentOrThrow()); + } + return result; + } + + @Override + public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, ExecutionContext ctx) { + instanceOf = (J.InstanceOf) super.visitInstanceOf(instanceOf, ctx); + if (instanceOf.getPattern() != null || !instanceOf.getSideEffects().isEmpty()) { + return instanceOf; + } + + Cursor maybeReplacementRoot = null; + J additionalContext = null; + boolean flowScopeBreakEncountered = false; + for (Iterator it = getCursor().getPathAsCursors(); it.hasNext(); ) { + Cursor next = it.next(); + Object value = next.getValue(); + if (value instanceof J.Binary) { + J.Binary binary = (J.Binary) value; + if (!flowScopeBreakEncountered && binary.getOperator() == J.Binary.Type.And) { + additionalContext = binary; + } else { + flowScopeBreakEncountered = true; + } + } else if (value instanceof J.Unary && ((J.Unary) value).getOperator() == J.Unary.Type.Not) { + // TODO this could be improved (the pattern variable may be applicable in the else case + // or even in subsequent statements (due to the flow scope semantics) + flowScopeBreakEncountered = true; + } else if (value instanceof Statement) { + maybeReplacementRoot = next; + break; + } + } + + if (maybeReplacementRoot != null) { + J root = maybeReplacementRoot.getValue(); + Set contexts = new HashSet<>(); + if (!flowScopeBreakEncountered) { + if (root instanceof J.If) { + contexts.add(((J.If) root).getThenPart()); + } else if (root instanceof J.Ternary) { + contexts.add(((J.Ternary) root).getTruePart()); + } + } + if (additionalContext != null) { + contexts.add(additionalContext); + } + + if (!contexts.isEmpty()) { + InstanceOfPatternReplacements replacements = maybeReplacementRoot + .computeMessageIfAbsent("flowTypeScope", k -> new InstanceOfPatternReplacements(root)); + replacements.registerInstanceOf(instanceOf, contexts); + } + } + return instanceOf; + } + + @Override + public J visitTypeCast(J.TypeCast typeCast, ExecutionContext ctx) { + J result = super.visitTypeCast(typeCast, ctx); + if (result instanceof J.TypeCast) { + InstanceOfPatternReplacements replacements = getCursor().getNearestMessage("flowTypeScope"); + if (replacements != null) { + replacements.registerTypeCast((J.TypeCast) result, getCursor()); + } + } + return result; + } + }); + } + + private static class ExpressionAndType { + private final Expression expression; + private final JavaType type; + public ExpressionAndType(Expression expression, JavaType type) { + this.expression = expression; + this.type = type; + } + public Expression getExpression() { + return expression; + } + public JavaType getType() { + return type; + } + + } + + private static class VariableAndTypeTree { + private final J.VariableDeclarations.NamedVariable variable; + private final TypeTree type; + public VariableAndTypeTree(NamedVariable variable, TypeTree type) { + this.variable = variable; + this.type = type; + } + public J.VariableDeclarations.NamedVariable getVariable() { + return variable; + } + public TypeTree getType() { + return type; + } + + } + + private static class InstanceOfPatternReplacements { + private final J root; + private final Map instanceOfs = new HashMap<>(); + private final Map> contexts = new HashMap<>(); + private final Map> contextScopes = new HashMap<>(); + private final Map replacements = new HashMap<>(); + private final Map variablesToDelete = new HashMap<>(); + + public InstanceOfPatternReplacements(J root) { + this.root = root; + } + + public void registerInstanceOf(J.InstanceOf instanceOf, Set contexts) { + Expression expression = instanceOf.getExpression(); + JavaType type = ((TypedTree) instanceOf.getClazz()).getType(); + if (type == null) { + return; + } + + Optional existing = instanceOfs.keySet().stream() + .filter(k -> TypeUtils.isAssignableTo(type, k.getType()) && + SemanticallyEqual.areEqual(k.getExpression(), expression)) + .findAny(); + if (!existing.isPresent()) { + instanceOfs.put(new ExpressionAndType(expression, type), instanceOf); + this.contexts.put(instanceOf, contexts); + } + } + + public void registerTypeCast(J.TypeCast typeCast, Cursor cursor) { + Expression expression = typeCast.getExpression(); + JavaType type = typeCast.getClazz().getTree().getType(); + + Optional match = instanceOfs.keySet().stream() + .filter(k -> TypeUtils.isAssignableTo(type, k.getType()) && + SemanticallyEqual.areEqual(k.getExpression(), expression)) + .findAny(); + if (match.isPresent()) { + Cursor parent = cursor.getParentTreeCursor(); + J.InstanceOf instanceOf = instanceOfs.get(match.get()); + Set validContexts = contexts.get(instanceOf); + for (Iterator it = cursor.getPath(); it.hasNext(); ) { + Object next = it.next(); + if (validContexts.contains(next)) { + if (isAcceptableTypeCast(typeCast) && isTheSameAsOtherTypeCasts(typeCast, instanceOf)) { + if (parent.getValue() instanceof J.VariableDeclarations.NamedVariable && + !variablesToDelete.containsKey(instanceOf)) { + variablesToDelete.put(instanceOf, new VariableAndTypeTree(parent.getValue(), + requireNonNull(parent.firstEnclosing(J.VariableDeclarations.class).getTypeExpression()))); + } else { + replacements.put(typeCast, instanceOf); + } + contextScopes.computeIfAbsent(instanceOf, k -> new HashSet<>()).add(cursor); + } else { + replacements.entrySet().removeIf(e -> e.getValue() == instanceOf); + variablesToDelete.remove(instanceOf); + contextScopes.remove(instanceOf); + contexts.remove(instanceOf); + instanceOfs.entrySet().removeIf(e -> e.getValue() == instanceOf); + } + break; + } else if (root == next) { + break; + } + } + } + } + + private boolean isAcceptableTypeCast(J.TypeCast typeCast) { + TypeTree typeTree = typeCast.getClazz().getTree(); + if (typeTree instanceof J.ParameterizedType) { + return requireNonNull(((J.ParameterizedType) typeTree).getTypeParameters()).stream().allMatch(J.Wildcard.class::isInstance); + } + return true; + } + + private boolean isTheSameAsOtherTypeCasts(J.TypeCast typeCast, J.InstanceOf instanceOf) { + return replacements + .entrySet() + .stream() + .filter(e -> e.getValue() == instanceOf) + .findFirst() + .map(e -> e.getKey().getType().equals(typeCast.getType())) + .orElse(true); + } + + public boolean isEmpty() { + return replacements.isEmpty() && variablesToDelete.isEmpty(); + } + + public J.InstanceOf processInstanceOf(J.InstanceOf instanceOf, Cursor cursor) { + if (!contextScopes.containsKey(instanceOf)) { + return instanceOf; + } + JavaType type = ((TypedTree) instanceOf.getClazz()).getType(); + String name = patternVariableName(instanceOf, cursor); + J.InstanceOf result = instanceOf.withPattern(new J.Identifier( + randomId(), + Space.build(" ", emptyList()), + Markers.EMPTY, + emptyList(), + name, + type, + null)); + + J currentTypeTree = instanceOf.getClazz(); + TypeTree typeCastTypeTree = computeTypeTreeFromTypeCasts(instanceOf); + // If type tree from type cast is not parameterized then NVM. Instance of should already have proper type + if (typeCastTypeTree instanceof J.ParameterizedType) { + J.ParameterizedType parameterizedType = (J.ParameterizedType) typeCastTypeTree; + result = result.withClazz(parameterizedType.withId(Tree.randomId()).withPrefix(currentTypeTree.getPrefix())); + } + + // update entry in replacements to share the pattern variable name + for (Map.Entry entry : replacements.entrySet()) { + if (entry.getValue() == instanceOf) { + entry.setValue(result); + } + } + return result; + } + + private TypeTree computeTypeTreeFromTypeCasts(J.InstanceOf instanceOf) { + TypeTree typeCastTypeTree = replacements + .entrySet() + .stream() + .filter(e -> e.getValue() == instanceOf) + .findFirst() + .map(e -> e.getKey().getClazz().getTree()) + .orElse(null); + if (typeCastTypeTree == null) { + VariableAndTypeTree variable = variablesToDelete.get(instanceOf); + if (variable != null) { + typeCastTypeTree = variable.getType(); + } + } + return typeCastTypeTree; + } + + private String patternVariableName(J.InstanceOf instanceOf, Cursor cursor) { + VariableNameStrategy strategy; + if (root instanceof J.If) { + VariableAndTypeTree variableData = variablesToDelete.get(instanceOf); + strategy = variableData != null ? + VariableNameStrategy.exact(variableData.getVariable().getSimpleName()) : + VariableNameStrategy.normal(contextScopes.get(instanceOf)); + } else { + strategy = VariableNameStrategy.short_(); + } + String baseName = strategy.variableName(((TypeTree) instanceOf.getClazz()).getType()); + return VariableNameUtils.generateVariableName(baseName, cursor, INCREMENT_NUMBER); + } + + public @Nullable J processTypeCast(J.TypeCast typeCast, Cursor cursor) { + J.InstanceOf instanceOf = replacements.get(typeCast); + if (instanceOf != null && instanceOf.getPattern() != null) { + String name = ((J.Identifier) instanceOf.getPattern()).getSimpleName(); + TypedTree owner = cursor.firstEnclosing(J.MethodDeclaration.class); + owner = owner != null ? owner : cursor.firstEnclosingOrThrow(J.ClassDeclaration.class); + JavaType.Variable fieldType = new JavaType.Variable(null, Flag.Default.getBitMask(), name, owner.getType(), typeCast.getType(), emptyList()); + return new J.Identifier( + randomId(), + typeCast.getPrefix(), + Markers.EMPTY, + emptyList(), + name, + typeCast.getType(), + fieldType); + } + return null; + } + + public @Nullable J processVariableDeclarations(J.VariableDeclarations multiVariable) { + return multiVariable.getVariables().stream().anyMatch(v -> variablesToDelete.values().stream().anyMatch(vd -> vd.getVariable() == v)) ? null : multiVariable; + } + } + + private static class UseInstanceOfPatternMatching extends JavaVisitor { + + private final InstanceOfPatternReplacements replacements; + + public UseInstanceOfPatternMatching(InstanceOfPatternReplacements replacements) { + this.replacements = replacements; + } + + static @Nullable J refactor(@Nullable J tree, InstanceOfPatternReplacements replacements, Cursor cursor) { + return new UseInstanceOfPatternMatching(replacements).visit(tree, 0, cursor); + } + + @Override + public J visitBinary(J.Binary original, Integer integer) { + Expression newLeft = (Expression) super.visitNonNull(original.getLeft(), integer); + if (newLeft != original.getLeft()) { + // The left side changed, so the right side should see any introduced variable names + J.Binary replacement = original.withLeft(newLeft); + Cursor widenedCursor = updateCursor(replacement); + + Expression newRight; + if (original.getRight() instanceof J.InstanceOf) { + newRight = replacements.processInstanceOf((J.InstanceOf) original.getRight(), widenedCursor); + } else if (original.getRight() instanceof J.Parentheses && + ((J.Parentheses) original.getRight()).getTree() instanceof J.InstanceOf) { + @SuppressWarnings("unchecked") + J.Parentheses originalRight = (J.Parentheses) original.getRight(); + newRight = originalRight.withTree(replacements.processInstanceOf(originalRight.getTree(), widenedCursor)); + } else { + newRight = (Expression) super.visitNonNull(original.getRight(), integer, widenedCursor); + } + return replacement.withRight(newRight); + } + // The left side didn't change, so the right side doesn't need to see any introduced variable names + return super.visitBinary(original, integer); + } + + @Override + public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, Integer executionContext) { + instanceOf = (J.InstanceOf) super.visitInstanceOf(instanceOf, executionContext); + instanceOf = replacements.processInstanceOf(instanceOf, getCursor()); + return instanceOf; + } + + @Override + public J visitParentheses(J.Parentheses parens, Integer executionContext) { + if (parens.getTree() instanceof J.TypeCast) { + J replacement = replacements.processTypeCast((J.TypeCast) parens.getTree(), getCursor()); + if (replacement != null) { + return replacement.withPrefix(parens.getPrefix()); + } + } + return super.visitParentheses(parens, executionContext); + } + + @Override + public J visitTypeCast(J.TypeCast typeCast, Integer executionContext) { + typeCast = (J.TypeCast) super.visitTypeCast(typeCast, executionContext); + J replacement = replacements.processTypeCast(typeCast, getCursor()); + if (replacement != null) { + return replacement; + } + return typeCast; + } + + @Override + public @Nullable J visitVariableDeclarations(J.VariableDeclarations multiVariable, Integer integer) { + multiVariable = (J.VariableDeclarations) super.visitVariableDeclarations(multiVariable, integer); + return replacements.processVariableDeclarations(multiVariable); + } + } + + private static class VariableNameStrategy { + public static final Pattern NAME_SPLIT_PATTERN = Pattern.compile("[$._]*(?=\\p{Upper}+[\\p{Lower}\\p{Digit}]*)"); + private final Style style; + + @Nullable + private final String name; + + private final Set contextScopes; + + enum Style { + SHORT, NORMAL, EXACT + } + + private VariableNameStrategy(Style style, @Nullable String exactName, Set contextScopes) { + this.style = style; + this.name = exactName; + this.contextScopes = contextScopes; + } + + static VariableNameStrategy short_() { + return new VariableNameStrategy(Style.SHORT, null, Collections.emptySet()); + } + + static VariableNameStrategy normal(Set contextScopes) { + return new VariableNameStrategy(Style.NORMAL, null, contextScopes); + } + + static VariableNameStrategy exact(String name) { + return new VariableNameStrategy(Style.EXACT, name, Collections.emptySet()); + } + + public String variableName(@Nullable JavaType type) { + // the instanceof operator only accepts classes (without generics) and arrays + if (style == Style.EXACT) { + //noinspection DataFlowIssue + return name; + } else if (type instanceof JavaType.FullyQualified) { + String className = ((JavaType.FullyQualified) type).getClassName(); + className = className.substring(className.lastIndexOf('.') + 1); + String baseName = null; + switch (style) { + case SHORT: + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < className.length(); i++) { + char c = className.charAt(i); + if (Character.isUpperCase(c)) { + builder.append(Character.toLowerCase(c)); + } + } + baseName = builder.length() > 0 ? builder.toString() : "o"; + break; + case NORMAL: + Set namesInScope = contextScopes.stream() + .flatMap(c -> VariableNameUtils.findNamesInScope(c).stream()) + .collect(Collectors.toSet()); + List nameSegments = Stream.of(NAME_SPLIT_PATTERN.split(className)) + .filter(s -> !s.isEmpty()).collect(Collectors.toList()); + for (int i = nameSegments.size() - 1; i >= 0; i--) { + String name = String.join("", nameSegments.subList(i, nameSegments.size())); + if (name.length() < 2) { + continue; + } + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + if (!namesInScope.contains(name)) { + baseName = name; + break; + } + } + if (baseName == null) { + baseName = Character.toLowerCase(className.charAt(0)) + className.substring(1); + } + break; + default: + baseName = "obj"; + } + String candidate = baseName; + OUTER: + while (true) { + for (Cursor scope : contextScopes) { + String newCandidate = VariableNameUtils.generateVariableName(candidate, scope, INCREMENT_NUMBER); + if (!newCandidate.equals(candidate)) { + candidate = newCandidate; + continue OUTER; + } + } + break; + } + return candidate; + } else if (type instanceof JavaType.Primitive) { + String keyword = ((JavaType.Primitive) type).getKeyword(); + return style == Style.SHORT ? keyword.substring(0, 1) : keyword; + } else if (type instanceof JavaType.Array) { + JavaType elemType = ((JavaType.Array) type).getElemType(); + while (elemType instanceof JavaType.Array) { + elemType = ((JavaType.Array) elemType).getElemType(); + } + return variableName(elemType) + 's'; + } + return style == Style.SHORT ? "o" : "obj"; + } + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java new file mode 100644 index 0000000000..129960d7ba --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java @@ -0,0 +1,100 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package org.openrewrite.staticanalysis; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Incubating; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; + +@Incubating( + since = "7.21.0" +) +public class RemoveUnneededBlock extends Recipe { + public RemoveUnneededBlock() { + } + + public String getDisplayName() { + return "Remove unneeded block"; + } + + public String getDescription() { + return "Flatten blocks into inline statements when possible."; + } + + public TreeVisitor getVisitor() { + return new RemoveUnneededBlockStatementVisitor(); + } + + static class RemoveUnneededBlockStatementVisitor extends JavaVisitor { + RemoveUnneededBlockStatementVisitor() { + } + + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + J.Block bl = (J.Block)super.visitBlock(block, ctx); + J directParent = (J)this.getCursor().getParentTreeCursor().getValue(); + return !(directParent instanceof J.NewClass) && !(directParent instanceof J.ClassDeclaration) ? this.maybeInlineBlock(bl, ctx) : bl; + } + + private J.Block maybeInlineBlock(J.Block block, ExecutionContext ctx) { + List statements = block.getStatements(); + if (statements.isEmpty()) { + return block; + } else { + Statement lastStatement = (Statement)statements.get(statements.size() - 1); + J.Block flattened = block.withStatements(ListUtils.flatMap(statements, (i, stmt) -> { + J.Block nested; + if (stmt instanceof J.Try) { + J.Try _try = (J.Try)stmt; + if (_try.getResources() != null || !_try.getCatches().isEmpty() || _try.getFinally() == null || !_try.getFinally().getStatements().isEmpty()) { + return stmt; + } + + nested = _try.getBody(); + } else { + if (!(stmt instanceof J.Block)) { + return stmt; + } + + nested = (J.Block)stmt; + } + + if (i < statements.size() - 1) { + Stream var10000 = nested.getStatements().stream(); + Objects.requireNonNull(J.VariableDeclarations.class); + if (var10000.anyMatch(J.VariableDeclarations.class::isInstance)) { + return stmt; + } + } + + return ListUtils.map(nested.getStatements(), (j, inlinedStmt) -> { + if (j == 0) { + inlinedStmt = (Statement)inlinedStmt.withPrefix(inlinedStmt.getPrefix().withComments(ListUtils.concatAll(nested.getComments(), inlinedStmt.getComments()))); + } + + return (Statement)this.autoFormat(inlinedStmt, ctx, this.getCursor()); + }); + })); + if (flattened == block) { + return block; + } else { + if (lastStatement instanceof J.Block) { + flattened = flattened.withEnd(flattened.getEnd().withComments(ListUtils.concatAll(((J.Block)lastStatement).getEnd().getComments(), flattened.getEnd().getComments()))); + } + + return flattened; + } + } + } + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java new file mode 100644 index 0000000000..fb3e293dbb --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java @@ -0,0 +1,138 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class ReplaceDeprecatedRuntimeExecMethods extends Recipe { + private static final MethodMatcher RUNTIME_EXEC_CMD = new MethodMatcher("java.lang.Runtime exec(String)"); + private static final MethodMatcher RUNTIME_EXEC_CMD_ENVP = new MethodMatcher("java.lang.Runtime exec(String, String[])"); + private static final MethodMatcher RUNTIME_EXEC_CMD_ENVP_FILE = new MethodMatcher("java.lang.Runtime exec(String, String[], java.io.File)"); + + @Override + public String getDisplayName() { + return "Replace deprecated `Runtime#exec()` methods"; + } + + @Override + public String getDescription() { + return "Replace `Runtime#exec(String)` methods to use `exec(String[])` instead because the former is deprecated " + + "after Java 18 and is no longer recommended for use by the Java documentation."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(3); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesJavaVersion<>(18), new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation m = super.visitMethodInvocation(method, ctx); + + if (RUNTIME_EXEC_CMD.matches(m) || RUNTIME_EXEC_CMD_ENVP.matches(m) || RUNTIME_EXEC_CMD_ENVP_FILE.matches(m)) { + Expression command = m.getArguments().get(0); + List commands = new ArrayList<>(); + boolean flattenAble = ChainStringBuilderAppendCalls.flatAdditiveExpressions(command, commands); + + StringBuilder sb = new StringBuilder(); + if (flattenAble) { + for (Expression e : commands) { + if (e instanceof J.Literal && ((J.Literal) e).getType() == JavaType.Primitive.String) { + sb.append(((J.Literal) e).getValue()); + } else { + flattenAble = false; + break; + } + } + } + + updateCursor(m); + if (flattenAble) { + String[] cmds = sb.toString().split(" "); + String templateCode = String.format("new String[] {%s}", toStringArguments(cmds)); + JavaTemplate template = JavaTemplate.builder(templateCode).build(); + + List args = m.getArguments(); + Cursor cursor = new Cursor(getCursor(), args.get(0)); + args.set(0, template.apply(cursor, args.get(0).getCoordinates().replace())); + + if (m.getMethodType() != null) { + List parameterTypes = m.getMethodType().getParameterTypes(); + parameterTypes.set(0, JavaType.ShallowClass.build("java.lang.String[]")); + + return m.withArguments(args) + .withMethodType(m.getMethodType().withParameterTypes(parameterTypes)); + } + } else { + // replace argument to 'command.split(" ")' + List args = m.getArguments(); + boolean needWrap = false; + Expression arg0 = args.get(0); + if (!(arg0 instanceof J.Identifier) && + !(arg0 instanceof J.Literal) && + !(arg0 instanceof J.MethodInvocation)) { + needWrap = true; + } + + String code = needWrap ? "(#{any()}).split(\" \")" : "#{any()}.split(\" \")"; + JavaTemplate template = JavaTemplate.builder(code).contextSensitive().build(); + Cursor cursor = new Cursor(getCursor(), args.get(0)); + arg0 = template.apply(cursor, args.get(0).getCoordinates().replace(), args.get(0)); + args.set(0, arg0); + + if (m.getMethodType() != null) { + List parameterTypes = m.getMethodType().getParameterTypes(); + parameterTypes.set(0, JavaType.ShallowClass.build("java.lang.String[]")); + + return m.withArguments(args).withMethodType(m.getMethodType().withParameterTypes(parameterTypes)); + } + return m; + } + } + + return m; + } + }); + } + + private static String toStringArguments(String[] cmds) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < cmds.length; i++) { + String token = cmds[i]; + if (i != 0) { + sb.append(", "); + } + sb.append("\"") + .append(token) + .append("\""); + } + return sb.toString(); + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java new file mode 100644 index 0000000000..acfe60807a --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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.staticanalysis.groovy; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.groovy.tree.G; +import org.openrewrite.marker.SearchResult; + +/** + * Add a search marker if vising a Groovy file + */ +public class GroovyFileChecker

extends TreeVisitor { + @Override + public @Nullable Tree visit(@Nullable Tree tree, P p) { + if (tree instanceof G.CompilationUnit) { + return SearchResult.found(tree); + } + return tree; + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java new file mode 100644 index 0000000000..8f6e89e815 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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.staticanalysis.kotlin; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +/** + * Add a search marker if vising a Kotlin file + */ +public class KotlinFileChecker

extends TreeVisitor { + @Override + public @Nullable Tree visit(@Nullable Tree tree, P p) { + if (tree instanceof J.CompilationUnit cu) { + if (cu.getSourcePath() != null ) { + String fileName = cu.getSourcePath().getFileName().toString(); + if (fileName.endsWith(".kt") || fileName.endsWith(".kts")) { + return SearchResult.found(cu); + } + } + } + return tree; + } +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java new file mode 100644 index 0000000000..5e562810e7 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java @@ -0,0 +1,34 @@ +package org.springframework.ide.vscode.commons.rewrite.java; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.NlsRewrite; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +public class JavaFileChecker extends Recipe { + @Override + public @NlsRewrite.DisplayName String getDisplayName() { + return "Checks if source is a Java file"; + } + + @Override + public @NlsRewrite.Description String getDescription() { + return "Checks if source is a Java file."; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext executionContext) { + if (cu.getSourcePath() != null && cu.getSourcePath().getFileName().toString().endsWith(".java")) { + return SearchResult.found(cu); + } + return cu; + } + }; + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java new file mode 100644 index 0000000000..cb3beed753 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java @@ -0,0 +1,192 @@ +/* + * 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class AddSerialAnnotationToSerialVersionUIDTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddSerialAnnotationToSerialVersionUID()) + .allSources(sourceSpec -> sourceSpec.markers(javaVersion(17))); + } + + @DocumentExample + @Test + void addSerialAnnotation() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + private static final long serialVersionUID = 1L; + } + """, + """ + import java.io.Serial; + import java.io.Serializable; + + class Example implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + } + """ + ) + ); + } + + @Test + void shouldAddToNewFieldWhenChained() { + rewriteRun( + spec -> spec.recipes( + new AddSerialVersionUidToSerializable(), + new AddSerialAnnotationToSerialVersionUID()), + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + } + """, + """ + import java.io.Serial; + import java.io.Serializable; + + class Example implements Serializable { + @Serial + private static final long serialVersionUID = 1; + } + """ + ) + ); + } + + @Test + void shouldNoopIfAlreadyPresent() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + import java.io.Serial; + + class Example implements Serializable { + String var1 = "first variable"; + @Serial + private static final long serialVersionUID = 1L; + int var3 = 666; + } + """ + ) + ); + } + + @Test + void shouldNotAnnotateNonSerializableClass() { + rewriteRun( + //language=java + java( + """ + class Example { + private static final long serialVersionUID = 1L; + } + """ + ) + ); + } + + @Test + void shouldNotAnnotateOnJava11() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + private static final long serialVersionUID = 1L; + } + """, + spec -> spec.markers(javaVersion(11)) + ) + ); + } + + @Test + void shouldNotAnnotateOtherFields() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + static final long serialVersionUID = 1L; + private final long serialVersionUID = 1L; + private static long serialVersionUID = 1L; + private static final int serialVersionUID = 1L; + private static final long foo = 1L; + + void doSomething() { + long serialVersionUID = 1L; + } + } + """ + ) + ); + } + + @Test + void shouldAnnotatedFieldsInInnerClasses() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Outer implements Serializable { + private static final long serialVersionUID = 1; + static class Inner implements Serializable { + private static final long serialVersionUID = 1; + } + } + """, + """ + import java.io.Serial; + import java.io.Serializable; + + class Outer implements Serializable { + @Serial + private static final long serialVersionUID = 1; + static class Inner implements Serializable { + @Serial + private static final long serialVersionUID = 1; + } + } + """ + ) + ); + } +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java new file mode 100644 index 0000000000..696f002486 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java @@ -0,0 +1,275 @@ +/* + * Copyright 2021 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings("MissingSerialAnnotation") +class AddSerialVersionUidToSerializableTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddSerialVersionUidToSerializable()); + } + + @Test + void doNothingNotSerializable() { + rewriteRun( + //language=java + java( + """ + public class Example { + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @DocumentExample + @Test + void addSerialVersionUID() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + private String fred; + private int numberOfFreds; + } + """, + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @Test + void fixSerialVersionUIDModifiers() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + private final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """, + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @Test + void fixSerialVersionUIDNoModifiers() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """, + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @Test + void fixSerialVersionUIDNoModifiersWrongType() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + Long serialVersionUID = 1L; + private String fred; + private int numberOfFreds; + } + """, + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1L; + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @Test + void uidAlreadyPresent() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + } + """ + ) + ); + } + + @Test + void methodDeclarationsAreNotVisited() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class Example implements Serializable { + private String fred; + private int numberOfFreds; + void doSomething() { + int serialVersionUID = 1; + } + } + """, + """ + import java.io.Serializable; + + public class Example implements Serializable { + private static final long serialVersionUID = 1; + private String fred; + private int numberOfFreds; + void doSomething() { + int serialVersionUID = 1; + } + } + """ + ) + ); + } + + @Test + void doNotAlterAnInterface() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public interface Example extends Serializable { + } + """ + ) + ); + } + + @Test + void doNotAlterAnException() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class MyException extends Exception implements Serializable { + } + """ + ) + ); + } + + @Test + void doNotAlterARuntimeException() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + public class MyException extends RuntimeException implements Serializable { + } + """ + ) + ); + } + + @Test + void serializableInnerClass() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + public class Outer implements Serializable { + public static class Inner implements Serializable { + } + } + """, + """ + import java.io.Serializable; + public class Outer implements Serializable { + private static final long serialVersionUID = 1; + public static class Inner implements Serializable { + private static final long serialVersionUID = 1; + } + } + """ + ) + ); + } +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java new file mode 100644 index 0000000000..a57b908df9 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java @@ -0,0 +1,313 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings({"StringConcatenationInsideStringBufferAppend", "StringBufferReplaceableByString"}) +class ChainStringBuilderAppendCallsTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ChainStringBuilderAppendCalls()); + } + + @DocumentExample(value = "Chain `StringBuilder.append()` calls instead of the '+' operator to efficiently concatenate strings and numbers.") + @Test + void objectsConcatenation() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A" + op + "B"); + sb.append(1 + op + 2); + } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A").append(op).append("B"); + sb.append(1).append(op).append(2); + } + } + """ + ) + ); + } + + @Test + void literalConcatenationIgnored() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + sb.append("A" + "B" + "C"); + } + } + """ + ) + ); + } + + @DocumentExample("Grouping concatenation.") + @Test + void groupedStringsConcatenation() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A" + "B" + "C" + op + "D" + "E"); + } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A" + "B" + "C").append(op).append("D" + "E"); + } + } + """ + ) + ); + } + + @Test + void unWrap() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append(("A" + op + "B")); + } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A").append(op).append("B"); + } + } + """ + ) + ); + } + + @Test + void chainedAppend() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append(("A" + op)).append("B"); + } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A").append(op).append("B"); + } + } + """ + ) + ); + } + + @Test + void runMultipleTimes() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append(("A" + op) + "B"); + } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append("A").append(op).append("B"); + } + } + """ + ) + ); + } + + @Test + void correctlyGroupConcatenations() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append(op + 1 + 2 + "A" + "B" + 'x'); + sb.append(1 + 2 + op + 3 + 4); + sb.append(1 + 2 + name() + 3 + 4); + sb.append(op + (1 + 2)); + } + String name() { return "name"; } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + String op = "+"; + sb.append(op).append(1).append(2).append("A" + "B").append('x'); + sb.append(1 + 2).append(op).append(3).append(4); + sb.append(1 + 2).append(name()).append(3).append(4); + sb.append(op).append(1 + 2); + } + String name() { return "name"; } + } + """ + ) + ); + } + + @Test + void appendMethods() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + sb.append(str1() + str2() + str3()); + } + + String str1() { return "A"; } + String str2() { return "B"; } + String str3() { return "C"; } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder(); + sb.append(str1()).append(str2()).append(str3()); + } + + String str1() { return "A"; } + String str2() { return "B"; } + String str3() { return "C"; } + } + """ + ) + ); + } + + @Test + void ChainedAppendWithConstructor() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder().append("A" + operator() + "B"); + } + + String operator() { return "+"; } + } + """, + """ + class A { + void method1() { + StringBuilder sb = new StringBuilder().append("A").append(operator()).append("B"); + } + + String operator() { return "+"; } + } + """ + ) + ); + } + + @Test + void methodArgument() { + rewriteRun( + //language=java + java( + """ + class A { + void method1() { + String op = "+"; + print(new StringBuilder().append("A" + op + "C").toString()); + } + + void print(String str) { + } + } + """, + """ + class A { + void method1() { + String op = "+"; + print(new StringBuilder().append("A").append(op).append("C").toString()); + } + + void print(String str) { + } + } + """ + ) + ); + } +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java new file mode 100644 index 0000000000..4eaee8b51a --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java @@ -0,0 +1,1296 @@ +/* + * Copyright 2021 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.staticanalysis; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.Issue; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.version; + +@SuppressWarnings({"RedundantCast", "DataFlowIssue", "ConstantValue", "ImplicitArrayToString", "PatternVariableCanBeUsed", "UnnecessaryLocalVariable", "SizeReplaceableByIsEmpty", "rawtypes", "ResultOfMethodCallIgnored", "ArraysAsListWithZeroOrOneArgument", "DuplicateCondition"}) +class InstanceOfPatternMatchTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new InstanceOfPatternMatch()) + .allSources(sourceSpec -> version(sourceSpec, 17)); + } + + + @Nested + class If { + @Test + void ifConditionWithoutPattern() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + Object s = 1; + if (o instanceof String && ((String) (o)).length() > 0) { + if (((String) o).length() > 1) { + System.out.println(o); + } + } + } + } + """, + """ + public class A { + void test(Object o) { + Object s = 1; + if (o instanceof String string && string.length() > 0) { + if (string.length() > 1) { + System.out.println(o); + } + } + } + } + """ + ) + ); + } + + @Test + void multipleCasts() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o, Object o2) { + Object string = 1; + if (o instanceof String && o2 instanceof Integer) { + System.out.println((String) o); + System.out.println((Integer) o2); + } + } + } + """, + """ + public class A { + void test(Object o, Object o2) { + Object string = 1; + if (o instanceof String string1 && o2 instanceof Integer integer) { + System.out.println(string1); + System.out.println(integer); + } + } + } + """ + ) + ); + } + + @Test + void longNames() { + rewriteRun( + //language=java + java( + """ + import java.util.ArrayList; + public class A { + void test(Object o) { + Object list = 1; + if (o instanceof ArrayList) { + System.out.println((ArrayList) o); + } + } + } + """, + """ + import java.util.ArrayList; + public class A { + void test(Object o) { + Object list = 1; + if (o instanceof ArrayList arrayList) { + System.out.println(arrayList); + } + } + } + """ + ) + ); + } + + @Test + void typeParameters_1() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + import java.util.stream.Stream; + public class A { + @SuppressWarnings("unchecked") + public static Stream> applyRoutesType(Object routes) { + if (routes instanceof List) { + List routesList = (List) routes; + if (routesList.isEmpty()) { + return Stream.empty(); + } + if (routesList.stream() + .anyMatch(route -> !(route instanceof Map))) { + return Stream.empty(); + } + return routesList.stream() + .map(route -> (Map) route); + } + return Stream.empty(); + } + } + """ + ) + ); + } + + @Test + void typeParameters_2() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + public class A { + public static List> applyRoutesType(Object routes) { + if (routes instanceof List) { + List routesList = (List) routes; + if (routesList.isEmpty()) { + return Collections.emptyList(); + } + } + return Collections.emptyList(); + } + } + """, + """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + public class A { + public static List> applyRoutesType(Object routes) { + if (routes instanceof List routesList) { + if (routesList.isEmpty()) { + return Collections.emptyList(); + } + } + return Collections.emptyList(); + } + } + """ + ) + ); + } + + @Test + void typeParameters_3() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + public static void applyRoutesType(Object routes) { + if (routes instanceof List) { + List routesList = (List) routes; + String.join(",", (List) routes); + } + } + } + """, + """ + import java.util.Collections; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + public static void applyRoutesType(Object routes) { + if (routes instanceof List list) { + List routesList = (List) routes; + String.join(",", list); + } + } + } + """ + ) + ); + } + + @Test + void typeParameters_4() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + public static void applyRoutesType(Object routes) { + if (routes instanceof List) { + String.join(",", (List) routes); + } + } + } + """, """ + import java.util.Collections; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + public static void applyRoutesType(Object routes) { + if (routes instanceof List list) { + String.join(",", list); + } + } + } + """ + ) + ); + } + + @Test + void typeParameters_5() { + rewriteRun( + //language=java + java( + """ + import java.util.Arrays; + import java.util.Collection; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + private Collection addValueToList(List previousValues, Object value) { + if (previousValues == null) { + return (value instanceof Collection) ? (Collection) value : Arrays.asList(value); + } + return List.of(); + } + } + """ + ) + ); + } + + @Test + void typeParameters_6() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + public class A { + @SuppressWarnings("unchecked") + public static List> applyRoutesType(Object routes) { + if (routes instanceof List) { + List routesList = (List) routes; + if (routesList.isEmpty()) { + return Collections.emptyList(); + } + if (routesList.stream() + .anyMatch(route -> !(route instanceof Map))) { + return Collections.emptyList(); + } + return routesList.stream() + .map(route -> (Map) route) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + } + """ + ) + ); + } + + @Test + void typeParameters_7() { + rewriteRun( + //language=java + java( + """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + public class A { + @SuppressWarnings("unchecked") + public static List> applyRoutesType(Object routes) { + if (routes instanceof List) { + return ((List) routes).stream() + .map(route -> (Map) route) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + } + """, """ + import java.util.Collections; + import java.util.List; + import java.util.Map; + import java.util.stream.Collectors; + public class A { + @SuppressWarnings("unchecked") + public static List> applyRoutesType(Object routes) { + if (routes instanceof List list) { + return list.stream() + .map(route -> (Map) route) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + } + """ + ) + ); + } + + @Test + void typeParameters_8() { + rewriteRun( + //language=java + java( + """ + import java.util.Arrays; + import java.util.Collection; + import java.util.List; + public class A { + @SuppressWarnings("unchecked") + private Collection addValueToList(List previousValues, Object value) { + Collection cl = List.of(); + if (previousValues == null) { + if (value instanceof Collection) { + cl = (Collection) value; + } else { + cl = Arrays.asList(value.toString()); + } + } + return cl; + } + } + """ + ) + ); + } + + @Test + void primitiveArray() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof int[]) { + System.out.println((int[]) o); + } + } + } + """, + """ + public class A { + void test(Object o) { + if (o instanceof int[] ints) { + System.out.println(ints); + } + } + } + """ + ) + ); + } + + @Test + void matchingVariableInBody() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String) { + String str = (String) o; + String str2 = (String) o; + System.out.println(str + str2); + } + } + } + """, + """ + public class A { + void test(Object o) { + if (o instanceof String str) { + String str2 = str; + System.out.println(str + str2); + } + } + } + """ + ) + ); + } + + @Test + void conflictingVariableInBody() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String) { + String string = "x"; + System.out.println((String) o); + // String string1 = "y"; + } + } + } + """, + """ + public class A { + void test(Object o) { + if (o instanceof String string1) { + String string = "x"; + System.out.println(string1); + // String string1 = "y"; + } + } + } + """ + ) + ); + } + + @Test + void conflictingVariableOfNestedType() { + rewriteRun( + //language=java + java( + """ + import java.util.Map; + + public class A { + void test(Object o) { + Map.Entry entry = null; + if (o instanceof Map.Entry) { + entry = (Map.Entry) o; + } + System.out.println(entry); + } + } + """, + """ + import java.util.Map; + + public class A { + void test(Object o) { + Map.Entry entry = null; + if (o instanceof Map.Entry entry1) { + entry = entry1; + } + System.out.println(entry); + } + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite/issues/2787") + @Disabled + @Test + void nestedPotentiallyConflictingIfs() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String) { + if (o instanceof String) { + System.out.println((String) o); + } + System.out.println((String) o); + } + } + } + """, + """ + public class A { + void test(Object o) { + if (o instanceof String string) { + if (o instanceof String string1) { + System.out.println(string1); + } + System.out.println(string); + } + } + } + """ + ) + ); + } + + @Test + void expressionWithSideEffects() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + Object s = 1; + if (convert(o) instanceof String && ((String) convert(o)).length() > 0) { + if (((String) convert(o)).length() > 1) { + System.out.println(o); + } + } + } + Object convert(Object o) { + return o; + } + } + """ + ) + ); + } + + @Test + void noTypeCast() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String) { + System.out.println(o); + } + } + } + """ + ) + ); + } + + @Test + void typeCastInElse() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String) { + System.out.println(o); + } else { + System.out.println((String) o); + } + } + } + """ + ) + ); + } + + @Test + void ifConditionWithPattern() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String s && s.length() > 0) { + System.out.println(s); + } + } + } + """ + ) + ); + } + + @Test + void orOperationInIfCondition() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (o instanceof String || ((String) o).length() > 0) { + if (((String) o).length() > 1) { + System.out.println(o); + } + } + } + } + """ + ) + ); + } + + @Test + void negatedInstanceOfMatchedInElse() { + rewriteRun( + //language=java + java( + """ + public class A { + void test(Object o) { + if (!(o instanceof String)) { + System.out.println(((String) o).length()); + } else { + System.out.println(((String) o).length()); + } + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/174") + void ifTwoDifferentInstanceOf() { + rewriteRun( + version( + //language=java + java( + """ + class A { + int combinedLength(Object o, Object o2) { + if (o instanceof String && o2 instanceof String) { + return ((String) o).length() + ((String) o2).length(); + } + return -1; + } + } + """, + """ + class A { + int combinedLength(Object o, Object o2) { + if (o instanceof String string && o2 instanceof String string1) { + return string.length() + string1.length(); + } + return -1; + } + } + """ + ), 17 + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/174") + void ifTwoDifferentInstanceOfWithParentheses() { + rewriteRun( + version( + //language=java + java( + """ + class A { + int combinedLength(Object o, Object o2) { + if (o instanceof String && (o2 instanceof String)) { + return ((String) o).length() + ((String) o2).length(); + } + return -1; + } + } + """, + """ + class A { + int combinedLength(Object o, Object o2) { + if (o instanceof String string && (o2 instanceof String string1)) { + return string.length() + string1.length(); + } + return -1; + } + } + """ + ), 17 + ) + ); + } + } + + @SuppressWarnings({"CastCanBeRemovedNarrowingVariableType", "ClassInitializerMayBeStatic"}) + @Nested + class Ternary { + + @Test + void typeCastInTrue() { + rewriteRun( + //language=java + java( + """ + public class A { + String test(Object o) { + return o instanceof String ? ((String) o).substring(1) : o.toString(); + } + } + """, + """ + public class A { + String test(Object o) { + return o instanceof String s ? s.substring(1) : o.toString(); + } + } + """ + ) + ); + } + + @Test + void multipleVariablesOnlyOneUsed() { + rewriteRun( + //language=java + java( + """ + public class A { + String test(Object o1, Object o2) { + return o1 instanceof String && o2 instanceof Number + ? ((String) o1).substring(1) : o1.toString(); + } + } + """, + """ + public class A { + String test(Object o1, Object o2) { + return o1 instanceof String s && o2 instanceof Number + ? s.substring(1) : o1.toString(); + } + } + """ + ) + ); + } + + @Test + void initBlocks() { + rewriteRun( + //language=java + java( + """ + public class A { + static { + Object o = null; + String s = o instanceof String ? ((String) o).substring(1) : String.valueOf(o); + } + { + Object o = null; + String s = o instanceof String ? ((String) o).substring(1) : String.valueOf(o); + } + } + """, + """ + public class A { + static { + Object o = null; + String s = o instanceof String s1 ? s1.substring(1) : String.valueOf(o); + } + { + Object o = null; + String s = o instanceof String s1 ? s1.substring(1) : String.valueOf(o); + } + } + """ + ) + ); + } + + @Test + void typeCastInFalse() { + rewriteRun( + //language=java + java( + """ + public class A { + String test(Object o) { + return o instanceof String ? o.toString() : ((String) o).substring(1); + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite-static-analysis/pull/265") + void multipleCastsInDifferentOperands() { + rewriteRun( + //language=java + java( + """ + import java.util.Comparator; + public class A { + Comparator comparator() { + return (a, b) -> + a instanceof String && b instanceof String ? ((String) a).compareTo((String) b) : 0; + } + } + """, + """ + import java.util.Comparator; + public class A { + Comparator comparator() { + return (a, b) -> + a instanceof String s && b instanceof String s1 ? s.compareTo(s1) : 0; + } + } + """ + ) + ); + } + } + + @Nested + class Binary { + + @Test + void onlyReplacementsBeforeOrOperator() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean test(Object o) { + return o instanceof String && ((String) o).length() > 1 || ((String) o).length() > 2; + } + } + """, + """ + public class A { + boolean test(Object o) { + return o instanceof String s && s.length() > 1 || ((String) o).length() > 2; + } + } + """ + ) + ); + } + + @Test + void methodCallBreaksFlowScope() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean m(Object o) { + return test(o instanceof String) && ((String) o).length() > 1; + } + boolean test(boolean b) { + return b; + } + } + """ + ) + ); + } + } + + @Nested + class Arrays { + + @Test + void string() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean test(Object o) { + return o instanceof String[] && ((java.lang.String[]) o).length > 1 || ((String[]) o).length > 2; + } + } + """, + """ + public class A { + boolean test(Object o) { + return o instanceof String[] ss && ss.length > 1 || ((String[]) o).length > 2; + } + } + """ + ) + ); + } + + @Test + void primitive() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean test(Object o) { + return o instanceof int[] && ((int[]) o).length > 1 || ((int[]) o).length > 2; + } + } + """, + """ + public class A { + boolean test(Object o) { + return o instanceof int[] is && is.length > 1 || ((int[]) o).length > 2; + } + } + """ + ) + ); + } + + @Test + void multiDimensional() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean test(Object o) { + return o instanceof int[][] && ((int[][]) o).length > 1 || ((int[][]) o).length > 2; + } + } + """, + """ + public class A { + boolean test(Object o) { + return o instanceof int[][] is && is.length > 1 || ((int[][]) o).length > 2; + } + } + """ + ) + ); + } + + @Test + void dimensionalMismatch() { + rewriteRun( + //language=java + java( + """ + public class A { + boolean test(Object o) { + return o instanceof int[][] && ((int[]) o).length > 1; + } + } + """ + ) + ); + } + } + + @SuppressWarnings("unchecked") + @Nested + class Generics { + @Test + void wildcardInstanceOf() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + public class A { + Object test(Object o) { + if (o instanceof List) { + return ((List) o).get(0); + } + return o.toString(); + } + } + """, + """ + import java.util.List; + public class A { + Object test(Object o) { + if (o instanceof List list) { + return list.get(0); + } + return o.toString(); + } + } + """ + ) + ); + } + + @Test + void rawInstanceOfAndWildcardParameterizedCast() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + public class A { + Object test(Object o) { + return o instanceof List ? ((List) o).get(0) : o.toString(); + } + } + """, + """ + import java.util.List; + public class A { + Object test(Object o) { + return o instanceof List l ? l.get(0) : o.toString(); + } + } + """ + ) + ); + } + + @Test + void rawInstanceOfAndObjectParameterizedCast() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + public class A { + Object test(Object o) { + return o instanceof List ? ((List) o).get(0) : o.toString(); + } + } + """/*, + """ + import java.util.List; + public class A { + Object test(Object o) { + return o instanceof List l ? l.get(0) : o.toString(); + } + } + """*/ + ) + ); + } + + @Test + void rawInstanceOfAndParameterizedCast() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + public class A { + String test(Object o) { + return o instanceof List ? ((List) o).get(0) : o.toString(); + } + } + """ + ) + ); + } + + @Test + void unboundGenericTypeVariable() { + rewriteRun( + //language=java + java( + """ + import java.util.List; + public class A { + void test(Object t) { + if (t instanceof List) { + List l = (List) t; + System.out.println(l.size()); + } + } + } + """ + ) + ); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Nested + class Various { + @Test + void unaryWithoutSideEffects() { + rewriteRun( + //language=java + java( + """ + public class A { + String test(Object o) { + return ((Object) ("1" + ~1)) instanceof String ? ((String) ((Object) ("1" + ~1))).substring(1) : o.toString(); + } + } + """, + """ + public class A { + String test(Object o) { + return ((Object) ("1" + ~1)) instanceof String s ? s.substring(1) : o.toString(); + } + } + """ + ) + ); + } + + @Test + void nestedClasses() { + rewriteRun( + //language=java + java( + """ + public class A { + public static class Else {} + String test(Object o) { + if (o instanceof Else) { + return ((Else) o).toString(); + } + return o.toString(); + } + } + """, + """ + public class A { + public static class Else {} + String test(Object o) { + if (o instanceof Else else1) { + return else1.toString(); + } + return o.toString(); + } + } + """ + ) + ); + } + + @Test + void iterableParameter() { + rewriteRun( + //language=java + java( + """ + import java.util.HashMap; + import java.util.List; + import java.util.Map; + + public class ApplicationSecurityGroupsParameterHelper { + static final String APPLICATION_SECURITY_GROUPS = "application-security-groups"; + public Map transformGatewayParameters(Map parameters) { + Map environment = new HashMap<>(); + Object applicationSecurityGroups = parameters.get(APPLICATION_SECURITY_GROUPS); + if (applicationSecurityGroups instanceof List) { + environment.put(APPLICATION_SECURITY_GROUPS, String.join(",", (List) applicationSecurityGroups)); + } + return environment; + } + } + """, + """ + import java.util.HashMap; + import java.util.List; + import java.util.Map; + + public class ApplicationSecurityGroupsParameterHelper { + static final String APPLICATION_SECURITY_GROUPS = "application-security-groups"; + public Map transformGatewayParameters(Map parameters) { + Map environment = new HashMap<>(); + Object applicationSecurityGroups = parameters.get(APPLICATION_SECURITY_GROUPS); + if (applicationSecurityGroups instanceof List list) { + environment.put(APPLICATION_SECURITY_GROUPS, String.join(",", list)); + } + return environment; + } + } + """ + ) + ); + } + } + + @Nested + class Throws { + @Test + void throwsException() { + rewriteRun( + //language=java + java( + """ + class A { + void test(Throwable t) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + } + } + """, + """ + class A { + void test(Throwable t) { + if (t instanceof RuntimeException exception) { + throw exception; + } + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/307") + void throwsExceptionWithExtraParentheses() { + rewriteRun( + //language=java + java( + """ + class A { + void test(Throwable t) { + if (t instanceof Exception) { + // Extra parentheses trips up the replacement + throw ((Exception) t); + } + } + } + """, + """ + class A { + void test(Throwable t) { + if (t instanceof Exception exception) { + // Extra parentheses trips up the replacement + throw exception; + } + } + } + """ + ) + ); + } + + } +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/RemoveUnneededBlockTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/RemoveUnneededBlockTest.java new file mode 100644 index 0000000000..88cdef6e32 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/RemoveUnneededBlockTest.java @@ -0,0 +1,568 @@ +/* + * Copyright 2022 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings({"UnusedLabel", "StatementWithEmptyBody", "Convert2Diamond", "ConstantConditions", "ClassInitializerMayBeStatic"}) +class RemoveUnneededBlockTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new RemoveUnneededBlock()); + } + + @Test + void doNotChangeMethod() { + rewriteRun( + //language=java + java( + """ + class A { + void test() { + } + } + """ + ) + ); + } + + @Test + void doNotChangeLabeledBlock() { + rewriteRun( + //language=java + java( + """ + class A { + void test() { + testLabel: { + System.out.println("hello!"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeEmptyIfBlock() { + rewriteRun( + //language=java + java( + """ + class A { + void test() { + if(true) { } + } + } + """ + ) + ); + } + + @Test + void doNotRemoveDoubleBraceInitBlocksInMethod() { + rewriteRun( + //language=java + java( + """ + import java.util.HashSet; + import java.util.Set; + public class T { + public void whenInitializeSetWithDoubleBraces_containsElements() { + Set countries = new HashSet() { + { + add("a"); + add("b"); + } + }; + } + } + """ + ) + ); + } + + @Test + void doNotRemoveDoubleBraceInitBlocks() { + rewriteRun( + //language=java + java( + """ + import java.util.HashSet; + import java.util.Set; + public class T { + final Set countries = new HashSet() { + { + add("a"); + add("b"); + } + }; + } + """ + ) + ); + } + + @Test + void doNotRemoveObjectArrayInitializer() { + rewriteRun( + //language=java + java( + """ + public class A { + Object[] a = new Object[] { + "a", + "b" + }; + } + """ + ) + ); + } + + @Test + void doNotRemoveObjectArrayArrayInitializer() { + rewriteRun( + //language=java + java( + """ + public class A { + Object[][] a = new Object[][] { + { "a", "b" }, + { "c", "d" } + }; + } + """ + ) + ); + } + + @DocumentExample + @Test + void simplifyNestedBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + void test() { + { + System.out.println("hello!"); + } + } + } + """, + """ + public class A { + void test() { + System.out.println("hello!"); + } + } + """ + ) + ); + } + + @Test + void simplifyDoublyNestedBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + void test() { + { + { System.out.println("hello!"); } + } + } + } + """, + """ + public class A { + void test() { + System.out.println("hello!"); + } + } + """ + ) + ); + } + + @Test + void simplifyBlockNestedInIfBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + void test() { + if (true) { + { System.out.println("hello!"); } + } + } + } + """, + """ + public class A { + void test() { + if (true) { + System.out.println("hello!"); + } + } + } + """ + ) + ); + } + + @Test + void simplifyBlockInStaticInitializerIfBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + static { + { + { + System.out.println("hello static!"); + System.out.println("goodbye static!"); + } + } + } + + { + { + System.out.println("hello init!"); + System.out.println("goodbye init!"); + } + } + } + """, + """ + public class A { + static { + System.out.println("hello static!"); + System.out.println("goodbye static!"); + } + + { + System.out.println("hello init!"); + System.out.println("goodbye init!"); + } + } + """ + ) + ); + } + + @Test + void simplifyCraziness() { + rewriteRun( + //language=java + java( + """ + import java.util.HashSet; + import java.util.Set; + public class A { + static { + { + new HashSet() { + { + add("a"); + add("b"); + { + System.out.println("hello static!"); + System.out.println("goodbye static!"); + } + } + }; + } + } + + { + { + new HashSet() { + { + add("a"); + add("b"); + { + System.out.println("hello init!"); + System.out.println("goodbye init!"); + } + } + }; + } + } + } + """, + """ + import java.util.HashSet; + import java.util.Set; + public class A { + static { + new HashSet() { + { + add("a"); + add("b"); + System.out.println("hello static!"); + System.out.println("goodbye static!"); + } + }; + } + + { + new HashSet() { + { + add("a"); + add("b"); + System.out.println("hello init!"); + System.out.println("goodbye init!"); + } + }; + } + } + """ + ) + ); + } + + @Test + void simplifyDoesNotFormatSurroundingCode() { + rewriteRun( + //language=java + java( + """ + public class A { + static int[] a; + static int[] b; + static { + a = new int[] { 1, 2, 3 }; + b = new int[] {4,5,6}; + { + System.out.println("hello static!"); + } + } + } + """, + """ + public class A { + static int[] a; + static int[] b; + static { + a = new int[] { 1, 2, 3 }; + b = new int[] {4,5,6}; + System.out.println("hello static!"); + } + } + """ + ) + ); + } + + @Test + void simplifyDoesNotFormatInternalCode() { + rewriteRun( + //language=java + java( + """ + public class A { + int[] a; + int[] b; + static { + a = new int[] { 1, 2, 3 }; + b = new int[] {4,5,6}; + { + System.out.println("hello!"); + System.out.println( "world!" ); + } + } + } + """, + """ + public class A { + int[] a; + int[] b; + static { + a = new int[] { 1, 2, 3 }; + b = new int[] {4,5,6}; + System.out.println("hello!"); + System.out.println("world!"); + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/3073") + void preserveComments() { + rewriteRun( + //language=java + java( + """ + public class A { + static { + // comment before (outside) + { + // comment before (inside) + System.out.println("hello world!"); + // comment after (inside) + } + // comment after (outside) + } + } + """, + """ + public class A { + static { + // comment before (outside) + // comment before (inside) + System.out.println("hello world!"); + // comment after (inside) + // comment after (outside) + } + } + """ + ) + ); + } + + @Test + void preserveBlocksContainingVariableDeclarations() { + rewriteRun( + //language=java + java( + """ + public class A { + static { + { + int i = 0; + } + System.out.println("hello world!"); + } + } + """ + ) + ); + } + + @Test + void inlineLastBlockContainingVariableDeclarations() { + rewriteRun( + //language=java + java( + """ + public class A { + static { + { + System.out.println("hello world!"); + } + { + int i = 0; + } + } + } + """, + """ + public class A { + static { + System.out.println("hello world!"); + int i = 0; + } + } + """ + ) + ); + } + + @Test + @SuppressWarnings("EmptyFinallyBlock") + void removeEmptyTryFinallyBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + public int foo() { + try { + int i = 1; + } finally { + } + } + } + """, + """ + public class A { + public int foo() { + int i = 1; + } + } + """ + ) + ); + } + + @Test + void keepNonEmptyTryFinallyBlock() { + rewriteRun( + //language=java + java( + """ + public class A { + public int foo() { + try { + int i = 1; + } finally { + System.out.println("hello world!"); + } + } + } + """ + ) + ); + } + + @Test + @SuppressWarnings("EmptyFinallyBlock") + void keepNonEmptyTryFinallyBlock2() { + rewriteRun( + //language=java + java( + """ + public class A { + public int foo() { + try { + int i = 1; + } finally { + } + int i = 1; + } + } + """ + ) + ); + } + +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethodsTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethodsTest.java new file mode 100644 index 0000000000..36ed0c3ab9 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethodsTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.version; + +class ReplaceDeprecatedRuntimeExecMethodsTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ReplaceDeprecatedRuntimeExecMethods()); + } + + @Test + void rawString() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.File; + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + + Process process1 = runtime.exec("ls -a -l"); + Process process2 = runtime.exec("ls -a -l", envp); + Process process3 = runtime.exec("ls -a -l", envp, dir); + } + } + """, + """ + import java.io.File; + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + + Process process1 = runtime.exec(new String[]{"ls", "-a", "-l"}); + Process process2 = runtime.exec(new String[]{"ls", "-a", "-l"}, envp); + Process process3 = runtime.exec(new String[]{"ls", "-a", "-l"}, envp, dir); + } + } + """ + ), 18) + ); + } + + @Test + void stringVariableAsInput() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.File; + import java.io.IOException; + + class B { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String command = "ls -al"; + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + Process process1 = runtime.exec(command); + Process process2 = runtime.exec(command, envp); + Process process3 = runtime.exec(command, envp, dir); + } + } + """, + """ + import java.io.File; + import java.io.IOException; + + class B { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String command = "ls -al"; + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + Process process1 = runtime.exec(command.split(" ")); + Process process2 = runtime.exec(command.split(" "), envp); + Process process3 = runtime.exec(command.split(" "), envp, dir); + } + } + """ + ), 18) + ); + } + + @Test + void methodInvocationAsInput() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.IOException; + + class B { + String command() { + return "ls -al"; + } + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec(command()); + } + } + """, + """ + import java.io.IOException; + + class B { + String command() { + return "ls -al"; + } + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec(command().split(" ")); + } + } + """ + ), 18) + ); + } + + @Test + void concatenatedRawStringsAsInput() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec("ls" + " " + "-a" + " " + "-l"); + } + } + """, + """ + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec(new String[]{"ls", "-a", "-l"}); + } + } + """ + ), 18) + ); + } + + @Test + void concatenatedObjectsAsInput() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.IOException; + + class B { + String options = "-a -l"; + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec("ls" + " " + options); + } + } + """, + """ + import java.io.IOException; + + class B { + String options = "-a -l"; + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + Process process = runtime.exec(("ls" + " " + options).split(" ")); + } + } + """ + ), 18) + ); + } + + @Test + void deprecatedMethod2() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + Process process = runtime.exec("ls -a -l", envp); + } + } + """, + """ + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + Process process = runtime.exec(new String[]{"ls", "-a", "-l"}, envp); + } + } + """ + ), 18) + ); + } + + + @Test + void doNotChangeIfUsingNewMethods() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.File; + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + + Process process1 = runtime.exec(new String[]{"ls", "-al"}); + Process process2 = runtime.exec(new String[]{"ls", "-al"}, envp); + Process process3 = runtime.exec(new String[]{"ls", "-a", "-l"}, envp, dir); + } + } + """ + ), 18) + ); + } + + @Test + void doNotChangeIfUnderJava18() { + rewriteRun( + version( + //language=java + java( + """ + import java.io.File; + import java.io.IOException; + + class A { + void method() throws IOException { + Runtime runtime = Runtime.getRuntime(); + String[] envp = { "E1=1", "E2=2"}; + File dir = new File("/tmp"); + + Process process1 = runtime.exec("ls -al"); + Process process2 = runtime.exec("ls -al", envp); + Process process3 = runtime.exec("ls -al", envp, dir); + } + } + """ + ), 17) + ); + } +} diff --git a/headless-services/commons/pom.xml b/headless-services/commons/pom.xml index 78ab9d1304..284eecb9a7 100644 --- a/headless-services/commons/pom.xml +++ b/headless-services/commons/pom.xml @@ -118,7 +118,7 @@ 1.13 - 2.22.0 + 2.23.1 8.11 2.7.0 6.9.0.202403050737-r diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java index eeb6727056..1cee486080 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java @@ -38,7 +38,7 @@ void listSubRecipe() throws Exception { assertEquals(11, recipes.size()); recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", List.of(0, 2)).get(); - assertEquals(20, recipes.size()); + assertEquals(22, recipes.size()); recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1",List.of(0, 345)).get(); assertEquals(0, recipes.size());