From d8f653a4c1f4562cf2c8612087300254e254817a Mon Sep 17 00:00:00 2001 From: Alex Meseldzija Date: Wed, 24 Jun 2026 11:32:01 +0200 Subject: [PATCH 1/3] Fix S2093 FN: JDK 26 autoclosables --- .../checks/TryWithResourcesCheck_java_26.java | 111 ++++++++++++++++++ .../java/checks/TryWithResourcesCheck.java | 18 ++- .../checks/TryWithResourcesCheckTest.java | 13 ++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java diff --git a/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java b/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java new file mode 100644 index 00000000000..b0a67d69ee7 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java @@ -0,0 +1,111 @@ +package checks; + +import java.io.IOException; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLXML; + +class TryWithResourcesCheck_java_26 { + + void processFromProcessBuilderIsCloseableAsOfJava26() throws IOException { + Process p = null; + try { // Noncompliant {{Change this "try" to a try-with-resources.}} + p = new ProcessBuilder("ls").start(); + p.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + if (p != null) { + p.close(); + } + } + } + + void processFromRuntimeExec() throws IOException { + Process p = null; + try { // Noncompliant + p = Runtime.getRuntime().exec(new String[] {"ls"}); + p.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + p.close(); + } + } + + void blobFromConnectionFactory(Connection connection) throws SQLException { + Blob blob = null; + try { // Noncompliant + blob = connection.createBlob(); + blob.setBytes(1L, new byte[] {0}); + } finally { + if (blob != null) { + blob.close(); + } + } + } + + void clobFromResultSetGetter(ResultSet rs) throws SQLException { + Clob clob = null; + try { // Noncompliant + clob = rs.getClob("payload"); + clob.length(); + } finally { + if (clob != null) { + clob.close(); + } + } + } + + void arrayFromResultSetGetter(ResultSet rs) throws SQLException { + Array array = null; + try { // Noncompliant + array = rs.getArray(1); + array.getBaseType(); + } finally { + if (array != null) { + array.close(); + } + } + } + + void sqlxmlFromConnectionFactory(Connection connection) throws SQLException { + SQLXML xml = null; + try { // Noncompliant + xml = connection.createSQLXML(); + xml.setString(""); + } finally { + if (xml != null) { + xml.close(); + } + } + } + + void blobFromCallableStatement(CallableStatement cs) throws SQLException { + Blob blob = null; + try { // Noncompliant + blob = cs.getBlob(1); + blob.length(); + } finally { + if (blob != null) { + blob.close(); + } + } + } + + void compliantTryWithResources(Connection connection) throws IOException, SQLException { + try (Process p = new ProcessBuilder("ls").start(); + Blob blob = connection.createBlob()) { + p.waitFor(); + blob.setBytes(1L, new byte[] {0}); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java index 4bffec2304f..e27cb57129d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java @@ -47,6 +47,21 @@ public class TryWithResourcesCheck extends IssuableSubscriptionVisitor implement .names("of") .addParametersMatcher("java.lang.CharSequence").build(); + private static final MethodMatchers AUTOCLOSEABLE_JAVA26_MATCHER = MethodMatchers.or( + MethodMatchers.create() + .ofTypes("java.lang.ProcessBuilder").names("start").addWithoutParametersMatcher().build(), + MethodMatchers.create() + .ofTypes("java.lang.Runtime").names("exec").withAnyParameters().build(), + MethodMatchers.create() + .ofSubTypes("java.sql.Connection") + .names("createBlob", "createClob", "createNClob", "createSQLXML", "createArrayOf") + .withAnyParameters().build(), + MethodMatchers.create() + .ofSubTypes("java.sql.ResultSet", "java.sql.CallableStatement") + .names("getBlob", "getClob", "getNClob", "getSQLXML", "getArray") + .withAnyParameters().build() + ); + private final Deque withinTry = new LinkedList<>(); private final Deque> toReport = new LinkedList<>(); @@ -83,7 +98,8 @@ public void visitNode(Tree tree) { private static boolean isNewAutocloseableOrBuilder(Tree tree, JavaFileScannerContext context) { return (tree instanceof NewClassTree newClass && newClass.symbolType().isSubtypeOf("java.lang.AutoCloseable")) || - (context.getJavaVersion().isJava21Compatible() && tree instanceof MethodInvocationTree mit && AUTOCLOSEABLE_BUILDER_MATCHER.matches(mit))|| + (context.getJavaVersion().isJava21Compatible() && tree instanceof MethodInvocationTree mit && AUTOCLOSEABLE_BUILDER_MATCHER.matches(mit)) || + (context.getJavaVersion().isJava26Compatible() && tree instanceof MethodInvocationTree mit3 && AUTOCLOSEABLE_JAVA26_MATCHER.matches(mit3)) || (tree instanceof MethodInvocationTree mit2 && AUTOCLOSEABLE_FACTORY_MATCHER.matches(mit2)); } diff --git a/java-checks/src/test/java/org/sonar/java/checks/TryWithResourcesCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/TryWithResourcesCheckTest.java index 5704c102616..40a29456f9c 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/TryWithResourcesCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/TryWithResourcesCheckTest.java @@ -52,4 +52,17 @@ void test_java_21() { .withJavaVersion(21) .verifyIssues(); } + + @Test + void test_java_26() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/TryWithResourcesCheck_java_26.java")) + .withCheck(new TryWithResourcesCheck()) + .verifyNoIssues(); + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/TryWithResourcesCheck_java_26.java")) + .withCheck(new TryWithResourcesCheck()) + .withJavaVersion(26) + .verifyIssues(); + } } From aaeb9fc09879ceb7591f3f696a4fa424ea3bfbcf Mon Sep 17 00:00:00 2001 From: Alex Meseldzija Date: Wed, 24 Jun 2026 13:14:59 +0200 Subject: [PATCH 2/3] Bot review --- .../checks/TryWithResourcesCheck_java_26.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java b/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java index b0a67d69ee7..2a19e9e5eb0 100644 --- a/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java +++ b/java-checks-test-sources/default/src/main/java/checks/TryWithResourcesCheck_java_26.java @@ -6,6 +6,7 @@ import java.sql.CallableStatement; import java.sql.Clob; import java.sql.Connection; +import java.sql.NClob; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLXML; @@ -98,6 +99,66 @@ void blobFromCallableStatement(CallableStatement cs) throws SQLException { } } + void clobFromConnectionFactory(Connection connection) throws SQLException { + Clob clob = null; + try { // Noncompliant + clob = connection.createClob(); + clob.length(); + } finally { + if (clob != null) { + clob.close(); + } + } + } + + void nclobFromConnectionFactory(Connection connection) throws SQLException { + NClob nclob = null; + try { // Noncompliant + nclob = connection.createNClob(); + nclob.length(); + } finally { + if (nclob != null) { + nclob.close(); + } + } + } + + void arrayFromConnectionFactory(Connection connection) throws SQLException { + Array array = null; + try { // Noncompliant + array = connection.createArrayOf("VARCHAR", new String[] {"a", "b"}); + array.getBaseType(); + } finally { + if (array != null) { + array.close(); + } + } + } + + void nclobFromResultSetGetter(ResultSet rs) throws SQLException { + NClob nclob = null; + try { // Noncompliant + nclob = rs.getNClob("payload"); + nclob.length(); + } finally { + if (nclob != null) { + nclob.close(); + } + } + } + + void sqlxmlFromResultSetGetter(ResultSet rs) throws SQLException { + SQLXML xml = null; + try { // Noncompliant + xml = rs.getSQLXML("payload"); + xml.getString(); + } finally { + if (xml != null) { + xml.close(); + } + } + } + void compliantTryWithResources(Connection connection) throws IOException, SQLException { try (Process p = new ProcessBuilder("ls").start(); Blob blob = connection.createBlob()) { From 8c9e3cbc5f872b2ca48bdf5f91819fe16a9d12d8 Mon Sep 17 00:00:00 2001 From: Alex Meseldzija Date: Wed, 24 Jun 2026 13:23:03 +0200 Subject: [PATCH 3/3] Refactor isNewAutocloseableOrBuilder --- .../sonar/java/checks/TryWithResourcesCheck.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java index e27cb57129d..5696f4be99f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/TryWithResourcesCheck.java @@ -37,7 +37,7 @@ @Rule(key = "S2093") public class TryWithResourcesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { - private static final MethodMatchers AUTOCLOSEABLE_BUILDER_MATCHER = MethodMatchers.or( + private static final MethodMatchers AUTOCLOSEABLE_JAVA21_MATCHER = MethodMatchers.or( MethodMatchers.create().ofTypes("java.net.http.HttpClient$Builder").names("build").addWithoutParametersMatcher().build(), MethodMatchers.create().ofTypes("java.net.http.HttpClient").names("newHttpClient").addWithoutParametersMatcher().build() ); @@ -97,10 +97,15 @@ public void visitNode(Tree tree) { } private static boolean isNewAutocloseableOrBuilder(Tree tree, JavaFileScannerContext context) { - return (tree instanceof NewClassTree newClass && newClass.symbolType().isSubtypeOf("java.lang.AutoCloseable")) || - (context.getJavaVersion().isJava21Compatible() && tree instanceof MethodInvocationTree mit && AUTOCLOSEABLE_BUILDER_MATCHER.matches(mit)) || - (context.getJavaVersion().isJava26Compatible() && tree instanceof MethodInvocationTree mit3 && AUTOCLOSEABLE_JAVA26_MATCHER.matches(mit3)) || - (tree instanceof MethodInvocationTree mit2 && AUTOCLOSEABLE_FACTORY_MATCHER.matches(mit2)); + if (tree instanceof NewClassTree newClass) { + return newClass.symbolType().isSubtypeOf("java.lang.AutoCloseable"); + } else if (tree instanceof MethodInvocationTree mit) { + return AUTOCLOSEABLE_FACTORY_MATCHER.matches(mit) || + (context.getJavaVersion().isJava21Compatible() && AUTOCLOSEABLE_JAVA21_MATCHER.matches(mit)) || + (context.getJavaVersion().isJava26Compatible() && AUTOCLOSEABLE_JAVA26_MATCHER.matches(mit)); + } else { + return false; + } } private static boolean isFollowedByTryWithFinally(Tree tree) {