diff --git a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
index 48f4c4aec75c..8be46228092a 100644
--- a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
+++ b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml
@@ -8,13 +8,15 @@
-
-
-
-
-
-
-
+ intellijPlatform {
+ bundledModule("intellij.platform.jewel.foundation")
+ bundledModule("intellij.platform.jewel.ui")
+ bundledModule("intellij.platform.jewel.ideLafBridge")
+ bundledModule("intellij.platform.jewel.markdown.core")
+ bundledModule("intellij.platform.jewel.markdown.ideLafBridgeStyling")
+ bundledModule("intellij.libraries.compose.foundation.desktop")
+ bundledModule("intellij.libraries.skiko")
+ }
messages.DevKitBundle
@@ -740,8 +742,8 @@
diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt
index 8cb4531954d4..d8f3b305effe 100644
--- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt
+++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt
@@ -15,9 +15,10 @@ class LockReqsAnalyzer {
private const val REQUIRES_READ_LOCK_ANNOTATION = "com.intellij.util.concurrency.annotations.RequiresReadLock"
private const val MAX_PATH_DEPTH = 1000
+ private val ASYNC_METHODS = setOf("invokeLater", "invokeAndWait", "runInEdt")
+
enum class LockCheckType {
- ANNOTATION,
- ASSERTION
+ ANNOTATION, ASSERTION
}
data class LockRequirement(
@@ -32,12 +33,13 @@ class LockReqsAnalyzer {
val pathString: String
get() = buildString {
append(methodChain.joinToString(" -> ") {
- "${it.containingClass?.name}.${it.name}" +
- when (lockRequirement.type) {
- LockCheckType.ANNOTATION -> "@RequiresReadLock"
- LockCheckType.ASSERTION -> "ThreadingAssertions.assertReadAccess()"
- }
+ "${it.containingClass?.name}.${it.name}"
})
+ append(" -> ")
+ when (lockRequirement.type) {
+ LockCheckType.ANNOTATION -> append("@RequiresReadLock")
+ LockCheckType.ASSERTION -> append("ThreadingAssertions.assertReadAccess()")
+ }
}
}
}
@@ -64,10 +66,18 @@ class LockReqsAnalyzer {
findLockChecks(method).forEach { check ->
paths.add(ExecutionPath(currentPath.toList(), LockRequirement(check, method)))
}
- getMethodCallees(method).forEach { callee -> processMethodDFS(callee, currentPath, paths) }
+ getMethodCallees(method).forEach {
+ if (!isAsyncMethod(it)) {
+ processMethodDFS(it, currentPath, paths)
+ }
+ }
currentPath.removeAt(currentPath.lastIndex)
}
+ private fun isAsyncMethod(method: PsiMethod): Boolean {
+ return method.name in ASYNC_METHODS
+ }
+
private fun getMethodCallees(method: PsiMethod): List {
val callees = mutableListOf()
method.body?.accept(object : JavaRecursiveElementVisitor() {
@@ -96,8 +106,7 @@ class LockReqsAnalyzer {
}
private fun isAssertReadAccess(expression: PsiMethodCallExpression): Boolean {
- return ASSERT_READ_ACCESS_METHOD == expression.methodExpression.referenceName &&
- THREADING_ASSERTIONS_CLASS == expression.resolveMethod()?.containingClass?.qualifiedName
+ return ASSERT_READ_ACCESS_METHOD == expression.methodExpression.referenceName && THREADING_ASSERTIONS_CLASS == expression.resolveMethod()?.containingClass?.qualifiedName
}
private fun hasAssertReadAccessCall(method: PsiMethod): Boolean {
diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsCompose.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsCompose.kt
new file mode 100644
index 000000000000..c74447b953f0
--- /dev/null
+++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsCompose.kt
@@ -0,0 +1,47 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.idea.devkit.threadingModelHelper
+
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import java.util.Locale
+import org.jetbrains.jewel.bridge.LocalComponent
+import org.jetbrains.jewel.foundation.modifier.trackActivation
+import org.jetbrains.jewel.foundation.modifier.trackComponentActivation
+import org.jetbrains.jewel.foundation.theme.JewelTheme
+import org.jetbrains.jewel.ui.Orientation
+import org.jetbrains.jewel.ui.component.Divider
+import org.jetbrains.jewel.ui.component.Text
+import org.jetbrains.jewel.ui.component.TextArea
+import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer
+import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding
+
+import com.intellij.openapi.project.Project
+
+@Composable
+fun LockReqsCompose(project: Project) {
+ Column() {
+ Text("Working now?")
+ }
+}
\ No newline at end of file
diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsService.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsService.kt
index c9933ecdd6fd..f75a64d8f7a8 100644
--- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsService.kt
+++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsService.kt
@@ -8,14 +8,23 @@ import com.intellij.psi.PsiMethod
@Service(Service.Level.PROJECT)
class LockReqsService(private val project: Project) {
- private var currentResults: List = emptyList()
+
+ companion object {
+ const val TOOL_WINDOW_ID: String = "Locking Requirements"
+ }
+
+ private var _currentResults: List = emptyList()
+ val currentResults: List
+ get() = _currentResults
+
+ var onResultsUpdated: (() -> Unit)? = null
fun updateResults(method: PsiMethod) {
val analyzer = LockReqsAnalyzer()
- val paths = analyzer.analyzeMethod(method).map { it.pathString }
- currentResults = paths
- ToolWindowManager.getInstance(project).getToolWindow("LockReqsToolWindow")?.show()
- }
+ val paths = analyzer.analyzeMethod(method)
+ _currentResults = paths.map { it.pathString }
+ onResultsUpdated?.invoke()
- fun getCurrentResults(): List = currentResults
-}
+ ToolWindowManager.getInstance(project).getToolWindow(TOOL_WINDOW_ID)?.show()
+ }
+}
\ No newline at end of file
diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt
index 688808e9da4f..1c45469218b3 100644
--- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt
+++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt
@@ -1,20 +1,36 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.threadingModelHelper
-import androidx.compose.runtime.Composable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
-import org.jetbrains.idea.devkit.DevKitBundle
-import org.jetbrains.jewel.bridge.addComposeTab
-import org.jetbrains.jewel.ui.component.Text
+import com.intellij.ui.components.JBScrollPane
+import com.intellij.ui.table.JBTable
+import javax.swing.JPanel
+import javax.swing.table.DefaultTableModel
+import java.awt.BorderLayout
class LockReqsToolWindowFactory : ToolWindowFactory {
+
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val service = project.service()
- toolWindow.addComposeTab(DevKitBundle.message("tab.title.locking.requirements")) {
- LockReqsToolWindow(service)
- }
+
+ val tableModel = DefaultTableModel(arrayOf("Execution Path"), 0)
+ val panel = JPanel(BorderLayout())
+ val table = JBTable(tableModel)
+ val scrollPane = JBScrollPane(table)
+ panel.add(scrollPane)
+
+ service.onResultsUpdated = { loadData(service.currentResults, tableModel) }
+ val factory = toolWindow.contentManager.factory
+ val content = factory.createContent(panel, null, false)
+ toolWindow.contentManager.addContent(content)
+ }
+
+ private fun loadData(results: List, tableModel: DefaultTableModel) {
+ tableModel.rowCount = 0
+ results.forEach { tableModel.addRow(arrayOf(it)) }
}
}
+
diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/RealWorldInsideUI.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/RealWorldInsideUI.java
new file mode 100644
index 000000000000..c1f8df4225e4
--- /dev/null
+++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/RealWorldInsideUI.java
@@ -0,0 +1,30 @@
+package org.jetbrains.idea.devkit.threadingModelHelper;
+import com.intellij.util.concurrency.ThreadingAssertions;
+import com.intellij.util.concurrency.annotations.RequiresReadLock;
+import testutils.ExpectedPath;
+
+@ExpectedPath("RealWorldUI.testMethod -> Helper.helperMethod -> Service.serviceMethod -> @RequiresReadLock")
+@ExpectedPath("RealWorldUI.testMethod -> Helper.helperMethod -> Service.serviceMethod -> ThreadingAssertions.assertReadAccess()")
+class RealWorldInsideUI {
+ void testMethod() {
+ Helper helper = new Helper();
+ helper.helperMethod();
+ }
+
+ class Helper {
+ void helperMethod() {
+ Service service = new Service();
+ service.serviceMethod();
+ }
+ }
+
+ class Service {
+ @RequiresReadLock
+ void serviceMethod() {
+ ThreadingAssertions.assertReadAccess();
+ }
+ }
+}
+
+
+