Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IJ/AS plugin] Add options to ignore fields when reporting unused fields #5197

Merged
merged 4 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
}
Loading