From b520242231b9c87449f8c5857854be7d339edf1d Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 23 Nov 2023 19:59:59 +0100 Subject: [PATCH] Add ApolloOneOfInputCreationInspection --- .../ApolloOneOfInputCreationInspection.kt | 44 +++++++++++++++++++ .../com/apollographql/ijplugin/util/Psi.kt | 8 +++- .../src/main/resources/META-INF/plugin.xml | 12 +++++ .../ApolloOneOfInputCreation.html | 8 ++++ .../messages/ApolloBundle.properties | 4 ++ .../ApolloOneOfInputCreationInspectionTest.kt | 24 ++++++++++ .../com/apollographql/apollo3/api/Optional.kt | 8 ++-- .../src/main/graphql/FindUserQuery.graphql | 5 +++ .../src/main/graphql/schema.graphqls | 26 +++++++++++ .../src/main/kotlin/com/example/OneOf.kt | 23 ++++++++++ 10 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspection.kt create mode 100644 intellij-plugin/src/main/resources/inspectionDescriptions/ApolloOneOfInputCreation.html create mode 100644 intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspectionTest.kt create mode 100644 tests/intellij-plugin-test-project/src/main/graphql/FindUserQuery.graphql create mode 100644 tests/intellij-plugin-test-project/src/main/kotlin/com/example/OneOf.kt diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspection.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspection.kt new file mode 100644 index 00000000000..cc91e213025 --- /dev/null +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspection.kt @@ -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()) + if (reference?.isApolloInputClassReference() != true) return + val inputTypeName = reference.text + val inputTypeDefinition = findInputTypeGraphQLDefinitions(reference.project, inputTypeName).firstOrNull() + ?.parentOfType() + ?: 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")) + } + } + } + } +} diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt index 3ef8aede51f..c7d9d76fa53 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt @@ -4,7 +4,9 @@ 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 @@ -12,6 +14,7 @@ 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 @@ -19,6 +22,7 @@ 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() @@ -72,6 +76,8 @@ fun KtCallExpression.getMethodName(): String? = calleeExpression.cast()?.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() diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index 8d16757923a..20044da41be 100644 --- a/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -136,6 +136,18 @@ level="INFO" /> + + + + + +Reports invalid constructor invocations of @oneOf input types. +

+ Exactly one field must be set, and it must be Present. +

+ + diff --git a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties index e7704028350..dd3506e0a88 100644 --- a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties +++ b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties @@ -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=@oneOf input must have exactly one field set +inspection.oneOfInputCreation.reportText.argIsAbsent=@oneOf input argument must be Present + inspection.suppress.field=Suppress for field notification.group.apollo.main=Apollo diff --git a/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspectionTest.kt b/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspectionTest.kt new file mode 100644 index 00000000000..4b9a9565e61 --- /dev/null +++ b/intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspectionTest.kt @@ -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}) + } +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Optional.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Optional.kt index 59995a5c5b1..71bd364413a 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Optional.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Optional.kt @@ -33,10 +33,10 @@ sealed class Optional { companion object { @JvmStatic - fun absent(): Optional = Absent + fun absent(): Absent = Absent @JvmStatic - fun present(value: V): Optional = Present(value) + fun present(value: V): Present = Present(value) @JvmStatic fun presentIfNotNull(value: V?): Optional = if (value == null) Absent else Present(value) @@ -51,7 +51,7 @@ fun Optional.getOrElse(fallback: V): V = (this as? Present)?.value ?: fal @ApolloExperimental fun Optional.map(mapper: (V) -> R): Optional { return when(this) { - is Optional.Absent -> Optional.Absent - is Optional.Present -> Optional.present(mapper(value)) + is Absent -> Absent + is Present -> Optional.present(mapper(value)) } } diff --git a/tests/intellij-plugin-test-project/src/main/graphql/FindUserQuery.graphql b/tests/intellij-plugin-test-project/src/main/graphql/FindUserQuery.graphql new file mode 100644 index 00000000000..bef701617ff --- /dev/null +++ b/tests/intellij-plugin-test-project/src/main/graphql/FindUserQuery.graphql @@ -0,0 +1,5 @@ +query FindUsersQuery($findUserInput: FindUserInput!) { + findUser(findUserInput: $findUserInput) { + id + } +} diff --git a/tests/intellij-plugin-test-project/src/main/graphql/schema.graphqls b/tests/intellij-plugin-test-project/src/main/graphql/schema.graphqls index 50198ab8c8a..3324b536bf2 100644 --- a/tests/intellij-plugin-test-project/src/main/graphql/schema.graphqls +++ b/tests/intellij-plugin-test-project/src/main/graphql/schema.graphqls @@ -2,6 +2,7 @@ type Query { animals: [Animal!]! computers: [Computer!]! myEnum(inputEnum: myEnum): myEnum + findUser(findUserInput: FindUserInput!): User } type Mutation { @@ -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! +} diff --git a/tests/intellij-plugin-test-project/src/main/kotlin/com/example/OneOf.kt b/tests/intellij-plugin-test-project/src/main/kotlin/com/example/OneOf.kt new file mode 100644 index 00000000000..5d085f79c3e --- /dev/null +++ b/tests/intellij-plugin-test-project/src/main/kotlin/com/example/OneOf.kt @@ -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("a@a.com"), + name = Optional.present("John"), + ) + + FindUserInput( + email = Optional.present("a@a.com") + ) + + val absentEmail: Absent = Optional.absent() + FindUserInput( + email = absentEmail + ) +}