Java tutorial
/** * Copyright (c) 2015-present, Jim Kynde Meyer * All rights reserved. * <p> * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.intellij.lang.jsgraphql.endpoint.ide.annotator; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.lang.jsgraphql.JSGraphQLScalars; import com.intellij.lang.jsgraphql.endpoint.JSGraphQLEndpointTokenTypes; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointAnnotation; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointArgumentsDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointFieldDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointFieldDefinitionSet; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointImplementsInterfaces; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointImportDeclaration; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointImportFileReference; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointInputObjectTypeDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointInterfaceTypeDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointNamedAnnotationArgument; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointNamedType; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointNamedTypeDef; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointNamedTypeDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointNamedTypePsiElement; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointObjectTypeDefinition; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointProperty; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointPsiUtil; import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointUnionTypeDefinition; import com.intellij.lang.jsgraphql.ide.configuration.JSGraphQLConfigurationProvider; import com.intellij.lang.jsgraphql.ide.configuration.JSGraphQLSchemaEndpointAnnotation; import com.intellij.openapi.editor.colors.CodeInsightColors; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiComment; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiTreeUtil; public class JSGraphQLEndpointErrorAnnotator implements Annotator { private static final Key<Multimap<String, JSGraphQLEndpointNamedTypeDefinition>> KNOWN_DEFINITIONS = Key .create("JSGraphQLEndpointErrorAnnotator.knownDefinitions"); private static final Key<List<JSGraphQLSchemaEndpointAnnotation>> ANNOTATIONS = Key .create(JSGraphQLSchemaEndpointAnnotation.class.getName()); @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { // references, e.g. to unknown types if (element instanceof JSGraphQLEndpointNamedTypePsiElement) { final PsiReference reference = element.getReference(); if (reference != null) { final PsiElement resolved = reference.resolve(); if (resolved == null) { holder.createErrorAnnotation(element, "Unknown type '" + element.getText() + "'. Are you missing an import?") .setTextAttributes(CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES); } else { // types referenced after implements must be interfaces if (PsiTreeUtil.getParentOfType(element, JSGraphQLEndpointImplementsInterfaces.class) != null) { if (PsiTreeUtil.getParentOfType(resolved, JSGraphQLEndpointInterfaceTypeDefinition.class) == null) { holder.createErrorAnnotation(element, "'" + element.getText() + "' must be an interface to be used here"); } } // input types must be declared before their use (imports are considered as declared before) final JSGraphQLEndpointInputObjectTypeDefinition resolvedInputDef = PsiTreeUtil .getParentOfType(resolved, JSGraphQLEndpointInputObjectTypeDefinition.class); if (resolvedInputDef != null) { if (resolvedInputDef.getTextOffset() > element.getTextOffset() && resolvedInputDef.getContainingFile() == element.getContainingFile()) { // non-imported input types must be declare earlier in the buffer than the usage holder.createErrorAnnotation(element, "Input type must be declared before use"); } } } } return; } // imports if (element instanceof JSGraphQLEndpointImportFileReference) { final PsiReference reference = element.getReference(); if (reference == null || reference.resolve() == null) { // file not found holder.createErrorAnnotation(element, "Cannot resolve file " + element.getText()); } final JSGraphQLEndpointImportDeclaration[] importDeclarations = PsiTreeUtil .getChildrenOfType(element.getContainingFile(), JSGraphQLEndpointImportDeclaration.class); if (importDeclarations != null) { final String importName = element.getText(); for (JSGraphQLEndpointImportDeclaration anImport : importDeclarations) { final JSGraphQLEndpointImportFileReference fileReference = anImport.getImportFileReference(); if (fileReference != null && fileReference != element) { if (Objects.equals(fileReference.getText(), importName)) { holder.createErrorAnnotation(element, element.getText() + " is imported more than once"); } } } } return; } // fields if (element instanceof JSGraphQLEndpointFieldDefinition) { final JSGraphQLEndpointFieldDefinition fieldDefinition = (JSGraphQLEndpointFieldDefinition) element; final PsiElement identifier = fieldDefinition.getProperty().getIdentifier(); final String fieldName = identifier.getText(); // duplicate fields final JSGraphQLEndpointFieldDefinitionSet fieldDefinitionSet = PsiTreeUtil.getParentOfType(element, JSGraphQLEndpointFieldDefinitionSet.class); if (fieldDefinitionSet != null) { final JSGraphQLEndpointFieldDefinition[] allFields = PsiTreeUtil .getChildrenOfType(fieldDefinitionSet, JSGraphQLEndpointFieldDefinition.class); if (allFields != null) { for (JSGraphQLEndpointFieldDefinition otherField : allFields) { if (otherField == fieldDefinition) { continue; } if (Objects.equals(otherField.getProperty().getIdentifier().getText(), fieldName)) { holder.createErrorAnnotation(identifier, "Field '" + identifier.getText() + "' is declared more than once"); } } } } // field return type must not be input inside non-input types if (fieldDefinition.getCompositeType() != null) { final JSGraphQLEndpointNamedTypePsiElement fieldReturnType = PsiTreeUtil.findChildOfType( fieldDefinition.getCompositeType(), JSGraphQLEndpointNamedTypePsiElement.class); if (fieldReturnType != null && PsiTreeUtil.getParentOfType(fieldDefinition, JSGraphQLEndpointInputObjectTypeDefinition.class) == null) { final PsiReference reference = fieldReturnType.getReference(); if (reference != null) { final PsiElement resolved = reference.resolve(); if (resolved != null && PsiTreeUtil.getParentOfType(resolved, JSGraphQLEndpointInputObjectTypeDefinition.class) != null) { holder.createErrorAnnotation(fieldReturnType, "Field return type '" + fieldReturnType.getText() + "' cannot be an input type"); } } } } final JSGraphQLEndpointFieldDefinition overridenField = getOverriddenField(fieldDefinition); if (overridenField != null) { if (!hasSameSignature(fieldDefinition, overridenField)) { final JSGraphQLEndpointInterfaceTypeDefinition overridenInterface = PsiTreeUtil .getParentOfType(overridenField, JSGraphQLEndpointInterfaceTypeDefinition.class); if (overridenInterface != null && overridenInterface.getNamedTypeDef() != null) { int endOffset = fieldDefinition.getProperty().getTextRange().getEndOffset(); if (fieldDefinition.getCompositeType() != null) { endOffset = fieldDefinition.getCompositeType().getTextRange().getEndOffset(); } holder.createErrorAnnotation( TextRange.create(fieldDefinition.getProperty().getTextOffset(), endOffset), "Field signature doesn't match the field it overrides in interface '" + overridenInterface.getNamedTypeDef().getText() + "'"); } } } return; } // argument types must be input, scalar, enum if (element instanceof JSGraphQLEndpointArgumentsDefinition) { final Collection<JSGraphQLEndpointNamedTypePsiElement> argumentTypes = PsiTreeUtil .findChildrenOfType(element, JSGraphQLEndpointNamedTypePsiElement.class); for (JSGraphQLEndpointNamedTypePsiElement argumentType : argumentTypes) { final PsiReference reference = argumentType.getReference(); if (reference != null) { final PsiElement resolved = reference.resolve(); if (resolved != null) { boolean valid = true; boolean builtInScalar = JSGraphQLScalars.SCALAR_TYPES.contains(resolved.getText()); if (!builtInScalar) { if (PsiTreeUtil.getParentOfType(resolved, JSGraphQLEndpointObjectTypeDefinition.class) != null) { valid = false; } else if (PsiTreeUtil.getParentOfType(resolved, JSGraphQLEndpointInterfaceTypeDefinition.class) != null) { valid = false; } else if (PsiTreeUtil.getParentOfType(resolved, JSGraphQLEndpointUnionTypeDefinition.class) != null) { valid = false; } if (!valid) { holder.createErrorAnnotation(argumentType, "Argument type '" + argumentType.getText() + "' must be one of the following: 'input', 'enum', 'scalar'"); } } } } } return; } // annotations if (element instanceof JSGraphQLEndpointAnnotation) { final JSGraphQLEndpointAnnotation annotation = (JSGraphQLEndpointAnnotation) element; final PsiElement atAnnotation = annotation.getAtAnnotation(); List<JSGraphQLSchemaEndpointAnnotation> annotations = holder.getCurrentAnnotationSession() .getUserData(ANNOTATIONS); if (annotations == null) { final JSGraphQLConfigurationProvider configurationProvider = JSGraphQLConfigurationProvider .getService(element.getProject()); annotations = configurationProvider.getEndpointAnnotations(); holder.getCurrentAnnotationSession().putUserData(ANNOTATIONS, annotations); } boolean knownAnnotation = false; for (JSGraphQLSchemaEndpointAnnotation endpointAnnotation : annotations) { if (Objects.equals("@" + endpointAnnotation.name, atAnnotation.getText())) { knownAnnotation = true; // check argument names and types if (endpointAnnotation.arguments != null) { final Map<String, String> argumentNameToType = endpointAnnotation.arguments.stream() .collect( Collectors.toMap(a -> a.name, a -> Optional.ofNullable(a.type).orElse(""))); if (annotation.getAnnotationArguments() != null && annotation.getAnnotationArguments().getNamedAnnotationArguments() != null) { for (JSGraphQLEndpointNamedAnnotationArgument namedArgument : annotation .getAnnotationArguments().getNamedAnnotationArguments() .getNamedAnnotationArgumentList()) { final String type = argumentNameToType.get(namedArgument.getIdentifier().getText()); if (type == null) { holder.createErrorAnnotation(namedArgument.getIdentifier(), "Unknown argument '" + namedArgument.getIdentifier().getText() + "'"); } else { if (namedArgument.getAnnotationArgumentValue() != null) { switch (type) { case "String": if (namedArgument.getAnnotationArgumentValue() .getQuotedString() == null) { holder.createErrorAnnotation( namedArgument.getAnnotationArgumentValue(), "String value expected"); } break; case "Boolean": { final PsiElement firstChild = namedArgument.getAnnotationArgumentValue() .getFirstChild(); if (firstChild != null) { if (!TokenSet .create(JSGraphQLEndpointTokenTypes.TRUE, JSGraphQLEndpointTokenTypes.FALSE) .contains(firstChild.getNode().getElementType())) { holder.createErrorAnnotation( namedArgument.getAnnotationArgumentValue(), "true or false expected"); } } break; } case "Number": { final PsiElement firstChild = namedArgument.getAnnotationArgumentValue() .getFirstChild(); if (firstChild != null) { if (!JSGraphQLEndpointTokenTypes.NUMBER .equals(firstChild.getNode().getElementType())) { holder.createErrorAnnotation( namedArgument.getAnnotationArgumentValue(), "Number expected"); } } break; } } } } } } } break; } } if (!knownAnnotation) { holder.createErrorAnnotation(atAnnotation, "Unknown annotation '" + atAnnotation.getText() + "'. Annotations are specified in the 'schema.endpoint.annotations' array in graphql.config.json."); } return; } // duplicate types with same name if (element instanceof JSGraphQLEndpointNamedTypeDef) { final JSGraphQLEndpointNamedTypeDef namedTypeDef = (JSGraphQLEndpointNamedTypeDef) element; // current file annotateRedeclarations(namedTypeDef, element.getContainingFile(), KNOWN_DEFINITIONS, holder); } } private void annotateRedeclarations(@NotNull JSGraphQLEndpointNamedTypeDef element, PsiFile importingFile, Key<Multimap<String, JSGraphQLEndpointNamedTypeDefinition>> key, @NotNull AnnotationHolder holder) { final Key<Boolean> annotationKey = Key .create(element.getContainingFile().getName() + ":" + element.getTextOffset()); if (holder.getCurrentAnnotationSession().getUserData(annotationKey) == Boolean.TRUE) { // already annotated about redeclaration return; } Multimap<String, JSGraphQLEndpointNamedTypeDefinition> knownDefinitionsByName = holder .getCurrentAnnotationSession().getUserData(key); if (knownDefinitionsByName == null) { knownDefinitionsByName = HashMultimap.create(); for (JSGraphQLEndpointNamedTypeDefinition definition : JSGraphQLEndpointPsiUtil .getKnownDefinitions(importingFile, JSGraphQLEndpointNamedTypeDefinition.class, true, null)) { if (definition.getNamedTypeDef() != null) { knownDefinitionsByName.put(definition.getNamedTypeDef().getText(), definition); } } } final String typeName = element.getText(); final Collection<JSGraphQLEndpointNamedTypeDefinition> typesWithSameName = knownDefinitionsByName .get(typeName); if (typesWithSameName != null && typesWithSameName.size() > 1) { final Set<String> files = typesWithSameName.stream() .map(t -> "'" + t.getContainingFile().getName() + "'").collect(Collectors.toSet()); holder.createErrorAnnotation(element, "'" + typeName + "' is redeclared in " + StringUtils.join(files, ", ")); holder.getCurrentAnnotationSession().putUserData(annotationKey, Boolean.TRUE); } } private JSGraphQLEndpointFieldDefinition getOverriddenField(JSGraphQLEndpointFieldDefinition override) { final String propertyName = override.getProperty().getIdentifier().getText(); final JSGraphQLEndpointObjectTypeDefinition typeDefinition = PsiTreeUtil.getParentOfType(override, JSGraphQLEndpointObjectTypeDefinition.class); if (typeDefinition != null) { final JSGraphQLEndpointImplementsInterfaces implementsInterfaces = PsiTreeUtil .findChildOfType(typeDefinition, JSGraphQLEndpointImplementsInterfaces.class); if (implementsInterfaces != null) { for (JSGraphQLEndpointNamedType namedType : implementsInterfaces.getNamedTypeList()) { final PsiReference reference = namedType.getReference(); if (reference != null) { final PsiElement interfaceTypeName = reference.resolve(); if (interfaceTypeName != null) { final JSGraphQLEndpointInterfaceTypeDefinition interfaceTypeDefinition = PsiTreeUtil .getParentOfType(interfaceTypeName, JSGraphQLEndpointInterfaceTypeDefinition.class); if (interfaceTypeDefinition != null) { for (JSGraphQLEndpointProperty property : PsiTreeUtil.findChildrenOfType( interfaceTypeDefinition, JSGraphQLEndpointProperty.class)) { if (property.getIdentifier().getText().equals(propertyName)) { return PsiTreeUtil.getParentOfType(property, JSGraphQLEndpointFieldDefinition.class); } } } } } } } } return null; } /** * Compares the signatures of two fields, ignoring comments, whitespace, and annotations */ private boolean hasSameSignature(JSGraphQLEndpointFieldDefinition override, JSGraphQLEndpointFieldDefinition toImplement) { final StringBuilder toImplementSignature = new StringBuilder(); final StringBuilder overrideSignature = new StringBuilder(); final Ref<StringBuilder> sb = new Ref<>(); final PsiElementVisitor visitor = new PsiRecursiveElementVisitor() { @Override public void visitElement(PsiElement element) { if (element instanceof JSGraphQLEndpointAnnotation) { return; } if (element instanceof PsiWhiteSpace) { return; } if (element instanceof PsiComment) { return; } if (element instanceof LeafPsiElement) { sb.get().append(element.getText()).append(" "); } super.visitElement(element); } }; sb.set(overrideSignature); override.accept(visitor); sb.set(toImplementSignature); toImplement.accept(visitor); return toImplementSignature.toString().equals(overrideSignature.toString()); } }