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 ApolloOneOfGraphQLViolationInspection #6125

Merged
merged 1 commit into from
Sep 9, 2024
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
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Run_IntelliJ_plugin.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.apollographql.ijplugin.graphql
import com.apollographql.ijplugin.gradle.ApolloKotlinService
import com.apollographql.ijplugin.gradle.GradleToolingModelService
import com.apollographql.ijplugin.project.apolloProjectService
import com.apollographql.ijplugin.settings.projectSettingsState
import com.apollographql.ijplugin.util.logd
import com.intellij.lang.jsgraphql.ide.config.GraphQLConfigContributor
import com.intellij.lang.jsgraphql.ide.config.loader.GraphQLConfigKeys
Expand All @@ -20,6 +21,9 @@ class ApolloGraphQLConfigContributor : GraphQLConfigContributor {
val projectDir = project.guessProjectDir() ?: return emptyList()
// This can be called early, don't initialize services right away. It's ok because it's called again later.
if (!project.apolloProjectService.isInitialized) return emptyList()

if (!project.projectSettingsState.contributeConfigurationToGraphqlPlugin) return emptyList()

return listOf(
GraphQLConfig(
project = project,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.apollographql.ijplugin.inspection

import com.apollographql.ijplugin.ApolloBundle
import com.apollographql.ijplugin.navigation.findFragmentSpreads
import com.apollographql.ijplugin.util.rawType
import com.apollographql.ijplugin.util.resolve
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.lang.jsgraphql.psi.GraphQLArgument
import com.intellij.lang.jsgraphql.psi.GraphQLArrayValue
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLInputObjectTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLInputValueDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLNonNullType
import com.intellij.lang.jsgraphql.psi.GraphQLNullValue
import com.intellij.lang.jsgraphql.psi.GraphQLObjectField
import com.intellij.lang.jsgraphql.psi.GraphQLObjectValue
import com.intellij.lang.jsgraphql.psi.GraphQLTypedOperationDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLVariable
import com.intellij.lang.jsgraphql.psi.GraphQLVariableDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLVisitor
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.parentOfType

class ApolloOneOfGraphQLViolationInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : GraphQLVisitor() {
override fun visitObjectValue(o: GraphQLObjectValue) {
super.visitObjectValue(o)
val parent = if (o.parent is GraphQLArrayValue) o.parent.parent else o.parent
val inputValueDefinition: GraphQLInputValueDefinition = (parent as? GraphQLArgument ?: parent as? GraphQLObjectField)
?.resolve()
?: return
val graphQLTypeName = inputValueDefinition.type?.rawType ?: return
val graphQLInputObjectTypeDefinition: GraphQLInputObjectTypeDefinition = graphQLTypeName.resolve() ?: return
val isOneOf = graphQLInputObjectTypeDefinition.directives.any { it.name == "oneOf" }
if (!isOneOf) return

if (o.objectFieldList.size != 1) {
holder.registerProblem(o, ApolloBundle.message("inspection.oneOfGraphQLViolation.reportText.oneFieldMustBeSupplied", graphQLTypeName.name!!))
} else {
val field = o.objectFieldList.first()
when (val value = field.value) {
is GraphQLNullValue -> {
holder.registerProblem(o, ApolloBundle.message("inspection.oneOfGraphQLViolation.reportText.fieldMustNotBeNull", field.name!!, graphQLTypeName.name!!))
}

is GraphQLVariable -> {
// Look for the parent operation - if there isn't one, we're in a fragment: search for an operation using this fragment
val operationDefinition = field.parentOfType<GraphQLTypedOperationDefinition>()
?: field.parentOfType<GraphQLFragmentDefinition>()?.let { fragmentParent ->
findFragmentSpreads(fragmentParent.project) { it.nameIdentifier.reference?.resolve() == fragmentParent.nameIdentifier }.firstOrNull()
?.parentOfType<GraphQLTypedOperationDefinition>()
}
?: return
val variableDefinition: GraphQLVariableDefinition = operationDefinition.variableDefinitions
?.variableDefinitions?.firstOrNull { it.variable.name == value.name }
?: return
if (variableDefinition.type !is GraphQLNonNullType) {
holder.registerProblem(
o,
ApolloBundle.message(
"inspection.oneOfGraphQLViolation.reportText.variableMustBeNonNullType",
value.name!!,
variableDefinition.type?.text ?: "Unknown"
)
)
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import com.intellij.lang.jsgraphql.psi.GraphQLFieldDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLFile
import com.intellij.lang.jsgraphql.psi.GraphQLIdentifier
import com.intellij.lang.jsgraphql.psi.GraphQLInterfaceTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLListType
import com.intellij.lang.jsgraphql.psi.GraphQLNamedElement
import com.intellij.lang.jsgraphql.psi.GraphQLNamedTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLNonNullType
import com.intellij.lang.jsgraphql.psi.GraphQLObjectTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLType
import com.intellij.lang.jsgraphql.psi.GraphQLTypeName
import com.intellij.lang.jsgraphql.psi.GraphQLValue
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import com.intellij.psi.util.parentOfTypes

/**
* Given a field identifier, returns the set of field coordinates that match it.
Expand Down Expand Up @@ -67,7 +74,8 @@ private fun matchingFieldCoordinates(
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>() }
val implementedInterfaceTypeDefinitions =
implementedInterfaces.typeNameList.mapNotNull { it.nameIdentifier.reference?.resolve()?.parentOfType<GraphQLInterfaceTypeDefinition>() }
if (implementedInterfaceTypeDefinitions.isEmpty()) return fieldCoordinates
return fieldCoordinates + implementedInterfaceTypeDefinitions.flatMap { matchingFieldCoordinates(fieldDefinition, it) }
}
Expand All @@ -84,4 +92,18 @@ fun GraphQLElement.schemaFiles(): List<GraphQLFile> {
}

fun GraphQLDirective.argumentValue(argumentName: String): GraphQLValue? =
arguments?.argumentList.orEmpty().firstOrNull { it.name == argumentName }?.value
arguments?.argumentList.orEmpty().firstOrNull { it.name == argumentName }?.value

inline fun <reified T : PsiElement> GraphQLNamedElement.resolve(): T? =
nameIdentifier?.reference?.resolve()?.parentOfTypes(T::class)

val GraphQLType.rawType: GraphQLTypeName?
get() {
@Suppress("RecursivePropertyAccessor")
return when (this) {
is GraphQLTypeName -> return this
is GraphQLNonNullType -> return this.type.rawType
is GraphQLListType -> return this.type.rawType
else -> null
}
}
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 @@ -188,6 +188,18 @@
level="WARNING"
/>

<!-- "@OneOf GraphQL violation" inspection -->
<!--suppress PluginXmlCapitalization -->
<localInspection
language="GraphQL"
implementationClass="com.apollographql.ijplugin.inspection.ApolloOneOfGraphQLViolationInspection"
groupPathKey="inspection.group.graphql"
groupKey="inspection.group.graphql.apolloKotlin"
key="inspection.oneOfGraphQLViolation.displayName"
enabledByDefault="true"
level="ERROR"
/>

<annotator
language="yaml"
implementationClass="com.apollographql.ijplugin.inspection.ApolloGraphQLConfigFilePresentAnnotator" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html>
<body>
Reports <code>@oneOf</code> violations in GraphQL.
<p>
<ul>
<li>Exactly one field must be supplied to OneOf input objects</li>
<li>The field supplied must not be null</li>
<li>If a variable is supplied, it must be of a non-null type</li>
</ul>
</p>

<p>
See <a href="https://github.com/graphql/graphql-spec/pull/825">the OneOf Input Objects RFC</a> for more information.
</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Reports invalid creation of <code>@oneOf</code> input types.
<p>
<ul>
<li>`@oneOf` input class must be created with exactly one <code>Present</code> / non-null argument.
<li><code>@oneOf</code> input class must be created with exactly one <code>Present</code> / non-null argument.</li>
</ul>
</p>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ inspection.graphQLConfigFilePresent.displayName=GraphQL config file present
inspection.graphQLConfigFilePresent.reportText=The Apollo plugin retrieves the GraphQL configuration from Gradle and doesn't use the GraphQL config file
inspection.graphQLConfigFilePresent.quickFix=Delete the {0} file

inspection.oneOfGraphQLViolation.displayName=@oneOf GraphQL violation
inspection.oneOfGraphQLViolation.reportText.oneFieldMustBeSupplied=Exactly one field must be supplied to the OneOf input object "{0}"
inspection.oneOfGraphQLViolation.reportText.fieldMustNotBeNull=The field "{0}" supplied to the OneOf input object "{1}" must not be null
inspection.oneOfGraphQLViolation.reportText.variableMustBeNonNullType=The variable "{0}" of type "{1}" used in a OneOf input type must be a non-null type
inspection.more=More...


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.apollographql.ijplugin.inspection

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

@TestDataPath("\$CONTENT_ROOT/testData/inspection/OneOfGraphQLViolation")
@RunWith(JUnit4::class)
class ApolloOneOfGraphQLViolationInspectionTest : ApolloTestCase() {

override fun getTestDataPath() = "src/test/testData/inspection/OneOfGraphQLViolation"

@Throws(Exception::class)
override fun setUp() {
super.setUp()
myFixture.enableInspections(ApolloOneOfGraphQLViolationInspection())
}

@Test
fun testInspection() {
myFixture.copyFileToProject("schema.graphqls", "schema.graphqls")
myFixture.copyFileToProject("graphql.config.yml", "graphql.config.yml")
myFixture.configureByFile("operations.graphql")

checkHighlighting()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema:
- schema.graphqls
documents: '**/*.graphql'
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Exactly one field must be supplied to the OneOf input object
query Query1 {
field(myInput: <error descr="Exactly one field must be supplied to the OneOf input object \"MyInput\"">{ a: "a", b: 2 }</error>)
}

query Query2 {
field(myInput: { c: <error descr="Exactly one field must be supplied to the OneOf input object \"MyInput2\"">{ d: "c", e: 4 }</error> })
}

# The field supplied to the OneOf input object must not be null
query Query3 {
field(myInput: <error descr="The field \"a\" supplied to the OneOf input object \"MyInput\" must not be null">{ a: null }</error>)
}

query Query4 {
field(myInput: { c: <error descr="The field \"d\" supplied to the OneOf input object \"MyInput2\" must not be null">{ d: null }</error> })
}

query Query5 {
field2(myInput: [ { a: "" }, <error descr="The field \"a\" supplied to the OneOf input object \"MyInput\" must not be null">{ a: null }</error> ])
}

# Variable used in a OneOf input type must be a non-null type
query Query6($var: String) {
field(myInput: <error descr="The variable \"var\" of type \"String\" used in a OneOf input type must be a non-null type">{ a: $var }</error>)
}

query Query7($var: String) {
field(myInput: { c: <error descr="The variable \"var\" of type \"String\" used in a OneOf input type must be a non-null type">{ d: $var }</error> })
}

query Query8($var: String) {
...MyFragment
}

fragment MyFragment on Query {
field(myInput: <error descr="The variable \"var\" of type \"String\" used in a OneOf input type must be a non-null type">{ a: $var }</error>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type Query {
field(myInput: MyInput): String
field2(myInput: [MyInput]): String
}

directive @oneOf on INPUT_OBJECT

input MyInput @oneOf {
a: String
b: Int
c: MyInput2
}

input MyInput2 @oneOf {
d: String
e: Int
}
Loading