Skip to content

Fix Java PSI resolution crash in analysis#12

Merged
ClankerGuru merged 1 commit intomainfrom
fix/java-psi-resolution-crash
Apr 13, 2026
Merged

Fix Java PSI resolution crash in analysis#12
ClankerGuru merged 1 commit intomainfrom
fix/java-psi-resolution-crash

Conversation

@ClankerGuru
Copy link
Copy Markdown
Owner

@ClankerGuru ClankerGuru commented Apr 13, 2026

Summary

  • Root cause: annotation.qualifiedName in resolveJavaTypeInfo triggers PsiJavaCodeReferenceElement.resolve()JavaPsiFacade.findClass() → Kotlin JavaElementFinderCliKotlinAsJavaSupportCliTraceHolder.getModule() — a lateinit property that is only initialized during compilation, not in srcx's parsing-only KotlinCoreEnvironment. This crashes every project that contains Java source files.
  • Fix: Use nameReferenceElement?.referenceName instead of qualifiedName — reads the annotation name text without triggering type resolution.
  • Safety net: Wrap parseJavaFile in runCatching so any remaining resolution-triggering operation logs a warning with full stack trace and skips the file instead of failing the entire project analysis.

Context

The previous PR (#11) fixed a real but separate thread-safety issue. This PR fixes the actual root cause seen in repos with Java source files.

Test plan

  • ./gradlew build passes (detekt, ktlint, tests, kover)
  • Run srcx-context on a repo with Java source files to confirm no lateinit errors

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for Java file parsing with comprehensive logging and error messages for better diagnostics.
  • Improvements

    • Enhanced Java annotation extraction for more reliable source file metadata analysis.

annotation.qualifiedName triggers PsiJavaCodeReferenceElement.resolve()
which goes through JavaPsiFacade → Kotlin JavaElementFinder →
CliKotlinAsJavaSupport → CliTraceHolder.module — a lateinit property
that is only set during compilation, not in srcx's parsing-only
environment. This crashes every project containing Java source files.

Use nameReferenceElement.referenceName instead, which reads the
annotation name text without triggering type resolution. Also wrap
parseJavaFile in runCatching so any remaining resolution-triggering
operation logs a warning and skips the file instead of failing the
entire project analysis.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Updated Java annotation extraction logic in source file metadata parsing by changing from qualified name extraction to reference element name retrieval. Added centralized error handling and logging for Java file parsing operations using runCatching.

Changes

Cohort / File(s) Summary
Java Parsing & Error Handling
src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt
Modified annotation extraction in resolveJavaTypeInfo to use nameReferenceElement?.referenceName instead of qualifiedName?.substringAfterLast('.'). Added javaParseLogger and wrapped parseJavaFile logic in runCatching, with getOrNull() for graceful failure and onFailure for warning logging that includes filename, exception message, and stack trace.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 Exceptions caught with gentle care,
Logs reveal what's hidden there,
Names extracted with new finesse,
Error handling—nothing less! 📋✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix Java PSI resolution crash in analysis' directly and clearly summarizes the main change: fixing a crash in Java PSI resolution during analysis.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/java-psi-resolution-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt (1)

231-231: Add a Java-annotation regression test for this fix.

SourceFileMetadataTest currently exercises only the unannotated Java happy path, so this resolution-avoidance change can regress silently. A small annotated Java fixture would pin both the simple-name behavior and the no-resolution requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt` at
line 231, Add a regression test to SourceFileMetadataTest that includes a small
Java source fixture with at least one annotation (e.g., `@MyAnno`) to verify the
SourceFileMetadata path that uses annotations = cls.annotations.mapNotNull {
it.nameReferenceElement?.referenceName } preserves simple-name values and does
not trigger PSI resolution; the test should load the Java file, invoke the same
metadata extraction used by SourceFileMetadata (or call the helper that builds
annotations from cls), and assert the resulting annotations list contains the
simple annotation name and that no resolution-dependent behavior occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt`:
- Around line 244-279: Move the LightVirtualFile creation and
psiManager.findFile(...) calls inside a try block within parseJavaFile and
replace the runCatching usage with an explicit try { ... } catch (e: Exception)
{ ... } so I/O or charset errors are handled by the fallback and fatal
Throwables are not swallowed; apply the same change to parseKotlinFile (ensure
you catch Exception, log via javaParseLogger.warn with the exception, and return
null on failure).

---

Nitpick comments:
In `@src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt`:
- Line 231: Add a regression test to SourceFileMetadataTest that includes a
small Java source fixture with at least one annotation (e.g., `@MyAnno`) to verify
the SourceFileMetadata path that uses annotations = cls.annotations.mapNotNull {
it.nameReferenceElement?.referenceName } preserves simple-name values and does
not trigger PSI resolution; the test should load the Java file, invoke the same
metadata extraction used by SourceFileMetadata (or call the helper that builds
annotations from cls), and assert the resulting annotations list contains the
simple annotation name and that no resolution-dependent behavior occurs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 447b097c-63e9-4ff5-ae2d-d2d29e983539

📥 Commits

Reviewing files that changed from the base of the PR and between 3a1f363 and 14fb822.

📒 Files selected for processing (1)
  • src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt

Comment on lines 244 to +279
private fun parseJavaFile(file: File, psiManager: PsiManager): SourceFileMetadata? {
val vf = LightVirtualFile(file.name, JavaFileType.INSTANCE, file.readText())
val javaFile = psiManager.findFile(vf) as? PsiJavaFile ?: return null

val packageName = javaFile.packageName
val imports =
javaFile.importList
?.importStatements
?.mapNotNull { it.qualifiedName }
?.filter { fqn -> PLATFORM_PREFIXES.none { fqn.startsWith(it) } }
?: emptyList()
return runCatching {
val packageName = javaFile.packageName
val imports =
javaFile.importList
?.importStatements
?.mapNotNull { it.qualifiedName }
?.filter { fqn -> PLATFORM_PREFIXES.none { fqn.startsWith(it) } }
?: emptyList()

val info = resolveJavaTypeInfo(javaFile, file.nameWithoutExtension)
val qualifiedName = if (packageName.isNotEmpty()) "$packageName.${info.className}" else info.className
val info = resolveJavaTypeInfo(javaFile, file.nameWithoutExtension)
val qualifiedName = if (packageName.isNotEmpty()) "$packageName.${info.className}" else info.className

return SourceFileMetadata(
file = file,
packageName = packageName,
qualifiedName = qualifiedName,
simpleName = info.className,
imports = imports,
annotations = info.annotations,
supertypes = info.supertypes,
isInterface = info.isInterface,
isAbstract = info.isAbstract,
isObject = false,
isDataClass = false,
language = SourceFileMetadata.Language.JAVA,
lineCount = countCodeLines(file.readLines()),
methods = info.methods,
declarationLine = info.declarationLine,
)
SourceFileMetadata(
file = file,
packageName = packageName,
qualifiedName = qualifiedName,
simpleName = info.className,
imports = imports,
annotations = info.annotations,
supertypes = info.supertypes,
isInterface = info.isInterface,
isAbstract = info.isAbstract,
isObject = false,
isDataClass = false,
language = SourceFileMetadata.Language.JAVA,
lineCount = countCodeLines(file.readLines()),
methods = info.methods,
declarationLine = info.declarationLine,
)
}.onFailure { e ->
javaParseLogger.warn("srcx: Failed to parse Java file '${file.name}': ${e.message}", e)
}.getOrNull()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and inspect the file
cd /tmp/repo 2>/dev/null || cd / || true
find . -name "SourceFileMetadata.kt" -type f 2>/dev/null | head -5

Repository: ClankerGuru/srcx

Length of output: 151


🏁 Script executed:

# Let's check the actual code at the specified lines
if [ -f "src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt" ]; then
  wc -l "src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt"
  sed -n '244,279p' "src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt"
fi

Repository: ClankerGuru/srcx

Length of output: 1681


🌐 Web query:

Kotlin stdlib runCatching catches Throwable or Exception documentation

💡 Result:

Kotlin stdlib's runCatching catches Throwable, not just Exception. It calls the specified function block and returns its encapsulated result if successful, catching any Throwable thrown from the block and encapsulating it as a Result.Failure. This has been the case since Kotlin 1.3 and remains current in the latest documentation.

Citations:


🏁 Script executed:

# Check for similar parsing patterns and usage context
rg "parseJavaFile|parseKotlinFile|runCatching" src/main/kotlin/zone/clanker/gradle/srcx/analysis/ -B 2 -A 2

Repository: ClankerGuru/srcx

Length of output: 2957


🏁 Script executed:

# Check if there's a corresponding parseKotlinFile to understand the pattern
grep -n "fun parse" src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt | head -10

Repository: ClankerGuru/srcx

Length of output: 380


Move file setup inside the try block and use catch (Exception) instead of runCatching.

The file.readText() and psiManager.findFile() calls at lines 245–246 are outside the error handler, so I/O or charset failures will crash the analysis before the fallback runs. Additionally, runCatching catches Throwable, which suppresses fatal JVM errors (OutOfMemoryError, StackOverflowError) that should never be caught.

Proposed fix
 private fun parseJavaFile(file: File, psiManager: PsiManager): SourceFileMetadata? {
-    val vf = LightVirtualFile(file.name, JavaFileType.INSTANCE, file.readText())
-    val javaFile = psiManager.findFile(vf) as? PsiJavaFile ?: return null
-
-    return runCatching {
+    return try {
+        val vf = LightVirtualFile(file.name, JavaFileType.INSTANCE, file.readText())
+        val javaFile = psiManager.findFile(vf) as? PsiJavaFile ?: return null
-    }.onFailure { e ->
-        javaParseLogger.warn("srcx: Failed to parse Java file '${file.name}': ${e.message}", e)
-    }.getOrNull()
+    } catch (e: Exception) {
+        javaParseLogger.warn("srcx: Failed to parse Java file '${file.path}': ${e.message}", e)
+        null
+    }
 }

Apply the same fix to parseKotlinFile (line 158).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/gradle/srcx/analysis/SourceFileMetadata.kt`
around lines 244 - 279, Move the LightVirtualFile creation and
psiManager.findFile(...) calls inside a try block within parseJavaFile and
replace the runCatching usage with an explicit try { ... } catch (e: Exception)
{ ... } so I/O or charset errors are handled by the fallback and fatal
Throwables are not swallowed; apply the same change to parseKotlinFile (ensure
you catch Exception, log via javaParseLogger.warn with the exception, and return
null on failure).

@ClankerGuru ClankerGuru merged commit 4c98e3d into main Apr 13, 2026
2 checks passed
@ClankerGuru ClankerGuru deleted the fix/java-psi-resolution-crash branch April 13, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant