Skip to content

Commit c24c1c3

Browse files
BoDmartinbonnin
andauthored
[IJ plugin] Inspection to suggest adding an introspection block (#5152)
Co-authored-by: Martin Bonnin <[email protected]>
1 parent c10f02a commit c24c1c3

24 files changed

+257
-44
lines changed

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspection.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.apollographql.ijplugin.inspection
22

33
import com.apollographql.ijplugin.ApolloBundle
44
import com.apollographql.ijplugin.action.ApolloV3ToV4MigrationAction
5+
import com.apollographql.ijplugin.project.apolloProjectService
56
import com.apollographql.ijplugin.util.getMethodName
67
import com.apollographql.ijplugin.util.unquoted
78
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
@@ -33,6 +34,7 @@ class Apollo4AvailableInspection : LocalInspectionTool() {
3334
private val registeredTomlVersionValues = mutableSetOf<PsiElement>()
3435

3536
override fun visitElement(element: PsiElement) {
37+
if (!element.project.apolloProjectService.apolloVersion.isAtLeastV3) return
3638
when {
3739
element.containingFile.name.endsWith(".versions.toml") && element is TomlLiteral -> {
3840
visitVersionsToml(element, holder)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloBundle
4+
import com.apollographql.ijplugin.project.apolloProjectService
5+
import com.apollographql.ijplugin.util.getMethodName
6+
import com.apollographql.ijplugin.util.lambdaBlockExpression
7+
import com.intellij.codeInspection.LocalInspectionTool
8+
import com.intellij.codeInspection.LocalQuickFix
9+
import com.intellij.codeInspection.ProblemDescriptor
10+
import com.intellij.codeInspection.ProblemsHolder
11+
import com.intellij.openapi.project.Project
12+
import com.intellij.psi.PsiElementVisitor
13+
import com.intellij.psi.util.findParentInFile
14+
import org.jetbrains.kotlin.psi.KtCallExpression
15+
import org.jetbrains.kotlin.psi.KtPsiFactory
16+
import org.jetbrains.kotlin.psi.KtVisitorVoid
17+
18+
class ApolloEndpointNotConfiguredInspection : LocalInspectionTool() {
19+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
20+
return object : KtVisitorVoid() {
21+
override fun visitCallExpression(expression: KtCallExpression) {
22+
super.visitCallExpression(expression)
23+
if (!expression.project.apolloProjectService.apolloVersion.isAtLeastV3) return
24+
if (expression.containingFile.name != "build.gradle.kts") return
25+
if (expression.getMethodName() == "service" && expression.findParentInFile { it is KtCallExpression && it.getMethodName() == "apollo" } != null) {
26+
val serviceBlockExpression = expression.lambdaBlockExpression() ?: return
27+
if (serviceBlockExpression.statements.none { it is KtCallExpression && it.getMethodName() == "introspection" }) {
28+
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.endpointNotConfigured.reportText"), AddIntrospectionBlockQuickFix)
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
35+
36+
object AddIntrospectionBlockQuickFix : LocalQuickFix {
37+
override fun getName() = ApolloBundle.message("inspection.endpointNotConfigured.quickFix")
38+
override fun getFamilyName() = name
39+
40+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
41+
val callExpression = descriptor.psiElement.parent as KtCallExpression
42+
val serviceBlockExpression = callExpression.lambdaBlockExpression() ?: return
43+
val ktFactory = KtPsiFactory(project)
44+
val newCallExpression = ktFactory.createExpression(
45+
"""
46+
introspection {
47+
endpointUrl.set("https://example.com/graphql")
48+
headers.put("api-key", "1234567890abcdef")
49+
schemaFile.set(file("src/main/graphql/schema.graphqls"))
50+
}
51+
""".trimIndent()
52+
)
53+
serviceBlockExpression.add(ktFactory.createNewLine())
54+
serviceBlockExpression.add(newCallExpression)
55+
}
56+
}

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspection.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ApolloUnusedFieldInspection : LocalInspectionTool() {
2929
if (!o.project.apolloProjectService.apolloVersion.isAtLeastV3) return
3030
if (isUnusedOperation) return
3131
val operation = o.findParentOfType<GraphQLTypedOperationDefinition>()
32-
if (operation != null && ApolloUnusedOperationInspection.isUnusedOperation(operation)) {
32+
if (operation != null && isUnusedOperation(operation)) {
3333
// The whole operation is unused, no need to check the fields
3434
isUnusedOperation = true
3535
return

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedOperationInspection.kt

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,25 @@ class ApolloUnusedOperationInspection : LocalInspectionTool() {
2525
}
2626
}
2727
}
28+
}
2829

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
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
4946
}
5047
}
48+
return true
5149
}

intellij-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
/>
105105

106106
<!-- "Apollo Kotlin 4 is available" inspection -->
107-
<!--suppress PluginXmlCapitalization -->
107+
<!--suppress PluginXmlCapitalization, PluginXmlExtensionRegistration -->
108108
<localInspection
109109
implementationClass="com.apollographql.ijplugin.inspection.Apollo4AvailableInspection"
110110
groupPathKey="inspection.group.graphql"
@@ -140,6 +140,18 @@
140140
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
141141
/>
142142

143+
<!-- "Missing introspection" inspection -->
144+
<!--suppress PluginXmlCapitalization -->
145+
<localInspection
146+
language="kotlin"
147+
implementationClass="com.apollographql.ijplugin.inspection.ApolloEndpointNotConfiguredInspection"
148+
groupPathKey="inspection.group.graphql"
149+
groupKey="inspection.group.graphql.apolloKotlin"
150+
key="inspection.endpointNotConfigured.displayName"
151+
enabledByDefault="true"
152+
level="INFO"
153+
/>
154+
143155
<!-- Fields insights service (fetch and cache data) -->
144156
<projectService
145157
serviceInterface="com.apollographql.ijplugin.studio.fieldinsights.FieldInsightsService"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<html>
2+
<body>
3+
Reports Apollo services that don't configure a GraphQL endpoint.
4+
<p>
5+
Configuring a service's GraphQL endpoint URL is recommended, as it enables the execution of queries directly from the IDE.<br>
6+
It also allows downloading the latest version of the schema easily, via a Gradle task or via the <kbd>Download Schema</kbd> action.<br>
7+
8+
</p>
9+
<p>
10+
<a href="https://www.apollographql.com/docs/kotlin/advanced/plugin-configuration#downloading-a-schema">More information</a>
11+
</p>
12+
</body>
13+
</html>

intellij-plugin/src/main/resources/messages/ApolloBundle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,5 +121,8 @@ inspection.unusedField.quickFix=Delete field
121121
inspection.apollo4Available.displayName=Apollo Kotlin 4 is available
122122
inspection.apollo4Available.reportText=Apollo Kotlin 4 is available
123123
inspection.apollo4Available.quickFix=Migrate to Apollo Kotlin 4
124+
inspection.endpointNotConfigured.displayName=GraphQL endpoint not configured
125+
inspection.endpointNotConfigured.reportText=GraphQL endpoint not configured
126+
inspection.endpointNotConfigured.quickFix=Add introspection block
124127

125128
notification.group.apollo=Apollo

intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/Apollo4AvailableInspectionTest.kt renamed to intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspectionTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package com.apollographql.ijplugin
1+
package com.apollographql.ijplugin.inspection
22

3-
import com.apollographql.ijplugin.inspection.Apollo4AvailableInspection
3+
import com.apollographql.ijplugin.ApolloTestCase
44
import com.intellij.testFramework.TestDataPath
55
import org.junit.Test
66
import org.junit.runner.RunWith
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloTestCase
4+
import com.intellij.testFramework.TestDataPath
5+
import org.junit.Test
6+
import org.junit.runner.RunWith
7+
import org.junit.runners.JUnit4
8+
9+
@TestDataPath("\$CONTENT_ROOT/testData/inspection")
10+
@RunWith(JUnit4::class)
11+
class ApolloEndpointNotConfiguredInspectionTest : ApolloTestCase() {
12+
13+
override fun getTestDataPath() = "src/test/testData/inspection"
14+
15+
@Throws(Exception::class)
16+
override fun setUp() {
17+
super.setUp()
18+
myFixture.enableInspections(ApolloEndpointNotConfiguredInspection())
19+
}
20+
21+
@Test
22+
fun testInspectionBuildGradleKts() {
23+
myFixture.openFileInEditor(myFixture.copyFileToProject("EndpointNotConfigured.gradle.kts", "build.gradle.kts"))
24+
var highlightInfos = doHighlighting().filter { it.description == "GraphQL endpoint not configured" }
25+
assertSize(2, highlightInfos)
26+
assertTrue(highlightInfos[0].line == 6)
27+
assertTrue(highlightInfos[1].line == 24)
28+
29+
moveCaret("service(\"a\")")
30+
var introspectionQuickFixAction = myFixture.findSingleIntention("Add introspection block")
31+
assertNotNull(introspectionQuickFixAction)
32+
myFixture.launchAction(introspectionQuickFixAction)
33+
34+
moveCaret("service(\"d\")")
35+
introspectionQuickFixAction = myFixture.findSingleIntention("Add introspection block")
36+
assertNotNull(introspectionQuickFixAction)
37+
myFixture.launchAction(introspectionQuickFixAction)
38+
39+
myFixture.checkResultByFile("EndpointNotConfigured_after.gradle.kts")
40+
highlightInfos = doHighlighting()
41+
assertTrue(highlightInfos.none { it.description == "GraphQL endpoint not configured" })
42+
}
43+
}

intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/ApolloFieldInsightsInspectionTest.kt renamed to intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloFieldInsightsInspectionTest.kt

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

3+
import com.apollographql.ijplugin.ApolloTestCase
34
import com.apollographql.ijplugin.gradle.ApolloKotlinService
45
import com.apollographql.ijplugin.studio.fieldinsights.ApolloFieldInsightsInspection
56
import com.apollographql.ijplugin.studio.fieldinsights.FieldInsightsService
@@ -38,10 +39,10 @@ class ApolloFieldInsightsInspectionTest : ApolloTestCase() {
3839
myFixture.configureByFile("FieldInsights.graphql")
3940
var highlightInfos = doHighlighting()
4041
assertTrue(highlightInfos.any { it.description == "firstName has a high latency: ~150 ms" })
41-
val quickFixAction = myFixture.findSingleIntention("Enclose in @defer fragment");
42+
val quickFixAction = myFixture.findSingleIntention("Enclose in @defer fragment")
4243
assertNotNull(quickFixAction)
4344
myFixture.launchAction(quickFixAction)
44-
myFixture.checkResultByFile("FieldInsights.after.graphql");
45+
myFixture.checkResultByFile("FieldInsights_after.graphql")
4546

4647
highlightInfos = doHighlighting()
4748
assertTrue(highlightInfos.none { it.description == "firstName has a high latency: ~150 ms" })

0 commit comments

Comments
 (0)