Skip to content

Commit

Permalink
[IJ/AS plugin] Add options to ignore fields when reporting unused fie…
Browse files Browse the repository at this point in the history
…lds (#5197)
  • Loading branch information
BoD authored Aug 21, 2023
1 parent 0386e42 commit eeba31e
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String> = mutableListOf()

override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
var isUnusedOperation = false
return object : GraphQLVisitor() {
Expand All @@ -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<String> = 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) {
Expand All @@ -64,9 +91,36 @@ class ApolloUnusedFieldInspection : LocalInspectionTool() {
holder.registerProblem(
if (isFragment) o.findParentOfType<GraphQLSelection>()!! else o,
ApolloBundle.message("inspection.unusedField.reportText"),
DeleteElementQuickFix("inspection.unusedField.quickFix") { it.findParentOfType<GraphQLSelection>(strict = false)!! },
*buildList {
add(DeleteElementQuickFix("inspection.unusedField.quickFix.deleteField") { it.findParentOfType<GraphQLSelection>(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
}
}
Original file line number Diff line number Diff line change
@@ -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<String> {
val fieldDefinition = fieldIdentifier.reference?.resolve()?.parent as? GraphQLFieldDefinition ?: return emptySet()
val namedTypeDefinition = fieldDefinition.parentOfType<GraphQLNamedTypeDefinition>() ?: return emptySet()
return matchingFieldCoordinates(fieldDefinition, namedTypeDefinition)
}

private fun matchingFieldCoordinates(
fieldDefinition: GraphQLFieldDefinition,
namedTypeDefinition: GraphQLNamedTypeDefinition,
): Set<String> {
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<GraphQLInterfaceTypeDefinition>() }
if (implementedInterfaceTypeDefinitions.isEmpty()) return fieldCoordinates
return fieldCoordinates + implementedInterfaceTypeDefinitions.flatMap { matchingFieldCoordinates(fieldDefinition, it) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,14 @@
<p>
This is also known as over-fetching.
</p>
<p>
Fields can be ignored by adding their coordinates of the form <code>TypeName.fieldName</code> to the table in the options below. These
are used as regexes. For example,
<ul>
<li><code>User\.name</code> ignores the <code>name</code> field of the <code>User</code> type</li>
<li><code>User\..+</code> ignores all fields of the <code>User</code> type</li>
<li><code>.+\.id</code> ignores all fields named <code>id</code></li>
</ul>
</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")

Expand All @@ -44,6 +48,7 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() {
# noinspection ApolloUnusedField
barkVolume
fieldOnDogAndCat
name
}
}
}
Expand Down Expand Up @@ -78,6 +83,7 @@ class ApolloUnusedFieldInspectionTest : ApolloTestCase() {
# noinspection ApolloUnusedField
barkVolume
fieldOnDogAndCat
name
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ query animals {
# noinspection ApolloUnusedField
barkVolume
fieldOnDogAndCat
name
}
}
}

0 comments on commit eeba31e

Please sign in to comment.