From 323320c1df186565367719d0b05bfc05d7b01c9f Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 17 Aug 2026 16:38:26 +0200 Subject: [PATCH 1/3] Implement new rule S9342 Detect empty archive entries where closeEntry() is called on a ZipOutputStream or JarOutputStream after putNextEntry() without any intervening write() call, which creates useless empty entries in the archive. --- .../checks/EmptyArchiveEntryCheckSample.java | 175 ++++++++++++++++++ .../java/checks/EmptyArchiveEntryCheck.java | 169 +++++++++++++++++ .../checks/EmptyArchiveEntryCheckTest.java | 42 +++++ .../org/sonar/l10n/java/rules/java/S9342.html | 48 +++++ .../org/sonar/l10n/java/rules/java/S9342.json | 23 +++ .../main/resources/profiles/Sonar_way/S9342 | 0 6 files changed, 457 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/EmptyArchiveEntryCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9342 diff --git a/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java new file mode 100644 index 00000000000..8dbed2494a1 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java @@ -0,0 +1,175 @@ +package checks; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +class EmptyArchiveEntryCheckSample { + + void emptyZipEntry() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("empty.txt")); +// ^^^^^^^^^^^^> + zos.closeEntry(); // Noncompliant {{Write content to this archive entry; it is empty.}} +// ^^^^^^^^^^ + zos.close(); + } + + void multipleEntriesOneEmpty() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("file.txt")); + zos.write("content".getBytes()); + zos.closeEntry(); // Compliant - content written + + zos.putNextEntry(new ZipEntry("empty.txt")); +// ^^^^^^^^^^^^> + zos.closeEntry(); // Noncompliant +// ^^^^^^^^^^ + zos.close(); + } + + void emptyEntryInLoop(String[] names) throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + for (String name : names) { + zos.putNextEntry(new ZipEntry(name)); +// ^^^^^^^^^^^^> + zos.closeEntry(); // Noncompliant +// ^^^^^^^^^^ + } + zos.close(); + } + + void emptyJarEntry() throws IOException { + JarOutputStream jos = new JarOutputStream(new FileOutputStream("app.jar")); + jos.putNextEntry(new JarEntry("empty.class")); +// ^^^^^^^^^^^^> + jos.closeEntry(); // Noncompliant +// ^^^^^^^^^^ + jos.close(); + } + + void jarWithZipEntry() throws IOException { + JarOutputStream jos = new JarOutputStream(new FileOutputStream("app.jar")); + jos.putNextEntry(new ZipEntry("empty.txt")); +// ^^^^^^^^^^^^> + jos.closeEntry(); // Noncompliant +// ^^^^^^^^^^ + jos.close(); + } + + void zipWithContent() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("file.txt")); + zos.write("Hello, World!".getBytes()); + zos.closeEntry(); // Compliant + zos.close(); + } + + void writeFromFile() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("data.bin")); + FileInputStream fis = new FileInputStream("data.bin"); + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + zos.write(buffer, 0, len); + } + fis.close(); + zos.closeEntry(); // Compliant + zos.close(); + } + + void writeByteArray() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("data.txt")); + byte[] data = "content".getBytes(); + zos.write(data, 0, data.length); + zos.closeEntry(); // Compliant + zos.close(); + } + + void jarWithContent() throws IOException { + JarOutputStream jos = new JarOutputStream(new FileOutputStream("app.jar")); + jos.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); + jos.write("Manifest-Version: 1.0\n".getBytes()); + jos.closeEntry(); // Compliant + jos.close(); + } + + void multipleJarEntriesAllWithContent() throws IOException { + JarOutputStream jos = new JarOutputStream(new FileOutputStream("app.jar")); + jos.putNextEntry(new JarEntry("a.class")); + jos.write(new byte[]{1, 2, 3}); + jos.closeEntry(); // Compliant + + jos.putNextEntry(new JarEntry("b.class")); + jos.write(new byte[]{4, 5, 6}); + jos.closeEntry(); // Compliant + jos.close(); + } + + void writeSingleByte() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("byte.txt")); + zos.write(65); + zos.closeEntry(); // Compliant + zos.close(); + } + + void writeInIfBlock() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("conditional.txt")); + if (System.currentTimeMillis() > 0) { + zos.write("data".getBytes()); + } + zos.closeEntry(); // Compliant - write is in a conditional block + zos.close(); + } + + void helperMethodWithStream() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("helper.txt")); + writeContent(zos); + zos.closeEntry(); // Compliant - stream passed to helper method + zos.close(); + } + + private void writeContent(ZipOutputStream zos) throws IOException { + zos.write("content".getBytes()); + } + + void twoStreamsIndependent() throws IOException { + ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream("a.zip")); + ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream("b.zip")); + + zos1.putNextEntry(new ZipEntry("file.txt")); + zos2.putNextEntry(new ZipEntry("file.txt")); + zos1.write("content".getBytes()); + zos1.closeEntry(); // Compliant - zos1 has content + + zos2.putNextEntry(new ZipEntry("empty.txt")); +// ^^^^^^^^^^^^> + zos2.closeEntry(); // Noncompliant +// ^^^^^^^^^^ + + zos1.close(); + zos2.close(); + } + + void noCloseEntry() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("file.txt")); + // No closeEntry - no issue raised (putNextEntry without closeEntry is not flagged) + zos.close(); + } + + void closeEntryWithoutPutNextEntry() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.closeEntry(); // Compliant - no preceding putNextEntry tracked + zos.close(); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java new file mode 100644 index 00000000000..ea53a26013c --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java @@ -0,0 +1,169 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckForNull; +import org.sonar.check.Rule; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.BlockTree; +import org.sonar.plugins.java.api.tree.ExpressionStatementTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.StatementTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S9342") +public class EmptyArchiveEntryCheck extends IssuableSubscriptionVisitor { + + private static final MethodMatchers PUT_NEXT_ENTRY = MethodMatchers.create() + .ofSubTypes("java.util.zip.ZipOutputStream") + .names("putNextEntry") + .addParametersMatcher("java.util.zip.ZipEntry") + .build(); + + private static final MethodMatchers CLOSE_ENTRY = MethodMatchers.create() + .ofSubTypes("java.util.zip.ZipOutputStream") + .names("closeEntry") + .addWithoutParametersMatcher() + .build(); + + private static final MethodMatchers WRITE = MethodMatchers.create() + .ofSubTypes("java.io.OutputStream") + .names("write") + .withAnyParameters() + .build(); + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.BLOCK); + } + + @Override + public void visitNode(Tree tree) { + Map pendingEntries = new HashMap<>(); + for (StatementTree statement : ((BlockTree) tree).body()) { + MethodInvocationTree mit = extractMethodInvocation(statement); + if (mit != null) { + handleMethodInvocation(mit, pendingEntries); + } else { + scanForTrackedSymbolUsage(statement, pendingEntries); + } + } + } + + private void handleMethodInvocation(MethodInvocationTree mit, Map pendingEntries) { + Symbol receiver = getReceiverSymbol(mit); + if (receiver == null) { + return; + } + if (PUT_NEXT_ENTRY.matches(mit)) { + pendingEntries.put(receiver, mit); + } else if (CLOSE_ENTRY.matches(mit)) { + MethodInvocationTree putNextEntry = pendingEntries.remove(receiver); + if (putNextEntry != null) { + reportIssue(ExpressionUtils.methodName(mit), "Write content to this archive entry; it is empty.", + Collections.singletonList(new JavaFileScannerContext.Location("Entry opened here", ExpressionUtils.methodName(putNextEntry))), null); + } + } else if (WRITE.matches(mit)) { + pendingEntries.remove(receiver); + } + } + + private static void scanForTrackedSymbolUsage(StatementTree statement, Map pendingEntries) { + if (pendingEntries.isEmpty()) { + return; + } + TrackedSymbolVisitor visitor = new TrackedSymbolVisitor(pendingEntries); + statement.accept(visitor); + for (Symbol used : visitor.usedSymbols) { + pendingEntries.remove(used); + } + } + + @CheckForNull + private static MethodInvocationTree extractMethodInvocation(StatementTree statement) { + if (statement.is(Tree.Kind.EXPRESSION_STATEMENT)) { + ExpressionTree expression = ((ExpressionStatementTree) statement).expression(); + if (expression.is(Tree.Kind.METHOD_INVOCATION)) { + return (MethodInvocationTree) expression; + } + } + return null; + } + + @CheckForNull + private static Symbol getReceiverSymbol(MethodInvocationTree mit) { + ExpressionTree methodSelect = mit.methodSelect(); + if (methodSelect.is(Tree.Kind.MEMBER_SELECT)) { + ExpressionTree expression = ((MemberSelectExpressionTree) methodSelect).expression(); + if (expression.is(Tree.Kind.IDENTIFIER)) { + Symbol symbol = ((IdentifierTree) expression).symbol(); + if (!symbol.isUnknown()) { + return symbol; + } + } + } + return null; + } + + private static class TrackedSymbolVisitor extends BaseTreeVisitor { + private final Map pendingEntries; + private final List usedSymbols = new ArrayList<>(); + + TrackedSymbolVisitor(Map pendingEntries) { + this.pendingEntries = pendingEntries; + } + + @Override + public void visitMethodInvocation(MethodInvocationTree mit) { + Symbol receiver = getReceiverSymbol(mit); + if (receiver != null && pendingEntries.containsKey(receiver)) { + if (WRITE.matches(mit)) { + usedSymbols.add(receiver); + } + } + // Check if any tracked symbol is passed as argument + for (ExpressionTree arg : mit.arguments()) { + if (arg.is(Tree.Kind.IDENTIFIER)) { + Symbol argSymbol = ((IdentifierTree) arg).symbol(); + if (pendingEntries.containsKey(argSymbol)) { + usedSymbols.add(argSymbol); + } + } + } + super.visitMethodInvocation(mit); + } + + @Override + public void visitIdentifier(IdentifierTree tree) { + // Intentionally not clearing state for simple identifier references; + // only method calls (write or passing as argument) should clear state. + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/EmptyArchiveEntryCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/EmptyArchiveEntryCheckTest.java new file mode 100644 index 00000000000..b94a2e9e51b --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/EmptyArchiveEntryCheckTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class EmptyArchiveEntryCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/EmptyArchiveEntryCheckSample.java")) + .withCheck(new EmptyArchiveEntryCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/EmptyArchiveEntryCheckSample.java")) + .withCheck(new EmptyArchiveEntryCheck()) + .withoutSemantic() + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.html new file mode 100644 index 00000000000..a570ff91de7 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.html @@ -0,0 +1,48 @@ +

An issue is raised when an operation that closes an archive entry is called immediately after an operation that opens or begins a new +entry in an archive output stream, without writing any content in between.

+

Why is this an issue?

+

When creating archive files (ZIP or JAR), entries are added using a three-step process:

+
    +
  1. Call an API method to declare the start of a new entry
  2. +
  3. Write the entry's content using output stream methods
  4. +
  5. Call an API method to finalize the entry
  6. +
+

Skipping the second step creates an empty entry in the archive. This is almost always a mistake.

+

Empty archive entries serve no useful purpose. In ZIP files, they waste space by storing metadata for entries with no content. In JAR files, +empty entries can cause runtime errors when the application expects to load classes or resources that don't exist.

+

In Java, this specifically refers to calling closeEntry() immediately after putNextEntry() on a +ZipOutputStream or JarOutputStream.

+

What is the potential impact?

+
    +
  • Build failures: Archive files with empty code file entries will fail when the runtime attempts to load those code + modules
  • +
  • Missing resources: Applications expecting configuration files, images, or other resources will fail when these entries are + empty
  • +
  • Confusion and debugging time: Developers may spend time investigating why expected files aren't working, not realizing they + exist but are empty
  • +
+

How to fix it

+

Write content to the archive entry between putNextEntry() and closeEntry() using the write() method. +The content can come from a byte array, file, or any other source.

+

Code examples

+

Noncompliant code example

+
+ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"));
+zos.putNextEntry(new ZipEntry("empty.txt"));
+zos.closeEntry(); // Noncompliant - no content written
+
+

Compliant solution

+
+ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"));
+zos.putNextEntry(new ZipEntry("file.txt"));
+zos.write("Hello, World!".getBytes());
+zos.closeEntry();
+
+

Resources

+

Documentation

+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.json new file mode 100644 index 00000000000..98e578fedd5 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9342.json @@ -0,0 +1,23 @@ +{ + "title": "Archive entries should not be empty", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "suspicious" + ], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-9342", + "sqKey": "S9342", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9342 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9342 new file mode 100644 index 00000000000..e69de29bb2d From d5599fa3d2d91cade3d33b98442912fdb560cc52 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 18 Aug 2026 11:53:25 +0200 Subject: [PATCH 2/3] Fix FPs in S9342: helper method args, directory entries, and constructor wrapping - Fix false positive when stream is passed to a helper method as argument (e.g., writeContent(zos)) by checking arguments when receiver is null - Fix false positive on ZIP directory entries (names ending with "/") - Fix false positive when stream is wrapped in a constructor (e.g., new PrintWriter(zos)) by adding visitNewClass to TrackedSymbolVisitor - Skip analysis when semantic model is unavailable to avoid false positives Co-Authored-By: Claude Opus 4.6 --- .../checks/EmptyArchiveEntryCheckSample.java | 17 +++++++ .../java/checks/EmptyArchiveEntryCheck.java | 44 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java index 8dbed2494a1..ace80b3a168 100644 --- a/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/EmptyArchiveEntryCheckSample.java @@ -172,4 +172,21 @@ void closeEntryWithoutPutNextEntry() throws IOException { zos.closeEntry(); // Compliant - no preceding putNextEntry tracked zos.close(); } + + void zipDirectoryEntry() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("dir/")); + zos.closeEntry(); // Compliant - directory entry + zos.close(); + } + + void writeViaWrapper() throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip")); + zos.putNextEntry(new ZipEntry("wrapped.txt")); + java.io.PrintWriter pw = new java.io.PrintWriter(zos); + pw.println("content via wrapper"); + pw.flush(); + zos.closeEntry(); // Compliant - written through wrapper + zos.close(); + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java index ea53a26013c..be7367abe1e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java @@ -33,8 +33,10 @@ import org.sonar.plugins.java.api.tree.ExpressionStatementTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LiteralTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.NewClassTree; import org.sonar.plugins.java.api.tree.StatementTree; import org.sonar.plugins.java.api.tree.Tree; @@ -66,6 +68,9 @@ public List nodesToVisit() { @Override public void visitNode(Tree tree) { + if (context.getSemanticModel() == null) { + return; + } Map pendingEntries = new HashMap<>(); for (StatementTree statement : ((BlockTree) tree).body()) { MethodInvocationTree mit = extractMethodInvocation(statement); @@ -80,10 +85,13 @@ public void visitNode(Tree tree) { private void handleMethodInvocation(MethodInvocationTree mit, Map pendingEntries) { Symbol receiver = getReceiverSymbol(mit); if (receiver == null) { + clearTrackedSymbolsUsedAsArguments(mit, pendingEntries); return; } if (PUT_NEXT_ENTRY.matches(mit)) { - pendingEntries.put(receiver, mit); + if (!isDirectoryEntry(mit)) { + pendingEntries.put(receiver, mit); + } } else if (CLOSE_ENTRY.matches(mit)) { MethodInvocationTree putNextEntry = pendingEntries.remove(receiver); if (putNextEntry != null) { @@ -117,6 +125,27 @@ private static MethodInvocationTree extractMethodInvocation(StatementTree statem return null; } + private static boolean isDirectoryEntry(MethodInvocationTree putNextEntry) { + if (putNextEntry.arguments().size() == 1 && putNextEntry.arguments().get(0).is(Tree.Kind.NEW_CLASS)) { + NewClassTree newClass = (NewClassTree) putNextEntry.arguments().get(0); + if (newClass.arguments().size() == 1 && newClass.arguments().get(0).is(Tree.Kind.STRING_LITERAL)) { + String value = ((LiteralTree) newClass.arguments().get(0)).value(); + // value includes quotes, e.g. "\"dir/\"" + return value.endsWith("/\""); + } + } + return false; + } + + private static void clearTrackedSymbolsUsedAsArguments(MethodInvocationTree mit, Map pendingEntries) { + for (ExpressionTree arg : mit.arguments()) { + if (arg.is(Tree.Kind.IDENTIFIER)) { + Symbol argSymbol = ((IdentifierTree) arg).symbol(); + pendingEntries.remove(argSymbol); + } + } + } + @CheckForNull private static Symbol getReceiverSymbol(MethodInvocationTree mit) { ExpressionTree methodSelect = mit.methodSelect(); @@ -160,6 +189,19 @@ public void visitMethodInvocation(MethodInvocationTree mit) { super.visitMethodInvocation(mit); } + @Override + public void visitNewClass(NewClassTree tree) { + for (ExpressionTree arg : tree.arguments()) { + if (arg.is(Tree.Kind.IDENTIFIER)) { + Symbol argSymbol = ((IdentifierTree) arg).symbol(); + if (pendingEntries.containsKey(argSymbol)) { + usedSymbols.add(argSymbol); + } + } + } + super.visitNewClass(tree); + } + @Override public void visitIdentifier(IdentifierTree tree) { // Intentionally not clearing state for simple identifier references; From 5b9aeaa03c9e623430738160d857f1ad6418c8b6 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 18 Aug 2026 13:35:11 +0200 Subject: [PATCH 3/3] Merge collapsible if statements in S9342 to fix Quality Gate Co-Authored-By: Claude Opus 4.6 --- .../java/org/sonar/java/checks/EmptyArchiveEntryCheck.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java index be7367abe1e..c63379eefb6 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/EmptyArchiveEntryCheck.java @@ -172,10 +172,8 @@ private static class TrackedSymbolVisitor extends BaseTreeVisitor { @Override public void visitMethodInvocation(MethodInvocationTree mit) { Symbol receiver = getReceiverSymbol(mit); - if (receiver != null && pendingEntries.containsKey(receiver)) { - if (WRITE.matches(mit)) { - usedSymbols.add(receiver); - } + if (receiver != null && pendingEntries.containsKey(receiver) && WRITE.matches(mit)) { + usedSymbols.add(receiver); } // Check if any tracked symbol is passed as argument for (ExpressionTree arg : mit.arguments()) {