Skip to content

Commit a8ef6b6

Browse files
committed
fix(np-null): eliminate all 26 NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE findings
All 26 SpotBugs NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE findings shared the same shape: calling `.toString()` on `Path.getFileName()` (or once on `Path.getParent()`) where the result can legitimately be null for root-like paths (e.g. `/`, a bare filename with no parent). Uniform fix: wrap every `path.getFileName().toString()` with `java.util.Objects.toString(path.getFileName(), fallback)`, choosing a sensible per-site fallback: - `""` where the string is then substring-matched against a known set (filenames, extensions, excluded-dir names); empty never matches those sets, preserving existing semantics. - `"unknown"` for human-facing project names in TopologyCommand, EnrichCommand service detection, StatsCommand output header. - `"bundle"` for BundleCommand's project-name derivation. - `"flow"` for FlowCommand's html project name. - `PROP_ROOT` (existing constant) in Analyzer. One non-`getFileName` case: AnalysisCache's constructor called `Files.createDirectories(dbPath.getParent())` unconditionally; rewrote as a null-guarded block so a bare-filename dbPath (no directory component) doesn't NPE. Files touched (12): analyzer/Analyzer.java (5 edits — incl. 1 replace_all that covered 3 triplicated blocks in analyzeFileWithRegistry / analyzeFile / createInventoryNode) analyzer/ConfigScanner.java (1) analyzer/FileClassifier.java (1) analyzer/FileDiscovery.java (2) analyzer/ServiceDetector.java (3) cache/AnalysisCache.java (1 — getParent null-guard) cli/BundleCommand.java (1) cli/EnrichCommand.java (2) cli/FlowCommand.java (1) cli/PluginsCommand.java (2) cli/StatsCommand.java (1) cli/TopologyCommand.java (1) Verified: - `mvn compile` clean. - `mvn spotbugs:spotbugs` re-run: NP_NULL count 26 -> 0. - `mvn test` (full suite): 3,059 tests, 0 failures, 0 errors. No behavior change for the common case: for any non-root path, `path.getFileName() != null` and Objects.toString returns the same value as the old `.toString()`. The fallback string is only observed if callers hand in a root-like path — previously an NPE, now a safe sentinel that flows through existing logic.
1 parent ecc224b commit a8ef6b6

13 files changed

Lines changed: 45 additions & 28 deletions

File tree

docs/superpowers/baselines/2026-04-17/BASELINE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ Ordered by severity. Each item cites the raw artifact it was derived from.
231231
- **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).
232232
- Raw: `raw/spotbugs.xml`, `raw/spotbugs-summary.json`.
233233
- **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`.
234+
- **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`.
234235

235236
### Medium
236237

src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
360360
// 5b. Detect service boundaries and create SERVICE nodes
361361
report.accept("Detecting service boundaries...");
362362
var serviceDetector = new ServiceDetector();
363-
String projectDirName = root.getFileName() != null ? root.getFileName().toString() : PROP_ROOT;
363+
String projectDirName = java.util.Objects.toString(root.getFileName(), PROP_ROOT);
364364
var serviceResult = serviceDetector.detect(allNodes, builder.getEdges(), projectDirName, root);
365365
if (!serviceResult.serviceNodes().isEmpty()) {
366366
serviceResult.serviceNodes().forEach(n -> n.setProvenance(builder.getProvenance()));
@@ -885,10 +885,11 @@ private AnalysisResult runSmartWithCache(Path root, Integer parallelism, int bat
885885
} else {
886886
filesSkipped++;
887887
// Zero data loss: create minimal inventory node for filtered files
888+
String filteredFileName = java.util.Objects.toString(file.path().getFileName(), "");
888889
CodeNode fileNode = new CodeNode(
889-
"file:" + file.path() + ":module:" + file.path().getFileName(),
890+
"file:" + file.path() + ":module:" + filteredFileName,
890891
NodeKind.MODULE,
891-
file.path().getFileName().toString());
892+
filteredFileName);
892893
fileNode.setFilePath(file.path().toString());
893894
fileNode.setModule(DetectorUtils.deriveModuleName(file.path().toString(), file.language()));
894895
fileNode.getProperties().put("status", "filtered");
@@ -1130,7 +1131,7 @@ Map<String, List<DiscoveredFile>> detectModules(Path root, List<DiscoveredFile>
11301131
// Collect unique module directories from boundary marker files
11311132
Set<String> moduleDirs = new java.util.TreeSet<>();
11321133
for (DiscoveredFile file : files) {
1133-
if (MODULE_BOUNDARY_MARKERS.contains(file.path().getFileName().toString())) {
1134+
if (MODULE_BOUNDARY_MARKERS.contains(java.util.Objects.toString(file.path().getFileName(), ""))) {
11341135
Path parent = file.path().getParent();
11351136
moduleDirs.add(parent != null ? parent.toString().replace('\\', '/') : "");
11361137
}
@@ -1235,10 +1236,11 @@ DetectorResult analyzeFileWithRegistry(DiscoveredFile file, Path repoPath,
12351236
if (isMinified(file, content)) {
12361237
log.debug("Skipping detectors for minified file: {}", file.path());
12371238
String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language());
1239+
String fileNameStr = java.util.Objects.toString(file.path().getFileName(), "");
12381240
CodeNode node = new CodeNode(
1239-
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()),
1241+
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr),
12401242
NodeKind.MODULE,
1241-
file.path().getFileName().toString());
1243+
fileNameStr);
12421244
node.setFilePath(file.path().toString());
12431245
node.setModule(moduleName);
12441246
node.setProperties(new java.util.LinkedHashMap<>(Map.of("minified", true, "file_type", "minified")));
@@ -1351,7 +1353,7 @@ private BoundedExecutor createExecutor(Integer parallelism) {
13511353
}
13521354

13531355
private boolean isMinified(DiscoveredFile file, String content) {
1354-
String name = file.path().getFileName().toString();
1356+
String name = java.util.Objects.toString(file.path().getFileName(), "");
13551357
boolean nameHint = name.endsWith(".min.js") || name.endsWith(".bundle.js")
13561358
|| name.endsWith(".min.css") || name.endsWith(".min.mjs");
13571359
boolean jsOrCss = name.endsWith(".js") || name.endsWith(".mjs") || name.endsWith(".cjs")
@@ -1434,10 +1436,11 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry
14341436
if (isMinified(file, content)) {
14351437
log.debug("Skipping detectors for minified file: {}", file.path());
14361438
String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language());
1439+
String fileNameStr = java.util.Objects.toString(file.path().getFileName(), "");
14371440
CodeNode node = new CodeNode(
1438-
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()),
1441+
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr),
14391442
NodeKind.MODULE,
1440-
file.path().getFileName().toString());
1443+
fileNameStr);
14411444
node.setFilePath(file.path().toString());
14421445
node.setModule(moduleName);
14431446
node.setProperties(new java.util.LinkedHashMap<>(Map.of("minified", true, "file_type", "minified")));
@@ -1520,10 +1523,11 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry
15201523
*/
15211524
private static DetectorResult createInventoryNode(DiscoveredFile file, String fileType) {
15221525
String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language());
1526+
String fileNameStr = java.util.Objects.toString(file.path().getFileName(), "");
15231527
CodeNode node = new CodeNode(
1524-
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : file.path().getFileName().toString()),
1528+
"file:" + file.path() + ":module:" + (moduleName != null ? moduleName : fileNameStr),
15251529
NodeKind.MODULE,
1526-
file.path().getFileName().toString());
1530+
fileNameStr);
15271531
node.setFilePath(file.path().toString());
15281532
node.setModule(moduleName);
15291533
node.setProperties(new java.util.LinkedHashMap<>(Map.of(

src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private void scanSpringConfig(Path root, InfrastructureRegistry registry) {
9696

9797
for (Path candidate : candidates) {
9898
if (Files.isRegularFile(candidate)) {
99-
String name = candidate.getFileName().toString();
99+
String name = java.util.Objects.toString(candidate.getFileName(), "");
100100
if (name.endsWith(".properties")) {
101101
parseSpringProperties(candidate, registry);
102102
} else {

src/main/java/io/github/randomcodespace/iq/analyzer/FileClassifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public enum FileType {
5454
*/
5555
public static FileType classify(Path relativePath, String language) {
5656
String pathStr = relativePath.toString().replace('\\', '/');
57-
String fileName = relativePath.getFileName().toString();
57+
String fileName = java.util.Objects.toString(relativePath.getFileName(), "");
5858
String ext = getExtension(fileName);
5959

6060
// Binary check first

src/main/java/io/github/randomcodespace/iq/analyzer/FileDiscovery.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ private List<DiscoveredFile> discoverViaWalk(Path root) {
185185
Files.walkFileTree(root, new SimpleFileVisitor<>() {
186186
@Override
187187
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
188-
String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "";
188+
// Objects.toString yields fallback when getFileName() is null (root paths).
189+
String dirName = java.util.Objects.toString(dir.getFileName(), "");
189190
if (DEFAULT_EXCLUDES.contains(dirName)) {
190191
return FileVisitResult.SKIP_SUBTREE;
191192
}
@@ -243,7 +244,7 @@ private boolean isExcluded(Path relPath) {
243244
}
244245

245246
private static boolean isExcludedFilename(Path relPath) {
246-
String filename = relPath.getFileName().toString();
247+
String filename = java.util.Objects.toString(relPath.getFileName(), "");
247248
return EXCLUDED_FILENAMES.contains(filename);
248249
}
249250
}

src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
200200
String filePath = node.getFilePath();
201201
if (filePath == null) continue;
202202

203-
String fileName = Path.of(filePath).getFileName().toString();
203+
String fileName = java.util.Objects.toString(Path.of(filePath).getFileName(), "");
204204
String dirPath = parentDir(filePath);
205205

206206
String buildTool = BUILD_FILES.get(fileName);
@@ -343,7 +343,7 @@ private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String
343343

344344
@Override
345345
public java.nio.file.FileVisitResult preVisitDirectory(Path dir, java.nio.file.attribute.BasicFileAttributes attrs) {
346-
String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "";
346+
String dirName = java.util.Objects.toString(dir.getFileName(), "");
347347
if (SKIP_DIRS.contains(dirName)) {
348348
return java.nio.file.FileVisitResult.SKIP_SUBTREE;
349349
}
@@ -352,7 +352,7 @@ public java.nio.file.FileVisitResult preVisitDirectory(Path dir, java.nio.file.a
352352

353353
@Override
354354
public java.nio.file.FileVisitResult visitFile(Path file, java.nio.file.attribute.BasicFileAttributes attrs) {
355-
String name = file.getFileName().toString();
355+
String name = java.util.Objects.toString(file.getFileName(), "");
356356
boolean isBuildFile = BUILD_FILES.containsKey(name)
357357
|| name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
358358
|| name.endsWith(VBPROJ_EXTENSION) || name.endsWith(GEMSPEC_EXTENSION)

src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ public AnalysisCache(Path dbPath, boolean readOnly) {
126126
this.dbPath = dbPath;
127127
try {
128128
if (!readOnly) {
129-
Files.createDirectories(dbPath.getParent());
129+
// dbPath.getParent() is null for a bare filename (no directory
130+
// component). Treat "already a directory" as nothing to create.
131+
Path parent = dbPath.getParent();
132+
if (parent != null) {
133+
Files.createDirectories(parent);
134+
}
130135
}
131136
// Strip .db extension if present — H2 appends its own .mv.db
132137
String dbFile = dbPath.toString();

src/main/java/io/github/randomcodespace/iq/cli/BundleCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public Integer call() {
115115
return 1;
116116
}
117117

118-
String projectName = root.getFileName().toString();
118+
String projectName = java.util.Objects.toString(root.getFileName(), "bundle");
119119
String bundleTag = tag != null ? tag : "latest";
120120

121121
Path zipPath = output != null ? output

src/main/java/io/github/randomcodespace/iq/cli/EnrichCommand.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ public Integer call() {
9797

9898
// 1. Open H2 file
9999
Path cachePath = root.resolve(config.getCacheDir()).resolve("analysis-cache.db");
100-
if (!Files.exists(cachePath.getParent())) {
101-
CliOutput.error("No index found at " + cachePath.getParent());
100+
// cachePath.getParent() is always non-null here because we resolve off
101+
// `root` (a directory), but null-guard explicitly for SpotBugs and to
102+
// protect against a future refactor that changes the resolution.
103+
Path cacheParent = cachePath.getParent();
104+
if (cacheParent == null || !Files.exists(cacheParent)) {
105+
CliOutput.error("No index found at " + cacheParent);
102106
CliOutput.info(" Run 'code-iq index " + root + "' first.");
103107
return 1;
104108
}
@@ -180,7 +184,7 @@ private int enrichFromCache(AnalysisCache cache, Path root, NumberFormat nf, Ins
180184
CliOutput.step("[^]", "Detecting service boundaries...");
181185
stepStart = Instant.now();
182186
var serviceDetector = new io.github.randomcodespace.iq.analyzer.ServiceDetector();
183-
String projectName = root.getFileName().toString();
187+
String projectName = java.util.Objects.toString(root.getFileName(), "unknown");
184188
var serviceResult = serviceDetector.detect(enrichedNodes, enrichedEdges, projectName, root);
185189
if (!serviceResult.serviceNodes().isEmpty()) {
186190
serviceResult.serviceNodes().forEach(n -> n.setProvenance(builder.getProvenance()));

src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ public Integer call() {
7676
String content;
7777

7878
if ("html".equalsIgnoreCase(format)) {
79-
String projectName = path.toAbsolutePath().getFileName().toString();
79+
String projectName = java.util.Objects.toString(
80+
path.toAbsolutePath().getFileName(), "flow");
8081
content = engine.renderInteractive(projectName);
8182
} else {
8283
FlowDiagram diagram = engine.generate(view.toLowerCase());

0 commit comments

Comments
 (0)