Skip to content

Commit

Permalink
GraphQL directive location checks
Browse files Browse the repository at this point in the history
  • Loading branch information
jmartisk committed Sep 11, 2023
1 parent b3e3115 commit d3b4b44
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 19 deletions.
5 changes: 5 additions & 0 deletions projects/lsp4mp/projects/maven/microprofile-graphql/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
<artifactId>smallrye-graphql-client-api</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-graphql-api</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -45,7 +46,7 @@ public Conditions currentConditions(@Name("location") String location) throws Un

@DenyAll
@Query
public List<Conditions> currentConditionsList(@Name("locations") List<String> locations)
public List<Conditions> currentConditionsList(@Optimistic @Name("locations") List<String> locations)
throws UnknownLocationException, GraphQLException {

List<Conditions> allConditions = new LinkedList<>();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.graphql;

public enum TypeSystemDirectiveLocation {
SCHEMA,
SCALAR,
OBJECT,
FIELD_DEFINITION,
ARGUMENT_DEFINITION,
INTERFACE,
UNION,
ENUM,
ENUM_VALUE,
INPUT_OBJECT,
INPUT_FIELD_DEFINITION
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/
public enum MicroProfileGraphQLErrorCode implements IJavaErrorCode {

WRONG_DIRECTIVE_PLACEMENT
;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}

0 comments on commit d3b4b44

Please sign in to comment.