ADFA-4387: add global analysis lock for Kotlin analysis#1428
ADFA-4387: add global analysis lock for Kotlin analysis#1428itsaky-adfa wants to merge 3 commits into
Conversation
Signed-off-by: Akash Yadav <itsaky01@gmail.com>
📝 Walkthrough
WalkthroughA ChangesAnalysis API Concurrency Serialization and Lifetime Safety
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt
| 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 { |
There was a problem hiding this comment.
🧩 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 -60Repository: 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 3Repository: appdevforall/CodeOnTheGo
Length of output: 50
🏁 Script executed:
# Search for withAnalysisLock definition in all code
rg -n 'fun withAnalysisLock|suspend fun.*withAnalysisLock' -t kotlinRepository: 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 kotlinRepository: 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 -100Repository: 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 ktRepository: 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 8Repository: 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 -20Repository: 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 8Repository: 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.ktRepository: 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 -80Repository: 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 2Repository: 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 3Repository: 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 -60Repository: 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 8Repository: 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 -40Repository: 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.ktRepository: 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 -50Repository: 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 -40Repository: 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
left a comment
There was a problem hiding this comment.
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 KotlinCodeActionsMenu → KotlinLanguageServer). 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) |
There was a problem hiding this comment.
🟧 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() |
There was a problem hiding this comment.
🟨 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` |
There was a problem hiding this comment.
🟨 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 { |
There was a problem hiding this comment.
🟦 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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt (1)
15-31: ⚡ Quick winConsider adding a test for lock reentrancy.
The KDoc in
KtFileExts.ktnotes thatanalysisLockis aReentrantLockspecifically 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
📒 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).
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.ktlsp/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
| 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 |
There was a problem hiding this comment.
🩺 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 -nRepository: 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 -50Repository: 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.ktRepository: 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.
See ADFA-4387 for more details.