[threading] IJPL-203569: Fix inspection, generalize quickfix to Kotlin

GitOrigin-RevId: eba99731eeb92f05b61f04354857b273f5d285a0
This commit is contained in:
Konstantin Nisht
2025-09-08 13:29:40 +00:00
committed by intellij-monorepo-bot
parent b730204479
commit d5a511bf25
14 changed files with 166 additions and 239 deletions
@@ -0,0 +1,21 @@
<html>
<body>
Reports usages of <code>Thread.ofVirtual</code> instead of <code>IntelliJVirtualThreads.ofVirtual</code>.
<p>
<code>Thread.ofVirtual</code> is a generic Java API for creation of virtual threads that is not suitable for IntelliJ Platform needs.
</p>
<p>
Quick fix performs replacement with <code>IntelliJVirtualThreads.ofVirtual</code>.
</p>
<p>
Example:
</p>
<pre><code lang="java">
// bad:
Thread thread = Thread.ofVirtual().start();
//good
Thread thread = IntelliJVirtualThreads.ofVirtual().start();
</code></pre>
</body>
</html>
@@ -784,6 +784,6 @@ compose.hot.reload.hot.reload.agent.has.been.saved=Restart your run configuratio
usage.type.module.in.content=In content
usage.type.module.in.dependencies=In dependencies
inspection.use.intellij.virtual.threads.display.name=Use IntelliJVirtualThreads.ofVirtual
inspection.use.intellij.virtual.threads.display.name=Use 'IntelliJVirtualThreads.ofVirtual'
inspection.use.intellij.virtual.threads.message=Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'
inspection.use.intellij.virtual.threads.fix.family.name=Replace with 'IntelliJVirtualThreads.ofVirtual()'
@@ -1,102 +1,84 @@
// 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.inspections;
package org.jetbrains.idea.devkit.inspections
import com.intellij.codeInspection.CleanupLocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.uast.UastHintedVisitorAdapter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.devkit.DevKitBundle;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UastContextKt;
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor;
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.util.IntentionFamilyName
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.intellij.uast.UastHintedVisitorAdapter
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.uast.*
import org.jetbrains.uast.generate.getUastElementFactory
import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
/**
* Suggest using IntelliJVirtualThreads.ofVirtual instead of Thread.ofVirtual
*/
public final class UseIntelliJVirtualThreadsInspection extends DevKitUastInspectionBase implements CleanupLocalInspectionTool {
private static final String THREAD_CLASS = "java.lang.Thread";
private static final String INTELLIJ_VIRTUAL_THREADS_FQN = "com.intellij.virtualThreads.IntelliJVirtualThreads";
@SuppressWarnings("unchecked")
private static final Class<? extends UElement>[] HINTS = new Class[]{UCallExpression.class};
@Override
protected boolean isAllowed(@NotNull ProblemsHolder holder) {
return DevKitInspectionUtil.isAllowed(holder.getFile()) &&
DevKitInspectionUtil.isClassAvailable(holder, INTELLIJ_VIRTUAL_THREADS_FQN);
class UseIntelliJVirtualThreadsInspection : DevKitUastInspectionBase() {
override fun isAllowed(holder: ProblemsHolder): Boolean {
return DevKitInspectionUtil.isAllowed(holder.file) &&
DevKitInspectionUtil.isClassAvailable(holder, INTELLIJ_VIRTUAL_THREADS_FQN)
}
@Override
public @NotNull PsiElementVisitor buildInternalVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return UastHintedVisitorAdapter.create(holder.getFile().getLanguage(), new AbstractUastNonRecursiveVisitor() {
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (!"ofVirtual".equals(node.getMethodName())) return super.visitCallExpression(node);
PsiMethod method = node.resolve();
boolean isThreadOfVirtual = false;
if (method != null) {
if (!method.hasModifierProperty(PsiModifier.STATIC)) return super.visitCallExpression(node);
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) return super.visitCallExpression(node);
String qName = containingClass.getQualifiedName();
isThreadOfVirtual = THREAD_CLASS.equals(qName) || "Thread".equals(containingClass.getName());
public override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return UastHintedVisitorAdapter.create(holder.file.getLanguage(), object : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
val method = node.takeIf {
it.methodName == "ofVirtual"
}?.resolve() ?: return super.visitCallExpression(node)
val clazz = method.containingClass ?: return super.visitCallExpression(node)
if (clazz.qualifiedName != THREAD_FQN) {
return super.visitCallExpression(node)
}
else {
// Fallback for environments where JDK doesn't provide Thread.ofVirtual (e.g., tests on older JDK):
PsiElement sp = node.getSourcePsi();
if (sp instanceof PsiMethodCallExpression) {
PsiExpression qualifier = ((PsiMethodCallExpression)sp).getMethodExpression().getQualifierExpression();
String qualifierText = qualifier == null ? null : qualifier.getText();
isThreadOfVirtual = THREAD_CLASS.equals(qualifierText) || "Thread".equals(qualifierText);
}
}
if (!isThreadOfVirtual) return super.visitCallExpression(node);
PsiElement psi = node.getSourcePsi();
val psi = node.sourcePsi
if (psi != null) {
LocalQuickFix[] fixes = psi.getLanguage().is(JavaLanguage.INSTANCE)
? new LocalQuickFix[]{new ReplaceWithIntelliJVirtualThreadsFix()}
: LocalQuickFix.EMPTY_ARRAY;
holder.registerProblem(psi, DevKitBundle.message("inspection.use.intellij.virtual.threads.message"), fixes);
val fixes: Array<LocalQuickFix> = if (psi.getLanguage().`is`(JavaLanguage.INSTANCE) || psi.language.id == "kotlin") {
arrayOf(ReplaceWithIntelliJVirtualThreadsFix())
}
else {
LocalQuickFix.EMPTY_ARRAY
}
holder.registerProblem(psi, DevKitBundle.message("inspection.use.intellij.virtual.threads.message"), *fixes)
}
return super.visitCallExpression(node);
return super.visitCallExpression(node)
}
}, HINTS);
}, HINTS)
}
private static class ReplaceWithIntelliJVirtualThreadsFix implements LocalQuickFix {
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return DevKitBundle.message("inspection.use.intellij.virtual.threads.fix.family.name");
private class ReplaceWithIntelliJVirtualThreadsFix : LocalQuickFix {
@IntentionFamilyName
override fun getFamilyName(): @IntentionFamilyName String {
return DevKitBundle.message("inspection.use.intellij.virtual.threads.fix.family.name")
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getPsiElement();
PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class, false);
if (call == null && element instanceof PsiMethodCallExpression) {
call = (PsiMethodCallExpression) element;
}
if (call == null) return;
PsiReferenceExpression methodExpression = call.getMethodExpression();
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiExpression newMethodRef = factory.createExpressionFromText(
INTELLIJ_VIRTUAL_THREADS_FQN + ".ofVirtual", call
);
PsiElement replaced = methodExpression.replace(newMethodRef);
JavaCodeStyleManager.getInstance(project).shortenClassReferences(replaced);
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement
val uCall = element.toUElement() ?: return
val factory = uCall.getUastElementFactory(project) ?: return
val newReceiver = factory.createQualifiedReference(INTELLIJ_VIRTUAL_THREADS_FQN, uCall.sourcePsi) ?: return
val newCall = factory.createCallExpression(
receiver = newReceiver,
methodName = "ofVirtual",
parameters = emptyList(),
expectedReturnType = null,
kind = UastCallKind.METHOD_CALL,
context = uCall.sourcePsi
) ?: return
val parent = uCall.uastParent
// we check for parent in case of `Thread.ofVirtual()` calls in Kotlin
val replacementUastElement = parent as? UQualifiedReferenceExpression ?: uCall
replacementUastElement.replace(newCall)
}
}
}
private const val THREAD_FQN = "java.lang.Thread"
private const val INTELLIJ_VIRTUAL_THREADS_FQN = "com.intellij.concurrency.virtualThreads.IntelliJVirtualThreads"
private val HINTS: Array<Class<out UElement>> = arrayOf(UCallExpression::class.java)
@@ -1,11 +1,7 @@
import com.intellij.virtualThreads.IntelliJVirtualThreads;
import java.lang.Thread;
class UseIntelliJVirtualThreads {
java.lang.Thread.Builder m() {
return <warning descr="Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'">java.lang.Thread.ofVirtual()<caret></warning>;
}
void ok() {
IntelliJVirtualThreads.ofVirtual();
public static void main(String[] args) {
<warning descr="Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'">Thread.ofVirtual()<caret></warning>;
}
}
@@ -1,11 +1,9 @@
import com.intellij.virtualThreads.IntelliJVirtualThreads;
import com.intellij.concurrency.virtualThreads.IntelliJVirtualThreads;
import java.lang.Thread;
class UseIntelliJVirtualThreads {
java.lang.Thread.Builder m() {
return IntelliJVirtualThreads.ofVirtual();
}
void ok() {
IntelliJVirtualThreads.ofVirtual();
public static void main(String[] args) {
IntelliJVirtualThreads.ofVirtual()<caret>;
}
}
@@ -12,7 +12,7 @@ import java.util.stream.Stream;
public class DevkitInspectionsRegistrationCheckTest extends BasePlatformTestCase {
private static final int EXPECTED_INSPECTIONS_NUMBER = 83;
private static final int EXPECTED_INSPECTIONS_NUMBER = 84;
/**
* Inspections that are finished and intentionally disabled.
@@ -1,48 +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.inspections;
import com.intellij.testFramework.TestDataPath;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.util.lang.JavaVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil;
@TestDataPath("$CONTENT_ROOT/testData/inspections/useIntelliJVirtualThreads")
public class UseIntelliJVirtualThreadsInspectionFixTest extends org.jetbrains.idea.devkit.inspections.quickfix.LightDevKitInspectionFixTestBase {
@Override
protected String getBasePath() {
return DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/useIntelliJVirtualThreads";
}
@Override
protected @NotNull String getFileExtension() {
return "java";
}
@Override
protected com.intellij.testFramework.LightProjectDescriptor getProjectDescriptor() {
return new DefaultLightProjectDescriptor(() -> IdeaTestUtil.getMockJdk(JavaVersion.compose(21)));
}
@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.enableInspections(new UseIntelliJVirtualThreadsInspection());
// Provide IntelliJVirtualThreads class used by the inspection
myFixture.addClass(
"package com.intellij.virtualThreads;" +
"public final class IntelliJVirtualThreads {" +
" private IntelliJVirtualThreads() {}" +
" public static java.lang.Thread.Builder ofVirtual() { return null; }" +
"}"
);
}
public void testUseIntelliJVirtualThreads() {
doTest(org.jetbrains.idea.devkit.DevKitBundle.message("inspection.use.intellij.virtual.threads.fix.family.name"));
}
}
@@ -1,41 +1,25 @@
// 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.inspections;
package org.jetbrains.idea.devkit.inspections
import com.intellij.testFramework.TestDataPath;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil;
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil
import org.jetbrains.idea.devkit.inspections.quickfix.LightDevKitInspectionFixTestBase
import org.jetbrains.idea.devkit.inspections.quickfix.UseIntelliJVirtualThreadsInspectionTestBase
import java.util.function.Supplier
@TestDataPath("$CONTENT_ROOT/testData/inspections/useIntelliJVirtualThreads")
public class UseIntelliJVirtualThreadsInspectionTest extends LightJavaCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/useIntelliJVirtualThreads";
}
@Override
protected com.intellij.testFramework.LightProjectDescriptor getProjectDescriptor() {
return new com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor(() ->
com.intellij.testFramework.IdeaTestUtil.getMockJdk(com.intellij.util.lang.JavaVersion.compose(21))
);
@TestDataPath($$"$CONTENT_ROOT/testData/inspections/useIntelliJVirtualThreads")
class UseIntelliJVirtualThreadsInspectionTest : UseIntelliJVirtualThreadsInspectionTestBase() {
override fun getBasePath(): String {
return DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/useIntelliJVirtualThreads"
}
@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.enableInspections(new UseIntelliJVirtualThreadsInspection());
// Provide IntelliJVirtualThreads class used by the inspection
myFixture.addClass(
"package com.intellij.virtualThreads;" +
"public final class IntelliJVirtualThreads {" +
" private IntelliJVirtualThreads() {}" +
" public static java.lang.Thread.Builder ofVirtual() { return null; }" +
"}"
);
fun testUseIntelliJVirtualThreads() {
doTest("Replace with 'IntelliJVirtualThreads.ofVirtual()'")
}
public void testUseIntelliJVirtualThreads() {
myFixture.testHighlighting(getTestName(false) + ".java");
}
override fun getFileExtension(): String = "java"
}
@@ -0,0 +1,7 @@
import java.lang.Thread
class UseIntelliJVirtualThreadsKotlin {
fun foo() {
Thread.<warning descr="Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'">ofV<caret>irtual()</warning>
}
}
@@ -1,10 +0,0 @@
import com.intellij.virtualThreads.IntelliJVirtualThreads
class UseIntelliJVirtualThreadsKotlin {
fun m(): Thread.Builder {
return java.lang.Thread.<warning descr="Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'">ofVirtual()</warning>
}
fun ok() {
IntelliJVirtualThreads.ofVirtual()
}
}
@@ -0,0 +1,8 @@
import com.intellij.concurrency.virtualThreads.IntelliJVirtualThreads
import java.lang.Thread
class UseIntelliJVirtualThreadsKotlin {
fun foo() {
IntelliJVirtualThreads.ofVirtual()
}
}
@@ -1,10 +0,0 @@
import com.intellij.virtualThreads.IntelliJVirtualThreads
class UseIntelliJVirtualThreadsKotlin {
fun m(): Thread.Builder {
return java.lang.Thread.<warning descr="Use 'IntelliJVirtualThreads.ofVirtual()' instead of 'Thread.ofVirtual()'">ofVirtual()</warning>
}
fun ok() {
IntelliJVirtualThreads.ofVirtual()
}
}
@@ -1,50 +1,19 @@
// 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.kotlin.inspections;
package org.jetbrains.idea.devkit.kotlin.inspections
import com.intellij.testFramework.TestDataPath;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.util.lang.JavaVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil;
import org.jetbrains.idea.devkit.inspections.UseIntelliJVirtualThreadsInspection;
import com.intellij.testFramework.TestDataPath
import org.jetbrains.idea.devkit.inspections.quickfix.UseIntelliJVirtualThreadsInspectionTestBase
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil
@TestDataPath("$CONTENT_ROOT/testData/inspections/useIntelliJVirtualThreads")
public class KtUseIntelliJVirtualThreadsInspectionTest extends LightJavaCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return DevkitKtTestsUtil.TESTDATA_PATH + "inspections/useIntelliJVirtualThreads";
@TestDataPath($$"$CONTENT_ROOT/testData/inspections/useIntelliJVirtualThreads")
class KtUseIntelliJVirtualThreadsInspectionTest : UseIntelliJVirtualThreadsInspectionTestBase() {
override fun getBasePath(): String {
return DevkitKtTestsUtil.TESTDATA_PATH + "inspections/useIntelliJVirtualThreads"
}
@Override
protected com.intellij.testFramework.LightProjectDescriptor getProjectDescriptor() {
return new DefaultLightProjectDescriptor(() -> IdeaTestUtil.getMockJdk(JavaVersion.compose(21)));
fun testUseIntelliJVirtualThreads() {
doTest("Replace with 'IntelliJVirtualThreads.ofVirtual()'")
}
@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.enableInspections(new UseIntelliJVirtualThreadsInspection());
// Provide IntelliJVirtualThreads class used by the inspection
myFixture.addClass(
"package com.intellij.virtualThreads;" +
"public final class IntelliJVirtualThreads {" +
" private IntelliJVirtualThreads() {}" +
" public static java.lang.Thread.Builder ofVirtual() { return null; }" +
"}"
);
}
@NotNull
@Override
protected String getTestName(boolean lowercaseFirstLetter) {
return super.getTestName(lowercaseFirstLetter).replace("_", "");
}
public void testUseIntelliJVirtualThreadsKotlin() {
myFixture.testHighlighting(getTestName(false) + ".kt");
}
override fun getFileExtension(): String = "kt"
}
@@ -0,0 +1,30 @@
// 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.inspections.quickfix
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor
import org.jetbrains.idea.devkit.inspections.UseIntelliJVirtualThreadsInspection
import java.util.function.Supplier
abstract class UseIntelliJVirtualThreadsInspectionTestBase : LightDevKitInspectionFixTestBase() {
override fun getProjectDescriptor(): LightProjectDescriptor {
return DefaultLightProjectDescriptor(Supplier { IdeaTestUtil.getMockJdk21() })
}
@Throws(Exception::class)
override fun setUp() {
super.setUp()
myFixture.enableInspections(UseIntelliJVirtualThreadsInspection())
myFixture.addClass(
"""package com.intellij.concurrency.virtualThreads;
|public final class IntelliJVirtualThreads {
| private IntelliJVirtualThreads() {}
| public static java.lang.Thread.Builder ofVirtual() {
| return null;
| }
|}""".trimMargin()
)
}
}