[devkit] IDEA-323204 implement new inspection

GitOrigin-RevId: 379d6e0776d132ee356ba62db23660ca388ea137
This commit is contained in:
Elena Lyulina
2023-08-24 14:37:41 +02:00
committed by intellij-monorepo-bot
parent 4e97148a7f
commit 4a112d1970
20 changed files with 235 additions and 3 deletions

View File

@@ -0,0 +1,12 @@
<html>
<body>
Reports listener implementations that implement <code>com.intellij.openapi.Disposable</code>.
<p>
Listener implementations must be stateless and must not implement life-cycle, including <code>com.intellij.openapi.Disposable</code>.
</p>
<p>
See <a href=https://plugins.jetbrains.com/docs/intellij/plugin-listeners.html>documentation</a> for more information.
</p>
<p><small>New in 2023.3</small>
</body>
</html>

View File

@@ -404,6 +404,12 @@
enabledByDefault="false" level="WARNING"
implementationClass="org.jetbrains.idea.devkit.inspections.StaticInitializationInExtensionsInspection"/>
<localInspection language="UAST" groupPathKey="inspections.group.path"
projectType="INTELLIJ_PLUGIN"
key="inspections.listener.implementation.must.not.be.disposable.name" groupKey="inspections.group.code"
enabledByDefault="false" level="ERROR"
implementationClass="org.jetbrains.idea.devkit.inspections.ListenerImplementationMustNotBeDisposableInspection"/>
<moduleConfigurationEditorProvider implementation="org.jetbrains.idea.devkit.module.PluginModuleEditorsProvider"/>
<implicitUsageProvider implementation="org.jetbrains.idea.devkit.inspections.DevKitImplicitUsageProvider"/>

View File

@@ -692,4 +692,7 @@ update.ide.from.sources=Update &IDE from sources
update.ide.from.sources.option=from sources
inspections.static.initialization.in.extensions.name=Static initialization in IDE extensions
inspections.static.initialization.in.extensions.message=IDE extensions must not use static initialization
inspections.static.initialization.in.extensions.message=IDE extensions must not use static initialization
inspections.listener.implementation.must.not.be.disposable.name=Listener implementation implements 'Disposable'
inspections.listener.implementation.must.not.implement.disposable=Listener implementation must not implement 'Disposable'

View File

@@ -0,0 +1,53 @@
// Copyright 2000-2023 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.ProblemsHolder
import com.intellij.codeInspection.registerUProblem
import com.intellij.lang.jvm.JvmClassKind
import com.intellij.openapi.Disposable
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.uast.UastHintedVisitorAdapter
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.idea.devkit.dom.index.IdeaPluginRegistrationIndex
import org.jetbrains.idea.devkit.util.PluginRelatedLocatorsUtils
import org.jetbrains.uast.UClass
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
internal class ListenerImplementationMustNotBeDisposableInspection : DevKitUastInspectionBase() {
override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return UastHintedVisitorAdapter.create(
holder.file.language,
object : AbstractUastNonRecursiveVisitor() {
override fun visitClass(node: UClass): Boolean {
val psiClass = node.javaPsi
if (psiClass.classKind != JvmClassKind.CLASS ||
PsiUtil.isAbstractClass(psiClass) ||
PsiUtil.isLocalOrAnonymousClass(psiClass) ||
PsiUtil.isInnerClass(psiClass)) return true
if (!InheritanceUtil.isInheritor(psiClass, Disposable::class.java.canonicalName)) return true
val isRegisteredListener = !IdeaPluginRegistrationIndex.processListener(
holder.project,
psiClass,
PluginRelatedLocatorsUtils.getCandidatesScope(holder.project),
) { false }
if (!isRegisteredListener) return true
holder.registerUProblem(
node,
DevKitBundle.message("inspections.listener.implementation.must.not.implement.disposable")
)
return true
}
},
arrayOf(UClass::class.java)
)
}
}

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable;
class <error descr="Listener implementation must not implement 'Disposable'">MyApplicationListener</error> implements BaseListenerInterface, Disposable { }

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable;
class <error descr="Listener implementation must not implement 'Disposable'">MyProjectListener</error> implements BaseListenerInterface, Disposable { }

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable;
class MyClass implements BaseListenerInterface, Disposable { }

View File

@@ -0,0 +1 @@
class MyApplicationListener implements BaseListenerInterface { }

View File

@@ -0,0 +1 @@
class MyProjectListener implements BaseListenerInterface { }

View File

@@ -0,0 +1,15 @@
<idea-plugin>
<id>com.intellij</id>
<applicationListeners>
<listener
class="MyApplicationListener"
topic="BaseListenerInterface"/>
</applicationListeners>
<projectListeners>
<listener
class="MyProjectListener"
topic="BaseListenerInterface"/>
</projectListeners>
</idea-plugin>

View File

@@ -27,7 +27,8 @@ public class DevkitInspectionsRegistrationCheckTest extends BasePlatformTestCase
"TokenSetInParserDefinition",
"CallingMethodShouldBeRequiresBlockingContext",
"IncorrectProcessCanceledExceptionHandling",
"StaticInitializationInExtensions");
"StaticInitializationInExtensions",
"ListenerImplementationMustNotBeDisposable");
/**
* Validates all DevKit inspections that are disabled by default match the expected known set.
@@ -36,7 +37,7 @@ public class DevkitInspectionsRegistrationCheckTest extends BasePlatformTestCase
List<LocalInspectionEP> devkitInspections = ContainerUtil.filter(LocalInspectionEP.LOCAL_INSPECTION.getExtensionList(), ep -> {
return "DevKit".equals(ep.getPluginDescriptor().getPluginId().getIdString());
});
assertEquals("Mismatch in total inspections, check classpath in test run configuration (intellij.devkit.plugin)", 64,
assertEquals("Mismatch in total inspections, check classpath in test run configuration (intellij.devkit.plugin)", 65,
devkitInspections.size());
List<LocalInspectionEP> disabledInspections = ContainerUtil.filter(devkitInspections, ep -> !ep.enabledByDefault);

View File

@@ -0,0 +1,34 @@
// Copyright 2000-2023 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
@TestDataPath("/inspections/listenerImplementationMustNotBeDisposable")
class ListenerImplementationMustNotBeDisposableInspectionTest : ListenerImplementationMustNotBeDisposableInspectionTestBase() {
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/listenerImplementationMustNotBeDisposable"
override fun getFileExtension(): String = "java"
fun testDisposableApplicationListener() {
doTest()
}
fun testDisposableProjectListener() {
doTest()
}
fun testDisposableUnregisteredListener() {
doTest()
}
fun testNonDisposableApplicationListener() {
doTest()
}
fun testNonDisposableProjectListener() {
doTest()
}
}

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable
class <error descr="Listener implementation must not implement 'Disposable'">MyApplicationListener</error> : BaseListenerInterface, Disposable

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable
class <error descr="Listener implementation must not implement 'Disposable'">MyProjectListener</error> : BaseListenerInterface, Disposable

View File

@@ -0,0 +1,3 @@
import com.intellij.openapi.Disposable
class MyClass : BaseListenerInterface, Disposable

View File

@@ -0,0 +1 @@
class MyApplicationListener : BaseListenerInterface

View File

@@ -0,0 +1 @@
class MyProjectListener : BaseListenerInterface

View File

@@ -0,0 +1,15 @@
<idea-plugin>
<id>com.intellij</id>
<applicationListeners>
<listener
class="MyApplicationListener"
topic="BaseListenerInterface"/>
</applicationListeners>
<projectListeners>
<listener
class="MyProjectListener"
topic="BaseListenerInterface"/>
</projectListeners>
</idea-plugin>

View File

@@ -0,0 +1,35 @@
// Copyright 2000-2023 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.testFramework.TestDataPath
import org.jetbrains.idea.devkit.inspections.ListenerImplementationMustNotBeDisposableInspectionTestBase
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil
@TestDataPath("/inspections/listenerImplementationMustNotBeDisposable")
class KtListenerImplementationMustNotBeDisposableInspectionTest : ListenerImplementationMustNotBeDisposableInspectionTestBase() {
override fun getBasePath() = DevkitKtTestsUtil.TESTDATA_PATH + "inspections/listenerImplementationMustNotBeDisposable"
override fun getFileExtension(): String = "kt"
fun testDisposableApplicationListener() {
doTest()
}
fun testDisposableProjectListener() {
doTest()
}
fun testDisposableUnregisteredListener() {
doTest()
}
fun testNonDisposableApplicationListener() {
doTest()
}
fun testNonDisposableProjectListener() {
doTest()
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2000-2023 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
abstract class ListenerImplementationMustNotBeDisposableInspectionTestBase : PluginModuleTestCase() {
protected abstract fun getFileExtension(): String
override fun setUp() {
super.setUp()
addPlatformClasses()
setPluginXml("plugin.xml")
myFixture.enableInspections(ListenerImplementationMustNotBeDisposableInspection())
}
private fun addPlatformClasses() {
myFixture.addClass(
"""
package com.intellij.openapi;
public interface Disposable { }
""".trimIndent()
)
myFixture.addClass(
"""
public interface BaseListenerInterface { }
""".trimIndent()
)
}
protected open fun doTest() {
myFixture.testHighlighting(getTestName(false) + '.' + getFileExtension())
}
}