[devkit] Split the "Incorrect or simplifiable retrieving service" inspection into two

IDEA-327590

GitOrigin-RevId: 721b34c773e16b93cc593080ba2dbc130606ed40
This commit is contained in:
Andrey Cherkasov
2023-10-03 04:01:17 +00:00
committed by intellij-monorepo-bot
parent 9feb83f731
commit 073114cdb2
33 changed files with 458 additions and 277 deletions
@@ -0,0 +1,28 @@
<html>
<body>
Reports the following problems when retrieving services:
<ul>
<li>Attempts to retrieve an unregistered service.</li>
<li>Mismatch when retrieving a service: attempting to get a project-level service as an application-level service, or vice versa.</li>
</ul>
<p>Example (Kotlin):</p>
<pre><code lang="kotlin">
@Service
class MyAppService
@Service(Service.Level.PROJECT)
class MyProjectService(private val project: Project)
</code></pre>
<pre><code lang="kotlin">
// Bad:
val projectService = service&lt;MyProjectService&gt;() // The project-level service is retrieved as an application-level service
val applicationService = project.service&lt;MyAppService&gt;() // The application-level service is retrieved as a project-level service
</code></pre>
<pre><code lang="kotlin">
// Good:
val projectService = project.service&lt;MyProjectService&gt;()
val applicationService = service&lt;MyAppService&gt;();
</code></pre>
<p><small>New in 2023.2</small>
</body>
</html>
@@ -1,13 +0,0 @@
<html>
<body>
Reports the following problems when retrieving services:
<ul>
<li>Attempts to retrieve an unregistered service</li>
<li>Mismatch when retrieving a service: attempting to get a project-level service as an application-level service, or vice versa.</li>
<li>Getting service call can be replaced with a call to an existing static <code>getInstance()</code> or <code>getInstance(Project)</code>
method.
</li>
</ul>
<p><small>New in 2023.2</small>
</body>
</html>
@@ -0,0 +1,33 @@
<html>
<body>
Reports service getting calls that can be replaced with a calls to an existing static <code>getInstance()</code>
or <code>getInstance(Project)</code> methods.
<p>Example (Java):</p>
<pre><code lang="java">
@Service
public class MyAppService {
public static MyAppService getInstance() {
return ApplicationManager.getApplication().getService(MyAppService.class);
}
}
@Service(Service.Level.PROJECT)
public class MyProjectService {
public static MyProjectService getInstance(Project project) {
return project.getService(MyProjectService.class);
}
}
</code></pre>
<pre><code lang="java">
// Bad:
MyAppService applicationService = ApplicationManager.getApplication().getService(MyAppService.class);
MyProjectService projectService = project.getService(MyProjectService.class);
</code></pre>
<pre><code lang="java">
// Good:
MyAppService applicationService = MyAppService.getInstance();
MyProjectService projectService = MyProjectService.getInstance(project);
</code></pre>
<p><small>New in 2023.2</small>
</body>
</html>
@@ -285,12 +285,18 @@
enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.devkit.inspections.MismatchedLightServiceLevelAndCtorInspection"
key="inspection.mismatched.light.service.level.and.ctor.display.name"/>
<localInspection language="UAST" shortName="RetrievingService"
<localInspection language="UAST"
projectType="INTELLIJ_PLUGIN"
groupPathKey="inspections.group.path" groupKey="inspections.group.code"
enabledByDefault="true" level="ERROR"
implementationClass="org.jetbrains.idea.devkit.inspections.RetrievingServiceInspection"
key="inspection.retrieving.service.display.name"/>
implementationClass="org.jetbrains.idea.devkit.inspections.IncorrectServiceRetrievingInspection"
key="inspection.incorrect.service.retrieving.display.name"/>
<localInspection language="UAST"
projectType="INTELLIJ_PLUGIN"
groupPathKey="inspections.group.path" groupKey="inspections.group.code"
enabledByDefault="true" level="WEAK WARNING"
implementationClass="org.jetbrains.idea.devkit.inspections.SimplifiableServiceRetrievingInspection"
key="inspection.simplifiable.service.retrieving.display.name"/>
<localInspection language="JVM" shortName="ExtensionClassShouldBeFinalAndNonPublic"
projectType="INTELLIJ_PLUGIN"
groupPathKey="inspections.group.path" groupKey="inspections.group.code"
@@ -629,12 +629,14 @@ inspection.mismatched.light.service.level.and.ctor.project.level.required=Light
inspection.mismatched.light.service.level.and.ctor.app.level.ctor.required=Application-level service requires a no-arg or single parameter constructor with 'kotlinx.coroutines.CoroutineScope' type
inspection.mismatched.light.service.level.and.ctor.specify.project.level.fix=Specify 'Service.Level.PROJECT' parameter in '@Service' annotation
inspection.retrieving.service.display.name=Incorrect or simplifiable retrieving service
inspection.retrieving.service.not.registered=The ''{0}'' class is not registered as a service
inspection.retrieving.service.mismatch.for.project.level=The project-level service is retrieved as an application-level service
inspection.retrieving.service.mismatch.for.app.level=The application-level service is retrieved as a project-level service
inspection.retrieving.service.can.be.replaced.with=Can be replaced with ''{0}.{1}()'' call
inspection.retrieving.service.replace.with=Replace with ''{0}.{1}()'' call
inspection.incorrect.service.retrieving.display.name=Incorrect service retrieving
inspection.incorrect.service.retrieving.not.registered=The ''{0}'' class is not registered as a service
inspection.incorrect.service.retrieving.mismatch.for.project.level=The project-level service is retrieved as an application-level service
inspection.incorrect.service.retrieving.mismatch.for.app.level=The application-level service is retrieved as a project-level service
inspection.simplifiable.service.retrieving.display.name=Simplifiable service retrieving
inspection.simplifiable.service.retrieving.can.be.replaced.with=Can be replaced with ''{0}.{1}()'' call
inspection.simplifiable.service.retrieving.replace.with=Replace with ''{0}.{1}()'' call
inspection.extension.class.should.be.final.and.non.public.display.name=Extension class should be final and non-public
inspection.extension.class.should.be.final.text=Extension class should be final
@@ -0,0 +1,47 @@
// 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.openapi.components.Service.Level
import com.intellij.psi.PsiElementVisitor
import com.intellij.uast.UastHintedVisitorAdapter
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
internal class IncorrectServiceRetrievingInspection : DevKitUastInspectionBase() {
override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return UastHintedVisitorAdapter.create(holder.file.language, object : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
val (howServiceRetrieved, serviceClass) = getServiceRetrievingInfo(node) ?: return true
val serviceLevel = getLevelType(holder.project, serviceClass)
if (serviceLevel == LevelType.MODULE) return true
if (serviceLevel == LevelType.NOT_REGISTERED) {
serviceClass.qualifiedName?.let { className ->
val message = DevKitBundle.message("inspection.incorrect.service.retrieving.not.registered", className)
holder.registerUProblem(node, message)
}
}
else if (!isServiceRetrievedCorrectly(serviceLevel, howServiceRetrieved)) {
val message = when (howServiceRetrieved) {
Level.APP -> DevKitBundle.message("inspection.incorrect.service.retrieving.mismatch.for.project.level")
Level.PROJECT -> DevKitBundle.message("inspection.incorrect.service.retrieving.mismatch.for.app.level")
}
holder.registerUProblem(node, message)
}
return true
}
}, arrayOf(UCallExpression::class.java))
}
private fun isServiceRetrievedCorrectly(serviceLevel: LevelType, howServiceRetrieved: Level): Boolean {
return serviceLevel == LevelType.NOT_SPECIFIED ||
when (howServiceRetrieved) {
Level.APP -> serviceLevel.isApp()
Level.PROJECT -> serviceLevel.isProject()
}
}
}
@@ -1,191 +0,0 @@
// 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.*
import com.intellij.openapi.application.Application
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.components.Service.Level
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.CommonClassNames
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.InheritanceUtil
import com.intellij.uast.UastHintedVisitorAdapter
import com.siyeh.ig.callMatcher.CallMatcher
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.uast.*
import org.jetbrains.uast.generate.UastCodeGenerationPlugin
import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
private val SERVICE_KT_METHODS =
CallMatcher.staticCall("com.intellij.openapi.components.ServiceKt", "service", "serviceOrNull", "serviceIfCreated")
.parameterCount(0)
private val SERVICES_KT_METHODS =
CallMatcher.staticCall("com.intellij.openapi.components.ServicesKt", "service", "serviceOrNull", "serviceIfCreated")
.parameterTypes(ComponentManager::class.java.canonicalName)
private val COMPONENT_MANAGER_GET_SERVICE = CallMatcher.anyOf(
CallMatcher.instanceCall(ComponentManager::class.java.canonicalName, "getService").parameterTypes(CommonClassNames.JAVA_LANG_CLASS),
CallMatcher.instanceCall(ComponentManager::class.java.canonicalName, "getService").parameterTypes(CommonClassNames.JAVA_LANG_CLASS,
"boolean"),
SERVICE_KT_METHODS,
SERVICES_KT_METHODS,
)
internal class RetrievingServiceInspection : DevKitUastInspectionBase() {
override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return UastHintedVisitorAdapter.create(holder.file.language, object : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
if (!COMPONENT_MANAGER_GET_SERVICE.uCallMatches(node)) return true
val howServiceRetrieved = howServiceRetrieved(node) ?: return true
val serviceType = node.returnType as? PsiClassType ?: return true
val serviceClass = serviceType.resolve()?.toUElement(UClass::class.java) ?: return true
val serviceLevel = getLevelType(holder.project, serviceClass)
if (serviceLevel == LevelType.MODULE) return true
if (serviceLevel == LevelType.NOT_REGISTERED) {
val className = serviceClass.qualifiedName
if (className != null) {
val message = DevKitBundle.message("inspection.retrieving.service.not.registered", className)
holder.registerUProblem(node, message)
}
}
else if (!isServiceRetrievedCorrectly(serviceLevel, howServiceRetrieved)) {
registerProblemMismatchedRetrieving(howServiceRetrieved, holder, node)
}
else {
val retrievingExpression = node.uastParent as? UQualifiedReferenceExpression ?: return true
val getInstanceMethod = findGetInstanceMethod(retrievingExpression, howServiceRetrieved, serviceClass)
if (getInstanceMethod != null) {
registerProblem(getInstanceMethod, howServiceRetrieved, holder, retrievingExpression)
}
}
return true
}
}, arrayOf(UCallExpression::class.java))
}
private fun registerProblemMismatchedRetrieving(howServiceRetrieved: Level,
holder: ProblemsHolder,
node: UCallExpression) {
val message = when (howServiceRetrieved) {
Level.APP -> DevKitBundle.message("inspection.retrieving.service.mismatch.for.project.level")
Level.PROJECT -> DevKitBundle.message("inspection.retrieving.service.mismatch.for.app.level")
}
holder.registerUProblem(node, message)
}
private fun isServiceRetrievedCorrectly(serviceLevel: LevelType, howServiceRetrieved: Level): Boolean {
return serviceLevel == LevelType.NOT_SPECIFIED ||
when (howServiceRetrieved) {
Level.APP -> serviceLevel.isApp()
Level.PROJECT -> serviceLevel.isProject()
}
}
private fun howServiceRetrieved(getServiceCandidate: UCallExpression): Level? {
if (SERVICE_KT_METHODS.uCallMatches(getServiceCandidate)) return Level.APP
val receiverType = getServiceCandidate.receiver?.getExpressionType() ?: return null
val aClass = (receiverType as? PsiClassType)?.resolve() ?: return null
return when {
InheritanceUtil.isInheritor(aClass, Application::class.java.canonicalName) -> Level.APP
InheritanceUtil.isInheritor(aClass, Project::class.java.canonicalName) -> Level.PROJECT
else -> null
}
}
private fun findGetInstanceMethod(retrievingExpression: UQualifiedReferenceExpression,
howServiceRetrieved: Level,
serviceClass: UClass): UMethod? {
val returnExpr = retrievingExpression.uastParent as? UReturnExpression
if (returnExpr != null) {
val containingMethod = returnExpr.jumpTarget as? UMethod
if (containingMethod != null) {
if (howServiceRetrieved == Level.APP && isGetInstanceApplicationLevel(containingMethod)) return null
if (howServiceRetrieved == Level.PROJECT && isGetInstanceProjectLevel(containingMethod)) return null
}
}
return when (howServiceRetrieved) {
Level.APP -> findGetInstanceApplicationLevel(serviceClass)
Level.PROJECT -> findGetInstanceProjectLevel(serviceClass)
}
}
private fun registerProblem(replacementMethod: UMethod,
howServiceRetrieved: Level,
holder: ProblemsHolder,
retrievingExpression: UQualifiedReferenceExpression) {
val qualifiedName = replacementMethod.getContainingUClass()?.qualifiedName ?: return
val serviceName = StringUtil.getShortName(qualifiedName)
val message = DevKitBundle.message("inspection.retrieving.service.can.be.replaced.with", serviceName, replacementMethod.name)
val fix = ReplaceWithGetInstanceCallFix(serviceName, replacementMethod.name, howServiceRetrieved)
holder.registerUProblem(retrievingExpression, message, fixes = arrayOf(fix), ProblemHighlightType.WEAK_WARNING)
}
private fun findGetInstanceProjectLevel(uClass: UClass): UMethod? {
return uClass.methods.find { isGetInstanceProjectLevel(it) }
}
private fun isGetInstanceProjectLevel(method: UMethod): Boolean {
if (!(method.isStaticOrJvmStatic &&
method.visibility == UastVisibility.PUBLIC &&
method.uastParameters.size == 1)) {
return false
}
val param = method.uastParameters[0]
if (param.type.canonicalText != Project::class.java.canonicalName) return false
val qualifiedRef = getReturnExpression(method)?.returnExpression as? UQualifiedReferenceExpression ?: return false
return COMPONENT_MANAGER_GET_SERVICE.uCallMatches(qualifiedRef.selector as? UCallExpression) &&
(qualifiedRef.receiver as? USimpleNameReferenceExpression)?.resolveToUElement() == param
}
private fun findGetInstanceApplicationLevel(uClass: UClass): UMethod? {
return uClass.methods.find { isGetInstanceApplicationLevel(it) }
}
private fun isGetInstanceApplicationLevel(method: UMethod): Boolean {
if (!(method.isStaticOrJvmStatic &&
method.visibility == UastVisibility.PUBLIC &&
method.uastParameters.isEmpty())) {
return false
}
val qualifiedRef = getReturnExpression(method)?.returnExpression as? UQualifiedReferenceExpression ?: return false
return COMPONENT_MANAGER_GET_SERVICE.uCallMatches(qualifiedRef.selector as? UCallExpression) &&
qualifiedRef.receiver.getExpressionType()?.isInheritorOf(Application::class.java.canonicalName) == true
}
private val UMethod.isStaticOrJvmStatic: Boolean
get() = this.isStatic || this.findAnnotation(JvmStatic::class.java.canonicalName) != null
private fun getReturnExpression(method: UMethod): UReturnExpression? {
return (method.uastBody as? UBlockExpression)?.expressions?.singleOrNull() as? UReturnExpression
}
private class ReplaceWithGetInstanceCallFix(private val serviceName: String,
private val methodName: String,
private val howServiceRetrieved: Level) : LocalQuickFix {
override fun getFamilyName(): String = DevKitBundle.message("inspection.retrieving.service.replace.with", serviceName, methodName)
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val oldCall = descriptor.psiElement.toUElement()?.getParentOfType<UQualifiedReferenceExpression>() ?: return
val generationPlugin = UastCodeGenerationPlugin.byLanguage(descriptor.psiElement.language) ?: return
val factory = generationPlugin.getElementFactory(project)
val serviceName = oldCall.getExpressionType()?.canonicalText ?: return
val parameters = when (howServiceRetrieved) {
Level.APP -> emptyList()
Level.PROJECT -> listOf(oldCall.receiver)
}
val context = oldCall.sourcePsi
val receiver = factory.createQualifiedReference(serviceName, context) ?: factory.createSimpleReference(serviceName, context)
val newCall = factory.createCallExpression(receiver = receiver,
methodName = methodName, parameters = parameters,
expectedReturnType = oldCall.getExpressionType(), kind = UastCallKind.METHOD_CALL,
context = null) ?: return
oldCall.replace(newCall)
}
}
}
@@ -0,0 +1,54 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("ServiceRetrievingUtil")
package org.jetbrains.idea.devkit.inspections
import com.intellij.openapi.application.Application
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.psi.CommonClassNames
import com.intellij.psi.PsiClassType
import com.intellij.psi.util.InheritanceUtil
import com.siyeh.ig.callMatcher.CallMatcher
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.toUElement
private val SERVICE_KT_METHODS =
CallMatcher.staticCall("com.intellij.openapi.components.ServiceKt", "service", "serviceOrNull", "serviceIfCreated")
.parameterCount(0)
private val SERVICES_KT_METHODS =
CallMatcher.staticCall("com.intellij.openapi.components.ServicesKt", "service", "serviceOrNull", "serviceIfCreated")
.parameterTypes(ComponentManager::class.java.canonicalName)
internal val COMPONENT_MANAGER_GET_SERVICE = CallMatcher.anyOf(
CallMatcher.instanceCall(ComponentManager::class.java.canonicalName, "getService").parameterTypes(CommonClassNames.JAVA_LANG_CLASS),
CallMatcher.instanceCall(ComponentManager::class.java.canonicalName, "getService").parameterTypes(CommonClassNames.JAVA_LANG_CLASS,
"boolean"),
SERVICE_KT_METHODS,
SERVICES_KT_METHODS,
)
internal data class ServiceRetrievingInfo(val howServiceRetrieved: Service.Level,
val serviceClass: UClass)
internal fun getServiceRetrievingInfo(node: UCallExpression): ServiceRetrievingInfo? {
if (!COMPONENT_MANAGER_GET_SERVICE.uCallMatches(node)) return null
val howServiceRetrieved = howServiceRetrieved(node) ?: return null
val serviceType = node.returnType as? PsiClassType ?: return null
val serviceClass = serviceType.resolve()?.toUElement(UClass::class.java) ?: return null
return ServiceRetrievingInfo(howServiceRetrieved, serviceClass)
}
internal fun howServiceRetrieved(getServiceCandidate: UCallExpression): Service.Level? {
if (SERVICE_KT_METHODS.uCallMatches(getServiceCandidate)) return Service.Level.APP
val receiverType = getServiceCandidate.receiver?.getExpressionType() ?: return null
val aClass = (receiverType as? PsiClassType)?.resolve() ?: return null
return when {
InheritanceUtil.isInheritor(aClass, Application::class.java.canonicalName) -> Service.Level.APP
InheritanceUtil.isInheritor(aClass, Project::class.java.canonicalName) -> Service.Level.PROJECT
else -> null
}
}
@@ -0,0 +1,122 @@
// 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.*
import com.intellij.openapi.application.Application
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
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.UastCodeGenerationPlugin
import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
internal class SimplifiableServiceRetrievingInspection : DevKitUastInspectionBase() {
override fun buildInternalVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return UastHintedVisitorAdapter.create(holder.file.language, object : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
val (howServiceRetrieved, serviceClass) = getServiceRetrievingInfo(node) ?: return true
val retrievingExpression = node.uastParent as? UQualifiedReferenceExpression ?: return true
val getInstanceMethod = findGetInstanceMethod(retrievingExpression, howServiceRetrieved, serviceClass)
if (getInstanceMethod != null) {
registerProblem(getInstanceMethod, howServiceRetrieved, holder, retrievingExpression)
}
return true
}
}, arrayOf(UCallExpression::class.java))
}
private fun findGetInstanceMethod(retrievingExpression: UQualifiedReferenceExpression,
howServiceRetrieved: Service.Level,
serviceClass: UClass): UMethod? {
val returnExpr = retrievingExpression.uastParent as? UReturnExpression
if (returnExpr != null) {
val containingMethod = returnExpr.jumpTarget as? UMethod
if (containingMethod != null) {
if (howServiceRetrieved == Service.Level.APP && isGetInstanceApplicationLevel(containingMethod)) return null
if (howServiceRetrieved == Service.Level.PROJECT && isGetInstanceProjectLevel(containingMethod)) return null
}
}
return when (howServiceRetrieved) {
Service.Level.APP -> findGetInstanceApplicationLevel(serviceClass)
Service.Level.PROJECT -> findGetInstanceProjectLevel(serviceClass)
}
}
private fun registerProblem(replacementMethod: UMethod,
howServiceRetrieved: Service.Level,
holder: ProblemsHolder,
retrievingExpression: UQualifiedReferenceExpression) {
val qualifiedName = replacementMethod.getContainingUClass()?.qualifiedName ?: return
val serviceName = StringUtil.getShortName(qualifiedName)
val message = DevKitBundle.message("inspection.simplifiable.service.retrieving.can.be.replaced.with", serviceName,
replacementMethod.name)
val fix = ReplaceWithGetInstanceCallFix(serviceName, replacementMethod.name, howServiceRetrieved)
holder.registerUProblem(retrievingExpression, message, fixes = arrayOf(fix))
}
private fun findGetInstanceProjectLevel(uClass: UClass): UMethod? {
return uClass.methods.find { isGetInstanceProjectLevel(it) }
}
private fun isGetInstanceProjectLevel(method: UMethod): Boolean {
if (!(method.isStaticOrJvmStatic && method.visibility == UastVisibility.PUBLIC && method.uastParameters.size == 1)) {
return false
}
val param = method.uastParameters[0]
if (param.type.canonicalText != Project::class.java.canonicalName) return false
val qualifiedRef = getReturnExpression(method)?.returnExpression as? UQualifiedReferenceExpression ?: return false
return COMPONENT_MANAGER_GET_SERVICE.uCallMatches(qualifiedRef.selector as? UCallExpression) &&
(qualifiedRef.receiver as? USimpleNameReferenceExpression)?.resolveToUElement() == param
}
private fun findGetInstanceApplicationLevel(uClass: UClass): UMethod? {
return uClass.methods.find { isGetInstanceApplicationLevel(it) }
}
private fun isGetInstanceApplicationLevel(method: UMethod): Boolean {
if (!(method.isStaticOrJvmStatic && method.visibility == UastVisibility.PUBLIC && method.uastParameters.isEmpty())) {
return false
}
val qualifiedRef = getReturnExpression(method)?.returnExpression as? UQualifiedReferenceExpression ?: return false
return COMPONENT_MANAGER_GET_SERVICE.uCallMatches(qualifiedRef.selector as? UCallExpression) &&
qualifiedRef.receiver.getExpressionType()?.isInheritorOf(Application::class.java.canonicalName) == true
}
private val UMethod.isStaticOrJvmStatic: Boolean
get() = this.isStatic || this.findAnnotation(JvmStatic::class.java.canonicalName) != null
private fun getReturnExpression(method: UMethod): UReturnExpression? {
return (method.uastBody as? UBlockExpression)?.expressions?.singleOrNull() as? UReturnExpression
}
private class ReplaceWithGetInstanceCallFix(private val serviceName: String,
private val methodName: String,
private val howServiceRetrieved: Service.Level) : LocalQuickFix {
override fun getFamilyName(): String = DevKitBundle.message("inspection.simplifiable.service.retrieving.replace.with", serviceName,
methodName)
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val oldCall = descriptor.psiElement.toUElement()?.getParentOfType<UQualifiedReferenceExpression>() ?: return
val generationPlugin = UastCodeGenerationPlugin.byLanguage(descriptor.psiElement.language) ?: return
val factory = generationPlugin.getElementFactory(project)
val serviceName = oldCall.getExpressionType()?.canonicalText ?: return
val parameters = when (howServiceRetrieved) {
Service.Level.APP -> emptyList()
Service.Level.PROJECT -> listOf(oldCall.receiver)
}
val context = oldCall.sourcePsi
val receiver = factory.createQualifiedReference(serviceName, context) ?: factory.createSimpleReference(serviceName, context)
val newCall = factory.createCallExpression(receiver = receiver, methodName = methodName, parameters = parameters,
expectedReturnType = oldCall.getExpressionType(), kind = UastCallKind.METHOD_CALL,
context = null) ?: return
oldCall.replace(newCall)
}
}
}
@@ -44,7 +44,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)", 67,
assertEquals("Mismatch in total inspections, check classpath in test run configuration (intellij.devkit.plugin)", 68,
devkitInspections.size());
List<LocalInspectionEP> disabledInspections = ContainerUtil.filter(devkitInspections, ep -> !ep.enabledByDefault);
@@ -0,0 +1,25 @@
// 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("\$CONTENT_ROOT/testData/inspections/incorrectServiceRetrieving")
internal class IncorrectServiceRetrievingInspectionTest : IncorrectServiceRetrievingInspectionTestBase() {
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/incorrectServiceRetrieving/"
override fun getFileExtension() = "java"
fun testAppLevelServiceAsProjectLevel() {
doTest()
}
fun testProjectLevelServiceAsAppLevel() {
doTest()
}
fun testUnregisteredService() {
doTest()
}
}
@@ -1,35 +0,0 @@
// 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.DevKitBundle
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil
import org.jetbrains.idea.devkit.inspections.quickfix.RetrievingServiceInspectionTestBase
@TestDataPath("\$CONTENT_ROOT/testData/inspections/retrievingService")
internal class RetrievingServiceInspectionTest : RetrievingServiceInspectionTestBase() {
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/retrievingService/"
override fun getFileExtension() = "java"
fun testAppLevelServiceAsProjectLevel() {
doTest()
}
fun testProjectLevelServiceAsAppLevel() {
doTest()
}
fun testReplaceWithGetInstanceApplicationLevel() {
doTest(DevKitBundle.message("inspection.retrieving.service.replace.with", "MyService", "getInstance"))
}
fun testReplaceWithGetInstanceProjectLevel() {
doTest(DevKitBundle.message("inspection.retrieving.service.replace.with", "MyService", "getInstance"))
}
fun testUnregisteredService() {
doTest()
}
}
@@ -0,0 +1,22 @@
// 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.quickfix
import com.intellij.testFramework.TestDataPath
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil
@TestDataPath("\$CONTENT_ROOT/testData/inspections/simplifiableServiceRetrieving")
internal class SimplifiableServiceRetrievingInspectionTest : SimplifiableServiceRetrievingInspectionTestBase() {
override fun getBasePath() = DevkitJavaTestsUtil.TESTDATA_PATH + "inspections/simplifiableServiceRetrieving/"
override fun getFileExtension() = "java"
fun testReplaceWithGetInstanceApplicationLevel() {
doTest(DevKitBundle.message("inspection.simplifiable.service.retrieving.replace.with", "MyService", "getInstance"))
}
fun testReplaceWithGetInstanceProjectLevel() {
doTest(DevKitBundle.message("inspection.simplifiable.service.retrieving.replace.with", "MyService", "getInstance"))
}
}
@@ -0,0 +1,32 @@
// 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.IncorrectServiceRetrievingInspectionTestBase
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil
@TestDataPath("\$CONTENT_ROOT/testData/inspections/incorrectServiceRetrieving")
internal class KtIncorrectServiceRetrievingInspectionTest : IncorrectServiceRetrievingInspectionTestBase() {
override fun setUp() {
super.setUp()
myFixture.configureByFile("service.kt")
myFixture.configureByFile("services.kt")
}
override fun getBasePath() = DevkitKtTestsUtil.TESTDATA_PATH + "inspections/incorrectServiceRetrieving/"
override fun getFileExtension() = "kt"
fun testRetrievingServiceAsProjectLevel() {
doTest()
}
fun testRetrievingServiceAsAppLevel() {
doTest()
}
fun testUnregisteredServices() {
doTest()
}
}
@@ -1,32 +1,18 @@
// 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
package org.jetbrains.idea.devkit.kotlin.inspections.quickfix
import com.intellij.testFramework.TestDataPath
import org.jetbrains.idea.devkit.DevKitBundle
import org.jetbrains.idea.devkit.inspections.quickfix.RetrievingServiceInspectionTestBase
import org.jetbrains.idea.devkit.inspections.quickfix.SimplifiableServiceRetrievingInspectionTestBase
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil
@TestDataPath("\$CONTENT_ROOT/testData/inspections/retrievingService")
internal class KtRetrievingServiceInspectionTest : RetrievingServiceInspectionTestBase() {
@TestDataPath("\$CONTENT_ROOT/testData/inspections/simplifiableServiceRetrieving")
internal class KtSimplifiableServiceRetrievingInspectionTest : SimplifiableServiceRetrievingInspectionTestBase() {
override fun setUp() {
super.setUp()
myFixture.configureByFile("service.kt")
myFixture.configureByFile("services.kt")
}
override fun getBasePath() = DevkitKtTestsUtil.TESTDATA_PATH + "inspections/retrievingService/"
override fun getBasePath() = DevkitKtTestsUtil.TESTDATA_PATH + "inspections/simplifiableServiceRetrieving/"
override fun getFileExtension() = "kt"
fun testRetrievingServiceAsProjectLevel() {
doTest()
}
fun testRetrievingServiceAsAppLevel() {
doTest()
}
fun testReplaceWithGetInstanceApplicationLevel() {
myFixture.addClass(
"""
@@ -40,7 +26,7 @@ internal class KtRetrievingServiceInspectionTest : RetrievingServiceInspectionTe
}
}
""")
doTest(DevKitBundle.message("inspection.retrieving.service.replace.with", "MyService", "getInstance"))
doTest(DevKitBundle.message("inspection.simplifiable.service.retrieving.replace.with", "MyService", "getInstance"))
}
fun testReplaceWithGetInstanceProjectLevel() {
@@ -57,10 +43,6 @@ internal class KtRetrievingServiceInspectionTest : RetrievingServiceInspectionTe
}
}
""")
doTest(DevKitBundle.message("inspection.retrieving.service.replace.with", "MyService", "getInstance"))
}
fun testUnregisteredServices() {
doTest()
doTest(DevKitBundle.message("inspection.simplifiable.service.retrieving.replace.with", "MyService", "getInstance"))
}
}
@@ -0,0 +1,67 @@
// 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 org.jetbrains.idea.devkit.inspections.quickfix.LightDevKitInspectionFixTestBase
abstract class IncorrectServiceRetrievingInspectionTestBase : LightDevKitInspectionFixTestBase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(IncorrectServiceRetrievingInspection())
myFixture.addClass(
"""
package com.intellij.openapi.components;
public @interface Service {
Level[] value() default Level.APP;
enum Level { APP, PROJECT }
}
""")
myFixture.addClass(
"""
package com.intellij.openapi.components;
public interface ComponentManager {
<T> T getService(@NotNull Class<T> serviceClass);
<T> T getService(@NotNull Class<T> serviceClass, boolean createIfNeeded);
}
""")
myFixture.addClass(
"""
package com.intellij.openapi.project;
import com.intellij.openapi.components.ComponentManager;
public interface Project extends ComponentManager {}
""")
myFixture.addClass(
"""
package com.intellij.openapi.application;
public final class ApplicationManager {
private static Application ourApplication;
public static Application getApplication() {
return ourApplication;
}
}
""")
myFixture.addClass(
"""
package com.intellij.openapi.application;
import com.intellij.openapi.components.ComponentManager;
public interface Application extends ComponentManager {}
""")
myFixture.addClass(
"""
package kotlin.reflect;
public class KClass<T> {
public Class<T> java;
}
""")
}
}
@@ -1,13 +1,13 @@
// 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.quickfix
import org.jetbrains.idea.devkit.inspections.RetrievingServiceInspection
import org.jetbrains.idea.devkit.inspections.SimplifiableServiceRetrievingInspection
abstract class RetrievingServiceInspectionTestBase : LightDevKitInspectionFixTestBase() {
abstract class SimplifiableServiceRetrievingInspectionTestBase : LightDevKitInspectionFixTestBase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(RetrievingServiceInspection())
myFixture.enableInspections(SimplifiableServiceRetrievingInspection())
myFixture.addClass(
"""
package com.intellij.openapi.components;