diff --git a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml index 9e9625689bef..43a137e106fa 100644 --- a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml +++ b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml @@ -741,7 +741,7 @@ @@ -934,6 +934,7 @@ + diff --git a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties index 56162f80ece3..2cf3d2fc440f 100644 --- a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties +++ b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties @@ -384,6 +384,8 @@ action.ShowHelpPageById.dialog.enter.help.id=Enter Help ID: action.ShowHelpPageById.dialog.title=Show Help Page +action.BFLReqsAction.text=BulkFileListener EDT Checker + action.ShowSerializedXml.text=Show Serialized XML for Class action.ShowSerializedXml.message.caret.must.be.at.class.identifier=Caret should be positioned at the name of the class action.ShowSerializedXml.message.cannot.find.class=Cannot find class ''{0}'' diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt index 596ee7556104..c875d59f345b 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsAnalyzer.kt @@ -10,9 +10,9 @@ class LockReqsAnalyzer(private val detector: LockReqsDetector = LockReqsDetector private data class TraversalContext( val config: AnalysisConfig, - val paths: MutableList = mutableListOf(), val currentPath: MutableList = mutableListOf(), val visited: MutableSet = mutableSetOf(), + val paths: MutableSet = mutableSetOf(), val messageBusTopics: MutableSet = mutableSetOf(), val swingComponents: MutableSet = mutableSetOf(), ) @@ -33,7 +33,7 @@ class LockReqsAnalyzer(private val detector: LockReqsDetector = LockReqsDetector val annotationRequirements = detector.findAnnotationRequirements(method) annotationRequirements.forEach { context.paths.add(ExecutionPath(context.currentPath.toList(), it)) } processMethodBody(method, context) - context.currentPath.removeAt(context.currentPath.lastIndex) + context.currentPath.removeLast() } @@ -46,16 +46,17 @@ class LockReqsAnalyzer(private val detector: LockReqsDetector = LockReqsDetector super.visitMethodCallExpression(expression) val resolvedMethod = expression.resolveMethod() ?: return - localRequirements += detector.findBodyRequirements(resolvedMethod, expression) - if (localRequirements.any { it.requirementReason == RequirementReason.SWING_COMPONENT }) context.swingComponents.add(MethodSignature.fromMethod(resolvedMethod)) - + 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, expression) + localRequirements += detector.findBodyRequirements(resolvedMethod) if (!detector.isAsyncBoundary(resolvedMethod)) traverseMethod(resolvedMethod, context) } } @@ -69,7 +70,8 @@ class LockReqsAnalyzer(private val detector: LockReqsDetector = LockReqsDetector }) localRequirements.forEach { requirement -> - val path = ExecutionPath(context.currentPath.toList(), requirement, isSpeculative = context.currentPath.any { it.isPolymorphic || it.isMessageBusCall }) + val path = ExecutionPath(context.currentPath.toList(), requirement, + context.currentPath.any { it.isPolymorphic || it.isMessageBusCall }) context.paths.add(path) } } diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsDetector.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsDetector.kt index 20d314d941b6..d450049e0607 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsDetector.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsDetector.kt @@ -20,14 +20,14 @@ class LockReqsDetector(private val patterns: LockReqsPatterns = DefaultLockReqsP return requirements } - fun findBodyRequirements(method: PsiMethod, source: PsiElement): List { + fun findBodyRequirements(method: PsiMethod): List { val requirements = mutableListOf() val className = method.containingClass?.qualifiedName val methodName = method.name patterns.assertionMethods[className]?.get(methodName)?.let { lockType -> - requirements.add(LockRequirement(source, lockType, RequirementReason.ASSERTION)) + requirements.add(LockRequirement(method, lockType, RequirementReason.ASSERTION)) } - if (isSwingMethod(method)) requirements.add(LockRequirement(source, LockType.EDT, RequirementReason.SWING_COMPONENT)) + if (isSwingMethod(method)) requirements.add(LockRequirement(method, LockType.EDT, RequirementReason.SWING_COMPONENT)) return requirements } diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsModel.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsModel.kt index bacb0f8280fb..50f1a7e8ada5 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsModel.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsModel.kt @@ -46,7 +46,7 @@ data class ExecutionPath( data class AnalysisResult( val method: PsiMethod, - val paths: List, + val paths: Set, val messageBusTopics: Set = emptySet(), val swingComponents: Set = emptySet(), ) diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsObsolete.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsObsolete.kt deleted file mode 100644 index c888d87210f2..000000000000 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsObsolete.kt +++ /dev/null @@ -1,133 +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.PsiMethod -import com.intellij.psi.PsiMethodCallExpression -import com.intellij.psi.JavaRecursiveElementVisitor -import com.intellij.psi.PsiMethodReferenceExpression - - -@Deprecated("LockReqsAnalyzer") -class LockReqsObsolete { - - companion object { - - private const val ASSERT_READ_ACCESS_METHOD = "assertReadAccess" - private const val THREADING_ASSERTIONS_CLASS = "com.intellij.util.concurrency.ThreadingAssertions" - 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 - } - - data class LockRequirement( - val type: LockCheckType, - val method: PsiMethod, - ) - - data class ExecutionPath( - val methodChain: List, - val lockRequirement: LockRequirement, - ) { - val pathString: String - get() = buildString { - append(methodChain.joinToString(" -> ") { - "${it.containingClass?.name}.${it.name}" - }) - append(" -> ") - when (lockRequirement.type) { - LockCheckType.ANNOTATION -> append("@RequiresReadLock") - LockCheckType.ASSERTION -> append("ThreadingAssertions.assertReadAccess()") - } - } - } - - data class AnalysisResult( - val method: PsiMethod, - val paths: List, - ) - } - - - private val processed = mutableSetOf() - - fun analyzeMethod(method: PsiMethod): List { - processed.clear() - val paths = mutableListOf() - val currentPath = mutableListOf() - processMethodDFS(method, currentPath, paths) - return paths - } - - private fun processMethodDFS( - method: PsiMethod, - currentPath: MutableList, - paths: MutableList, - ) { - - if (method in processed || currentPath.size > MAX_PATH_DEPTH) return - processed.add(method) - currentPath.add(method) - findLockChecks(method).forEach { check -> - paths.add(ExecutionPath(currentPath.toList(), LockRequirement(check, method))) - } - 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() { - 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) } - } - }) - return callees - } - - private fun findLockChecks(method: PsiMethod): List { - return buildList { - if (hasRequiresReadLockAnnotation(method)) add(LockCheckType.ANNOTATION) - if (hasAssertReadAccessCall(method)) add(LockCheckType.ASSERTION) - } - } - - private fun hasRequiresReadLockAnnotation(method: PsiMethod): Boolean { - return method.hasAnnotation(REQUIRES_READ_LOCK_ANNOTATION) - } - - private fun isAssertReadAccess(expression: PsiMethodCallExpression): Boolean { - return ASSERT_READ_ACCESS_METHOD == expression.methodExpression.referenceName && - THREADING_ASSERTIONS_CLASS == expression.resolveMethod()?.containingClass?.qualifiedName - } - - private fun hasAssertReadAccessCall(method: PsiMethod): Boolean { - var found = false - method.body?.accept(object : JavaRecursiveElementVisitor() { - override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { - if (!found) { - super.visitMethodCallExpression(expression) - if (isAssertReadAccess(expression)) found = true - } - } - }) - return found - } -} diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsPatterns.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsPatterns.kt index 2d9fba42f8c9..c5a2d418d237 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsPatterns.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsPatterns.kt @@ -38,7 +38,6 @@ class DefaultLockReqsPatterns : LockReqsPatterns { "com.intellij.util.concurrency.annotations.RequiresEdt" to LockType.EDT, "com.intellij.util.concurrency.annotations.RequiresBackgroundThread" to LockType.BGT, "com.intellij.util.concurrency.annotations.RequiresReadLockAbsence" to LockType.NO_READ, - "org.jetbrains.annotations.RequiresEdt" to LockType.EDT ) override val edtRequiredPackages: Set = setOf( diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactoryCompose.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt similarity index 87% rename from plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactoryCompose.kt rename to plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt index 272c7485735e..c58947258fdf 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactoryCompose.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactory.kt @@ -1,7 +1,6 @@ // 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.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory @@ -9,7 +8,7 @@ import com.intellij.ui.content.ContentFactory import org.jetbrains.idea.devkit.threadingModelHelper.ui.LockReqsToolWindow import org.jetbrains.jewel.bridge.JewelComposePanel -class LockReqsToolWindowFactoryCompose : ToolWindowFactory, DumbAware { +class LockReqsToolWindowFactory : ToolWindowFactory { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { val contentFactory = ContentFactory.getInstance() val panel = JewelComposePanel { LockReqsToolWindow(project) } diff --git a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactorySwing.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactorySwing.kt deleted file mode 100644 index 25013c08d647..000000000000 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/LockReqsToolWindowFactorySwing.kt +++ /dev/null @@ -1,37 +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.openapi.components.service -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -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 - -@Deprecated("Use LockReqsToolWindowFactoryCompose instead") -class LockReqsToolWindowFactorySwing : ToolWindowFactory { - - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val service = project.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.currentResult.paths.map{it.pathString}, 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-core/src/threadingModelHelper/ui/LockReqsToolWindow.kt b/plugins/devkit/devkit-core/src/threadingModelHelper/ui/LockReqsToolWindow.kt index c7b747ada263..dd9ec56540bc 100644 --- a/plugins/devkit/devkit-core/src/threadingModelHelper/ui/LockReqsToolWindow.kt +++ b/plugins/devkit/devkit-core/src/threadingModelHelper/ui/LockReqsToolWindow.kt @@ -33,7 +33,7 @@ internal fun LockReqsToolWindow(project: Project) { Column( modifier = Modifier.fillMaxSize() ) { - val paths = analysisResult?.paths ?: emptyList() + val paths = analysisResult?.paths?.toList() ?: emptyList() ToolWindowHeader( pathsCount = paths.size, diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AnnotationInChain.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AnnotationInChain.java index 1a64a226f9b2..f63adbf93693 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AnnotationInChain.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AnnotationInChain.java @@ -1,8 +1,5 @@ -import testutils.RequiresReadLock; -import testutils.ExpectedPath +import com.intellij.util.concurrency.annotations.RequiresReadLock; -@ExpectedPath("AnnotationInChain.testMethod -> AnnotationInChain.intermediateMethod -> " + - "AnnotationInChain.targetMethod -> @RequiresReadLock") class AnnotationInChain { void testMethod() { intermediateMethod(); diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AssertionInNestedBlock.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AssertionInNestedBlock.java index ce381f8ebbd4..8b25adff68b1 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AssertionInNestedBlock.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/AssertionInNestedBlock.java @@ -1,8 +1,6 @@ -import testutils.ThreadingAssertions; -import testutils.ExpectedPath +import com.intellij.util.concurrency.ThreadingAssertions; -@ExpectedPath("AnnotationInChain.testMethod -> @RequiresReadLock") -class AnnotationInChain { +class AssertionInNestedBlock { void testMethod() { if (true) { for (int i = 0; i < 10; i++) { diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/BothAnnotationAndAssertion.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/BothAnnotationAndAssertion.java index 6376efec1815..e0fcd942301e 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/BothAnnotationAndAssertion.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/BothAnnotationAndAssertion.java @@ -1,12 +1,9 @@ -import testutils.RequiresReadLock; -import testutils.ThreadingAssertions; -import test.ExpectedPath +import com.intellij.util.concurrency.ThreadingAssertions; +import com.intellij.util.concurrency.annotations.RequiresWriteLock; -@ExpectedPath("BothAnnotationAndAssertion.testMethod -> @RequiresReadLock") -@ExpectedPath("BothAnnotationAndAssertion.testMethod -> ThreadingAssertions.assertReadAccess()") class BothAnnotationAndAssertion { - @RequiresReadLock + @RequiresWriteLock void testMethod() { - ThreadingAssertions.assertReadAccess(); + ThreadingAssertions.assertBackgroundThread(); } } \ No newline at end of file diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/CyclicRecursiveCalls.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/CyclicRecursiveCalls.java index 4aceeecfa9ff..dffeb1edf9ca 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/CyclicRecursiveCalls.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/CyclicRecursiveCalls.java @@ -1,7 +1,6 @@ -import testutils.RequiresReadLock; -import testutils.ExpectedPath +import com.intellij.util.concurrency.annotations.RequiresReadLock; +import com.intellij.util.concurrency.ThreadingAssertions; -@ExpectedPath("CyclicRecursiveCalls.testMethod -> CyclicRecursiveCalls.methodB -> @RequiresReadLock") class CyclicRecursiveCalls { void testMethod() { methodB(); diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/DifferentClassesMethods.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/DifferentClassesMethods.java new file mode 100644 index 000000000000..0c242c35ab2a --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/DifferentClassesMethods.java @@ -0,0 +1,24 @@ +import com.intellij.util.concurrency.annotations.RequiresEdt; +import com.intellij.util.concurrency.ThreadingAssertions; + +class DifferentClassesMethods { + void testMethod() { + Helper helper = new Helper(); + helper.helperMethod(); + } + + class Helper { + void helperMethod() { + Service service = new Service(); + service.serviceMethod(); + } + } + + class Service { + @RequiresEdt + void serviceMethod() { + ThreadingAssertions.assertWriteAccess(); + } + } +} + diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/EDTSwingComponent.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/EDTSwingComponent.java new file mode 100644 index 000000000000..d325573a19fd --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/EDTSwingComponent.java @@ -0,0 +1,18 @@ +import javax.swing.JPanel; +import javax.swing.JButton; + +public class EDTSwingComponent { + public void testMethod() { + JPanel panel = new JPanel(); + panel.setVisible(true); // Requires EDT + panel.repaint(); // Safe method - no requirement + + setupButton(); + } + + private void setupButton() { + JButton button = new JButton("Click"); + button.setText("Updated"); + button.revalidate(); + } +} diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/LambdaWithMethodReference.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/LambdaWithMethodReference.java index 15aa6b5f406a..13cc6394eee9 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/LambdaWithMethodReference.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/LambdaWithMethodReference.java @@ -1,9 +1,7 @@ -import testutils.ThreadingAssertions; -import testutils.ExpectedPath +import com.intellij.util.concurrency.ThreadingAssertions; import java.util.Arrays; import java.util.List; -@ExpectedPath("LambdaWithMethodReference.testMethod -> LambdaWithMethodReference.processItem -> ThreadingAssertions.assertReadAccess()") class LambdaWithMethodReference { void testMethod() { List items = Arrays.asList("a", "b", "c"); diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MessageBusDetection.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MessageBusDetection.java index 4e2ba4706c4a..237a88142e6d 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MessageBusDetection.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MessageBusDetection.java @@ -1,4 +1,3 @@ -package test; public class C { public void testMessageBusDetection() { com.intellij.util.messages.MessageBus mb = null; diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MethodsInDifferentClasses.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MethodsInDifferentClasses.java deleted file mode 100644 index 185d7811ad96..000000000000 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MethodsInDifferentClasses.java +++ /dev/null @@ -1,27 +0,0 @@ -import testutils.RequiresReadLock; -import testutils.ThreadingAssertions; -import testutils.ExpectedPath; - -@ExpectedPath("MethodsInDifferentClasses.testMethod -> Helper.helperMethod -> Service.serviceMethod -> @RequiresReadLock") -@ExpectedPath("MethodsInDifferentClasses.testMethod -> Helper.helperMethod -> Service.serviceMethod -> ThreadingAssertions.assertReadAccess()") -class MethodsInDifferentClasses { - 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(); - } - } -} - diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MultipleAssertionsInMethod.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MultipleAssertionsInMethod.java index 5de0f9993415..ba1c9196ca6a 100644 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MultipleAssertionsInMethod.java +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/MultipleAssertionsInMethod.java @@ -1,7 +1,5 @@ -import testutils.ThreadingAssertions; -import testutils.ExpectedPath +import com.intellij.util.concurrency.ThreadingAssertions; -@ExpectedPath("MultipleAssertionsInMethod.testMethod -> ThreadingAssertions.assertReadAccess()") class MultipleAssertionsInMethod { void testMethod() { ThreadingAssertions.assertReadAccess(); diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/PolymorphicCallHandling.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/PolymorphicCallHandling.java deleted file mode 100644 index 4047d69cf1ba..000000000000 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/PolymorphicCallHandling.java +++ /dev/null @@ -1,20 +0,0 @@ -package test; - -import com.intellij.util.concurrency.annotations.RequiresReadLock; - -public class PolymorphicTest { - public void testMethod() { - Base base = new Derived(); - base.polymorphicMethod(); - } -} - -abstract class Base { - public abstract void polymorphicMethod(); -} - -class Derived extends Base { - @Override - @RequiresReadLock - public void polymorphicMethod() {} -} \ No newline at end of file diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SimpleSwingComponent.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SimpleSwingComponent.java deleted file mode 100644 index 91965ef10618..000000000000 --- a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SimpleSwingComponent.java +++ /dev/null @@ -1,11 +0,0 @@ -package test; - -import javax.swing.JPanel; - -public class SwingTest { - public void testMethod() { - JPanel panel = new JPanel(); - panel.paint(); - panel.repaint(); - } -} \ No newline at end of file diff --git a/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SubtypingPolymorphism.java b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SubtypingPolymorphism.java new file mode 100644 index 000000000000..b86cc429970f --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/threadingModelHelper/SubtypingPolymorphism.java @@ -0,0 +1,35 @@ +import com.intellij.util.concurrency.annotations.RequiresReadLock; +import com.intellij.util.concurrency.ThreadingAssertions; + +public class SubtypingPolymorphism { + public void testMethod() { + Service[] services = {new FileService(), new NetworkService(), new DatabaseService()}; + for (Service service : services) { + service.execute(); + } + } +} + +interface Service { + void execute(); +} + +class FileService implements Service { + @Override + @RequiresReadLock + public void execute() { } +} + +class UIService implements Service { + @Override + public void execute() { + ThreadingAssertions.assertEventDispatchThread(); + } +} + +class DBService implements Service { + @Override + public void execute() { + ThreadingAssertions.assertReadAccess(); + } +} \ No newline at end of file diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/threadingModelHelper/LockReqsUnitTest.kt b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/threadingModelHelper/LockReqsUnitTest.kt index 9f54eaeed1d2..c34041ff8fed 100644 --- a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/threadingModelHelper/LockReqsUnitTest.kt +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/threadingModelHelper/LockReqsUnitTest.kt @@ -15,68 +15,120 @@ class LockReqsUnitTest : BasePlatformTestCase() { override fun setUp() { super.setUp() analyzer = LockReqsAnalyzer() - myFixture.addFileToProject("mock/RequiresReadLock.java", """ - package mock; + myFixture.addFileToProject("com/intellij/util/concurrency/annotations.java", """ + package com.intellij.util.concurrency.annotations; public @interface RequiresReadLock {} + public @interface RequiresWriteLock {} + public @interface RequiresEdt {} + public @interface RequiresBackgroundThread {} + public @interface RequiresReadLockAbsence {} """.trimIndent()) - myFixture.addFileToProject("mock/ThreadingAssertions.java", """ - package mock; + myFixture.addFileToProject("com/intellij/util/concurrency/ThreadingAssertions.java", """ + package com.intellij.util.concurrency; public class ThreadingAssertions { public static void assertReadAccess() {} + public static void assertWriteAccess() {} + public static void assertWriteIntentReadAccess() {} + public static void assertEventDispatchThread() {} + public static void assertBackgroundThread() {} + } + """.trimIndent()) + myFixture.addFileToProject("mock/MessageBus.java", """ + package com.intellij.util.concurrency; + public interface MessageBus { + L syncPublisher(Class topic); } """.trimIndent()) } override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "threadingModelHelper/" - /* fun testNoLockRequirements() { - doTest("NoLockRequirements", "testMethod", emptyList()) - } - - fun testAnnotationInChain() { - doTest() + val result = doTest("NoLockRequirements", "testMethod") + val actualPaths = formatResult(result) + assertTrue(actualPaths.isEmpty()) } fun testAssertionInNestedBlock() { - doTest() + val result = doTest("AssertionInNestedBlock", "testMethod") + val expectedPaths = listOf("AssertionInNestedBlock.testMethod => READ.ASSERTION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) + } + + fun testAnnotationInChain() { + val result = doTest("AnnotationInChain", "testMethod") + val expectedPaths = listOf("AnnotationInChain.testMethod -> AnnotationInChain.intermediateMethod" + + " -> AnnotationInChain.targetMethod => READ.ANNOTATION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) } fun testBothAnnotationAndAssertion() { - doTest() + val result = doTest("BothAnnotationAndAssertion", "testMethod") + val expectedPaths = listOf("BothAnnotationAndAssertion.testMethod => WRITE.ANNOTATION", + "BothAnnotationAndAssertion.testMethod => BGT.ASSERTION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) } fun testCyclicRecursiveCalls() { - doTest() + val result = doTest("CyclicRecursiveCalls", "testMethod") + val expectedPaths = listOf("CyclicRecursiveCalls.testMethod -> CyclicRecursiveCalls.methodB => READ.ANNOTATION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) } - fun testMethodsInDifferentClasses() { - doTest() + fun testDifferentClassesMethods() { + val result = doTest("DifferentClassesMethods", "testMethod") + val expectedPaths = listOf("DifferentClassesMethods.testMethod -> Helper.helperMethod -> Service.serviceMethod => EDT.ANNOTATION", + "DifferentClassesMethods.testMethod -> Helper.helperMethod -> Service.serviceMethod => WRITE.ASSERTION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) } fun testMultipleAssertionsInMethod() { - doTest() + val result = doTest("MultipleAssertionsInMethod", "testMethod") + val expectedPaths = listOf("MultipleAssertionsInMethod.testMethod => READ.ASSERTION") + val actualPaths = formatResult(result) + assertEquals(1, actualPaths.size) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) } - fun testLambdaWithMethodReference() { - doTest() - }*/ - private fun doTest(className: String, methodName: String, expectedPaths: List) { + fun testLambdaWithMethodReference() { + val result = doTest("LambdaWithMethodReference", "testMethod") + val expectedPaths = listOf("LambdaWithMethodReference.testMethod -> LambdaWithMethodReference.processItem => READ.ASSERTION") + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) + + } + + fun testSubtypingPolymorphism() { + val result = doTest("SubtypingPolymorphism", "testMethod") + val expectedPaths = listOf( + "SubtypingPolymorphism.testMethod -> FileService.execute => READ.ANNOTATION", + "SubtypingPolymorphism.testMethod -> UIService.execute => EDT.ASSERTION", + "SubtypingPolymorphism.testMethod -> DBService.execute => READ.ASSERTION" + ) + val actualPaths = formatResult(result) + assertEquals(expectedPaths.sorted(), actualPaths.sorted()) + } + + private fun doTest(className: String, methodName: 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 == className } ?: error("Could not find method $methodName") + val targetMethod = targetClass.methods.find { it.name == methodName } ?: error("Could not find method $methodName") + return analyzer.analyzeMethod(targetMethod) + } - val result = analyzer.analyzeMethod(targetMethod) + private fun formatResult(result: AnalysisResult): List { val actualPaths = result.paths.map { path -> - buildString { - path.methodChain.joinToString(separator = " -> ", postfix = " -> ") { - "${it.method.containingClass?.name}.${it.method.name}" - } - "@${path.lockRequirement.lockType}" - } + val chain = path.methodChain.joinToString(" -> ") { "${it.method.containingClass?.name}.${it.method.name}" } + val requirement = "${path.lockRequirement.lockType.name}.${path.lockRequirement.requirementReason.name}" + "$chain => $requirement" } - assertEquals(expectedPaths, actualPaths) + return actualPaths } }