From 16d1a6aa83e6d2fbf6e4ecd766c2b734fb0bde0c Mon Sep 17 00:00:00 2001 From: "Konstantin.Hudyakov" Date: Sun, 1 Mar 2026 18:03:42 +0200 Subject: [PATCH] [devkit] IJPL-237218 Add an inspection that warns about JComponent.setToolTipText(String) usage `com.intellij.ide.HelpTooltipKt.setToolTipText(HtmlChunk)` should be used instead. Also, provide simple quick-fixes. (cherry picked from commit 3b084a7e22ca0389feafb7c940a1d554b982549c) IJ-CR-196509 GitOrigin-RevId: 83f52545a3ed3b33da0edc1197e59144b20d02af --- .../src/com/intellij/ide/HelpTooltip.kt | 1 + .../UseHtmlChunkToolTip.html | 23 +++++ .../resources/intellij.devkit.core.xml | 15 ++++ .../messages/DevKitBundle.properties | 5 ++ .../UseHtmlChunkToolTipInspection.kt | 85 +++++++++++++++++++ .../UseHtmlChunkToolTipFixProvider.kt | 65 ++++++++++++++ .../SetToolTipTextWithString.java | 11 +++ .../SetToolTipTextWithStringFix.java | 7 ++ .../SetToolTipTextWithStringFixRaw.java | 7 ++ .../SetToolTipTextWithStringFixRaw_after.java | 10 +++ .../SetToolTipTextWithStringFix_after.java | 10 +++ .../UseHtmlChunkToolTipInspectionTest.kt | 27 ++++++ .../KtUseHtmlChunkToolTipInspectionTest.kt | 52 ++++++++++++ .../SetToolTipTextPropertyStyleFix.kt | 9 ++ .../SetToolTipTextPropertyStyleFix_after.kt | 11 +++ .../SetToolTipTextWithString.kt | 15 ++++ .../SetToolTipTextWithStringFix.kt | 9 ++ .../SetToolTipTextWithStringFixRaw.kt | 9 ++ .../SetToolTipTextWithStringFixRaw_after.kt | 11 +++ .../SetToolTipTextWithStringFix_after.kt | 11 +++ .../UseHtmlChunkToolTipInspectionTestBase.kt | 37 ++++++++ .../resources/intellij.kotlin.devkit.xml | 3 + .../KtUseHtmlChunkToolTipFixProvider.kt | 80 +++++++++++++++++ 23 files changed, 513 insertions(+) create mode 100644 plugins/devkit/devkit-core/resources/inspectionDescriptions/UseHtmlChunkToolTip.html create mode 100644 plugins/devkit/devkit-core/src/inspections/UseHtmlChunkToolTipInspection.kt create mode 100644 plugins/devkit/devkit-core/src/inspections/quickfix/UseHtmlChunkToolTipFixProvider.kt create mode 100644 plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.java create mode 100644 plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.java create mode 100644 plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.java create mode 100644 plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.java create mode 100644 plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.java create mode 100644 plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UseHtmlChunkToolTipInspectionTest.kt create mode 100644 plugins/devkit/devkit-kotlin-fir-tests/testSrc/org/jetbrains/idea/devkit/k2/inspections/KtUseHtmlChunkToolTipInspectionTest.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix_after.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.kt create mode 100644 plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.kt create mode 100644 plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/UseHtmlChunkToolTipInspectionTestBase.kt create mode 100644 plugins/devkit/intellij.kotlin.devkit/src/inspections/KtUseHtmlChunkToolTipFixProvider.kt diff --git a/platform/platform-api/src/com/intellij/ide/HelpTooltip.kt b/platform/platform-api/src/com/intellij/ide/HelpTooltip.kt index 07dba95bd40d..68bcf869a39d 100644 --- a/platform/platform-api/src/com/intellij/ide/HelpTooltip.kt +++ b/platform/platform-api/src/com/intellij/ide/HelpTooltip.kt @@ -13,5 +13,6 @@ import javax.swing.JComponent * prefer using [HtmlChunk.text] to avoid accidental HTML injections. */ fun JComponent.setToolTipText(html: HtmlChunk?) { + @Suppress("UseHtmlChunkToolTip") this.toolTipText = html?.toString() } \ No newline at end of file diff --git a/plugins/devkit/devkit-core/resources/inspectionDescriptions/UseHtmlChunkToolTip.html b/plugins/devkit/devkit-core/resources/inspectionDescriptions/UseHtmlChunkToolTip.html new file mode 100644 index 000000000000..08d75307fbb9 --- /dev/null +++ b/plugins/devkit/devkit-core/resources/inspectionDescriptions/UseHtmlChunkToolTip.html @@ -0,0 +1,23 @@ + + +Reports usages of JComponent.setToolTipText(String). +

+ Passing raw strings may cause accidental HTML injections because IntelliJ Platform interprets HTML markup when renders the tooltip text. + com.intellij.ide.HelpTooltipKt.setToolTipText(HtmlChunk) should be used instead of + JComponent.setToolTipText(String). + Use HtmlChunk.text() to safely escape text, especially in cases when text can contain user input + or project content (for example, file names). + Or use HtmlChunk.raw() if the tooltip supposes to contain HTML. +

+

+ Example: +

+

+// Bad — raw string may contain HTML injection:
+component.setToolTipText(text)
+
+// Good — text is escaped:
+component.setToolTipText(HtmlChunk.text(text))
+
+ + \ No newline at end of file diff --git a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml index b570c596b028..c8164008ca41 100644 --- a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml +++ b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml @@ -253,6 +253,12 @@ enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.idea.devkit.inspections.UseIntelliJVirtualThreadsInspection" key="inspection.use.intellij.virtual.threads.display.name"/> + + @@ -830,6 +839,12 @@ + + + diff --git a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties index d884546240df..0a1e0387874a 100644 --- a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties +++ b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties @@ -850,6 +850,11 @@ inspection.use.intellij.virtual.threads.display.name=Use 'IntelliJVirtualThreads 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()' +inspection.use.html.chunk.tooltip.display.name=Use 'setToolTipText(HtmlChunk)' to avoid HTML injections +inspections.use.html.chunk.tooltip.message='JComponent.setToolTipText(String)' used instead of safe 'setToolTipText(HtmlChunk)' +inspections.use.html.chunk.tooltip.fix.wrap.text.family.name=Wrap with 'HtmlChunk.text()' +inspections.use.html.chunk.tooltip.fix.wrap.raw.family.name=Wrap with 'HtmlChunk.raw()' + category.platform.title=IntelliJ Platform category.platform.description=APIs provided by IntelliJ Platform modules category.plugins.title=Plugins diff --git a/plugins/devkit/devkit-core/src/inspections/UseHtmlChunkToolTipInspection.kt b/plugins/devkit/devkit-core/src/inspections/UseHtmlChunkToolTipInspection.kt new file mode 100644 index 000000000000..31e7b880ee10 --- /dev/null +++ b/plugins/devkit/devkit-core/src/inspections/UseHtmlChunkToolTipInspection.kt @@ -0,0 +1,85 @@ +// Copyright 2000-2026 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.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.InheritanceUtil +import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.idea.devkit.DevKitBundle +import org.jetbrains.idea.devkit.inspections.quickfix.UseHtmlChunkToolTipFixProviders +import org.jetbrains.uast.UBinaryExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UastBinaryOperator +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor + +/** + * Reports usages of [javax.swing.JComponent.setToolTipText] with a [String] argument + * and suggests using [com.intellij.ide.setToolTipText] to prevent accidental HTML injections. + * + * Detects both explicit method calls (`comp.setToolTipText(text)`) and + * Kotlin property-style setters (`comp.toolTipText = text`). + */ +internal class UseHtmlChunkToolTipInspection : DevKitUastInspectionBase() { + override fun isAllowed(holder: ProblemsHolder): Boolean { + return DevKitInspectionUtil.isAllowed(holder.file) && + DevKitInspectionUtil.isClassAvailable(holder, HTML_CHUNK_FQN) && + DevKitInspectionUtil.isClassAvailable(holder, HELP_TOOLTIP_KT_FQN) + } + + override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return UastHintedVisitorAdapter.create(holder.file.language, object : AbstractUastNonRecursiveVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (node.methodName != SET_TIP_TEXT_METHOD_NAME) return super.visitCallExpression(node) + val method = node.resolve() ?: return super.visitCallExpression(node) + if (!isSetToolTipTextOnJComponent(method)) return super.visitCallExpression(node) + if (isNullLiteral(node.valueArguments.firstOrNull())) return super.visitCallExpression(node) + + val psi = node.sourcePsi ?: return super.visitCallExpression(node) + registerProblem(psi) + return super.visitCallExpression(node) + } + + override fun visitBinaryExpression(node: UBinaryExpression): Boolean { + if (node.operator != UastBinaryOperator.ASSIGN) return super.visitBinaryExpression(node) + val method = node.resolveOperator() ?: return super.visitBinaryExpression(node) + if (method.name != SET_TIP_TEXT_METHOD_NAME) return super.visitBinaryExpression(node) + if (!isSetToolTipTextOnJComponent(method)) return super.visitBinaryExpression(node) + if (isNullLiteral(node.rightOperand)) return super.visitBinaryExpression(node) + + val psi = node.sourcePsi ?: return super.visitBinaryExpression(node) + registerProblem(psi) + return super.visitBinaryExpression(node) + } + + private fun registerProblem(psi: com.intellij.psi.PsiElement) { + val fixProvider = UseHtmlChunkToolTipFixProviders.forLanguage(psi.language) + val fixes = fixProvider?.createFixes(psi) ?: LocalQuickFix.EMPTY_ARRAY + holder.registerProblem(psi, DevKitBundle.message("inspections.use.html.chunk.tooltip.message"), *fixes) + } + }, HINTS) + } +} + +private const val SET_TIP_TEXT_METHOD_NAME = "setToolTipText" +private const val JCOMPONENT_FQN = "javax.swing.JComponent" +private const val HTML_CHUNK_FQN = "com.intellij.openapi.util.text.HtmlChunk" +private const val HELP_TOOLTIP_KT_FQN = "com.intellij.ide.HelpTooltipKt" + +private val HINTS: Array> = arrayOf(UCallExpression::class.java, UBinaryExpression::class.java) + +private fun isNullLiteral(expr: UExpression?): Boolean { + return expr is ULiteralExpression && expr.isNull +} + +private fun isSetToolTipTextOnJComponent(method: PsiMethod): Boolean { + val clazz = method.containingClass ?: return false + if (!InheritanceUtil.isInheritor(clazz, JCOMPONENT_FQN)) return false + val params = method.parameterList.parameters + return params.size == 1 && params[0].type.canonicalText == CommonClassNames.JAVA_LANG_STRING +} diff --git a/plugins/devkit/devkit-core/src/inspections/quickfix/UseHtmlChunkToolTipFixProvider.kt b/plugins/devkit/devkit-core/src/inspections/quickfix/UseHtmlChunkToolTipFixProvider.kt new file mode 100644 index 000000000000..e1ec72b30741 --- /dev/null +++ b/plugins/devkit/devkit-core/src/inspections/quickfix/UseHtmlChunkToolTipFixProvider.kt @@ -0,0 +1,65 @@ +// Copyright 2000-2026 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.codeInsight.intention.PriorityAction +import com.intellij.codeInsight.intention.PriorityAction.Priority +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.lang.LanguageExtension +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.IntellijInternalApi +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.idea.devkit.DevKitBundle + +private val EP_NAME = ExtensionPointName.create("DevKit.lang.useHtmlChunkToolTipFixProvider") + +internal object UseHtmlChunkToolTipFixProviders : LanguageExtension(EP_NAME.name) + +@IntellijInternalApi +@ApiStatus.Internal +interface UseHtmlChunkToolTipFixProvider { + fun createFixes(element: PsiElement): Array +} + +private const val SET_TOOLTIP_TEXT_METHOD_FQN = "com.intellij.ide.HelpTooltipKt.setToolTipText" +private const val HTML_CHUNK_FQN = "com.intellij.openapi.util.text.HtmlChunk" + +internal class JavaUseHtmlChunkToolTipFixProvider : UseHtmlChunkToolTipFixProvider { + override fun createFixes(element: PsiElement): Array { + return arrayOf( + JavaWrapWithHtmlChunkFix("text", "inspections.use.html.chunk.tooltip.fix.wrap.text.family.name", Priority.TOP), + JavaWrapWithHtmlChunkFix("raw", "inspections.use.html.chunk.tooltip.fix.wrap.raw.family.name", Priority.HIGH), + ) + } +} + +/** + * Replaces `comp.setToolTipText(arg)` with `HelpTooltipKt.setToolTipText(comp, HtmlChunk.text/raw(arg))`. + */ +private class JavaWrapWithHtmlChunkFix( + private val wrapMethodName: String, + private val familyNameKey: String, + private val fixPriority: Priority, +) : LocalQuickFix, PriorityAction { + override fun getPriority(): Priority = fixPriority + + @IntentionFamilyName + override fun getFamilyName(): String = DevKitBundle.message(familyNameKey) + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val callExpr = descriptor.psiElement as? PsiMethodCallExpression ?: return + val qualifierExpr = callExpr.methodExpression.qualifierExpression ?: return + val arg = callExpr.argumentList.expressions.firstOrNull() ?: return + + val newText = "$SET_TOOLTIP_TEXT_METHOD_FQN(${qualifierExpr.text}, $HTML_CHUNK_FQN.$wrapMethodName(${arg.text}))" + val newExpr = PsiElementFactory.getInstance(project).createExpressionFromText(newText, callExpr) + val replaced = callExpr.replace(newExpr) + JavaCodeStyleManager.getInstance(project).shortenClassReferences(replaced) + } +} diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.java b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.java new file mode 100644 index 000000000000..2f1bf3bfbc28 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.java @@ -0,0 +1,11 @@ +import javax.swing.*; + +class SetToolTipTextWithString { + void test(JComponent comp) { + comp.setToolTipText("hello"); + comp.setToolTipText(getString()); + comp.setToolTipText(null); + } + + String getString() { return ""; } +} diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.java b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.java new file mode 100644 index 000000000000..838dbd0c2f69 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.java @@ -0,0 +1,7 @@ +import javax.swing.*; + +class SetToolTipTextWithStringFix { + void test(JComponent comp) { + comp.setToolTipText("hello"); + } +} diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.java b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.java new file mode 100644 index 000000000000..d00e7a357ab1 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.java @@ -0,0 +1,7 @@ +import javax.swing.*; + +class SetToolTipTextWithStringFixRaw { + void test(JComponent comp) { + comp.setToolTipText("hello"); + } +} diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.java b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.java new file mode 100644 index 000000000000..f8e0302fbf95 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.java @@ -0,0 +1,10 @@ +import com.intellij.ide.HelpTooltipKt; +import com.intellij.openapi.util.text.HtmlChunk; + +import javax.swing.*; + +class SetToolTipTextWithStringFixRaw { + void test(JComponent comp) { + HelpTooltipKt.setToolTipText(comp, HtmlChunk.raw("hello")); + } +} diff --git a/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.java b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.java new file mode 100644 index 000000000000..99607b954736 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.java @@ -0,0 +1,10 @@ +import com.intellij.ide.HelpTooltipKt; +import com.intellij.openapi.util.text.HtmlChunk; + +import javax.swing.*; + +class SetToolTipTextWithStringFix { + void test(JComponent comp) { + HelpTooltipKt.setToolTipText(comp, HtmlChunk.text("hello")); + } +} diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UseHtmlChunkToolTipInspectionTest.kt b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UseHtmlChunkToolTipInspectionTest.kt new file mode 100644 index 000000000000..f01229eefbf3 --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UseHtmlChunkToolTipInspectionTest.kt @@ -0,0 +1,27 @@ +// Copyright 2000-2026 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 org.jetbrains.idea.devkit.DevkitJavaTestsUtil +import org.jetbrains.idea.devkit.inspections.quickfix.UseHtmlChunkToolTipInspectionTestBase + +@TestDataPath($$"$CONTENT_ROOT/testData/inspections/useHtmlChunkToolTip") +class UseHtmlChunkToolTipInspectionTest : UseHtmlChunkToolTipInspectionTestBase() { + override fun getBasePath(): String { + return DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/useHtmlChunkToolTip" + } + + override fun getFileExtension(): String = "java" + + fun testSetToolTipTextWithString() { + doTest() + } + + fun testSetToolTipTextWithStringFix() { + doTest("Wrap with 'HtmlChunk.text()'") + } + + fun testSetToolTipTextWithStringFixRaw() { + doTest("Wrap with 'HtmlChunk.raw()'") + } +} diff --git a/plugins/devkit/devkit-kotlin-fir-tests/testSrc/org/jetbrains/idea/devkit/k2/inspections/KtUseHtmlChunkToolTipInspectionTest.kt b/plugins/devkit/devkit-kotlin-fir-tests/testSrc/org/jetbrains/idea/devkit/k2/inspections/KtUseHtmlChunkToolTipInspectionTest.kt new file mode 100644 index 000000000000..5c481b42b3ed --- /dev/null +++ b/plugins/devkit/devkit-kotlin-fir-tests/testSrc/org/jetbrains/idea/devkit/k2/inspections/KtUseHtmlChunkToolTipInspectionTest.kt @@ -0,0 +1,52 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.k2.inspections + +import com.intellij.testFramework.TestDataPath +import org.jetbrains.idea.devkit.inspections.quickfix.UseHtmlChunkToolTipInspectionTestBase +import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil +import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode +import org.jetbrains.kotlin.idea.test.ExpectedPluginModeProvider +import org.jetbrains.kotlin.idea.test.setUpWithKotlinPlugin + +@TestDataPath("\$CONTENT_ROOT/testData/inspections/useHtmlChunkToolTip") +class KtUseHtmlChunkToolTipInspectionTest : UseHtmlChunkToolTipInspectionTestBase(), ExpectedPluginModeProvider { + override val pluginMode: KotlinPluginMode = KotlinPluginMode.K2 + + override fun setUp() { + setUpWithKotlinPlugin { super.setUp() } + } + + override fun getBasePath(): String { + return DevkitKtTestsUtil.TESTDATA_PATH + "inspections/useHtmlChunkToolTip" + } + + override fun getFileExtension(): String = "kt" + + override fun addSetToolTipTextExtensionStub() { + myFixture.addFileToProject( + "src/com/intellij/ide/HelpTooltip.kt", + """ + package com.intellij.ide + import javax.swing.JComponent + import com.intellij.openapi.util.text.HtmlChunk + fun JComponent.setToolTipText(html: HtmlChunk?) {} + """.trimIndent() + ) + } + + fun testSetToolTipTextWithString() { + doTest() + } + + fun testSetToolTipTextWithStringFix() { + doTest("Wrap with 'HtmlChunk.text()'") + } + + fun testSetToolTipTextWithStringFixRaw() { + doTest("Wrap with 'HtmlChunk.raw()'") + } + + fun testSetToolTipTextPropertyStyleFix() { + doTest("Wrap with 'HtmlChunk.text()'") + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix.kt new file mode 100644 index 000000000000..6288da182f37 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix.kt @@ -0,0 +1,9 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import javax.swing.JComponent + +class SetToolTipTextPropertyStyleFix { + fun test(comp: JComponent) { + comp.toolTipText = "hello" + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix_after.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix_after.kt new file mode 100644 index 000000000000..c4182bd48d23 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextPropertyStyleFix_after.kt @@ -0,0 +1,11 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import com.intellij.ide.setToolTipText +import com.intellij.openapi.util.text.HtmlChunk +import javax.swing.JComponent + +class SetToolTipTextPropertyStyleFix { + fun test(comp: JComponent) { + comp.setToolTipText(HtmlChunk.text("hello")) + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.kt new file mode 100644 index 000000000000..3ca3e310e965 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithString.kt @@ -0,0 +1,15 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import javax.swing.JComponent + +class SetToolTipTextWithString { + fun test(comp: JComponent) { + comp.setToolTipText("hello") + comp.setToolTipText(null) + comp.toolTipText = "hello" + comp.toolTipText = getString() + comp.toolTipText = null + } + + fun getString(): String = "" +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.kt new file mode 100644 index 000000000000..f382c6f1dff8 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix.kt @@ -0,0 +1,9 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import javax.swing.JComponent + +class SetToolTipTextWithStringFix { + fun test(comp: JComponent) { + comp.setToolTipText("hello") + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.kt new file mode 100644 index 000000000000..65460538e050 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw.kt @@ -0,0 +1,9 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import javax.swing.JComponent + +class SetToolTipTextWithStringFixRaw { + fun test(comp: JComponent) { + comp.setToolTipText("hello") + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.kt new file mode 100644 index 000000000000..f85cc3bac9a0 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFixRaw_after.kt @@ -0,0 +1,11 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import com.intellij.ide.setToolTipText +import com.intellij.openapi.util.text.HtmlChunk +import javax.swing.JComponent + +class SetToolTipTextWithStringFixRaw { + fun test(comp: JComponent) { + comp.setToolTipText(HtmlChunk.raw("hello")) + } +} diff --git a/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.kt b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.kt new file mode 100644 index 000000000000..a733c57eba02 --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testData/inspections/useHtmlChunkToolTip/SetToolTipTextWithStringFix_after.kt @@ -0,0 +1,11 @@ +@file:Suppress("MISSING_DEPENDENCY_SUPERCLASS") // mockSDK misses some superclasses + +import com.intellij.ide.setToolTipText +import com.intellij.openapi.util.text.HtmlChunk +import javax.swing.JComponent + +class SetToolTipTextWithStringFix { + fun test(comp: JComponent) { + comp.setToolTipText(HtmlChunk.text("hello")) + } +} diff --git a/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/UseHtmlChunkToolTipInspectionTestBase.kt b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/UseHtmlChunkToolTipInspectionTestBase.kt new file mode 100644 index 000000000000..8cdf98ea683c --- /dev/null +++ b/plugins/devkit/devkit-tests/testSrc/org/jetbrains/idea/devkit/inspections/quickfix/UseHtmlChunkToolTipInspectionTestBase.kt @@ -0,0 +1,37 @@ +// Copyright 2000-2026 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 org.jetbrains.idea.devkit.inspections.UseHtmlChunkToolTipInspection + +abstract class UseHtmlChunkToolTipInspectionTestBase : LightDevKitInspectionFixTestBase() { + @Throws(Exception::class) + override fun setUp() { + super.setUp() + myFixture.enableInspections(UseHtmlChunkToolTipInspection()) + myFixture.addClass( + """package javax.swing; + |public class JComponent { + | public String getToolTipText() { return ""; } + | public void setToolTipText(String text) {} + |}""".trimMargin() + ) + myFixture.addClass( + """package com.intellij.openapi.util.text; + |public class HtmlChunk { + | public static HtmlChunk text(String text) { return new HtmlChunk(); } + | public static HtmlChunk raw(String text) { return new HtmlChunk(); } + |}""".trimMargin() + ) + myFixture.addClass( + """package com.intellij.ide; + |import javax.swing.JComponent; + |import com.intellij.openapi.util.text.HtmlChunk; + |public class HelpTooltipKt { + | public static void setToolTipText(JComponent comp, HtmlChunk html) {} + |}""".trimMargin() + ) + addSetToolTipTextExtensionStub() + } + + protected open fun addSetToolTipTextExtensionStub() {} +} diff --git a/plugins/devkit/intellij.kotlin.devkit/resources/intellij.kotlin.devkit.xml b/plugins/devkit/intellij.kotlin.devkit/resources/intellij.kotlin.devkit.xml index d004fdebac81..cdb74e16b955 100644 --- a/plugins/devkit/intellij.kotlin.devkit/resources/intellij.kotlin.devkit.xml +++ b/plugins/devkit/intellij.kotlin.devkit/resources/intellij.kotlin.devkit.xml @@ -140,6 +140,9 @@ + diff --git a/plugins/devkit/intellij.kotlin.devkit/src/inspections/KtUseHtmlChunkToolTipFixProvider.kt b/plugins/devkit/intellij.kotlin.devkit/src/inspections/KtUseHtmlChunkToolTipFixProvider.kt new file mode 100644 index 000000000000..57afde6b7385 --- /dev/null +++ b/plugins/devkit/intellij.kotlin.devkit/src/inspections/KtUseHtmlChunkToolTipFixProvider.kt @@ -0,0 +1,80 @@ +// Copyright 2000-2026 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 + +import com.intellij.codeInsight.intention.PriorityAction +import com.intellij.codeInsight.intention.PriorityAction.Priority +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import org.jetbrains.idea.devkit.DevKitBundle +import org.jetbrains.idea.devkit.inspections.quickfix.UseHtmlChunkToolTipFixProvider +import org.jetbrains.kotlin.idea.base.codeInsight.ShortenReferencesFacility +import org.jetbrains.kotlin.idea.base.psi.imports.addImport +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPsiFactory + +internal class KtUseHtmlChunkToolTipFixProvider : UseHtmlChunkToolTipFixProvider { + override fun createFixes(element: PsiElement): Array { + return arrayOf( + KtWrapWithHtmlChunkFix("text", "inspections.use.html.chunk.tooltip.fix.wrap.text.family.name", Priority.TOP), + KtWrapWithHtmlChunkFix("raw", "inspections.use.html.chunk.tooltip.fix.wrap.raw.family.name", Priority.HIGH), + ) + } +} + +private const val HTML_CHUNK_FQN = "com.intellij.openapi.util.text.HtmlChunk" +private val SET_TOOL_TIP_TEXT_EXTENSION_FQN = FqName("com.intellij.ide.setToolTipText") + +private class KtWrapWithHtmlChunkFix( + private val wrapMethodName: String, + private val familyNameKey: String, + private val fixPriority: Priority, +) : LocalQuickFix, PriorityAction { + override fun getPriority(): Priority = fixPriority + + @IntentionFamilyName + override fun getFamilyName(): String = DevKitBundle.message(familyNameKey) + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement + val ktFile = element.containingFile as? KtFile ?: return + val psiFactory = KtPsiFactory(project) + + // Case 1: Method call — comp.setToolTipText(arg) + val dotCall = element as? KtDotQualifiedExpression + ?: (element.parent as? KtDotQualifiedExpression)?.takeIf { it.selectorExpression is KtCallExpression } + if (dotCall != null) { + val call = dotCall.selectorExpression as? KtCallExpression ?: return + val receiverText = dotCall.receiverExpression.text + val argText = call.valueArguments.firstOrNull()?.getArgumentExpression()?.text ?: return + replaceAndShortenReferences(ktFile, psiFactory, dotCall, receiverText, argText) + return + } + + // Case 2: Property assignment — comp.toolTipText = arg + val binaryExpr = element as? KtBinaryExpression ?: return + val left = binaryExpr.left as? KtDotQualifiedExpression ?: return + val receiverText = left.receiverExpression.text + val argText = binaryExpr.right?.text ?: return + replaceAndShortenReferences(ktFile, psiFactory, binaryExpr, receiverText, argText) + } + + private fun replaceAndShortenReferences( + ktFile: KtFile, + psiFactory: KtPsiFactory, + nodeToReplace: PsiElement, + receiverText: String, + argText: String, + ) { + val newExpr = psiFactory.createExpression("$receiverText.setToolTipText($HTML_CHUNK_FQN.$wrapMethodName($argText))") + val replaced = nodeToReplace.replace(newExpr) + ShortenReferencesFacility.getInstance().shorten(replaced as org.jetbrains.kotlin.psi.KtElement) + ktFile.addImport(SET_TOOL_TIP_TEXT_EXTENSION_FQN) + } +}