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 plugin] Add navigation to input types / fields #4968

Merged
merged 1 commit into from
May 22, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.KtNameReferenceExpression

/**
* Allows to navigate to the corresponding GraphQL definition when middle-clicking/cmd-clicking/cmd-b on an Apollo operation/fragment/enum
* reference, or model field.
* Allows to navigate to the corresponding GraphQL definition when middle-clicking/cmd-clicking/cmd-b on an Apollo element:
* - operation/fragment class
* - model field
* - enum class / value
* - input class / field
*/
class GraphQLGotoDeclarationHandler : GotoDeclarationHandler {
override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor?): Array<PsiElement>? {
Expand All @@ -37,6 +40,14 @@ class GraphQLGotoDeclarationHandler : GotoDeclarationHandler {
findEnumValueGraphQLDefinitions(nameReferenceExpression)
}

nameReferenceExpression.isApolloInputClassReference() -> {
findInputTypeGraphQLDefinitions(sourceElement.project, psiLeaf.text)
}

nameReferenceExpression.isApolloInputField() -> {
findInputFieldGraphQLDefinitions(nameReferenceExpression)
}

else -> return null
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apollographql.ijplugin.navigation

import com.apollographql.ijplugin.util.containingKtFile
import com.apollographql.ijplugin.util.findChildrenOfType
import com.apollographql.ijplugin.util.resolveKtName
import com.intellij.lang.jsgraphql.GraphQLFileType
Expand All @@ -11,13 +12,17 @@ import com.intellij.lang.jsgraphql.psi.GraphQLField
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentSpread
import com.intellij.lang.jsgraphql.psi.GraphQLInlineFragment
import com.intellij.lang.jsgraphql.psi.GraphQLInputObjectTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLInputValueDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLOperationDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLSelectionSet
import com.intellij.lang.jsgraphql.psi.GraphQLTypeNameDefinition
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.descendantsOfType
import com.intellij.util.castSafelyTo
import org.jetbrains.kotlin.idea.base.utils.fqname.fqName
import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName
Expand Down Expand Up @@ -47,15 +52,25 @@ fun KtNameReferenceExpression.isApolloOperationOrFragmentReference(): Boolean {
val resolvedElement = resolveKtName()
return (resolvedElement.castSafelyTo<KtClass>()
?: resolvedElement.castSafelyTo<KtConstructor<*>>()?.containingClass())
?.isOperationOrFragment() == true
?.isApolloOperationOrFragment() == true
}

fun KtNameReferenceExpression.isApolloModelField(): Boolean {
val resolved = resolveKtName()
// Parameter is for data classes, property is for interfaces
return (resolved is KtParameter || resolved is KtProperty) &&
(resolved as KtElement).topMostContainingClass()
?.isOperationOrFragment() == true
?.isApolloOperationOrFragment() == true
}

private fun KtClass.isApolloOperationOrFragment(): Boolean {
return superTypeListEntries.any {
val superType = it.typeAsUserType?.referenceExpression?.resolveKtName()?.getKotlinFqName()
superType == APOLLO_FRAGMENT_TYPE || superType in APOLLO_OPERATION_TYPES
} ||
// Fallback for fragments in responseBased codegen: they are interfaces generated in a .fragment package.
// This can lead to false positives, but consequences are not dire.
isInterface() && getKotlinFqName()?.parent()?.shortName()?.asString() == "fragment" && hasGeneratedByApolloComment()
}

fun KtNameReferenceExpression.isApolloEnumClassReference(): Boolean {
Expand All @@ -65,29 +80,44 @@ fun KtNameReferenceExpression.isApolloEnumClassReference(): Boolean {

private fun KtClass.isApolloEnumClass() = isEnum() &&
// Apollo enums have a companion object that has a property named "type" of type EnumType
companionObjects.any { companion ->
companion.declarations.filterIsInstance<KtProperty>().any { property ->
property.name == "type" &&
property.type()?.fqName == APOLLO_ENUM_TYPE
}
}
isEnum() && companionObjects.any { companion ->
companion.declarations.filterIsInstance<KtProperty>().any { property ->
property.name == "type" &&
property.type()?.fqName == APOLLO_ENUM_TYPE
}
}

fun KtNameReferenceExpression.isApolloEnumValueReference(): Boolean {
val ktEnumEntry = resolveKtName() as? KtEnumEntry ?: return false
return ktEnumEntry.containingClass()?.isApolloEnumClass() == true
}

fun KtNameReferenceExpression.isApolloInputClassReference(): Boolean {
val resolvedElement = resolveKtName()
return (resolvedElement.castSafelyTo<KtClass>()
?: resolvedElement.castSafelyTo<KtConstructor<*>>()?.containingClass())
?.isApolloInputClass() == true
}

private fun KtClass.isOperationOrFragment(): Boolean {
return superTypeListEntries.any {
val superType = it.typeAsUserType?.referenceExpression?.resolveKtName()?.getKotlinFqName()
superType == APOLLO_FRAGMENT_TYPE || superType in APOLLO_OPERATION_TYPES
} ||
// Fallback for fragments in responseBased codegen: they are interfaces generated in a .fragment package.
// This can lead to false positives, but consequences are not dire.
isInterface() && getKotlinFqName()?.parent()?.shortName()?.asString() == "fragment"
private fun KtClass.isApolloInputClass(): Boolean {
// Apollo input classes are data classes, generated in a package named "type", and we also look at the header comment.
// This can lead to false positives, but consequences are not dire.
return isData() &&
getKotlinFqName()?.parent()?.shortName()?.asString() == "type" &&
hasGeneratedByApolloComment()
}

fun KtNameReferenceExpression.isApolloInputField(): Boolean {
val resolved = resolveKtName()
// Parameter is for data classes, property is for interfaces
return (resolved is KtParameter || resolved is KtProperty) &&
(resolved as KtElement).topMostContainingClass()
?.isApolloInputClass() == true
}

private fun KtElement.hasGeneratedByApolloComment() =
containingKtFile()?.descendantsOfType<PsiComment>()?.any { it.text.contains("generated by Apollo GraphQL") } == true

private fun KtElement.topMostContainingClass(): KtClass? {
return if (containingClass() == null) {
this as? KtClass
Expand Down Expand Up @@ -142,6 +172,25 @@ fun findEnumValueGraphQLDefinitions(nameReferenceExpression: KtNameReferenceExpr
}
}

fun findInputTypeGraphQLDefinitions(project: Project, name: String): List<GraphQLTypeNameDefinition> {
return findGraphQLDefinitions(project) {
it is GraphQLInputObjectTypeDefinition && it.typeNameDefinition?.name == name
}
.mapNotNull { (it as GraphQLInputObjectTypeDefinition).typeNameDefinition }
}

fun findInputFieldGraphQLDefinitions(nameReferenceExpression: KtNameReferenceExpression): List<GraphQLInputValueDefinition> {
val project = nameReferenceExpression.project
val resolved = nameReferenceExpression.resolveKtName()
val ktElement = if (resolved is KtParameter || resolved is KtProperty) resolved as KtElement else return emptyList()
val inputClassName = ktElement.containingClass()?.name ?: return emptyList()
return findInputTypeGraphQLDefinitions(project, inputClassName).flatMap {
val inputObjectTypeDefinition = it.parent as GraphQLInputObjectTypeDefinition
inputObjectTypeDefinition.inputObjectValueDefinitions?.inputValueDefinitionList?.filter {
it.name == ktElement.name
} ?: emptyList()
}
}

fun findGraphQLElements(nameReferenceExpression: KtNameReferenceExpression): List<GraphQLElement> {
val elements = mutableListOf<GraphQLElement>()
Expand Down
2 changes: 1 addition & 1 deletion intellij-plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
implementationClass="com.apollographql.ijplugin.navigation.GraphQLDefinitionMarkerProvider"
/>

<!-- Operation/fragment/field/enum 'go to declaration' handler -->
<!-- Operation/fragment/field/enum/input 'go to declaration' handler -->
<gotoDeclarationHandler implementation="com.apollographql.ijplugin.navigation.GraphQLGotoDeclarationHandler" />

<!-- Operation/fragment/field/enum 'go to type declaration' handler -->
Expand Down