Skip to content

Commit

Permalink
Add ApolloOneOfInputCreationInspection
Browse files Browse the repository at this point in the history
  • Loading branch information
BoD committed Nov 23, 2023
1 parent f5efb16 commit b520242
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.apollographql.ijplugin.inspection

import com.apollographql.ijplugin.ApolloBundle
import com.apollographql.ijplugin.navigation.findInputTypeGraphQLDefinitions
import com.apollographql.ijplugin.navigation.isApolloInputClassReference
import com.apollographql.ijplugin.project.apolloProjectService
import com.apollographql.ijplugin.util.cast
import com.apollographql.ijplugin.util.type
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.lang.jsgraphql.psi.GraphQLInputObjectTypeDefinition
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.idea.base.utils.fqname.fqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtVisitorVoid

class ApolloOneOfInputCreationInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : KtVisitorVoid() {
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
if (!expression.project.apolloProjectService.apolloVersion.isAtLeastV4) return
val reference = (expression.calleeExpression.cast<KtNameReferenceExpression>())
if (reference?.isApolloInputClassReference() != true) return
val inputTypeName = reference.text
val inputTypeDefinition = findInputTypeGraphQLDefinitions(reference.project, inputTypeName).firstOrNull()
?.parentOfType<GraphQLInputObjectTypeDefinition>()
?: return
val isOneOf = inputTypeDefinition.directives.any { it.name == "oneOf" }
if (!isOneOf) return
if (expression.valueArguments.size != 1) {
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.wrongNumberOfArgs"))
return
}
val arg = expression.valueArguments.first()
if (arg.getArgumentExpression()?.type()?.fqName?.asString() == "com.apollographql.apollo3.api.Optional.Absent") {
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.argIsAbsent"))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.caches.resolve.safeAnalyze
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtConstructor
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportList
import org.jetbrains.kotlin.psi.KtLambdaArgument
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull

fun PsiElement.containingKtFile(): KtFile? = getStrictParentOfType()
Expand Down Expand Up @@ -72,6 +76,8 @@ fun KtCallExpression.getMethodName(): String? = calleeExpression.cast<KtNameRefe

fun KtCallExpression.lambdaBlockExpression(): KtBlockExpression? = valueArguments.firstIsInstanceOrNull<KtLambdaArgument>()?.getLambdaExpression()?.bodyExpression

fun KtDeclaration.type() = (resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType
fun KtDeclaration.type(): KotlinType? = (resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType

fun KtExpression.type(): KotlinType? = safeAnalyze(getResolutionFacade()).getType(this)

fun KtReferenceExpression.resolve() = mainReference.resolve()
12 changes: 12 additions & 0 deletions intellij-plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@
level="INFO"
/>

<!-- "OneOf Input creation" inspection -->
<!--suppress PluginXmlCapitalization -->
<localInspection
language="kotlin"
implementationClass="com.apollographql.ijplugin.inspection.ApolloOneOfInputCreationInspection"
groupPathKey="inspection.group.graphql"
groupKey="inspection.group.graphql.apolloKotlin"
key="inspection.oneOfInputCreation.displayName"
enabledByDefault="true"
level="ERROR"
/>

<!-- Suppression of inspections on individual fields -->
<lang.inspectionSuppressor
language="GraphQL"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<body>
Reports invalid constructor invocations of <code>@oneOf</code> input types.
<p>
Exactly one field must be set, and it must be <code>Present</code>.
</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ inspection.endpointNotConfigured.displayName=GraphQL endpoint not configured
inspection.endpointNotConfigured.reportText=GraphQL endpoint not configured
inspection.endpointNotConfigured.quickFix=Add introspection block

inspection.oneOfInputCreation.displayName=OneOf Input Object creation issue
inspection.oneOfInputCreation.reportText.wrongNumberOfArgs=<html><tt>@oneOf</tt> input must have exactly one field set
inspection.oneOfInputCreation.reportText.argIsAbsent=<html><tt>@oneOf</tt> input argument must be <tt>Present</tt>

inspection.suppress.field=Suppress for field

notification.group.apollo.main=Apollo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.apollographql.ijplugin.inspection

import com.apollographql.ijplugin.ApolloTestCase
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class ApolloOneOfInputCreationInspectionTest : ApolloTestCase() {
@Throws(Exception::class)
override fun setUp() {
super.setUp()
myFixture.enableInspections(ApolloOneOfInputCreationInspection())
}

@Test
fun testOneOfConstructorInvocations() {
myFixture.configureFromTempProjectFile("src/main/kotlin/com/example/OneOf.kt")
val highlightInfos = doHighlighting()
assertTrue(highlightInfos.any { it.description == "@oneOf input must have exactly one field set" && it.text == "FindUserInput" && it.line == 8})
assertTrue(highlightInfos.any { it.description == "@oneOf input must have exactly one field set" && it.text == "FindUserInput" && it.line == 10})
assertTrue(highlightInfos.any { it.description == "@oneOf input argument must be Present" && it.text == "FindUserInput" && it.line == 20})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ sealed class Optional<out V> {

companion object {
@JvmStatic
fun <V> absent(): Optional<V> = Absent
fun absent(): Absent = Absent

@JvmStatic
fun <V> present(value: V): Optional<V> = Present(value)
fun <V> present(value: V): Present<V> = Present(value)

@JvmStatic
fun <V : Any> presentIfNotNull(value: V?): Optional<V> = if (value == null) Absent else Present(value)
Expand All @@ -51,7 +51,7 @@ fun <V> Optional<V>.getOrElse(fallback: V): V = (this as? Present)?.value ?: fal
@ApolloExperimental
fun <V, R> Optional<V>.map(mapper: (V) -> R): Optional<R> {
return when(this) {
is Optional.Absent -> Optional.Absent
is Optional.Present -> Optional.present(mapper(value))
is Absent -> Absent
is Present -> Optional.present(mapper(value))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query FindUsersQuery($findUserInput: FindUserInput!) {
findUser(findUserInput: $findUserInput) {
id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type Query {
animals: [Animal!]!
computers: [Computer!]!
myEnum(inputEnum: myEnum): myEnum
findUser(findUserInput: FindUserInput!): User
}

type Mutation {
Expand Down Expand Up @@ -64,3 +65,28 @@ input AddressInput {
num: Int
street: String
}



directive @oneOf on INPUT_OBJECT

type User {
id: ID!
}

input FindUserInput @oneOf {
email: String
name: String
identity: FindUserBySocialNetworkInput
friends: FindUserByFriendInput
}

input FindUserBySocialNetworkInput @oneOf {
facebookId: String
googleId: String
}

input FindUserByFriendInput {
socialNetworkId: ID!
friendId: ID!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example

import com.apollographql.apollo3.api.Optional
import com.apollographql.apollo3.api.Optional.Absent
import com.example.generated.type.FindUserInput

fun oneOf() {
FindUserInput()

FindUserInput(
email = Optional.present("[email protected]"),
name = Optional.present("John"),
)

FindUserInput(
email = Optional.present("[email protected]")
)

val absentEmail: Absent = Optional.absent()
FindUserInput(
email = absentEmail
)
}

0 comments on commit b520242

Please sign in to comment.