diff --git a/projects/lsp4mp/projects/maven/microprofile-graphql/pom.xml b/projects/lsp4mp/projects/maven/microprofile-graphql/pom.xml
index e74b19efe..3bf3eae3c 100644
--- a/projects/lsp4mp/projects/maven/microprofile-graphql/pom.xml
+++ b/projects/lsp4mp/projects/maven/microprofile-graphql/pom.xml
@@ -39,6 +39,11 @@
smallrye-graphql-client-api
1.0.2
+
+ io.smallrye
+ smallrye-graphql-api
+ 2.4.0
+
org.slf4j
slf4j-api
diff --git a/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/Optimistic.java b/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/Optimistic.java
new file mode 100644
index 000000000..174a26cb0
--- /dev/null
+++ b/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/Optimistic.java
@@ -0,0 +1,15 @@
+package io.openliberty.graphql.sample;
+
+import io.smallrye.graphql.api.Directive;
+import io.smallrye.graphql.api.DirectiveLocation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Directive(on = {DirectiveLocation.ENUM})
+@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Optimistic {
+}
diff --git a/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/WeatherService.java b/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/WeatherService.java
index a2e6ad5ef..b2270bc48 100644
--- a/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/WeatherService.java
+++ b/projects/lsp4mp/projects/maven/microprofile-graphql/src/main/java/io/openliberty/graphql/sample/WeatherService.java
@@ -36,6 +36,7 @@ public class WeatherService {
@Query
+ @Optimistic
public Conditions currentConditions(@Name("location") String location) throws UnknownLocationException {
if ("nowhere".equalsIgnoreCase(location)) {
throw new UnknownLocationException(location);
@@ -45,7 +46,7 @@ public Conditions currentConditions(@Name("location") String location) throws Un
@DenyAll
@Query
- public List currentConditionsList(@Name("locations") List locations)
+ public List currentConditionsList(@Optimistic @Name("locations") List locations)
throws UnknownLocationException, GraphQLException {
List allConditions = new LinkedList<>();
@@ -69,7 +70,7 @@ public int reset() {
return cleared;
}
- public double wetBulbTempF(@Source @Name("conditions") Conditions conditions) {
+ public double wetBulbTempF(@Source @Name("conditions") @Optimistic Conditions conditions) {
// TODO: pretend like this is a really expensive operation
System.out.println("wetBulbTempF for location " + conditions.getLocation());
return conditions.getTemperatureF() - 3.0;
diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/MicroProfileGraphQLConstants.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/MicroProfileGraphQLConstants.java
index 24a309f4b..b86141ff5 100644
--- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/MicroProfileGraphQLConstants.java
+++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/MicroProfileGraphQLConstants.java
@@ -22,9 +22,11 @@ private MicroProfileGraphQLConstants() {
}
public static final String QUERY_ANNOTATION = "org.eclipse.microprofile.graphql.Query";
-
public static final String MUTATION_ANNOTATION = "org.eclipse.microprofile.graphql.Mutation";
-
+ public static final String SUBSCRIPTION_ANNOTATION = "org.eclipse.microprofile.graphql.Subscription";
+ public static final String GRAPHQL_API_ANNOTATION = "org.eclipse.microprofile.graphql.GraphQLApi";
+ public static final String UNION_ANNOTATION = "io.smallrye.graphql.api.Union";
+ public static final String DIRECTIVE_ANNOTATION = "io.smallrye.graphql.api.Directive";
public static final String DIAGNOSTIC_SOURCE = "microprofile-graphql";
}
\ No newline at end of file
diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/TypeSystemDirectiveLocation.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/TypeSystemDirectiveLocation.java
new file mode 100644
index 000000000..296a87fef
--- /dev/null
+++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/TypeSystemDirectiveLocation.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+* Copyright (c) 2023 Red Hat Inc. and others.
+*
+* This program and the accompanying materials are made available under the
+* terms of the Eclipse Public License v. 2.0 which is available at
+* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+* which is available at https://www.apache.org/licenses/LICENSE-2.0.
+*
+* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+*
+* Contributors:
+* Red Hat Inc. - initial API and implementation
+*******************************************************************************/
+package com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql;
+
+/**
+ * GraphQL schema element types - used for declaring the allowed
+ * placement of directives in a GraphQL schema.
+ * See http://spec.graphql.org/draft/#TypeSystemDirectiveLocation
+ */
+public enum TypeSystemDirectiveLocation {
+ SCHEMA,
+ SCALAR,
+ OBJECT,
+ FIELD_DEFINITION,
+ ARGUMENT_DEFINITION,
+ INTERFACE,
+ UNION,
+ ENUM,
+ ENUM_VALUE,
+ INPUT_OBJECT,
+ INPUT_FIELD_DEFINITION
+}
diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLASTValidator.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLASTValidator.java
index dbeb63148..3b14933f8 100644
--- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLASTValidator.java
+++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLASTValidator.java
@@ -14,44 +14,176 @@
package com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.java;
+import java.text.MessageFormat;
+import java.util.Arrays;
import java.util.logging.Logger;
+import com.intellij.lang.jvm.JvmMethod;
+import com.intellij.lang.jvm.types.JvmArrayType;
+import com.intellij.lang.jvm.types.JvmPrimitiveType;
+import com.intellij.lang.jvm.types.JvmReferenceType;
+import com.intellij.lang.jvm.types.JvmType;
+import com.intellij.lang.jvm.types.JvmTypeVisitor;
+import com.intellij.lang.jvm.types.JvmWildcardType;
import com.intellij.openapi.module.Module;
import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiAnnotationMemberValue;
+import com.intellij.psi.PsiArrayInitializerMemberValue;
+import com.intellij.psi.PsiArrayType;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiEnumConstant;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiJvmModifiersOwner;
import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiParameter;
+import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiType;
+import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.java.diagnostics.JavaDiagnosticsContext;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.java.validators.JavaASTValidator;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.PsiTypeUtils;
-import org.eclipse.lsp4j.DiagnosticSeverity;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.MicroProfileGraphQLConstants;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.TypeSystemDirectiveLocation;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.jetbrains.annotations.NotNull;
+import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.AnnotationUtils.getAnnotation;
import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.AnnotationUtils.isMatchAnnotation;
/**
* Diagnostics for microprofile-graphql.
*
+ * TODO: We currently don't check directives on input/output objects and their properties, because
+ * it's not trivial to determine whether a class is used as an input, or an output, or both. That
+ * will possibly require building the whole GraphQL schema on-the-fly, which might be too expensive.
+ *
* @see https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html
*/
public class MicroProfileGraphQLASTValidator extends JavaASTValidator {
- private static final Logger LOGGER = Logger.getLogger(MicroProfileGraphQLASTValidator.class.getName());
+ private static final Logger LOGGER = Logger.getLogger(MicroProfileGraphQLASTValidator.class.getName());
+
+ private static final String WRONG_DIRECTIVE_PLACEMENT = "Directive ''{0}'' is not allowed on element type ''{1}''";
+
+ @Override
+ public boolean isAdaptedForDiagnostics(JavaDiagnosticsContext context) {
+ Module javaProject = context.getJavaProject();
+ // Check if microprofile-graphql is on the path
+ return PsiTypeUtils.findType(javaProject, MicroProfileGraphQLConstants.QUERY_ANNOTATION) != null;
+ }
+
+ @Override
+ public void visitMethod(PsiMethod node) {
+ validateDirectivesOnMethod(node);
+ super.visitMethod(node);
+ }
+
+ @Override
+ public void visitClass(PsiClass node) {
+ validateDirectivesOnClass(node);
+ super.visitClass(node);
+ }
+
+
+ private void validateDirectivesOnMethod(PsiMethod node) {
+ for (PsiAnnotation annotation : node.getAnnotations()) {
+ // a query/mutation/subscription may only have directives allowed on FIELD_DEFINITION
+ if (isMatchAnnotation(annotation, MicroProfileGraphQLConstants.QUERY_ANNOTATION) ||
+ isMatchAnnotation(annotation, MicroProfileGraphQLConstants.MUTATION_ANNOTATION) ||
+ isMatchAnnotation(annotation, MicroProfileGraphQLConstants.SUBSCRIPTION_ANNOTATION)) {
+ validateDirectives(node, TypeSystemDirectiveLocation.FIELD_DEFINITION);
+ }
+ }
+ // any parameter may only have directives allowed on ARGUMENT_DEFINITION
+ for (PsiParameter parameter : node.getParameterList().getParameters()) {
+ validateDirectives(parameter, TypeSystemDirectiveLocation.ARGUMENT_DEFINITION);
+ }
+ }
+
+ private void validateDirectivesOnClass(PsiClass node) {
+ // a class with @GraphQLApi may only have directives allowed on SCHEMA
+ if(getAnnotation(node, MicroProfileGraphQLConstants.GRAPHQL_API_ANNOTATION) != null) {
+ validateDirectives(node, TypeSystemDirectiveLocation.SCHEMA);
+ }
+ // if an interface has a `@Union` annotation, it may only have directives allowed on UNION
+ // otherwise it may only have directives allowed on INTERFACE
+ if (node.isInterface()) {
+ if(getAnnotation(node, MicroProfileGraphQLConstants.UNION_ANNOTATION) != null) {
+ validateDirectives(node, TypeSystemDirectiveLocation.UNION);
+ } else {
+ validateDirectives(node, TypeSystemDirectiveLocation.INTERFACE);
+ }
+ }
+ // an enum may only have directives allowed on ENUM
+ if (node.isEnum()) {
+ validateDirectives(node, TypeSystemDirectiveLocation.ENUM);
+ // enum values may only have directives allowed on ENUM_VALUE
+ for (PsiField field : node.getFields()) {
+ if(field instanceof PsiEnumConstant) {
+ validateDirectives(field, TypeSystemDirectiveLocation.ENUM_VALUE);
+ }
+ }
+ }
+ }
+
+ private void validateDirectives(PsiJvmModifiersOwner node, TypeSystemDirectiveLocation actualLocation) {
+ directiveLoop:
+ for (PsiAnnotation annotation : node.getAnnotations()) {
+ PsiClass directiveDeclaration = getDirectiveDeclaration(annotation);
+ if (directiveDeclaration != null) {
+ LOGGER.severe("Checking directive: " + annotation.getQualifiedName() + " on node: " + node + " (location type = " + actualLocation.name() + ")");
+ PsiArrayInitializerMemberValue allowedLocations = (PsiArrayInitializerMemberValue) directiveDeclaration
+ .getAnnotation(MicroProfileGraphQLConstants.DIRECTIVE_ANNOTATION)
+ .findAttributeValue("on");
+ if (allowedLocations != null) {
+ for (PsiAnnotationMemberValue initializer : allowedLocations.getInitializers()) {
+ String allowedLocation = initializer.getText().substring(initializer.getText().indexOf(".") + 1);
+ if (allowedLocation.equals(actualLocation.name())) {
+ // ok, this directive is allowed on this element type
+ continue directiveLoop;
+ }
+ }
- @Override
- public boolean isAdaptedForDiagnostics(JavaDiagnosticsContext context) {
- Module javaProject = context.getJavaProject();
- // Check if microprofile-graphql is on the path
- return PsiTypeUtils.findType(javaProject, MicroProfileGraphQLConstants.QUERY_ANNOTATION) != null;
- }
+ String message = MessageFormat.format(WRONG_DIRECTIVE_PLACEMENT, directiveDeclaration.getQualifiedName(), actualLocation.name());
+ super.addDiagnostic(message,
+ MicroProfileGraphQLConstants.DIAGNOSTIC_SOURCE,
+ annotation,
+ MicroProfileGraphQLErrorCode.WRONG_DIRECTIVE_PLACEMENT,
+ DiagnosticSeverity.Error);
+ }
+ }
+ }
- @Override
- public void visitMethod(PsiMethod node) {
- validateMethod(node);
- super.visitMethod(node);
- }
+ }
- private void validateMethod(PsiMethod node) {
+ // If this annotation is not a directive at all, returns null.
+ // If this annotation is a directive, returns its annotation class.
+ // If this annotation is a container of a repeatable directive, returns the annotation class of the directive (not the container).
+ private PsiClass getDirectiveDeclaration(PsiAnnotation annotation) {
+ PsiClass declaration = PsiTypeUtils.findType(getContext().getJavaProject(), annotation.getQualifiedName());
+ if (declaration == null) {
+ return null;
+ }
+ if (declaration.getAnnotation(MicroProfileGraphQLConstants.DIRECTIVE_ANNOTATION) != null) {
+ return declaration;
+ }
+ // check whether this is a container of repeatable directives
+ PsiMethod[] annoParams = declaration.findMethodsByName("value", false);
+ for (PsiMethod annoParam : annoParams) {
+ if (annoParam.getReturnType() instanceof PsiArrayType) {
+ PsiType componentType = ((PsiArrayType) annoParam.getReturnType()).getComponentType();
+ if (componentType instanceof PsiClassReferenceType) {
+ PsiClass directiveDeclaration = PsiTypeUtils.findType(getContext().getJavaProject(),
+ ((PsiClassReferenceType) componentType).getReference().getQualifiedName());
+ if (directiveDeclaration != null && directiveDeclaration.getAnnotation(MicroProfileGraphQLConstants.DIRECTIVE_ANNOTATION) != null) {
+ return directiveDeclaration;
+ }
+ }
+ }
+ }
+ return null;
+ }
- }
}
\ No newline at end of file
diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLErrorCode.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLErrorCode.java
index 5993c51a0..750d21a1a 100644
--- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLErrorCode.java
+++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/internal/graphql/java/MicroProfileGraphQLErrorCode.java
@@ -20,6 +20,7 @@
*/
public enum MicroProfileGraphQLErrorCode implements IJavaErrorCode {
+ WRONG_DIRECTIVE_PLACEMENT
;
@Override
diff --git a/src/test/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/graphql/java/MicroProfileGraphQLValidationTest.java b/src/test/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/graphql/java/MicroProfileGraphQLValidationTest.java
index e487c7e11..c7e33c80f 100644
--- a/src/test/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/graphql/java/MicroProfileGraphQLValidationTest.java
+++ b/src/test/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/graphql/java/MicroProfileGraphQLValidationTest.java
@@ -13,11 +13,57 @@
*******************************************************************************/
package com.redhat.devtools.intellij.lsp4mp4ij.psi.core.graphql.java;
+import com.intellij.openapi.module.Module;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.LSP4MPMavenModuleImportingTestCase;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.MicroProfileMavenProjectName;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.IPsiUtils;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.MicroProfileGraphQLConstants;
+import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.java.MicroProfileGraphQLErrorCode;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4mp.commons.DocumentFormat;
+import org.eclipse.lsp4mp.commons.MicroProfileJavaDiagnosticsParams;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.MicroProfileForJavaAssert.*;
+
+import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.MicroProfileForJavaAssert.getFileUri;
/**
* Tests for {@link com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql.java.MicroProfileGraphQLASTValidator}.
*/
public class MicroProfileGraphQLValidationTest extends LSP4MPMavenModuleImportingTestCase {
+ @Test
+ public void testIncorrectDirectivePlacement() throws Exception {
+ Module javaProject = loadMavenProject(MicroProfileMavenProjectName.microprofile_graphql);
+ IPsiUtils utils = PsiUtilsLSImpl.getInstance(myProject);
+
+ MicroProfileJavaDiagnosticsParams diagnosticsParams = new MicroProfileJavaDiagnosticsParams();
+ String javaFileUri = getFileUri("/src/main/java/io/openliberty/graphql/sample/WeatherService.java", javaProject);
+ diagnosticsParams.setUris(Collections.singletonList(javaFileUri));
+ diagnosticsParams.setDocumentFormat(DocumentFormat.Markdown);
+
+ Diagnostic d1 = d(38, 4, 15,
+ "Directive 'io.openliberty.graphql.sample.Optimistic' is not allowed on element type 'FIELD_DEFINITION'",
+ DiagnosticSeverity.Error, MicroProfileGraphQLConstants.DIAGNOSTIC_SOURCE,
+ MicroProfileGraphQLErrorCode.WRONG_DIRECTIVE_PLACEMENT);
+
+ Diagnostic d2 = d(48, 50, 61,
+ "Directive 'io.openliberty.graphql.sample.Optimistic' is not allowed on element type 'ARGUMENT_DEFINITION'",
+ DiagnosticSeverity.Error, MicroProfileGraphQLConstants.DIAGNOSTIC_SOURCE,
+ MicroProfileGraphQLErrorCode.WRONG_DIRECTIVE_PLACEMENT);
+
+ Diagnostic d3 = d(72, 59, 70,
+ "Directive 'io.openliberty.graphql.sample.Optimistic' is not allowed on element type 'ARGUMENT_DEFINITION'",
+ DiagnosticSeverity.Error, MicroProfileGraphQLConstants.DIAGNOSTIC_SOURCE,
+ MicroProfileGraphQLErrorCode.WRONG_DIRECTIVE_PLACEMENT);
+
+ assertJavaDiagnostics(diagnosticsParams, utils,
+ d1, d2, d3);
+ }
+
}
\ No newline at end of file