Skip to content

Commit fa78387

Browse files
authored
[IJ Plugin] Suggest Apollo 4 migration from version catalog and build.gradle.kts dependencies (#5141)
1 parent 52e03ff commit fa78387

File tree

14 files changed

+332
-38
lines changed

14 files changed

+332
-38
lines changed

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/action/ApolloV3ToV4MigrationAction.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import com.intellij.openapi.actionSystem.AnActionEvent
1212
import com.intellij.openapi.ui.Messages
1313

1414
class ApolloV3ToV4MigrationAction : AnAction() {
15+
companion object {
16+
val ACTION_ID: String = ApolloV3ToV4MigrationAction::class.java.simpleName
17+
}
18+
1519
override fun actionPerformed(e: AnActionEvent) {
1620
logd()
1721
val okCancelResult = Messages.showOkCancelDialog(
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.apollographql.ijplugin.inspection
2+
3+
import com.apollographql.ijplugin.ApolloBundle
4+
import com.apollographql.ijplugin.action.ApolloV3ToV4MigrationAction
5+
import com.apollographql.ijplugin.util.getMethodName
6+
import com.apollographql.ijplugin.util.unquoted
7+
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
8+
import com.intellij.codeInspection.LocalInspectionTool
9+
import com.intellij.codeInspection.LocalQuickFix
10+
import com.intellij.codeInspection.ProblemDescriptor
11+
import com.intellij.codeInspection.ProblemsHolder
12+
import com.intellij.openapi.actionSystem.ActionManager
13+
import com.intellij.openapi.project.Project
14+
import com.intellij.psi.PsiElement
15+
import com.intellij.psi.PsiElementVisitor
16+
import org.jetbrains.kotlin.psi.KtBinaryExpression
17+
import org.jetbrains.kotlin.psi.KtCallExpression
18+
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
19+
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
20+
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
21+
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
22+
import org.toml.lang.psi.TomlInlineTable
23+
import org.toml.lang.psi.TomlLiteral
24+
import org.toml.lang.psi.TomlTable
25+
import org.toml.lang.psi.ext.TomlLiteralKind
26+
import org.toml.lang.psi.ext.kind
27+
28+
private const val apollo3 = "com.apollographql.apollo3"
29+
30+
class Apollo4AvailableInspection : LocalInspectionTool() {
31+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
32+
return object : PsiElementVisitor() {
33+
private val registeredTomlVersionValues = mutableSetOf<PsiElement>()
34+
35+
override fun visitElement(element: PsiElement) {
36+
when {
37+
element.containingFile.name.endsWith(".versions.toml") && element is TomlLiteral -> {
38+
visitVersionsToml(element, holder)
39+
}
40+
41+
element.containingFile.name == "build.gradle.kts" && element is KtCallExpression -> {
42+
visitBuildGradleKts(element, holder)
43+
}
44+
}
45+
}
46+
47+
private fun visitVersionsToml(element: TomlLiteral, holder: ProblemsHolder) {
48+
if (element.kind !is TomlLiteralKind.String) return
49+
val dependencyText = element.text.unquoted()
50+
if (dependencyText == apollo3 || dependencyText.startsWith("$apollo3:")) {
51+
// Find the associated version
52+
val versionEntry = (element.parent.parent as? TomlInlineTable)?.entries
53+
?.first { it.key.text == "version" || it.key.text == "version.ref" } ?: return
54+
if (versionEntry.key.text == "version") {
55+
val version = versionEntry.value?.firstChild?.text?.unquoted() ?: return
56+
if (!version.startsWith("4")) {
57+
holder.registerProblem(element.parent.parent.parent, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
58+
}
59+
} else {
60+
// Resolve the reference
61+
val versionsTable = element.containingFile.children.filterIsInstance<TomlTable>()
62+
.firstOrNull { it.header.key?.text == "versions" } ?: return
63+
val versionRefKey = versionEntry.value?.text?.unquoted()
64+
val refTarget = versionsTable.entries.firstOrNull { it.key.text == versionRefKey } ?: return
65+
val version = refTarget.value?.firstChild?.text?.unquoted() ?: return
66+
if (!version.startsWith("4")) {
67+
// Do not highlight the same element several times
68+
if (refTarget.value!! !in registeredTomlVersionValues) {
69+
holder.registerProblem(refTarget.value!!, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
70+
registeredTomlVersionValues.add(refTarget.value!!)
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
private fun visitBuildGradleKts(callExpression: KtCallExpression, holder: ProblemsHolder) {
78+
when (callExpression.getMethodName()) {
79+
"id" -> {
80+
// id("xxx")
81+
val dependencyText = callExpression.getArgumentAsStringTemplateEntries(0)?.getSingleEntry() ?: return
82+
if (dependencyText != apollo3) return
83+
when (val element = callExpression.parent) {
84+
is KtBinaryExpression -> {
85+
// id("xxx") version yyy
86+
val version = (element.right as? KtStringTemplateExpression)?.entries?.getSingleEntry() ?: return
87+
if (!version.startsWith("4")) {
88+
holder.registerProblem(element, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
89+
}
90+
}
91+
92+
is KtDotQualifiedExpression -> {
93+
// id("xxx").version(yyy)
94+
val versionCallExpression = element.selectorExpression as? KtCallExpression
95+
val version = versionCallExpression?.getArgumentAsStringTemplateEntries(0)?.getSingleEntry() ?: return
96+
if (!version.startsWith("4")) {
97+
holder.registerProblem(element, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
98+
}
99+
}
100+
}
101+
}
102+
103+
"implementation", "api", "testImplementation", "testApi" -> {
104+
when (callExpression.valueArguments.size) {
105+
// implementation("xxx:yyy:zzz")
106+
1 -> {
107+
val dependency = callExpression.getArgumentAsStringTemplateEntries(0)?.getSingleEntry() ?: return
108+
val dependencyElements = dependency.split(":")
109+
if (dependencyElements.size != 3) return
110+
val groupId = dependencyElements[0]
111+
if (groupId != apollo3) return
112+
val version = dependencyElements[2]
113+
if (!version.startsWith("4")) {
114+
holder.registerProblem(callExpression, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
115+
}
116+
}
117+
118+
119+
// implementation("xxx", "yyy", "zzz")
120+
3 -> {
121+
val groupId = callExpression.getArgumentAsStringTemplateEntries(0)?.getSingleEntry() ?: return
122+
if (groupId != apollo3) return
123+
val version = callExpression.getArgumentAsStringTemplateEntries(2)?.getSingleEntry() ?: return
124+
if (!version.startsWith("4")) {
125+
holder.registerProblem(callExpression, ApolloBundle.message("inspection.apollo4Available.reportText"), Apollo4AvailableQuickFix)
126+
}
127+
}
128+
}
129+
}
130+
}
131+
}
132+
133+
private fun KtCallExpression.getArgumentAsStringTemplateEntries(index: Int): Array<KtStringTemplateEntry>? =
134+
(valueArgumentList?.arguments?.getOrNull(index)
135+
?.children?.firstOrNull() as? KtStringTemplateExpression)?.entries
136+
137+
// Only consider simple strings (no templates)
138+
private fun Array<KtStringTemplateEntry>.getSingleEntry(): String? {
139+
if (size != 1 || this[0] !is KtLiteralStringTemplateEntry) return null
140+
return this[0].text.unquoted()
141+
}
142+
}
143+
}
144+
}
145+
146+
object Apollo4AvailableQuickFix : LocalQuickFix {
147+
override fun getName() = ApolloBundle.message("inspection.apollo4Available.quickFix")
148+
149+
override fun getFamilyName() = name
150+
151+
override fun availableInBatchMode() = false
152+
153+
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor) = IntentionPreviewInfo.EMPTY
154+
155+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
156+
val action = ActionManager.getInstance().getAction(ApolloV3ToV4MigrationAction.ACTION_ID)
157+
ActionManager.getInstance().tryToExecute(action, null, null, null, false)
158+
}
159+
}

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

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

33
import com.apollographql.ijplugin.ApolloBundle
4+
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
45
import com.intellij.codeInspection.LocalInspectionTool
56
import com.intellij.codeInspection.LocalQuickFix
67
import com.intellij.codeInspection.ProblemDescriptor
@@ -44,6 +45,8 @@ class ApolloSchemaInGraphqlFileInspection : LocalInspectionTool() {
4445
}
4546
override fun getFamilyName() = name
4647

48+
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor) = IntentionPreviewInfo.EMPTY
49+
4750
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
4851
val psiFile = descriptor.psiElement.containingFile
4952
val newName = psiFile.name.replace(".graphql", ".graphqls")

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/item/CommentDependenciesInToml.kt

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

3-
import com.apollographql.ijplugin.util.findPsiFilesByName
3+
import com.apollographql.ijplugin.util.findPsiFilesByExtension
44
import com.apollographql.ijplugin.util.unquoted
55
import com.intellij.codeInspection.SuppressionUtil.createComment
66
import com.intellij.openapi.project.Project
@@ -20,7 +20,7 @@ class CommentDependenciesInToml(
2020
private vararg val artifactId: String,
2121
) : MigrationItem() {
2222
override fun findUsages(project: Project, migration: PsiMigration, searchScope: GlobalSearchScope): List<MigrationItemUsageInfo> {
23-
val libsVersionTomlFiles: List<PsiFile> = project.findPsiFilesByName("libs.versions.toml", searchScope)
23+
val libsVersionTomlFiles: List<PsiFile> = project.findPsiFilesByExtension("versions.toml", searchScope)
2424
val usages = mutableListOf<MigrationItemUsageInfo>()
2525
for (file in libsVersionTomlFiles) {
2626
if (file !is TomlFile) continue

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/item/UpdateGradleDependenciesInToml.kt

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

3-
import com.apollographql.ijplugin.util.findPsiFilesByName
3+
import com.apollographql.ijplugin.util.findPsiFilesByExtension
44
import com.apollographql.ijplugin.util.quoted
55
import com.apollographql.ijplugin.util.unquoted
66
import com.intellij.openapi.project.Project
@@ -22,7 +22,7 @@ class UpdateGradleDependenciesInToml(
2222
private val newVersion: String,
2323
) : MigrationItem() {
2424
override fun findUsages(project: Project, migration: PsiMigration, searchScope: GlobalSearchScope): List<MigrationItemUsageInfo> {
25-
val libsVersionTomlFiles: List<PsiFile> = project.findPsiFilesByName("libs.versions.toml", searchScope)
25+
val libsVersionTomlFiles: List<PsiFile> = project.findPsiFilesByExtension("versions.toml", searchScope)
2626
val usages = mutableListOf<MigrationItemUsageInfo>()
2727
for (file in libsVersionTomlFiles) {
2828
if (file !is TomlFile) continue

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/studio/sandbox/OpenInSandboxAction.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class OpenInSandboxAction : AnAction(
1919
) {
2020

2121
companion object {
22-
val ACTION_ID = OpenInSandboxAction::class.java.simpleName
22+
val ACTION_ID: String = OpenInSandboxAction::class.java.simpleName
2323
}
2424

2525
override fun update(e: AnActionEvent) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ fun Project.findPsiFilesByName(fileName: String, searchScope: GlobalSearchScope)
1414
return PsiUtilCore.toPsiFiles(PsiManager.getInstance(this), virtualFiles)
1515
}
1616

17+
fun Project.findPsiFilesByExtension(extension: String, searchScope: GlobalSearchScope): List<PsiFile> {
18+
val virtualFiles = FilenameIndex.getAllFilesByExt(this, extension, searchScope)
19+
return PsiUtilCore.toPsiFiles(PsiManager.getInstance(this), virtualFiles)
20+
}
21+
1722
fun VirtualFile.isGenerated(project: Project): Boolean {
1823
return GeneratedSourcesFilter.isGeneratedSourceByAnyFilter(this, project) || isApolloGenerated() || name.endsWith(".keystream")
1924
}

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

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -81,48 +81,63 @@
8181

8282
<!-- Fields insights inspection ("expensive field") -->
8383
<!--suppress PluginXmlCapitalization -->
84-
<localInspection language="GraphQL"
85-
implementationClass="com.apollographql.ijplugin.studio.fieldinsights.ApolloFieldInsightsInspection"
86-
groupPathKey="inspection.group.graphql"
87-
groupKey="inspection.group.graphql.studio"
88-
key="inspection.fieldInsights.displayName"
89-
enabledByDefault="true"
90-
level="WEAK WARNING"
84+
<localInspection
85+
language="GraphQL"
86+
implementationClass="com.apollographql.ijplugin.studio.fieldinsights.ApolloFieldInsightsInspection"
87+
groupPathKey="inspection.group.graphql"
88+
groupKey="inspection.group.graphql.studio"
89+
key="inspection.fieldInsights.displayName"
90+
enabledByDefault="true"
91+
level="WEAK WARNING"
9192
/>
9293

9394
<!-- "Schema in .graphql file" inspection -->
9495
<!--suppress PluginXmlCapitalization -->
95-
<localInspection language="GraphQL"
96-
implementationClass="com.apollographql.ijplugin.inspection.ApolloSchemaInGraphqlFileInspection"
97-
groupPathKey="inspection.group.graphql"
98-
groupKey="inspection.group.graphql.apolloKotlin"
99-
key="inspection.schemaInGraphqlFile.displayName"
100-
enabledByDefault="true"
101-
level="WARNING"
96+
<localInspection
97+
language="GraphQL"
98+
implementationClass="com.apollographql.ijplugin.inspection.ApolloSchemaInGraphqlFileInspection"
99+
groupPathKey="inspection.group.graphql"
100+
groupKey="inspection.group.graphql.apolloKotlin"
101+
key="inspection.schemaInGraphqlFile.displayName"
102+
enabledByDefault="true"
103+
level="WARNING"
104+
/>
105+
106+
<!-- "Apollo Kotlin 4 is available" inspection -->
107+
<!--suppress PluginXmlCapitalization -->
108+
<localInspection
109+
implementationClass="com.apollographql.ijplugin.inspection.Apollo4AvailableInspection"
110+
groupPathKey="inspection.group.graphql"
111+
groupKey="inspection.group.graphql.apolloKotlin"
112+
key="inspection.apollo4Available.displayName"
113+
enabledByDefault="true"
114+
level="WARNING"
102115
/>
103116

104117
<!-- Unused operation inspection -->
105118
<!--suppress PluginXmlCapitalization -->
106-
<localInspection language="GraphQL"
107-
implementationClass="com.apollographql.ijplugin.inspection.ApolloUnusedOperationInspection"
108-
groupPathKey="inspection.group.graphql"
109-
groupKey="inspection.group.graphql.apolloKotlin"
110-
key="inspection.unusedOperation.displayName"
111-
enabledByDefault="true"
112-
level="WARNING"
113-
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
119+
<localInspection
120+
language="GraphQL"
121+
implementationClass="com.apollographql.ijplugin.inspection.ApolloUnusedOperationInspection"
122+
groupPathKey="inspection.group.graphql"
123+
groupKey="inspection.group.graphql.apolloKotlin"
124+
key="inspection.unusedOperation.displayName"
125+
enabledByDefault="true"
126+
level="WARNING"
127+
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
114128
/>
115129

116130
<!-- Unused field inspection -->
117131
<!--suppress PluginXmlCapitalization -->
118-
<localInspection language="GraphQL"
119-
implementationClass="com.apollographql.ijplugin.inspection.ApolloUnusedFieldInspection"
120-
groupPathKey="inspection.group.graphql"
121-
groupKey="inspection.group.graphql.apolloKotlin"
122-
key="inspection.unusedField.displayName"
123-
enabledByDefault="true"
124-
level="WARNING"
125-
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
132+
<localInspection
133+
language="GraphQL"
134+
implementationClass="com.apollographql.ijplugin.inspection.ApolloUnusedFieldInspection"
135+
groupPathKey="inspection.group.graphql"
136+
groupKey="inspection.group.graphql.apolloKotlin"
137+
key="inspection.unusedField.displayName"
138+
enabledByDefault="true"
139+
level="WARNING"
140+
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
126141
/>
127142

128143
<!-- Fields insights service (fetch and cache data) -->
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
Suggests to migrate to Apollo Kotlin 4 on projects using an older version.
4+
</body>
5+
</html>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,6 @@ inspection.unusedOperation.quickFix=Delete operation
106106
inspection.unusedField.displayName=Unused field
107107
inspection.unusedField.reportText=Unused field
108108
inspection.unusedField.quickFix=Delete field
109+
inspection.apollo4Available.displayName=Apollo Kotlin 4 is available
110+
inspection.apollo4Available.reportText=Apollo Kotlin 4 is available
111+
inspection.apollo4Available.quickFix=Migrate to Apollo Kotlin 4

0 commit comments

Comments
 (0)