diff --git a/docs/superpowers/baselines/2026-04-17/BASELINE.md b/docs/superpowers/baselines/2026-04-17/BASELINE.md index cfa4cc3b..60550846 100644 --- a/docs/superpowers/baselines/2026-04-17/BASELINE.md +++ b/docs/superpowers/baselines/2026-04-17/BASELINE.md @@ -231,6 +231,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`. + - **PARTIALLY RESOLVED — NP_NULL subset (2026-04-17, branch `phase-a/fix-np-null`)**: all 26 `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` findings fixed — count 26 → 0. All 26 shared one pattern: calling `.toString()` on `Path.getFileName()` (or once `Path.getParent()`) where the result can be null for root-like paths. Fix: replaced every site with `java.util.Objects.toString(path.getFileName(), fallback)` using sensible per-site fallbacks (`""` for filename comparisons, `"unknown"` / `"bundle"` / `"flow"` for human-facing project names, `PROP_ROOT` in Analyzer). One `AnalysisCache` constructor call (`Files.createDirectories(dbPath.getParent())`) rewritten as a null-guarded block. 12 files touched (Analyzer.java 5 sites inc. a triplicated block, plus FileDiscovery, FileClassifier, ServiceDetector, ConfigScanner, AnalysisCache, BundleCommand, EnrichCommand, FlowCommand, PluginsCommand, StatsCommand, TopologyCommand). Full `mvn test` still green (3,059 tests, 0 failures). The broader SpotBugs triage (noise exclude + priority-1 fixes) lives on `phase-a/fixups-spotbugs`. ### Medium 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 dab92dd2..56010a38 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java @@ -360,7 +360,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach // 5b. Detect service boundaries and create SERVICE nodes report.accept("Detecting service boundaries..."); var serviceDetector = new ServiceDetector(); - String projectDirName = root.getFileName() != null ? root.getFileName().toString() : PROP_ROOT; + String projectDirName = java.util.Objects.toString(root.getFileName(), PROP_ROOT); var serviceResult = serviceDetector.detect(allNodes, builder.getEdges(), projectDirName, root); if (!serviceResult.serviceNodes().isEmpty()) { serviceResult.serviceNodes().forEach(n -> n.setProvenance(builder.getProvenance())); @@ -885,10 +885,11 @@ private AnalysisResult runSmartWithCache(Path root, Integer parallelism, int bat } else { filesSkipped++; // Zero data loss: create minimal inventory node for filtered files + String filteredFileName = java.util.Objects.toString(file.path().getFileName(), ""); CodeNode fileNode = new CodeNode( - "file:" + file.path() + ":module:" + file.path().getFileName(), + "file:" + file.path() + ":module:" + filteredFileName, NodeKind.MODULE, - file.path().getFileName().toString()); + filteredFileName); fileNode.setFilePath(file.path().toString()); fileNode.setModule(DetectorUtils.deriveModuleName(file.path().toString(), file.language())); fileNode.getProperties().put("status", "filtered"); @@ -1130,7 +1131,7 @@ Map> detectModules(Path root, List // Collect unique module directories from boundary marker files Set moduleDirs = new java.util.TreeSet<>(); for (DiscoveredFile file : files) { - if (MODULE_BOUNDARY_MARKERS.contains(file.path().getFileName().toString())) { + if (MODULE_BOUNDARY_MARKERS.contains(java.util.Objects.toString(file.path().getFileName(), ""))) { Path parent = file.path().getParent(); moduleDirs.add(parent != null ? parent.toString().replace('\\', '/') : ""); } @@ -1235,10 +1236,11 @@ DetectorResult analyzeFileWithRegistry(DiscoveredFile file, Path repoPath, if (isMinified(file, content)) { log.debug("Skipping detectors for minified file: {}", file.path()); String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language()); + String fileNameStr = java.util.Objects.toString(file.path().getFileName(), ""); CodeNode node = new CodeNode( - "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()), + "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr), NodeKind.MODULE, - file.path().getFileName().toString()); + fileNameStr); node.setFilePath(file.path().toString()); node.setModule(moduleName); node.setProperties(new java.util.LinkedHashMap<>(Map.of("minified", true, "file_type", "minified"))); @@ -1351,7 +1353,7 @@ private BoundedExecutor createExecutor(Integer parallelism) { } private boolean isMinified(DiscoveredFile file, String content) { - String name = file.path().getFileName().toString(); + String name = java.util.Objects.toString(file.path().getFileName(), ""); boolean nameHint = name.endsWith(".min.js") || name.endsWith(".bundle.js") || name.endsWith(".min.css") || name.endsWith(".min.mjs"); boolean jsOrCss = name.endsWith(".js") || name.endsWith(".mjs") || name.endsWith(".cjs") @@ -1434,10 +1436,11 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry if (isMinified(file, content)) { log.debug("Skipping detectors for minified file: {}", file.path()); String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language()); + String fileNameStr = java.util.Objects.toString(file.path().getFileName(), ""); CodeNode node = new CodeNode( - "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()), + "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr), NodeKind.MODULE, - file.path().getFileName().toString()); + fileNameStr); node.setFilePath(file.path().toString()); node.setModule(moduleName); node.setProperties(new java.util.LinkedHashMap<>(Map.of("minified", true, "file_type", "minified"))); @@ -1520,10 +1523,11 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry */ private static DetectorResult createInventoryNode(DiscoveredFile file, String fileType) { String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language()); + String fileNameStr = java.util.Objects.toString(file.path().getFileName(), ""); CodeNode node = new CodeNode( - "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()), + "file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr), NodeKind.MODULE, - file.path().getFileName().toString()); + fileNameStr); node.setFilePath(file.path().toString()); node.setModule(moduleName); node.setProperties(new java.util.LinkedHashMap<>(Map.of( diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java b/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java index 9570a8d5..bb725596 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java @@ -96,7 +96,7 @@ private void scanSpringConfig(Path root, InfrastructureRegistry registry) { for (Path candidate : candidates) { if (Files.isRegularFile(candidate)) { - String name = candidate.getFileName().toString(); + String name = java.util.Objects.toString(candidate.getFileName(), ""); if (name.endsWith(".properties")) { parseSpringProperties(candidate, registry); } else { diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/FileClassifier.java b/src/main/java/io/github/randomcodespace/iq/analyzer/FileClassifier.java index 5fb3ae36..ddcf182e 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/FileClassifier.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/FileClassifier.java @@ -54,7 +54,7 @@ public enum FileType { */ public static FileType classify(Path relativePath, String language) { String pathStr = relativePath.toString().replace('\\', '/'); - String fileName = relativePath.getFileName().toString(); + String fileName = java.util.Objects.toString(relativePath.getFileName(), ""); String ext = getExtension(fileName); // Binary check first diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/FileDiscovery.java b/src/main/java/io/github/randomcodespace/iq/analyzer/FileDiscovery.java index 5cd4d473..0bae6d1e 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/FileDiscovery.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/FileDiscovery.java @@ -185,7 +185,8 @@ private List discoverViaWalk(Path root) { Files.walkFileTree(root, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - String dirName = dir.getFileName() != null ? dir.getFileName().toString() : ""; + // Objects.toString yields fallback when getFileName() is null (root paths). + String dirName = java.util.Objects.toString(dir.getFileName(), ""); if (DEFAULT_EXCLUDES.contains(dirName)) { return FileVisitResult.SKIP_SUBTREE; } @@ -243,7 +244,7 @@ private boolean isExcluded(Path relPath) { } private static boolean isExcludedFilename(Path relPath) { - String filename = relPath.getFileName().toString(); + String filename = java.util.Objects.toString(relPath.getFileName(), ""); return EXCLUDED_FILENAMES.contains(filename); } } diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java b/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java index 046c9957..cf3b6aab 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java @@ -200,7 +200,7 @@ public ServiceDetectionResult detect(List nodes, List edges, String filePath = node.getFilePath(); if (filePath == null) continue; - String fileName = Path.of(filePath).getFileName().toString(); + String fileName = java.util.Objects.toString(Path.of(filePath).getFileName(), ""); String dirPath = parentDir(filePath); String buildTool = BUILD_FILES.get(fileName); @@ -343,7 +343,7 @@ private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map n.setProvenance(builder.getProvenance())); diff --git a/src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java index f618c1e1..28fa6445 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java @@ -76,7 +76,8 @@ public Integer call() { String content; if ("html".equalsIgnoreCase(format)) { - String projectName = path.toAbsolutePath().getFileName().toString(); + String projectName = java.util.Objects.toString( + path.toAbsolutePath().getFileName(), "flow"); content = engine.renderInteractive(projectName); } else { FlowDiagram diagram = engine.generate(view.toLowerCase()); 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 b56c7f45..57542283 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/PluginsCommand.java @@ -346,7 +346,7 @@ public Integer call() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - String name = dir.getFileName() != null ? dir.getFileName().toString() : ""; + String name = java.util.Objects.toString(dir.getFileName(), ""); if (SKIP_DIRS.contains(name)) { return FileVisitResult.SKIP_SUBTREE; } @@ -356,7 +356,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (!attrs.isRegularFile()) return FileVisitResult.CONTINUE; - String name = file.getFileName().toString(); + String name = java.util.Objects.toString(file.getFileName(), ""); int dot = name.lastIndexOf('.'); if (dot > 0) { String ext = name.substring(dot); diff --git a/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java index 6181dae7..8ab96b22 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java @@ -138,7 +138,8 @@ public Integer call() { int outputPretty(Map stats) { NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US); - String projectName = path.toAbsolutePath().normalize().getFileName().toString(); + String projectName = java.util.Objects.toString( + path.toAbsolutePath().normalize().getFileName(), "unknown"); out.println(); CliOutput.print(out, "@|bold [=] Code IQ Stats -- " + projectName + "|@"); diff --git a/src/main/java/io/github/randomcodespace/iq/cli/TopologyCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/TopologyCommand.java index 48a40af4..a0259b7d 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/TopologyCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/TopologyCommand.java @@ -79,7 +79,7 @@ public Integer call() { // Check if service nodes exist; if not, run ServiceDetector boolean hasServices = nodes.stream().anyMatch(n -> n.getKind() == NodeKind.SERVICE); if (!hasServices) { - String projectName = root.getFileName().toString(); + String projectName = java.util.Objects.toString(root.getFileName(), "unknown"); var detector = new ServiceDetector(); var result = detector.detect(nodes, edges, projectName); nodes.addAll(result.serviceNodes());