Skip to content

Commit 0fc496f

Browse files
authored
[IJ Plugin] Report invalid oneOf input object builder uses (#5416)
1 parent 9e199e8 commit 0fc496f

File tree

8 files changed

+95
-13
lines changed

8 files changed

+95
-13
lines changed

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspection.kt

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,91 @@ package com.apollographql.ijplugin.inspection
22

33
import com.apollographql.ijplugin.ApolloBundle
44
import com.apollographql.ijplugin.navigation.findInputTypeGraphQLDefinitions
5+
import com.apollographql.ijplugin.navigation.isApolloInputClass
56
import com.apollographql.ijplugin.navigation.isApolloInputClassReference
67
import com.apollographql.ijplugin.project.apolloProjectService
8+
import com.apollographql.ijplugin.util.canBeNull
79
import com.apollographql.ijplugin.util.cast
810
import com.apollographql.ijplugin.util.type
911
import com.intellij.codeInspection.LocalInspectionTool
12+
import com.intellij.codeInspection.ProblemHighlightType
1013
import com.intellij.codeInspection.ProblemsHolder
1114
import com.intellij.lang.jsgraphql.psi.GraphQLInputObjectTypeDefinition
1215
import com.intellij.psi.PsiElementVisitor
1316
import com.intellij.psi.util.parentOfType
1417
import org.jetbrains.kotlin.idea.base.utils.fqname.fqName
18+
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isNullExpression
19+
import org.jetbrains.kotlin.idea.references.mainReference
1520
import org.jetbrains.kotlin.psi.KtCallExpression
21+
import org.jetbrains.kotlin.psi.KtClass
22+
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
23+
import org.jetbrains.kotlin.psi.KtExpression
1624
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
1725
import org.jetbrains.kotlin.psi.KtVisitorVoid
26+
import org.jetbrains.kotlin.psi.psiUtil.containingClass
27+
import org.jetbrains.kotlin.resolve.calls.util.getCalleeExpressionIfAny
28+
import org.jetbrains.kotlin.types.typeUtil.isNullableNothing
1829

1930
class ApolloOneOfInputCreationInspection : LocalInspectionTool() {
2031
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
2132
return object : KtVisitorVoid() {
33+
private val expressionsWithFieldSet = mutableSetOf<KtExpression>()
34+
35+
// For constructor calls
2236
override fun visitCallExpression(expression: KtCallExpression) {
2337
super.visitCallExpression(expression)
2438
if (!expression.project.apolloProjectService.apolloVersion.isAtLeastV4) return
2539
val reference = (expression.calleeExpression.cast<KtNameReferenceExpression>())
2640
if (reference?.isApolloInputClassReference() != true) return
2741
val inputTypeName = reference.text
28-
val inputTypeDefinition = findInputTypeGraphQLDefinitions(reference.project, inputTypeName).firstOrNull()
42+
val inputTypeDefinition = findInputTypeGraphQLDefinitions(expression.project, inputTypeName).firstOrNull()
2943
?.parentOfType<GraphQLInputObjectTypeDefinition>()
3044
?: return
3145
val isOneOf = inputTypeDefinition.directives.any { it.name == "oneOf" }
3246
if (!isOneOf) return
3347
if (expression.valueArguments.size != 1) {
34-
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.wrongNumberOfArgs"))
48+
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.constructor.wrongNumberOfArgs"))
3549
return
3650
}
3751
val arg = expression.valueArguments.first()
3852
if (arg.getArgumentExpression()?.type()?.fqName?.asString() == "com.apollographql.apollo3.api.Optional.Absent") {
39-
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.argIsAbsent"))
53+
holder.registerProblem(expression.calleeExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.constructor.argIsAbsent"))
54+
}
55+
}
56+
57+
// For builder calls
58+
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
59+
super.visitDotQualifiedExpression(expression)
60+
if (!expression.project.apolloProjectService.apolloVersion.isAtLeastV4) return
61+
val expressionClass = expression.selectorExpression.getCalleeExpressionIfAny()?.mainReference?.resolve()?.parentOfType<KtClass>(withSelf = true)
62+
?: return
63+
if (expressionClass.name != "Builder") return
64+
val containingClass = expressionClass.containingClass() ?: return
65+
if (!containingClass.isApolloInputClass()) return
66+
val inputTypeName = containingClass.name ?: return
67+
val inputTypeDefinition = findInputTypeGraphQLDefinitions(expression.project, inputTypeName).firstOrNull()
68+
?.parentOfType<GraphQLInputObjectTypeDefinition>()
69+
?: return
70+
val isOneOf = inputTypeDefinition.directives.any { it.name == "oneOf" }
71+
if (!isOneOf) return
72+
val arguments = expression.selectorExpression.cast<KtCallExpression>()?.valueArguments ?: return
73+
// No arguments: we're on the MyInput.Builder() or .build() call
74+
if (arguments.size == 0) return
75+
76+
val argumentExpression = arguments.first().getArgumentExpression()
77+
val argumentExpressionType = argumentExpression?.type()
78+
if (argumentExpression?.isNullExpression() == true || argumentExpressionType?.isNullableNothing() == true) {
79+
// `null`
80+
holder.registerProblem(expression.selectorExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.builder.argIsNull"))
81+
} else if (argumentExpressionType?.canBeNull() == true) {
82+
// a nullable type: warning only
83+
holder.registerProblem(expression.selectorExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.builder.argIsNull"), ProblemHighlightType.WARNING)
84+
}
85+
86+
if (expression.receiverExpression in expressionsWithFieldSet) {
87+
holder.registerProblem(expression.selectorExpression!!, ApolloBundle.message("inspection.oneOfInputCreation.reportText.builder.wrongNumberOfArgs"))
4088
}
89+
expressionsWithFieldSet.add(expression)
4190
}
4291
}
4392
}

intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Psi.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.psi.KtReferenceExpression
2323
import org.jetbrains.kotlin.psi.psiUtil.containingClass
2424
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
2525
import org.jetbrains.kotlin.types.KotlinType
26+
import org.jetbrains.kotlin.types.isNullabilityFlexible
2627
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
2728

2829
fun PsiElement.containingKtFile(): KtFile? = getStrictParentOfType()
@@ -81,3 +82,5 @@ fun KtDeclaration.type(): KotlinType? = (resolveToDescriptorIfAny() as? Callable
8182
fun KtExpression.type(): KotlinType? = safeAnalyze(getResolutionFacade()).getType(this)
8283

8384
fun KtReferenceExpression.resolve() = mainReference.resolve()
85+
86+
fun KotlinType.canBeNull() = isMarkedNullable || isNullabilityFlexible()
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<html>
22
<body>
3-
Reports invalid constructor invocations of <code>@oneOf</code> input types.
3+
Reports invalid creation of <code>@oneOf</code> input types.
44
<p>
5-
Constructor of `@oneOf` input class must have exactly one <code>Present</code> argument.
5+
<ul>
6+
<li>`@oneOf` input class must be created with exactly one <code>Present</code> / non-null argument.
7+
</ul>
68
</p>
79
</body>
810
</html>

intellij-plugin/src/main/resources/messages/ApolloBundle.properties

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,10 @@ inspection.endpointNotConfigured.reportText=GraphQL endpoint not configured
143143
inspection.endpointNotConfigured.quickFix=Add introspection block
144144

145145
inspection.oneOfInputCreation.displayName=OneOf Input Object creation issue
146-
inspection.oneOfInputCreation.reportText.wrongNumberOfArgs=<html><tt>@oneOf</tt> input class constructor must have exactly one argument
147-
inspection.oneOfInputCreation.reportText.argIsAbsent=<html><tt>@oneOf</tt> input class argument must be <tt>Present</tt>
146+
inspection.oneOfInputCreation.reportText.constructor.wrongNumberOfArgs=<html><tt>@oneOf</tt> input class constructor must have exactly one argument
147+
inspection.oneOfInputCreation.reportText.constructor.argIsAbsent=<html><tt>@oneOf</tt> input class argument must be <tt>Present</tt>
148+
inspection.oneOfInputCreation.reportText.builder.wrongNumberOfArgs=<html><tt>@oneOf</tt> input class builder must have exactly one field set
149+
inspection.oneOfInputCreation.reportText.builder.argIsNull=<html><tt>@oneOf</tt> input class argument must be non-null
148150

149151
inspection.suppress.field=Suppress for field
150152

intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloOneOfInputCreationInspectionTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ class ApolloOneOfInputCreationInspectionTest : ApolloTestCase() {
2020
assertTrue(highlightInfos.any { it.description == "@oneOf input class constructor must have exactly one argument" && it.text == "FindUserInput" && it.line == 8 })
2121
assertTrue(highlightInfos.any { it.description == "@oneOf input class constructor must have exactly one argument" && it.text == "FindUserInput" && it.line == 10 })
2222
assertTrue(highlightInfos.any { it.description == "@oneOf input class argument must be Present" && it.text == "FindUserInput" && it.line == 20 })
23+
assertTrue(highlightInfos.any { it.description == "@oneOf input class builder must have exactly one field set" && it.text == "name(\"John\")" && it.line == 28 })
24+
assertTrue(highlightInfos.any { it.description == "@oneOf input class argument must be non-null" && it.text == "email(null)" && it.line == 36 })
25+
assertTrue(highlightInfos.any { it.description == "@oneOf input class argument must be non-null" && it.text == "email(someNullableEmail)" && it.line == 41 && it.type.getSeverity(null).name == "WARNING" })
2326
}
2427
}

intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/navigation/GraphQLGotoDeclarationHandlerTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ class GraphQLGotoDeclarationHandlerTest : ApolloTestCase() {
9999
fromFile = "src/main/graphql/schema.graphqls",
100100
fromElement = { elementAt<PsiElement>("lastName", afterText = "input personInput {")!! },
101101
toFile = "build/generated/source/apollo/main/com/example/generated/type/PersonInput.kt",
102-
toElement = { elementAt<KtParameter>("lastName")!! },
102+
toElement = { elementAt<KtParameter>("lastName: String?")!! },
103+
// We have 2 targets because of builders
104+
multipleTarget = true,
103105
)
104106
}
105107

tests/intellij-plugin-test-project/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ apollo {
1313
service("main") {
1414
packageName.set("com.example.generated")
1515
languageVersion.set("1.5")
16+
generateInputBuilders.set(true)
1617
}
1718
}

tests/intellij-plugin-test-project/src/main/kotlin/com/example/OneOf.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,40 @@ import com.apollographql.apollo3.api.Optional
44
import com.apollographql.apollo3.api.Optional.Absent
55
import com.example.generated.type.FindUserInput
66

7-
fun oneOf() {
7+
fun oneOfConstructor() {
88
FindUserInput()
99

1010
FindUserInput(
11-
email = Optional.present("[email protected]"),
12-
name = Optional.present("John"),
11+
email = Optional.present("[email protected]"),
12+
name = Optional.present("John"),
1313
)
1414

1515
FindUserInput(
16-
email = Optional.present("[email protected]")
16+
email = Optional.present("[email protected]")
1717
)
1818

1919
val absentEmail: Absent = Optional.absent()
2020
FindUserInput(
21-
email = absentEmail
21+
email = absentEmail
2222
)
2323
}
24+
25+
fun oneOfBuilder() {
26+
FindUserInput.Builder()
27+
28+
.name("John")
29+
.build()
30+
31+
FindUserInput.Builder()
32+
33+
.build()
34+
35+
FindUserInput.Builder()
36+
.email(null)
37+
.build()
38+
39+
val someNullableEmail: String? = null
40+
FindUserInput.Builder()
41+
.email(someNullableEmail)
42+
.build()
43+
}

0 commit comments

Comments
 (0)