Skip to content

ADFA-4387: add global analysis lock for Kotlin analysis#1428

Open
itsaky-adfa wants to merge 3 commits into
stagefrom
fix/ADFA-4387
Open

ADFA-4387: add global analysis lock for Kotlin analysis#1428
itsaky-adfa wants to merge 3 commits into
stagefrom
fix/ADFA-4387

Conversation

@itsaky-adfa

@itsaky-adfa itsaky-adfa commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

See ADFA-4387 for more details.

Signed-off-by: Akash Yadav <itsaky01@gmail.com>
@itsaky-adfa itsaky-adfa requested a review from a team June 19, 2026 16:59
@itsaky-adfa itsaky-adfa self-assigned this Jun 19, 2026
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
  • Introduced a global Kotlin Analysis lock (ReentrantLock) to serialize Kotlin Analysis API entry points and prevent concurrent lifetime/session corruption.
  • Added internal withAnalysisLock { ... } in KtFileExts.kt, and updated analyzeMaybeDangling so the analyze/analyzeCopy selection and execution run under the lock.
  • Updated completion computation (KotlinCompletions.kt) to rely on analyzeMaybeDangling instead of direct/conditional analyzeCopy, consolidating analysis entry-point usage.
  • Improved diagnostics handling to avoid carrying live KaDiagnosticWithPsi objects across lifetimes:
    • KotlinDiagnosticProvider now stores only unresolved-reference name data (String?) + env as diagnostic “extra”.
    • AddImportAction now reads unresolved-reference data via KotlinDiagnosticExtra.unresolvedReference (no KaFirDiagnostic cast).
  • Fixed a write-lock race condition by moving CompilationEnvironment.notifyElementModifiedForPath’s handleElementModification(...) inside project.write { ... } to avoid session mutation races with concurrent analysis.
  • Added AnalysisSerializationTest to verify:
    • concurrent analyzeMaybeDangling calls do not throw lifetime/context errors
    • overlapping analyses are effectively serialized (max in-flight == 1)
  • Documentation/best-practice update: clarified in KtFileExts.kt that code executed under withAnalysisLock must not call project.write, due to non-upgradeable read→write lock constraints.
  • Risks / best-practice callouts
    • CRITICAL contract: Kotlin Analysis API access must go through withAnalysisLock / analyzeMaybeDangling; bypassing direct analyze/analyzeCopy can break the safety guarantee.
    • Potential throughput impact: the global lock may reduce responsiveness under high concurrency (e.g., simultaneous analysis-driven features like completions/diagnostics).

Walkthrough

A ReentrantLock and withAnalysisLock helper are added to KtFileExts.kt to serialize Kotlin Analysis API access. analyzeMaybeDangling and KotlinCompletions are wrapped with the lock. KotlinDiagnosticProvider extracts unresolved-reference data primitively inside the analysis context, stored in KotlinDiagnosticExtra to avoid lifetime issues. AddImportAction is refactored to use extracted reference data. CompilationEnvironment.notifyElementModifiedForPath moves session mutations inside the write lock to prevent races with concurrent reads. Two regression tests validate serialization under concurrent load.

Changes

Analysis API Concurrency Serialization and Lifetime Safety

Layer / File(s) Summary
Analysis lock infrastructure and withAnalysisLock helper
lsp/kotlin/.../compiler/modules/KtFileExts.kt
Adds ReentrantLock and kotlin.concurrent.withLock imports; defines private analysisLock and creates internal withAnalysisLock helper with KDoc specifying required usage, reentrancy semantics, and deadlock avoidance constraints.
Serialized analysis in completions and analyzeMaybeDangling
lsp/kotlin/.../compiler/modules/KtFileExts.kt, lsp/kotlin/.../completion/KotlinCompletions.kt
Wraps analyzeMaybeDangling analyze/analyzeCopy branching in withAnalysisLock; imports and applies lock in KotlinCompletions.doComplete around analyzeCopy block to ensure mutual exclusion.
Safe diagnostic data extraction and AddImportAction integration
lsp/kotlin/.../diagnostic/KotlinDiagnosticProvider.kt, lsp/kotlin/.../actions/AddImportAction.kt
KotlinDiagnosticProvider extracts unresolvedReference (primitive String) from KaFirDiagnostic.UnresolvedReference inside analysis context; KotlinDiagnosticExtra updated to store this primitive instead of diagnostic wrapper; AddImportAction refactored to extract and use reference from KotlinDiagnosticExtra.
Write lock ordering for concurrent session mutations
lsp/kotlin/.../compiler/CompilationEnvironment.kt
Moves KaSourceModificationService.handleElementModification inside project.write block with crossinline typeProvider parameter; write lock now covers session state changes to prevent races with concurrent analyze calls.
Concurrency regression tests
lsp/kotlin/.../compiler/modules/AnalysisSerializationTest.kt
Introduces two regression tests: first spawns many concurrent coroutines performing analyzeMaybeDangling and asserts no KaInaccessibleLifetimeOwnerAccessException; second measures in-flight analysis count via atomics and validates maximum observed concurrency is exactly 1.

Sequence Diagram

sequenceDiagram
  participant Thread1 as Thread 1
  participant Thread2 as Thread 2
  participant withAnalysisLock
  participant ReentrantLock
  participant KaSession

  Thread1->>withAnalysisLock: analyzeMaybeDangling call
  Thread2->>withAnalysisLock: analyzeMaybeDangling call (concurrent)
  withAnalysisLock->>ReentrantLock: acquire lock
  ReentrantLock-->>withAnalysisLock: acquired
  withAnalysisLock->>KaSession: analyzeCopy/analyze
  KaSession-->>withAnalysisLock: session result
  ReentrantLock->>Thread2: waiting...
  withAnalysisLock->>ReentrantLock: release lock
  withAnalysisLock-->>Thread1: return result
  ReentrantLock-->>Thread2: lock acquired
  Thread2->>withAnalysisLock: proceed with analysis
  withAnalysisLock->>KaSession: analyzeCopy/analyze
  KaSession-->>withAnalysisLock: session result
  withAnalysisLock-->>Thread2: return result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#1214: Updates indexing flows to run analysis inside analyzeMaybeDangling, directly relying on the serialization guard and safe data extraction patterns introduced in this PR.
  • appdevforall/CodeOnTheGo#1112: Both PRs touch Kotlin diagnostics and analysis API usage; the retrieved PR's KotlinDiagnosticProvider analysis path is affected by the new locking behavior and safe data extraction patterns introduced here.

Suggested reviewers

  • dara-abijo-adfa
  • jatezzz

Poem

🐇 A lock hops through the analysis stack,
One thread at a time—no turning back,
Diagnostic data flows out safe and clean,
No lifetime ghosts, no cryptic scene,
The mutex guards each KaSession's dance,
Concurrent calls get their rightful chance! 🔒

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a global analysis lock for Kotlin analysis, which is the core objective across all modified files.
Description check ✅ Passed The description is related to the changeset, referencing an external issue tracker that contextualizes the change. It provides meaningful information about the PR's objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/ADFA-4387

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt`:
- Around line 150-153: Move the withAnalysisLock block outside and after the
env.project.read section to avoid deadlock risks from nested lock acquisition.
Since completionKtFile is already created in the earlier project.read block
(lines 137-145) and its content is stable, remove the second env.project.read
block entirely. Keep only the withAnalysisLock call wrapping the analysis work
that follows, ensuring the analysis lock is acquired after the project read lock
is released rather than held concurrently.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 491ec53b-f447-412a-86a2-ec1c9d81839e

📥 Commits

Reviewing files that changed from the base of the PR and between d73ec0b and d8c32bd.

📒 Files selected for processing (2)
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt

Comment on lines +150 to +153
env.project.read {
abortIfCancelled()

analyzeCopy(
useSiteElement = completionKtFile,
resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF,
) {
val ctx =
resolveAnalysisContext(
env = env,
file = params.file,
ktFile = completionKtFile,
offset = completionOffset,
partial = partial
)

if (ctx == null) {
logger.error(
"Unable to determine context at offset {} in file {}",
completionOffset,
params.file
)
return@analyzeCopy CompletionResult.EMPTY
}

abortIfCancelled()
context(ctx) {
val items = mutableListOf<CompletionItem>()
val completionContext = determineCompletionContext(ctx.psiElement)
when (completionContext) {
CompletionContext.Scope ->
collectScopeCompletions(to = items)

CompletionContext.Member ->
collectMemberCompletions(to = items)
withAnalysisLock {

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
# Description: Audit lock/read ordering around analysis entry points.
# Expected: the project consistently uses one ordering policy and avoids waiting
# for withAnalysisLock while already holding long-lived project read sections.

rg -nP --glob '*.kt' -C8 '\bwithAnalysisLock\s*\{|\banalyzeMaybeDangling\s*\(|\bread\s*\{|\b(?:analyze|analyzeCopy)\s*\('

Repository: appdevforall/CodeOnTheGo

Length of output: 50380


🏁 Script executed:

# Get the exact code around lines 150-160 in KotlinCompletions.kt
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt | sed -n '145,175p'

Repository: appdevforall/CodeOnTheGo

Length of output: 925


🏁 Script executed:

# Check what withAnalysisLock is - search for its definition
rg -n 'fun withAnalysisLock|inline fun withAnalysisLock' lsp/kotlin/

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Check for other places where project.read and withAnalysisLock/analyzeMaybeDangling are nested
rg -n '\bproject\.read\s*\{' lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ -A 10 | head -60

Repository: appdevforall/CodeOnTheGo

Length of output: 2619


🏁 Script executed:

# Check how analyzeMaybeDangling and analyzeCopy acquire locks
rg -n 'fun analyzeMaybeDangling|fun analyzeCopy|suspend fun analyzeMaybeDangling' lsp/kotlin/ -A 3

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Search for withAnalysisLock definition in all code
rg -n 'fun withAnalysisLock|suspend fun.*withAnalysisLock' -t kotlin

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Check imports in KotlinCompletions.kt to find where withAnalysisLock comes from
head -50 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt | grep -E 'import|package'

Repository: appdevforall/CodeOnTheGo

Length of output: 3108


🏁 Script executed:

# Search for analyzeCopy to see where it's defined
rg -n 'fun analyzeCopy|suspend fun.*analyzeCopy' -t kotlin

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Find the withAnalysisLock implementation
find lsp/kotlin/src -name "*.kt" -type f | xargs grep -l "fun withAnalysisLock"

Repository: appdevforall/CodeOnTheGo

Length of output: 52


🏁 Script executed:

# Check if there's a modules file
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules.kt | head -100

Repository: appdevforall/CodeOnTheGo

Length of output: 178


🏁 Script executed:

# Locate kotlin compiler modules files
fd -t f "modules" lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ --extension kt

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Search for withAnalysisLock in all kotlin files
rg "fun withAnalysisLock" lsp/kotlin/src/main/java --type kotlin -B 2 -A 8

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Alternative: check all .kt files in compiler directory
fd -t f . lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ --extension kt | head -20

Repository: appdevforall/CodeOnTheGo

Length of output: 1887


🏁 Script executed:

# Search for withAnalysisLock in the modules directory
rg "withAnalysisLock" lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/ -B 2 -A 8

Repository: appdevforall/CodeOnTheGo

Length of output: 1657


🏁 Script executed:

# Also check CompilerExts.kt since that might have extension functions
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 410


🏁 Script executed:

# Find the analysisLock definition in KtFileExts.kt
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt | head -80

Repository: appdevforall/CodeOnTheGo

Length of output: 2974


🏁 Script executed:

# Search for analysisLock property/variable
rg "analysisLock\s*=" lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/ -B 2 -A 2

Repository: appdevforall/CodeOnTheGo

Length of output: 653


🏁 Script executed:

# Check how diagnostic provider handles the lock ordering
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt | sed -n '50,95p'

Repository: appdevforall/CodeOnTheGo

Length of output: 1636


🏁 Script executed:

# Search for other completion patterns to see if there's a better approach
rg "withAnalysisLock.*project\.read|project\.read.*withAnalysisLock" lsp/kotlin/src/main/java --type kotlin -B 3 -A 3

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Check if there are places where the locks are acquired in the opposite order (analysis first, then read)
rg "withAnalysisLock\s*\{" lsp/kotlin/src/main/java --type kotlin -B 5 | grep -A 5 "withAnalysisLock" | head -60

Repository: appdevforall/CodeOnTheGo

Length of output: 1190


🏁 Script executed:

# Check what project.read is - find its definition
rg "fun read|inline fun read" lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ -B 2 -A 8

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Check if there are examples of the opposite order (analysis lock acquired first)
rg "analyzeMaybeDangling|withAnalysisLock" lsp/kotlin/src/main/java --type kotlin -B 8 | grep -E "project\.read|read\s*\{|withAnalysisLock|analyzeMaybeDangling" | head -40

Repository: appdevforall/CodeOnTheGo

Length of output: 1870


🏁 Script executed:

# Check ReadWriteLock.kt to understand the read/write mechanism
cat -n lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 899


🏁 Script executed:

# Check if there are any other analysis callers to understand the broader pattern
rg "project\.read" lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin --type kotlin -A 10 | grep -E "project\.read|withAnalysisLock|analyzeMaybeDangling|analyzeCopy" | head -50

Repository: appdevforall/CodeOnTheGo

Length of output: 3406


🏁 Script executed:

# Verify the exact issue: can read lock block writes while waiting for analysis lock?
# Check if there are write operations that might be blocked
rg "project\.write" lsp/kotlin/src/main/java --type kotlin -B 2 -A 5 | head -40

Repository: appdevforall/CodeOnTheGo

Length of output: 3296


Move withAnalysisLock outside the env.project.read section.

Lines 150–153 acquire the analysis lock while holding the project read lock. If indexing or diagnostics already holds the analysis lock, this completion request remains idle while holding the read lock, starving pending writes and blocking other read-lock requesters. The same problematic pattern appears in KotlinDiagnosticProvider.kt (lines 59–78) and SourceFileIndexer.kt.

Since completionKtFile is created within a prior project.read block (lines 137–145) and its content is stable, the second read section can be removed entirely and withAnalysisLock can wrap just the analysis work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt`
around lines 150 - 153, Move the withAnalysisLock block outside and after the
env.project.read section to avoid deadlock risks from nested lock acquisition.
Since completionKtFile is already created in the earlier project.read block
(lines 137-145) and its content is stable, remove the second env.project.read
block entirely. Keep only the withAnalysisLock call wrapping the analysis work
that follows, ensuring the analysis lock is acquired after the project read lock
is released rather than held concurrently.

Signed-off-by: Akash Yadav <itsaky01@gmail.com>

@hal-eisen-adfa hal-eisen-adfa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated high-effort review (code-review, recall-biased). The global analysisLock is a reasonable fix for the reported crash — verifier-confirmed root cause is concurrent analyze from indexing + diagnostics running under the shared project.read (the RW lock allows multiple readers), so serializing analyze closes that gap.

However, two confirmed, still-open instances of the same KaInaccessibleLifetimeOwnerAccessException class are not fixed by this PR. They're in files this PR doesn't touch, so I couldn't attach them inline — flagging here:


🟥 1. Escaped lifetime-owner: diagnostics → AddImportAction (CONFIRMED)
KotlinDiagnosticProvider.kt:~83 stores the live KaDiagnosticWithPsi (a KaLifetimeOwner) into DiagnosticItem.extra (KotlinDiagnosticExtra(diagnostic, env)) instead of extracting primitives inside the analyze block. Later AddImportAction.prepare() (AddImportAction.kt:56) and execAction() (:80) read diagnostic.reference outside any analyze block. prepare() runs whenever the code-actions menu is built for a Kotlin file with an unresolved-reference diagnostic (action is registered via KotlinCodeActionsMenuKotlinLanguageServer). A lock can't keep an escaped token valid → same exception, from the quick-fix path. Fix: extract the reference string/FqName (plain data) inside the analyze block; don't store Ka* lifetime owners in extra.

🟥 2. handleElementModification outside the write lock (CONFIRMED)
CompilationEnvironment.kt:218-221 (notifyElementModifiedForPath, used by onFileCreated/onFileRemoved) calls KaSourceModificationService.handleElementModification(...) before the project.write { block opens at line 226. That session mutation is serialized by neither the RW lock nor the new analysisLock, so it can race a concurrent analyze (which holds only project.read) → the same lifetime/session corruption. (onFileContentChanged at :283 correctly keeps it inside project.write.) Fix: move the call inside project.write {} (or take analysisLock around it).


If the goal is to eliminate this exception class (not just the indexing-vs-diagnostics race), #1 and #2 should be addressed too. The inline comments below cover responsiveness/altitude/cleanup on the changed lines.

* this helper (or [analyzeMaybeDangling], which already does); never call `analyze` / `analyzeCopy`
* directly, or the serialization guarantee is lost.
*/
internal inline fun <R> withAnalysisLock(action: () -> R): R = analysisLock.withLock(action)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟧 Responsiveness / pool starvation. This is a blocking ReentrantLock held across the entire analyze block and acquired from coroutines on Dispatchers.Default. Indexing (full-file declaration traversal) and diagnostics (collectDiagnostics(EXTENDED_AND_COMMON_CHECKERS) — slow, uncancellable) hold it for the whole resolution, so a user completion request parks behind background batch work; abortIfCancelled() can't interrupt a pending lock.lock(). Because the wait blocks the underlying Default worker thread (bounded ~Ncpu, 2–4 on low-end devices), several contending analyses pin most of the pool while blocked. Net effect: previously-parallel indexing/diagnostics/completion become fully serial and interactive latency regresses. Consider a kotlinx.coroutines.sync.Mutex (suspends instead of blocking the thread) and/or holding the lock only around session entry rather than the full resolution.

* Holding this lock around every analysis entry point makes analyses mutually exclusive. It is a
* [ReentrantLock] so an (indirect) nested analysis on the same thread cannot deadlock.
*/
private val analysisLock = ReentrantLock()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟨 Altitude — second global lock overlaps the existing one. ReadWriteLock.kt's lock is already a single top-level ReentrantReadWriteLock shared into every project (verified), i.e. process-global. The root cause is that analyze runs under its read (shared) side. Running analysis under an exclusive section of that existing lock (or a per-CompilationEnvironment analysis lock) would fix concurrent-analyze without introducing a second global lock to reason about. Footgun: with two locks, any future analyze block that calls project.write while holding analysisLock + read would self-deadlock (the RW lock is non-upgradeable).


/**
* Runs [action] while holding the shared [analysisLock]. **All** Analysis API access must go through
* this helper (or [analyzeMaybeDangling], which already does); never call `analyze` / `analyzeCopy`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟨 Convention isn't enforced. "Never call analyze/analyzeCopy directly" is documentation-only; nothing stops a new feature (hover, go-to-def, a new code action) from calling analyze() or reading a Ka* result without the lock — it compiles, passes single-threaded tests, and crashes intermittently under concurrent indexing. Findings #1/#2 in the review summary are existing instances. Making analyze/analyzeCopy reachable only through the locked helper (and returning extracted data, not lifetime owners) would make the guarantee structural rather than by-convention.


CompletionContext.Member ->
collectMemberCompletions(to = items)
withAnalysisLock {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟦 Cleanup — duplicates analyzeMaybeDangling. completionKtFile is a dangling copy, so analyzeMaybeDangling(completionKtFile) { ... } would take the identical analyzeCopy(PREFER_SELF) branch and already holds the lock. Calling it here would drop the manual withAnalysisLock, the direct analyzeCopy, the extra import, and one nesting level — and avoid drift if the dangling-resolution policy changes in the helper but not here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt (1)

15-31: ⚡ Quick win

Consider adding a test for lock reentrancy.

The KDoc in KtFileExts.kt notes that analysisLock is a ReentrantLock specifically so "an (indirect) nested analysis on the same thread cannot deadlock." While these tests validate mutual exclusion and lifetime exception prevention, they don't verify that nested or reentrant analysis calls work correctly.

If the lock were later changed to a non-reentrant implementation, these tests would still pass but nested analysis scenarios would deadlock.

Example test structure
`@Test`
fun `nested analyzeMaybeDangling calls do not deadlock`(): Unit = runBlocking {
  val file = createSourceFile("Nested.kt", "class Outer { class Inner }")
  
  env.project.read {
    analyzeMaybeDangling(file) {
      // Outer analysis
      file.declarations.first().symbol
      
      // Nested analysis on same thread
      analyzeMaybeDangling(file) {
        file.declarations.first().symbol
      }
    }
  }
  // If lock is non-reentrant, this would deadlock
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt`
around lines 15 - 31, The AnalysisSerializationTest class currently validates
mutual exclusion and lifetime exception prevention but does not test that nested
or reentrant analyzeMaybeDangling calls work correctly without deadlocking. Add
a new test method to the AnalysisSerializationTest class that verifies
reentrancy by calling analyzeMaybeDangling from within another
analyzeMaybeDangling block on the same thread, ensuring the ReentrantLock
behavior is preserved. This test should complete without deadlock and
demonstrate that the analysisLock reentrant implementation allows nested
analysis calls to proceed safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt`:
- Around line 15-31: The AnalysisSerializationTest class currently validates
mutual exclusion and lifetime exception prevention but does not test that nested
or reentrant analyzeMaybeDangling calls work correctly without deadlocking. Add
a new test method to the AnalysisSerializationTest class that verifies
reentrancy by calling analyzeMaybeDangling from within another
analyzeMaybeDangling block on the same thread, ensuring the ReentrantLock
behavior is preserved. This test should complete without deadlock and
demonstrate that the analysisLock reentrant implementation allows nested
analysis calls to proceed safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb359812-c8e8-4308-87b4-6ec25f69befd

📥 Commits

Reviewing files that changed from the base of the PR and between d8c32bd and dc471d3.

📒 Files selected for processing (1)
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt

… completion cleanup)

Apply the actionable items from Hal's review on PR #1428:

- Diagnostics: extract the unresolved-reference name inside the analyze block
  instead of storing the live KaDiagnosticWithPsi (a KaLifetimeOwner) in
  DiagnosticItem.extra. AddImportAction now reads the pre-extracted string,
  preventing KaInaccessibleLifetimeOwnerAccessException from the quick-fix path.
- CompilationEnvironment.notifyElementModifiedForPath: run
  handleElementModification inside project.write so the session mutation can't
  race a concurrent analyze (mirrors onFileContentChanged).
- KotlinCompletions: collapse manual withAnalysisLock + analyzeCopy into
  analyzeMaybeDangling, removing the only in-prod direct analyzeCopy call.
- KtFileExts: document that code under withAnalysisLock must not call
  project.write (non-upgradeable RW lock footgun).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt`:
- Around line 213-219: The PSI and module resolution calls in the code block
(involving psiManager.findFile, structureProvider.getModule, and
findModuleForSourceId) are executing without read lock protection, creating a
race condition vulnerability since concurrent analyze operations hold read
locks. Wrap the entire code block from structureProvider.getInstance through the
module assignment in a project.read block, following the same pattern used in
the loadKtFile method which correctly protects its psiManager.findFile call with
project.read.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ac09d2a-2484-41fe-af4b-6bcd7daa70e1

📥 Commits

Reviewing files that changed from the base of the PR and between dc471d3 and 90e5471.

📒 Files selected for processing (5)
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt

Comment on lines 213 to 219
val structureProvider = ProjectStructureProvider.getInstance(project)
val ktFile = path.toVirtualFileOrNull()?.let {
psiManager.findFile(it) as? KtFile
}

if (ktFile != null) {
KaSourceModificationService.getInstance(project)
.handleElementModification(ktFile, typeProvider(ktFile))
}

val module = (ktFile?.let { structureProvider.getModule(it, null) }
?: structureProvider.findModuleForSourceId(path.pathString)) as? AbstractKtModule

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how psiManager.findFile / getModule are guarded elsewhere in this module.
rg -nP -C4 '\b(psiManager\.findFile|structureProvider\.getModule|findModuleForSourceId)\b' \
  lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin
# Show the project.read/write helper contracts.
fd -t f 'ReadWriteLock.kt' lsp/kotlin/src/main/java --exec cat -n {}

Repository: appdevforall/CodeOnTheGo

Length of output: 13091


🏁 Script executed:

#!/bin/bash
# Get the full method context around lines 213-219
sed -n '200,230p' lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt

# Check if this is inside any existing read/write lock wrapper
sed -n '205,225p' lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt | cat -n

Repository: appdevforall/CodeOnTheGo

Length of output: 2228


🏁 Script executed:

#!/bin/bash
# Search for other usages of psiManager.findFile to see how they're typically guarded
rg -nB2 -A2 'psiManager\.findFile' lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin --type kt | head -50

Repository: appdevforall/CodeOnTheGo

Length of output: 189


🏁 Script executed:

#!/bin/bash
# Check the method signature and find if there's a wrapping read lock
sed -n '205,230p' lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 1133


Wrap PSI/module resolution in a read lock. The calls to psiManager.findFile(...) and structureProvider.getModule(...)/findModuleForSourceId(...) at lines 214-219 run without protection, while the explicit comment at line 222-223 acknowledges that concurrent analyze operations holding read locks can race against the subsequent write mutation. Compare with loadKtFile() (line 298), which correctly wraps psiManager.findFile() in project.read { }. Protect these PSI/module-structure reads by wrapping lines 213-219 in project.read { ... }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt`
around lines 213 - 219, The PSI and module resolution calls in the code block
(involving psiManager.findFile, structureProvider.getModule, and
findModuleForSourceId) are executing without read lock protection, creating a
race condition vulnerability since concurrent analyze operations hold read
locks. Wrap the entire code block from structureProvider.getInstance through the
module assignment in a project.read block, following the same pattern used in
the loadKtFile method which correctly protects its psiManager.findFile call with
project.read.

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.

3 participants