Skip to content

Try with resources #599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft

Conversation

iddeepak
Copy link
Contributor

@iddeepak iddeepak commented Jun 20, 2025

Why
Handling resources in finally blocks is verbose and prone to errors. Adopting try-with-resources guarantees that all AutoCloseable resources are closed automatically, resulting in cleaner, safer, and more maintainable code.

Acknowledgments
Huge thanks to @knutwannheden for crafting the original draft of the recipe. #593
Thank you, @Pankraz76 for raising the original issue that sparked this recipe. #591

/cc @knutwannheden for the original groundwork and inspiration!

@github-project-automation github-project-automation bot moved this to In Progress in OpenRewrite Jun 20, 2025
Copy link
Contributor

@Pankraz76 Pankraz76 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well done. thank you for dedication.

could give more separation on impl. but this random detail. Its working fine, due to good testing. @knutwannheden thanks for the starter.

return false;
}

private J.Try transformToTryWithResources(J.Try tryable, Map<String, J.VariableDeclarations> resourcesThatAreClosed, Map<String, Expression> resourceInitializers) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not even fit my screen. SOC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

// Create resources for the try-with-resources statement
List<J.Try.Resource> resources = new ArrayList<>();

List<Map.Entry<String, J.VariableDeclarations>> entries = new ArrayList<>(resourcesThatAreClosed.entrySet());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be core logic making some iteration / transformation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly

Comment on lines 424 to 432
// Remove assignments to resources in the try block
List<Statement> newBodyStatements = new ArrayList<>();
for (Statement statement : tryWithResources.getBody().getStatements()) {
if (!(statement instanceof J.Assignment) ||
!isAssignmentToResource(statement, resourcesThatAreClosed.keySet())) {
newBodyStatements.add(statement);
}
}
tryWithResources = tryWithResources.withBody(tryWithResources.getBody().withStatements(newBodyStatements));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Remove assignments to resources in the try block
List<Statement> newBodyStatements = new ArrayList<>();
for (Statement statement : tryWithResources.getBody().getStatements()) {
if (!(statement instanceof J.Assignment) ||
!isAssignmentToResource(statement, resourcesThatAreClosed.keySet())) {
newBodyStatements.add(statement);
}
}
tryWithResources = tryWithResources.withBody(tryWithResources.getBody().withStatements(newBodyStatements));
RemoveAssignments();

SOC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@iddeepak
Copy link
Contributor Author

Agreed could be refined more. Also, what if we could move some of the functionality to some other class like ResourceUtils if could be used by other recipe ?
@Pankraz76

"""
)
);
}
Copy link
Contributor

@knutwannheden knutwannheden Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few more test cases to try to "break" the recipe:

    @Test
    void resourceClosedInCatchBlock() {
        rewriteRun(
          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) {
                              }
                          }
                      }
                  }
              }
              """
          )
        );
    }

    @Test
    void qualifiedCloseMethodCall() {
        rewriteRun(
          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 nonAutoCloseableResource() {
        rewriteRun(
          java(
            """
              import java.io.*;
              
              class Test {
                  static class CustomResource {
                      public void close() {
                          // Custom close logic
                      }
                      public void doSomething() {}
                  }
              
                  void method() {
                      CustomResource resource = new CustomResource();
                      try {
                          resource.doSomething();
                      } finally {
                          resource.close();
                      }
                  }
              }
              """
          )
        );
    }

    @Test
    void resourceAssignedToField() {
        rewriteRun(
          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 resourceWithComplexFinallyLogic() {
        rewriteRun(
          java(
            """
              import java.io.*;
              
              class Test {
                  void method() throws IOException {
                      InputStream in = new FileInputStream("file.txt");
                      boolean success = false;
                      try {
                          int data = in.read();
                          success = true;
                      } finally {
                          if (success) {
                              System.out.println("Success!");
                          } else {
                              System.out.println("Failed!");
                          }
                          if (in != null) {
                              in.close();
                          }
                          System.out.println("Cleanup done");
                      }
                  }
              }
              """,
            """
              import java.io.*;
              
              class Test {
                  void method() throws IOException {
                      boolean success = false;
                      try (InputStream in = new FileInputStream("file.txt")) {
                          int data = in.read();
                          success = true;
                      } finally {
                          if (success) {
                              System.out.println("Success!");
                          } else {
                              System.out.println("Failed!");
                          }
                          System.out.println("Cleanup done");
                      }
                  }
              }
              """
          )
        );
    }

    @Test
    void resourceUsedAfterTryBlock() {
        rewriteRun(
          java(
            """
              import java.io.*;
              
              class Test {
                  void method() throws IOException {
                      InputStream in = new FileInputStream("file.txt");
                      try {
                          int data = in.read();
                      } finally {
                          in.close();
                      }
                      // Resource is referenced after try - can still use try(in) syntax
                      System.out.println("Stream was: " + in);
                  }
              }
              """,
            """
              import java.io.FileInputStream;
              import java.io.IOException;
              import java.io.InputStream;
              
              class Test {
                  void method() throws IOException {
                      InputStream in = new FileInputStream("file.txt");
                      try (in) {
                          int data = in.read();
                      }
                      // Resource is referenced after try - can still use try(in) syntax
                      System.out.println("Stream was: " + in);
                  }
              }
              """
          )
        );
    }

    @Test
    void resourceReassignedInTryBlock() {
        rewriteRun(
          java(
            """
              import java.io.*;
              
              class Test {
                  void method() throws IOException {
                      InputStream in = new FileInputStream("file1.txt");
                      try {
                          if (Math.random() > 0.5) {
                              in = new FileInputStream("file2.txt");
                          }
                          int data = in.read();
                      } finally {
                          in.close();
                      }
                  }
              }
              """
          )
        );
    }

    @Test
    void resourceWithStaticCloseCall() {
        rewriteRun(
          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);
                      }
                  }
              }
              """
          )
        );
    }

Copy link
Contributor

@knutwannheden knutwannheden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case you find it useful (although I must warn you that this is mostly AI generated), this should fix some more tests:

Subject: [PATCH] `TryWithResources` recipe

Issues:
 - #591
---
Index: src/main/java/org/openrewrite/staticanalysis/TryWithResources.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main/java/org/openrewrite/staticanalysis/TryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/TryWithResources.java
--- a/src/main/java/org/openrewrite/staticanalysis/TryWithResources.java	(revision f17b2245b77e4388629ed6fc8f0187409043a25e)
+++ b/src/main/java/org/openrewrite/staticanalysis/TryWithResources.java	(date 1750453794223)
@@ -75,14 +75,16 @@
                 }
 
                 // Find resources that are closed in the finally block
-                Map<String, J.VariableDeclarations> resourcesThatAreClosed = findResourcesThatAreClosedInFinally(variableDeclarations, t.getFinally());
+                Map<String, J.VariableDeclarations> resourcesThatAreClosed = findResourcesThatAreClosedInFinallyWithSafetyChecks(variableDeclarations, t.getFinally(), t);
 
                 if (resourcesThatAreClosed.isEmpty()) {
                     return t;
                 }
 
-                // Transform the try block to use try-with-resources
-                return transformToTryWithResources(t, resourcesThatAreClosed, findResourceInitializers(t, resourcesThatAreClosed.keySet()));
+                // For visitTry we don't have access to the containing block, so we can't detect
+                // if resources are used after the try block. This is handled by processBlock instead.
+                // Return the original try block unchanged.
+                return t;
             }
 
             private List<J.VariableDeclarations> collectVariableDeclarations(J.Try t) {
@@ -119,18 +121,33 @@
                     }
 
                     // Find resources that are closed in the finally block
-                    Map<String, J.VariableDeclarations> resourcesThatAreClosed = findResourcesThatAreClosedInFinally(variableDeclarations, tryBlock.getFinally());
+                    Map<String, J.VariableDeclarations> resourcesThatAreClosed = findResourcesThatAreClosedInFinallyWithSafetyChecks(variableDeclarations, tryBlock.getFinally(), tryBlock);
 
                     if (resourcesThatAreClosed.isEmpty()) {
                         continue;
                     }
 
+                    // Determine which resources are used after the try block
+                    Set<String> resourcesUsedAfterTry = new HashSet<>();
+                    for (String varName : resourcesThatAreClosed.keySet()) {
+                        if (isVariableUsedAfterStatement(varName, newBody, tryBlock)) {
+                            resourcesUsedAfterTry.add(varName);
+                        }
+                    }
+
                     // Transform the try block to use try-with-resources
                     J.Try newTryBlock = transformToTryWithResources(tryBlock, resourcesThatAreClosed,
-                            findResourceInitializers(tryBlock, resourcesThatAreClosed.keySet()));
+                            findResourceInitializers(tryBlock, resourcesThatAreClosed.keySet()), resourcesUsedAfterTry, newBody);
 
                     // Replace the old try block with the new one and remove the variable declarations
-                    newBody = replaceTryBlockAndRemoveDeclarations(newBody, tryBlock, newTryBlock, resourcesThatAreClosed.values());
+                    // Don't remove declarations for resources that use Java 9+ syntax
+                    Collection<J.VariableDeclarations> declarationsToRemove = new ArrayList<>();
+                    for (Map.Entry<String, J.VariableDeclarations> entry : resourcesThatAreClosed.entrySet()) {
+                        if (!resourcesUsedAfterTry.contains(entry.getKey())) {
+                            declarationsToRemove.add(entry.getValue());
+                        }
+                    }
+                    newBody = replaceTryBlockAndRemoveDeclarations(newBody, tryBlock, newTryBlock, declarationsToRemove);
                 }
 
                 return newBody;
@@ -211,6 +228,43 @@
                         }
                     }
                 }
+
+                return resourcesThatAreClosed;
+            }
+
+            private Map<String, J.VariableDeclarations> findResourcesThatAreClosedInFinallyWithSafetyChecks(List<J.VariableDeclarations> variableDeclarations, J.Block finallyBlock, J.Try tryBlock) {
+                Map<String, J.VariableDeclarations> resourcesThatAreClosed = new HashMap<>();
+
+                // Find variable declarations that implement AutoCloseable
+                for (J.VariableDeclarations varDecl : variableDeclarations) {
+                    // Check if the variable type implements AutoCloseable
+                    JavaType.FullyQualified type = TypeUtils.asFullyQualified(varDecl.getType());
+                    if (type != null && TypeUtils.isAssignableTo(AUTO_CLOSEABLE_TYPE, type)) {
+                        for (J.VariableDeclarations.NamedVariable namedVar : varDecl.getVariables()) {
+                            String varName = namedVar.getSimpleName();
+
+                            // Check if this variable is closed in the finally block
+                            if (isClosedInFinally(varName, finallyBlock)) {
+                                // Additional safety checks
+                                boolean isSafeToTransform = true;
+
+                                // Check if resource is closed in any catch blocks (unsafe)
+                                if (!tryBlock.getCatches().isEmpty() && isResourceClosedInCatchBlocks(varName, tryBlock)) {
+                                    isSafeToTransform = false;
+                                }
+
+                                // Check if resource is reassigned in try block (unsafe)
+                                if (isSafeToTransform && isResourceReassignedInTry(varName, tryBlock, namedVar)) {
+                                    isSafeToTransform = false;
+                                }
+
+                                if (isSafeToTransform) {
+                                    resourcesThatAreClosed.put(varName, varDecl);
+                                }
+                            }
+                        }
+                    }
+                }
 
                 return resourcesThatAreClosed;
             }
@@ -224,6 +278,70 @@
                 return false;
             }
 
+            private boolean isResourceClosedInCatchBlocks(String varName, J.Try tryBlock) {
+                for (J.Try.Catch catchBlock : tryBlock.getCatches()) {
+                    for (Statement statement : catchBlock.getBody().getStatements()) {
+                        if (isCloseStatement(statement, varName)) {
+                            return true;
+                        }
+                    }
+                }
+                return false;
+            }
+
+            private boolean isResourceReassignedInTry(String varName, J.Try tryBlock, J.VariableDeclarations.NamedVariable namedVar) {
+                // If variable is initialized to null, assignment in try block is safe
+                if (namedVar.getInitializer() instanceof J.Literal) {
+                    J.Literal literal = (J.Literal) namedVar.getInitializer();
+                    if (literal.getValue() == null) {
+                        return false; // null -> value assignment is safe
+                    }
+                }
+
+                // If variable has non-null initializer, any assignment in try block is unsafe
+                AtomicBoolean foundAssignment = new AtomicBoolean(false);
+
+                new JavaIsoVisitor<ExecutionContext>() {
+                    @Override
+                    public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) {
+                        if (assignment.getVariable() instanceof J.Identifier) {
+                            J.Identifier identifier = (J.Identifier) assignment.getVariable();
+                            if (identifier.getSimpleName().equals(varName)) {
+                                foundAssignment.set(true);
+                            }
+                        }
+                        return super.visitAssignment(assignment, ctx);
+                    }
+                }.visit(tryBlock.getBody(), null);
+                return foundAssignment.get();
+            }
+
+            private boolean isVariableUsedAfterStatement(String varName, J.Block containingBlock, Statement statement) {
+                List<Statement> statements = containingBlock.getStatements();
+                int statementIndex = statements.indexOf(statement);
+                if (statementIndex == -1 || statementIndex == statements.size() - 1) {
+                    return false;
+                }
+
+                AtomicBoolean found = new AtomicBoolean(false);
+                for (int i = statementIndex + 1; i < statements.size(); i++) {
+                    new JavaIsoVisitor<ExecutionContext>() {
+                        @Override
+                        public J.Identifier visitIdentifier(J.Identifier identifier, ExecutionContext ctx) {
+                            if (identifier.getSimpleName().equals(varName)) {
+                                found.set(true);
+                            }
+                            return super.visitIdentifier(identifier, ctx);
+                        }
+                    }.visit(statements.get(i), null);
+
+                    if (found.get()) {
+                        break;
+                    }
+                }
+                return found.get();
+            }
+
             private J.Block replaceTryBlockAndRemoveDeclarations(J.Block block, J.Try oldTry, J.Try newTry, Collection<J.VariableDeclarations> declarations) {
                 Set<Statement> declarationsToRemove = new HashSet<>(declarations);
                 return block.withStatements(ListUtils.map(block.getStatements(), statement -> {
@@ -349,7 +467,9 @@
             private J.Try transformToTryWithResources(
                     J.Try tryable, Map<String,
                     J.VariableDeclarations> resourcesThatAreClosed,
-                    Map<String, Expression> resourceInitializers) {
+                    Map<String, Expression> resourceInitializers,
+                    Set<String> resourcesUsedAfterTry,
+                    J.Block containingBlock) {
                 // Create resources for the try-with-resources statement
                 List<J.Try.Resource> resources = new ArrayList<>();
 
@@ -359,11 +479,19 @@
                     String varName = entry.getKey();
                     J.VariableDeclarations varDecl = entry.getValue();
 
-                    // Find the named variable
-                    for (J.VariableDeclarations.NamedVariable namedVar : varDecl.getVariables()) {
-                        if (namedVar.getSimpleName().equals(varName)) {
-                            resources.add(createResources(tryable, resourceInitializers, namedVar, varDecl, varName, i, entries));
-                            break;
+                    // Check if we should use Java 9+ syntax for this resource
+                    boolean useJava9Syntax = resourcesUsedAfterTry.contains(varName);
+
+                    if (useJava9Syntax) {
+                        // Create a simple identifier reference for Java 9+ syntax
+                        resources.add(createJava9Resource(varName, i, entries));
+                    } else {
+                        // Find the named variable for traditional syntax
+                        for (J.VariableDeclarations.NamedVariable namedVar : varDecl.getVariables()) {
+                            if (namedVar.getSimpleName().equals(varName)) {
+                                resources.add(createResources(tryable, resourceInitializers, namedVar, varDecl, varName, i, entries));
+                                break;
+                            }
                         }
                     }
                 }
@@ -384,6 +512,36 @@
                 return removeAssignments(resourcesThatAreClosed, tryWithResources);
             }
 
+            private J.Try.Resource createJava9Resource(String varName, int i, List<Map.Entry<String, J.VariableDeclarations>> entries) {
+                // Create a resource with just an identifier for Java 9+ syntax
+                Space prefix;
+                if (i == 0) {
+                    prefix = Space.EMPTY;
+                } else {
+                    prefix = Space.SINGLE_SPACE;
+                }
+
+                // Create an identifier for the variable
+                J.Identifier identifier = new J.Identifier(
+                        Tree.randomId(),
+                        Space.EMPTY,
+                        Markers.EMPTY,
+                        Collections.emptyList(),
+                        varName,
+                        null,
+                        null
+                );
+
+                // Create the resource
+                return new J.Try.Resource(
+                        Tree.randomId(),
+                        prefix,
+                        Markers.EMPTY,
+                        identifier,
+                        i < entries.size() - 1
+                );
+            }
+
             private J.Try.Resource createResources(J.Try tryable, Map<String, Expression> resourceInitializers, J.VariableDeclarations.NamedVariable namedVar, J.VariableDeclarations varDecl, String varName, int i, List<Map.Entry<String, J.VariableDeclarations>> entries) {
                 // Create a new variable declaration with just this variable
                 J.VariableDeclarations singleVarDecl = varDecl;
@@ -415,8 +573,7 @@
                 if (i == 0) {
                     prefix = Space.EMPTY;
                 } else {
-                    // For multiple resources, format with newline and indentation for better readability
-                    prefix = entries.size() > 1 ? Space.format("\n             ") : Space.format(" ");
+                    prefix = Space.SINGLE_SPACE;
                 }
 
                 // Create the resource - only the last one should not have a semicolon

knutwannheden added a commit to openrewrite/rewrite that referenced this pull request Jun 24, 2025
* Java: Fix formatting of try-with-resources

Issues:
 - openrewrite/rewrite-static-analysis#599

* Update rewrite-java/src/main/java/org/openrewrite/java/format/ColumnPositionCalculator.java

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Tim te Beek <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@timtebeek timtebeek moved this from In Progress to Ready to Review in OpenRewrite Jun 24, 2025
Copy link
Member

@timtebeek timtebeek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see already; I've applied some small fixes as well as Knut's autoformat suggestion. Would you be ok to apply those further improvements before a next round of review?

I've also not yet tried Knut's other suggested tests or fixes; those could be helpful still as well.

Comment on lines 417 to 425
// Remove assignments to resources in the try block
List<Statement> newBodyStatements = new ArrayList<>();
for (Statement statement : tryWithResources.getBody().getStatements()) {
if (!(statement instanceof J.Assignment) ||
!isAssignmentToResource(statement, resourcesThatAreClosed.keySet())) {
newBodyStatements.add(statement);
}
}
return tryWithResources.withBody(tryWithResources.getBody().withStatements(newBodyStatements));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this pattern repeated quite often: you create a new list, then loop over a collection, and selectively add/modify elements to place into the new list. Would you mind converting those over to use ListUtils as seen in 59b6970 ?

ListUtils is a way to make that type of operation not create a new list unless there is a underlying change to the collection. That has benefits both for performance, and for immutability in our LST model. I've pointed this out here, but it also applies above and below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@github-project-automation github-project-automation bot moved this from Ready to Review to In Progress in OpenRewrite Jun 24, 2025
…java

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…java

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Copy link
Contributor

@Pankraz76 Pankraz76 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx

@timtebeek timtebeek self-requested a review July 5, 2025 11:41
@timtebeek timtebeek moved this from In Progress to Ready to Review in OpenRewrite Jul 5, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions could not be made:

  • src/main/java/org/openrewrite/staticanalysis/ExplicitLambdaArgumentTypes.java
    • lines 167-168

@timtebeek timtebeek moved this from Ready to Review to In Progress in OpenRewrite Jul 5, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions could not be made:

  • src/main/java/org/openrewrite/staticanalysis/ExplicitLambdaArgumentTypes.java
    • lines 167-168

@timtebeek timtebeek marked this pull request as draft July 5, 2025 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

4 participants