From 9ebf551105a67648edcd1e2b4df6339d0496cb8e Mon Sep 17 00:00:00 2001 From: martinfrancois Date: Sun, 9 Aug 2026 16:54:09 +0200 Subject: [PATCH 1/4] UnnecessaryCloseInTryWithResources: only remove a trailing close of a Closeable The recipe removed an explicit `close()` from any top level position in the try body, which changes what the code does. Statements after the call used to observe a closed resource and now observe an open one, so an intentional early close is silently deferred to the end of the block. `AutoCloseable` explicitly allows `close()` to have visible effects when invoked a second time, so replacing an explicit call with the implicit one can change close counts and the exceptions that are thrown or suppressed. Resources are also closed in reverse order of declaration, so dropping the close of anything but the last declared resource reorders the closes, and removing the call stranded any comment attached to it. The call is now removed only when it sits at the end of the body, it closes the last declared resource, the receiver resolves to that resource, no comment is attached to it or trailing it, and the resource is a `java.io.Closeable`, whose contract is that closing an already closed resource has no effect. Everything else is left alone. A run of repeated trailing closes is collapsed in one pass, so the recipe still finishes in a single cycle. The recipe description was rewritten, because the old wording named `AutoCloseable`, claimed every declared resource qualified and did not mention the trailing position, none of which is true now. `recipes.csv` embeds that description and was regenerated to match. Two things for a reviewer to weigh. The recipe deliberately no longer touches a resource that implements only `AutoCloseable`, because such a close is not required to be idempotent and nothing in the type tells us whether a second call is observable. Narrowing the matcher to `java.io.Closeable` also relies on type attribution, so an unattributed resource is left unchanged rather than guessed at. The existing tests, including the `@DocumentExample`, still pass with their original expectations. --- .../UnnecessaryCloseInTryWithResources.java | 82 +++++--- .../resources/META-INF/rewrite/recipes.csv | 2 +- ...nnecessaryCloseInTryWithResourcesTest.java | 196 ++++++++++++++++++ 3 files changed, 247 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java index 8db5f32e2..ed967ff6a 100644 --- a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java +++ b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java @@ -23,12 +23,15 @@ import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.SemanticallyEqual; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; import org.openrewrite.staticanalysis.groovy.GroovyFileChecker; import org.openrewrite.staticanalysis.java.JavaFileChecker; import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker; import java.time.Duration; +import java.util.List; import java.util.Set; import static java.util.Collections.singleton; @@ -38,10 +41,10 @@ public class UnnecessaryCloseInTryWithResources extends Recipe { final String displayName = "Unnecessary close in try-with-resources"; @Getter - final String description = "Remove unnecessary `AutoCloseable#close()` statements in " + - "try-with-resources. Try-with-resources already guarantees that each " + - "declared resource is closed when the block exits, so an explicit " + - "`close()` call is redundant and can be confusing."; + final String description = "Remove `close()` calls at the end of a try-with-resources block that close the last " + + "declared resource, when that resource is a `java.io.Closeable`, whose `close()` has no effect once the " + + "resource is already closed. A `close()` in any other position, or on any other resource, is left in " + + "place, because removing it would change when, or in what order, resources are closed."; @Getter final Duration estimatedEffortPerOccurrence = Duration.ofMinutes(2); @@ -62,40 +65,55 @@ public TreeVisitor getVisitor() { } private static class UnnecessaryAutoCloseableVisitor extends JavaIsoVisitor { - private static final MethodMatcher AUTO_CLOSEABLE_METHOD_MATCHER = new MethodMatcher("java.lang.AutoCloseable close()", true); + // `AutoCloseable#close()` is explicitly allowed to have visible side effects when called twice, so removing an + // explicit call would change how often the resource is closed. `java.io.Closeable#close()` is the stronger + // contract: "If the stream is already closed then invoking this method has no effect." + private static final MethodMatcher CLOSEABLE_CLOSE_METHOD_MATCHER = new MethodMatcher("java.io.Closeable close()", true); @Override public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { J.Try tr = super.visitTry(aTry, ctx); - if (tr.getResources() != null) { - String[] resourceNames = new String[tr.getResources().size()]; - for (int i = 0; i < tr.getResources().size(); i++) { - J.Try.Resource tryResource = tr.getResources().get(i); - if (tryResource.getVariableDeclarations() instanceof J.VariableDeclarations) { - J.VariableDeclarations varDecls = (J.VariableDeclarations) tryResource.getVariableDeclarations(); - resourceNames[i] = varDecls.getVariables().get(0).getSimpleName(); - } else if (tryResource.getVariableDeclarations() instanceof J.Identifier) { - J.Identifier identifier = (J.Identifier) tryResource.getVariableDeclarations(); - resourceNames[i] = identifier.getSimpleName(); - } - } + if (tr.getResources() == null || tr.getResources().isEmpty()) { + return tr; + } + + // Resources are closed in reverse order of declaration, so only the last declared resource is closed at the + // very position the explicit call occupies; dropping the call for any other resource would reorder closes. + J lastResource = tr.getResources().get(tr.getResources().size() - 1).getVariableDeclarations(); + J.Identifier lastResourceName; + if (lastResource instanceof J.VariableDeclarations) { + lastResourceName = ((J.VariableDeclarations) lastResource).getVariables().get(0).getName(); + } else if (lastResource instanceof J.Identifier) { + lastResourceName = (J.Identifier) lastResource; + } else { + return tr; + } - tr = tr.withBody(tr.getBody().withStatements(ListUtils.map(tr.getBody().getStatements(), statement -> { - if (statement instanceof J.MethodInvocation) { - J.MethodInvocation mi = (J.MethodInvocation) statement; - if (AUTO_CLOSEABLE_METHOD_MATCHER.matches(mi) && mi.getSelect() instanceof J.Identifier) { - String selectName = ((J.Identifier) mi.getSelect()).getSimpleName(); - for (String resourceName : resourceNames) { - if (resourceName.equals(selectName)) { - return null; - } - } - } - } - return statement; - }))); + J.Block body = tr.getBody(); + if (!body.getEnd().getComments().isEmpty()) { + // A comment sitting between the last statement and the closing brace would be stranded. + return tr; } - return tr; + + // Only statements at the very end of the body are redundant; anything following an explicit close can + // observe the resource in its closed state. Every trailing close is dropped in this one pass, so a + // repeated close does not need another cycle. + List statements = body.getStatements(); + int keep = statements.size(); + while (keep > 0 && statements.get(keep - 1) instanceof J.MethodInvocation) { + J.MethodInvocation mi = (J.MethodInvocation) statements.get(keep - 1); + if (!CLOSEABLE_CLOSE_METHOD_MATCHER.matches(mi) || + !(mi.getSelect() instanceof J.Identifier) || + !SemanticallyEqual.areEqual(lastResourceName, mi.getSelect()) || + // A comment attached to the call would be stranded by its removal. + !mi.getComments().isEmpty()) { + break; + } + keep--; + } + + int firstRemoved = keep; + return tr.withBody(body.withStatements(ListUtils.map(statements, (i, statement) -> i < firstRemoved ? statement : null))); } } } diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index bdfbd9e95..33e10a465 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -188,7 +188,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.URLEqualsHashCodeRecipes$URLHashCodeRecipe,URL Hash Code,"Uses of `hashCode()` 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.,, 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.UnnecessaryCatch,Remove catch for a checked exception if the try block does not throw that exception,A refactoring operation may result in a checked exception that is no longer thrown from a `try` block. This recipe will find and remove unnecessary catch blocks.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,"[{""name"":""includeJavaLangException"",""type"":""boolean"",""displayName"":""Include `java.lang.Exception`"",""description"":""Whether to include `java.lang.Exception` in the list of checked exceptions to remove. Unlike other checked exceptions, `java.lang.Exception` is also the superclass of unchecked exceptions. So removing `catch(Exception e)` may result in changed runtime behavior in the presence of unchecked exceptions. Default `false`"",""value"":false},{""name"":""includeJavaLangThrowable"",""type"":""boolean"",""displayName"":""Include `java.lang.Throwable`"",""description"":""Whether to include `java.lang.Throwable` in the list of exceptions to remove. Unlike other checked exceptions, `java.lang.Throwable` is also the superclass of unchecked exceptions. So removing `catch(Throwable e)` may result in changed runtime behavior in the presence of unchecked exceptions. Default `false`"",""value"":false}]", -maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UnnecessaryCloseInTryWithResources,Unnecessary close in try-with-resources,"Remove unnecessary `AutoCloseable#close()` statements in try-with-resources. Try-with-resources already guarantees that each declared resource is closed when the block exits, so an explicit `close()` call is redundant and can be confusing.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,, +maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UnnecessaryCloseInTryWithResources,Unnecessary close in try-with-resources,"Remove `close()` calls at the end of a try-with-resources block that close the last declared resource, when that resource is a `java.io.Closeable`, whose `close()` has no effect once the resource is already closed. A `close()` in any other position, or on any other resource, is left in place, because removing it would change when, or in what order, resources are closed.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,, maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UnnecessaryExplicitTypeArguments,Unnecessary explicit type arguments,"When explicit type arguments are inferable by the compiler, they may be removed.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,, maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UnnecessaryParentheses,Remove unnecessary parentheses,"Removes unnecessary parentheses from code where extra parentheses pairs are redundant. Redundant parentheses add visual noise and can obscure the actual structure of an expression, making code harder to read at a glance.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,, maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.UnnecessaryPrimitiveAnnotations,Remove `@Nullable` and `@CheckForNull` annotations from primitives,"Primitives can't be null anyway, so these annotations are not useful in this context. Leaving them in place gives the false impression that a null value is possible, which can confuse readers and static analysis tools alike.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,, diff --git a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java index c85b31572..668b3bd7c 100644 --- a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java @@ -84,6 +84,71 @@ public void doSomething() { ); } + @Test + void removeCloseOfReferencedResource() { + rewriteRun( + //language=java + java( + """ + import java.util.Scanner; + + class A { + public void doSomething(Scanner scanner) { + try (scanner) { + boolean hasNext = scanner.hasNext(); + scanner.close(); + } + } + } + """, + """ + import java.util.Scanner; + + class A { + public void doSomething(Scanner scanner) { + try (scanner) { + boolean hasNext = scanner.hasNext(); + } + } + } + """ + ) + ); + } + + @Test + void removeRepeatedTrailingClose() { + rewriteRun( + //language=java + java( + """ + import java.util.Scanner; + + class A { + public void doSomething() { + try (Scanner scanner = new Scanner("abc")) { + boolean hasNext = scanner.hasNext(); + scanner.close(); + scanner.close(); + } + } + } + """, + """ + import java.util.Scanner; + + class A { + public void doSomething() { + try (Scanner scanner = new Scanner("abc")) { + boolean hasNext = scanner.hasNext(); + } + } + } + """ + ) + ); + } + @Test void onlyRemoveAutoCloseableClose() { rewriteRun( @@ -119,4 +184,135 @@ public void doSomething() { ) ); } + + @Test + void doNotRemoveCloseWhenResourceStateIsObservedAfterwards() { + rewriteRun( + //language=java + java( + """ + class Test { + static class Resource implements AutoCloseable { + boolean closed; + + @Override + public void close() { + closed = true; + } + } + + void declaration() { + try (Resource resource = new Resource()) { + resource.close(); + if (!resource.closed) { + throw new AssertionError(); + } + } + } + + void reference(Resource resource) { + try (resource) { + resource.close(); + if (!resource.closed) { + throw new AssertionError(); + } + } + } + } + """ + ) + ); + } + + @Test + void doNotRemoveCloseFollowedByOtherStatements() { + rewriteRun( + //language=java + java( + """ + import java.util.Scanner; + + class A { + public void doSomething() { + try (Scanner scanner = new Scanner("abc")) { + scanner.close(); + System.out.println("scanner released early"); + } + } + } + """ + ) + ); + } + + @Test + void doNotRemoveCloseOfResourceThatIsNotClosedFirst() { + rewriteRun( + //language=java + java( + """ + import java.io.FileWriter; + import java.util.Scanner; + + class A { + public void doSomething() { + try (FileWriter fileWriter = new FileWriter("test"); Scanner scanner = new Scanner("abc")) { + fileWriter.write(scanner.next()); + fileWriter.close(); + } + } + } + """ + ) + ); + } + + @Test + void doNotRemoveCloseOfResourceThatIsNotCloseable() { + rewriteRun( + //language=java + java( + """ + class A { + static class Latch implements AutoCloseable { + int closeCount; + + @Override + public void close() { + closeCount++; + } + } + + public void doSomething() { + try (Latch latch = new Latch()) { + latch.close(); + } + } + } + """ + ) + ); + } + + @Test + void doNotRemoveCloseWithComment() { + rewriteRun( + //language=java + java( + """ + import java.util.Scanner; + + class A { + public void doSomething() { + try (Scanner scanner = new Scanner("abc")) { + boolean hasNext = scanner.hasNext(); + // release the scanner before the block exits + scanner.close(); + } + } + } + """ + ) + ); + } } From 8a485ed501ce82c70d57d8b552ac9b562fc993cc Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 11 Aug 2026 09:16:13 +0200 Subject: [PATCH 2/4] Review fixes: guard comments anywhere in the removed call, and cover the positional rule with a Closeable --- .../UnnecessaryCloseInTryWithResources.java | 31 ++++++++++++------- ...nnecessaryCloseInTryWithResourcesTest.java | 25 ++++++++++++++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java index ed967ff6a..e0e74ae8f 100644 --- a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java +++ b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java @@ -25,6 +25,7 @@ import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.search.SemanticallyEqual; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Space; import org.openrewrite.java.tree.Statement; import org.openrewrite.staticanalysis.groovy.GroovyFileChecker; import org.openrewrite.staticanalysis.java.JavaFileChecker; @@ -33,6 +34,7 @@ import java.time.Duration; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Collections.singleton; @@ -65,9 +67,7 @@ public TreeVisitor getVisitor() { } private static class UnnecessaryAutoCloseableVisitor extends JavaIsoVisitor { - // `AutoCloseable#close()` is explicitly allowed to have visible side effects when called twice, so removing an - // explicit call would change how often the resource is closed. `java.io.Closeable#close()` is the stronger - // contract: "If the stream is already closed then invoking this method has no effect." + // `AutoCloseable#close()` may have visible side effects when called twice; `java.io.Closeable#close()` may not. private static final MethodMatcher CLOSEABLE_CLOSE_METHOD_MATCHER = new MethodMatcher("java.io.Closeable close()", true); @Override @@ -77,8 +77,8 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { return tr; } - // Resources are closed in reverse order of declaration, so only the last declared resource is closed at the - // very position the explicit call occupies; dropping the call for any other resource would reorder closes. + // Resources are closed in reverse order of declaration, so dropping a close of any but the last declared + // resource would reorder closes. J lastResource = tr.getResources().get(tr.getResources().size() - 1).getVariableDeclarations(); J.Identifier lastResourceName; if (lastResource instanceof J.VariableDeclarations) { @@ -91,13 +91,11 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { J.Block body = tr.getBody(); if (!body.getEnd().getComments().isEmpty()) { - // A comment sitting between the last statement and the closing brace would be stranded. return tr; } - // Only statements at the very end of the body are redundant; anything following an explicit close can - // observe the resource in its closed state. Every trailing close is dropped in this one pass, so a - // repeated close does not need another cycle. + // Anything following an explicit close can observe the resource in its closed state, so only trailing + // closes are redundant. List statements = body.getStatements(); int keep = statements.size(); while (keep > 0 && statements.get(keep - 1) instanceof J.MethodInvocation) { @@ -105,8 +103,7 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { if (!CLOSEABLE_CLOSE_METHOD_MATCHER.matches(mi) || !(mi.getSelect() instanceof J.Identifier) || !SemanticallyEqual.areEqual(lastResourceName, mi.getSelect()) || - // A comment attached to the call would be stranded by its removal. - !mi.getComments().isEmpty()) { + containsComment(mi)) { break; } keep--; @@ -115,5 +112,17 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { int firstRemoved = keep; return tr.withBody(body.withStatements(ListUtils.map(statements, (i, statement) -> i < firstRemoved ? statement : null))); } + + private static boolean containsComment(J tree) { + return new JavaIsoVisitor() { + @Override + public Space visitSpace(Space space, Space.Location loc, AtomicBoolean found) { + if (!space.getComments().isEmpty()) { + found.set(true); + } + return space; + } + }.reduce(tree, new AtomicBoolean()).get(); + } } } diff --git a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java index 668b3bd7c..215421195 100644 --- a/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResourcesTest.java @@ -191,8 +191,10 @@ void doNotRemoveCloseWhenResourceStateIsObservedAfterwards() { //language=java java( """ + import java.io.Closeable; + class Test { - static class Resource implements AutoCloseable { + static class Resource implements Closeable { boolean closed; @Override @@ -315,4 +317,25 @@ public void doSomething() { ) ); } + + @Test + void doNotRemoveCloseWithCommentInsideTheCall() { + rewriteRun( + //language=java + java( + """ + import java.util.Scanner; + + class A { + public void doSomething() { + try (Scanner scanner = new Scanner("abc")) { + boolean hasNext = scanner.hasNext(); + scanner./* release early */close(/* no arguments */); + } + } + } + """ + ) + ); + } } From 3c69e67b530722b120cd5d6840b466ea4dbe8bd0 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 12 Aug 2026 00:22:40 +0200 Subject: [PATCH 3/4] Trim commentary --- .../UnnecessaryCloseInTryWithResources.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java index e0e74ae8f..9dfd91363 100644 --- a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java +++ b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java @@ -67,7 +67,7 @@ public TreeVisitor getVisitor() { } private static class UnnecessaryAutoCloseableVisitor extends JavaIsoVisitor { - // `AutoCloseable#close()` may have visible side effects when called twice; `java.io.Closeable#close()` may not. + // `AutoCloseable#close()` may have visible side effects when called twice; `Closeable#close()` may not private static final MethodMatcher CLOSEABLE_CLOSE_METHOD_MATCHER = new MethodMatcher("java.io.Closeable close()", true); @Override @@ -77,8 +77,7 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { return tr; } - // Resources are closed in reverse order of declaration, so dropping a close of any but the last declared - // resource would reorder closes. + // Resources close in reverse order of declaration, so dropping any but the last would reorder closes J lastResource = tr.getResources().get(tr.getResources().size() - 1).getVariableDeclarations(); J.Identifier lastResourceName; if (lastResource instanceof J.VariableDeclarations) { @@ -94,8 +93,7 @@ public J.Try visitTry(J.Try aTry, ExecutionContext ctx) { return tr; } - // Anything following an explicit close can observe the resource in its closed state, so only trailing - // closes are redundant. + // Anything after an explicit close can observe the closed resource, so only trailing closes are redundant List statements = body.getStatements(); int keep = statements.size(); while (keep > 0 && statements.get(keep - 1) instanceof J.MethodInvocation) { From 9845c404a58aeac154e71f8b217a34a2036cefc3 Mon Sep 17 00:00:00 2001 From: martinfrancois Date: Sun, 16 Aug 2026 22:17:15 +0200 Subject: [PATCH 4/4] Clarify the repeated close contract --- .../staticanalysis/UnnecessaryCloseInTryWithResources.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java index 9dfd91363..ef99765c8 100644 --- a/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java +++ b/src/main/java/org/openrewrite/staticanalysis/UnnecessaryCloseInTryWithResources.java @@ -67,7 +67,7 @@ public TreeVisitor getVisitor() { } private static class UnnecessaryAutoCloseableVisitor extends JavaIsoVisitor { - // `AutoCloseable#close()` may have visible side effects when called twice; `Closeable#close()` may not + // Closeable requires close() to be idempotent; AutoCloseable does not make that guarantee. private static final MethodMatcher CLOSEABLE_CLOSE_METHOD_MATCHER = new MethodMatcher("java.io.Closeable close()", true); @Override