SONARJAVA-6769 Implement new rule S9342: Archive entries should not be empty - #5938
Conversation
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.
…tor 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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nathsou
left a comment
There was a problem hiding this comment.
I found three verified false-positive paths in S9342. The focused submitted test passes (mvn -pl java-checks -Dtest=EmptyArchiveEntryCheckTest test), but each case below produces an unexpected issue when analyzed with the new check.
|
|
||
| private void handleMethodInvocation(MethodInvocationTree mit, Map<Symbol, MethodInvocationTree> pendingEntries) { | ||
| Symbol receiver = getReceiverSymbol(mit); | ||
| if (receiver == null) { |
There was a problem hiding this comment.
Argument escape handling only runs when the invocation has no receiver. Consequently, this.writeContent(stream) (or helper.writeContent(stream)) leaves the entry pending and closeEntry() is reported even though the helper writes content. Apply the conservative argument clearing for other non-rule invocations too, and add a qualified-helper regression case.
| return null; | ||
| } | ||
|
|
||
| private static boolean isDirectoryEntry(MethodInvocationTree putNextEntry) { |
There was a problem hiding this comment.
The directory exemption only recognizes an inline new ZipEntry("dir/"). A common equivalent—constructing ZipEntry directory = new ZipEntry("dir/") first and passing directory to putNextEntry()—is reported as an empty file. Either recognize the entry value through the local symbol or conservatively avoid reporting when directory status cannot be established; add this form to the sample.
| 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)) { |
There was a problem hiding this comment.
Pending entries are keyed only by the ZipOutputStream symbol. If an OutputStream writer = new BufferedOutputStream(stream) is created before putNextEntry(), writer.write(...) removes writer rather than stream, so a subsequent stream.closeEntry() is falsely reported. Track aliases/wrappers or conservatively clear the wrapped stream’s state, including for wrappers created before an entry opens.
… wrapper streams - Clear tracked symbols when passed as arguments to qualified method calls (e.g. this.writeContent(zos)) that don't match PUT/CLOSE/WRITE matchers - Resolve variable-based ZipEntry arguments in isDirectoryEntry() by tracing identifiers back to their initializer via symbol declaration - Add test cases for all three false positive scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nathsou
left a comment
There was a problem hiding this comment.
Approving — found two false-negative gaps worth tracking, not blocking:
| 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); |
There was a problem hiding this comment.
putNextEntry/closeEntry correlation is scoped to a single BLOCK's immediate statement list, so the common try/finally idiom (putNextEntry() in the outer block, closeEntry() inside a nested finally/if block) is never analyzed, silently missing genuinely empty archive entries.
Example: zos.putNextEntry(new ZipEntry("empty.txt")); try { } finally { zos.closeEntry(); } creates a truly empty entry, but the rule never reports it — the nested block gets its own fresh, empty pendingEntries map in visitNode, and TrackedSymbolVisitor never treats CLOSE_ENTRY as clearing/using a tracked symbol.
| } | ||
|
|
||
| @Override | ||
| public void visitNewClass(NewClassTree tree) { |
There was a problem hiding this comment.
TrackedSymbolVisitor.visitNewClass clears a pending entry as soon as the tracked stream is passed to any constructor, even if the resulting wrapper's write method is never actually called.
Example: zos.putNextEntry(new ZipEntry("file.txt")); OutputStream w = new BufferedOutputStream(zos); zos.closeEntry(); (wrapper constructed but w.write(...) never invoked) produces a genuinely empty entry, yet this removes zos from pendingEntries merely because it appears as a constructor argument — a false negative for the exact bug the rule targets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Code Review ✅ Approved 2 resolved / 2 findingsImplements rule S9342 to detect empty archive entries in Zip and Jar output streams, addressing false positives on directory entries and wrapper streams. No issues found. ✅ 2 resolved✅ Edge Case: False positive on intentional ZIP directory entries
✅ Edge Case: FP when stream content written via wrapper stream/writer
Implementation Status ✅ 1 / 1 issues implemented✅ SONARJAVA-6769 — 1 / 1 objectivesThe PR implements the new rule S9342 regarding empty archive entries. ✅ 1 complete
OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |




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.