mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 13:02:30 +07:00
[threading] IJPL-179707 Create initial Analyzer, Action, and Service classes for locking requirements analysis
GitOrigin-RevId: 97f4331731dca7808eddbe403789719c911c3523
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0a394b8fa4
commit
08fe7ab2bc
@@ -103,5 +103,9 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.xerces" />
|
||||
<orderEntry type="module" module-name="intellij.platform.usageView.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.foundation" />
|
||||
<orderEntry type="library" name="studio-platform" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.ideLafBridge" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.ui" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -7,6 +7,14 @@
|
||||
<module name="intellij.properties.backend"/>
|
||||
<module name="intellij.properties.backend.psi"/>
|
||||
<module name="intellij.libraries.xerces"/>
|
||||
|
||||
<module name="intellij.platform.jewel.foundation"/>
|
||||
<module name="intellij.platform.jewel.ui"/>
|
||||
<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>
|
||||
@@ -709,6 +717,7 @@
|
||||
<notificationGroup id="testdata" displayType="BALLOON" bundle="messages.DevKitBundle" key="notification.group.testdata"/>
|
||||
|
||||
<!-- Threading Model Helper -->
|
||||
|
||||
<buildProcess.parametersProvider implementation="org.jetbrains.idea.devkit.threadingModelHelper.TMHBuildProcessParametersProvider"/>
|
||||
<registryKey key="tmh.generate.assertions.for.annotations" defaultValue="true"
|
||||
description="Generate assertions for @RequiresEdt and similar annotations.
|
||||
@@ -730,6 +739,11 @@
|
||||
|
||||
<generatedSourcesFilter implementation="org.jetbrains.idea.devkit.contentReport.ContentReportGeneratedSourcesFilter"/>
|
||||
|
||||
<toolWindow
|
||||
id="LockReqsToolWindow"
|
||||
factoryClass="org.jetbrains.idea.devkit.threadingModelHelper.LockReqsToolWindowFactory"
|
||||
anchor="bottom"/>
|
||||
|
||||
<!-- INTERNAL -->
|
||||
<consoleFilterProvider implementation="org.jetbrains.idea.devkit.run.ModulePathFilterProvider"/>
|
||||
|
||||
@@ -919,6 +933,7 @@
|
||||
<action internal="true" class="org.jetbrains.idea.devkit.actions.ShowSerializedXmlAction" id="ShowSerializedXml"/>
|
||||
<action internal="true" class="org.jetbrains.idea.devkit.actions.ShowHelpPageByIdAction" id="ShowHelpPageById"/>
|
||||
<action id="AnalyzeUnloadablePlugins" internal="true" class="org.jetbrains.idea.devkit.internal.AnalyzeUnloadablePluginsAction"/>
|
||||
<action id="LockReqsAnalysis" internal="true" class="org.jetbrains.idea.devkit.threadingModelHelper.LockReqsAction"/>
|
||||
<add-to-group group-id="Internal" anchor="last"/>
|
||||
</group>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.psi.PsiJavaFile
|
||||
import com.intellij.psi.PsiMethod
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.openapi.components.service
|
||||
|
||||
class LockReqsAction : AnAction() {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
|
||||
val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return
|
||||
|
||||
val element = psiFile.findElementAt(editor.caretModel.offset)
|
||||
val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return
|
||||
|
||||
project.service<LockReqsService>().updateResults(method)
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
|
||||
e.presentation.isEnabledAndVisible = psiFile is PsiJavaFile
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
|
||||
class LockReqsAnalyzer {
|
||||
|
||||
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
|
||||
|
||||
enum class LockCheckType {
|
||||
ANNOTATION,
|
||||
ASSERTION
|
||||
}
|
||||
|
||||
data class LockRequirement(
|
||||
val type: LockCheckType,
|
||||
val method: PsiMethod,
|
||||
)
|
||||
|
||||
data class ExecutionPath(
|
||||
val methodChain: List<PsiMethod>,
|
||||
val lockRequirement: LockRequirement,
|
||||
) {
|
||||
val pathString: String
|
||||
get() = buildString {
|
||||
append(methodChain.joinToString(" -> ") {
|
||||
"${it.containingClass?.name}.${it.name}" +
|
||||
when (lockRequirement.type) {
|
||||
LockCheckType.ANNOTATION -> "@RequiresReadLock"
|
||||
LockCheckType.ASSERTION -> "ThreadingAssertions.assertReadAccess()"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val processed = mutableSetOf<PsiMethod>()
|
||||
|
||||
fun analyzeMethod(method: PsiMethod): List<ExecutionPath> {
|
||||
processed.clear()
|
||||
val paths = mutableListOf<ExecutionPath>()
|
||||
val currentPath = mutableListOf<PsiMethod>()
|
||||
processMethodDFS(method, currentPath, paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
private fun processMethodDFS(
|
||||
method: PsiMethod,
|
||||
currentPath: MutableList<PsiMethod>,
|
||||
paths: MutableList<ExecutionPath>,
|
||||
) {
|
||||
|
||||
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 { callee -> processMethodDFS(callee, currentPath, paths) }
|
||||
currentPath.removeAt(currentPath.lastIndex)
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
})
|
||||
return callees
|
||||
}
|
||||
|
||||
private fun findLockChecks(method: PsiMethod): List<LockCheckType> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.ToolWindowManager
|
||||
import com.intellij.psi.PsiMethod
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class LockReqsService(private val project: Project) {
|
||||
private var currentResults: List<String> = emptyList()
|
||||
|
||||
fun updateResults(method: PsiMethod) {
|
||||
val analyzer = LockReqsAnalyzer()
|
||||
val paths = analyzer.analyzeMethod(method).map { it.pathString }
|
||||
currentResults = paths
|
||||
ToolWindowManager.getInstance(project).getToolWindow("LockReqsToolWindow")?.show()
|
||||
}
|
||||
|
||||
fun getCurrentResults(): List<String> = currentResults
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import org.jetbrains.jewel.ui.component.Text
|
||||
|
||||
|
||||
@Composable
|
||||
fun LockReqsToolWindow(service: LockReqsService) {
|
||||
val results = service.getCurrentResults()
|
||||
Column() {
|
||||
Text("Found ${results.size} execution paths:")
|
||||
Column() { items(results) { path -> Text(text = path) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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
|
||||
|
||||
class LockReqsToolWindowFactory : ToolWindowFactory {
|
||||
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
||||
val service = project.service<LockReqsService>()
|
||||
toolWindow.addComposeTab(DevKitBundle.message("tab.title.locking.requirements")) {
|
||||
LockReqsToolWindow(service)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import testutils.RequiresReadLock;
|
||||
import testutils.ExpectedPath
|
||||
|
||||
@ExpectedPath("AnnotationInChain.testMethod -> AnnotationInChain.intermediateMethod -> " +
|
||||
"AnnotationInChain.targetMethod -> @RequiresReadLock")
|
||||
class AnnotationInChain {
|
||||
void testMethod() {
|
||||
intermediateMethod();
|
||||
}
|
||||
|
||||
void intermediateMethod() {
|
||||
targetMethod();
|
||||
}
|
||||
|
||||
@RequiresReadLock
|
||||
void targetMethod() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import testutils.ThreadingAssertions;
|
||||
import testutils.ExpectedPath
|
||||
|
||||
@ExpectedPath("AnnotationInChain.testMethod -> @RequiresReadLock")
|
||||
class AnnotationInChain {
|
||||
void testMethod() {
|
||||
if (true) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import testutils.RequiresReadLock;
|
||||
import testutils.ThreadingAssertions;
|
||||
import test.ExpectedPath
|
||||
|
||||
@ExpectedPath("BothAnnotationAndAssertion.testMethod -> @RequiresReadLock")
|
||||
@ExpectedPath("BothAnnotationAndAssertion.testMethod -> ThreadingAssertions.assertReadAccess()")
|
||||
class BothAnnotationAndAssertion {
|
||||
@RequiresReadLock
|
||||
void testMethod() {
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import testutils.RequiresReadLock;
|
||||
import testutils.ExpectedPath
|
||||
|
||||
@ExpectedPath("CyclicRecursiveCalls.testMethod -> CyclicRecursiveCalls.methodB -> @RequiresReadLock")
|
||||
class CyclicRecursiveCalls {
|
||||
void testMethod() {
|
||||
methodB();
|
||||
}
|
||||
|
||||
@RequiresReadLock
|
||||
void methodB() {
|
||||
methodC();
|
||||
}
|
||||
|
||||
void methodC() {
|
||||
testMethod();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import testutils.ThreadingAssertions;
|
||||
import testutils.ExpectedPath
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ExpectedPath("LambdaWithMethodReference.testMethod -> LambdaWithMethodReference.processItem -> ThreadingAssertions.assertReadAccess()")
|
||||
class LambdaWithMethodReference {
|
||||
void testMethod() {
|
||||
List<String> items = Arrays.asList("a", "b", "c");
|
||||
items.forEach(this::processItem);
|
||||
}
|
||||
|
||||
void processItem(String item) {
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import testutils.ThreadingAssertions;
|
||||
import testutils.ExpectedPath
|
||||
|
||||
@ExpectedPath("MultipleAssertionsInMethod.testMethod -> ThreadingAssertions.assertReadAccess()")
|
||||
class MultipleAssertionsInMethod {
|
||||
void testMethod() {
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
doSomething();
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
if (condition()) {
|
||||
ThreadingAssertions.assertReadAccess();
|
||||
}
|
||||
}
|
||||
|
||||
void doSomething() {}
|
||||
boolean condition() { return true; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
public class NoLockRequirements {
|
||||
void testMethod() {
|
||||
regularMethod();
|
||||
}
|
||||
|
||||
void regularMethod() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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.PsiJavaFile
|
||||
import com.intellij.testFramework.TestDataPath
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil
|
||||
|
||||
|
||||
@TestDataPath($$"$CONTENT_ROOT/testData/threadingModelHelper/")
|
||||
class LockReqsUnitTest : BasePlatformTestCase() {
|
||||
|
||||
private lateinit var analyzer: LockReqsAnalyzer
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
analyzer = LockReqsAnalyzer()
|
||||
myFixture.addFileToProject("testutils/RequiresReadLock.java", """
|
||||
package testutils;
|
||||
public @interface RequiresReadLock {}
|
||||
""".trimIndent())
|
||||
myFixture.addFileToProject("testutils/ThreadingAssertions.java", """
|
||||
package testutils;
|
||||
public class ThreadingAssertions {
|
||||
public static void assertReadAccess() {}
|
||||
}
|
||||
""".trimIndent())
|
||||
myFixture.addFileToProject("testutils/ExpectedPath.java", """
|
||||
package testutils;
|
||||
import java.lang.annotation.*;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ExpectedPath {
|
||||
String value();
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "threadingModelHelper/"
|
||||
|
||||
fun testNoLockRequirements() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testAnnotationInChain() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testAssertionInNestedBlock() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testBothAnnotationAndAssertion() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testCyclicRecursiveCalls() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testMethodsInDifferentClasses() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testMultipleAssertionsInMethod() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testLambdaWithMethodReference() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
private fun doTest() {
|
||||
val fileName = "${getTestName(false)}.java"
|
||||
val sourceMethodName = "testMethod"
|
||||
val psiJavaFile = myFixture.configureByFile(fileName) as PsiJavaFile
|
||||
|
||||
val testClass = psiJavaFile.classes.first()
|
||||
val expectedPaths = testClass.annotations
|
||||
.filter { it.qualifiedName == "test.ExpectedPath" }
|
||||
.mapNotNull { it.findAttributeValue("value")?.text?.removeSurrounding("\"") }
|
||||
.sorted()
|
||||
|
||||
val sourceMethod = testClass.findMethodsByName(sourceMethodName, false).first()
|
||||
val actualPaths = analyzer.analyzeMethod(sourceMethod).map { it.pathString }.sorted()
|
||||
|
||||
assertEquals(expectedPaths, actualPaths)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user