Skip to content

Commit f1d3938

Browse files
authored
Merge pull request #2772 from digma-ai/fix-psi-errors
Fix psi errors Closes #2769
2 parents 7db87f1 + e321b99 commit f1d3938

File tree

19 files changed

+816
-197
lines changed

19 files changed

+816
-197
lines changed

build.gradle.kts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import org.apache.tools.ant.filters.ReplaceTokens
1111
import org.jetbrains.changelog.date
1212
import org.jetbrains.changelog.exceptions.MissingVersionException
1313
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
14-
import org.jetbrains.intellij.platform.gradle.models.ProductRelease
1514
import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask
1615

1716
fun properties(key: String) = properties(key, project)
@@ -286,14 +285,14 @@ tasks {
286285

287286
val deleteLog by registering(Delete::class) {
288287
outputs.upToDateWhen { false }
289-
val ideFolderName = if (platformType == IntelliJPlatformType.Rider) {
290-
"${platformType.code}-${project.currentProfile().riderVersion}"
291-
} else {
292-
"${platformType.code}-${project.currentProfile().platformVersion}"
293-
}
294-
project.layout.buildDirectory.dir("idea-sandbox/$ideFolderName/log").get().asFile.walk().forEach {
295-
if (it.name.endsWith(".log")) {
296-
delete(it)
288+
289+
doLast {
290+
val sandboxDir = project.layout.buildDirectory.dir("idea-sandbox").get().asFile
291+
sandboxDir.listFiles { file -> file.isDirectory }?.forEach { versionDir ->
292+
val logFile = File(versionDir, "log/idea.log")
293+
if (logFile.exists()) {
294+
logFile.delete()
295+
}
297296
}
298297
}
299298
}

common-build-logic/src/main/kotlin/common/BuildProfile.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ object BuildProfiles {
203203
Profile.p252 to BuildProfile(
204204
isEAP = true,
205205
profile = Profile.p252,
206-
platformVersion = "252.21735-EAP-CANDIDATE-SNAPSHOT",
206+
platformVersion = "252.23309-EAP-CANDIDATE-SNAPSHOT",
207207
riderVersion = "2025.2-EAP5-SNAPSHOT",
208208
pycharmVersion = "252.21735-EAP-CANDIDATE-SNAPSHOT",
209209
riderTargetFramework = "net8.0",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.digma.intellij.plugin.common
2+
3+
inline fun <T> measureTimeMillisWithResult(block: () -> T): Pair<T, Long> {
4+
val start = System.currentTimeMillis()
5+
val result = block()
6+
val duration = System.currentTimeMillis() - start
7+
return result to duration
8+
}

ide-common/src/main/kotlin/org/digma/intellij/plugin/discovery/AbstractNavigationDiscoveryManager.kt

Lines changed: 443 additions & 107 deletions
Large diffs are not rendered by default.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package org.digma.intellij.plugin.discovery
2+
3+
import com.intellij.openapi.application.readAction
4+
import com.intellij.openapi.diagnostic.Logger
5+
import com.intellij.openapi.project.Project
6+
import com.intellij.openapi.util.Disposer
7+
import com.intellij.openapi.vfs.VirtualFile
8+
import com.intellij.psi.PsiFile
9+
import com.intellij.psi.PsiManager
10+
import com.intellij.psi.PsiTreeAnyChangeAbstractAdapter
11+
import com.intellij.psi.util.PsiModificationTracker
12+
import kotlinx.coroutines.CancellationException
13+
import kotlinx.coroutines.coroutineScope
14+
import kotlinx.coroutines.delay
15+
import kotlinx.coroutines.ensureActive
16+
import kotlinx.coroutines.isActive
17+
import kotlinx.coroutines.selects.select
18+
import org.digma.intellij.plugin.common.isValidVirtualFile
19+
import org.digma.intellij.plugin.errorreporting.ErrorReporter
20+
import org.digma.intellij.plugin.kotlin.ext.asyncWithErrorReporting
21+
import org.digma.intellij.plugin.kotlin.ext.launchWithErrorReporting
22+
import org.digma.intellij.plugin.log.Log
23+
import java.util.concurrent.atomic.AtomicBoolean
24+
import kotlin.coroutines.coroutineContext
25+
import kotlin.time.Duration
26+
import kotlin.time.Duration.Companion.milliseconds
27+
28+
class FileProcessingMonitor(
29+
private val project: Project,
30+
private val logger: Logger,
31+
private val checkInterval: Duration = 100.milliseconds
32+
) {
33+
34+
/**
35+
* Executes a block of code with file change monitoring.
36+
* Cancels the operation if file changes or PSI/stub mismatch is detected.
37+
*/
38+
suspend fun <T> executeWithFileMonitoring(
39+
virtualFile: VirtualFile,
40+
operation: suspend () -> T
41+
): ProcessingResult<T> = coroutineScope {
42+
43+
val psiFile = readAction { PsiManager.getInstance(project).findFile(virtualFile) }
44+
if (psiFile == null) {
45+
return@coroutineScope ProcessingResult.Error("Psi file is null for file ${virtualFile.url}")
46+
}
47+
48+
val psiTreeChangeDisposable = Disposer.newDisposable()
49+
50+
//Start monitoring.
51+
//The monitorJob will be canceled when a change in the file is detected.
52+
//When the monitorJob is canceled, the select clause will select it and return ProcessingResult.Cancelled.
53+
//The operationJob will be canceled in the finally block, and so the file discovery will be canceled.
54+
55+
val monitorJob = launchWithErrorReporting("FileProcessingMonitor.executeWithFileMonitoring", logger) {
56+
val initialSnapshot = createFileSnapshot(virtualFile, psiFile)
57+
if (initialSnapshot == FileProcessingSnapshot.INVALID) {
58+
Log.trace(logger, project, "FileProcessingMonitor: Cancelling file processing for {} because snapshot is invalid", virtualFile.url)
59+
throw CancellationException("File processing cancelled: initial snapshot invalid")
60+
}
61+
if (!initialSnapshot.isVirtualFileValid || !initialSnapshot.isPsiFileValid) {
62+
Log.trace(logger, project, "FileProcessingMonitor: Cancelling file processing for {} because virtual file or psi file is invalid", virtualFile.url)
63+
throw CancellationException("File processing cancelled: virtual file or psi file invalid")
64+
}
65+
66+
val psiChanged = AtomicBoolean(false)
67+
PsiManager.getInstance(project).addPsiTreeChangeListener(object : PsiTreeAnyChangeAbstractAdapter() {
68+
override fun onChange(file: PsiFile?) {
69+
file?.let {
70+
if (it.virtualFile == virtualFile) {
71+
psiChanged.set(true)
72+
}
73+
}
74+
}
75+
}, psiTreeChangeDisposable)
76+
77+
78+
while (isActive) {
79+
if (psiChanged.get()) {
80+
throw CancellationException("File processing cancelled: psi tree changed")
81+
}
82+
delay(checkInterval)
83+
if (psiChanged.get()) {
84+
throw CancellationException("File processing cancelled: psi tree changed")
85+
}
86+
monitorFileChanges(virtualFile, psiFile, initialSnapshot)
87+
}
88+
}
89+
90+
// Execute the operation
91+
val operationJob = asyncWithErrorReporting("FileProcessingMonitor.executeWithFileMonitoring", logger) {
92+
operation()
93+
}
94+
95+
try {
96+
//Select the first job that completes.
97+
//If monitorJob was canceled due to change in the file, it will be selected and the finally block will cancel
98+
// the operationJob.
99+
//If the operationJob completed, the processing result will be returned, and the finally block will cancel
100+
// the monitorJob
101+
//The select clause is biased towards the first job.
102+
select<ProcessingResult<T>> {
103+
operationJob.onAwait { ProcessingResult.Success(it) }
104+
monitorJob.onJoin { ProcessingResult.Cancelled("File monitoring detected changes") }
105+
}
106+
} catch (@Suppress("IncorrectCancellationExceptionHandling") e: CancellationException) {
107+
//this is a CancellationException from operationJob.onAwait. it should not be thrown. it will be thrown here in case the operation
108+
// was canceled somewhere else
109+
ProcessingResult.Cancelled(e.message ?: "File monitoring cancelled")
110+
} catch (e: Exception) {
111+
ProcessingResult.Error("Processing failed: ${e.message}", e)
112+
} finally {
113+
monitorJob.cancel()
114+
operationJob.cancel()
115+
Disposer.dispose(psiTreeChangeDisposable)
116+
}
117+
}
118+
119+
/**
120+
* Monitor file for changes and PSI/stub consistency
121+
*/
122+
private suspend fun monitorFileChanges(file: VirtualFile, psiFile: PsiFile, initialSnapshot: FileProcessingSnapshot) {
123+
val currentSnapshot = createFileSnapshot(file, psiFile)
124+
coroutineContext.ensureActive()
125+
val changeReason = detectChanges(initialSnapshot, currentSnapshot)
126+
coroutineContext.ensureActive()
127+
if (changeReason != null) {
128+
Log.trace(logger, project, "FileProcessingMonitor: Cancelling file processing for {}: {}", file.url, changeReason)
129+
throw CancellationException("File processing cancelled: $changeReason")
130+
}
131+
}
132+
133+
/**
134+
* Create a snapshot of the file's current state
135+
*/
136+
private suspend fun createFileSnapshot(file: VirtualFile, psiFile: PsiFile): FileProcessingSnapshot {
137+
return try {
138+
readAction {
139+
FileProcessingSnapshot(
140+
virtualFileStamp = file.modificationStamp,
141+
isVirtualFileValid = isValidVirtualFile(file),
142+
psiModificationCount = PsiModificationTracker.getInstance(project).modificationCount,
143+
psiFileStamp = psiFile.modificationStamp,
144+
isPsiFileValid = psiFile.isValid,
145+
fileLength = file.length,
146+
psiTextLength = psiFile.textLength
147+
)
148+
149+
}
150+
} catch (e: CancellationException) {
151+
throw e
152+
} catch (e: Exception) {
153+
Log.warnWithException(logger, project, e, "FileProcessingMonitor: Failed to create snapshot for {}", file.url)
154+
ErrorReporter.getInstance().reportError(project, "FileProcessingMonitor.createFileSnapshot", e)
155+
FileProcessingSnapshot.INVALID
156+
}
157+
}
158+
159+
160+
/**
161+
* Detect what changed between snapshots
162+
*/
163+
private fun detectChanges(
164+
initial: FileProcessingSnapshot,
165+
current: FileProcessingSnapshot
166+
): String? {
167+
return when {
168+
!current.isVirtualFileValid -> "Virtual file became invalid"
169+
170+
current.virtualFileStamp != initial.virtualFileStamp ->
171+
"Virtual file modified"
172+
173+
!current.isPsiFileValid && initial.isPsiFileValid ->
174+
"PSI file became invalid"
175+
176+
current.psiFileStamp != initial.psiFileStamp && current.psiFileStamp != -1L ->
177+
"PSI file modified"
178+
179+
current.psiModificationCount != initial.psiModificationCount ->
180+
"PSI tree globally modified"
181+
182+
current.fileLength != initial.fileLength ->
183+
"File length changed"
184+
185+
current.psiTextLength != initial.psiTextLength ->
186+
"PSI text length changed"
187+
188+
else -> null
189+
}
190+
}
191+
}
192+
193+
/**
194+
* Snapshot of file state for change detection
195+
*/
196+
data class FileProcessingSnapshot(
197+
val virtualFileStamp: Long,
198+
val isVirtualFileValid: Boolean,
199+
val psiModificationCount: Long,
200+
val psiFileStamp: Long,
201+
val isPsiFileValid: Boolean,
202+
val fileLength: Long,
203+
val psiTextLength: Int,
204+
) {
205+
companion object {
206+
val INVALID = FileProcessingSnapshot(
207+
virtualFileStamp = -1,
208+
isVirtualFileValid = false,
209+
psiModificationCount = -1,
210+
psiFileStamp = -1,
211+
isPsiFileValid = false,
212+
fileLength = -1,
213+
psiTextLength = -1,
214+
)
215+
}
216+
}
217+
218+
/**
219+
* Result of a file processing operation
220+
*/
221+
sealed class ProcessingResult<T> {
222+
data class Success<T>(val result: T) : ProcessingResult<T>()
223+
data class Cancelled<T>(val reason: String) : ProcessingResult<T>()
224+
data class Error<T>(val message: String, val exception: Exception? = null) : ProcessingResult<T>()
225+
}

ide-common/src/main/kotlin/org/digma/intellij/plugin/kotlin/ext/CoroutineScopeExtensions.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ fun <T> CoroutineScope.asyncWithErrorReporting(
102102
} catch (e: Throwable) {
103103
Log.warnWithException(logger, e, "Error in coroutine {}", e)
104104
ErrorReporter.getInstance().reportError("asyncWithErrorReporting.$name", e)
105-
//todo: will jetbrains consider it as unhandled exception? if yes swallow the exceptions and return empty Deferred
106105
throw e
107106
}
108107
}

jvm-common/src/main/kotlin/org/digma/intellij/plugin/idea/AbstractJvmLanguageService.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -257,20 +257,25 @@ abstract class AbstractJvmLanguageService(protected val project: Project, protec
257257
@RequiresReadLock
258258
private fun detectMethodUnderCaretImpl(virtualFile: VirtualFile, caretOffset: Int): MethodUnderCaret {
259259

260-
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return MethodUnderCaret.empty(virtualFile.url)
261-
val packageName = psiFile.toUElementOfType<UFile>()?.packageName ?: ""
262-
val underCaret: PsiElement = psiFile.findElementAt(caretOffset) ?: return MethodUnderCaret.empty(virtualFile.url)
263-
val uMethod = findParentMethod(underCaret)
264-
val className: String = uMethod?.getParentOfType<UClass>()?.let {
260+
if (!isValidVirtualFile(virtualFile)) {
261+
return MethodUnderCaret.EMPTY
262+
}
263+
264+
val psiFile = PsiManager.getInstance(project).findFile(virtualFile)?.takeIf { it.isValid && virtualFile.isValid } ?: return MethodUnderCaret.empty(virtualFile.url)
265+
val packageName = psiFile.takeIf { it.isValid }?.toUElementOfType<UFile>()?.takeIf { it.isPsiValid && virtualFile.isValid }?.packageName ?: ""
266+
val underCaret: PsiElement = psiFile.takeIf { it.isValid && virtualFile.isValid }?.findElementAt(caretOffset) ?: return MethodUnderCaret.empty(virtualFile.url)
267+
val uMethod = underCaret.takeIf { psiFile.isValid && virtualFile.isValid }?.let {
268+
findParentMethod(it)
269+
}
270+
val className: String = uMethod?.takeIf { psiFile.isValid && virtualFile.isValid }?.getParentOfType<UClass>()?.let {
265271
getClassSimpleName(it)
266272
} ?: ""
267273

268-
if (uMethod != null) {
269-
274+
return uMethod?.takeIf { psiFile.isValid && virtualFile.isValid }?.let {
270275
val methodId = createMethodCodeObjectId(uMethod)
271276
val endpointTextRange = findEndpointTextRange(virtualFile, caretOffset, methodId)
272277

273-
return MethodUnderCaret(
278+
MethodUnderCaret(
274279
methodId,
275280
uMethod.name,
276281
className,
@@ -279,8 +284,7 @@ abstract class AbstractJvmLanguageService(protected val project: Project, protec
279284
caretOffset,
280285
endpointTextRange
281286
)
282-
}
283-
return MethodUnderCaret.Companion.empty(virtualFile.url)
287+
} ?: MethodUnderCaret.empty(virtualFile.url)
284288
}
285289

286290

jvm-common/src/main/kotlin/org/digma/intellij/plugin/idea/discovery/AbstractCodeObjectDiscovery.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,7 @@ abstract class AbstractCodeObjectDiscovery(private val spanDiscovery: AbstractSp
9696
}
9797

9898
//maybe uFile is null,there is nothing to do without a UFile.
99-
val fileData = readAction {
100-
FileData.buildFileData(psiFile)
101-
} ?: return null
99+
val fileData = FileData.buildFileData(psiFile) ?: return null
102100
coroutineContext.ensureActive()
103101

104102
val packageName = fileData.packageName
@@ -208,10 +206,12 @@ abstract class AbstractCodeObjectDiscovery(private val spanDiscovery: AbstractSp
208206

209207
private class FileData(val uFile: UFile, val packageName: String) {
210208
companion object {
211-
fun buildFileData(psiFile: PsiFile): FileData? {
212-
val uFile: UFile? = psiFile.toUElementOfType<UFile>()
213-
return uFile?.let {
214-
val packageName = it.packageName
209+
suspend fun buildFileData(psiFile: PsiFile): FileData? {
210+
coroutineContext.ensureActive()
211+
val uFile: UFile? = readAction { psiFile.takeIf { it.isValid }?.toUElementOfType<UFile>() }
212+
return uFile?.takeIf { it.isPsiValid }?.let {
213+
coroutineContext.ensureActive()
214+
val packageName = readAction { it.packageName }
215215
FileData(it, packageName)
216216
}
217217
}

jvm-common/src/main/kotlin/org/digma/intellij/plugin/idea/discovery/JvmPsiUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ fun findNearestAnnotation(psiMethod: PsiMethod, annotationFqn: String): PsiAnnot
111111
return annotClass
112112
}
113113

114-
val superMethods = psiMethod.findSuperMethods(false)
114+
val superMethods = psiMethod.findSuperMethods(true)
115115
superMethods.forEach {
116116
val theAnnotation = it.getAnnotation(annotationFqn)
117117
if (theAnnotation != null) {

0 commit comments

Comments
 (0)