mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-13 09:19:13 +07:00
[devkit] Split the "Incorrect or simplifiable retrieving service" inspection into two
IDEA-327590 GitOrigin-RevId: 721b34c773e16b93cc593080ba2dbc130606ed40
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9feb83f731
commit
073114cdb2
+28
@@ -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<MyProjectService>() // The project-level service is retrieved as an application-level service
|
||||
val applicationService = project.service<MyAppService>() // The application-level service is retrieved as a project-level service
|
||||
</code></pre>
|
||||
<pre><code lang="kotlin">
|
||||
// Good:
|
||||
val projectService = project.service<MyProjectService>()
|
||||
val applicationService = service<MyAppService>();
|
||||
</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>
|
||||
+33
@@ -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
|
||||
}
|
||||
}
|
||||
+122
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+25
@@ -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()
|
||||
}
|
||||
}
|
||||
-35
@@ -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()
|
||||
}
|
||||
}
|
||||
+22
@@ -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"))
|
||||
}
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
}
|
||||
+7
-25
@@ -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"))
|
||||
}
|
||||
}
|
||||
+67
@@ -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;
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
Reference in New Issue
Block a user