diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspection.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspection.kt index 6aea30aa8a2..31506972ce8 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspection.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Apollo4AvailableInspection.kt @@ -152,7 +152,7 @@ object Apollo4AvailableQuickFix : LocalQuickFix { override fun availableInBatchMode() = false - override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor) = IntentionPreviewInfo.EMPTY + override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val action = ActionManager.getInstance().getAction(ApolloV3ToV4MigrationAction.ACTION_ID) diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloSchemaInGraphqlFileInspection.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloSchemaInGraphqlFileInspection.kt index fc6e7ad8dc6..9f0dcea22da 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloSchemaInGraphqlFileInspection.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloSchemaInGraphqlFileInspection.kt @@ -45,7 +45,7 @@ class ApolloSchemaInGraphqlFileInspection : LocalInspectionTool() { } override fun getFamilyName() = name - override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor) = IntentionPreviewInfo.EMPTY + override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val psiFile = descriptor.psiElement.containingFile diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspection.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspection.kt index 9e000e1df6a..2390a38ff03 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspection.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspection.kt @@ -7,8 +7,13 @@ import com.apollographql.ijplugin.navigation.findKotlinFragmentSpreadDefinitions import com.apollographql.ijplugin.navigation.findKotlinInlineFragmentDefinitions import com.apollographql.ijplugin.project.apolloProjectService import com.apollographql.ijplugin.util.isProcessCanceled +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.ui.InspectionOptionsPanel +import com.intellij.codeInspection.ui.ListEditForm import com.intellij.lang.jsgraphql.psi.GraphQLField import com.intellij.lang.jsgraphql.psi.GraphQLFragmentSpread import com.intellij.lang.jsgraphql.psi.GraphQLIdentifier @@ -17,10 +22,16 @@ import com.intellij.lang.jsgraphql.psi.GraphQLSelection import com.intellij.lang.jsgraphql.psi.GraphQLTypeName import com.intellij.lang.jsgraphql.psi.GraphQLTypedOperationDefinition import com.intellij.lang.jsgraphql.psi.GraphQLVisitor +import com.intellij.openapi.project.Project +import com.intellij.profile.codeInspection.ProjectInspectionProfileManager import com.intellij.psi.PsiElementVisitor import com.intellij.psi.util.findParentOfType +import javax.swing.JComponent class ApolloUnusedFieldInspection : LocalInspectionTool() { + @JvmField + var fieldsToIgnore: MutableList = mutableListOf() + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { var isUnusedOperation = false return object : GraphQLVisitor() { @@ -36,20 +47,36 @@ class ApolloUnusedFieldInspection : LocalInspectionTool() { } var isFragment = false - val ktDefinitions = when (val parent = o.parent) { + val parent = o.parent + val ktDefinitions = when (parent) { is GraphQLField -> findKotlinFieldDefinitions(parent) is GraphQLFragmentSpread -> { isFragment = true findKotlinFragmentSpreadDefinitions(parent) } + is GraphQLTypeName -> { val inlineFragment = parent.parent?.parent as? GraphQLInlineFragment ?: return isFragment = true findKotlinInlineFragmentDefinitions(inlineFragment) } + else -> return }.ifEmpty { return } + val matchingFieldCoordinates: Collection = if (parent is GraphQLField) { + matchingFieldCoordinates(o) + } else { + emptySet() + } + val shouldIgnoreField = fieldsToIgnore.any { fieldToIgnore -> + matchingFieldCoordinates.any match@{ fieldCoordinates -> + val regex = runCatching { Regex(fieldToIgnore) }.getOrNull() ?: return@match false + fieldCoordinates.matches(regex) + } + } + if (shouldIgnoreField) return + val kotlinFindUsagesHandlerFactory = KotlinFindUsagesHandlerFactoryCompat(o.project) val hasUsageProcessor = HasUsageProcessor() for (kotlinDefinition in ktDefinitions) { @@ -64,9 +91,36 @@ class ApolloUnusedFieldInspection : LocalInspectionTool() { holder.registerProblem( if (isFragment) o.findParentOfType()!! else o, ApolloBundle.message("inspection.unusedField.reportText"), - DeleteElementQuickFix("inspection.unusedField.quickFix") { it.findParentOfType(strict = false)!! }, + *buildList { + add(DeleteElementQuickFix("inspection.unusedField.quickFix.deleteField") { it.findParentOfType(strict = false)!! }) + for (matchingFieldCoordinate in matchingFieldCoordinates) { + add(IgnoreFieldQuickFix(matchingFieldCoordinate)) + } + }.toTypedArray() ) } } } + + // In a future version of the platform we should use `AddToInspectionOptionListFix` instead. + private inner class IgnoreFieldQuickFix(private val fieldCoordinates: String) : LocalQuickFix { + override fun getName() = ApolloBundle.message("inspection.unusedField.quickFix.ignoreField", fieldCoordinates) + override fun getFamilyName() = name + + override fun availableInBatchMode() = false + override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + fieldsToIgnore += fieldCoordinates.replace(".", "\\.") + + // Save the inspection settings + ProjectInspectionProfileManager.getInstance(project).fireProfileChanged() + } + } + + override fun createOptionsPanel(): JComponent { + val form = ListEditForm(ApolloBundle.message("inspection.unusedField.options.fieldsToIgnore.title"), ApolloBundle.message("inspection.unusedField.options.fieldsToIgnore.label"), fieldsToIgnore) + form.contentPanel.minimumSize = InspectionOptionsPanel.getMinimumListSize() + return form.contentPanel + } } diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt new file mode 100644 index 00000000000..e38268c1eb9 --- /dev/null +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt @@ -0,0 +1,68 @@ +package com.apollographql.ijplugin.inspection + +import com.intellij.lang.jsgraphql.psi.GraphQLFieldDefinition +import com.intellij.lang.jsgraphql.psi.GraphQLIdentifier +import com.intellij.lang.jsgraphql.psi.GraphQLInterfaceTypeDefinition +import com.intellij.lang.jsgraphql.psi.GraphQLNamedTypeDefinition +import com.intellij.lang.jsgraphql.psi.GraphQLObjectTypeDefinition +import com.intellij.psi.util.parentOfType + +/** + * Given a field identifier, returns the set of field coordinates that match it. + * A field coordinate is a string of the form "TypeName.fieldName". + * Interfaces are taken into account which is why multiple coordinates can be returned. + * + * For example, given the following schema: + * ```graphql + * interface Node { + * id: ID! + * } + * + * interface HasId { + * id: ID! + * } + * + * type User implements Node & HasId { + * id: ID! + * name: String! + * } + * ``` + * + * And the following query: + * ```graphql + * query { + * user { + * id + * name + * } + * } + * ``` + * + * The following coordinates will be returned for `id`: + * - `User.id` + * - `Node.id` + * - `HasId.id` + */ +fun matchingFieldCoordinates(fieldIdentifier: GraphQLIdentifier): Set { + val fieldDefinition = fieldIdentifier.reference?.resolve()?.parent as? GraphQLFieldDefinition ?: return emptySet() + val namedTypeDefinition = fieldDefinition.parentOfType() ?: return emptySet() + return matchingFieldCoordinates(fieldDefinition, namedTypeDefinition) +} + +private fun matchingFieldCoordinates( + fieldDefinition: GraphQLFieldDefinition, + namedTypeDefinition: GraphQLNamedTypeDefinition, +): Set { + val typeName = namedTypeDefinition.typeNameDefinition?.name ?: return emptySet() + + val fieldsDefinitions = (namedTypeDefinition as? GraphQLObjectTypeDefinition)?.fieldsDefinition + ?: (namedTypeDefinition as? GraphQLInterfaceTypeDefinition)?.fieldsDefinition ?: return emptySet() + if (fieldsDefinitions.fieldDefinitionList.none { it.name == fieldDefinition.name }) return emptySet() + + val fieldCoordinates = mutableSetOf(typeName + "." + fieldDefinition.name) + val implementedInterfaces = (namedTypeDefinition as? GraphQLObjectTypeDefinition)?.implementsInterfaces + ?: (namedTypeDefinition as? GraphQLInterfaceTypeDefinition)?.implementsInterfaces ?: return fieldCoordinates + val implementedInterfaceTypeDefinitions = implementedInterfaces.typeNameList.mapNotNull { it.nameIdentifier.reference?.resolve()?.parentOfType() } + if (implementedInterfaceTypeDefinitions.isEmpty()) return fieldCoordinates + return fieldCoordinates + implementedInterfaceTypeDefinitions.flatMap { matchingFieldCoordinates(fieldDefinition, it) } +} diff --git a/intellij-plugin/src/main/resources/inspectionDescriptions/ApolloUnusedField.html b/intellij-plugin/src/main/resources/inspectionDescriptions/ApolloUnusedField.html index 6488fb2f3b1..11304d6d13e 100644 --- a/intellij-plugin/src/main/resources/inspectionDescriptions/ApolloUnusedField.html +++ b/intellij-plugin/src/main/resources/inspectionDescriptions/ApolloUnusedField.html @@ -4,5 +4,14 @@

This is also known as over-fetching.

+

+ Fields can be ignored by adding their coordinates of the form TypeName.fieldName to the table in the options below. These + are used as regexes. For example, +

    +
  • User\.name ignores the name field of the User type
  • +
  • User\..+ ignores all fields of the User type
  • +
  • .+\.id ignores all fields named id
  • +
+

diff --git a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties index 5bac6eb0b3b..4438a90f14a 100644 --- a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties +++ b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties @@ -117,7 +117,11 @@ inspection.unusedOperation.reportText=Unused operation inspection.unusedOperation.quickFix=Delete operation inspection.unusedField.displayName=Unused field inspection.unusedField.reportText=Unused field -inspection.unusedField.quickFix=Delete field +inspection.unusedField.quickFix.deleteField=Delete field +inspection.unusedField.quickFix.ignoreField=Ignore field {0} +inspection.unusedField.options.fieldsToIgnore.title=Ignored Fields +inspection.unusedField.options.fieldsToIgnore.label=Field coordinates to ignore (regexes): + inspection.apollo4Available.displayName=Apollo Kotlin 4 is available inspection.apollo4Available.reportText=Apollo Kotlin 4 is available inspection.apollo4Available.quickFix=Migrate to Apollo Kotlin 4 diff --git a/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspectionTest.kt b/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspectionTest.kt index b9976151e17..a9db7e91a0a 100644 --- a/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspectionTest.kt +++ b/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloUnusedFieldInspectionTest.kt @@ -10,7 +10,9 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() { @Throws(Exception::class) override fun setUp() { super.setUp() - myFixture.enableInspections(ApolloUnusedFieldInspection()) + myFixture.enableInspections(ApolloUnusedFieldInspection().apply { + fieldsToIgnore = mutableListOf("dog\\.name") + }) } @Test @@ -20,11 +22,13 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() { // id is unused assertTrue(highlightInfos.any { it.description == "Unused field" && it.text == "id" && it.line == 3}) // name is used - assertTrue(highlightInfos.none { it.description == "Unused field" && it.text == "name" }) + assertTrue(highlightInfos.none { it.description == "Unused field" && it.text == "name" && it.line == 4 }) // id inside the fragment is used assertTrue(highlightInfos.none { it.description == "Unused field" && it.text == "id" && it.line > 3 }) // barkVolume is unused, but the inspection is suppressed assertTrue(highlightInfos.none { it.description == "Unused field" && it.text == "barkVolume"}) + // name is unused, but the field is ignored + assertTrue(highlightInfos.none { it.description == "Unused field" && it.text == "name" && it.line == 14}) moveCaret("id") @@ -44,6 +48,7 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() { # noinspection ApolloUnusedField barkVolume fieldOnDogAndCat + name } } } @@ -78,6 +83,7 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() { # noinspection ApolloUnusedField barkVolume fieldOnDogAndCat + name } } } diff --git a/tests/intellij-plugin-test-project/src/main/graphql/AnimalsQuery.graphql b/tests/intellij-plugin-test-project/src/main/graphql/AnimalsQuery.graphql index 05b6676b09c..ec25e07015a 100644 --- a/tests/intellij-plugin-test-project/src/main/graphql/AnimalsQuery.graphql +++ b/tests/intellij-plugin-test-project/src/main/graphql/AnimalsQuery.graphql @@ -11,6 +11,7 @@ query animals { # noinspection ApolloUnusedField barkVolume fieldOnDogAndCat + name } } }