From 93499a51f83c57cb3a90fdbbb5094180194b4940 Mon Sep 17 00:00:00 2001
From: Benoit Lubek
Date: Fri, 22 Dec 2023 14:12:01 +0100
Subject: [PATCH] [IJ Plugin] Inspection: missing directive import (#5494)
* Add ApolloMissingGraphQLDefinitionImport (wip)
* Add GraphQLUnresolvedReferenceInspectionSuppressor
* Quickfix for ApolloMissingGraphQLDefinitionImport
* Add description
* Also add a @catch directive to the schema if we're importing @catch
* Add unit test
* Fix blank lines
* Make @link prefix aware and use apollo-ast for the definitions
* Report missing CatchTo import too
* Update test
* Change wording
* Update intellij-plugin/src/main/resources/inspectionDescriptions/ApolloMissingGraphQLDefinitionImport.html
Co-authored-by: Martin Bonnin
* Use definitions to handle the import directive argument type case
* Warn for usage of non-imported kotlin_labs definitions
* Update intellij-plugin/src/main/resources/messages/ApolloBundle.properties
Co-authored-by: Martin Bonnin
* Update intellij-plugin/src/main/resources/inspectionDescriptions/ApolloMissingGraphQLDefinitionImport.html
Co-authored-by: Martin Bonnin
* Update test after text change
* Don't add @catch on the schema, and add imports at the top of the file
---------
Co-authored-by: Martin Bonnin
---
intellij-plugin/build.gradle.kts | 1 +
.../gradle/GradleToolingModelService.kt | 9 +-
...issingGraphQLDefinitionImportInspection.kt | 178 ++++++++++++++++++
.../ijplugin/inspection/GraphQL.kt | 20 ++
...UnresolvedReferenceInspectionSuppressor.kt | 47 +++++
.../apollographql/ijplugin/inspection/Link.kt | 88 +++++++++
.../ijplugin/telemetry/TelemetrySession.kt | 4 +
.../com/apollographql/ijplugin/util/Files.kt | 11 ++
.../src/main/resources/META-INF/plugin.xml | 19 ++
.../ApolloMissingGraphQLDefinitionImport.html | 25 +++
.../messages/ApolloBundle.properties | 13 +-
...ngGraphQLDefinitionImportInspectionTest.kt | 85 +++++++++
.../missing-CatchTo-extra.graphqls | 5 +
.../missing-CatchTo-extra_after.graphqls | 5 +
.../missing-CatchTo.config.yml | 4 +
.../missing-CatchTo.graphql | 8 +
.../missing-CatchTo.graphqls | 12 ++
.../missing-catch-extra_after.graphqls | 5 +
.../missing-catch.config.yml | 4 +
.../missing-catch.graphql | 8 +
.../missing-catch.graphqls | 12 ++
.../missing-targetName-extra.graphqls | 7 +
.../missing-targetName-extra_after.graphqls | 13 ++
.../missing-targetName.config.yml | 4 +
.../missing-targetName.graphql | 8 +
.../missing-targetName.graphqls | 12 ++
26 files changed, 605 insertions(+), 2 deletions(-)
create mode 100644 intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloMissingGraphQLDefinitionImportInspection.kt
create mode 100644 intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQLUnresolvedReferenceInspectionSuppressor.kt
create mode 100644 intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Link.kt
create mode 100644 intellij-plugin/src/main/resources/inspectionDescriptions/ApolloMissingGraphQLDefinitionImport.html
create mode 100644 intellij-plugin/src/test/kotlin/com/apollographql/ijplugin/inspection/ApolloMissingGraphQLDefinitionImportInspectionTest.kt
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-CatchTo-extra.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-CatchTo-extra_after.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-CatchTo.config.yml
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-CatchTo.graphql
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-CatchTo.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-catch-extra_after.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-catch.config.yml
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-catch.graphql
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-catch.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-targetName-extra.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-targetName-extra_after.graphqls
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-targetName.config.yml
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-targetName.graphql
create mode 100644 intellij-plugin/src/test/testData/inspection/MissingGraphQLDefinitionImport/missing-targetName.graphqls
diff --git a/intellij-plugin/build.gradle.kts b/intellij-plugin/build.gradle.kts
index 86fdfad6860..bc4d86669bb 100644
--- a/intellij-plugin/build.gradle.kts
+++ b/intellij-plugin/build.gradle.kts
@@ -217,6 +217,7 @@ publishing {
dependencies {
implementation(project(":apollo-gradle-plugin-external"))
+ implementation(project(":apollo-ast"))
implementation(project(":apollo-tooling"))
implementation(project(":apollo-normalized-cache-sqlite"))
implementation(libs.sqlite.jdbc)
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
index d3fe8e4a341..ec7e0a07c9e 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/gradle/GradleToolingModelService.kt
@@ -117,6 +117,11 @@ class GradleToolingModelService(
})
}
+ fun triggerFetchToolingModels() {
+ logd()
+ startOrAbortFetchToolingModels()
+ }
+
private fun startOrAbortFetchToolingModels() {
logd()
abortFetchToolingModels()
@@ -181,7 +186,7 @@ class GradleToolingModelService(
logd("Fetch tooling model for :${gradleProject.name}")
return@execute try {
val id = ExternalSystemTaskId.create(GRADLE_SYSTEM_ID, ExternalSystemTaskType.RESOLVE_PROJECT, project)
- gradleExecutionHelper.getModelBuilder(ApolloGradleToolingModel::class.java, connection,id, executionSettings, ExternalSystemTaskNotificationListenerAdapter.NULL_OBJECT)
+ gradleExecutionHelper.getModelBuilder(ApolloGradleToolingModel::class.java, connection, id, executionSettings, ExternalSystemTaskNotificationListenerAdapter.NULL_OBJECT)
.withCancellationToken(gradleCancellation!!.token())
.get()
.takeIf {
@@ -296,3 +301,5 @@ class GradleToolingModelService(
}
}
}
+
+val Project.gradleToolingModelService get() = service()
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloMissingGraphQLDefinitionImportInspection.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloMissingGraphQLDefinitionImportInspection.kt
new file mode 100644
index 00000000000..fd0cf8a96bc
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/ApolloMissingGraphQLDefinitionImportInspection.kt
@@ -0,0 +1,178 @@
+package com.apollographql.ijplugin.inspection
+
+import com.apollographql.apollo3.ast.GQLDefinition
+import com.apollographql.apollo3.ast.GQLDirectiveDefinition
+import com.apollographql.apollo3.ast.GQLEnumTypeDefinition
+import com.apollographql.apollo3.ast.GQLInputObjectTypeDefinition
+import com.apollographql.apollo3.ast.GQLNamed
+import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
+import com.apollographql.apollo3.ast.rawType
+import com.apollographql.ijplugin.ApolloBundle
+import com.apollographql.ijplugin.gradle.gradleToolingModelService
+import com.apollographql.ijplugin.project.apolloProjectService
+import com.apollographql.ijplugin.telemetry.TelemetryEvent
+import com.apollographql.ijplugin.telemetry.telemetryService
+import com.apollographql.ijplugin.util.cast
+import com.apollographql.ijplugin.util.findChildrenOfType
+import com.apollographql.ijplugin.util.quoted
+import com.apollographql.ijplugin.util.unquoted
+import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
+import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils
+import com.intellij.codeInspection.LocalInspectionTool
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.codeInspection.ProblemHighlightType
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.lang.jsgraphql.psi.GraphQLArrayValue
+import com.intellij.lang.jsgraphql.psi.GraphQLDirective
+import com.intellij.lang.jsgraphql.psi.GraphQLElementFactory
+import com.intellij.lang.jsgraphql.psi.GraphQLSchemaExtension
+import com.intellij.lang.jsgraphql.psi.GraphQLVisitor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElementVisitor
+import com.intellij.psi.util.parentOfType
+
+class ApolloMissingGraphQLDefinitionImportInspection : LocalInspectionTool() {
+ override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
+ return object : GraphQLVisitor() {
+ override fun visitDirective(o: GraphQLDirective) {
+ super.visitDirective(o)
+ if (!o.project.apolloProjectService.apolloVersion.isAtLeastV4) return
+ visitDirective(o, holder, NULLABILITY_DEFINITIONS, NULLABILITY_URL, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL)
+ visitDirective(o, holder, KOTLIN_LABS_DEFINITIONS, KOTLIN_LABS_URL, ProblemHighlightType.WEAK_WARNING)
+ }
+ }
+ }
+
+ private fun visitDirective(
+ directiveElement: GraphQLDirective,
+ holder: ProblemsHolder,
+ definitions: List,
+ definitionsUrl: String,
+ highlightType: ProblemHighlightType,
+ ) {
+ if (directiveElement.name !in definitions.directives().map { it.name }) return
+ val message = if (highlightType == ProblemHighlightType.WEAK_WARNING) "inspection.missingGraphQLDefinitionImport.reportText.warning" else "inspection.missingGraphQLDefinitionImport.reportText.error"
+ if (!directiveElement.isImported(definitionsUrl)) {
+ val typeKind = ApolloBundle.message("inspection.missingGraphQLDefinitionImport.reportText.directive")
+ holder.registerProblem(
+ directiveElement,
+ ApolloBundle.message(message, typeKind, directiveElement.name!!),
+ highlightType,
+ ImportDefinitionQuickFix(typeKind = typeKind, elementName = directiveElement.name!!, definitions = definitions, definitionsUrl = definitionsUrl),
+ )
+ } else {
+ val directiveDefinition = definitions.directives().firstOrNull { it.name == directiveElement.name } ?: return
+ val knownDefinitionNames = definitions.filterIsInstance().map { it.name }
+ val arguments = directiveElement.arguments?.argumentList.orEmpty()
+ for (argument in arguments) {
+ val argumentDefinition = directiveDefinition.arguments.firstOrNull { it.name == argument.name } ?: continue
+ val argumentTypeToImport = argumentDefinition.type.rawType().name.takeIf { it in knownDefinitionNames } ?: continue
+ if (!isImported(directiveElement, argumentTypeToImport, definitionsUrl)) {
+ val typeKind = getTypeKind(argumentTypeToImport)
+ holder.registerProblem(
+ argument,
+ ApolloBundle.message(message, typeKind, argumentTypeToImport),
+ highlightType,
+ ImportDefinitionQuickFix(typeKind = typeKind, elementName = argumentTypeToImport, definitions = definitions, definitionsUrl = definitionsUrl),
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun getTypeKind(typeName: String): String {
+ val typeDefinition = NULLABILITY_DEFINITIONS.firstOrNull { it is GQLNamed && it.name == typeName } ?: return "unknown"
+ return ApolloBundle.message(
+ when (typeDefinition) {
+ is GQLDirectiveDefinition -> "inspection.missingGraphQLDefinitionImport.reportText.directive"
+ is GQLEnumTypeDefinition -> "inspection.missingGraphQLDefinitionImport.reportText.enum"
+ is GQLInputObjectTypeDefinition -> "inspection.missingGraphQLDefinitionImport.reportText.input"
+ is GQLScalarTypeDefinition -> "inspection.missingGraphQLDefinitionImport.reportText.scalar"
+ else -> return "unknown"
+ }
+ )
+}
+
+private class ImportDefinitionQuickFix(
+ val typeKind: String,
+ val elementName: String,
+ private val definitions: List,
+ private val definitionsUrl: String,
+) : LocalQuickFix {
+ override fun getName() = ApolloBundle.message("inspection.missingGraphQLDefinitionImport.quickFix", typeKind, "'$elementName'")
+ override fun getFamilyName() = name
+
+ override fun availableInBatchMode() = false
+ override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ if (!IntentionPreviewUtils.isIntentionPreviewActive()) project.telemetryService.logEvent(TelemetryEvent.ApolloIjMissingGraphQLDefinitionImportQuickFix())
+
+ val element = descriptor.psiElement.parentOfType(withSelf = true)!!
+ val schemaFiles = element.schemaFiles()
+ val linkDirective = schemaFiles.flatMap { it.linkDirectives(definitionsUrl) }.firstOrNull()
+
+ if (linkDirective == null) {
+ val linkDirectiveSchemaExtension = createLinkDirectiveSchemaExtension(project, setOf(element.nameForImport), definitions, definitionsUrl)
+ val extraSchemaFile = schemaFiles.firstOrNull { it.name == "extra.graphqls" }
+ if (extraSchemaFile == null) {
+ GraphQLElementFactory.createFile(project, linkDirectiveSchemaExtension.text).also {
+ // Save the file to the project
+ it.name = "extra.graphqls"
+ schemaFiles.first().containingDirectory!!.add(it)
+
+ // There's a new schema file, reload the configuration
+ project.gradleToolingModelService.triggerFetchToolingModels()
+ }
+ } else {
+ val addedElement = extraSchemaFile.addBefore(linkDirectiveSchemaExtension, extraSchemaFile.firstChild)
+ extraSchemaFile.addAfter(GraphQLElementFactory.createWhiteSpace(project, "\n\n"), addedElement)
+ }
+ } else {
+ val importedNames = buildSet {
+ addAll(linkDirective.arguments!!.argumentList.firstOrNull { it.name == "import" }?.value?.cast()?.valueList.orEmpty().map { it.text.unquoted() })
+ add(element.nameForImport)
+ }
+ linkDirective.replace(createLinkDirective(project, importedNames, definitions, definitionsUrl))
+ }
+ }
+}
+
+private fun createLinkDirectiveSchemaExtension(
+ project: Project,
+ importedNames: Set,
+ definitions: List,
+ definitionsUrl: String,
+): GraphQLSchemaExtension {
+ // If any of the imported name is a directive, add its argument types to the import list
+ val knownDefinitionNames = definitions.filterIsInstance().map { it.name }
+ val additionalNames = importedNames.flatMap { importedName ->
+ definitions.directives().firstOrNull { "@${it.name}" == importedName }
+ ?.arguments
+ ?.map { it.type.rawType().name }
+ ?.filter { it in knownDefinitionNames }.orEmpty()
+ }.toSet()
+
+ return GraphQLElementFactory.createFile(
+ project,
+ """
+ extend schema
+ @link(
+ url: "$definitionsUrl",
+ import: [${(importedNames + additionalNames).joinToString { it.quoted() }}]
+ )
+ """.trimIndent()
+ )
+ .findChildrenOfType().single()
+}
+
+private fun createLinkDirective(
+ project: Project,
+ importedNames: Set,
+ definitions: List,
+ definitionsUrl: String,
+): GraphQLDirective {
+ return createLinkDirectiveSchemaExtension(project, importedNames, definitions, definitionsUrl).directives.single()
+}
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt
index e38268c1eb9..c87952ba3d5 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQL.kt
@@ -1,10 +1,16 @@
package com.apollographql.ijplugin.inspection
+import com.apollographql.ijplugin.util.findPsiFileByUrl
+import com.intellij.lang.jsgraphql.ide.config.GraphQLConfigProvider
+import com.intellij.lang.jsgraphql.psi.GraphQLDirective
+import com.intellij.lang.jsgraphql.psi.GraphQLElement
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.GraphQLNamedTypeDefinition
import com.intellij.lang.jsgraphql.psi.GraphQLObjectTypeDefinition
+import com.intellij.lang.jsgraphql.psi.GraphQLValue
import com.intellij.psi.util.parentOfType
/**
@@ -66,3 +72,17 @@ private fun matchingFieldCoordinates(
if (implementedInterfaceTypeDefinitions.isEmpty()) return fieldCoordinates
return fieldCoordinates + implementedInterfaceTypeDefinitions.flatMap { matchingFieldCoordinates(fieldDefinition, it) }
}
+
+/**
+ * Return the schema files associated with the given element.
+ */
+fun GraphQLElement.schemaFiles(): List {
+ val containingFile = containingFile ?: return emptyList()
+ val projectConfig = GraphQLConfigProvider.getInstance(project).resolveProjectConfig(containingFile) ?: return emptyList()
+ return projectConfig.schema.mapNotNull { schema ->
+ schema.filePath?.let { path -> project.findPsiFileByUrl(schema.dir.url + "/" + path) } as? GraphQLFile
+ }
+}
+
+fun GraphQLDirective.argumentValue(argumentName: String): GraphQLValue? =
+ arguments?.argumentList.orEmpty().firstOrNull { it.name == argumentName }?.value
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQLUnresolvedReferenceInspectionSuppressor.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQLUnresolvedReferenceInspectionSuppressor.kt
new file mode 100644
index 00000000000..738c6e32240
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/GraphQLUnresolvedReferenceInspectionSuppressor.kt
@@ -0,0 +1,47 @@
+package com.apollographql.ijplugin.inspection
+
+import com.apollographql.apollo3.ast.GQLDirectiveDefinition
+import com.apollographql.apollo3.ast.linkDefinitions
+import com.intellij.codeInspection.InspectionSuppressor
+import com.intellij.codeInspection.SuppressQuickFix
+import com.intellij.lang.jsgraphql.psi.GraphQLArgument
+import com.intellij.lang.jsgraphql.psi.GraphQLDirective
+import com.intellij.lang.jsgraphql.psi.GraphQLDirectivesAware
+import com.intellij.psi.PsiElement
+
+private val KNOWN_DIRECTIVES: List by lazy {
+ linkDefinitions().directives() + NULLABILITY_DEFINITIONS.directives()
+}
+
+/**
+ * Do not highlight certain known directives as unresolved references.
+ *
+ * TODO: remove this once https://github.com/JetBrains/js-graphql-intellij-plugin/pull/698 is merged.
+ */
+class GraphQLUnresolvedReferenceInspectionSuppressor : InspectionSuppressor {
+ override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
+ val parent = element.parent
+ return when (toolId) {
+ "GraphQLUnresolvedReference" -> parent.isKnownDirective() || parent.isKnownDirectiveArgument()
+
+ "GraphQLMissingType" -> element is GraphQLDirectivesAware && element.directives.all { it.isKnownDirective() }
+
+ // We need to suppress this one too because the plugin doesn't know that @link is repeatable
+ "GraphQLDuplicateDirective" -> element is GraphQLDirective && element.name == "link"
+
+ else -> false
+ }
+ }
+
+ override fun getSuppressActions(psiElement: PsiElement?, s: String): Array = SuppressQuickFix.EMPTY_ARRAY
+}
+
+private fun PsiElement.isKnownDirective(): Boolean {
+ return this is GraphQLDirective && (name in KNOWN_DIRECTIVES.map { it.name } || this.isImported(NULLABILITY_URL))
+}
+
+private fun PsiElement.isKnownDirectiveArgument(): Boolean {
+ return this is GraphQLArgument &&
+ parent?.parent?.isKnownDirective() == true &&
+ name in KNOWN_DIRECTIVES.firstOrNull { it.name == (parent.parent as GraphQLDirective).nameWithoutPrefix }?.arguments?.map { it.name }.orEmpty()
+}
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Link.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Link.kt
new file mode 100644
index 00000000000..7e923cd31f9
--- /dev/null
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/inspection/Link.kt
@@ -0,0 +1,88 @@
+@file:OptIn(ApolloInternal::class)
+
+package com.apollographql.ijplugin.inspection
+
+import com.apollographql.apollo3.annotations.ApolloInternal
+import com.apollographql.apollo3.ast.GQLDefinition
+import com.apollographql.apollo3.ast.GQLDirectiveDefinition
+import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
+import com.apollographql.apollo3.ast.NULLABILITY_VERSION
+import com.apollographql.apollo3.ast.kotlinLabsDefinitions
+import com.apollographql.apollo3.ast.nullabilityDefinitions
+import com.apollographql.ijplugin.util.quoted
+import com.apollographql.ijplugin.util.unquoted
+import com.intellij.lang.jsgraphql.psi.GraphQLArrayValue
+import com.intellij.lang.jsgraphql.psi.GraphQLDirective
+import com.intellij.lang.jsgraphql.psi.GraphQLElement
+import com.intellij.lang.jsgraphql.psi.GraphQLFile
+import com.intellij.lang.jsgraphql.psi.GraphQLNamedElement
+import com.intellij.lang.jsgraphql.psi.GraphQLSchemaDefinition
+import com.intellij.lang.jsgraphql.psi.GraphQLSchemaExtension
+import com.intellij.lang.jsgraphql.psi.GraphQLStringValue
+
+const val NULLABILITY_URL = "https://specs.apollo.dev/nullability/$NULLABILITY_VERSION"
+
+val NULLABILITY_DEFINITIONS: List by lazy {
+ nullabilityDefinitions(NULLABILITY_VERSION)
+}
+
+const val KOTLIN_LABS_URL = "https://specs.apollo.dev/kotlin_labs/$KOTLIN_LABS_VERSION"
+
+val KOTLIN_LABS_DEFINITIONS: List by lazy {
+ kotlinLabsDefinitions(KOTLIN_LABS_VERSION)
+}
+
+const val CATCH = "catch"
+
+fun List.directives(): List {
+ return filterIsInstance()
+}
+
+fun GraphQLNamedElement.isImported(definitionsUrl: String): Boolean {
+ for (schemaFile in schemaFiles()) {
+ if (schemaFile.hasImportFor(this.name!!, this is GraphQLDirective, definitionsUrl)) return true
+ }
+ return false
+}
+
+fun isImported(element: GraphQLElement, enumName: String, definitionsUrl: String): Boolean {
+ for (schemaFile in element.schemaFiles()) {
+ if (schemaFile.hasImportFor(enumName, false, definitionsUrl)) return true
+ }
+ return false
+}
+
+fun GraphQLFile.linkDirectives(definitionsUrl: String): List {
+ val schemaDirectives = typeDefinitions.filterIsInstance().flatMap { it.directives } +
+ typeDefinitions.filterIsInstance().flatMap { it.directives }
+ return schemaDirectives.filter { directive ->
+ directive.name == "link" &&
+ directive.arguments?.argumentList.orEmpty().any { arg -> arg.name == "url" && arg.value?.text == definitionsUrl.quoted() }
+ }
+}
+
+private fun GraphQLFile.hasImportFor(name: String, isDirective: Boolean, definitionsUrl: String): Boolean {
+ for (directive in linkDirectives(definitionsUrl)) {
+ val importArgValue = directive.argumentValue("import") as? GraphQLArrayValue
+ if (importArgValue == null) {
+ // Default import is everything - see https://specs.apollo.dev/link/v1.0/#@link.url
+ val asArgValue = directive.argumentValue("as") as? GraphQLStringValue
+ // Default prefix is the name part of the url
+ val prefix = (asArgValue?.text?.unquoted() ?: "nullability") + "__"
+ if (name.startsWith(prefix)) return true
+ } else {
+ if (importArgValue.valueList.any { it.text == name.nameForImport(isDirective).quoted() }) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+val String.nameWithoutPrefix get() = substringAfter("__")
+
+val GraphQLNamedElement.nameWithoutPrefix get() = name!!.nameWithoutPrefix
+
+fun String.nameForImport(isDirective: Boolean) = "${if (isDirective) "@" else ""}${this.nameWithoutPrefix}"
+
+val GraphQLNamedElement.nameForImport get() = if (this is GraphQLDirective) "@$nameWithoutPrefix" else nameWithoutPrefix
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt
index 2074f8d202e..e5d05cf9ca1 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt
@@ -384,6 +384,10 @@ sealed class TelemetryEvent(
*/
class ApolloIjInputConstructorChangeToBuilderIntentionApply : TelemetryEvent("akij_input_constructor_change_to_builder_intention_apply", null)
+ /**
+ * User applied the 'Import directive' quickfix for the 'Missing GraphQL definition import' inspection of the Apollo Kotlin IntelliJ plugin.
+ */
+ class ApolloIjMissingGraphQLDefinitionImportQuickFix : TelemetryEvent("akij_missing_graphql_definition_import_quickfix", null)
}
class TelemetryEventList {
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Files.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Files.kt
index fb603515660..488f4efa56c 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Files.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/util/Files.kt
@@ -3,17 +3,28 @@ package com.apollographql.ijplugin.util
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.GeneratedSourcesFilter
import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiUtilCore
+import java.nio.file.Path
fun Project.findPsiFilesByName(fileName: String, searchScope: GlobalSearchScope): List {
val virtualFiles = FilenameIndex.getVirtualFilesByName(fileName, searchScope)
return PsiUtilCore.toPsiFiles(PsiManager.getInstance(this), virtualFiles)
}
+fun Project.findPsiFileByPath(path: String): PsiFile? {
+ return VirtualFileManager.getInstance().findFileByNioPath(Path.of(path))?.let { PsiManager.getInstance(this).findFile(it) }
+}
+
+fun Project.findPsiFileByUrl(url: String): PsiFile? {
+ return VirtualFileManager.getInstance().findFileByUrl(url)?.let { PsiManager.getInstance(this).findFile(it) }
+}
+
+
fun Project.findPsiFilesByExtension(extension: String, searchScope: GlobalSearchScope): List {
val virtualFiles = FilenameIndex.getAllFilesByExt(this, extension, searchScope)
return PsiUtilCore.toPsiFiles(PsiManager.getInstance(this), virtualFiles)
diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml
index 56076fde257..0a0787bb420 100644
--- a/intellij-plugin/src/main/resources/META-INF/plugin.xml
+++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml
@@ -124,6 +124,19 @@
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
/>
+
+
+
+
+
+
+
+
+Reports usages of types and directives that are not imported or imported implicitly.
+
+ Before being referenced, directives and types supported by Apollo Kotlin must be imported by your schema using the @link
directive
.
+ For instance, to use the @semanticNonNull
directive, import it from the
+ nullability
definitions:
+
+ extend schema
+ @link(
+ url: "https://specs.apollo.dev/nullability/v0.1",
+ import: ["@semanticNonNull"]
+ )
+
+
+
+ Note: the kotlin_labs
directives such as @typePolicy
+ are currently implicitly imported, so an explicit import is not required, but encouraged. This inspection will report
+ their non-imported usage as a warning.
+
+
+ More information about the @link
directive
+
+
+