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