diff --git a/docs/superpowers/baselines/2026-04-17/BASELINE.md b/docs/superpowers/baselines/2026-04-17/BASELINE.md index f2beb9fc..cfa4cc3b 100644 --- a/docs/superpowers/baselines/2026-04-17/BASELINE.md +++ b/docs/superpowers/baselines/2026-04-17/BASELINE.md @@ -230,6 +230,7 @@ Ordered by severity. Each item cites the raw artifact it was derived from. - **SpotBugs: 8 HIGH-priority findings (priority=1) + 1,484 at priority=2.** Total 1,492. HIGH findings must be triaged individually (read `raw/spotbugs.xml`). Noise-dominant rules (`NM_METHOD_NAMING_CONVENTION`=730, `SF_SWITCH_NO_DEFAULT`=448) should be filtered via a SpotBugs exclude file so real signal surfaces; real-concern patterns that deserve review now: `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` (26), `BC_UNCONFIRMED_CAST` (55), `UL_UNRELEASED_LOCK_EXCEPTION_PATH` (1), `WMI_WRONG_MAP_ITERATOR` (2), `ES_COMPARING_STRINGS_WITH_EQ` (2), `MT_CORRECTNESS` category (1). - Raw: `raw/spotbugs.xml`, `raw/spotbugs-summary.json`. + - **RESOLVED (2026-04-17, branch `phase-a/fixups-spotbugs`)**: Added `spotbugs-exclude.xml` covering ANTLR-generated parsers and global noise rules (`NM_METHOD_NAMING_CONVENTION`, `SF_SWITCH_NO_DEFAULT`, `EI_EXPOSE_REP`/`EI_EXPOSE_REP2`, `MS_PKGPROTECT`/`MS_FINAL_PKGPROTECT`), wired via `pom.xml`. Fixed all 8 priority-1 findings in codeiq code (UTF-8 in `Analyzer.getGitHead`, narrowed catch in `IndexCommand`, dead-store removed in `PluginsCommand`, `.equals()` in `AntlrParserFactory` + `CSharpPreprocessorParserBase`, try-finally unlock in `AnalysisCache.removeFile`, merged duplicate branches in `CodeIqApplication`, removed dead `BundleCommand.writeEntry` overload, `entrySet()` iteration in `PluginsCommand` + `GitLabCiDetector`, narrowed `VersionCommand` catch). **Final: 1,492 → 38 (-97.5%); priority-1: 8 → 0.** Remaining 38 are priority-2 STYLE/BAD_PRACTICE; no CORRECTNESS/MT_CORRECTNESS/SECURITY left. Next-pass candidates: 26 `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE`. Post-triage summary: `raw/spotbugs-summary-after-triage.json`. ### Medium diff --git a/pom.xml b/pom.xml index 436a1d62..c193d8ed 100644 --- a/pom.xml +++ b/pom.xml @@ -323,6 +323,9 @@ com.github.spotbugs spotbugs-maven-plugin ${spotbugs.version} + + spotbugs-exclude.xml + diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 00000000..019922f6 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/io/github/randomcodespace/iq/CodeIqApplication.java b/src/main/java/io/github/randomcodespace/iq/CodeIqApplication.java index 9ed17454..4b831488 100644 --- a/src/main/java/io/github/randomcodespace/iq/CodeIqApplication.java +++ b/src/main/java/io/github/randomcodespace/iq/CodeIqApplication.java @@ -96,17 +96,12 @@ public static void main(String[] args) { // Point Neo4j config to the graph path (enriched or new empty db). // GraphBootstrapper will auto-load from H2 cache if no enriched graph exists. System.setProperty("codeiq.graph.path", graphDbPath.toString()); - } else if (isIndex) { - app.setAdditionalProfiles("indexing"); - // Index command: no web server, no Neo4j - app.setWebApplicationType(org.springframework.boot.WebApplicationType.NONE); - } else if (isEnrich) { - // Enrich command: no web server, Neo4j started programmatically - app.setAdditionalProfiles("indexing"); - app.setWebApplicationType(org.springframework.boot.WebApplicationType.NONE); } else { + // All non-serve commands (index, enrich, analyze, stats, ...) share the same + // Spring setup: "indexing" profile, no web server. index/enrich open Neo4j + // programmatically when needed. Previously split into three identical + // branches — SpotBugs DB_DUPLICATE_BRANCHES. app.setAdditionalProfiles("indexing"); - // Disable web server for non-serve commands app.setWebApplicationType(org.springframework.boot.WebApplicationType.NONE); } diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java index a3faa8a4..dab92dd2 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java @@ -1,5 +1,6 @@ package io.github.randomcodespace.iq.analyzer; +import java.nio.charset.StandardCharsets; import io.github.randomcodespace.iq.analyzer.linker.Linker; import io.github.randomcodespace.iq.cache.AnalysisCache; import io.github.randomcodespace.iq.cache.FileHasher; @@ -1608,7 +1609,7 @@ private String getGitHead(Path repoPath) { .directory(repoPath.toFile()) .redirectErrorStream(true); Process proc = pb.start(); - String sha = new String(proc.getInputStream().readAllBytes()).trim(); + String sha = new String(proc.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); int exitCode = proc.waitFor(); if (exitCode == 0 && sha.length() >= 7) { return sha; diff --git a/src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java b/src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java index 7002b352..81c6722b 100644 --- a/src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java +++ b/src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java @@ -372,10 +372,16 @@ public void storeResults(String contentHash, String filePath, String language, log.warn("Failed to store cached results for hash {}", contentHash, e); } finally { try { - conn.setAutoCommit(true); - } catch (SQLException ignored) { + try { + conn.setAutoCommit(true); + } catch (SQLException ignored) { + // best-effort restore; the INSERTs have already been committed or rolled back. + } + } finally { + // Guarantee unlock even if conn.setAutoCommit throws a non-SQLException + // (RuntimeException / Error). Fixes SpotBugs UL_UNRELEASED_LOCK_EXCEPTION_PATH. + rwLock.writeLock().unlock(); } - rwLock.writeLock().unlock(); } } @@ -453,10 +459,16 @@ public void removeFile(String contentHash) { log.warn("Failed to remove cached file {}", contentHash, e); } finally { try { - conn.setAutoCommit(true); - } catch (SQLException ignored) { + try { + conn.setAutoCommit(true); + } catch (SQLException ignored) { + // best-effort restore; the DELETEs have already been committed or rolled back. + } + } finally { + // Guarantee unlock even if conn.setAutoCommit throws a non-SQLException + // (RuntimeException / Error). Fixes SpotBugs UL_UNRELEASED_LOCK_EXCEPTION_PATH. + rwLock.writeLock().unlock(); } - rwLock.writeLock().unlock(); } } @@ -599,10 +611,16 @@ public void replaceAll(List nodes, List edges) { log.warn("Failed to replace cache with enriched data", e); } finally { try { - conn.setAutoCommit(true); - } catch (SQLException ignored) { + try { + conn.setAutoCommit(true); + } catch (SQLException ignored) { + // best-effort restore; the INSERTs have already been committed or rolled back. + } + } finally { + // Guarantee unlock even if conn.setAutoCommit throws a non-SQLException + // (RuntimeException / Error). Fixes SpotBugs UL_UNRELEASED_LOCK_EXCEPTION_PATH. + rwLock.writeLock().unlock(); } - rwLock.writeLock().unlock(); } } diff --git a/src/main/java/io/github/randomcodespace/iq/cli/BundleCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/BundleCommand.java index bb24ac4c..f8f928ea 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/BundleCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/BundleCommand.java @@ -464,9 +464,4 @@ private void writeEntry(ZipOutputStream zos, String name, String content) throws zos.closeEntry(); } - private void writeEntry(ZipOutputStream zos, String name, String content, String lineEnding) - throws IOException { - writeEntry(zos, name, content); - } - } diff --git a/src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java index bb8a30b4..9ead0fa7 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java @@ -85,8 +85,15 @@ public Integer call() { if (java.nio.file.Files.exists(cacheDir)) { try { try (var walk = java.nio.file.Files.walk(cacheDir)) { + var logger = org.slf4j.LoggerFactory.getLogger(IndexCommand.class); walk.sorted(java.util.Comparator.reverseOrder()) - .forEach(p -> { try { java.nio.file.Files.deleteIfExists(p); } catch (Exception ignored) {} }); + .forEach(p -> { + try { + java.nio.file.Files.deleteIfExists(p); + } catch (java.io.IOException ex) { + logger.debug("Could not delete cache entry {}: {}", p, ex.getMessage()); + } + }); } CliOutput.info(" Deleted existing cache at " + cacheDir); } catch (Exception e) { diff --git a/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java index ea0b1d30..b56c7f45 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java @@ -100,8 +100,6 @@ public Integer call() { * or falls back to a sensible default. */ static String categoryDescription(String category, List detectors) { - // Collect unique frameworks from DetectorInfo if available - Set frameworks = new TreeSet<>(); for (Detector d : detectors) { DetectorInfo info = d.getClass().getAnnotation(DetectorInfo.class); if (info != null && info.description() != null && !info.description().isEmpty()) { @@ -406,8 +404,9 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) { yaml.append("# Optimized for this project's detected languages\n\n"); yaml.append("languages:\n"); - for (String lang : languageCounts.keySet()) { - yaml.append(" - ").append(lang).append(" # ").append(languageCounts.get(lang)).append(" files\n"); + for (Map.Entry entry : languageCounts.entrySet()) { + yaml.append(" - ").append(entry.getKey()) + .append(" # ").append(entry.getValue()).append(" files\n"); } yaml.append("\ndetectors:\n"); diff --git a/src/main/java/io/github/randomcodespace/iq/cli/VersionCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/VersionCommand.java index aed2f24c..c660acb4 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/VersionCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/VersionCommand.java @@ -38,8 +38,8 @@ private static String resolveVersion() { String v = props.getProperty("build.version"); if (v != null && !v.isBlank()) return v; } - } catch (Exception ignored) { - // intentionally empty + } catch (java.io.IOException ignored) { + // build-info.properties is optional; fall through to manifest lookup. } // Fallback: Implementation-Version from JAR manifest String v = VersionCommand.class.getPackage().getImplementationVersion(); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java index d040f9a6..eff1b011 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java @@ -135,9 +135,10 @@ public DetectorResult detect(DetectorContext ctx) { // Collect job names List jobNames = new ArrayList<>(); - for (String key : data.keySet()) { + for (Map.Entry entry : data.entrySet()) { + String key = entry.getKey(); if (GITLAB_CI_KEYWORDS.contains(key)) continue; - if (data.get(key) instanceof Map) { + if (entry.getValue() instanceof Map) { jobNames.add(key); } } diff --git a/src/main/java/io/github/randomcodespace/iq/grammar/AntlrParserFactory.java b/src/main/java/io/github/randomcodespace/iq/grammar/AntlrParserFactory.java index 5559bff0..2765d193 100644 --- a/src/main/java/io/github/randomcodespace/iq/grammar/AntlrParserFactory.java +++ b/src/main/java/io/github/randomcodespace/iq/grammar/AntlrParserFactory.java @@ -113,9 +113,11 @@ public static ParseTree parse(String language, String content) { return null; } - // Check thread-local cache — same content object means same file + // Check thread-local cache. Using .equals() is correct because the parse tree + // is a deterministic function of content — equal content yields an equivalent tree. + // (Previously used == for identity fast-path; SpotBugs ES_COMPARING_PARAMETER_STRING_WITH_EQ.) var cached = PARSE_CACHE.get(); - if (cached != null && cached.getKey() == content) { + if (cached != null && content.equals(cached.getKey())) { return cached.getValue(); } diff --git a/src/main/java/io/github/randomcodespace/iq/grammar/csharp/CSharpPreprocessorParserBase.java b/src/main/java/io/github/randomcodespace/iq/grammar/csharp/CSharpPreprocessorParserBase.java index 538f0614..1e8a2dd2 100644 --- a/src/main/java/io/github/randomcodespace/iq/grammar/csharp/CSharpPreprocessorParserBase.java +++ b/src/main/java/io/github/randomcodespace/iq/grammar/csharp/CSharpPreprocessorParserBase.java @@ -179,14 +179,14 @@ protected void OnPreprocessorExpressionConditionalEq() { ParserRuleContext c = this._ctx; CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext)c; - d.value = (d.expr1.value == d.expr2.value ? "true" : "false"); + d.value = (java.util.Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false"); } protected void OnPreprocessorExpressionConditionalNe() { ParserRuleContext c = this._ctx; CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext)c; - d.value = (d.expr1.value != d.expr2.value ? "true" : "false"); + d.value = (!java.util.Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false"); } protected void OnPreprocessorExpressionConditionalAnd()