[kotlin] Add ObjectInheritsException inspection

IJ-CR-168258
#KTIJ-30892


(cherry picked from commit e7e379821883d5f5b582488a71a3d0575f2dee5a)

IJ-MR-169152

GitOrigin-RevId: c2b4c7d94df4fd42a190015275bdc1bf2091901a
This commit is contained in:
Vladimir Dolzhenko
2025-07-04 07:34:21 +02:00
committed by intellij-monorepo-bot
parent 7e92a25089
commit 5f6f79cb8f
11 changed files with 218 additions and 0 deletions

View File

@@ -2404,6 +2404,11 @@ inspection.deprecated.callable.add.replace.with.display.name=@Deprecated annotat
inspection.replace.collection.count.with.size.display.name=Collection count can be converted to size
inspection.simplify.assert.not.null.display.name='assert' call can be replaced with '!!' or '?:'
inspection.object.literal.to.lambda.display.name=Object literal can be converted to lambda
inspection.object.exception.to.class.display.name=Exception should not be an object
inspection.object.exception.to.class.warning=Exception should not be an object
inspection.object.exception.to.class.quick.fix.name=Change exception object to class
remove.redundant.elvis.return.null.text=Remove redundant '?: return null'
inspection.redundant.elvis.return.null.descriptor=Redundant '?: return null'
inspection.redundant.elvis.return.null.display.name=Redundant '?: return null'

View File

@@ -0,0 +1,13 @@
<html>
<body>
Reports object inherited from <code>Exception</code> because exceptions are inherently stateful, containing information like the stacktrace.
<p><b>Example:</b></p>
<pre><code>
object MyEx: Exception()
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
class MyEx: Exception()
</code></pre>
</body>
</html>

View File

@@ -173,6 +173,16 @@
key="inspection.object.literal.to.lambda.display.name"
bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.probable.bugs"
enabledByDefault="true"
level="WARNING"
language="kotlin"
cleanupTool="true"
key="inspection.object.exception.to.class.display.name"
bundle="messages.KotlinBundle"/>
<localInspection
implementationClass="org.jetbrains.kotlin.idea.codeInsight.inspections.shared.JavaIoSerializableObjectMustHaveReadResolveInspection"
groupPath="Kotlin"

View File

@@ -0,0 +1,103 @@
// 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.kotlin.idea.codeInsight.inspections.shared
import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils
import com.intellij.codeInspection.CleanupLocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.util.IntentionFamilyName
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.PsiUpdateModCommandQuickFix
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KaNamedClassSymbol
import org.jetbrains.kotlin.builtins.StandardNames.BUILT_INS_PACKAGE_FQ_NAME
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinReferencesSearchParameters
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtUserType
import org.jetbrains.kotlin.psi.KtVisitorVoid
internal class ObjectInheritsExceptionInspection : AbstractKotlinInspection(), CleanupLocalInspectionTool {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor =
object : KtVisitorVoid() {
override fun visitObjectDeclaration(declaration: KtObjectDeclaration) {
val isException = analyze(declaration) {
val symbol = declaration.symbol as? KaNamedClassSymbol ?: return
symbol.superTypes.any {
it.isClassType(exceptionClassId) ||
it.isSubtypeOf(exceptionClassId)
}
}
if (!isException) return
holder.registerProblem(
declaration.getObjectKeyword() ?: return,
KotlinBundle.message("inspection.object.exception.to.class.warning"),
ChangeObjectToClassQuickFix()
)
}
}
private class ChangeObjectToClassQuickFix : PsiUpdateModCommandQuickFix() {
override fun getFamilyName(): @IntentionFamilyName String = KotlinBundle.message("inspection.object.exception.to.class.quick.fix.name")
override fun applyFix(project: Project, element: PsiElement, updater: ModPsiUpdater) {
val objectDeclaration =
(element as? LeafPsiElement)?.let { it.parent as? KtObjectDeclaration } ?: return
val psiFactory = KtPsiFactory(project)
val searchParameters = KotlinReferencesSearchParameters(
objectDeclaration, objectDeclaration.useScope, ignoreAccessScope = false
)
val query = ReferencesSearch.search(searchParameters)
val references = if (IntentionPreviewUtils.isIntentionPreviewActive()) {
listOfNotNull(query.findFirst())
} else {
query.findAll()
}
references
.mapNotNull {
if (it !is KtSimpleNameReference) return@mapNotNull null
val expression = it.element
if (expression.parent is KtUserType ||
PsiTreeUtil.getParentOfType(
/* element = */ expression,
/* aClass = */ KtImportDirective::class.java,
/* strict = */ false,
/* ...stopAt = */ KtBlockExpression::class.java
)
!= null
) {
return@mapNotNull null
}
updater.getWritable(expression)
}.forEach {
val referencedName = it.getReferencedName()
it.replace(psiFactory.createExpression("$referencedName()"))
}
objectDeclaration.getObjectKeyword()?.replace(psiFactory.createClassKeyword())
}
}
}
private val exceptionClassId = ClassId(BUILT_INS_PACKAGE_FQ_NAME , Name.identifier("Exception"))

View File

@@ -20,6 +20,30 @@ import org.junit.runner.RunWith;
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix")
public abstract class SharedK1QuickFixTestGenerated extends AbstractSharedK1QuickFixTest {
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix/objectInheritsException")
public static class ObjectInheritsException extends AbstractSharedK1QuickFixTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K1;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
runTest("../testData/quickfix/objectInheritsException/simple.kt");
}
@TestMetadata("withCallSites.kt")
public void testWithCallSites() throws Exception {
runTest("../testData/quickfix/objectInheritsException/withCallSites.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix/redundantSemicolon")
public static class RedundantSemicolon extends AbstractSharedK1QuickFixTest {

View File

@@ -22,6 +22,30 @@ public abstract class K2SharedQuickFixTestGenerated extends AbstractK2SharedQuic
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix")
public abstract static class Quickfix extends AbstractK2SharedQuickFixTest {
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix/objectInheritsException")
public static class ObjectInheritsException extends AbstractK2SharedQuickFixTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K2;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
runTest("../testData/quickfix/objectInheritsException/simple.kt");
}
@TestMetadata("withCallSites.kt")
public void testWithCallSites() throws Exception {
runTest("../testData/quickfix/objectInheritsException/withCallSites.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/quickfix/redundantSemicolon")
public static class RedundantSemicolon extends AbstractK2SharedQuickFixTest {

View File

@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection

View File

@@ -0,0 +1,5 @@
// "Change exception object to class" "true"
// WITH_STDLIB
<caret>object MyException : Exception()
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix
// FUS_K2_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix

View File

@@ -0,0 +1,5 @@
// "Change exception object to class" "true"
// WITH_STDLIB
<caret>class MyException : Exception()
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix
// FUS_K2_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix

View File

@@ -0,0 +1,14 @@
// "Change exception object to class" "true"
// WITH_STDLIB
package some
import some.MyException
<caret>object MyException : Exception()
fun foo() {
throw MyException
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix
// FUS_K2_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix

View File

@@ -0,0 +1,14 @@
// "Change exception object to class" "true"
// WITH_STDLIB
package some
import some.MyException
<caret>class MyException : Exception()
fun foo() {
throw MyException()
}
// FUS_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix
// FUS_K2_QUICKFIX_NAME: org.jetbrains.kotlin.idea.codeInsight.inspections.shared.ObjectInheritsExceptionInspection$ChangeObjectToClassQuickFix