[threading] IJPL-179707: Minor bug fix and refactoring of Analyzer

GitOrigin-RevId: ca372c621f208ccba68219f8bf91e43c2958bc63
This commit is contained in:
Moncef Slimani
2025-11-17 13:31:32 +00:00
committed by intellij-monorepo-bot
parent 6c81db53bc
commit ae7ce93178
10 changed files with 177 additions and 179 deletions
@@ -13,8 +13,6 @@
<module name="intellij.platform.jewel.ideLafBridge"/>
<module name="intellij.libraries.compose.foundation.desktop"/>
<module name="intellij.libraries.skiko"/>
<module name="intellij.platform.jewel.markdown.core"/>
<module name="intellij.platform.jewel.markdown.ideLafBridgeStyling"/>
</dependencies>
<resource-bundle>messages.DevKitBundle</resource-bundle>
@@ -1,129 +0,0 @@
// 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 com.intellij.psi.*
import com.intellij.psi.search.searches.OverridingMethodsSearch
import com.intellij.psi.search.searches.ClassInheritorsSearch
class LockReqsAnalyzer(private val detector: LockReqsDetector = LockReqsDetector()) {
private data class TraversalContext(
val config: AnalysisConfig,
val currentPath: MutableList<MethodCall> = mutableListOf(),
val visited: MutableSet<MethodSignature> = mutableSetOf(),
val paths: MutableSet<ExecutionPath> = mutableSetOf(),
val messageBusTopics: MutableSet<String> = mutableSetOf(),
val swingComponents: MutableSet<MethodSignature> = mutableSetOf(),
)
fun analyzeMethod(method: PsiMethod, config: AnalysisConfig = AnalysisConfig.forProject(method.project)): AnalysisResult {
val context = TraversalContext(config)
traverseMethod(method, context)
return AnalysisResult(method, context.paths, context.messageBusTopics, context.swingComponents)
}
private fun traverseMethod(method: PsiMethod, context: TraversalContext, isPolymorphic: Boolean = false) {
if (context.currentPath.size >= context.config.maxDepth) return
val signature = MethodSignature.fromMethod(method)
if (signature in context.visited) return
context.visited.add(signature)
context.currentPath.add(MethodCall(method, isPolymorphic))
val annotationRequirements = detector.findAnnotationRequirements(method)
annotationRequirements.forEach { context.paths.add(ExecutionPath(context.currentPath.toList(), it)) }
processMethodBody(method, context)
context.currentPath.removeLast()
}
private fun processMethodBody(method: PsiMethod, context: TraversalContext) {
val body = method.body ?: return
val localRequirements = mutableListOf<LockRequirement>()
body.accept(object : JavaRecursiveElementVisitor() {
override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
super.visitMethodCallExpression(expression)
val resolvedMethod = expression.resolveMethod() ?: return
localRequirements += detector.findBodyRequirements(resolvedMethod)
if (localRequirements.any { it.requirementReason == RequirementReason.SWING_COMPONENT }) {
context.swingComponents.add(MethodSignature.fromMethod(resolvedMethod))
}
handleMethodCall(resolvedMethod, expression, context)
}
override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) {
super.visitMethodReferenceExpression(expression)
(expression.resolve() as? PsiMethod)?.let { resolvedMethod ->
localRequirements += detector.findBodyRequirements(resolvedMethod)
if (!detector.isAsyncBoundary(resolvedMethod)) traverseMethod(resolvedMethod, context)
}
}
override fun visitNewExpression(expression: PsiNewExpression) {
super.visitNewExpression(expression)
expression.resolveMethod()?.let { resolvedMethod ->
if (!detector.isAsyncBoundary(resolvedMethod)) traverseMethod(resolvedMethod, context)
}
}
})
localRequirements.forEach { requirement ->
val path = ExecutionPath(context.currentPath.toList(), requirement,
context.currentPath.any { it.isPolymorphic || it.isMessageBusCall })
context.paths.add(path)
}
}
private fun handleMethodCall(method: PsiMethod, expression: PsiMethodCallExpression, context: TraversalContext) {
when {
context.config.includeMessageBus && detector.isMessageBusCall(expression) -> handleMessageBusCall(method, expression, context)
context.config.includePolymorphic && detector.isPolymorphicCall(method) -> handlePolymorphicCall(method, context)
else -> traverseMethod(method, context)
}
}
private fun handleMessageBusCall(method: PsiMethod, expression: PsiMethodCallExpression, context: TraversalContext) {
detector.extractMessageBusTopic(expression)?.let { context.messageBusTopics.add(it) }
if (method.name != "syncPublisher") return
val topicType = method.returnType as? PsiClassType ?: return
val topicInterface = topicType.resolve() ?: return
if (!topicInterface.isInterface) return
findTopicListeners(topicInterface, context.config).forEach { listener ->
listener.methods.forEach { method -> traverseMethod(method, context) }
}
}
private fun handlePolymorphicCall(method: PsiMethod, context: TraversalContext) {
val implementations = findImplementations(method, context.config)
if (implementations.size > context.config.maxImplementations) {
val requirement = LockRequirement(method, LockType.READ, RequirementReason.IMPLICIT)
context.currentPath.add(MethodCall(method, isPolymorphic = true))
context.paths.add(ExecutionPath(context.currentPath.toList(), requirement, true))
return
}
implementations.forEach { traverseMethod(it, context) }
}
private fun findImplementations(method: PsiMethod, config: AnalysisConfig): List<PsiMethod> {
val implementations = mutableListOf<PsiMethod>()
if (method.body != null) implementations.add(method)
if (method.hasModifierProperty(PsiModifier.ABSTRACT) || method.containingClass?.isInterface == true) {
val query = OverridingMethodsSearch.search(method, config.scope, true)
implementations.addAll(query.findAll().take(config.maxImplementations))
}
return implementations
}
private fun findTopicListeners(topicInterface: PsiClass, config: AnalysisConfig): List<PsiClass> {
val query = ClassInheritorsSearch.search(topicInterface, config.scope, true)
return query.findAll().take(config.maxImplementations).toList()
}
}
@@ -0,0 +1,114 @@
// 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 com.intellij.psi.PsiMethod
import com.intellij.psi.JavaRecursiveElementVisitor
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiMethodReferenceExpression
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiNewExpression
import com.intellij.psi.search.searches.OverridingMethodsSearch
import com.intellij.util.Processor
class LockReqsAnalyzerDFS(private val detector: LockReqsDetector = LockReqsDetector()) {
private data class TraversalContext(
val config: AnalysisConfig,
val currentPath: MutableList<MethodCall> = mutableListOf(),
val visited: MutableSet<MethodSignature> = mutableSetOf(),
val paths: MutableSet<ExecutionPath> = mutableSetOf(),
val messageBusTopics: MutableSet<PsiClass> = mutableSetOf(),
val swingComponents: MutableSet<MethodSignature> = mutableSetOf(),
)
fun analyzeMethod(method: PsiMethod, config: AnalysisConfig = AnalysisConfig.forProject(method.project)): AnalysisResult {
val context = TraversalContext(config)
traverseMethod(method, context)
return AnalysisResult(method, context.paths, context.messageBusTopics, context.swingComponents)
}
private fun traverseMethod(method: PsiMethod, context: TraversalContext) {
println("Traversing ${method.containingClass?.qualifiedName}.${method.name}")
val signature = MethodSignature.fromMethod(method)
if (context.currentPath.size >= context.config.maxDepth || signature in context.visited) return
context.visited.add(signature)
context.currentPath.add(MethodCall(method))
val annotationRequirement = detector.findAnnotationRequirements(method)
annotationRequirement.forEach { context.paths.add(ExecutionPath(context.currentPath.toList(), it)) }
getMethodCallees(method).forEach { processCallee(it, context) }
context.currentPath.removeLast()
}
private fun getMethodCallees(method: PsiMethod): List<PsiMethod> {
val callees = mutableListOf<PsiMethod>()
method.body?.accept(object : JavaRecursiveElementVisitor() {
override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
super.visitMethodCallExpression(expression)
expression.resolveMethod()?.let { callees.add(it) }
}
override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) {
super.visitMethodReferenceExpression(expression)
(expression.resolve() as? PsiMethod)?.let { callees.add(it) }
}
override fun visitNewExpression(expression: PsiNewExpression) {
super.visitNewExpression(expression)
expression.resolveMethod()?.let { callees.add(it) }
}
})
return callees
}
private fun processCallee(callee: PsiMethod, context: TraversalContext) {
detector.findBodyRequirements(callee).forEach { requirement ->
context.paths.add(ExecutionPath(context.currentPath.toList(), requirement))
}
if (!detector.isAsyncDispatch(callee)) {
when {
detector.isMessageBusCall(callee) -> handleMessageBusCall(callee, context)
detector.isPolymorphicCall(callee) -> handlePolymorphic(callee, context)
else -> traverseMethod(callee, context)
}
}
}
private fun handlePolymorphic(method: PsiMethod, context: TraversalContext) {
val implementations = findImplementations(method, context.config)
implementations.forEach { traverseMethod(it, context) }
}
private fun findImplementations(method: PsiMethod, config: AnalysisConfig): List<PsiMethod> {
val implementations = mutableListOf<PsiMethod>()
if (method.body != null) {
implementations.add(method)
}
if (method.hasModifierProperty(PsiModifier.ABSTRACT) || method.containingClass?.isInterface == true) {
val query = OverridingMethodsSearch.search(method, config.scope, true)
query.forEach(Processor<PsiMethod> {
if (implementations.size >= config.maxImplementations) return@Processor false
implementations.add(it)
})
}
return implementations
}
private fun handleMessageBusCall(method: PsiMethod, context: TraversalContext) {
detector.extractMessageBusTopic(method)?.let { context.messageBusTopics.add(it) }
val topicType = method.returnType as? PsiClassType ?: return
val topicInterface = topicType.resolve() ?: return
if (!topicInterface.isInterface) return
detector.findTopicListeners(topicInterface, context.config).forEach { listener ->
listener.methods.forEach { method -> traverseMethod(method, context) }
}
}
}
@@ -1,12 +1,14 @@
// 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 com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiModifier
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.psi.util.InheritanceUtil
import com.intellij.util.Processor
class LockReqsDetector(private val patterns: LockReqsPatterns = DefaultLockReqsPatterns()) {
@@ -27,15 +29,12 @@ class LockReqsDetector(private val patterns: LockReqsPatterns = DefaultLockReqsP
patterns.assertionMethods[className]?.get(methodName)?.let { lockType ->
requirements.add(LockRequirement(method, lockType, RequirementReason.ASSERTION))
}
if (isSwingMethod(method)) requirements.add(LockRequirement(method, LockType.EDT, RequirementReason.SWING_COMPONENT))
if (isSwingMethod(method)) {
requirements.add(LockRequirement(method, LockType.EDT, RequirementReason.SWING_COMPONENT))
}
return requirements
}
private fun isSwingComponent(className: String): Boolean {
return patterns.edtRequiredClasses.contains(className) ||
patterns.edtRequiredPackages.any { className.startsWith("$it.") }
}
private fun isSwingMethod(method: PsiMethod): Boolean {
val containingClass = method.containingClass ?: return false
val className = containingClass.qualifiedName ?: return false
@@ -45,24 +44,13 @@ class LockReqsDetector(private val patterns: LockReqsPatterns = DefaultLockReqsP
}
}
fun isAsyncBoundary(method: PsiMethod): Boolean {
return method.name in patterns.asyncMethods || method.containingClass?.qualifiedName in patterns.asyncClasses
private fun isSwingComponent(className: String): Boolean {
return patterns.edtRequiredClasses.contains(className) ||
patterns.edtRequiredPackages.any { className.startsWith("$it.") }
}
fun isMessageBusCall(expression: PsiMethodCallExpression): Boolean {
val method = expression.resolveMethod() ?: return false
val containingClass = method.containingClass?.qualifiedName ?: return false
return patterns.messageBusClasses.contains(containingClass) && method.name in patterns.messageBusMethods
}
fun extractMessageBusTopic(expression: PsiMethodCallExpression): String? {
val method = expression.resolveMethod() ?: return null
if (method.name == "syncPublisher") {
val returnType = method.returnType as? PsiClassType ?: return null
val resolvedTopicInterface = returnType.resolve() ?: return null
return resolvedTopicInterface.qualifiedName
}
return null
fun isAsyncDispatch(method: PsiMethod): Boolean {
return method.name in patterns.asyncMethods && method.containingClass?.qualifiedName in patterns.asyncClasses
}
fun isPolymorphicCall(method: PsiMethod): Boolean {
@@ -72,4 +60,28 @@ class LockReqsDetector(private val patterns: LockReqsPatterns = DefaultLockReqsP
val containingClass = method.containingClass ?: return false
return !containingClass.hasModifierProperty(PsiModifier.FINAL)
}
fun isMessageBusCall(method: PsiMethod): Boolean {
val containingClass = method.containingClass?.qualifiedName ?: return false
return patterns.messageBusClasses.contains(containingClass) && method.name in patterns.messageBusSyncMethods
}
fun extractMessageBusTopic(method: PsiMethod): PsiClass? {
if (method.name in patterns.messageBusSyncMethods) {
val returnType = method.returnType as? PsiClassType ?: return null
val resolvedTopicInterface = returnType.resolve() ?: return null
return resolvedTopicInterface
}
return null
}
fun findTopicListeners(topicInterface: PsiClass, config: AnalysisConfig): List<PsiClass> {
val listeners = mutableListOf<PsiClass>()
val query = ClassInheritorsSearch.search(topicInterface, config.scope, true)
query.forEach(Processor {
if (listeners.size > config.maxImplementations) return@Processor false
listeners.add(it)
})
return listeners
}
}
@@ -2,6 +2,7 @@
package org.jetbrains.idea.devkit.threadingModelHelper
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import com.intellij.psi.search.GlobalSearchScope
@@ -47,7 +48,7 @@ data class ExecutionPath(
data class AnalysisResult(
val method: PsiMethod,
val paths: Set<ExecutionPath>,
val messageBusTopics: Set<String> = emptySet(),
val messageBusTopics: Set<PsiClass> = emptySet(),
val swingComponents: Set<MethodSignature> = emptySet(),
)
@@ -9,7 +9,7 @@ interface LockReqsPatterns {
val asyncClasses: Set<String>
val asyncMethods: Set<String>
val messageBusClasses: Set<String>
val messageBusMethods: Set<String>
val messageBusSyncMethods: Set<String>
val safeSwingMethods: Set<String>
}
@@ -66,12 +66,11 @@ class DefaultLockReqsPatterns : LockReqsPatterns {
)
override val messageBusClasses: Set<String> = setOf(
"com.intellij.util.messages.MessageBus",
"com.intellij.util.messages.MessageBusConnection"
"com.intellij.util.messages.MessageBus"
)
override val messageBusMethods: Set<String> = setOf(
"syncPublisher", "connect", "simpleConnect"
override val messageBusSyncMethods: Set<String> = setOf(
"syncPublisher"
)
override val safeSwingMethods: Set<String> = setOf()
@@ -20,7 +20,7 @@ class LockReqsService(private val project: Project) {
var onResultsUpdated: ((AnalysisResult?) -> Unit)? = null
fun updateResults(method: PsiMethod) {
val analyzer = LockReqsAnalyzer()
val analyzer = LockReqsAnalyzerDFS()
_currentResult = analyzer.analyzeMethod(method)
onResultsUpdated?.invoke(_currentResult)
@@ -11,14 +11,13 @@ class DifferentClassesMethods {
void helperMethod() {
Service service = new Service();
service.serviceMethod();
ThreadingAssertions.assertWriteAccess();
}
}
class Service {
@RequiresEdt
void serviceMethod() {
ThreadingAssertions.assertWriteAccess();
}
void serviceMethod() {}
}
}
@@ -3,7 +3,7 @@ import com.intellij.util.concurrency.ThreadingAssertions;
public class SubtypingPolymorphism {
public void testMethod() {
Service[] services = {new FileService(), new NetworkService(), new DatabaseService()};
Service[] services = {new FileService(), new UIService(), new DatabaseService()};
for (Service service : services) {
service.execute();
}
@@ -10,11 +10,11 @@ import org.jetbrains.idea.devkit.DevkitJavaTestsUtil
@TestDataPath($$"$CONTENT_ROOT/testData/threadingModelHelper/")
class LockReqsUnitTest : BasePlatformTestCase() {
private lateinit var analyzer: LockReqsAnalyzer
private lateinit var analyzer: LockReqsAnalyzerDFS
override fun setUp() {
super.setUp()
analyzer = LockReqsAnalyzer()
analyzer = LockReqsAnalyzerDFS()
myFixture.addFileToProject("com/intellij/util/concurrency/annotations.java", """
package com.intellij.util.concurrency.annotations;
public @interface RequiresReadLock {}
@@ -44,20 +44,20 @@ class LockReqsUnitTest : BasePlatformTestCase() {
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "threadingModelHelper/"
fun testNoLockRequirements() {
val result = doTest("NoLockRequirements", "testMethod")
val result = doTest("NoLockRequirements")
val actualPaths = formatResult(result)
assertTrue(actualPaths.isEmpty())
}
fun testAssertionInNestedBlock() {
val result = doTest("AssertionInNestedBlock", "testMethod")
val result = doTest("AssertionInNestedBlock")
val expectedPaths = listOf("AssertionInNestedBlock.testMethod => READ.ASSERTION")
val actualPaths = formatResult(result)
assertEquals(expectedPaths.sorted(), actualPaths.sorted())
}
fun testAnnotationInChain() {
val result = doTest("AnnotationInChain", "testMethod")
val result = doTest("AnnotationInChain")
val expectedPaths = listOf("AnnotationInChain.testMethod -> AnnotationInChain.intermediateMethod" +
" -> AnnotationInChain.targetMethod => READ.ANNOTATION")
val actualPaths = formatResult(result)
@@ -65,7 +65,7 @@ class LockReqsUnitTest : BasePlatformTestCase() {
}
fun testBothAnnotationAndAssertion() {
val result = doTest("BothAnnotationAndAssertion", "testMethod")
val result = doTest("BothAnnotationAndAssertion")
val expectedPaths = listOf("BothAnnotationAndAssertion.testMethod => WRITE.ANNOTATION",
"BothAnnotationAndAssertion.testMethod => BGT.ASSERTION")
val actualPaths = formatResult(result)
@@ -73,22 +73,22 @@ class LockReqsUnitTest : BasePlatformTestCase() {
}
fun testCyclicRecursiveCalls() {
val result = doTest("CyclicRecursiveCalls", "testMethod")
val result = doTest("CyclicRecursiveCalls")
val expectedPaths = listOf("CyclicRecursiveCalls.testMethod -> CyclicRecursiveCalls.methodB => READ.ANNOTATION")
val actualPaths = formatResult(result)
assertEquals(expectedPaths.sorted(), actualPaths.sorted())
}
fun testDifferentClassesMethods() {
val result = doTest("DifferentClassesMethods", "testMethod")
val result = doTest("DifferentClassesMethods")
val expectedPaths = listOf("DifferentClassesMethods.testMethod -> Helper.helperMethod -> Service.serviceMethod => EDT.ANNOTATION",
"DifferentClassesMethods.testMethod -> Helper.helperMethod -> Service.serviceMethod => WRITE.ASSERTION")
"DifferentClassesMethods.testMethod -> Helper.helperMethod => WRITE.ASSERTION")
val actualPaths = formatResult(result)
assertEquals(expectedPaths.sorted(), actualPaths.sorted())
}
fun testMultipleAssertionsInMethod() {
val result = doTest("MultipleAssertionsInMethod", "testMethod")
val result = doTest("MultipleAssertionsInMethod")
val expectedPaths = listOf("MultipleAssertionsInMethod.testMethod => READ.ASSERTION")
val actualPaths = formatResult(result)
assertEquals(1, actualPaths.size)
@@ -97,7 +97,7 @@ class LockReqsUnitTest : BasePlatformTestCase() {
fun testLambdaWithMethodReference() {
val result = doTest("LambdaWithMethodReference", "testMethod")
val result = doTest("LambdaWithMethodReference")
val expectedPaths = listOf("LambdaWithMethodReference.testMethod -> LambdaWithMethodReference.processItem => READ.ASSERTION")
val actualPaths = formatResult(result)
assertEquals(expectedPaths.sorted(), actualPaths.sorted())
@@ -105,7 +105,7 @@ class LockReqsUnitTest : BasePlatformTestCase() {
}
fun testSubtypingPolymorphism() {
val result = doTest("SubtypingPolymorphism", "testMethod")
val result = doTest("SubtypingPolymorphism")
val expectedPaths = listOf(
"SubtypingPolymorphism.testMethod -> FileService.execute => READ.ANNOTATION",
"SubtypingPolymorphism.testMethod -> UIService.execute => EDT.ASSERTION",
@@ -115,11 +115,11 @@ class LockReqsUnitTest : BasePlatformTestCase() {
assertEquals(expectedPaths.sorted(), actualPaths.sorted())
}
private fun doTest(className: String, methodName: String): AnalysisResult {
private fun doTest(className: String): AnalysisResult {
val fileName = "${getTestName(false)}.java"
val psiJavaFile = myFixture.configureByFile(fileName) as PsiJavaFile
val targetClass = psiJavaFile.classes.find { it.name == className } ?: error("Could not find class $className")
val targetMethod = targetClass.methods.find { it.name == methodName } ?: error("Could not find method $methodName")
val targetMethod = targetClass.methods.find { it.name == testMethodName } ?: error("Could not find method $testMethodName")
return analyzer.analyzeMethod(targetMethod)
}
@@ -131,4 +131,8 @@ class LockReqsUnitTest : BasePlatformTestCase() {
}
return actualPaths
}
companion object {
private const val testMethodName = "testMethod"
}
}