diff --git a/src/main/java/org/openrewrite/staticanalysis/UseTryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/UseTryWithResources.java new file mode 100644 index 000000000..dcd328ab6 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/UseTryWithResources.java @@ -0,0 +1,358 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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 lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.SemanticallyEqual; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; +import org.openrewrite.staticanalysis.java.JavaFileChecker; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; + +public class UseTryWithResources extends Recipe { + + private static final MethodMatcher CLOSE = new MethodMatcher("java.lang.AutoCloseable close()", true); + private static final JavaType.ShallowClass AUTO_CLOSEABLE = JavaType.ShallowClass.build("java.lang.AutoCloseable"); + + @Getter + final String displayName = "Use try-with-resources"; + + @Getter + final String description = "Refactor try/finally blocks to use try-with-resources when the finally block only closes an `AutoCloseable` resource."; + + @Getter + final Duration estimatedEffortPerOccurrence = Duration.ofMinutes(5); + + @Getter + final Set tags = singleton("RSPEC-S2093"); + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and(new UsesJavaVersion<>(9), new JavaFileChecker<>()), + new JavaIsoVisitor() { + @Override + public J.Block visitBlock(J.Block block, ExecutionContext ctx) { + J.Block b = super.visitBlock(block, ctx); + List stmts = b.getStatements(); + return b.withStatements(ListUtils.map(stmts, (i, stmt) -> { + if (stmt instanceof J.Try && i > 0 + && stmts.get(i - 1) instanceof J.VariableDeclarations) { + J.VariableDeclarations prevDecl = (J.VariableDeclarations) stmts.get(i - 1); + J.Try tryStmt = (J.Try) stmt; + if (canTransform(prevDecl, tryStmt)) { + boolean usedAfter = isUsedAfter(prevDecl.getVariables().get(0).getSimpleName(), stmts, i); + if (usedAfter) { + return transformJava9(prevDecl, tryStmt); + } + return transform(prevDecl, tryStmt); + } + } + // Remove varDecl that was merged into the following try-with-resources + if (stmt instanceof J.VariableDeclarations && i + 1 < stmts.size() + && stmts.get(i + 1) instanceof J.Try + && canTransform((J.VariableDeclarations) stmt, (J.Try) stmts.get(i + 1)) + && !isUsedAfter(((J.VariableDeclarations) stmt).getVariables().get(0).getSimpleName(), stmts, i + 1)) { + return null; + } + return stmt; + })); + } + }); + } + + private static boolean canTransform(J.VariableDeclarations varDecl, J.Try tryStmt) { + // Single variable only + if (varDecl.getVariables().size() != 1) { + return false; + } + J.VariableDeclarations.NamedVariable namedVar = varDecl.getVariables().get(0); + String varName = namedVar.getSimpleName(); + + // Must have a non-null initializer + Expression init = namedVar.getInitializer(); + if (init == null || init instanceof J.Literal && ((J.Literal) init).getValue() == null) { + return false; + } + + // Must implement AutoCloseable + JavaType.FullyQualified type = TypeUtils.asFullyQualified(varDecl.getType()); + if (type == null || !TypeUtils.isAssignableTo(AUTO_CLOSEABLE, type)) { + return false; + } + + // Must have a finally block + if (tryStmt.getFinally() == null) { + return false; + } + + // Must not already be a resource in the try + if (tryStmt.getResources() != null) { + J.VariableDeclarations normalized = varDecl.withPrefix(Space.EMPTY); + for (J.Try.Resource res : tryStmt.getResources()) { + if (SemanticallyEqual.areEqual(res.getVariableDeclarations(), normalized)) { + return false; + } + } + } + + // Finally block must contain a close for this variable + if (!finallyContainsClose(tryStmt.getFinally(), varName)) { + return false; + } + + // Variable must not be reassigned in try body + if (isReassigned(varName, tryStmt.getBody())) { + return false; + } + + // Variable must not be closed in catch blocks + for (J.Try.Catch aCatch : tryStmt.getCatches()) { + if (containsClose(varName, aCatch.getBody())) { + return false; + } + } + + return true; + } + + private static J.Try transform(J.VariableDeclarations varDecl, J.Try tryStmt) { + return addResource(varDecl.withPrefix(Space.EMPTY), + varDecl.getVariables().get(0).getSimpleName(), + tryStmt.withPrefix(varDecl.getPrefix())); + } + + private static J.Try transformJava9(J.VariableDeclarations varDecl, J.Try tryStmt) { + J.VariableDeclarations.NamedVariable namedVar = varDecl.getVariables().get(0); + J.Identifier resourceRef = new J.Identifier( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + emptyList(), + namedVar.getSimpleName(), + namedVar.getType(), + null + ); + return addResource(resourceRef, namedVar.getSimpleName(), tryStmt); + } + + private static J.Try addResource(TypedTree resourceExpr, String varName, J.Try tryStmt) { + J.Try.Resource resource = new J.Try.Resource( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + resourceExpr, + false + ); + List> existingResources = tryStmt.getPadding().getResources() != null ? + tryStmt.getPadding().getResources().getPadding().getElements() : emptyList(); + List> newResources; + if (existingResources.isEmpty()) { + newResources = singletonList(JRightPadded.build(resource)); + } else { + newResources = new ArrayList<>(existingResources); + // Set semicolon on the previous last resource + int lastIdx = newResources.size() - 1; + JRightPadded prev = newResources.get(lastIdx); + newResources.set(lastIdx, prev.withElement(prev.getElement().withTerminatedWithSemicolon(true))); + newResources.add(JRightPadded.build(resource.withPrefix(Space.SINGLE_SPACE))); + } + J.Try result = tryStmt.getPadding() + .withResources(JContainer.build( + tryStmt.getPadding().getResources() != null ? + tryStmt.getPadding().getResources().getBefore() : Space.SINGLE_SPACE, + newResources, + Markers.EMPTY + )); + return result.getPadding() + .withFinally(stripCloseFromFinally(result.getPadding().getFinally(), varName)); + } + + private static boolean finallyContainsClose(J.Block finallyBlock, String varName) { + for (Statement stmt : finallyBlock.getStatements()) { + if (isCloseStatement(stmt, varName)) { + return true; + } + } + return false; + } + + /** + * Remove the close statement from the finally block. Returns null if the finally block + * becomes empty (so it can be removed entirely), otherwise returns the pruned finally block. + */ + @SuppressWarnings("DataFlowIssue") + private static JLeftPadded stripCloseFromFinally(@Nullable JLeftPadded finallyPadded, String varName) { + if (finallyPadded == null) { + return null; + } + J.Block finallyBlock = finallyPadded.getElement(); + List remaining = ListUtils.map(finallyBlock.getStatements(), + stmt -> isCloseStatement(stmt, varName) ? null : stmt); + if (remaining.isEmpty()) { + return null; + } + return finallyPadded.withElement(finallyBlock.withStatements(remaining)); + } + + private static boolean isCloseStatement(Statement stmt, String varName) { + // Direct: var.close() + if (isDirectClose(stmt, varName)) { + return true; + } + + // Null-guarded: if (var != null) { ... close ... } + if (stmt instanceof J.If) { + J.If ifStmt = (J.If) stmt; + if (ifStmt.getElsePart() != null) { + return false; + } + if (!isNullCheck(ifStmt.getIfCondition().getTree(), varName)) { + return false; + } + Statement inner; + if (ifStmt.getThenPart() instanceof J.Block) { + J.Block thenBlock = (J.Block) ifStmt.getThenPart(); + if (thenBlock.getStatements().size() != 1) { + return false; + } + inner = thenBlock.getStatements().get(0); + } else { + inner = ifStmt.getThenPart(); + } + return isDirectClose(inner, varName) || isTryCatchClose(inner, varName); + } + + // Try-catch wrapped: try { var.close(); } catch (...) {} + return isTryCatchClose(stmt, varName); + } + + private static boolean isDirectClose(Statement stmt, String varName) { + return stmt instanceof J.MethodInvocation && isCloseInvocation((J.MethodInvocation) stmt, varName); + } + + private static boolean isCloseInvocation(J.MethodInvocation mi, String varName) { + return CLOSE.matches(mi) + && mi.getSelect() instanceof J.Identifier + && ((J.Identifier) mi.getSelect()).getSimpleName().equals(varName); + } + + private static boolean isTryCatchClose(Statement stmt, String varName) { + if (!(stmt instanceof J.Try)) { + return false; + } + J.Try innerTry = (J.Try) stmt; + if (innerTry.getFinally() != null) { + return false; + } + List body = innerTry.getBody().getStatements(); + return body.size() == 1 && isDirectClose(body.get(0), varName); + } + + private static boolean isNullCheck(Expression condition, String varName) { + if (!(condition instanceof J.Binary)) { + return false; + } + J.Binary binary = (J.Binary) condition; + if (binary.getOperator() != J.Binary.Type.NotEqual) { + return false; + } + if (isIdentifier(binary.getLeft(), varName) && J.Literal.isLiteralValue(binary.getRight(), null)) { + return true; + } + return J.Literal.isLiteralValue(binary.getLeft(), null) && isIdentifier(binary.getRight(), varName); + } + + private static boolean isIdentifier(Expression expr, String name) { + return expr instanceof J.Identifier && ((J.Identifier) expr).getSimpleName().equals(name); + } + + private static boolean isUsedAfter(String varName, List stmts, int tryIndex) { + for (int i = tryIndex + 1; i < stmts.size(); i++) { + if (new JavaIsoVisitor() { + @Override + public J.Identifier visitIdentifier(J.Identifier identifier, AtomicBoolean f) { + if (identifier.getSimpleName().equals(varName)) { + f.set(true); + } + return super.visitIdentifier(identifier, f); + } + }.reduce(stmts.get(i), new AtomicBoolean()).get()) { + return true; + } + } + return false; + } + + private static boolean isReassigned(String varName, J tree) { + return new JavaIsoVisitor() { + @Override + public J.Assignment visitAssignment(J.Assignment assignment, AtomicBoolean f) { + if (isIdentifier(assignment.getVariable(), varName)) { + f.set(true); + } + return super.visitAssignment(assignment, f); + } + + @Override + public J.AssignmentOperation visitAssignmentOperation(J.AssignmentOperation assignOp, AtomicBoolean f) { + if (isIdentifier(assignOp.getVariable(), varName)) { + f.set(true); + } + return super.visitAssignmentOperation(assignOp, f); + } + + @Override + public J.Unary visitUnary(J.Unary unary, AtomicBoolean f) { + if (unary.getOperator().isModifying() && isIdentifier(unary.getExpression(), varName)) { + f.set(true); + } + return super.visitUnary(unary, f); + } + }.reduce(tree, new AtomicBoolean()).get(); + } + + private static boolean containsClose(String varName, J tree) { + return new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, AtomicBoolean f) { + if (isCloseInvocation(mi, varName)) { + f.set(true); + } + return super.visitMethodInvocation(mi, f); + } + }.reduce(tree, new AtomicBoolean()).get(); + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 7a17411cc..80e656b09 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -148,6 +148,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.StaticMethodNotFinal,Static methods need not be final,Static methods do not need to be declared final because they cannot be overridden.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.StringLiteralEquality,Use `String.equals()` on `String` literals,"`String.equals()` should be used when checking value equality on String literals. Using `==` or `!=` compares object references, not the actual value of the Strings. This only modifies code where at least one side of the binary operation (`==` or `!=`) is a String literal, such as `""someString"" == someVariable;`. This is to prevent inadvertently changing code where referential equality is the user's intent.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.TernaryOperatorsShouldNotBeNested,Ternary operators should not be nested,"Nested ternary operators can be hard to read quickly. Prefer simpler constructs for improved readability. If supported, this recipe will try to replace nested ternaries with switch expressions.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., +maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UseTryWithResources,Use try-with-resources,Refactor try/finally blocks to use try-with-resources when the finally block only closes an `AutoCloseable` resource.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.TypecastParenPad,Typecast parenthesis padding,"Fixes whitespace padding between a typecast type identifier and the enclosing left and right parentheses. For example, when configured to remove spacing, `( int ) 0L;` becomes `(int) 0L;`.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.URLEqualsHashCodeRecipes,URL Equals and Hash Code,"Uses of `equals()` and `hashCode()` cause `java.net.URL` to make blocking internet connections. Instead, use `java.net.URI`.",3,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.URLEqualsHashCodeRecipes$URLEqualsRecipe,URL Equals,"Uses of `equals()` cause `java.net.URL` to make blocking internet connections. Instead, use `java.net.URI`.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., diff --git a/src/test/java/org/openrewrite/staticanalysis/UseTryWithResourcesTest.java b/src/test/java/org/openrewrite/staticanalysis/UseTryWithResourcesTest.java new file mode 100644 index 000000000..7664feb8b --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/UseTryWithResourcesTest.java @@ -0,0 +1,580 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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; + +@SuppressWarnings({"ConstantConditions", "TryFinallyCanBeTryWithResources", "unused"}) +class UseTryWithResourcesTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseTryWithResources()) + .allSources(s -> s.markers(javaVersion(9))); + } + + @DocumentExample + @Test + void simpleResourceWithDirectClose() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + in.close(); + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void nullGuardedClose() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + if (in != null) { + in.close(); + } + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void nullGuardedCloseWithoutBraces() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + if (in != null) + in.close(); + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void tryCatchWrappedClose() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void nullGuardedWithTryCatchClose() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void preservesCatchBlocks() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + in.close(); + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + """ + ) + ); + } + + @Test + void resourceUsedAfterTryUsesJava9Syntax() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + in.close(); + } + System.out.println(in); + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try (in) { + int data = in.read(); + } + System.out.println(in); + } + } + """ + ) + ); + } + + @Test + void doNotChangeResourceReassignedInTry() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file1.txt"); + try { + in = new FileInputStream("file2.txt"); + int data = in.read(); + } finally { + in.close(); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeNonAutoCloseable() { + rewriteRun( + //language=java + java( + """ + class Test { + static class CustomResource { + public void close() {} + public void doSomething() {} + } + + void method() { + CustomResource resource = new CustomResource(); + try { + resource.doSomething(); + } finally { + resource.close(); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeAlreadyTryWithResources() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeCloseViaHelperMethod() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + static void closeQuietly(InputStream stream) { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException ignored) {} + } + + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + closeQuietly(in); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeResourceClosedInCatch() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } catch (IOException e) { + in.close(); + throw e; + } finally { + in.close(); + } + } + } + """ + ) + ); + } + + @Test + void finallyWithExtraLogicKeepsRemainingStatements() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + int data = in.read(); + } finally { + System.out.println("closing"); + in.close(); + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("file.txt")) { + int data = in.read(); + } finally { + System.out.println("closing"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeQualifiedCloseMethodCall() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + static class Wrapper { + InputStream stream; + Wrapper(InputStream s) { this.stream = s; } + InputStream getStream() { return stream; } + } + + void method() throws IOException { + Wrapper wrapper = new Wrapper(new FileInputStream("file.txt")); + try { + int data = wrapper.getStream().read(); + } finally { + wrapper.getStream().close(); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeResourceAssignedToField() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + private InputStream fieldStream; + + void method() throws IOException { + fieldStream = new FileInputStream("file.txt"); + try { + int data = fieldStream.read(); + } finally { + fieldStream.close(); + } + } + } + """ + ) + ); + } + + @Test + void addResourceToExistingTryWithResources() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() throws IOException { + OutputStream out = new FileOutputStream("out.txt"); + try (InputStream in = new FileInputStream("in.txt")) { + out.write(in.read()); + } finally { + out.close(); + } + } + } + """, + """ + import java.io.*; + + class Test { + void method() throws IOException { + try (InputStream in = new FileInputStream("in.txt"); OutputStream out = new FileOutputStream("out.txt")) { + out.write(in.read()); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeNullInitializedResourceClosedInCatch() { + rewriteRun( + //language=java + java( + """ + import java.io.*; + + class Test { + void method() { + InputStream in = null; + try { + in = new FileInputStream("file.txt"); + int data = in.read(); + } catch (IOException e) { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + throw new RuntimeException(e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + } + """ + ) + ); + } +}