Skip to content

Commit a788b77

Browse files
authored
[IJ/AS Plugin] Add unused operation and unused field inspections (#5069)
1 parent ac3f2a1 commit a788b77

26 files changed

+515
-55
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloBundle
4+
import com.apollographql.ijplugin.navigation.compat.KotlinFindUsagesHandlerFactoryCompat
5+
import com.apollographql.ijplugin.navigation.findKotlinFieldDefinitions
6+
import com.apollographql.ijplugin.navigation.findKotlinFragmentSpreadDefinitions
7+
import com.apollographql.ijplugin.navigation.findKotlinInlineFragmentDefinitions
8+
import com.apollographql.ijplugin.project.apolloProjectService
9+
import com.apollographql.ijplugin.util.isProcessCanceled
10+
import com.intellij.codeInspection.LocalInspectionTool
11+
import com.intellij.codeInspection.ProblemsHolder
12+
import com.intellij.lang.jsgraphql.psi.GraphQLField
13+
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentSpread
14+
import com.intellij.lang.jsgraphql.psi.GraphQLIdentifier
15+
import com.intellij.lang.jsgraphql.psi.GraphQLInlineFragment
16+
import com.intellij.lang.jsgraphql.psi.GraphQLSelection
17+
import com.intellij.lang.jsgraphql.psi.GraphQLTypeName
18+
import com.intellij.lang.jsgraphql.psi.GraphQLTypedOperationDefinition
19+
import com.intellij.lang.jsgraphql.psi.GraphQLVisitor
20+
import com.intellij.psi.PsiElementVisitor
21+
import com.intellij.psi.util.findParentOfType
22+
23+
class ApolloUnusedFieldInspection : LocalInspectionTool() {
24+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
25+
var isUnusedOperation = false
26+
return object : GraphQLVisitor() {
27+
override fun visitIdentifier(o: GraphQLIdentifier) {
28+
if (isProcessCanceled()) return
29+
if (!o.project.apolloProjectService.apolloVersion.isAtLeastV3) return
30+
if (isUnusedOperation) return
31+
val operation = o.findParentOfType<GraphQLTypedOperationDefinition>()
32+
if (operation != null && ApolloUnusedOperationInspection.isUnusedOperation(operation)) {
33+
// The whole operation is unused, no need to check the fields
34+
isUnusedOperation = true
35+
return
36+
}
37+
38+
var isFragment = false
39+
val ktDefinitions = when (val parent = o.parent) {
40+
is GraphQLField -> findKotlinFieldDefinitions(parent)
41+
is GraphQLFragmentSpread -> {
42+
isFragment = true
43+
findKotlinFragmentSpreadDefinitions(parent)
44+
}
45+
is GraphQLTypeName -> {
46+
val inlineFragment = parent.parent?.parent as? GraphQLInlineFragment ?: return
47+
isFragment = true
48+
findKotlinInlineFragmentDefinitions(inlineFragment)
49+
}
50+
else -> return
51+
}.ifEmpty { return }
52+
53+
val kotlinFindUsagesHandlerFactory = KotlinFindUsagesHandlerFactoryCompat(o.project)
54+
val hasUsageProcessor = HasUsageProcessor()
55+
for (kotlinDefinition in ktDefinitions) {
56+
if (kotlinFindUsagesHandlerFactory.canFindUsages(kotlinDefinition)) {
57+
val kotlinFindUsagesHandler = kotlinFindUsagesHandlerFactory.createFindUsagesHandler(kotlinDefinition, false)
58+
?: return
59+
val findUsageOptions = kotlinFindUsagesHandlerFactory.findPropertyOptions ?: return
60+
kotlinFindUsagesHandler.processElementUsages(kotlinDefinition, hasUsageProcessor, findUsageOptions)
61+
if (hasUsageProcessor.foundUsage) return
62+
}
63+
}
64+
holder.registerProblem(
65+
if (isFragment) o.findParentOfType<GraphQLSelection>()!! else o,
66+
ApolloBundle.message("inspection.unusedField.reportText"),
67+
DeleteElementQuickFix("inspection.unusedField.quickFix") { it.findParentOfType<GraphQLSelection>(strict = false)!! },
68+
)
69+
}
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloBundle
4+
import com.apollographql.ijplugin.navigation.compat.KotlinFindUsagesHandlerFactoryCompat
5+
import com.apollographql.ijplugin.navigation.findKotlinOperationDefinitions
6+
import com.apollographql.ijplugin.project.apolloProjectService
7+
import com.apollographql.ijplugin.util.isProcessCanceled
8+
import com.intellij.codeInspection.LocalInspectionTool
9+
import com.intellij.codeInspection.ProblemsHolder
10+
import com.intellij.lang.jsgraphql.psi.GraphQLTypedOperationDefinition
11+
import com.intellij.lang.jsgraphql.psi.GraphQLVisitor
12+
import com.intellij.psi.PsiElementVisitor
13+
14+
class ApolloUnusedOperationInspection : LocalInspectionTool() {
15+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
16+
return object : GraphQLVisitor() {
17+
override fun visitTypedOperationDefinition(o: GraphQLTypedOperationDefinition) {
18+
if (isUnusedOperation(o)) {
19+
holder.registerProblem(
20+
o,
21+
ApolloBundle.message("inspection.unusedOperation.reportText"),
22+
DeleteElementQuickFix("inspection.unusedOperation.quickFix") { it }
23+
)
24+
}
25+
}
26+
}
27+
}
28+
29+
companion object {
30+
fun isUnusedOperation(operationDefinition: GraphQLTypedOperationDefinition): Boolean {
31+
if (isProcessCanceled()) return false
32+
if (!operationDefinition.project.apolloProjectService.apolloVersion.isAtLeastV3) return false
33+
val ktClasses = findKotlinOperationDefinitions(operationDefinition).ifEmpty {
34+
// Didn't find any generated class: maybe in the middle of writing a new operation, let's not report an error yet.
35+
return false
36+
}
37+
val kotlinFindUsagesHandlerFactory = KotlinFindUsagesHandlerFactoryCompat(operationDefinition.project)
38+
val hasUsageProcessor = HasUsageProcessor()
39+
for (kotlinDefinition in ktClasses) {
40+
if (kotlinFindUsagesHandlerFactory.canFindUsages(kotlinDefinition)) {
41+
val kotlinFindUsagesHandler = kotlinFindUsagesHandlerFactory.createFindUsagesHandler(kotlinDefinition, false)
42+
?: return false
43+
val findUsageOptions = kotlinFindUsagesHandlerFactory.findClassOptions ?: return false
44+
kotlinFindUsagesHandler.processElementUsages(kotlinDefinition, hasUsageProcessor, findUsageOptions)
45+
if (hasUsageProcessor.foundUsage) return false
46+
}
47+
}
48+
return true
49+
}
50+
}
51+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloBundle
4+
import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview
5+
import com.intellij.codeInspection.LocalQuickFix
6+
import com.intellij.codeInspection.ProblemDescriptor
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.psi.PsiElement
9+
10+
class DeleteElementQuickFix(
11+
private val label: String,
12+
13+
@SafeFieldForPreview
14+
private val elementToDelete: (PsiElement) -> PsiElement,
15+
) : LocalQuickFix {
16+
override fun getName() = ApolloBundle.message(label)
17+
18+
override fun getFamilyName() = name
19+
20+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
21+
elementToDelete(descriptor.psiElement).delete()
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.util.isGenerated
4+
import com.intellij.usageView.UsageInfo
5+
import com.intellij.util.Processor
6+
7+
class HasUsageProcessor : Processor<UsageInfo> {
8+
var foundUsage = false
9+
private set
10+
11+
override fun process(usageInfo: UsageInfo): Boolean {
12+
if (usageInfo.virtualFile?.isGenerated(usageInfo.project) == false) {
13+
foundUsage = true
14+
return false
15+
}
16+
return true
17+
}
18+
}

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/navigation/KotlinNavigation.kt

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.apollographql.ijplugin.navigation
22

33
import com.apollographql.ijplugin.util.capitalizeFirstLetter
4+
import com.apollographql.ijplugin.util.decapitalizeFirstLetter
45
import com.apollographql.ijplugin.util.findChildrenOfType
6+
import com.intellij.lang.jsgraphql.psi.GraphQLElement
57
import com.intellij.lang.jsgraphql.psi.GraphQLEnumTypeDefinition
68
import com.intellij.lang.jsgraphql.psi.GraphQLEnumValue
79
import com.intellij.lang.jsgraphql.psi.GraphQLField
810
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentDefinition
911
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentSpread
12+
import com.intellij.lang.jsgraphql.psi.GraphQLInlineFragment
1013
import com.intellij.lang.jsgraphql.psi.GraphQLInputObjectTypeDefinition
1114
import com.intellij.lang.jsgraphql.psi.GraphQLInputValueDefinition
1215
import com.intellij.lang.jsgraphql.psi.GraphQLTypedOperationDefinition
@@ -17,9 +20,13 @@ import com.intellij.psi.search.GlobalSearchScope
1720
import com.intellij.psi.search.PsiShortNamesCache
1821
import com.intellij.psi.util.parentOfType
1922
import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass
23+
import org.jetbrains.kotlin.idea.base.utils.fqname.fqName
24+
import org.jetbrains.kotlin.nj2k.postProcessing.type
2025
import org.jetbrains.kotlin.psi.KtClass
2126
import org.jetbrains.kotlin.psi.KtEnumEntry
27+
import org.jetbrains.kotlin.psi.KtNamedDeclaration
2228
import org.jetbrains.kotlin.psi.KtParameter
29+
import org.jetbrains.kotlin.psi.KtProperty
2330

2431
fun findKotlinOperationDefinitions(operationDefinition: GraphQLTypedOperationDefinition): List<KtClass> {
2532
val operationName = operationDefinition.name ?: return emptyList()
@@ -36,23 +43,102 @@ fun findKotlinOperationDefinitions(operationDefinition: GraphQLTypedOperationDef
3643
}
3744

3845
fun findKotlinFieldDefinitions(graphQLField: GraphQLField): List<PsiElement> {
39-
// TODO We can disambiguate fields with the same name by using the path to the field
40-
return (
41-
// Try operation first
42-
graphQLField.parentOfType<GraphQLTypedOperationDefinition>()?.let { operationDefinition ->
43-
findKotlinOperationDefinitions(operationDefinition)
46+
val path = graphQLField.pathFromRoot()
47+
val ktClasses = findKotlinClassOfParent(graphQLField)
48+
return ktClasses?.mapNotNull { ktClass ->
49+
// Try Data class first (operations)
50+
var c = ktClass.findChildrenOfType<KtClass> { it.name == "Data" }.firstOrNull()
51+
// Fallback to class itself (fragments)
52+
?: ktClass
53+
var ktFieldDefinition: KtNamedDeclaration? = null
54+
for ((i, pathElement) in path.withIndex()) {
55+
// Look for the element in the constructor parameters (for data classes) and in the properties (for interfaces)
56+
val properties = c.primaryConstructor?.valueParameters.orEmpty() + c.getProperties()
57+
ktFieldDefinition = properties.firstOrNull { it.name == pathElement } ?: continue
58+
val parameterType = ktFieldDefinition.type()
59+
val parameterTypeFqName =
60+
// Try Lists first
61+
parameterType?.arguments?.firstOrNull()?.type?.fqName
62+
// Fallback to regular type
63+
?: parameterType?.fqName
64+
?: break
65+
if (i != path.lastIndex) {
66+
c = ktClass.findChildrenOfType<KtClass> { it.fqName == parameterTypeFqName }.firstOrNull() ?: return@mapNotNull null
4467
}
45-
// Fallback to fragment
46-
?: graphQLField.parentOfType<GraphQLFragmentDefinition>()?.let { fragmentDefinition ->
47-
findKotlinFragmentClassDefinitions(fragmentDefinition)
48-
}
49-
)
68+
}
69+
ktFieldDefinition
70+
}
71+
// Fallback to just finding any property with the name (for responseBased)
72+
?: ktClasses?.flatMap { ktClass ->
73+
ktClass.findChildrenOfType<KtProperty> { it.name == graphQLField.name }
74+
}
75+
?: emptyList()
76+
}
77+
78+
/**
79+
* Ex:
80+
* ```graphql
81+
* a {
82+
* b
83+
* ... on MyType {
84+
* c
85+
* }
86+
* }
87+
* ```
88+
* returns `["a", "b", "onMyType", "c"]`
89+
*/
90+
private fun GraphQLField.pathFromRoot(): List<String> {
91+
val path = mutableListOf<String>()
92+
var element: GraphQLElement = this
93+
while (true) {
94+
element = when (element) {
95+
is GraphQLInlineFragment -> {
96+
path.add(0,element.kotlinFieldName() ?: break)
97+
element.parent?.parent?.parent?.parent as? GraphQLElement ?: break
98+
}
99+
100+
is GraphQLField -> {
101+
path.add(0,element.name!!)
102+
element.parent?.parent?.parent as? GraphQLElement ?: break
103+
}
104+
105+
else -> break
106+
}
107+
if (element !is GraphQLField && element !is GraphQLInlineFragment) break
108+
}
109+
return path
110+
}
111+
112+
fun findKotlinFragmentSpreadDefinitions(graphQLFragmentSpread: GraphQLFragmentSpread): List<PsiElement> {
113+
return findKotlinClassOfParent(graphQLFragmentSpread)
50114
?.flatMap { psiClass ->
51-
psiClass.findChildrenOfType<KtParameter> { it.name == graphQLField.name }
115+
psiClass.findChildrenOfType<KtParameter> { it.name == graphQLFragmentSpread.name?.decapitalizeFirstLetter() }
52116
}
53117
?: emptyList()
54118
}
55119

120+
fun findKotlinInlineFragmentDefinitions(graphQLFragmentSpread: GraphQLInlineFragment): List<PsiElement> {
121+
return findKotlinClassOfParent(graphQLFragmentSpread)
122+
?.flatMap { psiClass ->
123+
psiClass.findChildrenOfType<KtParameter> { it.name == graphQLFragmentSpread.kotlinFieldName() }
124+
}
125+
?: emptyList()
126+
}
127+
128+
private fun GraphQLInlineFragment.kotlinFieldName() = typeCondition?.typeName?.name?.capitalizeFirstLetter()?.let { "on$it" }
129+
130+
private fun findKotlinClassOfParent(gqlElement: GraphQLElement): List<KtClass>? {
131+
// Try operation first
132+
return gqlElement.parentOfType<GraphQLTypedOperationDefinition>()?.let { operationDefinition ->
133+
findKotlinOperationDefinitions(operationDefinition)
134+
}
135+
// Fallback to fragment
136+
?: gqlElement.parentOfType<GraphQLFragmentDefinition>()?.let { fragmentDefinition ->
137+
findKotlinFragmentClassDefinitions(fragmentDefinition)
138+
}
139+
}
140+
141+
56142
fun findKotlinFragmentClassDefinitions(fragmentSpread: GraphQLFragmentSpread): List<KtClass> {
57143
val fragmentName = fragmentSpread.nameIdentifier.referenceName ?: return emptyList()
58144
return findKotlinClass(fragmentSpread.project, fragmentName) { it.isApolloFragment() }

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/navigation/compat/KotlinFindUsagesHandlerFactoryCompat.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ class KotlinFindUsagesHandlerFactoryCompat(project: Project) : FindUsagesHandler
1515
// Try with the recent version first (changed package since platform 231)
1616
Class.forName(POST_231_CLASS_NAME)
1717
}
18-
.onFailure { logw(it, "Could not load $POST_231_CLASS_NAME") }
1918
.recoverCatching {
2019
// Fallback to the old version
2120
Class.forName(PRE_231_CLASS_NAME)
2221
}
23-
.onFailure { logw(it, "Could not load <231 KotlinFindUsagesHandlerFactory") }
22+
.onFailure { logw(it, "Could not load either $POST_231_CLASS_NAME nor $PRE_231_CLASS_NAME") }
2423
.getOrNull()
2524

2625
private val delegate: FindUsagesHandlerFactory? = delegateClass?.let { it.getConstructor(Project::class.java).newInstance(project) as FindUsagesHandlerFactory }

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Files.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ fun Project.findPsiFilesByName(fileName: String, searchScope: GlobalSearchScope)
1515
}
1616

1717
fun VirtualFile.isGenerated(project: Project): Boolean {
18-
return GeneratedSourcesFilter.isGeneratedSourceByAnyFilter(this, project) || isApolloGenerated()
18+
return GeneratedSourcesFilter.isGeneratedSourceByAnyFilter(this, project) || isApolloGenerated() || name.endsWith(".keystream")
1919
}

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.apollographql.ijplugin.util
22

3-
import com.intellij.openapi.progress.JobCanceledException
3+
import com.intellij.openapi.diagnostic.ControlFlowException
44
import com.intellij.psi.PsiElement
55
import com.intellij.psi.util.PsiTreeUtil
66
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
@@ -51,9 +51,8 @@ fun PsiElement.resolveKtName(): PsiElement? = runCatching {
5151
references.firstIsInstanceOrNull<KtSimpleNameReference>()?.resolve()
5252
}.onFailure { t ->
5353
// Sometimes KotlinIdeaResolutionException is thrown
54-
@Suppress("UnstableApiUsage")
55-
// JobCanceledException is a normal thing to happen though so no need to log
56-
if (t !is JobCanceledException) logw(t, "Could not resolve $this")
54+
// But ControlFlowException is a normal thing to happen, so no need to log
55+
if (t !is ControlFlowException) logw(t, "Could not resolve $this")
5756
}.getOrNull()
5857

5958
fun PsiElement.asKtClass(): KtClass? = cast<KtClass>() ?: cast<KtConstructor<*>>()?.containingClass()

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/String.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ fun String.unquoted(): String {
1111
}
1212

1313
fun String.capitalizeFirstLetter() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
14+
fun String.decapitalizeFirstLetter() = replaceFirstChar { if (it.isUpperCase()) it.lowercase(Locale.ROOT) else it.toString() }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
package com.apollographql.ijplugin.util
22

33
import com.intellij.openapi.application.ApplicationManager
4+
import com.intellij.openapi.progress.ProcessCanceledException
5+
import com.intellij.openapi.progress.ProgressManager
46

57
fun runWriteActionInEdt(action: () -> Unit) {
68
ApplicationManager.getApplication().invokeLater {
79
ApplicationManager.getApplication().runWriteAction<Unit>(action)
810
}
911
}
12+
13+
fun isProcessCanceled(): Boolean {
14+
try {
15+
ProgressManager.checkCanceled()
16+
} catch (e: ProcessCanceledException) {
17+
return true
18+
}
19+
return false
20+
}

0 commit comments

Comments
 (0)