diff --git a/src/jmh/java/benchmark/OverlappingFieldValidationBenchmark.java b/src/jmh/java/benchmark/OverlappingFieldValidationBenchmark.java index 837a0f639e..8dd5eff964 100644 --- a/src/jmh/java/benchmark/OverlappingFieldValidationBenchmark.java +++ b/src/jmh/java/benchmark/OverlappingFieldValidationBenchmark.java @@ -8,11 +8,11 @@ import graphql.schema.GraphQLSchema; import graphql.schema.idl.SchemaGenerator; import graphql.validation.LanguageTraversal; -import graphql.validation.RulesVisitor; +import graphql.validation.OperationValidationRule; +import graphql.validation.OperationValidator; import graphql.validation.ValidationContext; import graphql.validation.ValidationError; import graphql.validation.ValidationErrorCollector; -import graphql.validation.rules.OverlappingFieldsCanBeMerged; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -25,7 +25,6 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -78,9 +77,10 @@ private List validateQuery(GraphQLSchema schema, Document docum ValidationErrorCollector errorCollector = new ValidationErrorCollector(); I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH); ValidationContext validationContext = new ValidationContext(schema, document, i18n); - OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector); + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + rule -> rule == OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED); LanguageTraversal languageTraversal = new LanguageTraversal(); - languageTraversal.traverse(document, new RulesVisitor(validationContext, Collections.singletonList(overlappingFieldsCanBeMerged))); + languageTraversal.traverse(document, operationValidator); return errorCollector.getErrors(); } } diff --git a/src/jmh/java/performance/OverlappingFieldValidationPerformance.java b/src/jmh/java/performance/OverlappingFieldValidationPerformance.java index 37fab1cf81..f9b7207939 100644 --- a/src/jmh/java/performance/OverlappingFieldValidationPerformance.java +++ b/src/jmh/java/performance/OverlappingFieldValidationPerformance.java @@ -9,11 +9,11 @@ import graphql.schema.GraphQLSchema; import graphql.schema.idl.SchemaGenerator; import graphql.validation.LanguageTraversal; -import graphql.validation.RulesVisitor; +import graphql.validation.OperationValidationRule; +import graphql.validation.OperationValidator; import graphql.validation.ValidationContext; import graphql.validation.ValidationError; import graphql.validation.ValidationErrorCollector; -import graphql.validation.rules.OverlappingFieldsCanBeMerged; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -27,7 +27,6 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -151,9 +150,10 @@ private List validateQuery(GraphQLSchema schema, Document docum ValidationErrorCollector errorCollector = new ValidationErrorCollector(); I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH); ValidationContext validationContext = new ValidationContext(schema, document, i18n); - OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector); + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + rule -> rule == OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED); LanguageTraversal languageTraversal = new LanguageTraversal(); - languageTraversal.traverse(document, new RulesVisitor(validationContext, Collections.singletonList(overlappingFieldsCanBeMerged))); + languageTraversal.traverse(document, operationValidator); Assert.assertTrue(errorCollector.getErrors().size() == 0); return errorCollector.getErrors(); } diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index 1d99ad0377..fc2eb94b87 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -25,6 +25,7 @@ import graphql.execution.preparsed.PreparsedDocumentProvider; import graphql.language.Document; import graphql.schema.GraphQLSchema; +import graphql.validation.OperationValidationRule; import graphql.validation.ValidationError; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullUnmarked; @@ -598,8 +599,8 @@ private List validate(ExecutionInput executionInput, Document d InstrumentationContext> validationCtx = nonNullCtx(instrumentation.beginValidation(new InstrumentationValidationParameters(executionInput, document, graphQLSchema), instrumentationState)); validationCtx.onDispatched(); - Predicate> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); - Locale locale = executionInput.getLocale(); + Predicate validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true); + Locale locale = executionInput.getLocale() != null ? executionInput.getLocale() : Locale.getDefault(); List validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, locale); validationCtx.onCompleted(validationErrors, null); diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index 0e7f1a2b21..cece9159fb 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -6,8 +6,10 @@ import graphql.parser.ParserEnvironment; import graphql.parser.ParserOptions; import graphql.schema.GraphQLSchema; +import graphql.validation.OperationValidationRule; import graphql.validation.ValidationError; import graphql.validation.Validator; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import java.util.List; @@ -28,7 +30,7 @@ public class ParseAndValidate { /** * This {@link GraphQLContext} hint can be used to supply a Predicate to the Validator so that certain rules can be skipped. - *

+ * * This is an internal capability that you should use at your own risk. While we intend for this to be present for some time, the validation * rule class names may change, as may this mechanism. */ @@ -44,7 +46,7 @@ public class ParseAndValidate { * * @return a result object that indicates how this operation went */ - public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchema, ExecutionInput executionInput) { + public static ParseAndValidateResult parseAndValidate(@NonNull GraphQLSchema graphQLSchema, @NonNull ExecutionInput executionInput) { ParseAndValidateResult result = parse(executionInput); if (!result.isFailure()) { List errors = validate(graphQLSchema, assertNotNull(result.getDocument(), "Parse result document cannot be null when parse succeeded"), executionInput.getLocale()); @@ -60,7 +62,7 @@ public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchem * * @return a result object that indicates how this operation went */ - public static ParseAndValidateResult parse(ExecutionInput executionInput) { + public static ParseAndValidateResult parse(@NonNull ExecutionInput executionInput) { try { // // we allow the caller to specify new parser options by context @@ -68,7 +70,7 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) { // we use the query parser options by default if they are not specified parserOptions = ofNullable(parserOptions).orElse(ParserOptions.getDefaultOperationParserOptions()); Parser parser = new Parser(); - Locale locale = executionInput.getLocale(); + Locale locale = executionInput.getLocale() == null ? Locale.getDefault() : executionInput.getLocale(); ParserEnvironment parserEnvironment = ParserEnvironment.newParserEnvironment() .document(executionInput.getQuery()).parserOptions(parserOptions) .locale(locale) @@ -89,8 +91,8 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) { * * @return a result object that indicates how this operation went */ - public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument, Locale locale) { - return validate(graphQLSchema, parsedDocument, ruleClass -> true, locale); + public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Locale locale) { + return validate(graphQLSchema, parsedDocument, rule -> true, locale); } /** @@ -101,8 +103,8 @@ public static List validate(GraphQLSchema graphQLSchema, Docume * * @return a result object that indicates how this operation went */ - public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument) { - return validate(graphQLSchema, parsedDocument, ruleClass -> true, Locale.getDefault()); + public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument) { + return validate(graphQLSchema, parsedDocument, rule -> true, Locale.getDefault()); } /** @@ -115,7 +117,7 @@ public static List validate(GraphQLSchema graphQLSchema, Docume * * @return a result object that indicates how this operation went */ - public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate> rulePredicate, Locale locale) { + public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Predicate rulePredicate, @NonNull Locale locale) { Validator validator = new Validator(); return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, locale); } @@ -129,7 +131,7 @@ public static List validate(GraphQLSchema graphQLSchema, Docume * * @return a result object that indicates how this operation went */ - public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate> rulePredicate) { + public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Predicate rulePredicate) { Validator validator = new Validator(); return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, Locale.getDefault()); } diff --git a/src/main/java/graphql/execution/MergedField.java b/src/main/java/graphql/execution/MergedField.java index 59238b1c90..e15404262a 100644 --- a/src/main/java/graphql/execution/MergedField.java +++ b/src/main/java/graphql/execution/MergedField.java @@ -61,7 +61,7 @@ * * These examples make clear that you need to consider all merged fields together to have the full picture. *

- * The actual logic when fields can be successfully merged together is implemented in {#graphql.validation.rules.OverlappingFieldsCanBeMerged} + * The actual logic when fields can be successfully merged together is implemented in {#graphql.validation.OperationValidator} */ @PublicApi @NullMarked diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 5150eee5a4..4c6aad6d27 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -168,7 +168,7 @@ private static boolean compareWithoutChildren(ExecutableNormalizedField one, Exe return true; } - // copied from graphql.validation.rules.OverlappingFieldsCanBeMerged + // copied from graphql.validation.OperationValidator private static boolean sameArguments(List arguments1, List arguments2) { if (arguments1.size() != arguments2.size()) { return false; diff --git a/src/main/java/graphql/normalized/nf/NormalizedFieldsMerger.java b/src/main/java/graphql/normalized/nf/NormalizedFieldsMerger.java index 4261aa5307..fa61e36cea 100644 --- a/src/main/java/graphql/normalized/nf/NormalizedFieldsMerger.java +++ b/src/main/java/graphql/normalized/nf/NormalizedFieldsMerger.java @@ -166,7 +166,7 @@ private static boolean compareWithoutChildren(NormalizedField one, NormalizedFie return true; } - // copied from graphql.validation.rules.OverlappingFieldsCanBeMerged + // copied from graphql.validation.OperationValidator private static boolean sameArguments(List arguments1, List arguments2) { if (arguments1.size() != arguments2.size()) { return false; diff --git a/src/main/java/graphql/validation/AbstractRule.java b/src/main/java/graphql/validation/AbstractRule.java deleted file mode 100644 index fc5cf07771..0000000000 --- a/src/main/java/graphql/validation/AbstractRule.java +++ /dev/null @@ -1,211 +0,0 @@ -package graphql.validation; - - -import graphql.Internal; -import graphql.i18n.I18nMsg; -import graphql.language.Argument; -import graphql.language.Directive; -import graphql.language.Document; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.Node; -import graphql.language.ObjectValue; -import graphql.language.OperationDefinition; -import graphql.language.SelectionSet; -import graphql.language.SourceLocation; -import graphql.language.TypeName; -import graphql.language.VariableDefinition; -import graphql.language.VariableReference; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static graphql.validation.ValidationError.newValidationError; -import static java.lang.System.arraycopy; - -@Internal -public class AbstractRule { - - private final ValidationContext validationContext; - private final ValidationErrorCollector validationErrorCollector; - private final ValidationUtil validationUtil; - private boolean visitFragmentSpreads; - - public AbstractRule(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - this.validationContext = validationContext; - this.validationErrorCollector = validationErrorCollector; - this.validationUtil = new ValidationUtil(); - } - - public boolean isVisitFragmentSpreads() { - return visitFragmentSpreads; - } - - public void setVisitFragmentSpreads(boolean visitFragmentSpreads) { - this.visitFragmentSpreads = visitFragmentSpreads; - } - - public ValidationUtil getValidationUtil() { - return validationUtil; - } - - public void addError(ValidationErrorType validationErrorType, Collection> locations, String description) { - List locationList = new ArrayList<>(); - for (Node node : locations) { - locationList.add(node.getSourceLocation()); - } - addError(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocations(locationList) - .description(description)); - } - - public void addError(ValidationErrorType validationErrorType, SourceLocation location, String description) { - addError(newValidationError() - .validationErrorType(validationErrorType) - .sourceLocation(location) - .description(description)); - } - - public void addError(ValidationError.Builder validationError) { - validationErrorCollector.addError(validationError.queryPath(getQueryPath()).build()); - } - - public List getErrors() { - return validationErrorCollector.getErrors(); - } - - public ValidationContext getValidationContext() { - return validationContext; - } - - public ValidationErrorCollector getValidationErrorCollector() { - return validationErrorCollector; - } - - protected List getQueryPath() { - return validationContext.getQueryPath(); - } - - /** - * Verifies if the experimental API key is enabled - * @param key to be checked - * @return if the experimental API key is enabled - */ - protected Boolean isExperimentalApiKeyEnabled(String key) { - return (getValidationContext() != null && - getValidationContext().getGraphQLContext() != null || - getValidationContext().getGraphQLContext().get(key) != null || - ((Boolean) getValidationContext().getGraphQLContext().get(key))); - } - /** - * Creates an I18n message using the {@link graphql.i18n.I18nMsg} - * - * @param validationErrorType the type of validation failure - * @param i18nMsg the i18n message object - * - * @return the formatted I18n message - */ - public String i18n(ValidationErrorType validationErrorType, I18nMsg i18nMsg) { - return i18n(validationErrorType, i18nMsg.getMsgKey(), i18nMsg.getMsgArguments()); - } - - /** - * Creates an I18N message using the key and arguments - * - * @param validationErrorType the type of validation failure - * @param msgKey the key in the underlying message bundle - * @param msgArgs the message arguments - * - * @return the formatted I18N message - */ - public String i18n(ValidationErrorType validationErrorType, String msgKey, Object... msgArgs) { - Object[] params = new Object[msgArgs.length + 1]; - params[0] = mkTypeAndPath(validationErrorType); - arraycopy(msgArgs, 0, params, 1, msgArgs.length); - - return validationContext.i18n(msgKey, params); - } - - private String mkTypeAndPath(ValidationErrorType validationErrorType) { - List queryPath = getQueryPath(); - StringBuilder sb = new StringBuilder(); - sb.append(validationErrorType); - if (queryPath != null) { - sb.append("@[").append(String.join("/", queryPath)).append("]"); - } - return sb.toString(); - } - - public void checkDocument(Document document) { - - } - - public void checkArgument(Argument argument) { - - } - - public void checkTypeName(TypeName typeName) { - - } - - public void checkVariableDefinition(VariableDefinition variableDefinition) { - - } - - public void checkField(Field field) { - - } - - public void checkInlineFragment(InlineFragment inlineFragment) { - - } - - public void checkDirective(Directive directive, List ancestors) { - - } - - public void checkFragmentSpread(FragmentSpread fragmentSpread) { - - } - - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - - } - - public void checkOperationDefinition(OperationDefinition operationDefinition) { - - } - - public void leaveOperationDefinition(OperationDefinition operationDefinition) { - - } - - public void checkSelectionSet(SelectionSet selectionSet) { - - } - - public void leaveSelectionSet(SelectionSet selectionSet) { - - } - - public void checkVariable(VariableReference variableReference) { - - } - - public void documentFinished(Document document) { - - } - - public void checkObjectValue(ObjectValue objectValue) { - - } - - @Override - public String toString() { - return "Rule{" + validationContext + "}"; - } -} diff --git a/src/main/java/graphql/validation/OperationValidationRule.java b/src/main/java/graphql/validation/OperationValidationRule.java new file mode 100644 index 0000000000..d5645aa5af --- /dev/null +++ b/src/main/java/graphql/validation/OperationValidationRule.java @@ -0,0 +1,187 @@ +package graphql.validation; + +import graphql.PublicApi; +import org.jspecify.annotations.NullMarked; + +/** + * Enumerates the individual validation rules that can be applied to a GraphQL operation document. + * Each value corresponds to a validation rule defined in the GraphQL specification. + * + *

This enum is used with {@link OperationValidator} to selectively enable or disable + * individual validation rules via a {@code Predicate}. + * + *

Rule Categories by Traversal Behavior

+ * + *

The {@link OperationValidator} tracks two independent state variables during traversal: + * {@code fragmentRetraversalDepth} (0 = primary traversal, >0 = inside fragment retraversal) + * and {@code operationScope} (true = inside an operation, false = outside). + * + *

This creates three possible traversal states: + *

+ * ┌────────────────────────────────────┬──────────────────────────────────────────────────┐
+ * │ State                              │ When                                             │
+ * ├────────────────────────────────────┼──────────────────────────────────────────────────┤
+ * │ depth=0, operationScope=false      │ Fragment definitions at document level           │
+ * │ depth=0, operationScope=true       │ Nodes directly inside an operation               │
+ * │ depth>0, operationScope=true       │ Nodes inside fragments reached via spread        │
+ * └────────────────────────────────────┴──────────────────────────────────────────────────┘
+ * 
+ * + *

Rules are categorized by which states they run in: + *

+ * ┌──────────────────────┬──────────────────────┬─────────────────────┬─────────────────────┐
+ * │ Rule Category        │ depth=0              │ depth=0             │ depth>0             │
+ * │                      │ operationScope=false │ operationScope=true │ operationScope=true │
+ * ├──────────────────────┼──────────────────────┼─────────────────────┼─────────────────────┤
+ * │ Document-Level Rules │         RUN          │         RUN         │        SKIP         │
+ * │ Operation-Scoped     │        SKIP          │         RUN         │         RUN         │
+ * └──────────────────────┴──────────────────────┴─────────────────────┴─────────────────────┘
+ * 
+ * + *

Document-Level Rules

+ *

Condition: {@code fragmentRetraversalDepth == 0} + *

Validates each AST node exactly once during primary traversal. Skips during fragment + * retraversal to avoid duplicate errors (fragments are validated at document level). + *

    + *
  • {@link #EXECUTABLE_DEFINITIONS} - only executable definitions allowed
  • + *
  • {@link #ARGUMENTS_OF_CORRECT_TYPE} - argument values match declared types
  • + *
  • {@link #FIELDS_ON_CORRECT_TYPE} - fields exist on parent type
  • + *
  • {@link #FRAGMENTS_ON_COMPOSITE_TYPE} - fragments on object/interface/union types
  • + *
  • {@link #KNOWN_ARGUMENT_NAMES} - arguments are defined on field/directive
  • + *
  • {@link #KNOWN_DIRECTIVES} - directives are defined in schema
  • + *
  • {@link #KNOWN_FRAGMENT_NAMES} - fragment spreads reference defined fragments
  • + *
  • {@link #KNOWN_TYPE_NAMES} - type references exist in schema
  • + *
  • {@link #NO_FRAGMENT_CYCLES} - fragments do not form cycles
  • + *
  • {@link #NO_UNUSED_FRAGMENTS} - all fragments are used by operations
  • + *
  • {@link #OVERLAPPING_FIELDS_CAN_BE_MERGED} - fields with same response key are mergeable
  • + *
  • {@link #POSSIBLE_FRAGMENT_SPREADS} - fragment type conditions overlap with parent
  • + *
  • {@link #PROVIDED_NON_NULL_ARGUMENTS} - required arguments are provided
  • + *
  • {@link #SCALAR_LEAVES} - scalar fields have no subselections, composites require them
  • + *
  • {@link #VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE} - variable defaults match type
  • + *
  • {@link #VARIABLES_ARE_INPUT_TYPES} - variables use input types only
  • + *
  • {@link #LONE_ANONYMOUS_OPERATION} - anonymous operations are alone in document
  • + *
  • {@link #UNIQUE_OPERATION_NAMES} - operation names are unique
  • + *
  • {@link #UNIQUE_FRAGMENT_NAMES} - fragment names are unique
  • + *
  • {@link #UNIQUE_DIRECTIVE_NAMES_PER_LOCATION} - non-repeatable directives appear once
  • + *
  • {@link #UNIQUE_ARGUMENT_NAMES} - argument names are unique per field/directive
  • + *
  • {@link #UNIQUE_VARIABLE_NAMES} - variable names are unique per operation
  • + *
  • {@link #SUBSCRIPTION_UNIQUE_ROOT_FIELD} - subscriptions have single root field
  • + *
  • {@link #UNIQUE_OBJECT_FIELD_NAME} - input object fields are unique
  • + *
  • {@link #DEFER_DIRECTIVE_LABEL} - defer labels are unique strings
  • + *
  • {@link #KNOWN_OPERATION_TYPES} - schema supports the operation type
  • + *
+ * + *

Operation-Scoped Rules

+ *

Condition: {@code operationScope == true} + *

Tracks state across an entire operation, following fragment spreads to see all code paths. + * Skips outside operations (e.g., fragment definitions at document level) where there is no + * operation context to validate against. + *

    + *
  • {@link #NO_UNDEFINED_VARIABLES} - all variable references are defined in operation
  • + *
  • {@link #NO_UNUSED_VARIABLES} - all defined variables are used somewhere
  • + *
  • {@link #VARIABLE_TYPES_MATCH} - variable types match usage location types
  • + *
  • {@link #DEFER_DIRECTIVE_ON_ROOT_LEVEL} - defer not on mutation/subscription root
  • + *
  • {@link #DEFER_DIRECTIVE_ON_VALID_OPERATION} - defer not in subscriptions
  • + *
+ * + *

See {@link OperationValidator} class documentation for a detailed traversal example. + * + * @see OperationValidator + */ +@PublicApi +@NullMarked +public enum OperationValidationRule { + + /** Only executable definitions (operations and fragments) are allowed. */ + EXECUTABLE_DEFINITIONS, + + /** Argument values must be compatible with their declared types. */ + ARGUMENTS_OF_CORRECT_TYPE, + + /** Fields must exist on the parent type. */ + FIELDS_ON_CORRECT_TYPE, + + /** Fragment type conditions must be on composite types (object, interface, union). */ + FRAGMENTS_ON_COMPOSITE_TYPE, + + /** Arguments must be defined on the field or directive. */ + KNOWN_ARGUMENT_NAMES, + + /** Directives must be defined in the schema and used in valid locations. */ + KNOWN_DIRECTIVES, + + /** Fragment spreads must reference defined fragments. */ + KNOWN_FRAGMENT_NAMES, + + /** Type references must exist in the schema. */ + KNOWN_TYPE_NAMES, + + /** Fragments must not form cycles through spreads. */ + NO_FRAGMENT_CYCLES, + + /** All defined fragments must be used by at least one operation. */ + NO_UNUSED_FRAGMENTS, + + /** Fields with the same response key must be mergeable. */ + OVERLAPPING_FIELDS_CAN_BE_MERGED, + + /** Fragment type conditions must overlap with the parent type. */ + POSSIBLE_FRAGMENT_SPREADS, + + /** Required (non-null without default) arguments must be provided. */ + PROVIDED_NON_NULL_ARGUMENTS, + + /** Scalar fields must not have subselections; composite fields must have them. */ + SCALAR_LEAVES, + + /** Variable default values must match the variable type. */ + VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE, + + /** Variables must be declared with input types (scalars, enums, input objects). */ + VARIABLES_ARE_INPUT_TYPES, + + /** Anonymous operations must be the only operation in the document. */ + LONE_ANONYMOUS_OPERATION, + + /** Operation names must be unique within the document. */ + UNIQUE_OPERATION_NAMES, + + /** Fragment names must be unique within the document. */ + UNIQUE_FRAGMENT_NAMES, + + /** Non-repeatable directives must appear at most once per location. */ + UNIQUE_DIRECTIVE_NAMES_PER_LOCATION, + + /** Argument names must be unique within a field or directive. */ + UNIQUE_ARGUMENT_NAMES, + + /** Variable names must be unique within an operation. */ + UNIQUE_VARIABLE_NAMES, + + /** Subscriptions must have exactly one root field (not introspection). */ + SUBSCRIPTION_UNIQUE_ROOT_FIELD, + + /** Input object field names must be unique. */ + UNIQUE_OBJECT_FIELD_NAME, + + /** Defer directive labels must be unique static strings. */ + DEFER_DIRECTIVE_LABEL, + + /** The schema must support the operation type (query/mutation/subscription). */ + KNOWN_OPERATION_TYPES, + + /** All variable references must be defined in the operation. Requires fragment traversal. */ + NO_UNDEFINED_VARIABLES, + + /** All defined variables must be used somewhere in the operation. Requires fragment traversal. */ + NO_UNUSED_VARIABLES, + + /** Variable types must be compatible with usage location types. Requires fragment traversal. */ + VARIABLE_TYPES_MATCH, + + /** Defer directive must not be on mutation or subscription root level. Requires operation context. */ + DEFER_DIRECTIVE_ON_ROOT_LEVEL, + + /** Defer directive must not be used in subscription operations. Requires operation context. */ + DEFER_DIRECTIVE_ON_VALID_OPERATION, +} diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java new file mode 100644 index 0000000000..3e32d5d67c --- /dev/null +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -0,0 +1,1743 @@ +package graphql.validation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import graphql.Assert; +import graphql.Directives; +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.execution.FieldCollector; +import graphql.execution.FieldCollectorParameters; +import graphql.execution.MergedField; +import graphql.execution.MergedSelectionSet; +import graphql.execution.TypeFromAST; +import graphql.execution.ValuesResolver; +import graphql.i18n.I18nMsg; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.Argument; +import graphql.language.AstComparator; +import graphql.language.BooleanValue; +import graphql.language.Definition; +import graphql.language.Directive; +import graphql.language.DirectiveDefinition; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Node; +import graphql.language.NodeUtil; +import graphql.language.NullValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.OperationDefinition; +import graphql.language.SchemaDefinition; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.language.SourceLocation; +import graphql.language.StringValue; +import graphql.language.TypeDefinition; +import graphql.language.TypeName; +import graphql.language.Value; +import graphql.language.VariableDefinition; +import graphql.language.VariableReference; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLCompositeType; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.GraphQLUnionType; +import graphql.schema.GraphQLUnmodifiedType; +import graphql.schema.InputValueWithState; +import graphql.util.StringKit; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import static graphql.collect.ImmutableKit.addToList; +import static graphql.collect.ImmutableKit.emptyList; +import static graphql.schema.GraphQLTypeUtil.isEnum; +import static graphql.schema.GraphQLTypeUtil.isInput; +import static graphql.schema.GraphQLTypeUtil.isLeaf; +import static graphql.schema.GraphQLTypeUtil.isList; +import static graphql.schema.GraphQLTypeUtil.isNonNull; +import static graphql.schema.GraphQLTypeUtil.isNotWrapped; +import static graphql.schema.GraphQLTypeUtil.isNullable; +import static graphql.schema.GraphQLTypeUtil.isScalar; +import static graphql.schema.GraphQLTypeUtil.simplePrint; +import static graphql.schema.GraphQLTypeUtil.unwrapAll; +import static graphql.schema.GraphQLTypeUtil.unwrapOne; + +import static graphql.validation.ValidationError.newValidationError; +import static graphql.validation.ValidationErrorType.BadValueForDefaultArg; +import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; +import static graphql.validation.ValidationErrorType.DuplicateDirectiveName; +import static graphql.validation.ValidationErrorType.DuplicateFragmentName; +import static graphql.validation.ValidationErrorType.DuplicateIncrementalLabel; +import static graphql.validation.ValidationErrorType.DuplicateOperationName; +import static graphql.validation.ValidationErrorType.DuplicateVariableName; +import static graphql.validation.ValidationErrorType.FieldUndefined; +import static graphql.validation.ValidationErrorType.FieldsConflict; +import static graphql.validation.ValidationErrorType.FragmentCycle; +import static graphql.validation.ValidationErrorType.FragmentTypeConditionInvalid; +import static graphql.validation.ValidationErrorType.InlineFragmentTypeConditionInvalid; +import static graphql.validation.ValidationErrorType.InvalidFragmentType; +import static graphql.validation.ValidationErrorType.LoneAnonymousOperationViolation; +import static graphql.validation.ValidationErrorType.MisplacedDirective; +import static graphql.validation.ValidationErrorType.MissingDirectiveArgument; +import static graphql.validation.ValidationErrorType.MissingFieldArgument; +import static graphql.validation.ValidationErrorType.NonExecutableDefinition; +import static graphql.validation.ValidationErrorType.NonInputTypeOnVariable; +import static graphql.validation.ValidationErrorType.NullValueForNonNullArgument; +import static graphql.validation.ValidationErrorType.SubselectionNotAllowed; +import static graphql.validation.ValidationErrorType.SubselectionRequired; +import static graphql.validation.ValidationErrorType.SubscriptionIntrospectionRootField; +import static graphql.validation.ValidationErrorType.SubscriptionMultipleRootFields; +import static graphql.validation.ValidationErrorType.UndefinedFragment; +import static graphql.validation.ValidationErrorType.UndefinedVariable; +import static graphql.validation.ValidationErrorType.UnknownArgument; +import static graphql.validation.ValidationErrorType.UnknownDirective; +import static graphql.validation.ValidationErrorType.UnknownOperation; +import static graphql.validation.ValidationErrorType.UnknownType; +import static graphql.validation.ValidationErrorType.UnusedFragment; +import static graphql.validation.ValidationErrorType.UnusedVariable; +import static graphql.validation.ValidationErrorType.VariableTypeMismatch; +import static graphql.validation.ValidationErrorType.WrongType; +import static java.lang.System.arraycopy; +import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; +import static graphql.validation.ValidationErrorType.UniqueObjectFieldName; + +/** + * Consolidated operation validator that implements all GraphQL validation rules + * from the specification. Replaces the former 31 separate rule classes and the + * RulesVisitor dispatch layer. + * + *

Traversal Model

+ * + *

This validator tracks two independent state variables during traversal: + * + *

    + *
  • {@code fragmentRetraversalDepth} - Tracks whether we are in the primary document + * traversal ({@code == 0}) or inside a manual re-traversal of a fragment via a spread + * ({@code > 0}).
  • + *
  • {@code operationScope} - Tracks whether we are currently inside an operation + * definition ({@code true}) or outside of any operation ({@code false}).
  • + *
+ * + *

Traversal States

+ * + *

These two variables create four possible states, but only three actually occur: + * + *

+ * ┌────────────────────────────────────────┬────────────────────────────────────────────────┐
+ * │ State                                  │ Description                                    │
+ * ├────────────────────────────────────────┼────────────────────────────────────────────────┤
+ * │ depth=0, operationScope=false          │ PRIMARY TRAVERSAL, OUTSIDE OPERATION           │
+ * │                                        │ Visiting document root or fragment definitions │
+ * │                                        │ after all operations have been processed.      │
+ * │                                        │ Example: FragmentDefinition at document level  │
+ * ├────────────────────────────────────────┼────────────────────────────────────────────────┤
+ * │ depth=0, operationScope=true           │ PRIMARY TRAVERSAL, INSIDE OPERATION            │
+ * │                                        │ Visiting nodes directly within an operation.   │
+ * │                                        │ Example: Field, InlineFragment in operation    │
+ * ├────────────────────────────────────────┼────────────────────────────────────────────────┤
+ * │ depth>0, operationScope=true           │ FRAGMENT RETRAVERSAL, INSIDE OPERATION         │
+ * │                                        │ Manually traversing into a fragment via spread.│
+ * │                                        │ Example: Nodes reached via ...FragmentName     │
+ * ├────────────────────────────────────────┼────────────────────────────────────────────────┤
+ * │ depth>0, operationScope=false          │ NEVER OCCURS                                   │
+ * │                                        │ Retraversal only happens within an operation.  │
+ * └────────────────────────────────────────┴────────────────────────────────────────────────┘
+ * 
+ * + *

Rule Categories

+ * + *

Rules are categorized by which states they should run in: + * + *

+ * ┌──────────────────────┬──────────────────────┬─────────────────────┬─────────────────────┐
+ * │ Rule Category        │ depth=0              │ depth=0             │ depth>0             │
+ * │                      │ operationScope=false │ operationScope=true │ operationScope=true │
+ * ├──────────────────────┼──────────────────────┼─────────────────────┼─────────────────────┤
+ * │ Document-Level Rules │         RUN          │        RUN          │        SKIP         │
+ * ├──────────────────────┼──────────────────────┼─────────────────────┼─────────────────────┤
+ * │ Operation-Scoped     │        SKIP          │        RUN          │        RUN          │
+ * │ Rules                │                      │                     │                     │
+ * └──────────────────────┴──────────────────────┴─────────────────────┴─────────────────────┘
+ * 
+ * + *

Document-Level Rules

+ *

Check: {@code fragmentRetraversalDepth == 0} (via {@link #shouldRunDocumentLevelRules()}) + *

Purpose: Validate each AST node exactly once. Skip during fragment retraversal to avoid + * duplicate errors (the fragment was already validated at document level). + *

Examples: {@code FieldsOnCorrectType}, {@code UniqueFragmentNames}, {@code ScalarLeaves} + * + *

Operation-Scoped Rules

+ *

Check: {@code operationScope == true} (via {@link #shouldRunOperationScopedRules()}) + *

Purpose: Track state across an entire operation, including all fragments it references. + * These rules need to "follow" fragment spreads to see variable usages, defer directives, etc. + *

Examples: {@code NoUndefinedVariables}, {@code NoUnusedVariables}, {@code VariableTypesMatch} + * + *

Traversal Example

+ * + *

Consider this GraphQL document: + *

{@code
+ * query GetUser($id: ID!) {
+ *   user(id: $id) {
+ *     ...UserFields
+ *   }
+ * }
+ *
+ * fragment UserFields on User {
+ *   name
+ *   friends {
+ *     ...UserFields   # recursive spread
+ *   }
+ * }
+ * }
+ * + *

The traversal proceeds as follows: + * + *

+ * STEP  NODE                        depth  operationScope  DOC-LEVEL  OP-SCOPED
+ * ────  ──────────────────────────  ─────  ──────────────  ─────────  ─────────
+ *  1    Document                      0        false          RUN       SKIP
+ *  2    OperationDefinition           0        true           RUN       RUN
+ *  3    ├─ VariableDefinition $id     0        true           RUN       RUN
+ *  4    ├─ Field "user"               0        true           RUN       RUN
+ *  5    │  └─ FragmentSpread          0        true           RUN       RUN
+ *       │     ...UserFields
+ *       │     ┌─────────────────────────────────────────────────────────────┐
+ *       │     │ MANUAL RETRAVERSAL INTO FRAGMENT                            │
+ *       │     └─────────────────────────────────────────────────────────────┘
+ *  6    │     FragmentDefinition      1        true          SKIP       RUN
+ *  7    │     ├─ Field "name"         1        true          SKIP       RUN
+ *  8    │     ├─ Field "friends"      1        true          SKIP       RUN
+ *  9    │     │  └─ FragmentSpread    1        true          SKIP       RUN
+ *       │     │     ...UserFields
+ *       │     │     (already visited - skip to avoid infinite loop)
+ *       │     └─────────────────────────────────────────────────────────────┘
+ * 10    └─ (leave OperationDef)       0        false      [finalize op-scoped rules]
+ * 11    FragmentDefinition            0        false          RUN       SKIP
+ *       "UserFields" (at doc level)
+ * 12    ├─ Field "name"               0        false          RUN       SKIP
+ * 13    ├─ Field "friends"            0        false          RUN       SKIP
+ * 14    │  └─ FragmentSpread          0        false          RUN       SKIP
+ * 
+ * + *

Key Observations

+ * + *
    + *
  • Steps 6-9: During retraversal, document-level rules SKIP because the fragment + * will be validated at steps 11-14. This prevents duplicate "field not found" errors.
  • + *
  • Steps 6-9: Operation-scoped rules RUN to track that variables used inside + * {@code UserFields} are defined in the operation.
  • + *
  • Steps 11-14: Operation-scoped rules SKIP because there's no operation context + * to track variables against.
  • + *
  • Step 9: Recursive fragment spreads are tracked via {@code visitedFragmentSpreads} + * to prevent infinite loops during retraversal.
  • + *
+ * + * @see OperationValidationRule + */ +@Internal +@NullMarked +@SuppressWarnings("rawtypes") +public class OperationValidator implements DocumentVisitor { + + // --- Infrastructure --- + private final ValidationContext validationContext; + private final ValidationErrorCollector errorCollector; + private final ValidationUtil validationUtil; + private final Predicate rulePredicate; + + // --- Traversal context --- + /** True when currently processing within an operation definition. */ + private boolean operationScope = false; + /** Depth of manual fragment traversal; 0 means primary document traversal. */ + private int fragmentRetraversalDepth = 0; + /** Tracks which fragments have been traversed via spreads to avoid infinite loops. */ + private final Set visitedFragmentSpreads = new HashSet<>(); + + // --- State: NoFragmentCycles --- + private final Map> fragmentSpreadsMap = new HashMap<>(); + + // --- State: NoUnusedFragments --- + private final List allDeclaredFragments = new ArrayList<>(); + private List unusedFragTracking_usedFragments = new ArrayList<>(); + private final Map> spreadsInDefinition = new LinkedHashMap<>(); + private final List> fragmentsUsedDirectlyInOperation = new ArrayList<>(); + + // --- State: NoUndefinedVariables --- + private final Set definedVariableNames = new LinkedHashSet<>(); + + // --- State: NoUnusedVariables --- + private final List unusedVars_variableDefinitions = new ArrayList<>(); + private final Set unusedVars_usedVariables = new LinkedHashSet<>(); + + // --- State: VariableTypesMatch --- + private final VariablesTypesMatcher variablesTypesMatcher = new VariablesTypesMatcher(); + private @Nullable Map variableDefinitionMap; + + // --- State: OverlappingFieldsCanBeMerged --- + private final Set> sameResponseShapeChecked = new LinkedHashSet<>(); + private final Set> sameForCommonParentsChecked = new LinkedHashSet<>(); + private final Set> conflictsReported = new LinkedHashSet<>(); + + // --- State: LoneAnonymousOperation --- + private boolean hasAnonymousOp = false; + private int loneAnon_count = 0; + + // --- State: UniqueOperationNames --- + private final Set operationNames = new LinkedHashSet<>(); + + // --- State: UniqueFragmentNames --- + private final Set fragmentNames = new LinkedHashSet<>(); + + // --- State: DeferDirectiveLabel --- + private final Set checkedDeferLabels = new LinkedHashSet<>(); + + // --- State: SubscriptionUniqueRootField --- + private final FieldCollector fieldCollector = new FieldCollector(); + + // --- Track whether we're in a context where fragment spread rules should run --- + // fragmentRetraversalDepth == 0 means we're NOT inside a manually-traversed fragment => run non-fragment-spread checks + // operationScope means we're inside an operation => can trigger fragment traversal + + private final boolean allRulesEnabled; + + public OperationValidator(ValidationContext validationContext, ValidationErrorCollector errorCollector, Predicate rulePredicate) { + this.validationContext = validationContext; + this.errorCollector = errorCollector; + this.validationUtil = new ValidationUtil(); + this.rulePredicate = rulePredicate; + this.allRulesEnabled = detectAllRulesEnabled(rulePredicate); + prepareFragmentSpreadsMap(); + } + + private static boolean detectAllRulesEnabled(Predicate predicate) { + for (OperationValidationRule rule : OperationValidationRule.values()) { + if (!predicate.test(rule)) { + return false; + } + } + return true; + } + + private boolean isRuleEnabled(OperationValidationRule rule) { + return allRulesEnabled || rulePredicate.test(rule); + } + + /** + * Returns true when document-level rules should run. + * + *

Document-level rules validate each AST node exactly once during the primary + * document traversal. They do NOT re-run when fragments are traversed through + * spreads, which prevents duplicate validation errors. + * + *

Examples: {@code FieldsOnCorrectType}, {@code UniqueFragmentNames}, + * {@code ScalarLeaves}, {@code KnownDirectives}. + * + * @return true if {@code fragmentRetraversalDepth == 0} (primary traversal) + */ + private boolean shouldRunDocumentLevelRules() { + return fragmentRetraversalDepth == 0; + } + + /** + * Returns true when operation-scoped rules should run. + * + *

Operation-scoped rules must follow fragment spreads to see the complete + * picture of an operation. They track state across all code paths, including + * fragments referenced by the operation. + * + *

Examples: {@code NoUndefinedVariables}, {@code NoUnusedVariables}, + * {@code VariableTypesMatch}, {@code DeferDirectiveOnRootLevel}. + * + * @return true if currently processing within an operation scope + */ + private boolean shouldRunOperationScopedRules() { + return operationScope; + } + + @Override + public void enter(Node node, List ancestors) { + validationContext.getTraversalContext().enter(node, ancestors); + + if (node instanceof Document) { + checkDocument((Document) node); + } else if (node instanceof Argument) { + checkArgument((Argument) node); + } else if (node instanceof TypeName) { + checkTypeName((TypeName) node); + } else if (node instanceof VariableDefinition) { + checkVariableDefinition((VariableDefinition) node); + } else if (node instanceof Field) { + checkField((Field) node); + } else if (node instanceof InlineFragment) { + checkInlineFragment((InlineFragment) node); + } else if (node instanceof Directive) { + checkDirective((Directive) node, ancestors); + } else if (node instanceof FragmentSpread) { + checkFragmentSpread((FragmentSpread) node, ancestors); + } else if (node instanceof FragmentDefinition) { + checkFragmentDefinition((FragmentDefinition) node); + } else if (node instanceof OperationDefinition) { + checkOperationDefinition((OperationDefinition) node); + } else if (node instanceof VariableReference) { + checkVariable((VariableReference) node); + } else if (node instanceof SelectionSet) { + checkSelectionSet((SelectionSet) node); + } else if (node instanceof ObjectValue) { + checkObjectValue((ObjectValue) node); + } + } + + @Override + public void leave(Node node, List ancestors) { + validationContext.getTraversalContext().leave(node, ancestors); + + if (node instanceof Document) { + documentFinished((Document) node); + } else if (node instanceof OperationDefinition) { + leaveOperationDefinition((OperationDefinition) node); + } else if (node instanceof SelectionSet) { + leaveSelectionSet((SelectionSet) node); + } else if (node instanceof FragmentDefinition) { + leaveFragmentDefinition(); + } + } + + private void addError(ValidationErrorType validationErrorType, Collection> locations, String description) { + List locationList = new ArrayList<>(); + for (Node node : locations) { + locationList.add(node.getSourceLocation()); + } + addError(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocations(locationList) + .description(description)); + } + + private void addError(ValidationErrorType validationErrorType, @Nullable SourceLocation location, String description) { + addError(newValidationError() + .validationErrorType(validationErrorType) + .sourceLocation(location) + .description(description)); + } + + private void addError(ValidationError.Builder validationError) { + errorCollector.addError(validationError.queryPath(getQueryPath()).build()); + } + + private @Nullable List getQueryPath() { + return validationContext.getQueryPath(); + } + + private String i18n(ValidationErrorType validationErrorType, I18nMsg i18nMsg) { + return i18n(validationErrorType, i18nMsg.getMsgKey(), i18nMsg.getMsgArguments()); + } + + private String i18n(ValidationErrorType validationErrorType, String msgKey, Object... msgArgs) { + Object[] params = new Object[msgArgs.length + 1]; + params[0] = mkTypeAndPath(validationErrorType); + arraycopy(msgArgs, 0, params, 1, msgArgs.length); + return validationContext.i18n(msgKey, params); + } + + private String mkTypeAndPath(ValidationErrorType validationErrorType) { + List queryPath = getQueryPath(); + StringBuilder sb = new StringBuilder(); + sb.append(validationErrorType); + if (queryPath != null) { + sb.append("@[").append(String.join("/", queryPath)).append("]"); + } + return sb.toString(); + } + + private boolean isExperimentalApiKeyEnabled(String key) { + if (validationContext == null || validationContext.getGraphQLContext() == null) { + return false; + } + Object value = validationContext.getGraphQLContext().get(key); + return value instanceof Boolean && (Boolean) value; + } + + private void checkDocument(Document document) { + if (isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { + validateExecutableDefinitions(document); + } + } + + private void checkArgument(Argument argument) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.ARGUMENTS_OF_CORRECT_TYPE)) { + validateArgumentsOfCorrectType(argument); + } + if (isRuleEnabled(OperationValidationRule.KNOWN_ARGUMENT_NAMES)) { + validateKnownArgumentNames(argument); + } + } + } + + private void checkTypeName(TypeName typeName) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.KNOWN_TYPE_NAMES)) { + validateKnownTypeNames(typeName); + } + } + } + + private void checkVariableDefinition(VariableDefinition variableDefinition) { + if (isRuleEnabled(OperationValidationRule.VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE)) { + validateVariableDefaultValuesOfCorrectType(variableDefinition); + } + if (isRuleEnabled(OperationValidationRule.VARIABLES_ARE_INPUT_TYPES)) { + validateVariablesAreInputTypes(variableDefinition); + } + if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { + definedVariableNames.add(variableDefinition.getName()); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + unusedVars_variableDefinitions.add(variableDefinition); + } + if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { + if (variableDefinitionMap != null) { + variableDefinitionMap.put(variableDefinition.getName(), variableDefinition); + } + } + } + + private void checkField(Field field) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.FIELDS_ON_CORRECT_TYPE)) { + validateFieldsOnCorrectType(field); + } + if (isRuleEnabled(OperationValidationRule.SCALAR_LEAVES)) { + validateScalarLeaves(field); + } + if (isRuleEnabled(OperationValidationRule.PROVIDED_NON_NULL_ARGUMENTS)) { + validateProvidedNonNullArguments_field(field); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_ARGUMENT_NAMES)) { + validateUniqueArgumentNames_field(field); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_DIRECTIVE_NAMES_PER_LOCATION)) { + validateUniqueDirectiveNamesPerLocation(field, field.getDirectives()); + } + } + } + + private void checkInlineFragment(InlineFragment inlineFragment) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.FRAGMENTS_ON_COMPOSITE_TYPE)) { + validateFragmentsOnCompositeType_inline(inlineFragment); + } + if (isRuleEnabled(OperationValidationRule.POSSIBLE_FRAGMENT_SPREADS)) { + validatePossibleFragmentSpreads_inline(inlineFragment); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_DIRECTIVE_NAMES_PER_LOCATION)) { + validateUniqueDirectiveNamesPerLocation(inlineFragment, inlineFragment.getDirectives()); + } + } + } + + private void checkDirective(Directive directive, List ancestors) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.KNOWN_DIRECTIVES)) { + validateKnownDirectives(directive, ancestors); + } + if (isRuleEnabled(OperationValidationRule.PROVIDED_NON_NULL_ARGUMENTS)) { + validateProvidedNonNullArguments_directive(directive); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_ARGUMENT_NAMES)) { + validateUniqueArgumentNames_directive(directive, ancestors); + } + if (isRuleEnabled(OperationValidationRule.DEFER_DIRECTIVE_LABEL)) { + validateDeferDirectiveLabel(directive); + } + } + if (shouldRunOperationScopedRules()) { + if (isRuleEnabled(OperationValidationRule.DEFER_DIRECTIVE_ON_ROOT_LEVEL)) { + validateDeferDirectiveOnRootLevel(directive); + } + if (isRuleEnabled(OperationValidationRule.DEFER_DIRECTIVE_ON_VALID_OPERATION)) { + validateDeferDirectiveOnValidOperation(directive, ancestors); + } + } + } + + private void checkFragmentSpread(FragmentSpread node, List ancestors) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.KNOWN_FRAGMENT_NAMES)) { + validateKnownFragmentNames(node); + } + if (isRuleEnabled(OperationValidationRule.POSSIBLE_FRAGMENT_SPREADS)) { + validatePossibleFragmentSpreads_spread(node); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + unusedFragTracking_usedFragments.add(node.getName()); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_DIRECTIVE_NAMES_PER_LOCATION)) { + validateUniqueDirectiveNamesPerLocation(node, node.getDirectives()); + } + } + + // Manually traverse into fragment definition during operation scope + if (operationScope) { + FragmentDefinition fragment = validationContext.getFragment(node.getName()); + if (fragment != null && !visitedFragmentSpreads.contains(node.getName())) { + visitedFragmentSpreads.add(node.getName()); + fragmentRetraversalDepth++; + new LanguageTraversal(ancestors).traverse(fragment, this); + fragmentRetraversalDepth--; + } + } + } + + private void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.FRAGMENTS_ON_COMPOSITE_TYPE)) { + validateFragmentsOnCompositeType_definition(fragmentDefinition); + } + if (isRuleEnabled(OperationValidationRule.NO_FRAGMENT_CYCLES)) { + validateNoFragmentCycles(fragmentDefinition); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + allDeclaredFragments.add(fragmentDefinition); + unusedFragTracking_usedFragments = new ArrayList<>(); + spreadsInDefinition.put(fragmentDefinition.getName(), unusedFragTracking_usedFragments); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_FRAGMENT_NAMES)) { + validateUniqueFragmentNames(fragmentDefinition); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_DIRECTIVE_NAMES_PER_LOCATION)) { + validateUniqueDirectiveNamesPerLocation(fragmentDefinition, fragmentDefinition.getDirectives()); + } + } + } + + private void checkOperationDefinition(OperationDefinition operationDefinition) { + operationScope = true; + + if (isRuleEnabled(OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED)) { + validateOverlappingFieldsCanBeMerged(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.LONE_ANONYMOUS_OPERATION)) { + validateLoneAnonymousOperation(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_OPERATION_NAMES)) { + validateUniqueOperationNames(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_VARIABLE_NAMES)) { + validateUniqueVariableNames(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.SUBSCRIPTION_UNIQUE_ROOT_FIELD)) { + validateSubscriptionUniqueRootField(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.UNIQUE_DIRECTIVE_NAMES_PER_LOCATION)) { + validateUniqueDirectiveNamesPerLocation(operationDefinition, operationDefinition.getDirectives()); + } + if (isRuleEnabled(OperationValidationRule.KNOWN_OPERATION_TYPES)) { + validateKnownOperationTypes(operationDefinition); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + unusedFragTracking_usedFragments = new ArrayList<>(); + fragmentsUsedDirectlyInOperation.add(unusedFragTracking_usedFragments); + } + if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { + definedVariableNames.clear(); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + unusedVars_usedVariables.clear(); + unusedVars_variableDefinitions.clear(); + } + if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { + variableDefinitionMap = new LinkedHashMap<>(); + } + } + + private void checkVariable(VariableReference variableReference) { + if (shouldRunOperationScopedRules()) { + if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { + validateNoUndefinedVariables(variableReference); + } + if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { + validateVariableTypesMatch(variableReference); + } + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + unusedVars_usedVariables.add(variableReference.getName()); + } + } + } + + private void checkSelectionSet(SelectionSet selectionSet) { + // No rules currently check selection set on enter + } + + private void checkObjectValue(ObjectValue objectValue) { + if (shouldRunDocumentLevelRules()) { + if (isRuleEnabled(OperationValidationRule.UNIQUE_OBJECT_FIELD_NAME)) { + validateUniqueObjectFieldName(objectValue); + } + } + } + + private void leaveOperationDefinition(OperationDefinition operationDefinition) { + // fragments should be revisited for each operation + visitedFragmentSpreads.clear(); + operationScope = false; + + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + for (VariableDefinition variableDefinition : unusedVars_variableDefinitions) { + if (!unusedVars_usedVariables.contains(variableDefinition.getName())) { + String message = i18n(UnusedVariable, "NoUnusedVariables.unusedVariable", variableDefinition.getName()); + addError(UnusedVariable, variableDefinition.getSourceLocation(), message); + } + } + } + } + + private void leaveSelectionSet(SelectionSet selectionSet) { + // No rules currently use leaveSelectionSet + } + + private void leaveFragmentDefinition() { + // No special handling needed - the fragment spread depth tracking + // is handled in checkFragmentSpread + } + + private void documentFinished(Document document) { + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + validateNoUnusedFragments(); + } + if (isRuleEnabled(OperationValidationRule.LONE_ANONYMOUS_OPERATION)) { + hasAnonymousOp = false; + } + } + + // --- ExecutableDefinitions --- + private void validateExecutableDefinitions(Document document) { + document.getDefinitions().forEach(definition -> { + if (!(definition instanceof OperationDefinition) + && !(definition instanceof FragmentDefinition)) { + String message = nonExecutableDefinitionMessage(definition); + addError(NonExecutableDefinition, definition.getSourceLocation(), message); + } + }); + } + + private String nonExecutableDefinitionMessage(Definition definition) { + if (definition instanceof TypeDefinition) { + return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableType", ((TypeDefinition) definition).getName()); + } else if (definition instanceof SchemaDefinition) { + return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableSchema"); + } else if (definition instanceof DirectiveDefinition) { + return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableDirective", ((DirectiveDefinition) definition).getName()); + } + return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableDefinition"); + } + + // --- ArgumentsOfCorrectType --- + private void validateArgumentsOfCorrectType(Argument argument) { + GraphQLArgument fieldArgument = validationContext.getArgument(); + if (fieldArgument == null) { + return; + } + ArgumentValidationUtil argValidationUtil = new ArgumentValidationUtil(argument); + if (!argValidationUtil.isValidLiteralValue(argument.getValue(), fieldArgument.getType(), + validationContext.getSchema(), validationContext.getGraphQLContext(), validationContext.getI18n().getLocale())) { + String message = i18n(WrongType, argValidationUtil.getMsgAndArgs()); + addError(newValidationError() + .validationErrorType(WrongType) + .sourceLocation(argument.getSourceLocation()) + .description(message) + .extensions(argValidationUtil.getErrorExtensions())); + } + } + + // --- FieldsOnCorrectType --- + private void validateFieldsOnCorrectType(Field field) { + GraphQLCompositeType parentType = validationContext.getParentType(); + if (parentType == null) return; + GraphQLFieldDefinition fieldDef = validationContext.getFieldDef(); + if (fieldDef == null) { + String message = i18n(FieldUndefined, "FieldsOnCorrectType.unknownField", field.getName(), parentType.getName()); + addError(FieldUndefined, field.getSourceLocation(), message); + } + } + + // --- FragmentsOnCompositeType --- + private void validateFragmentsOnCompositeType_inline(InlineFragment inlineFragment) { + if (inlineFragment.getTypeCondition() == null) { + return; + } + GraphQLType type = validationContext.getSchema().getType(inlineFragment.getTypeCondition().getName()); + if (type == null) return; + if (!(type instanceof GraphQLCompositeType)) { + String message = i18n(InlineFragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidInlineTypeCondition"); + addError(InlineFragmentTypeConditionInvalid, inlineFragment.getSourceLocation(), message); + } + } + + private void validateFragmentsOnCompositeType_definition(FragmentDefinition fragmentDefinition) { + GraphQLType type = validationContext.getSchema().getType(fragmentDefinition.getTypeCondition().getName()); + if (type == null) return; + if (!(type instanceof GraphQLCompositeType)) { + String message = i18n(FragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidFragmentTypeCondition"); + addError(FragmentTypeConditionInvalid, fragmentDefinition.getSourceLocation(), message); + } + } + + // --- KnownArgumentNames --- + private void validateKnownArgumentNames(Argument argument) { + GraphQLDirective directiveDef = validationContext.getDirective(); + if (directiveDef != null) { + GraphQLArgument directiveArgument = directiveDef.getArgument(argument.getName()); + if (directiveArgument == null) { + String message = i18n(UnknownDirective, "KnownArgumentNames.unknownDirectiveArg", argument.getName()); + addError(UnknownDirective, argument.getSourceLocation(), message); + } + return; + } + GraphQLFieldDefinition fieldDef = validationContext.getFieldDef(); + if (fieldDef == null) return; + GraphQLArgument fieldArgument = fieldDef.getArgument(argument.getName()); + if (fieldArgument == null) { + String message = i18n(UnknownArgument, "KnownArgumentNames.unknownFieldArg", argument.getName()); + addError(UnknownArgument, argument.getSourceLocation(), message); + } + } + + // --- KnownDirectives --- + private void validateKnownDirectives(Directive directive, List ancestors) { + GraphQLDirective graphQLDirective = validationContext.getSchema().getDirective(directive.getName()); + if (graphQLDirective == null) { + String message = i18n(UnknownDirective, "KnownDirectives.unknownDirective", directive.getName()); + addError(UnknownDirective, directive.getSourceLocation(), message); + return; + } + Node ancestor = ancestors.get(ancestors.size() - 1); + if (hasInvalidLocation(graphQLDirective, ancestor)) { + String message = i18n(MisplacedDirective, "KnownDirectives.directiveNotAllowed", directive.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } + } + + @SuppressWarnings("deprecation") + private boolean hasInvalidLocation(GraphQLDirective directive, Node ancestor) { + EnumSet validLocations = directive.validLocations(); + if (ancestor instanceof OperationDefinition) { + OperationDefinition.Operation operation = ((OperationDefinition) ancestor).getOperation(); + if (OperationDefinition.Operation.QUERY.equals(operation)) { + return !validLocations.contains(DirectiveLocation.QUERY); + } else if (OperationDefinition.Operation.MUTATION.equals(operation)) { + return !validLocations.contains(DirectiveLocation.MUTATION); + } else if (OperationDefinition.Operation.SUBSCRIPTION.equals(operation)) { + return !validLocations.contains(DirectiveLocation.SUBSCRIPTION); + } + } else if (ancestor instanceof Field) { + return !(validLocations.contains(DirectiveLocation.FIELD)); + } else if (ancestor instanceof FragmentSpread) { + return !(validLocations.contains(DirectiveLocation.FRAGMENT_SPREAD)); + } else if (ancestor instanceof FragmentDefinition) { + return !(validLocations.contains(DirectiveLocation.FRAGMENT_DEFINITION)); + } else if (ancestor instanceof InlineFragment) { + return !(validLocations.contains(DirectiveLocation.INLINE_FRAGMENT)); + } else if (ancestor instanceof VariableDefinition) { + return !(validLocations.contains(DirectiveLocation.VARIABLE_DEFINITION)); + } + return true; + } + + // --- KnownFragmentNames --- + private void validateKnownFragmentNames(FragmentSpread fragmentSpread) { + FragmentDefinition fragmentDefinition = validationContext.getFragment(fragmentSpread.getName()); + if (fragmentDefinition == null) { + String message = i18n(UndefinedFragment, "KnownFragmentNames.undefinedFragment", fragmentSpread.getName()); + addError(UndefinedFragment, fragmentSpread.getSourceLocation(), message); + } + } + + // --- KnownTypeNames --- + private void validateKnownTypeNames(TypeName typeName) { + if (validationContext.getSchema().getType(typeName.getName()) == null) { + String message = i18n(UnknownType, "KnownTypeNames.unknownType", typeName.getName()); + addError(UnknownType, typeName.getSourceLocation(), message); + } + } + + // --- NoFragmentCycles --- + private void prepareFragmentSpreadsMap() { + List definitions = validationContext.getDocument().getDefinitions(); + for (Definition definition : definitions) { + if (definition instanceof FragmentDefinition) { + FragmentDefinition fragmentDefinition = (FragmentDefinition) definition; + fragmentSpreadsMap.put(fragmentDefinition.getName(), gatherSpreads(fragmentDefinition)); + } + } + } + + private Set gatherSpreads(FragmentDefinition fragmentDefinition) { + final Set spreads = new HashSet<>(); + DocumentVisitor visitor = new DocumentVisitor() { + @Override + public void enter(Node node, List path) { + if (node instanceof FragmentSpread) { + spreads.add(((FragmentSpread) node).getName()); + } + } + + @Override + public void leave(Node node, List path) { + } + }; + new LanguageTraversal().traverse(fragmentDefinition, visitor); + return spreads; + } + + private void validateNoFragmentCycles(FragmentDefinition fragmentDefinition) { + ArrayList path = new ArrayList<>(); + path.add(fragmentDefinition.getName()); + Map> transitiveSpreads = buildTransitiveSpreads(path, new HashMap<>()); + + for (Map.Entry> entry : transitiveSpreads.entrySet()) { + if (entry.getValue().contains(entry.getKey())) { + String message = i18n(FragmentCycle, "NoFragmentCycles.cyclesNotAllowed"); + addError(FragmentCycle, Collections.singletonList(fragmentDefinition), message); + } + } + } + + private Map> buildTransitiveSpreads(ArrayList path, Map> transitiveSpreads) { + String name = path.get(path.size() - 1); + if (transitiveSpreads.containsKey(name)) { + return transitiveSpreads; + } + Set spreads = fragmentSpreadsMap.get(name); + if (spreads == null || spreads.isEmpty()) { + return transitiveSpreads; + } + for (String ancestor : path) { + Set ancestorSpreads = transitiveSpreads.get(ancestor); + if (ancestorSpreads == null) { + ancestorSpreads = new HashSet<>(); + } + ancestorSpreads.addAll(spreads); + transitiveSpreads.put(ancestor, ancestorSpreads); + } + for (String child : spreads) { + if (path.contains(child) || transitiveSpreads.containsKey(child)) { + continue; + } + ArrayList childPath = new ArrayList<>(path); + childPath.add(child); + buildTransitiveSpreads(childPath, transitiveSpreads); + } + return transitiveSpreads; + } + + // --- NoUndefinedVariables --- + private void validateNoUndefinedVariables(VariableReference variableReference) { + if (!definedVariableNames.contains(variableReference.getName())) { + String message = i18n(UndefinedVariable, "NoUndefinedVariables.undefinedVariable", variableReference.getName()); + addError(UndefinedVariable, variableReference.getSourceLocation(), message); + } + } + + // --- NoUnusedFragments --- + private void validateNoUnusedFragments() { + Set allUsedFragments = new HashSet<>(); + for (List fragmentsInOneOperation : fragmentsUsedDirectlyInOperation) { + for (String fragment : fragmentsInOneOperation) { + collectUsedFragmentsInDefinition(allUsedFragments, fragment); + } + } + for (FragmentDefinition fragmentDefinition : allDeclaredFragments) { + if (!allUsedFragments.contains(fragmentDefinition.getName())) { + String message = i18n(UnusedFragment, "NoUnusedFragments.unusedFragments", fragmentDefinition.getName()); + addError(UnusedFragment, fragmentDefinition.getSourceLocation(), message); + } + } + } + + private void collectUsedFragmentsInDefinition(Set result, String fragmentName) { + if (!result.add(fragmentName)) return; + List spreadList = spreadsInDefinition.get(fragmentName); + if (spreadList == null) { + return; + } + for (String fragment : spreadList) { + collectUsedFragmentsInDefinition(result, fragment); + } + } + + // --- OverlappingFieldsCanBeMerged --- + private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationDefinition) { + overlappingFieldsImpl(operationDefinition.getSelectionSet(), validationContext.getOutputType()); + } + + private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLOutputType graphQLOutputType) { + Map> fieldMap = new LinkedHashMap<>(selectionSet.getSelections().size()); + Set visitedFragments = new LinkedHashSet<>(); + overlappingFields_collectFields(fieldMap, selectionSet, graphQLOutputType, visitedFragments); + List conflicts = findConflicts(fieldMap); + for (Conflict conflict : conflicts) { + if (conflictsReported.contains(conflict.fields)) { + continue; + } + conflictsReported.add(conflict.fields); + addError(FieldsConflict, conflict.fields, conflict.reason); + } + } + + private void overlappingFields_collectFields(Map> fieldMap, SelectionSet selectionSet, @Nullable GraphQLType parentType, Set visitedFragments) { + for (Selection selection : selectionSet.getSelections()) { + if (selection instanceof Field) { + overlappingFields_collectFieldsForField(fieldMap, parentType, (Field) selection); + } else if (selection instanceof InlineFragment) { + overlappingFields_collectFieldsForInlineFragment(fieldMap, visitedFragments, parentType, (InlineFragment) selection); + } else if (selection instanceof FragmentSpread) { + overlappingFields_collectFieldsForFragmentSpread(fieldMap, visitedFragments, (FragmentSpread) selection); + } + } + } + + private void overlappingFields_collectFieldsForFragmentSpread(Map> fieldMap, Set visitedFragments, FragmentSpread fragmentSpread) { + FragmentDefinition fragment = validationContext.getFragment(fragmentSpread.getName()); + if (fragment == null) return; + if (visitedFragments.contains(fragment.getName())) return; + visitedFragments.add(fragment.getName()); + GraphQLType graphQLType = TypeFromAST.getTypeFromAST(validationContext.getSchema(), fragment.getTypeCondition()); + overlappingFields_collectFields(fieldMap, fragment.getSelectionSet(), graphQLType, visitedFragments); + } + + private void overlappingFields_collectFieldsForInlineFragment(Map> fieldMap, Set visitedFragments, @Nullable GraphQLType parentType, InlineFragment inlineFragment) { + GraphQLType graphQLType; + if (inlineFragment.getTypeCondition() == null) { + graphQLType = parentType; + } else { + graphQLType = TypeFromAST.getTypeFromAST(validationContext.getSchema(), inlineFragment.getTypeCondition()); + } + overlappingFields_collectFields(fieldMap, inlineFragment.getSelectionSet(), graphQLType, visitedFragments); + } + + private void overlappingFields_collectFieldsForField(Map> fieldMap, @Nullable GraphQLType parentType, Field field) { + String responseName = field.getResultKey(); + GraphQLOutputType fieldType = null; + GraphQLUnmodifiedType unwrappedParent = parentType != null ? unwrapAll(parentType) : null; + if (unwrappedParent instanceof GraphQLFieldsContainer) { + GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) unwrappedParent; + GraphQLFieldDefinition fieldDefinition = validationContext.getSchema().getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, field.getName()); + fieldType = fieldDefinition != null ? fieldDefinition.getType() : null; + } + fieldMap.computeIfAbsent(responseName, k -> new LinkedHashSet<>()).add(new FieldAndType(field, fieldType, unwrappedParent)); + } + + private List findConflicts(Map> fieldMap) { + List result = new ArrayList<>(); + sameResponseShapeByName(fieldMap, emptyList(), result); + sameForCommonParentsByName(fieldMap, emptyList(), result); + return result; + } + + private void sameResponseShapeByName(Map> fieldMap, ImmutableList currentPath, List conflictsResult) { + for (Map.Entry> entry : fieldMap.entrySet()) { + if (sameResponseShapeChecked.contains(entry.getValue())) { + continue; + } + ImmutableList newPath = addToList(currentPath, entry.getKey()); + sameResponseShapeChecked.add(entry.getValue()); + Conflict conflict = requireSameOutputTypeShape(newPath, entry.getValue()); + if (conflict != null) { + conflictsResult.add(conflict); + continue; + } + Map> subSelections = mergeSubSelections(entry.getValue()); + sameResponseShapeByName(subSelections, newPath, conflictsResult); + } + } + + private Map> mergeSubSelections(Set sameNameFields) { + Map> fieldMap = new LinkedHashMap<>(); + for (FieldAndType fieldAndType : sameNameFields) { + if (fieldAndType.field.getSelectionSet() != null) { + Set visitedFragments = new LinkedHashSet<>(); + overlappingFields_collectFields(fieldMap, fieldAndType.field.getSelectionSet(), fieldAndType.graphQLType, visitedFragments); + } + } + return fieldMap; + } + + private void sameForCommonParentsByName(Map> fieldMap, ImmutableList currentPath, List conflictsResult) { + for (Map.Entry> entry : fieldMap.entrySet()) { + List> groups = groupByCommonParents(entry.getValue()); + ImmutableList newPath = addToList(currentPath, entry.getKey()); + for (Set group : groups) { + if (sameForCommonParentsChecked.contains(group)) { + continue; + } + sameForCommonParentsChecked.add(group); + Conflict conflict = requireSameNameAndArguments(newPath, group); + if (conflict != null) { + conflictsResult.add(conflict); + continue; + } + Map> subSelections = mergeSubSelections(group); + sameForCommonParentsByName(subSelections, newPath, conflictsResult); + } + } + } + + private List> groupByCommonParents(Set fields) { + // Single-pass: partition into abstract types and concrete groups simultaneously + List abstractTypes = null; + Map> concreteGroups = null; + + for (FieldAndType fieldAndType : fields) { + if (isInterfaceOrUnion(fieldAndType.parentType)) { + if (abstractTypes == null) { + abstractTypes = new ArrayList<>(); + } + abstractTypes.add(fieldAndType); + } else if (fieldAndType.parentType instanceof GraphQLObjectType) { + if (concreteGroups == null) { + concreteGroups = new LinkedHashMap<>(); + } + concreteGroups.computeIfAbsent(fieldAndType.parentType, k -> new LinkedHashSet<>()).add(fieldAndType); + } + } + + if (concreteGroups == null || concreteGroups.isEmpty()) { + // No concrete types — return all abstract types as a single group + if (abstractTypes == null) { + return Collections.singletonList(fields); + } + return Collections.singletonList(new LinkedHashSet<>(abstractTypes)); + } + + List> result = new ArrayList<>(concreteGroups.size()); + for (Set concreteGroup : concreteGroups.values()) { + if (abstractTypes != null) { + concreteGroup.addAll(abstractTypes); + } + result.add(concreteGroup); + } + return result; + } + + private boolean isInterfaceOrUnion(@Nullable GraphQLType type) { + return type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType; + } + + private @Nullable Conflict requireSameNameAndArguments(ImmutableList path, Set fieldAndTypes) { + if (fieldAndTypes.size() <= 1) { + return null; + } + String name = null; + List arguments = null; + List fields = new ArrayList<>(); + for (FieldAndType fieldAndType : fieldAndTypes) { + Field field = fieldAndType.field; + fields.add(field); + if (name == null) { + name = field.getName(); + arguments = field.getArguments(); + continue; + } + if (!field.getName().equals(name)) { + String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentFields", pathToString(path), name, field.getName()); + return new Conflict(reason, fields); + } + if (!sameArguments(field.getArguments(), arguments)) { + String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentArgs", pathToString(path)); + return new Conflict(reason, fields); + } + } + return null; + } + + private String pathToString(ImmutableList path) { + return String.join("/", path); + } + + private boolean sameArguments(List arguments1, @Nullable List arguments2) { + if (arguments2 == null || arguments1.size() != arguments2.size()) { + return false; + } + for (Argument argument : arguments1) { + Argument matchedArgument = findArgumentByName(argument.getName(), arguments2); + if (matchedArgument == null) { + return false; + } + if (!AstComparator.sameValue(argument.getValue(), matchedArgument.getValue())) { + return false; + } + } + return true; + } + + private @Nullable Argument findArgumentByName(String name, List arguments) { + for (Argument argument : arguments) { + if (argument.getName().equals(name)) { + return argument; + } + } + return null; + } + + private @Nullable Conflict requireSameOutputTypeShape(ImmutableList path, Set fieldAndTypes) { + if (fieldAndTypes.size() <= 1) { + return null; + } + List fields = new ArrayList<>(); + GraphQLType typeAOriginal = null; + for (FieldAndType fieldAndType : fieldAndTypes) { + fields.add(fieldAndType.field); + if (typeAOriginal == null) { + typeAOriginal = fieldAndType.graphQLType; + continue; + } + GraphQLType typeA = typeAOriginal; + GraphQLType typeB = fieldAndType.graphQLType; + if (typeA == null || typeB == null) { + if (typeA != typeB) { + return mkNotSameTypeError(path, fields, typeA, typeB); + } + continue; + } + while (true) { + if (isNonNull(typeA) || isNonNull(typeB)) { + if (isNullable(typeA) || isNullable(typeB)) { + String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentNullability", pathToString(path)); + return new Conflict(reason, fields); + } + } + if (isList(typeA) || isList(typeB)) { + if (!isList(typeA) || !isList(typeB)) { + String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentLists", pathToString(path)); + return new Conflict(reason, fields); + } + } + if (isNotWrapped(typeA) && isNotWrapped(typeB)) { + break; + } + typeA = unwrapOne(typeA); + typeB = unwrapOne(typeB); + } + if (isScalar(typeA) || isScalar(typeB)) { + if (!sameType(typeA, typeB)) { + return mkNotSameTypeError(path, fields, typeA, typeB); + } + } + if (isEnum(typeA) || isEnum(typeB)) { + if (!sameType(typeA, typeB)) { + return mkNotSameTypeError(path, fields, typeA, typeB); + } + } + } + return null; + } + + private Conflict mkNotSameTypeError(ImmutableList path, List fields, @Nullable GraphQLType typeA, @Nullable GraphQLType typeB) { + String name1 = typeA != null ? simplePrint(typeA) : "null"; + String name2 = typeB != null ? simplePrint(typeB) : "null"; + String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentReturnTypes", pathToString(path), name1, name2); + return new Conflict(reason, fields); + } + + private boolean sameType(@Nullable GraphQLType type1, @Nullable GraphQLType type2) { + if (type1 == null || type2 == null) { + return true; + } + return type1.equals(type2); + } + + private static class FieldAndType { + final Field field; + final @Nullable GraphQLType graphQLType; + final @Nullable GraphQLType parentType; + + public FieldAndType(Field field, @Nullable GraphQLType graphQLType, @Nullable GraphQLType parentType) { + this.field = field; + this.graphQLType = graphQLType; + this.parentType = parentType; + } + + @Override + public String toString() { + return "FieldAndType{" + + "field=" + field + + ", graphQLType=" + graphQLType + + ", parentType=" + parentType + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldAndType that = (FieldAndType) o; + return Objects.equals(field, that.field); + } + + @Override + public int hashCode() { + return Objects.hashCode(field); + } + } + + private static class Conflict { + final String reason; + final Set fields = new LinkedHashSet<>(); + + public Conflict(String reason, List fields) { + this.reason = reason; + this.fields.addAll(fields); + } + } + + // --- PossibleFragmentSpreads --- + private void validatePossibleFragmentSpreads_inline(InlineFragment inlineFragment) { + GraphQLOutputType fragType = validationContext.getOutputType(); + GraphQLCompositeType parentType = validationContext.getParentType(); + if (fragType == null || parentType == null) return; + + if (isValidTargetCompositeType(fragType) && isValidTargetCompositeType(parentType) && !doTypesOverlap(fragType, parentType)) { + String message = i18n(InvalidFragmentType, "PossibleFragmentSpreads.inlineIncompatibleTypes", parentType.getName(), simplePrint(fragType)); + addError(InvalidFragmentType, inlineFragment.getSourceLocation(), message); + } + } + + private void validatePossibleFragmentSpreads_spread(FragmentSpread fragmentSpread) { + FragmentDefinition fragment = validationContext.getFragment(fragmentSpread.getName()); + if (fragment == null) return; + GraphQLType typeCondition = TypeFromAST.getTypeFromAST(validationContext.getSchema(), fragment.getTypeCondition()); + GraphQLCompositeType parentType = validationContext.getParentType(); + if (typeCondition == null || parentType == null) return; + + if (isValidTargetCompositeType(typeCondition) && isValidTargetCompositeType(parentType) && !doTypesOverlap(typeCondition, parentType)) { + String message = i18n(InvalidFragmentType, "PossibleFragmentSpreads.fragmentIncompatibleTypes", fragmentSpread.getName(), parentType.getName(), simplePrint(typeCondition)); + addError(InvalidFragmentType, fragmentSpread.getSourceLocation(), message); + } + } + + private boolean doTypesOverlap(GraphQLType type, GraphQLCompositeType parent) { + if (type == parent) return true; + List possibleParentTypes = getPossibleType(parent); + List possibleConditionTypes = getPossibleType(type); + return !Collections.disjoint(possibleParentTypes, possibleConditionTypes); + } + + private List getPossibleType(GraphQLType type) { + if (type instanceof GraphQLObjectType) { + return Collections.singletonList(type); + } else if (type instanceof GraphQLInterfaceType) { + List implementations = validationContext.getSchema().getImplementations((GraphQLInterfaceType) type); + return implementations != null ? implementations : Collections.emptyList(); + } else if (type instanceof GraphQLUnionType) { + return ((GraphQLUnionType) type).getTypes(); + } else { + Assert.assertShouldNeverHappen(); + } + return Collections.emptyList(); + } + + private boolean isValidTargetCompositeType(GraphQLType type) { + return type instanceof GraphQLCompositeType; + } + + // --- ProvidedNonNullArguments --- + private void validateProvidedNonNullArguments_field(Field field) { + GraphQLFieldDefinition fieldDef = validationContext.getFieldDef(); + if (fieldDef == null) return; + List providedArguments = field.getArguments(); + + for (GraphQLArgument graphQLArgument : fieldDef.getArguments()) { + Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); + boolean nonNullType = isNonNull(graphQLArgument.getType()); + boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); + if (argument == null && nonNullType && noDefaultValue) { + String message = i18n(MissingFieldArgument, "ProvidedNonNullArguments.missingFieldArg", graphQLArgument.getName()); + addError(MissingFieldArgument, field.getSourceLocation(), message); + } + if (argument != null) { + Value value = argument.getValue(); + if ((value == null || value instanceof NullValue) && nonNullType && noDefaultValue) { + String message = i18n(NullValueForNonNullArgument, "ProvidedNonNullArguments.nullValue", graphQLArgument.getName()); + addError(NullValueForNonNullArgument, field.getSourceLocation(), message); + } + } + } + } + + private void validateProvidedNonNullArguments_directive(Directive directive) { + GraphQLDirective graphQLDirective = validationContext.getDirective(); + if (graphQLDirective == null) return; + List providedArguments = directive.getArguments(); + + for (GraphQLArgument graphQLArgument : graphQLDirective.getArguments()) { + Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); + boolean nonNullType = isNonNull(graphQLArgument.getType()); + boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); + if (argument == null && nonNullType && noDefaultValue) { + String message = i18n(MissingDirectiveArgument, "ProvidedNonNullArguments.missingDirectiveArg", graphQLArgument.getName()); + addError(MissingDirectiveArgument, directive.getSourceLocation(), message); + } + } + } + + private static @Nullable Argument findArgumentByName(List arguments, String name) { + for (int i = 0; i < arguments.size(); i++) { + Argument argument = arguments.get(i); + if (argument.getName().equals(name)) { + return argument; + } + } + return null; + } + + // --- ScalarLeaves --- + private void validateScalarLeaves(Field field) { + GraphQLOutputType type = validationContext.getOutputType(); + if (type == null) return; + if (isLeaf(type)) { + if (field.getSelectionSet() != null) { + String message = i18n(SubselectionNotAllowed, "ScalarLeaves.subselectionOnLeaf", simplePrint(type), field.getName()); + addError(SubselectionNotAllowed, field.getSourceLocation(), message); + } + } else { + if (field.getSelectionSet() == null) { + String message = i18n(SubselectionRequired, "ScalarLeaves.subselectionRequired", simplePrint(type), field.getName()); + addError(SubselectionRequired, field.getSourceLocation(), message); + } + } + } + + // --- VariableDefaultValuesOfCorrectType --- + private void validateVariableDefaultValuesOfCorrectType(VariableDefinition variableDefinition) { + GraphQLInputType inputType = validationContext.getInputType(); + if (inputType == null) return; + if (variableDefinition.getDefaultValue() != null + && !validationUtil.isValidLiteralValue(variableDefinition.getDefaultValue(), inputType, + validationContext.getSchema(), validationContext.getGraphQLContext(), validationContext.getI18n().getLocale())) { + String message = i18n(BadValueForDefaultArg, "VariableDefaultValuesOfCorrectType.badDefault", variableDefinition.getDefaultValue(), simplePrint(inputType)); + addError(BadValueForDefaultArg, variableDefinition.getSourceLocation(), message); + } + } + + // --- VariablesAreInputTypes --- + private void validateVariablesAreInputTypes(VariableDefinition variableDefinition) { + TypeName unmodifiedAstType = validationUtil.getUnmodifiedType(variableDefinition.getType()); + GraphQLType type = validationContext.getSchema().getType(unmodifiedAstType.getName()); + if (type == null) return; + if (!isInput(type)) { + String message = i18n(NonInputTypeOnVariable, "VariablesAreInputTypes.wrongType", variableDefinition.getName(), unmodifiedAstType.getName()); + addError(NonInputTypeOnVariable, variableDefinition.getSourceLocation(), message); + } + } + + // --- VariableTypesMatch --- + private void validateVariableTypesMatch(VariableReference variableReference) { + if (variableDefinitionMap == null) return; + VariableDefinition variableDefinition = variableDefinitionMap.get(variableReference.getName()); + if (variableDefinition == null) return; + GraphQLType variableType = TypeFromAST.getTypeFromAST(validationContext.getSchema(), variableDefinition.getType()); + if (variableType == null) return; + GraphQLInputType locationType = validationContext.getInputType(); + Optional locationDefault = Optional.ofNullable(validationContext.getDefaultValue()); + if (locationType == null) return; + Value locationDefaultValue = null; + if (locationDefault.isPresent() && locationDefault.get().isLiteral()) { + locationDefaultValue = (Value) locationDefault.get().getValue(); + } else if (locationDefault.isPresent() && locationDefault.get().isSet()) { + locationDefaultValue = ValuesResolver.valueToLiteral(locationDefault.get(), locationType, + validationContext.getGraphQLContext(), validationContext.getI18n().getLocale()); + } + boolean variableDefMatches = variablesTypesMatcher.doesVariableTypesMatch(variableType, variableDefinition.getDefaultValue(), locationType, locationDefaultValue); + if (!variableDefMatches) { + GraphQLType effectiveType = variablesTypesMatcher.effectiveType(variableType, variableDefinition.getDefaultValue()); + String message = i18n(VariableTypeMismatch, "VariableTypesMatchRule.unexpectedType", + variableDefinition.getName(), + GraphQLTypeUtil.simplePrint(effectiveType), + GraphQLTypeUtil.simplePrint(locationType)); + addError(VariableTypeMismatch, variableReference.getSourceLocation(), message); + } + } + + // --- LoneAnonymousOperation --- + private void validateLoneAnonymousOperation(OperationDefinition operationDefinition) { + String name = operationDefinition.getName(); + if (name == null) { + hasAnonymousOp = true; + if (loneAnon_count > 0) { + String message = i18n(LoneAnonymousOperationViolation, "LoneAnonymousOperation.withOthers"); + addError(LoneAnonymousOperationViolation, operationDefinition.getSourceLocation(), message); + } + } else { + if (hasAnonymousOp) { + String message = i18n(LoneAnonymousOperationViolation, "LoneAnonymousOperation.namedOperation", name); + addError(LoneAnonymousOperationViolation, operationDefinition.getSourceLocation(), message); + } + } + loneAnon_count++; + } + + // --- UniqueOperationNames --- + private void validateUniqueOperationNames(OperationDefinition operationDefinition) { + String name = operationDefinition.getName(); + if (name == null) return; + if (operationNames.contains(name)) { + String message = i18n(DuplicateOperationName, "UniqueOperationNames.oneOperation", operationDefinition.getName()); + addError(DuplicateOperationName, operationDefinition.getSourceLocation(), message); + } else { + operationNames.add(name); + } + } + + // --- UniqueFragmentNames --- + private void validateUniqueFragmentNames(FragmentDefinition fragmentDefinition) { + String name = fragmentDefinition.getName(); + if (name == null) return; + if (fragmentNames.contains(name)) { + String message = i18n(DuplicateFragmentName, "UniqueFragmentNames.oneFragment", name); + addError(DuplicateFragmentName, fragmentDefinition.getSourceLocation(), message); + } else { + fragmentNames.add(name); + } + } + + // --- UniqueDirectiveNamesPerLocation --- + private void validateUniqueDirectiveNamesPerLocation(Node directivesContainer, List directives) { + Set directiveNames = new LinkedHashSet<>(); + for (Directive directive : directives) { + String name = directive.getName(); + GraphQLDirective graphQLDirective = validationContext.getSchema().getDirective(name); + boolean nonRepeatable = graphQLDirective != null && graphQLDirective.isNonRepeatable(); + if (directiveNames.contains(name) && nonRepeatable) { + String message = i18n(DuplicateDirectiveName, "UniqueDirectiveNamesPerLocation.uniqueDirectives", name, directivesContainer.getClass().getSimpleName()); + addError(DuplicateDirectiveName, directive.getSourceLocation(), message); + } else { + directiveNames.add(name); + } + } + } + + // --- UniqueArgumentNames --- + private void validateUniqueArgumentNames_field(Field field) { + if (field.getArguments() == null || field.getArguments().size() <= 1) return; + Set arguments = Sets.newHashSetWithExpectedSize(field.getArguments().size()); + for (Argument argument : field.getArguments()) { + if (arguments.contains(argument.getName())) { + String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.uniqueArgument", argument.getName()); + addError(DuplicateArgumentNames, field.getSourceLocation(), message); + } else { + arguments.add(argument.getName()); + } + } + } + + private void validateUniqueArgumentNames_directive(Directive directive, List ancestors) { + if (directive.getArguments() == null || directive.getArguments().size() <= 1) return; + Set arguments = Sets.newHashSetWithExpectedSize(directive.getArguments().size()); + for (Argument argument : directive.getArguments()) { + if (arguments.contains(argument.getName())) { + String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.uniqueArgument", argument.getName()); + addError(DuplicateArgumentNames, directive.getSourceLocation(), message); + } else { + arguments.add(argument.getName()); + } + } + } + + // --- UniqueVariableNames --- + private void validateUniqueVariableNames(OperationDefinition operationDefinition) { + List variableDefinitions = operationDefinition.getVariableDefinitions(); + if (variableDefinitions == null || variableDefinitions.size() <= 1) return; + Set variableNameList = Sets.newLinkedHashSetWithExpectedSize(variableDefinitions.size()); + for (VariableDefinition variableDefinition : variableDefinitions) { + if (variableNameList.contains(variableDefinition.getName())) { + String message = i18n(DuplicateVariableName, "UniqueVariableNames.oneVariable", variableDefinition.getName()); + addError(DuplicateVariableName, variableDefinition.getSourceLocation(), message); + } else { + variableNameList.add(variableDefinition.getName()); + } + } + } + + // --- SubscriptionUniqueRootField --- + private void validateSubscriptionUniqueRootField(OperationDefinition operationDef) { + if (operationDef.getOperation() == SUBSCRIPTION) { + GraphQLObjectType subscriptionType = validationContext.getSchema().getSubscriptionType(); + FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() + .schema(validationContext.getSchema()) + .fragments(NodeUtil.getFragmentsByName(validationContext.getDocument())) + .variables(CoercedVariables.emptyVariables().toMap()) + .objectType(subscriptionType) + .graphQLContext(validationContext.getGraphQLContext()) + .build(); + MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); + if (fields.size() > 1) { + String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); + addError(SubscriptionMultipleRootFields, operationDef.getSourceLocation(), message); + } else { + MergedField mergedField = fields.getSubFieldsList().get(0); + if (isIntrospectionField(mergedField)) { + String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), mergedField.getName()); + addError(SubscriptionIntrospectionRootField, mergedField.getSingleField().getSourceLocation(), message); + } + } + } + } + + private boolean isIntrospectionField(MergedField field) { + return field.getName().startsWith("__"); + } + + // --- UniqueObjectFieldName --- + private void validateUniqueObjectFieldName(ObjectValue objectValue) { + Set fieldNames = Sets.newHashSetWithExpectedSize(objectValue.getObjectFields().size()); + for (ObjectField field : objectValue.getObjectFields()) { + String fieldName = field.getName(); + if (fieldNames.contains(fieldName)) { + String message = i18n(UniqueObjectFieldName, "UniqueObjectFieldName.duplicateFieldName", fieldName); + addError(UniqueObjectFieldName, objectValue.getSourceLocation(), message); + } else { + fieldNames.add(fieldName); + } + } + } + + // --- DeferDirectiveOnRootLevel --- + private void validateDeferDirectiveOnRootLevel(Directive directive) { + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { + return; + } + if (!Directives.DeferDirective.getName().equals(directive.getName())) { + return; + } + GraphQLObjectType mutationType = validationContext.getSchema().getMutationType(); + GraphQLObjectType subscriptionType = validationContext.getSchema().getSubscriptionType(); + GraphQLCompositeType parentType = validationContext.getParentType(); + if (mutationType != null && parentType != null && parentType.getName().equals(mutationType.getName())) { + String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelMutation", parentType.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } else if (subscriptionType != null && parentType != null && parentType.getName().equals(subscriptionType.getName())) { + String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelSubscription", parentType.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } + } + + // --- DeferDirectiveOnValidOperation --- + private void validateDeferDirectiveOnValidOperation(Directive directive, List ancestors) { + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { + return; + } + if (!Directives.DeferDirective.getName().equals(directive.getName())) { + return; + } + Optional operationDefinition = getOperationDefinition(ancestors); + if (operationDefinition.isPresent() && + SUBSCRIPTION.equals(operationDefinition.get().getOperation()) && + !ifArgumentMightBeFalse(directive)) { + String message = i18n(MisplacedDirective, "IncrementalDirective.notAllowedSubscriptionOperation", directive.getName()); + addError(MisplacedDirective, directive.getSourceLocation(), message); + } + } + + private Optional getOperationDefinition(List ancestors) { + return ancestors.stream() + .filter(doc -> doc instanceof OperationDefinition) + .map(def -> (OperationDefinition) def) + .findFirst(); + } + + private Boolean ifArgumentMightBeFalse(Directive directive) { + Argument ifArgument = directive.getArgumentsByName().get("if"); + if (ifArgument == null) { + return false; + } + if (ifArgument.getValue() instanceof BooleanValue) { + return !((BooleanValue) ifArgument.getValue()).isValue(); + } + if (ifArgument.getValue() instanceof VariableReference) { + return true; + } + return false; + } + + // --- DeferDirectiveLabel --- + private void validateDeferDirectiveLabel(Directive directive) { + if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT) || + !Directives.DeferDirective.getName().equals(directive.getName()) || + directive.getArguments().size() == 0) { + return; + } + Argument labelArgument = directive.getArgument("label"); + if (labelArgument == null || labelArgument.getValue() instanceof NullValue) { + return; + } + Value labelArgumentValue = labelArgument.getValue(); + if (!(labelArgumentValue instanceof StringValue)) { + String message = i18n(WrongType, "DeferDirective.labelMustBeStaticString"); + addError(WrongType, directive.getSourceLocation(), message); + } else { + if (checkedDeferLabels.contains(((StringValue) labelArgumentValue).getValue())) { + String message = i18n(DuplicateIncrementalLabel, "IncrementalDirective.uniqueArgument", labelArgument.getName(), directive.getName()); + addError(DuplicateIncrementalLabel, directive.getSourceLocation(), message); + } else { + checkedDeferLabels.add(((StringValue) labelArgumentValue).getValue()); + } + } + } + + // --- KnownOperationTypes --- + private void validateKnownOperationTypes(OperationDefinition operationDefinition) { + OperationDefinition.Operation documentOperation = operationDefinition.getOperation(); + if (documentOperation == OperationDefinition.Operation.MUTATION + && validationContext.getSchema().getMutationType() == null) { + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } else if (documentOperation == OperationDefinition.Operation.SUBSCRIPTION + && validationContext.getSchema().getSubscriptionType() == null) { + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } else if (documentOperation == OperationDefinition.Operation.QUERY + && validationContext.getSchema().getQueryType() == null) { + String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); + addError(UnknownOperation, operationDefinition.getSourceLocation(), message); + } + } + + private String formatOperation(OperationDefinition.Operation operation) { + return StringKit.capitalize(operation.name().toLowerCase()); + } + + @Override + public String toString() { + return "OperationValidator{" + validationContext + "}"; + } +} diff --git a/src/main/java/graphql/validation/RulesVisitor.java b/src/main/java/graphql/validation/RulesVisitor.java deleted file mode 100644 index df3131ce7c..0000000000 --- a/src/main/java/graphql/validation/RulesVisitor.java +++ /dev/null @@ -1,197 +0,0 @@ -package graphql.validation; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.common.collect.ImmutableList; - -import graphql.Internal; -import graphql.language.Argument; -import graphql.language.Directive; -import graphql.language.Document; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.Node; -import graphql.language.ObjectValue; -import graphql.language.OperationDefinition; -import graphql.language.SelectionSet; -import graphql.language.TypeName; -import graphql.language.VariableDefinition; -import graphql.language.VariableReference; - -@Internal -@SuppressWarnings("rawtypes") -public class RulesVisitor implements DocumentVisitor { - private final ValidationContext validationContext; - private final List allRules; - private List currentRules; - private final Set visitedFragmentSpreads = new HashSet<>(); - private final List fragmentSpreadVisitRules; - private final List nonFragmentSpreadRules; - private boolean operationScope = false; - private int fragmentSpreadVisitDepth = 0; - - public RulesVisitor(ValidationContext validationContext, List rules) { - this.validationContext = validationContext; - this.allRules = rules; - this.currentRules = allRules; - this.nonFragmentSpreadRules = filterRulesVisitingFragmentSpreads(allRules, false); - this.fragmentSpreadVisitRules = filterRulesVisitingFragmentSpreads(allRules, true); - } - - private List filterRulesVisitingFragmentSpreads(List rules, boolean isVisitFragmentSpreads) { - ImmutableList.Builder builder = ImmutableList.builder(); - for (AbstractRule rule : rules) { - if (rule.isVisitFragmentSpreads() == isVisitFragmentSpreads) { - builder.add(rule); - } - } - return builder.build(); - } - - @Override - public void enter(Node node, List ancestors) { - validationContext.getTraversalContext().enter(node, ancestors); - - if (node instanceof Document){ - checkDocument((Document) node); - } else if (node instanceof Argument) { - checkArgument((Argument) node); - } else if (node instanceof TypeName) { - checkTypeName((TypeName) node); - } else if (node instanceof VariableDefinition) { - checkVariableDefinition((VariableDefinition) node); - } else if (node instanceof Field) { - checkField((Field) node); - } else if (node instanceof InlineFragment) { - checkInlineFragment((InlineFragment) node); - } else if (node instanceof Directive) { - checkDirective((Directive) node, ancestors); - } else if (node instanceof FragmentSpread) { - checkFragmentSpread((FragmentSpread) node, ancestors); - } else if (node instanceof FragmentDefinition) { - checkFragmentDefinition((FragmentDefinition) node); - } else if (node instanceof OperationDefinition) { - checkOperationDefinition((OperationDefinition) node); - } else if (node instanceof VariableReference) { - checkVariable((VariableReference) node); - } else if (node instanceof SelectionSet) { - checkSelectionSet((SelectionSet) node); - } else if (node instanceof ObjectValue) { - checkObjectValue((ObjectValue) node); - } - } - - private void checkDocument(Document node) { - currentRules.forEach(r -> r.checkDocument(node)); - } - - private void checkArgument(Argument node) { - currentRules.forEach(r -> r.checkArgument(node)); - } - - private void checkTypeName(TypeName node) { - currentRules.forEach(r -> r.checkTypeName(node)); - } - - private void checkVariableDefinition(VariableDefinition node) { - currentRules.forEach(r -> r.checkVariableDefinition(node)); - } - - private void checkField(Field node) { - currentRules.forEach(r -> r.checkField(node)); - } - - private void checkInlineFragment(InlineFragment node) { - currentRules.forEach(r -> r.checkInlineFragment(node)); - } - - private void checkDirective(Directive node, List ancestors) { - currentRules.forEach(r -> r.checkDirective(node, ancestors)); - } - - private void checkFragmentSpread(FragmentSpread node, List ancestors) { - currentRules.forEach(r -> r.checkFragmentSpread(node)); - - if (operationScope) { - FragmentDefinition fragment = validationContext.getFragment(node.getName()); - if (fragment != null && !visitedFragmentSpreads.contains(node.getName())) { - // Manually traverse into the FragmentDefinition - visitedFragmentSpreads.add(node.getName()); - List prevRules = currentRules; - currentRules = fragmentSpreadVisitRules; - fragmentSpreadVisitDepth++; - new LanguageTraversal(ancestors).traverse(fragment, this); - fragmentSpreadVisitDepth--; - currentRules = prevRules; - } - } - } - - private void checkFragmentDefinition(FragmentDefinition node) { - // If we've encountered a FragmentDefinition and we got here without coming through - // an OperationDefinition, then suspend all isVisitFragmentSpread rules for this subtree. - // Expect these rules to be checked when the FragmentSpread is traversed - if (fragmentSpreadVisitDepth == 0) { - currentRules = nonFragmentSpreadRules; - } - - currentRules.forEach(r -> r.checkFragmentDefinition(node)); - } - - private void checkOperationDefinition(OperationDefinition node) { - operationScope = true; - currentRules.forEach(r -> r.checkOperationDefinition(node)); - } - - private void checkSelectionSet(SelectionSet node) { - currentRules.forEach(r -> r.checkSelectionSet(node)); - } - - private void checkVariable(VariableReference node) { - currentRules.forEach(r -> r.checkVariable(node)); - } - - private void checkObjectValue(ObjectValue node) { - currentRules.forEach(r -> r.checkObjectValue(node)); - } - - @Override - public void leave(Node node, List ancestors) { - validationContext.getTraversalContext().leave(node, ancestors); - - if (node instanceof Document) { - documentFinished((Document) node); - } else if (node instanceof OperationDefinition) { - leaveOperationDefinition((OperationDefinition) node); - } else if (node instanceof SelectionSet) { - leaveSelectionSet((SelectionSet) node); - } else if (node instanceof FragmentDefinition) { - leaveFragmentDefinition((FragmentDefinition) node); - } - } - - private void leaveSelectionSet(SelectionSet node) { - currentRules.forEach(r -> r.leaveSelectionSet(node)); - } - - private void leaveOperationDefinition(OperationDefinition node) { - // fragments should be revisited for each operation - visitedFragmentSpreads.clear(); - operationScope = false; - currentRules.forEach(r -> r.leaveOperationDefinition(node)); - } - - private void documentFinished(Document node) { - currentRules.forEach(r -> r.documentFinished(node)); - } - - private void leaveFragmentDefinition(FragmentDefinition node) { - if (fragmentSpreadVisitDepth == 0) { - currentRules = allRules; - } - } -} diff --git a/src/main/java/graphql/validation/TraversalContext.java b/src/main/java/graphql/validation/TraversalContext.java index d9d93af7f1..a8ce012f1d 100644 --- a/src/main/java/graphql/validation/TraversalContext.java +++ b/src/main/java/graphql/validation/TraversalContext.java @@ -1,6 +1,5 @@ package graphql.validation; - import graphql.Assert; import graphql.Internal; import graphql.execution.TypeFromAST; @@ -33,6 +32,8 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -43,6 +44,7 @@ import static graphql.schema.GraphQLTypeUtil.unwrapOne; @Internal +@NullMarked public class TraversalContext implements DocumentVisitor { private final GraphQLSchema schema; private final List outputTypeStack = new ArrayList<>(); @@ -51,8 +53,8 @@ public class TraversalContext implements DocumentVisitor { private final List defaultValueStack = new ArrayList<>(); private final List fieldDefStack = new ArrayList<>(); private final List nameStack = new ArrayList<>(); - private GraphQLDirective directive; - private GraphQLArgument argument; + private @Nullable GraphQLDirective directive; + private @Nullable GraphQLArgument argument; public TraversalContext(GraphQLSchema graphQLSchema) { @@ -86,7 +88,8 @@ public void enter(Node node, List path) { private void enterImpl(SelectionSet selectionSet) { - GraphQLUnmodifiedType rawType = unwrapAll(getOutputType()); + GraphQLOutputType outputType = getOutputType(); + GraphQLUnmodifiedType rawType = outputType != null ? unwrapAll(outputType) : null; GraphQLCompositeType parentType = null; if (rawType instanceof GraphQLCompositeType) { parentType = (GraphQLCompositeType) rawType; @@ -164,7 +167,7 @@ private void enterImpl(Argument argument) { private void enterImpl(ArrayValue arrayValue) { GraphQLNullableType nullableType = getNullableType(getInputType()); GraphQLInputType inputType = null; - if (isList(nullableType)) { + if (nullableType != null && isList(nullableType)) { inputType = (GraphQLInputType) unwrapOne(nullableType); } addInputType(inputType); @@ -173,7 +176,8 @@ private void enterImpl(ArrayValue arrayValue) { } private void enterImpl(ObjectField objectField) { - GraphQLUnmodifiedType objectType = unwrapAll(getInputType()); + GraphQLInputType currentInputType = getInputType(); + GraphQLUnmodifiedType objectType = currentInputType != null ? unwrapAll(currentInputType) : null; GraphQLInputType inputType = null; GraphQLInputObjectField inputField = null; if (objectType instanceof GraphQLInputObjectType) { @@ -187,7 +191,7 @@ private void enterImpl(ObjectField objectField) { addDefaultValue(inputField != null ? inputField.getInputFieldDefaultValue() : null); } - private GraphQLArgument find(List arguments, String name) { + private @Nullable GraphQLArgument find(List arguments, String name) { for (GraphQLArgument argument : arguments) { if (argument.getName().equals(name)) { return argument; @@ -245,7 +249,10 @@ private boolean isEmpty(String name) { return name == null || name.isEmpty(); } - private GraphQLNullableType getNullableType(GraphQLType type) { + private @Nullable GraphQLNullableType getNullableType(@Nullable GraphQLType type) { + if (type == null) { + return null; + } return (GraphQLNullableType) (isNonNull(type) ? unwrapOne(type) : type); } @@ -253,15 +260,15 @@ private GraphQLNullableType getNullableType(GraphQLType type) { * @return can be null if current node does not have a OutputType associated: for example * if the current field is unknown */ - public GraphQLOutputType getOutputType() { + public @Nullable GraphQLOutputType getOutputType() { return lastElement(outputTypeStack); } - private void addOutputType(GraphQLOutputType type) { + private void addOutputType(@Nullable GraphQLOutputType type) { outputTypeStack.add(type); } - private T lastElement(List list) { + private @Nullable T lastElement(List list) { if (list.isEmpty()) { return null; } @@ -275,53 +282,54 @@ private T pop(List list) { /** * @return can be null if the parent is not a CompositeType */ - public GraphQLCompositeType getParentType() { + public @Nullable GraphQLCompositeType getParentType() { return lastElement(parentTypeStack); } - private void addParentType(GraphQLCompositeType compositeType) { + private void addParentType(@Nullable GraphQLCompositeType compositeType) { parentTypeStack.add(compositeType); } - public GraphQLInputType getInputType() { + public @Nullable GraphQLInputType getInputType() { return lastElement(inputTypeStack); } - public InputValueWithState getDefaultValue() { + + public @Nullable InputValueWithState getDefaultValue() { return lastElement(defaultValueStack); } - private void addInputType(GraphQLInputType graphQLInputType) { + private void addInputType(@Nullable GraphQLInputType graphQLInputType) { inputTypeStack.add(graphQLInputType); } - private void addDefaultValue(InputValueWithState defaultValue) { + private void addDefaultValue(@Nullable InputValueWithState defaultValue) { defaultValueStack.add(defaultValue); } - public GraphQLFieldDefinition getFieldDef() { + public @Nullable GraphQLFieldDefinition getFieldDef() { return lastElement(fieldDefStack); } - public List getQueryPath() { + public @Nullable List getQueryPath() { if (nameStack.isEmpty()) { return null; } return new ArrayList<>(nameStack); } - private void addFieldDef(GraphQLFieldDefinition fieldDefinition) { + private void addFieldDef(@Nullable GraphQLFieldDefinition fieldDefinition) { fieldDefStack.add(fieldDefinition); } - public GraphQLDirective getDirective() { + public @Nullable GraphQLDirective getDirective() { return directive; } - public GraphQLArgument getArgument() { + public @Nullable GraphQLArgument getArgument() { return argument; } - private GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLType parentType, Field field) { + private @Nullable GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLType parentType, Field field) { if (schema.getQueryType().equals(parentType)) { if (field.getName().equals(schema.getIntrospectionSchemaFieldDefinition().getName())) { return schema.getIntrospectionSchemaFieldDefinition(); diff --git a/src/main/java/graphql/validation/ValidationContext.java b/src/main/java/graphql/validation/ValidationContext.java index 18fe678177..873783785f 100644 --- a/src/main/java/graphql/validation/ValidationContext.java +++ b/src/main/java/graphql/validation/ValidationContext.java @@ -1,6 +1,5 @@ package graphql.validation; - import graphql.GraphQLContext; import graphql.Internal; import graphql.i18n.I18n; @@ -15,6 +14,8 @@ import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLSchema; import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.LinkedHashMap; import java.util.List; @@ -22,6 +23,7 @@ import java.util.Map; @Internal +@NullMarked public class ValidationContext { private final GraphQLSchema schema; @@ -63,39 +65,39 @@ public Document getDocument() { return document; } - public FragmentDefinition getFragment(String name) { + public @Nullable FragmentDefinition getFragment(String name) { return fragmentDefinitionMap.get(name); } - public GraphQLCompositeType getParentType() { + public @Nullable GraphQLCompositeType getParentType() { return traversalContext.getParentType(); } - public GraphQLInputType getInputType() { + public @Nullable GraphQLInputType getInputType() { return traversalContext.getInputType(); } - public InputValueWithState getDefaultValue() { + public @Nullable InputValueWithState getDefaultValue() { return traversalContext.getDefaultValue(); } - public GraphQLFieldDefinition getFieldDef() { + public @Nullable GraphQLFieldDefinition getFieldDef() { return traversalContext.getFieldDef(); } - public GraphQLDirective getDirective() { + public @Nullable GraphQLDirective getDirective() { return traversalContext.getDirective(); } - public GraphQLArgument getArgument() { + public @Nullable GraphQLArgument getArgument() { return traversalContext.getArgument(); } - public GraphQLOutputType getOutputType() { + public @Nullable GraphQLOutputType getOutputType() { return traversalContext.getOutputType(); } - public List getQueryPath() { + public @Nullable List getQueryPath() { return traversalContext.getQueryPath(); } diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index 52709109d6..654eec5cef 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -5,43 +5,10 @@ import graphql.i18n.I18n; import graphql.language.Document; import graphql.schema.GraphQLSchema; -import graphql.validation.rules.ArgumentsOfCorrectType; -import graphql.validation.rules.DeferDirectiveLabel; -import graphql.validation.rules.DeferDirectiveOnRootLevel; -import graphql.validation.rules.DeferDirectiveOnValidOperation; -import graphql.validation.rules.KnownOperationTypes; -import graphql.validation.rules.UniqueObjectFieldName; -import graphql.validation.rules.ExecutableDefinitions; -import graphql.validation.rules.FieldsOnCorrectType; -import graphql.validation.rules.FragmentsOnCompositeType; -import graphql.validation.rules.KnownArgumentNames; -import graphql.validation.rules.KnownDirectives; -import graphql.validation.rules.KnownFragmentNames; -import graphql.validation.rules.KnownTypeNames; -import graphql.validation.rules.LoneAnonymousOperation; -import graphql.validation.rules.NoFragmentCycles; -import graphql.validation.rules.NoUndefinedVariables; -import graphql.validation.rules.NoUnusedFragments; -import graphql.validation.rules.NoUnusedVariables; -import graphql.validation.rules.OverlappingFieldsCanBeMerged; -import graphql.validation.rules.PossibleFragmentSpreads; -import graphql.validation.rules.ProvidedNonNullArguments; -import graphql.validation.rules.ScalarLeaves; -import graphql.validation.rules.SubscriptionUniqueRootField; -import graphql.validation.rules.UniqueArgumentNames; -import graphql.validation.rules.UniqueDirectiveNamesPerLocation; -import graphql.validation.rules.UniqueFragmentNames; -import graphql.validation.rules.UniqueOperationNames; -import graphql.validation.rules.UniqueVariableNames; -import graphql.validation.rules.VariableDefaultValuesOfCorrectType; -import graphql.validation.rules.VariableTypesMatch; -import graphql.validation.rules.VariablesAreInputTypes; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.function.Predicate; -import java.util.stream.Collectors; @Internal public class Validator { @@ -66,113 +33,22 @@ public static int getMaxValidationErrors() { } public List validateDocument(GraphQLSchema schema, Document document, Locale locale) { - return validateDocument(schema, document, ruleClass -> true, locale); + return validateDocument(schema, document, rule -> true, locale); } - public List validateDocument(GraphQLSchema schema, Document document, Predicate> applyRule, Locale locale) { + public List validateDocument(GraphQLSchema schema, Document document, Predicate rulePredicate, Locale locale) { I18n i18n = I18n.i18n(I18n.BundleType.Validation, locale); ValidationContext validationContext = new ValidationContext(schema, document, i18n); ValidationErrorCollector validationErrorCollector = new ValidationErrorCollector(MAX_VALIDATION_ERRORS); - List rules = createRules(validationContext, validationErrorCollector); - // filter out any rules they don't want applied - rules = rules.stream().filter(r -> applyRule.test(r.getClass())).collect(Collectors.toList()); + OperationValidator operationValidator = new OperationValidator(validationContext, validationErrorCollector, rulePredicate); LanguageTraversal languageTraversal = new LanguageTraversal(); try { - languageTraversal.traverse(document, new RulesVisitor(validationContext, rules)); + languageTraversal.traverse(document, operationValidator); } catch (ValidationErrorCollector.MaxValidationErrorsReached ignored) { // if we have generated enough errors, then we can shortcut out } return validationErrorCollector.getErrors(); } - - public List createRules(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - List rules = new ArrayList<>(); - - ExecutableDefinitions executableDefinitions = new ExecutableDefinitions(validationContext, validationErrorCollector); - rules.add(executableDefinitions); - - ArgumentsOfCorrectType argumentsOfCorrectType = new ArgumentsOfCorrectType(validationContext, validationErrorCollector); - rules.add(argumentsOfCorrectType); - - FieldsOnCorrectType fieldsOnCorrectType = new FieldsOnCorrectType(validationContext, validationErrorCollector); - rules.add(fieldsOnCorrectType); - FragmentsOnCompositeType fragmentsOnCompositeType = new FragmentsOnCompositeType(validationContext, validationErrorCollector); - rules.add(fragmentsOnCompositeType); - - KnownArgumentNames knownArgumentNames = new KnownArgumentNames(validationContext, validationErrorCollector); - rules.add(knownArgumentNames); - KnownDirectives knownDirectives = new KnownDirectives(validationContext, validationErrorCollector); - rules.add(knownDirectives); - KnownFragmentNames knownFragmentNames = new KnownFragmentNames(validationContext, validationErrorCollector); - rules.add(knownFragmentNames); - KnownTypeNames knownTypeNames = new KnownTypeNames(validationContext, validationErrorCollector); - rules.add(knownTypeNames); - - NoFragmentCycles noFragmentCycles = new NoFragmentCycles(validationContext, validationErrorCollector); - rules.add(noFragmentCycles); - NoUndefinedVariables noUndefinedVariables = new NoUndefinedVariables(validationContext, validationErrorCollector); - rules.add(noUndefinedVariables); - NoUnusedFragments noUnusedFragments = new NoUnusedFragments(validationContext, validationErrorCollector); - rules.add(noUnusedFragments); - NoUnusedVariables noUnusedVariables = new NoUnusedVariables(validationContext, validationErrorCollector); - rules.add(noUnusedVariables); - - OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, validationErrorCollector); - rules.add(overlappingFieldsCanBeMerged); - - PossibleFragmentSpreads possibleFragmentSpreads = new PossibleFragmentSpreads(validationContext, validationErrorCollector); - rules.add(possibleFragmentSpreads); - ProvidedNonNullArguments providedNonNullArguments = new ProvidedNonNullArguments(validationContext, validationErrorCollector); - rules.add(providedNonNullArguments); - - ScalarLeaves scalarLeaves = new ScalarLeaves(validationContext, validationErrorCollector); - rules.add(scalarLeaves); - - VariableDefaultValuesOfCorrectType variableDefaultValuesOfCorrectType = new VariableDefaultValuesOfCorrectType(validationContext, validationErrorCollector); - rules.add(variableDefaultValuesOfCorrectType); - VariablesAreInputTypes variablesAreInputTypes = new VariablesAreInputTypes(validationContext, validationErrorCollector); - rules.add(variablesAreInputTypes); - VariableTypesMatch variableTypesMatch = new VariableTypesMatch(validationContext, validationErrorCollector); - rules.add(variableTypesMatch); - - LoneAnonymousOperation loneAnonymousOperation = new LoneAnonymousOperation(validationContext, validationErrorCollector); - rules.add(loneAnonymousOperation); - - UniqueOperationNames uniqueOperationNames = new UniqueOperationNames(validationContext, validationErrorCollector); - rules.add(uniqueOperationNames); - - UniqueFragmentNames uniqueFragmentNames = new UniqueFragmentNames(validationContext, validationErrorCollector); - rules.add(uniqueFragmentNames); - - UniqueDirectiveNamesPerLocation uniqueDirectiveNamesPerLocation = new UniqueDirectiveNamesPerLocation(validationContext, validationErrorCollector); - rules.add(uniqueDirectiveNamesPerLocation); - - UniqueArgumentNames uniqueArgumentNamesRule = new UniqueArgumentNames(validationContext, validationErrorCollector); - rules.add(uniqueArgumentNamesRule); - - UniqueVariableNames uniqueVariableNamesRule = new UniqueVariableNames(validationContext, validationErrorCollector); - rules.add(uniqueVariableNamesRule); - - SubscriptionUniqueRootField uniqueSubscriptionRootField = new SubscriptionUniqueRootField(validationContext, validationErrorCollector); - rules.add(uniqueSubscriptionRootField); - - UniqueObjectFieldName uniqueObjectFieldName = new UniqueObjectFieldName(validationContext, validationErrorCollector); - rules.add(uniqueObjectFieldName); - - DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, validationErrorCollector); - rules.add(deferDirectiveOnRootLevel); - - DeferDirectiveOnValidOperation deferDirectiveOnValidOperation = new DeferDirectiveOnValidOperation(validationContext, validationErrorCollector); - rules.add(deferDirectiveOnValidOperation); - - DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector); - rules.add(deferDirectiveLabel); - - KnownOperationTypes knownOperationTypes = new KnownOperationTypes(validationContext, validationErrorCollector); - rules.add(knownOperationTypes); - - return rules; - } } diff --git a/src/main/java/graphql/validation/rules/VariablesTypesMatcher.java b/src/main/java/graphql/validation/VariablesTypesMatcher.java similarity index 98% rename from src/main/java/graphql/validation/rules/VariablesTypesMatcher.java rename to src/main/java/graphql/validation/VariablesTypesMatcher.java index 0b47fcec78..854f553f9d 100644 --- a/src/main/java/graphql/validation/rules/VariablesTypesMatcher.java +++ b/src/main/java/graphql/validation/VariablesTypesMatcher.java @@ -1,4 +1,4 @@ -package graphql.validation.rules; +package graphql.validation; import graphql.Internal; diff --git a/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java b/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java deleted file mode 100644 index ab54c59d3a..0000000000 --- a/src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java +++ /dev/null @@ -1,38 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.Argument; -import graphql.schema.GraphQLArgument; -import graphql.validation.AbstractRule; -import graphql.validation.ArgumentValidationUtil; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationError; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.WrongType; - -@Internal -public class ArgumentsOfCorrectType extends AbstractRule { - - public ArgumentsOfCorrectType(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkArgument(Argument argument) { - GraphQLArgument fieldArgument = getValidationContext().getArgument(); - if (fieldArgument == null) { - return; - } - ArgumentValidationUtil validationUtil = new ArgumentValidationUtil(argument); - if (!validationUtil.isValidLiteralValue(argument.getValue(), fieldArgument.getType(), getValidationContext().getSchema(), getValidationContext().getGraphQLContext(), getValidationContext().getI18n().getLocale())) { - String message = i18n(WrongType, validationUtil.getMsgAndArgs()); - addError(ValidationError.newValidationError() - .validationErrorType(WrongType) - .sourceLocation(argument.getSourceLocation()) - .description(message) - .extensions(validationUtil.getErrorExtensions())); - } - } -} diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java b/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java deleted file mode 100644 index fdf85fda82..0000000000 --- a/src/main/java/graphql/validation/rules/DeferDirectiveLabel.java +++ /dev/null @@ -1,66 +0,0 @@ -package graphql.validation.rules; - -import graphql.Directives; -import graphql.ExperimentalApi; -import graphql.language.Argument; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.language.NullValue; -import graphql.language.StringValue; -import graphql.language.Value; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateIncrementalLabel; -import static graphql.validation.ValidationErrorType.WrongType; - -/** - * Defer and stream directive labels are unique - * - * A GraphQL document is only valid if defer and stream directives' label argument is static and unique. - * - * See proposed spec:spec/Section 5 -- Validation.md ### ### Defer And Stream Directive Labels Are Unique - */ -@ExperimentalApi -public class DeferDirectiveLabel extends AbstractRule { - private Set checkedLabels = new LinkedHashSet<>(); - public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkDirective(Directive directive, List ancestors) { - // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true - if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT) || - !Directives.DeferDirective.getName().equals(directive.getName()) || - directive.getArguments().size() == 0) { - return; - } - - Argument labelArgument = directive.getArgument("label"); - if (labelArgument == null || labelArgument.getValue() instanceof NullValue){ - return; - } - Value labelArgumentValue = labelArgument.getValue(); - - if (!(labelArgumentValue instanceof StringValue)) { - String message = i18n(WrongType, "DeferDirective.labelMustBeStaticString"); - addError(WrongType, directive.getSourceLocation(), message); - } else { - if (checkedLabels.contains(((StringValue) labelArgumentValue).getValue())) { - String message = i18n(DuplicateIncrementalLabel, "IncrementalDirective.uniqueArgument", labelArgument.getName(), directive.getName()); - addError(DuplicateIncrementalLabel, directive.getSourceLocation(), message); - } else { - checkedLabels.add(((StringValue) labelArgumentValue).getValue()); - } - } - } - - - -} \ No newline at end of file diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java deleted file mode 100644 index 39586a265b..0000000000 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.validation.rules; - -import graphql.Directives; -import graphql.ExperimentalApi; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.language.OperationDefinition; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLObjectType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.MisplacedDirective; - -/** - * Defer and stream directives are used on valid root field - * - * A GraphQL document is only valid if defer directives are not used on root mutation or subscription types. - * - * See proposed spec:spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Root Field - */ -@ExperimentalApi -public class DeferDirectiveOnRootLevel extends AbstractRule { - private Set invalidOperations = new LinkedHashSet(Arrays.asList(OperationDefinition.Operation.MUTATION, OperationDefinition.Operation.SUBSCRIPTION)); - public DeferDirectiveOnRootLevel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - this.setVisitFragmentSpreads(true); - } - - @Override - public void checkDirective(Directive directive, List ancestors) { - // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true - if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { - return; - } - - if (!Directives.DeferDirective.getName().equals(directive.getName())) { - return; - } - GraphQLObjectType mutationType = getValidationContext().getSchema().getMutationType(); - GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); - GraphQLCompositeType parentType = getValidationContext().getParentType(); - if (mutationType != null && parentType != null && parentType.getName().equals(mutationType.getName())){ - String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelMutation", parentType.getName()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } else if (subscriptionType != null && parentType != null && parentType.getName().equals(subscriptionType.getName())) { - String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelSubscription", parentType.getName()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } - } - -} diff --git a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java b/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java deleted file mode 100644 index 6baa9df948..0000000000 --- a/src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java +++ /dev/null @@ -1,84 +0,0 @@ -package graphql.validation.rules; - -import graphql.Directives; -import graphql.ExperimentalApi; -import graphql.language.Argument; -import graphql.language.BooleanValue; -import graphql.language.Directive; -import graphql.language.Node; -import graphql.language.OperationDefinition; -import graphql.language.VariableReference; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.List; -import java.util.Optional; - -import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; -import static graphql.validation.ValidationErrorType.MisplacedDirective; - -/** - * Defer Directive is Used On Valid Operations - * - * A GraphQL document is only valid if defer directives are not used on subscription types. - * - * See proposed spec:spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Operations - * - */ -@ExperimentalApi -public class DeferDirectiveOnValidOperation extends AbstractRule { - public DeferDirectiveOnValidOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - this.setVisitFragmentSpreads(true); - } - - - @Override - public void checkDirective(Directive directive, List ancestors) { - // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true - if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { - return; - } - - if (!Directives.DeferDirective.getName().equals(directive.getName())) { - return; - } - // check if the directive is on allowed operation - Optional operationDefinition = getOperationDefinition(ancestors); - if (operationDefinition.isPresent() && - SUBSCRIPTION.equals(operationDefinition.get().getOperation()) && - !ifArgumentMightBeFalse(directive) ){ - String message = i18n(MisplacedDirective, "IncrementalDirective.notAllowedSubscriptionOperation", directive.getName()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } - } - - /** - * Extract from ancestors the OperationDefinition using the document ancestor. - * @param ancestors list of ancestors - * @return Optional of OperationDefinition - */ - private Optional getOperationDefinition(List ancestors) { - return ancestors.stream() - .filter(doc -> doc instanceof OperationDefinition) - .map((def -> (OperationDefinition) def)) - .findFirst(); - } - - private Boolean ifArgumentMightBeFalse(Directive directive) { - Argument ifArgument = directive.getArgumentsByName().get("if"); - if (ifArgument == null) { - return false; - } - if(ifArgument.getValue() instanceof BooleanValue){ - return !((BooleanValue) ifArgument.getValue()).isValue(); - } - if(ifArgument.getValue() instanceof VariableReference){ - return true; - } - return false; - } - -} - diff --git a/src/main/java/graphql/validation/rules/ExecutableDefinitions.java b/src/main/java/graphql/validation/rules/ExecutableDefinitions.java deleted file mode 100644 index 9a81f9c4aa..0000000000 --- a/src/main/java/graphql/validation/rules/ExecutableDefinitions.java +++ /dev/null @@ -1,53 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Definition; -import graphql.language.DirectiveDefinition; -import graphql.language.Document; -import graphql.language.FragmentDefinition; -import graphql.language.OperationDefinition; -import graphql.language.SchemaDefinition; -import graphql.language.TypeDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.NonExecutableDefinition; - -@Internal -public class ExecutableDefinitions extends AbstractRule { - - public ExecutableDefinitions(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - /** - * Executable definitions - * - * A GraphQL document is only valid for execution if all definitions are either - * operation or fragment definitions. - */ - @Override - public void checkDocument(Document document) { - document.getDefinitions().forEach(definition -> { - if (!(definition instanceof OperationDefinition) - && !(definition instanceof FragmentDefinition)) { - - String message = nonExecutableDefinitionMessage(definition); - addError(NonExecutableDefinition, definition.getSourceLocation(), message); - } - }); - } - - private String nonExecutableDefinitionMessage(Definition definition) { - if (definition instanceof TypeDefinition) { - return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableType", ((TypeDefinition) definition).getName()); - } else if (definition instanceof SchemaDefinition) { - return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableSchema"); - } else if (definition instanceof DirectiveDefinition) { - return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableDirective", ((DirectiveDefinition) definition).getName()); - } - - return i18n(NonExecutableDefinition, "ExecutableDefinitions.notExecutableDefinition"); - } -} diff --git a/src/main/java/graphql/validation/rules/FieldsOnCorrectType.java b/src/main/java/graphql/validation/rules/FieldsOnCorrectType.java deleted file mode 100644 index df9a403e26..0000000000 --- a/src/main/java/graphql/validation/rules/FieldsOnCorrectType.java +++ /dev/null @@ -1,35 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.Field; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLFieldDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.FieldUndefined; - -@Internal -public class FieldsOnCorrectType extends AbstractRule { - - - public FieldsOnCorrectType(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - - @Override - public void checkField(Field field) { - GraphQLCompositeType parentType = getValidationContext().getParentType(); - // this means the parent type is not a CompositeType, which is an error handled elsewhere - if (parentType == null) return; - GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); - if (fieldDef == null) { - String message = i18n(FieldUndefined, "FieldsOnCorrectType.unknownField", field.getName(), parentType.getName()); - addError(FieldUndefined, field.getSourceLocation(), message); - } - - } -} diff --git a/src/main/java/graphql/validation/rules/FragmentsOnCompositeType.java b/src/main/java/graphql/validation/rules/FragmentsOnCompositeType.java deleted file mode 100644 index fd7cd3c21f..0000000000 --- a/src/main/java/graphql/validation/rules/FragmentsOnCompositeType.java +++ /dev/null @@ -1,46 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.FragmentDefinition; -import graphql.language.InlineFragment; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.FragmentTypeConditionInvalid; -import static graphql.validation.ValidationErrorType.InlineFragmentTypeConditionInvalid; - -@Internal -public class FragmentsOnCompositeType extends AbstractRule { - - - public FragmentsOnCompositeType(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkInlineFragment(InlineFragment inlineFragment) { - if (inlineFragment.getTypeCondition() == null) { - return; - } - GraphQLType type = getValidationContext().getSchema().getType(inlineFragment.getTypeCondition().getName()); - if (type == null) return; - if (!(type instanceof GraphQLCompositeType)) { - String message = i18n(InlineFragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidInlineTypeCondition"); - addError(InlineFragmentTypeConditionInvalid, inlineFragment.getSourceLocation(), message); - } - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - GraphQLType type = getValidationContext().getSchema().getType(fragmentDefinition.getTypeCondition().getName()); - if (type == null) return; - if (!(type instanceof GraphQLCompositeType)) { - String message = i18n(FragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidFragmentTypeCondition"); - addError(FragmentTypeConditionInvalid, fragmentDefinition.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/KnownArgumentNames.java b/src/main/java/graphql/validation/rules/KnownArgumentNames.java deleted file mode 100644 index 8f123f7412..0000000000 --- a/src/main/java/graphql/validation/rules/KnownArgumentNames.java +++ /dev/null @@ -1,45 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Argument; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLFieldDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.UnknownArgument; -import static graphql.validation.ValidationErrorType.UnknownDirective; - - -@Internal -public class KnownArgumentNames extends AbstractRule { - - public KnownArgumentNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - - @Override - public void checkArgument(Argument argument) { - GraphQLDirective directiveDef = getValidationContext().getDirective(); - if (directiveDef != null) { - GraphQLArgument directiveArgument = directiveDef.getArgument(argument.getName()); - if (directiveArgument == null) { - String message = i18n(UnknownDirective, "KnownArgumentNames.unknownDirectiveArg", argument.getName()); - addError(UnknownDirective, argument.getSourceLocation(), message); - } - - return; - } - - GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); - if (fieldDef == null) return; - GraphQLArgument fieldArgument = fieldDef.getArgument(argument.getName()); - if (fieldArgument == null) { - String message = i18n(UnknownArgument, "KnownArgumentNames.unknownFieldArg", argument.getName()); - addError(UnknownArgument, argument.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/KnownDirectives.java b/src/main/java/graphql/validation/rules/KnownDirectives.java deleted file mode 100644 index eb011fb682..0000000000 --- a/src/main/java/graphql/validation/rules/KnownDirectives.java +++ /dev/null @@ -1,75 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.introspection.Introspection.DirectiveLocation; -import graphql.language.Directive; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.Node; -import graphql.language.OperationDefinition; -import graphql.language.OperationDefinition.Operation; -import graphql.language.VariableDefinition; -import graphql.schema.GraphQLDirective; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.List; -import java.util.EnumSet; - -import static graphql.validation.ValidationErrorType.MisplacedDirective; -import static graphql.validation.ValidationErrorType.UnknownDirective; - -@Internal -public class KnownDirectives extends AbstractRule { - - - public KnownDirectives(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkDirective(Directive directive, List ancestors) { - GraphQLDirective graphQLDirective = getValidationContext().getSchema().getDirective(directive.getName()); - if (graphQLDirective == null) { - String message = i18n(UnknownDirective, "KnownDirectives.unknownDirective", directive.getName()); - addError(UnknownDirective, directive.getSourceLocation(), message); - return; - } - - Node ancestor = ancestors.get(ancestors.size() - 1); - if (hasInvalidLocation(graphQLDirective, ancestor)) { - String message = i18n(MisplacedDirective, "KnownDirectives.directiveNotAllowed", directive.getName()); - addError(MisplacedDirective, directive.getSourceLocation(), message); - } - } - - @SuppressWarnings("deprecation") // the suppression stands because it's deprecated but still in graphql spec - private boolean hasInvalidLocation(GraphQLDirective directive, Node ancestor) { - EnumSet validLocations = directive.validLocations(); - if (ancestor instanceof OperationDefinition) { - Operation operation = ((OperationDefinition) ancestor).getOperation(); - if (Operation.QUERY.equals(operation)) { - return !validLocations.contains(DirectiveLocation.QUERY); - } else if (Operation.MUTATION.equals(operation)) { - return !validLocations.contains(DirectiveLocation.MUTATION); - } else if (Operation.SUBSCRIPTION.equals(operation)) { - return !validLocations.contains(DirectiveLocation.SUBSCRIPTION); - } - } else if (ancestor instanceof Field) { - return !(validLocations.contains(DirectiveLocation.FIELD)); - } else if (ancestor instanceof FragmentSpread) { - return !(validLocations.contains(DirectiveLocation.FRAGMENT_SPREAD)); - } else if (ancestor instanceof FragmentDefinition) { - return !(validLocations.contains(DirectiveLocation.FRAGMENT_DEFINITION)); - } else if (ancestor instanceof InlineFragment) { - return !(validLocations.contains(DirectiveLocation.INLINE_FRAGMENT)); - } else if (ancestor instanceof VariableDefinition) { - return !(validLocations.contains(DirectiveLocation.VARIABLE_DEFINITION)); - } - return true; - } -} diff --git a/src/main/java/graphql/validation/rules/KnownFragmentNames.java b/src/main/java/graphql/validation/rules/KnownFragmentNames.java deleted file mode 100644 index 0ec66508a0..0000000000 --- a/src/main/java/graphql/validation/rules/KnownFragmentNames.java +++ /dev/null @@ -1,28 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.UndefinedFragment; - -@Internal -public class KnownFragmentNames extends AbstractRule { - - public KnownFragmentNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkFragmentSpread(FragmentSpread fragmentSpread) { - FragmentDefinition fragmentDefinition = getValidationContext().getFragment(fragmentSpread.getName()); - if (fragmentDefinition == null) { - String message = i18n(UndefinedFragment, "KnownFragmentNames.undefinedFragment", fragmentSpread.getName()); - addError(UndefinedFragment, fragmentSpread.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/KnownOperationTypes.java b/src/main/java/graphql/validation/rules/KnownOperationTypes.java deleted file mode 100644 index 2ef9af48fe..0000000000 --- a/src/main/java/graphql/validation/rules/KnownOperationTypes.java +++ /dev/null @@ -1,48 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.OperationDefinition; -import graphql.schema.GraphQLSchema; -import graphql.util.StringKit; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.UnknownOperation; - -/** - * Unique variable names - *

- * A GraphQL operation is only valid if all its variables are uniquely named. - */ -@Internal -public class KnownOperationTypes extends AbstractRule { - - public KnownOperationTypes(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - OperationDefinition.Operation documentOperation = operationDefinition.getOperation(); - GraphQLSchema graphQLSchema = getValidationContext().getSchema(); - if (documentOperation == OperationDefinition.Operation.MUTATION - && graphQLSchema.getMutationType() == null) { - String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); - addError(UnknownOperation, operationDefinition.getSourceLocation(), message); - } else if (documentOperation == OperationDefinition.Operation.SUBSCRIPTION - && graphQLSchema.getSubscriptionType() == null) { - String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); - addError(UnknownOperation, operationDefinition.getSourceLocation(), message); - } else if (documentOperation == OperationDefinition.Operation.QUERY - && graphQLSchema.getQueryType() == null) { - // This is unlikely to happen, as a validated GraphQLSchema must have a Query type by definition - String message = i18n(UnknownOperation, "KnownOperationTypes.noOperation", formatOperation(documentOperation)); - addError(UnknownOperation, operationDefinition.getSourceLocation(), message); - } - } - - private String formatOperation(OperationDefinition.Operation operation) { - return StringKit.capitalize(operation.name().toLowerCase()); - } -} diff --git a/src/main/java/graphql/validation/rules/KnownTypeNames.java b/src/main/java/graphql/validation/rules/KnownTypeNames.java deleted file mode 100644 index d28db91925..0000000000 --- a/src/main/java/graphql/validation/rules/KnownTypeNames.java +++ /dev/null @@ -1,27 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.TypeName; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.validation.ValidationErrorType.UnknownType; - -@Internal -public class KnownTypeNames extends AbstractRule { - - - public KnownTypeNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkTypeName(TypeName typeName) { - if ((getValidationContext().getSchema().getType(typeName.getName())) == null) { - String message = i18n(UnknownType, "KnownTypeNames.unknownType", typeName.getName()); - addError(UnknownType, typeName.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/LoneAnonymousOperation.java b/src/main/java/graphql/validation/rules/LoneAnonymousOperation.java deleted file mode 100644 index e89bec68e9..0000000000 --- a/src/main/java/graphql/validation/rules/LoneAnonymousOperation.java +++ /dev/null @@ -1,48 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Document; -import graphql.language.OperationDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; - -import static graphql.validation.ValidationErrorType.LoneAnonymousOperationViolation; - -@Internal -public class LoneAnonymousOperation extends AbstractRule { - - boolean hasAnonymousOp = false; - int count = 0; - - public LoneAnonymousOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - super.checkOperationDefinition(operationDefinition); - String name = operationDefinition.getName(); - - if (name == null) { - hasAnonymousOp = true; - if (count > 0) { - String message = i18n(LoneAnonymousOperationViolation, "LoneAnonymousOperation.withOthers"); - addError(ValidationErrorType.LoneAnonymousOperationViolation, operationDefinition.getSourceLocation(), message); - } - } else { - if (hasAnonymousOp) { - String message = i18n(LoneAnonymousOperationViolation, "LoneAnonymousOperation.namedOperation", name); - addError(ValidationErrorType.LoneAnonymousOperationViolation, operationDefinition.getSourceLocation(), message); - } - } - count++; - } - - @Override - public void documentFinished(Document document) { - super.documentFinished(document); - hasAnonymousOp = false; - } -} diff --git a/src/main/java/graphql/validation/rules/NoFragmentCycles.java b/src/main/java/graphql/validation/rules/NoFragmentCycles.java deleted file mode 100644 index 32c0f751b5..0000000000 --- a/src/main/java/graphql/validation/rules/NoFragmentCycles.java +++ /dev/null @@ -1,118 +0,0 @@ -package graphql.validation.rules; - - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import graphql.Internal; -import graphql.language.Definition; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.Node; -import graphql.validation.AbstractRule; -import graphql.validation.DocumentVisitor; -import graphql.validation.LanguageTraversal; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; -import graphql.validation.ValidationErrorType; - -import static graphql.validation.ValidationErrorType.FragmentCycle; - -@Internal -public class NoFragmentCycles extends AbstractRule { - - private final Map> fragmentSpreads = new HashMap<>(); - - public NoFragmentCycles(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - prepareFragmentMap(); - } - - private void prepareFragmentMap() { - List definitions = getValidationContext().getDocument().getDefinitions(); - for (Definition definition : definitions) { - if (definition instanceof FragmentDefinition) { - FragmentDefinition fragmentDefinition = (FragmentDefinition) definition; - fragmentSpreads.put(fragmentDefinition.getName(), gatherSpreads(fragmentDefinition)); - } - } - } - - private Set gatherSpreads(FragmentDefinition fragmentDefinition) { - final Set fragmentSpreads = new HashSet<>(); - DocumentVisitor visitor = new DocumentVisitor() { - @Override - public void enter(Node node, List path) { - if (node instanceof FragmentSpread) { - fragmentSpreads.add(((FragmentSpread) node).getName()); - } - } - - @Override - public void leave(Node node, List path) { - - } - }; - - new LanguageTraversal().traverse(fragmentDefinition, visitor); - return fragmentSpreads; - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - LinkedList path = new LinkedList<>(); - path.add(0, fragmentDefinition.getName()); - Map> transitiveSpreads = buildTransitiveSpreads(path, new HashMap<>()); - - for (Map.Entry> entry : transitiveSpreads.entrySet()) { - if (entry.getValue().contains(entry.getKey())) { - String message = i18n(FragmentCycle, "NoFragmentCycles.cyclesNotAllowed"); - addError(ValidationErrorType.FragmentCycle, Collections.singletonList(fragmentDefinition), message); - } - } - } - - private Map> buildTransitiveSpreads(LinkedList path, Map> transitiveSpreads) { - String name = path.peekFirst(); - - if (transitiveSpreads.containsKey(name)) { - return transitiveSpreads; - } - - Set spreads = fragmentSpreads.get(name); - - // spreads may be null when there is no corresponding FragmentDefinition for this spread. - // This will be handled by KnownFragmentNames - if (spreads == null || spreads.isEmpty()) { - return transitiveSpreads; - } - - // Add the current spreads to the transitive spreads of each ancestor in the traversal path - for (String ancestor : path) { - Set ancestorSpreads = transitiveSpreads.get(ancestor); - if (ancestorSpreads == null) { - ancestorSpreads = new HashSet<>(); - } - ancestorSpreads.addAll(spreads); - transitiveSpreads.put(ancestor, ancestorSpreads); - } - - for (String child : spreads) { - // don't recurse infinitely, expect the recursion check to happen in checkFragmentDefinition - if (path.contains(child) || transitiveSpreads.containsKey(child)) { - continue; - } - - // descend into each spread in the current fragment - LinkedList childPath = new LinkedList<>(path); - childPath.add(0, child); - buildTransitiveSpreads(childPath, transitiveSpreads); - } - return transitiveSpreads; - } -} diff --git a/src/main/java/graphql/validation/rules/NoUndefinedVariables.java b/src/main/java/graphql/validation/rules/NoUndefinedVariables.java deleted file mode 100644 index f0592bb7df..0000000000 --- a/src/main/java/graphql/validation/rules/NoUndefinedVariables.java +++ /dev/null @@ -1,50 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.FragmentDefinition; -import graphql.language.OperationDefinition; -import graphql.language.VariableDefinition; -import graphql.language.VariableReference; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.UndefinedVariable; - -@Internal -public class NoUndefinedVariables extends AbstractRule { - - private final Set variableNames = new LinkedHashSet<>(); - - public NoUndefinedVariables(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - setVisitFragmentSpreads(true); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - variableNames.clear(); - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - super.checkFragmentDefinition(fragmentDefinition); - } - - @Override - public void checkVariable(VariableReference variableReference) { - if (!variableNames.contains(variableReference.getName())) { - String message = i18n(UndefinedVariable, "NoUndefinedVariables.undefinedVariable", variableReference.getName()); - addError(UndefinedVariable, variableReference.getSourceLocation(), message); - } - } - - @Override - public void checkVariableDefinition(VariableDefinition variableDefinition) { - variableNames.add(variableDefinition.getName()); - } -} diff --git a/src/main/java/graphql/validation/rules/NoUnusedFragments.java b/src/main/java/graphql/validation/rules/NoUnusedFragments.java deleted file mode 100644 index c0a61a50a6..0000000000 --- a/src/main/java/graphql/validation/rules/NoUnusedFragments.java +++ /dev/null @@ -1,85 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.Document; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.OperationDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.validation.ValidationErrorType.UnusedFragment; - -@Internal -public class NoUnusedFragments extends AbstractRule { - - - private final List allDeclaredFragments = new ArrayList<>(); - - private List usedFragments = new ArrayList<>(); - private final Map> spreadsInDefinition = new LinkedHashMap<>(); - private final List> fragmentsUsedDirectlyInOperation = new ArrayList<>(); - - public NoUnusedFragments(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - usedFragments = new ArrayList<>(); - fragmentsUsedDirectlyInOperation.add(usedFragments); - } - - - @Override - public void checkFragmentSpread(FragmentSpread fragmentSpread) { - usedFragments.add(fragmentSpread.getName()); - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - allDeclaredFragments.add(fragmentDefinition); - usedFragments = new ArrayList<>(); - spreadsInDefinition.put(fragmentDefinition.getName(), usedFragments); - } - - @Override - public void documentFinished(Document document) { - - List allUsedFragments = new ArrayList<>(); - for (List fragmentsInOneOperation : fragmentsUsedDirectlyInOperation) { - for (String fragment : fragmentsInOneOperation) { - collectUsedFragmentsInDefinition(allUsedFragments, fragment); - } - } - - for (FragmentDefinition fragmentDefinition : allDeclaredFragments) { - if (!allUsedFragments.contains(fragmentDefinition.getName())) { - String message = i18n(UnusedFragment, "NoUnusedFragments.unusedFragments", fragmentDefinition.getName()); - addError(UnusedFragment, fragmentDefinition.getSourceLocation(), message); - } - } - - } - - private void collectUsedFragmentsInDefinition(List result, String fragmentName) { - if (result.contains(fragmentName)) return; - result.add(fragmentName); - List spreadList = spreadsInDefinition.get(fragmentName); - if (spreadList == null) { - return; - } - for (String fragment : spreadList) { - collectUsedFragmentsInDefinition(result, fragment); - } - - } - -} \ No newline at end of file diff --git a/src/main/java/graphql/validation/rules/NoUnusedVariables.java b/src/main/java/graphql/validation/rules/NoUnusedVariables.java deleted file mode 100644 index 80e3aa9429..0000000000 --- a/src/main/java/graphql/validation/rules/NoUnusedVariables.java +++ /dev/null @@ -1,55 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.OperationDefinition; -import graphql.language.VariableDefinition; -import graphql.language.VariableReference; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.UnusedVariable; - -@Internal -public class NoUnusedVariables extends AbstractRule { - - private final List variableDefinitions = new ArrayList<>(); - private final Set usedVariables = new LinkedHashSet<>(); - - public NoUnusedVariables(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - setVisitFragmentSpreads(true); - } - - @Override - public void leaveOperationDefinition(OperationDefinition operationDefinition) { - for (VariableDefinition variableDefinition : variableDefinitions) { - if (!usedVariables.contains(variableDefinition.getName())) { - String message = i18n(UnusedVariable, "NoUnusedVariables.unusedVariable", variableDefinition.getName()); - addError(UnusedVariable, variableDefinition.getSourceLocation(), message); - } - } - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - usedVariables.clear(); - variableDefinitions.clear(); - } - - @Override - public void checkVariableDefinition(VariableDefinition variableDefinition) { - variableDefinitions.add(variableDefinition); - } - - @Override - public void checkVariable(VariableReference variableReference) { - usedVariables.add(variableReference.getName()); - } -} diff --git a/src/main/java/graphql/validation/rules/OverlappingFieldsCanBeMerged.java b/src/main/java/graphql/validation/rules/OverlappingFieldsCanBeMerged.java deleted file mode 100644 index aa877d8cfd..0000000000 --- a/src/main/java/graphql/validation/rules/OverlappingFieldsCanBeMerged.java +++ /dev/null @@ -1,404 +0,0 @@ -package graphql.validation.rules; - - -import com.google.common.collect.ImmutableList; -import graphql.Internal; -import graphql.execution.TypeFromAST; -import graphql.language.Argument; -import graphql.language.AstComparator; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.OperationDefinition; -import graphql.language.Selection; -import graphql.language.SelectionSet; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLUnionType; -import graphql.schema.GraphQLUnmodifiedType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import static graphql.collect.ImmutableKit.addToList; -import static graphql.collect.ImmutableKit.emptyList; -import static graphql.schema.GraphQLTypeUtil.isEnum; -import static graphql.schema.GraphQLTypeUtil.isList; -import static graphql.schema.GraphQLTypeUtil.isNonNull; -import static graphql.schema.GraphQLTypeUtil.isNotWrapped; -import static graphql.schema.GraphQLTypeUtil.isNullable; -import static graphql.schema.GraphQLTypeUtil.isScalar; -import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.schema.GraphQLTypeUtil.unwrapAll; -import static graphql.schema.GraphQLTypeUtil.unwrapOne; -import static graphql.util.FpKit.filterSet; -import static graphql.util.FpKit.groupingBy; -import static graphql.validation.ValidationErrorType.FieldsConflict; - -@Internal -public class OverlappingFieldsCanBeMerged extends AbstractRule { - - - private final Set> sameResponseShapeChecked = new LinkedHashSet<>(); - private final Set> sameForCommonParentsChecked = new LinkedHashSet<>(); - private final Set> conflictsReported = new LinkedHashSet<>(); - - public OverlappingFieldsCanBeMerged(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - super.checkOperationDefinition(operationDefinition); - impl(operationDefinition.getSelectionSet(), getValidationContext().getOutputType()); - } - - public void impl(SelectionSet selectionSet, GraphQLOutputType graphQLOutputType) { - Map> fieldMap = new LinkedHashMap<>(); - Set visitedFragmentSpreads = new LinkedHashSet<>(); - collectFields(fieldMap, selectionSet, graphQLOutputType, visitedFragmentSpreads); - List conflicts = findConflicts(fieldMap); - for (Conflict conflict : conflicts) { - if (conflictsReported.contains(conflict.fields)) { - continue; - } - conflictsReported.add(conflict.fields); - // each error contains a reference to the current querypath via validationContext.getQueryPath() - // queryPath is null for the first selection set - addError(FieldsConflict, conflict.fields, conflict.reason); - } - } - - private void collectFields(Map> fieldMap, SelectionSet selectionSet, GraphQLType parentType, Set visitedFragmentSpreads) { - - for (Selection selection : selectionSet.getSelections()) { - if (selection instanceof Field) { - collectFieldsForField(fieldMap, parentType, (Field) selection); - - } else if (selection instanceof InlineFragment) { - collectFieldsForInlineFragment(fieldMap, visitedFragmentSpreads, parentType, (InlineFragment) selection); - - } else if (selection instanceof FragmentSpread) { - collectFieldsForFragmentSpread(fieldMap, visitedFragmentSpreads, (FragmentSpread) selection); - } - } - } - - private void collectFieldsForFragmentSpread(Map> fieldMap, Set visitedFragmentSpreads, FragmentSpread fragmentSpread) { - FragmentDefinition fragment = getValidationContext().getFragment(fragmentSpread.getName()); - if (fragment == null) { - return; - } - if (visitedFragmentSpreads.contains(fragment.getName())) { - return; - } - visitedFragmentSpreads.add(fragment.getName()); - GraphQLType graphQLType = getGraphQLTypeForFragmentDefinition(fragment); - collectFields(fieldMap, fragment.getSelectionSet(), graphQLType, visitedFragmentSpreads); - } - - private GraphQLType getGraphQLTypeForFragmentDefinition(FragmentDefinition fragment) { - return TypeFromAST.getTypeFromAST(getValidationContext().getSchema(), - fragment.getTypeCondition()); - } - - private void collectFieldsForInlineFragment(Map> fieldMap, Set visitedFragmentSpreads, GraphQLType parentType, InlineFragment inlineFragment) { - GraphQLType graphQLType = getGraphQLTypeForInlineFragment(parentType, inlineFragment); - collectFields(fieldMap, inlineFragment.getSelectionSet(), graphQLType, visitedFragmentSpreads); - } - - private GraphQLType getGraphQLTypeForInlineFragment(GraphQLType parentType, InlineFragment inlineFragment) { - if (inlineFragment.getTypeCondition() == null) { - return parentType; - } - return TypeFromAST.getTypeFromAST(getValidationContext().getSchema(), inlineFragment.getTypeCondition()); - } - - private void collectFieldsForField(Map> fieldMap, GraphQLType parentType, Field field) { - String responseName = field.getResultKey(); - if (!fieldMap.containsKey(responseName)) { - fieldMap.put(responseName, new LinkedHashSet<>()); - } - GraphQLOutputType fieldType = null; - GraphQLUnmodifiedType unwrappedParent = unwrapAll(parentType); - if (unwrappedParent instanceof GraphQLFieldsContainer) { - GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) unwrappedParent; - GraphQLFieldDefinition fieldDefinition = getVisibleFieldDefinition(fieldsContainer, field); - fieldType = fieldDefinition != null ? fieldDefinition.getType() : null; - } - fieldMap.get(responseName).add(new FieldAndType(field, fieldType, unwrappedParent)); - } - - private GraphQLFieldDefinition getVisibleFieldDefinition(GraphQLFieldsContainer fieldsContainer, Field field) { - return getValidationContext().getSchema().getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, field.getName()); - } - - - private List findConflicts(Map> fieldMap) { - /* - * The algorithm implemented here is not the one from the Spec, but is based on - * https://tech.xing.com/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01 - * . It is not the final version (Listing 11), but Listing 10 adopted to this code base. - */ - List result = new ArrayList<>(); - sameResponseShapeByName(fieldMap, emptyList(), result); - sameForCommonParentsByName(fieldMap, emptyList(), result); - return result; - } - - private void sameResponseShapeByName(Map> fieldMap, ImmutableList currentPath, List conflictsResult) { - for (Map.Entry> entry : fieldMap.entrySet()) { - if (sameResponseShapeChecked.contains(entry.getValue())) { - continue; - } - ImmutableList newPath = addToList(currentPath, entry.getKey()); - sameResponseShapeChecked.add(entry.getValue()); - Conflict conflict = requireSameOutputTypeShape(newPath, entry.getValue()); - if (conflict != null) { - conflictsResult.add(conflict); - continue; - } - Map> subSelections = mergeSubSelections(entry.getValue()); - sameResponseShapeByName(subSelections, newPath, conflictsResult); - } - } - - private Map> mergeSubSelections(Set sameNameFields) { - Map> fieldMap = new LinkedHashMap<>(); - for (FieldAndType fieldAndType : sameNameFields) { - if (fieldAndType.field.getSelectionSet() != null) { - Set visitedFragmentSpreads = new LinkedHashSet<>(); - collectFields(fieldMap, fieldAndType.field.getSelectionSet(), fieldAndType.graphQLType, visitedFragmentSpreads); - } - } - return fieldMap; - } - - private void sameForCommonParentsByName(Map> fieldMap, ImmutableList currentPath, List conflictsResult) { - for (Map.Entry> entry : fieldMap.entrySet()) { - List> groups = groupByCommonParents(entry.getValue()); - ImmutableList newPath = addToList(currentPath, entry.getKey()); - for (Set group : groups) { - if (sameForCommonParentsChecked.contains(group)) { - continue; - } - sameForCommonParentsChecked.add(group); - Conflict conflict = requireSameNameAndArguments(newPath, group); - if (conflict != null) { - conflictsResult.add(conflict); - continue; - } - Map> subSelections = mergeSubSelections(group); - sameForCommonParentsByName(subSelections, newPath, conflictsResult); - } - } - } - - private List> groupByCommonParents(Set fields) { - Set abstractTypes = filterSet(fields, fieldAndType -> isInterfaceOrUnion(fieldAndType.parentType)); - Set concreteTypes = filterSet(fields, fieldAndType -> fieldAndType.parentType instanceof GraphQLObjectType); - if (concreteTypes.isEmpty()) { - return Collections.singletonList(abstractTypes); - } - Map> groupsByConcreteParent = groupingBy(concreteTypes, fieldAndType -> fieldAndType.parentType); - List> result = new ArrayList<>(); - for (ImmutableList concreteGroup : groupsByConcreteParent.values()) { - Set oneResultGroup = new LinkedHashSet<>(concreteGroup); - oneResultGroup.addAll(abstractTypes); - result.add(oneResultGroup); - } - return result; - } - - private boolean isInterfaceOrUnion(GraphQLType type) { - return type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType; - } - - private Conflict requireSameNameAndArguments(ImmutableList path, Set fieldAndTypes) { - if (fieldAndTypes.size() <= 1) { - return null; - } - String name = null; - List arguments = null; - List fields = new ArrayList<>(); - for (FieldAndType fieldAndType : fieldAndTypes) { - Field field = fieldAndType.field; - fields.add(field); - if (name == null) { - name = field.getName(); - arguments = field.getArguments(); - continue; - } - if (!field.getName().equals(name)) { - String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentFields", pathToString(path), name, field.getName()); - return new Conflict(reason, fields); - } - if (!sameArguments(field.getArguments(), arguments)) { - String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentArgs", pathToString(path)); - return new Conflict(reason, fields); - } - - } - return null; - } - - private String pathToString(ImmutableList path) { - return String.join("/", path); - } - - private boolean sameArguments(List arguments1, List arguments2) { - if (arguments1.size() != arguments2.size()) { - return false; - } - for (Argument argument : arguments1) { - Argument matchedArgument = findArgumentByName(argument.getName(), arguments2); - if (matchedArgument == null) { - return false; - } - if (!AstComparator.sameValue(argument.getValue(), matchedArgument.getValue())) { - return false; - } - } - return true; - } - - private Argument findArgumentByName(String name, List arguments) { - for (Argument argument : arguments) { - if (argument.getName().equals(name)) { - return argument; - } - } - return null; - } - - - private Conflict requireSameOutputTypeShape(ImmutableList path, Set fieldAndTypes) { - if (fieldAndTypes.size() <= 1) { - return null; - } - List fields = new ArrayList<>(); - GraphQLType typeAOriginal = null; - for (FieldAndType fieldAndType : fieldAndTypes) { - fields.add(fieldAndType.field); - if (typeAOriginal == null) { - typeAOriginal = fieldAndType.graphQLType; - continue; - } - GraphQLType typeA = typeAOriginal; - GraphQLType typeB = fieldAndType.graphQLType; - while (true) { - if (isNonNull(typeA) || isNonNull(typeB)) { - if (isNullable(typeA) || isNullable(typeB)) { - String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentNullability", pathToString(path)); - return new Conflict(reason, fields); - } - } - if (isList(typeA) || isList(typeB)) { - if (!isList(typeA) || !isList(typeB)) { - String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentLists", pathToString(path)); - return new Conflict(reason, fields); - } - } - if (isNotWrapped(typeA) && isNotWrapped(typeB)) { - break; - } - typeA = unwrapOne(typeA); - typeB = unwrapOne(typeB); - } - if (isScalar(typeA) || isScalar(typeB)) { - if (!sameType(typeA, typeB)) { - return mkNotSameTypeError(path, fields, typeA, typeB); - } - } - if (isEnum(typeA) || isEnum(typeB)) { - if (!sameType(typeA, typeB)) { - return mkNotSameTypeError(path, fields, typeA, typeB); - } - } - } - return null; - } - - private Conflict mkNotSameTypeError(ImmutableList path, List fields, GraphQLType typeA, GraphQLType typeB) { - String name1 = typeA != null ? simplePrint(typeA) : "null"; - String name2 = typeB != null ? simplePrint(typeB) : "null"; - String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentReturnTypes", pathToString(path), name1, name2); - return new Conflict(reason, fields); - } - - - private boolean sameType(GraphQLType type1, GraphQLType type2) { - if (type1 == null || type2 == null) { - return true; - } - return type1.equals(type2); - } - - - private static class FieldAndType { - final Field field; - final GraphQLType graphQLType; - final GraphQLType parentType; - - public FieldAndType(Field field, GraphQLType graphQLType, GraphQLType parentType) { - this.field = field; - this.graphQLType = graphQLType; - this.parentType = parentType; - } - - @Override - public String toString() { - return "FieldAndType{" + - "field=" + field + - ", graphQLType=" + graphQLType + - ", parentType=" + parentType + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - FieldAndType that = (FieldAndType) o; - - return Objects.equals(field, that.field); - } - - @Override - public int hashCode() { - return Objects.hashCode(field); - } - } - - private static class Conflict { - final String reason; - final Set fields = new LinkedHashSet<>(); - - - public Conflict(String reason, List fields) { - this.reason = reason; - this.fields.addAll(fields); - } - } - - -} diff --git a/src/main/java/graphql/validation/rules/PossibleFragmentSpreads.java b/src/main/java/graphql/validation/rules/PossibleFragmentSpreads.java deleted file mode 100644 index a14740e113..0000000000 --- a/src/main/java/graphql/validation/rules/PossibleFragmentSpreads.java +++ /dev/null @@ -1,94 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Assert; -import graphql.Internal; -import graphql.execution.TypeFromAST; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLUnionType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.Collections; -import java.util.List; - -import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.validation.ValidationErrorType.InvalidFragmentType; - -@Internal -public class PossibleFragmentSpreads extends AbstractRule { - - public PossibleFragmentSpreads(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkInlineFragment(InlineFragment inlineFragment) { - GraphQLOutputType fragType = getValidationContext().getOutputType(); - GraphQLCompositeType parentType = getValidationContext().getParentType(); - if (fragType == null || parentType == null) return; - - if (isValidTargetCompositeType(fragType) && isValidTargetCompositeType(parentType) && !doTypesOverlap(fragType, parentType)) { - String message = i18n(InvalidFragmentType, "PossibleFragmentSpreads.inlineIncompatibleTypes", parentType.getName(), simplePrint(fragType)); - addError(InvalidFragmentType, inlineFragment.getSourceLocation(), message); - } - } - - @Override - public void checkFragmentSpread(FragmentSpread fragmentSpread) { - FragmentDefinition fragment = getValidationContext().getFragment(fragmentSpread.getName()); - if (fragment == null) return; - GraphQLType typeCondition = TypeFromAST.getTypeFromAST(getValidationContext().getSchema(), fragment.getTypeCondition()); - GraphQLCompositeType parentType = getValidationContext().getParentType(); - if (typeCondition == null || parentType == null) return; - - if (isValidTargetCompositeType(typeCondition) && isValidTargetCompositeType(parentType) && !doTypesOverlap(typeCondition, parentType)) { - String message = i18n(InvalidFragmentType, "PossibleFragmentSpreads.fragmentIncompatibleTypes", fragmentSpread.getName(), parentType.getName(), simplePrint(typeCondition)); - addError(InvalidFragmentType, fragmentSpread.getSourceLocation(), message); - } - } - - private boolean doTypesOverlap(GraphQLType type, GraphQLCompositeType parent) { - if (type == parent) { - return true; - } - - List possibleParentTypes = getPossibleType(parent); - List possibleConditionTypes = getPossibleType(type); - - return !Collections.disjoint(possibleParentTypes, possibleConditionTypes); - - } - - private List getPossibleType(GraphQLType type) { - List possibleConditionTypes = null; - if (type instanceof GraphQLObjectType) { - possibleConditionTypes = Collections.singletonList(type); - } else if (type instanceof GraphQLInterfaceType) { - possibleConditionTypes = getValidationContext().getSchema().getImplementations((GraphQLInterfaceType) type); - } else if (type instanceof GraphQLUnionType) { - possibleConditionTypes = ((GraphQLUnionType) type).getTypes(); - } else { - Assert.assertShouldNeverHappen(); - } - return possibleConditionTypes; - } - - /** - * Per spec: The target type of fragment (type condition) - * must have kind UNION, INTERFACE, or OBJECT. - * @param type GraphQLType - * @return true if it is a union, interface, or object. - */ - private boolean isValidTargetCompositeType(GraphQLType type) { - return type instanceof GraphQLCompositeType; - } -} diff --git a/src/main/java/graphql/validation/rules/ProvidedNonNullArguments.java b/src/main/java/graphql/validation/rules/ProvidedNonNullArguments.java deleted file mode 100644 index 765e4ac276..0000000000 --- a/src/main/java/graphql/validation/rules/ProvidedNonNullArguments.java +++ /dev/null @@ -1,88 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.Argument; -import graphql.language.Directive; -import graphql.language.Field; -import graphql.language.Node; -import graphql.language.NullValue; -import graphql.language.Value; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLFieldDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.schema.GraphQLTypeUtil.isNonNull; -import static graphql.validation.ValidationErrorType.MissingDirectiveArgument; -import static graphql.validation.ValidationErrorType.MissingFieldArgument; -import static graphql.validation.ValidationErrorType.NullValueForNonNullArgument; - -@Internal -public class ProvidedNonNullArguments extends AbstractRule { - - public ProvidedNonNullArguments(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkField(Field field) { - GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); - if (fieldDef == null) { - return; - } - Map argumentMap = argumentMap(field.getArguments()); - - for (GraphQLArgument graphQLArgument : fieldDef.getArguments()) { - Argument argument = argumentMap.get(graphQLArgument.getName()); - boolean nonNullType = isNonNull(graphQLArgument.getType()); - boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); - if (argument == null && nonNullType && noDefaultValue) { - String message = i18n(MissingFieldArgument, "ProvidedNonNullArguments.missingFieldArg", graphQLArgument.getName()); - addError(MissingFieldArgument, field.getSourceLocation(), message); - } - - if (argument != null) { - Value value = argument.getValue(); - if ((value == null || value instanceof NullValue) && nonNullType && noDefaultValue) { - String message = i18n(NullValueForNonNullArgument, "ProvidedNonNullArguments.nullValue", graphQLArgument.getName()); - addError(NullValueForNonNullArgument, field.getSourceLocation(), message); - } - } - } - } - - - @Override - public void checkDirective(Directive directive, List ancestors) { - GraphQLDirective graphQLDirective = getValidationContext().getDirective(); - if (graphQLDirective == null) { - return; - } - Map argumentMap = argumentMap(directive.getArguments()); - - for (GraphQLArgument graphQLArgument : graphQLDirective.getArguments()) { - Argument argument = argumentMap.get(graphQLArgument.getName()); - boolean nonNullType = isNonNull(graphQLArgument.getType()); - boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); - if (argument == null && nonNullType && noDefaultValue) { - String message = i18n(MissingDirectiveArgument, "ProvidedNonNullArguments.missingDirectiveArg", graphQLArgument.getName()); - addError(MissingDirectiveArgument, directive.getSourceLocation(), message); - } - } - } - - private Map argumentMap(List arguments) { - Map result = new LinkedHashMap<>(); - for (Argument argument : arguments) { - result.put(argument.getName(), argument); - } - return result; - } -} diff --git a/src/main/java/graphql/validation/rules/ScalarLeaves.java b/src/main/java/graphql/validation/rules/ScalarLeaves.java deleted file mode 100644 index 072ba15080..0000000000 --- a/src/main/java/graphql/validation/rules/ScalarLeaves.java +++ /dev/null @@ -1,39 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.Field; -import graphql.schema.GraphQLOutputType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.schema.GraphQLTypeUtil.isLeaf; -import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.validation.ValidationErrorType.SubselectionNotAllowed; -import static graphql.validation.ValidationErrorType.SubselectionRequired; - -@Internal -public class ScalarLeaves extends AbstractRule { - - public ScalarLeaves(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkField(Field field) { - GraphQLOutputType type = getValidationContext().getOutputType(); - if (type == null) return; - if (isLeaf(type)) { - if (field.getSelectionSet() != null) { - String message = i18n(SubselectionNotAllowed, "ScalarLeaves.subselectionOnLeaf", simplePrint(type), field.getName()); - addError(SubselectionNotAllowed, field.getSourceLocation(), message); - } - } else { - if (field.getSelectionSet() == null) { - String message = i18n(SubselectionRequired, "ScalarLeaves.subselectionRequired", simplePrint(type), field.getName()); - addError(SubselectionRequired, field.getSourceLocation(), message); - } - } - } -} diff --git a/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java b/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java deleted file mode 100644 index 0ded9ca632..0000000000 --- a/src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java +++ /dev/null @@ -1,72 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.execution.CoercedVariables; -import graphql.execution.FieldCollector; -import graphql.execution.FieldCollectorParameters; -import graphql.execution.MergedField; -import graphql.execution.MergedSelectionSet; -import graphql.language.NodeUtil; -import graphql.language.OperationDefinition; -import graphql.language.Selection; -import graphql.schema.GraphQLObjectType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.List; - -import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; -import static graphql.validation.ValidationErrorType.SubscriptionIntrospectionRootField; -import static graphql.validation.ValidationErrorType.SubscriptionMultipleRootFields; - - -/** - * A subscription operation must only have one root field - * A subscription operation's single root field must not be an introspection field - * https://spec.graphql.org/draft/#sec-Single-root-field - */ -@Internal -public class SubscriptionUniqueRootField extends AbstractRule { - private final FieldCollector fieldCollector = new FieldCollector(); - public SubscriptionUniqueRootField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDef) { - if (operationDef.getOperation() == SUBSCRIPTION) { - - GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); - - FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() - .schema(getValidationContext().getSchema()) - .fragments(NodeUtil.getFragmentsByName(getValidationContext().getDocument())) - .variables(CoercedVariables.emptyVariables().toMap()) - .objectType(subscriptionType) - .graphQLContext(getValidationContext().getGraphQLContext()) - .build(); - - MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); - List subscriptionSelections = operationDef.getSelectionSet().getSelections(); - - if (fields.size() > 1) { - String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); - addError(SubscriptionMultipleRootFields, operationDef.getSourceLocation(), message); - } else { // Only one item in selection set, size == 1 - - MergedField mergedField = fields.getSubFieldsList().get(0); - - - if (isIntrospectionField(mergedField)) { - String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), mergedField.getName()); - addError(SubscriptionIntrospectionRootField, mergedField.getSingleField().getSourceLocation(), message); - } - } - } - } - - private boolean isIntrospectionField(MergedField field) { - return field.getName().startsWith("__"); - } -} diff --git a/src/main/java/graphql/validation/rules/UniqueArgumentNames.java b/src/main/java/graphql/validation/rules/UniqueArgumentNames.java deleted file mode 100644 index 8122446a13..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueArgumentNames.java +++ /dev/null @@ -1,66 +0,0 @@ -package graphql.validation.rules; - -import com.google.common.collect.Sets; -import graphql.Internal; -import graphql.language.Argument; -import graphql.language.Directive; -import graphql.language.Field; -import graphql.language.Node; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; - - -/** - * Unique argument names - * - * A GraphQL field or directive is only valid if all supplied arguments are uniquely named. - */ -@Internal -public class UniqueArgumentNames extends AbstractRule { - public UniqueArgumentNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkField(Field field) { - if (field.getArguments() == null || field.getArguments().size() <= 1) { - return; - } - - Set arguments = Sets.newHashSetWithExpectedSize(field.getArguments().size()); - - for (Argument argument : field.getArguments()) { - if (arguments.contains(argument.getName())) { - String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.uniqueArgument", argument.getName()); - addError(DuplicateArgumentNames, field.getSourceLocation(), message); - } else { - arguments.add(argument.getName()); - } - } - } - - @Override - public void checkDirective(Directive directive, List ancestors) { - if (directive.getArguments() == null || directive.getArguments().size() <= 1) { - return; - } - - Set arguments = Sets.newHashSetWithExpectedSize(directive.getArguments().size()); - - for (Argument argument : directive.getArguments()) { - if (arguments.contains(argument.getName())) { - String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.uniqueArgument", argument.getName()); - addError(DuplicateArgumentNames, directive.getSourceLocation(), message); - } else { - arguments.add(argument.getName()); - } - } - - } -} diff --git a/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java b/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java deleted file mode 100644 index 0aa5eb68ac..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java +++ /dev/null @@ -1,79 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.Directive; -import graphql.language.Document; -import graphql.language.Field; -import graphql.language.FragmentDefinition; -import graphql.language.FragmentSpread; -import graphql.language.InlineFragment; -import graphql.language.Node; -import graphql.language.OperationDefinition; -import graphql.schema.GraphQLDirective; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateDirectiveName; - - -/** - * https://facebook.github.io/graphql/June2018/#sec-Directives-Are-Unique-Per-Location - */ -@Internal -public class UniqueDirectiveNamesPerLocation extends AbstractRule { - - public UniqueDirectiveNamesPerLocation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkDocument(Document document) { - super.checkDocument(document); - } - - @Override - public void checkInlineFragment(InlineFragment inlineFragment) { - checkDirectivesUniqueness(inlineFragment, inlineFragment.getDirectives()); - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - checkDirectivesUniqueness(fragmentDefinition, fragmentDefinition.getDirectives()); - } - - @Override - public void checkFragmentSpread(FragmentSpread fragmentSpread) { - checkDirectivesUniqueness(fragmentSpread, fragmentSpread.getDirectives()); - } - - @Override - public void checkField(Field field) { - checkDirectivesUniqueness(field, field.getDirectives()); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - checkDirectivesUniqueness(operationDefinition, operationDefinition.getDirectives()); - } - - private void checkDirectivesUniqueness(Node directivesContainer, List directives) { - Set directiveNames = new LinkedHashSet<>(); - for (Directive directive : directives) { - String name = directive.getName(); - GraphQLDirective graphQLDirective = getValidationContext().getSchema().getDirective(name); - boolean nonRepeatable = graphQLDirective != null && graphQLDirective.isNonRepeatable(); - if (directiveNames.contains(name) && nonRepeatable) { - String message = i18n(DuplicateDirectiveName, "UniqueDirectiveNamesPerLocation.uniqueDirectives", name, directivesContainer.getClass().getSimpleName()); - addError(DuplicateDirectiveName, directive.getSourceLocation(), message); - } else { - directiveNames.add(name); - } - } - } - -} diff --git a/src/main/java/graphql/validation/rules/UniqueFragmentNames.java b/src/main/java/graphql/validation/rules/UniqueFragmentNames.java deleted file mode 100644 index 34adb39854..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueFragmentNames.java +++ /dev/null @@ -1,40 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.FragmentDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateFragmentName; - - -@Internal -public class UniqueFragmentNames extends AbstractRule { - - - private Set fragmentNames = new LinkedHashSet<>(); - - - public UniqueFragmentNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - String name = fragmentDefinition.getName(); - if (name == null) { - return; - } - - if (fragmentNames.contains(name)) { - String message = i18n(DuplicateFragmentName, "UniqueFragmentNames.oneFragment", name); - addError(DuplicateFragmentName, fragmentDefinition.getSourceLocation(), message); - } else { - fragmentNames.add(name); - } - } -} diff --git a/src/main/java/graphql/validation/rules/UniqueObjectFieldName.java b/src/main/java/graphql/validation/rules/UniqueObjectFieldName.java deleted file mode 100644 index 25c7c2410d..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueObjectFieldName.java +++ /dev/null @@ -1,34 +0,0 @@ -package graphql.validation.rules; - -import static graphql.validation.ValidationErrorType.UniqueObjectFieldName; - -import com.google.common.collect.Sets; -import graphql.language.ObjectField; -import graphql.language.ObjectValue; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.Set; - -public class UniqueObjectFieldName extends AbstractRule { - public UniqueObjectFieldName(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkObjectValue(ObjectValue objectValue) { - Set fieldNames = Sets.newHashSetWithExpectedSize(objectValue.getObjectFields().size()); - - for (ObjectField field : objectValue.getObjectFields()) { - String fieldName = field.getName(); - - if (fieldNames.contains(fieldName)) { - String message = i18n(UniqueObjectFieldName, "UniqueObjectFieldName.duplicateFieldName", fieldName); - addError(UniqueObjectFieldName, objectValue.getSourceLocation(), message); - } else { - fieldNames.add(fieldName); - } - } - } -} diff --git a/src/main/java/graphql/validation/rules/UniqueOperationNames.java b/src/main/java/graphql/validation/rules/UniqueOperationNames.java deleted file mode 100644 index f3eecf00d8..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueOperationNames.java +++ /dev/null @@ -1,44 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.OperationDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateOperationName; - -/** - * A GraphQL document is only valid if all defined operations have unique names. - * https://spec.graphql.org/October2021/#sec-Operation-Name-Uniqueness - */ -@Internal -public class UniqueOperationNames extends AbstractRule { - - private Set operationNames = new LinkedHashSet<>(); - - public UniqueOperationNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - super.checkOperationDefinition(operationDefinition); - String name = operationDefinition.getName(); - - // skip validation for anonymous operations - if (name == null) { - return; - } - - if (operationNames.contains(name)) { - String message = i18n(DuplicateOperationName, "UniqueOperationNames.oneOperation", operationDefinition.getName()); - addError(DuplicateOperationName, operationDefinition.getSourceLocation(), message); - } else { - operationNames.add(name); - } - } -} diff --git a/src/main/java/graphql/validation/rules/UniqueVariableNames.java b/src/main/java/graphql/validation/rules/UniqueVariableNames.java deleted file mode 100644 index e8706dc1e2..0000000000 --- a/src/main/java/graphql/validation/rules/UniqueVariableNames.java +++ /dev/null @@ -1,48 +0,0 @@ -package graphql.validation.rules; - -import com.google.common.collect.Sets; -import graphql.Internal; -import graphql.language.OperationDefinition; -import graphql.language.VariableDefinition; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.validation.ValidationErrorType.DuplicateVariableName; - -/** - * Unique variable names - *

- * A GraphQL operation is only valid if all its variables are uniquely named. - */ -@Internal -public class UniqueVariableNames extends AbstractRule { - - public UniqueVariableNames(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - List variableDefinitions = operationDefinition.getVariableDefinitions(); - if (variableDefinitions == null || variableDefinitions.size() <= 1) { - return; - } - - Set variableNameList = Sets.newLinkedHashSetWithExpectedSize(variableDefinitions.size()); - - - for (VariableDefinition variableDefinition : variableDefinitions) { - if (variableNameList.contains(variableDefinition.getName())) { - String message = i18n(DuplicateVariableName, "UniqueVariableNames.oneVariable", variableDefinition.getName()); - addError(DuplicateVariableName, variableDefinition.getSourceLocation(), message); - } else { - variableNameList.add(variableDefinition.getName()); - } - } - } -} diff --git a/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java b/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java deleted file mode 100644 index 115b70e895..0000000000 --- a/src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java +++ /dev/null @@ -1,33 +0,0 @@ -package graphql.validation.rules; - -import graphql.Internal; -import graphql.language.VariableDefinition; -import graphql.schema.GraphQLInputType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.validation.ValidationErrorType.BadValueForDefaultArg; - -@Internal -public class VariableDefaultValuesOfCorrectType extends AbstractRule { - - - public VariableDefaultValuesOfCorrectType(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkVariableDefinition(VariableDefinition variableDefinition) { - GraphQLInputType inputType = getValidationContext().getInputType(); - if (inputType == null) { - return; - } - if (variableDefinition.getDefaultValue() != null - && !getValidationUtil().isValidLiteralValue(variableDefinition.getDefaultValue(), inputType, getValidationContext().getSchema(), getValidationContext().getGraphQLContext(), getValidationContext().getI18n().getLocale())) { - String message = i18n(BadValueForDefaultArg, "VariableDefaultValuesOfCorrectType.badDefault", variableDefinition.getDefaultValue(), simplePrint(inputType)); - addError(BadValueForDefaultArg, variableDefinition.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/VariableTypesMatch.java b/src/main/java/graphql/validation/rules/VariableTypesMatch.java deleted file mode 100644 index ef7c4069f4..0000000000 --- a/src/main/java/graphql/validation/rules/VariableTypesMatch.java +++ /dev/null @@ -1,84 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.execution.TypeFromAST; -import graphql.execution.ValuesResolver; -import graphql.language.OperationDefinition; -import graphql.language.Value; -import graphql.language.VariableDefinition; -import graphql.language.VariableReference; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; -import graphql.schema.InputValueWithState; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - -import static graphql.validation.ValidationErrorType.VariableTypeMismatch; - -@Internal -public class VariableTypesMatch extends AbstractRule { - - final VariablesTypesMatcher variablesTypesMatcher; - - private Map variableDefinitionMap; - - public VariableTypesMatch(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - this(validationContext, validationErrorCollector, new VariablesTypesMatcher()); - } - - VariableTypesMatch(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector, VariablesTypesMatcher variablesTypesMatcher) { - super(validationContext, validationErrorCollector); - setVisitFragmentSpreads(true); - this.variablesTypesMatcher = variablesTypesMatcher; - } - - @Override - public void checkOperationDefinition(OperationDefinition operationDefinition) { - variableDefinitionMap = new LinkedHashMap<>(); - } - - @Override - public void checkVariableDefinition(VariableDefinition variableDefinition) { - variableDefinitionMap.put(variableDefinition.getName(), variableDefinition); - } - - @Override - public void checkVariable(VariableReference variableReference) { - VariableDefinition variableDefinition = variableDefinitionMap.get(variableReference.getName()); - if (variableDefinition == null) { - return; - } - GraphQLType variableType = TypeFromAST.getTypeFromAST(getValidationContext().getSchema(), variableDefinition.getType()); - if (variableType == null) { - return; - } - GraphQLInputType locationType = getValidationContext().getInputType(); - Optional locationDefault = Optional.ofNullable(getValidationContext().getDefaultValue()); - if (locationType == null) { - // we must have an unknown variable say to not have a known type - return; - } - Value locationDefaultValue = null; - if (locationDefault.isPresent() && locationDefault.get().isLiteral()) { - locationDefaultValue = (Value) locationDefault.get().getValue(); - } else if (locationDefault.isPresent() && locationDefault.get().isSet()) { - locationDefaultValue = ValuesResolver.valueToLiteral(locationDefault.get(), locationType, getValidationContext().getGraphQLContext(), getValidationContext().getI18n().getLocale()); - } - boolean variableDefMatches = variablesTypesMatcher.doesVariableTypesMatch(variableType, variableDefinition.getDefaultValue(), locationType, locationDefaultValue); - if (!variableDefMatches) { - GraphQLType effectiveType = variablesTypesMatcher.effectiveType(variableType, variableDefinition.getDefaultValue()); - String message = i18n(VariableTypeMismatch, "VariableTypesMatchRule.unexpectedType", - variableDefinition.getName(), - GraphQLTypeUtil.simplePrint(effectiveType), - GraphQLTypeUtil.simplePrint(locationType)); - addError(VariableTypeMismatch, variableReference.getSourceLocation(), message); - } - } -} diff --git a/src/main/java/graphql/validation/rules/VariablesAreInputTypes.java b/src/main/java/graphql/validation/rules/VariablesAreInputTypes.java deleted file mode 100644 index 3962ffeef4..0000000000 --- a/src/main/java/graphql/validation/rules/VariablesAreInputTypes.java +++ /dev/null @@ -1,35 +0,0 @@ -package graphql.validation.rules; - - -import graphql.Internal; -import graphql.language.TypeName; -import graphql.language.VariableDefinition; -import graphql.schema.GraphQLType; -import graphql.validation.AbstractRule; -import graphql.validation.ValidationContext; -import graphql.validation.ValidationErrorCollector; - -import static graphql.schema.GraphQLTypeUtil.isInput; -import static graphql.validation.ValidationErrorType.NonInputTypeOnVariable; - -@Internal -public class VariablesAreInputTypes extends AbstractRule { - - public VariablesAreInputTypes(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { - super(validationContext, validationErrorCollector); - } - - @Override - public void checkVariableDefinition(VariableDefinition variableDefinition) { - TypeName unmodifiedAstType = getValidationUtil().getUnmodifiedType(variableDefinition.getType()); - - GraphQLType type = getValidationContext().getSchema().getType(unmodifiedAstType.getName()); - if (type == null) { - return; - } - if (!isInput(type)) { - String message = i18n(NonInputTypeOnVariable, "VariablesAreInputTypes.wrongType", variableDefinition.getName(), unmodifiedAstType.getName()); - addError(NonInputTypeOnVariable, variableDefinition.getSourceLocation(), message); - } - } -} diff --git a/src/test/groovy/graphql/ParseAndValidateTest.groovy b/src/test/groovy/graphql/ParseAndValidateTest.groovy index fa66c3cbed..5b9c409559 100644 --- a/src/test/groovy/graphql/ParseAndValidateTest.groovy +++ b/src/test/groovy/graphql/ParseAndValidateTest.groovy @@ -6,9 +6,9 @@ import graphql.parser.InvalidSyntaxException import graphql.parser.Parser import graphql.schema.idl.SchemaParser import graphql.schema.idl.UnExecutableSchemaGenerator +import graphql.validation.OperationValidationRule import graphql.validation.ValidationError import graphql.validation.ValidationErrorType -import graphql.validation.rules.NoUnusedFragments import spock.lang.Specification import java.util.function.Predicate @@ -123,10 +123,10 @@ class ParseAndValidateTest extends Specification { def sdl = '''type Query { foo : ID } ''' def graphQL = TestUtil.graphQL(sdl).build() - Predicate> predicate = new Predicate>() { + Predicate predicate = new Predicate() { @Override - boolean test(Class aClass) { - if (aClass == NoUnusedFragments.class) { + boolean test(OperationValidationRule rule) { + if (rule == OperationValidationRule.NO_UNUSED_FRAGMENTS) { return false } return true @@ -135,7 +135,7 @@ class ParseAndValidateTest extends Specification { def query = ''' query { foo } - + fragment UnusedFrag on Query { foo } diff --git a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index d70f003b27..1b6cdafa61 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -274,11 +274,7 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.util.TraversalControl", "graphql.util.TraverserContext", "graphql.util.TreeTransformer", - "graphql.util.TreeTransformerUtil", - // These classes will not be public API later, exempt here while marked as experimental - "graphql.validation.rules.DeferDirectiveLabel", - "graphql.validation.rules.DeferDirectiveOnRootLevel", - "graphql.validation.rules.DeferDirectiveOnValidOperation" + "graphql.util.TreeTransformerUtil" ] as Set def "ensure all public API and experimental API classes have @NullMarked annotation"() { @@ -328,4 +324,4 @@ ${annotatedButExempted.sort().join("\n")} Please remove them from the exemption list in ${JSpecifyAnnotationsCheck.class.simpleName}.groovy.""") } } -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.groovy new file mode 100644 index 0000000000..a48fae4fe0 --- /dev/null +++ b/src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.groovy @@ -0,0 +1,267 @@ +package graphql.validation + +import graphql.TestUtil +import graphql.parser.Parser +import graphql.validation.SpecValidationSchema +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class ArgumentsOfCorrectTypeTest extends Specification { + + def "error message uses locale of client (German), not server (English)"() { + def query = """ + query getDog { + dog @objectArgumentDirective(myObject: { id: "1" }) { + name + } + } + """ + def document = new Parser().parseDocument(query) + + when: + def validationErrors = new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.GERMAN) + + then: + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validierungsfehler (WrongType@[dog]) : Argument 'myObject' mit Wert 'ObjectValue{objectFields=[ObjectField{name='id', value=StringValue{value='1'}}]}' fehlen Pflichtfelder '[name]'" + } + + def "valid type results in no error"() { + def query = """ + query getDog(\$cmd: DogCommand!) { + dog { + doesKnowCommand(dogCommand: \$cmd) + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.isEmpty() + } + + def "invalid type results in error"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: "notAnEnum") + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.isEmpty() + validationErrors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + def "invalid type scalar results in error with message"() { + def query = """ + query getDog { + dog(arg1: 1) { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'arg1' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" + } + + def "type missing fields results in error with message"() { + def query = """ + query getDog { + dog @objectArgumentDirective(myObject: { id: "1" }) { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'ObjectValue{objectFields=[ObjectField{name='id', value=StringValue{value='1'}}]}' is missing required fields '[name]'" + } + + def "invalid not object type results in error with message"() { + def query = """ + query getDog { + dog @objectArgumentDirective(myObject: 1) { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'IntValue{value=1}' must be an object type" + } + + def "type null results in error with message"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: null) + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 2 // First error is NullValueForNonNullArgument + validationErrors.get(1).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(1).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'NullValue{}' must not be null" + } + + def "type with extra fields results in error with message"() { + def query = """ + query getDog { + dog @objectArgumentDirective(myObject: { name: "Gary", extraField: "ShouldNotBeHere" }) { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'ObjectValue{objectFields=[ObjectField{name='name', value=StringValue{value='Gary'}}, ObjectField{name='extraField', value=StringValue{value='ShouldNotBeHere'}}]}' contains a field not in 'Input': 'extraField'" + } + + def "invalid enum type results in error with message"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: PRETTY) + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'EnumValue{name='PRETTY'}' is not a valid 'DogCommand' - Literal value not in allowable values for enum 'DogCommand' - 'EnumValue{name='PRETTY'}'" + } + + def "invalid @oneOf argument - has more than 1 key - case #why"() { + when: + def validationErrors = validate(query) + + then: + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType + validationErrors.get(0).message == "Validation error (WrongType@[oneOfField]) : Exactly one key must be specified for OneOf type 'oneOfInputType'." + + where: + why | query | _ + 'some variables' | + ''' + query q($v1 : String) { + oneOfField(oneOfArg : { a : $v1, b : "y" }) + } + ''' | _ + 'all variables' | + ''' + query q($v1 : String, $v2 : String) { + oneOfField(oneOfArg : { a : $v1, b : $v2 }) + } + ''' | _ + 'all literals' | + ''' + query q { + oneOfField(oneOfArg : { a : "x", b : "y" }) + } + ''' | _ + } + + def "invalid input object field type results in error"() { + def schema = TestUtil.schema(""" + type Query { field(arg: TestInput): String } + input TestInput { flag: Boolean } + """) + def document = new Parser().parseDocument('{ field(arg: { flag: "notABoolean" }) }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + errors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + def "invalid list of input objects results in error"() { + def schema = TestUtil.schema(""" + type Query { field(arg: [TestInput]): String } + input TestInput { flag: Boolean } + """) + def document = new Parser().parseDocument('{ field(arg: [{ flag: true }, { flag: "wrong" }]) }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + errors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + def "invalid nested list inside input object results in error"() { + def schema = TestUtil.schema(""" + type Query { field(arg: [TestInput]): String } + input TestInput { flags: [Boolean] } + """) + def document = new Parser().parseDocument('{ field(arg: [{ flags: [true, "wrong"] }]) }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + errors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + def "invalid simple list type results in error"() { + def schema = TestUtil.schema(""" + type Query { field(arg: [Boolean]): String } + """) + def document = new Parser().parseDocument('{ field(arg: [true, "wrong"]) }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + errors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + def "null value for non-null field in input object results in error"() { + def query = """ + query getDog { + dog @objectArgumentDirective(myObject: { id: "1", name: null }) { + name + } + } + """ + when: + def validationErrors = validate(query) + then: + validationErrors.any { it.validationErrorType == ValidationErrorType.WrongType } + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy similarity index 61% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy index d70ef5f207..d1eac2e903 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy @@ -1,13 +1,13 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi -import graphql.GraphQLContext +import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.SpecValidationSchema -import graphql.validation.TraversalContext import graphql.validation.ValidationContext import graphql.validation.ValidationError import graphql.validation.ValidationErrorCollector @@ -17,20 +17,16 @@ import spock.lang.Specification class DeferDirectiveLabelTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, errorCollector) - - def setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getSchema() >> SpecValidationSchema.specValidationSchema - validationContext.getGraphQLContext() >> GraphQLContext.newContext().of( - ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true - ).build(); - validationContext.getTraversalContext() >> traversalContext - validationContext.i18n(_, _) >> "test error message" + def traverse(String query) { + Document document = new Parser().parseDocument(query) + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) + ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) + validationContext.getGraphQLContext().put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) + LanguageTraversal languageTraversal = new LanguageTraversal() + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, + { rule -> rule == OperationValidationRule.DEFER_DIRECTIVE_LABEL })) } def "Allow unique label directive"() { @@ -44,11 +40,9 @@ class DeferDirectiveLabelTest extends Specification { } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -61,23 +55,20 @@ class DeferDirectiveLabelTest extends Specification { query defer_query { dog { ... @defer(label: "name") { - name + name } } alien { ... @defer(label: "name") { - name + name } } - + } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: !errorCollector.errors.isEmpty() @@ -90,19 +81,17 @@ class DeferDirectiveLabelTest extends Specification { query defer_query { dog { ... @defer { - name + name } ... @defer { - name + name } } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -114,19 +103,17 @@ class DeferDirectiveLabelTest extends Specification { query defer_query { dog { ... @defer(label: "name") { - name + name } ... @defer(label: "nameAgain") { - name + name } } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -145,11 +132,8 @@ class DeferDirectiveLabelTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: !errorCollector.errors.isEmpty() @@ -163,16 +147,14 @@ class DeferDirectiveLabelTest extends Specification { query defer_query { dog { ... @defer(label: 1) { - name + name } } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: !errorCollector.errors.isEmpty() @@ -195,11 +177,9 @@ class DeferDirectiveLabelTest extends Specification { } } ''' - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [deferDirectiveLabel])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -211,4 +191,3 @@ class DeferDirectiveLabelTest extends Specification { return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) } } - diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy similarity index 85% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy index ec65ec1937..10fc35007c 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy @@ -1,11 +1,12 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.SpecValidationSchema import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector @@ -25,7 +26,8 @@ class DeferDirectiveOnRootLevelTest extends Specification { validationContext.getGraphQLContext().put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnRootLevel(validationContext, errorCollector)])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, + { rule -> rule == OperationValidationRule.DEFER_DIRECTIVE_ON_ROOT_LEVEL })) } @@ -35,9 +37,9 @@ class DeferDirectiveOnRootLevelTest extends Specification { subscription pets { ... @defer { dog { - name + name } - } + } } """ @@ -80,7 +82,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { query defer_query { ... @defer { dog { - name + name } } } @@ -102,8 +104,8 @@ class DeferDirectiveOnRootLevelTest extends Specification { name } } - - } + + } } """ when: @@ -120,15 +122,15 @@ class DeferDirectiveOnRootLevelTest extends Specification { given: def query = """ subscription pets { - ...{ + ...{ ...{ ... @defer { dog { - name + name } } } - } + } } """ when: @@ -149,20 +151,20 @@ class DeferDirectiveOnRootLevelTest extends Specification { fragment doggo on PetMutationType { ... { ... @defer { - createDog(id: "1") { + createDog(id: "1") { id } } - } + } } - + mutation doggoMutation { ...{ ...doggo } } - + """ when: traverse(query) @@ -183,11 +185,9 @@ class DeferDirectiveOnRootLevelTest extends Specification { ... @defer { name } - } + } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: traverse(query) @@ -212,7 +212,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { id } } - + """ when: traverse(query) @@ -226,7 +226,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { given: def query = """ mutation doggo { - ...doggoCreate + ...doggoCreate } fragment doggoCreate on PetMutationType { @@ -240,7 +240,7 @@ class DeferDirectiveOnRootLevelTest extends Specification { id } } - + """ when: traverse(query) @@ -255,26 +255,24 @@ class DeferDirectiveOnRootLevelTest extends Specification { given: def query = """ subscription pets { - dog { - name - } + dog { + name + } } subscription dog { ... @defer { dog { - name + name } } } - + subscription morePets { - cat { - name - } + cat { + name + } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() when: traverse(query) @@ -294,11 +292,11 @@ class DeferDirectiveOnRootLevelTest extends Specification { ...createDoggo } } - + mutation createDoggoRootOp { ...createDoggoRoot } - + fragment createDoggo on PetMutationType { ... { ... @defer { @@ -306,9 +304,9 @@ class DeferDirectiveOnRootLevelTest extends Specification { name } } - } + } } - + """ when: @@ -331,13 +329,13 @@ class DeferDirectiveOnRootLevelTest extends Specification { ... { ...createDoggoLevel2 } - } - } - + } + } + fragment createDoggoLevel2 on PetMutationType { ...createDoggo - } - + } + fragment createDoggo on PetMutationType { ... { ... @defer { @@ -345,21 +343,21 @@ class DeferDirectiveOnRootLevelTest extends Specification { name } } - } - } - + } + } + query pets1 { ... @defer { dog { - name - } - } - } + name + } + } + } mutation createDoggo { ...createDoggoLevel1 } - + """ when: @@ -380,15 +378,13 @@ class DeferDirectiveOnRootLevelTest extends Specification { subscription pets{ ... @defer(if:false) { dog { - - name + + name } nickname - } + } } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal()\ when: traverse(query) @@ -407,11 +403,11 @@ class DeferDirectiveOnRootLevelTest extends Specification { subscription pets{ ... @defer(if:true) { dog { - + name } nickname - } + } } """ @@ -430,11 +426,11 @@ class DeferDirectiveOnRootLevelTest extends Specification { def query = """ mutation pets(\$ifVar:Boolean){ ... @defer(if:\$ifVar) { - createDog(input: {id: "1"}) { + createDog(input: {id: "1"}) { name } - } - + } + } """ @@ -452,10 +448,10 @@ class DeferDirectiveOnRootLevelTest extends Specification { def query = """ mutation pets{ ... @defer(if:true) { - createDog(input: {id: "1"}) { + createDog(input: {id: "1"}) { name } - } + } } """ @@ -470,4 +466,3 @@ class DeferDirectiveOnRootLevelTest extends Specification { } } - diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy similarity index 81% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy index 1430b7743b..1e160ddc16 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy @@ -1,11 +1,12 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.SpecValidationSchema import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector @@ -21,7 +22,8 @@ class DeferDirectiveOnValidOperationTest extends Specification { ValidationContext validationContext = new ValidationContext(SpecValidationSchema.specValidationSchema, document, i18n) validationContext.getGraphQLContext().put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, true) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [new DeferDirectiveOnValidOperation(validationContext, errorCollector)])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, + { rule -> rule == OperationValidationRule.DEFER_DIRECTIVE_ON_VALID_OPERATION })) } def "Allow simple defer on query with fragment definition"() { @@ -31,7 +33,7 @@ class DeferDirectiveOnValidOperationTest extends Specification { ... DogFields @defer } } - + fragment DogFields on Dog { name } @@ -51,7 +53,7 @@ class DeferDirectiveOnValidOperationTest extends Specification { ... DogFields @defer } } - + fragment DogFields on Dog { name } @@ -69,11 +71,11 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ subscription pets { dog { - ... @defer { - name + ... @defer { + name } nickname - } + } } """ @@ -93,11 +95,11 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ subscription pets { dog { - ... @defer(if:false) { - name + ... @defer(if:false) { + name } nickname - } + } } """ @@ -117,7 +119,7 @@ class DeferDirectiveOnValidOperationTest extends Specification { ... DogFields @defer } } - + fragment DogFields on Dog { name } @@ -136,23 +138,23 @@ class DeferDirectiveOnValidOperationTest extends Specification { given: def query = """ fragment doggo on PetMutationType { - ... { - dog { + ... { + dog { ... @defer { id } nickname } - - } + + } } - + subscription doggoMutation { ...{ ...doggo } } - + """ when: @@ -168,23 +170,23 @@ class DeferDirectiveOnValidOperationTest extends Specification { given: def query = """ fragment doggo on PetMutationType { - ... { - dog { + ... { + dog { ... @defer(if:false) { id } nickname } - - } + + } } - + subscription doggoMutation { ...{ ...doggo } } - + """ when: @@ -200,34 +202,34 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ fragment doggoSubscription on SubscriptionRoot { - ... { + ... { dog { ...doggo } - } + } } - + query pets { ... @defer { dog { - name - } - } + name + } + } } - + subscription pets2 { - ...doggoSubscription - } - + ...doggoSubscription + } + query pets3 { dog { - name - } - } - - fragment doggo on Dog{ + name + } + } + + fragment doggo on Dog{ ... @defer { - name + name } } """ @@ -250,20 +252,20 @@ class DeferDirectiveOnValidOperationTest extends Specification { query pets { ... @defer { dog { - name - } - } + name + } + } } - + subscription pets2 { dog { ... @defer { - name + name } - } - } - - + } + } + + """ when: @@ -283,9 +285,9 @@ class DeferDirectiveOnValidOperationTest extends Specification { mutation pets { dog { ... @defer { - name + name } - } + } } """ when: @@ -301,11 +303,11 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ subscription pets{ dog { - ... @defer(if:false) { - name + ... @defer(if:false) { + name } nickname - } + } } """ @@ -322,11 +324,11 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ subscription pets{ dog { - ... @defer(if:true) { + ... @defer(if:true) { name } nickname - } + } } """ @@ -346,11 +348,11 @@ class DeferDirectiveOnValidOperationTest extends Specification { def query = """ subscription pets(\$ifVar:Boolean){ dog { - ... @defer(if:\$ifVar) { - name + ... @defer(if:\$ifVar) { + name } nickname - } + } } """ @@ -363,4 +365,4 @@ class DeferDirectiveOnValidOperationTest extends Specification { -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/validation/rules/ExecutableDefinitionsTest.groovy b/src/test/groovy/graphql/validation/ExecutableDefinitionsTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/ExecutableDefinitionsTest.groovy rename to src/test/groovy/graphql/validation/ExecutableDefinitionsTest.groovy index 4002b14ab4..1ab1a576eb 100644 --- a/src/test/groovy/graphql/validation/rules/ExecutableDefinitionsTest.groovy +++ b/src/test/groovy/graphql/validation/ExecutableDefinitionsTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.language.SourceLocation import graphql.parser.Parser diff --git a/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/FieldsOnCorrectTypeTest.groovy similarity index 69% rename from src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy rename to src/test/groovy/graphql/validation/FieldsOnCorrectTypeTest.groovy index e5a28d69b5..1919679c9f 100644 --- a/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/FieldsOnCorrectTypeTest.groovy @@ -1,67 +1,35 @@ -package graphql.validation.rules +package graphql.validation -import graphql.language.Field import graphql.parser.Parser -import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLObjectType import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification class FieldsOnCorrectTypeTest extends Specification { - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - ValidationContext validationContext = Mock(ValidationContext) - FieldsOnCorrectType fieldsOnCorrectType = new FieldsOnCorrectType(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - - - def "should add error to collector when field definition is null"() { - given: - def parentType = GraphQLObjectType.newObject().name("parentType").build() - validationContext.getParentType() >> parentType - validationContext.getFieldDef() >> null - def field = new Field("name") - - when: - fieldsOnCorrectType.checkField(field) - - then: - errorCollector.containsValidationError(ValidationErrorType.FieldUndefined) - errorCollector.errors.size() == 1 - } - - def "should results in no error when field definition is filled"() { - given: - def parentType = GraphQLObjectType.newObject().name("parentType").build() - validationContext.getParentType() >> parentType - validationContext.getFieldDef() >> Mock(GraphQLFieldDefinition) - def field = new Field("name") - + def "should add error when field is undefined on type"() { + def query = """ + { dog { unknownField } } + """ when: - fieldsOnCorrectType.checkField(field) + def validationErrors = validate(query) then: - errorCollector.errors.isEmpty() + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.FieldUndefined } } - def "should results in no error when parent type is null"() { - given: - validationContext.getParentType() >> null - def field = new Field("name") - + def "should result in no error when field is defined"() { + def query = """ + { dog { name } } + """ when: - fieldsOnCorrectType.checkField(field) + def validationErrors = validate(query) then: - errorCollector.errors.isEmpty() + validationErrors.empty } def '5.2.1 Field Selections on ... fieldNotDefined'() { diff --git a/src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy b/src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy new file mode 100644 index 0000000000..d614c6d419 --- /dev/null +++ b/src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy @@ -0,0 +1,189 @@ +package graphql.validation + +import graphql.ExecutionInput +import graphql.GraphQL +import graphql.TestUtil +import graphql.parser.Parser +import graphql.validation.SpecValidationSchema +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class FragmentsOnCompositeTypeTest extends Specification { + + def "inline fragment type condition must refer to a composite type"() { + def query = """ + { + dog { + ... on String { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.any { it.validationErrorType == ValidationErrorType.InlineFragmentTypeConditionInvalid } + } + + def "should result in no error for inline fragment without type condition"() { + def query = """ + { + dog { + ... { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "should result in no error for inline fragment with composite type condition"() { + def query = """ + { + dog { + ... on Pet { + name + } + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "fragment type condition must refer to a composite type"() { + def query = """ + { + dog { + ...frag + } + } + fragment frag on String { + length + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.any { it.validationErrorType == ValidationErrorType.FragmentTypeConditionInvalid } + } + + def schema = TestUtil.schema(""" + type Query { + nothing: String + } + + type Mutation { + updateUDI(input: UDIInput!): UDIOutput + } + + type UDIOutput { + device: String + version: String + } + + input UDIInput { + device: String + version: String + } + """) + + def graphQL = GraphQL.newGraphQL(schema).build() + + + def "#1440 when fragment type condition is input type it should return validation error - not classCastException"() { + when: + def executionInput = ExecutionInput.newExecutionInput() + .query(''' + mutation UpdateUDI($input: UDIInput!) { + updateUDI(input: $input) { + ...fragOnInputType + __typename + } + } + + # fragment should only target composite types + fragment fragOnInputType on UDIInput { + device + version + __typename + } + + ''') + .variables([input: [device: 'device', version: 'version'] ]) + .build() + + def executionResult = graphQL.execute(executionInput) + + then: + + executionResult.data == null + executionResult.errors.size() == 1 + (executionResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.FragmentTypeConditionInvalid + (executionResult.errors[0] as ValidationError).message == "Validation error (FragmentTypeConditionInvalid@[fragOnInputType]) : Fragment type condition is invalid, must be on Object/Interface/Union" + } + + def "#1440 when inline fragment type condition is input type it should return validation error - not classCastException"() { + when: + def executionInput = ExecutionInput.newExecutionInput() + .query(''' + mutation UpdateUDI($input: UDIInput!) { + updateUDI(input: $input) { + # fragment should only target composite types + ... on UDIInput { + device + version + __typename + } + __typename + } + } + ''') + .variables([input: [device: 'device', version: 'version'] ]) + .build() + + def executionResult = graphQL.execute(executionInput) + + then: + + executionResult.data == null + executionResult.errors.size() == 1 + (executionResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.InlineFragmentTypeConditionInvalid + (executionResult.errors[0] as ValidationError).message == "Validation error (InlineFragmentTypeConditionInvalid@[updateUDI]) : Inline fragment type condition is invalid, must be on Object/Interface/Union" + } + + def "unknown type on inline fragment should not trigger composite type error"() { + def query = """ + { + dog { + ... on StrangeType { + __typename + } + } + } + """ + when: + def validationErrors = validate(query) + then: + // Should have KnownTypeNames error, but NOT InlineFragmentTypeConditionInvalid + !validationErrors.any { it.validationErrorType == ValidationErrorType.InlineFragmentTypeConditionInvalid } + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} diff --git a/src/test/groovy/graphql/validation/rules/Harness.java b/src/test/groovy/graphql/validation/Harness.java similarity index 99% rename from src/test/groovy/graphql/validation/rules/Harness.java rename to src/test/groovy/graphql/validation/Harness.java index 2b233b77d0..5f80af87fa 100644 --- a/src/test/groovy/graphql/validation/rules/Harness.java +++ b/src/test/groovy/graphql/validation/Harness.java @@ -1,4 +1,4 @@ -package graphql.validation.rules; +package graphql.validation; import graphql.schema.GraphQLEnumType; import graphql.schema.GraphQLInputObjectType; diff --git a/src/test/groovy/graphql/validation/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/KnownArgumentNamesTest.groovy new file mode 100644 index 0000000000..1fececc10a --- /dev/null +++ b/src/test/groovy/graphql/validation/KnownArgumentNamesTest.groovy @@ -0,0 +1,128 @@ +package graphql.validation + +import graphql.parser.Parser +import graphql.validation.SpecValidationSchema +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +class KnownArgumentNamesTest extends Specification { + + def "unknown field argument"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT, unknownArg: false) + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.UnknownArgument } + } + + def "known field argument"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "unknown directive argument"() { + def query = """ + query getDogName { + dog @dogDirective(unknownArg: "value") { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.UnknownDirective } + } + + def "known directive argument results in no error"() { + def query = """ + query getDogName { + dog { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "directive missing argument validation error with message"() { + def query = """ + query getDogName { + dog @dogDirective(notArgument: "value"){ + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.UnknownDirective + validationErrors.get(0).message == "Validation error (UnknownDirective@[dog]) : Unknown directive argument 'notArgument'" + } + + def "field missing argument validation error with message"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT, notArgument: false) + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors.get(0).getValidationErrorType() == ValidationErrorType.UnknownArgument + validationErrors.get(0).message == "Validation error (UnknownArgument@[dog/doesKnowCommand]) : Unknown field argument 'notArgument'" + } + + def "directive argument not validated against field arguments"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT) @dogDirective(dogCommand: SIT) + } + } + """ + when: + def validationErrors = validate(query) + then: + validationErrors.any { it.validationErrorType == ValidationErrorType.UnknownDirective } + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} diff --git a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy b/src/test/groovy/graphql/validation/KnownDirectivesTest.groovy similarity index 65% rename from src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy rename to src/test/groovy/graphql/validation/KnownDirectivesTest.groovy index c2eac19865..4c37fce407 100644 --- a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownDirectivesTest.groovy @@ -1,12 +1,13 @@ -package graphql.validation.rules +package graphql.validation import graphql.StarWarsSchema import graphql.TestUtil +import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor -import graphql.validation.TraversalContext +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType @@ -15,15 +16,16 @@ import spock.lang.Specification class KnownDirectivesTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) ValidationErrorCollector errorCollector = new ValidationErrorCollector() - KnownDirectives knownDirectives = new KnownDirectives(validationContext, errorCollector) - def setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - validationContext.getTraversalContext() >> traversalContext - validationContext.i18n(_, _) >> "test error message" + def traverse(String query) { + Document document = new Parser().parseDocument(query) + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) + ValidationContext validationContext = new ValidationContext(StarWarsSchema.starWarsSchema, document, i18n) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.KNOWN_DIRECTIVES }) + LanguageTraversal languageTraversal = new LanguageTraversal() + languageTraversal.traverse(document, operationValidator) } @@ -35,11 +37,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) @@ -54,11 +53,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -73,11 +69,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -93,11 +86,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) @@ -114,11 +104,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.UnknownDirective) @@ -130,18 +117,15 @@ class KnownDirectivesTest extends Specification { given: def query = """ fragment getName on Foo @include(if: true) { - name + name } query Foo { ...getName } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.MisplacedDirective) @@ -162,11 +146,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -183,11 +164,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -204,11 +182,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -226,11 +201,8 @@ class KnownDirectivesTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [knownDirectives])) + traverse(query) then: errorCollector.errors.isEmpty() @@ -238,19 +210,19 @@ class KnownDirectivesTest extends Specification { } def sdl = ''' - - directive @queryDirective on QUERY - + + directive @queryDirective on QUERY + directive @subDirective on SUBSCRIPTION - + type Query { field: String } - + type Subscription { field: String } - + ''' def schema = TestUtil.schema(sdl) @@ -258,7 +230,7 @@ class KnownDirectivesTest extends Specification { def "invalid directive on SUBSCRIPTION"() { def spec = ''' subscription sub @queryDirective{ - field + field } ''' @@ -276,7 +248,7 @@ class KnownDirectivesTest extends Specification { def "unknown directive on SUBSCRIPTION"() { def spec = ''' subscription sub @unknownDirective{ - field + field } ''' @@ -294,7 +266,7 @@ class KnownDirectivesTest extends Specification { def "valid directive on SUBSCRIPTION"() { def spec = ''' subscription sub @subDirective{ - field + field } ''' diff --git a/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy b/src/test/groovy/graphql/validation/KnownFragmentNamesTest.groovy similarity index 58% rename from src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy rename to src/test/groovy/graphql/validation/KnownFragmentNamesTest.groovy index 0e09a73aa9..026305afee 100644 --- a/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownFragmentNamesTest.groovy @@ -1,34 +1,28 @@ -package graphql.validation.rules +package graphql.validation -import graphql.language.FragmentSpread import graphql.parser.Parser import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification class KnownFragmentNamesTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - KnownFragmentNames knownFragmentNames = new KnownFragmentNames(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - def "unknown fragment reference in fragment spread"() { - given: - FragmentSpread fragmentSpread = FragmentSpread.newFragmentSpread("fragment").build() - knownFragmentNames.validationContext.getFragment("fragment") >> null + def query = """ + { + dog { + ...unknownFragment + } + } + """ when: - knownFragmentNames.checkFragmentSpread(fragmentSpread) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.UndefinedFragment) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.UndefinedFragment } } def '5.4.2.1 Fragment spread target defined '() { @@ -36,7 +30,7 @@ class KnownFragmentNamesTest extends Specification { query getDogName { dog { ... FragmentDoesNotExist - } + } } """ when: diff --git a/src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy b/src/test/groovy/graphql/validation/KnownTypeNamesTest.groovy similarity index 76% rename from src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy rename to src/test/groovy/graphql/validation/KnownTypeNamesTest.groovy index 8dc27e2a4c..92b40e2b95 100644 --- a/src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownTypeNamesTest.groovy @@ -1,35 +1,30 @@ -package graphql.validation.rules +package graphql.validation -import graphql.StarWarsSchema -import graphql.language.TypeName import graphql.parser.Parser import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification class KnownTypeNamesTest extends Specification { - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - ValidationContext validationContext = Mock(ValidationContext) - KnownTypeNames knownTypeNames = new KnownTypeNames(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - def "unknown types is an error"() { - given: - knownTypeNames.validationContext.getSchema() >> StarWarsSchema.starWarsSchema - + def query = """ + { + dog { + ... on Simpson { + name + } + } + } + """ when: - knownTypeNames.checkTypeName(TypeName.newTypeName("Simpson").build()) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.UnknownType) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.UnknownType } } def '5.7.3 Variables Are Input Types - unknown type'() { diff --git a/src/test/groovy/graphql/validation/rules/LoneAnonymousOperationTest.groovy b/src/test/groovy/graphql/validation/LoneAnonymousOperationTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/LoneAnonymousOperationTest.groovy rename to src/test/groovy/graphql/validation/LoneAnonymousOperationTest.groovy index f2543b7815..a52e94bcb9 100644 --- a/src/test/groovy/graphql/validation/rules/LoneAnonymousOperationTest.groovy +++ b/src/test/groovy/graphql/validation/LoneAnonymousOperationTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser import graphql.validation.SpecValidationSchema diff --git a/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy b/src/test/groovy/graphql/validation/NoFragmentCyclesTest.groovy similarity index 91% rename from src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy rename to src/test/groovy/graphql/validation/NoFragmentCyclesTest.groovy index ea3e1807f7..b54ad740bc 100644 --- a/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy +++ b/src/test/groovy/graphql/validation/NoFragmentCyclesTest.groovy @@ -1,15 +1,15 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType -import graphql.validation.Validator import spock.lang.Specification class NoFragmentCyclesTest extends Specification { @@ -20,9 +20,10 @@ class NoFragmentCyclesTest extends Specification { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) - NoFragmentCycles noFragmentCycles = new NoFragmentCycles(validationContext, errorCollector) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.NO_FRAGMENT_CYCLES }) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noFragmentCycles])) + languageTraversal.traverse(document, operationValidator) } def 'single reference is valid'() { @@ -227,10 +228,11 @@ class NoFragmentCyclesTest extends Specification { I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) def validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) - def rules = new Validator().createRules(validationContext, errorCollector) + def operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.NO_FRAGMENT_CYCLES }) when: LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, rules)) + languageTraversal.traverse(document, operationValidator) then: diff --git a/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy b/src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy similarity index 90% rename from src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy rename to src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy index 90f9976a4d..62ddeb7dd2 100644 --- a/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy @@ -1,11 +1,12 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType @@ -18,10 +19,11 @@ class NoUndefinedVariablesTest extends Specification { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) - NoUndefinedVariables noUndefinedVariables = new NoUndefinedVariables(validationContext, errorCollector) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.NO_UNDEFINED_VARIABLES }) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUndefinedVariables])) + languageTraversal.traverse(document, operationValidator) } def "undefined variable"() { @@ -131,8 +133,8 @@ class NoUndefinedVariablesTest extends Specification { def query = """ query Foo(\$a: String) { ...A } query Bar(\$a: String) { ...A } - - fragment A on Type { + + fragment A on Type { field(a: \$a) } """ @@ -149,8 +151,8 @@ class NoUndefinedVariablesTest extends Specification { def query = """ query Foo(\$a: String) { ...A } query Bar { ...A } - - fragment A on Type { + + fragment A on Type { field(a: \$a) } """ @@ -168,8 +170,8 @@ class NoUndefinedVariablesTest extends Specification { def query = """ query Foo { ...A } query Bar { ...A } - - fragment A on Type { + + fragment A on Type { field(a: \$a) } """ diff --git a/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy b/src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy similarity index 78% rename from src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy rename to src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy index 1bb7e5b635..157bd38c10 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy +++ b/src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy @@ -1,11 +1,13 @@ -package graphql.validation.rules +package graphql.validation +import graphql.TestUtil +import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.SpecValidationSchema -import graphql.validation.TraversalContext import graphql.validation.ValidationContext import graphql.validation.ValidationError import graphql.validation.ValidationErrorCollector @@ -15,14 +17,16 @@ import spock.lang.Specification class NoUnusedFragmentsTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) ValidationErrorCollector errorCollector = new ValidationErrorCollector() - NoUnusedFragments noUnusedFragments = new NoUnusedFragments(validationContext, errorCollector) - def setup() { - def traversalContext = Mock(TraversalContext) - validationContext.getTraversalContext() >> traversalContext - validationContext.i18n(_, _) >> "test error message" + def traverse(String query) { + Document document = new Parser().parseDocument(query) + I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) + ValidationContext validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.NO_UNUSED_FRAGMENTS }) + LanguageTraversal languageTraversal = new LanguageTraversal() + languageTraversal.traverse(document, operationValidator) } def "all fragment names are used"() { @@ -48,17 +52,15 @@ class NoUnusedFragmentsTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUnusedFragments])) + traverse(query) then: errorCollector.getErrors().isEmpty() } def "all fragment names are used by multiple operations"() { + given: def query = """ query Foo { human(id: 4) { @@ -82,11 +84,8 @@ class NoUnusedFragmentsTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUnusedFragments])) + traverse(query) then: errorCollector.getErrors().isEmpty() @@ -94,6 +93,7 @@ class NoUnusedFragmentsTest extends Specification { def "contains unknown fragments"() { + given: def query = """ query Foo { human(id: 4) { @@ -123,11 +123,8 @@ class NoUnusedFragmentsTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUnusedFragments])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.UnusedFragment) @@ -168,11 +165,8 @@ class NoUnusedFragmentsTest extends Specification { } """ - Document document = new Parser().parseDocument(query) - LanguageTraversal languageTraversal = new LanguageTraversal() - when: - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUnusedFragments])) + traverse(query) then: errorCollector.containsValidationError(ValidationErrorType.UnusedFragment) @@ -184,7 +178,7 @@ class NoUnusedFragmentsTest extends Specification { query getDogName { dog { name - } + } } fragment dogFragment on Dog { barkVolume } """.stripIndent() @@ -202,4 +196,4 @@ class NoUnusedFragmentsTest extends Specification { def document = new Parser().parseDocument(query) return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) } -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy b/src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy similarity index 90% rename from src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy rename to src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy index 9b7d0dc8de..4717f3810f 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy @@ -1,11 +1,12 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.SpecValidationSchema import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector @@ -20,10 +21,11 @@ class NoUnusedVariablesTest extends Specification { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) - NoUnusedVariables noUnusedVariables = new NoUnusedVariables(validationContext, errorCollector) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.NO_UNUSED_VARIABLES }) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [noUnusedVariables])) + languageTraversal.traverse(document, operationValidator) } def "uses all variables in fragments"() { @@ -124,7 +126,7 @@ class NoUnusedVariablesTest extends Specification { query getDogName($arg1: String, $unusedArg: Int) { dog(arg1: $arg1) { name - } + } } ''' when: diff --git a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy b/src/test/groovy/graphql/validation/OverlappingFieldsCanBeMergedTest.groovy similarity index 97% rename from src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy rename to src/test/groovy/graphql/validation/OverlappingFieldsCanBeMergedTest.groovy index d35ec38084..779ac756ea 100644 --- a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy +++ b/src/test/groovy/graphql/validation/OverlappingFieldsCanBeMergedTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.i18n.I18n import graphql.language.Document @@ -7,7 +7,8 @@ import graphql.parser.Parser import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLSchema import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import spock.lang.Specification @@ -38,10 +39,11 @@ class OverlappingFieldsCanBeMergedTest extends Specification { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(schema, document, i18n) - OverlappingFieldsCanBeMerged overlappingFieldsCanBeMerged = new OverlappingFieldsCanBeMerged(validationContext, errorCollector) + OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector, + { r -> r == OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED }) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [overlappingFieldsCanBeMerged])) + languageTraversal.traverse(document, operationValidator) } def "identical fields are ok"() { @@ -401,7 +403,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { } """ def schema = schema(''' - type Query { + type Query { cat: Cat! # non null parent type } type Cat { @@ -486,7 +488,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { c: String } schema { - query: Type + query: Type } ''') when: @@ -636,7 +638,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { d: String } type Field { - deepField: Type + deepField: Type } type Query { field: Field @@ -677,7 +679,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { d: String } type Field { - deepField: Type + deepField: Type } type Query { field: Field @@ -840,7 +842,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { } } ... on Pet { - friends { + friends { name } } @@ -894,9 +896,9 @@ class OverlappingFieldsCanBeMergedTest extends Specification { } } ... on Cat { - friends { + friends { ... on Cat { - conflict: height + conflict: height } } } @@ -950,7 +952,7 @@ class OverlappingFieldsCanBeMergedTest extends Specification { } } ... on Cat { - friends { + friends { breed } } diff --git a/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy b/src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy similarity index 96% rename from src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy rename to src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy index 87c465fd38..4571a14c4b 100644 --- a/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy +++ b/src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy @@ -1,10 +1,11 @@ -package graphql.validation.rules +package graphql.validation import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import spock.lang.Specification @@ -18,10 +19,10 @@ class PossibleFragmentSpreadsTest extends Specification { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) ValidationContext validationContext = new ValidationContext(Harness.Schema, document, i18n) - PossibleFragmentSpreads possibleFragmentSpreads = new PossibleFragmentSpreads(validationContext, errorCollector) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [possibleFragmentSpreads])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, + { rule -> rule == OperationValidationRule.POSSIBLE_FRAGMENT_SPREADS })) } def 'of the same object'() { @@ -307,7 +308,7 @@ class PossibleFragmentSpreadsTest extends Specification { ...LeashInputFragment } } - + fragment LeashInputFragment on LeashInput { id } @@ -325,10 +326,10 @@ class PossibleFragmentSpreadsTest extends Specification { query { dogWithInput { ...on LeashInput { - id + id } } - } + } """ when: traverse(query) diff --git a/src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy new file mode 100644 index 0000000000..2cb36463ad --- /dev/null +++ b/src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy @@ -0,0 +1,202 @@ +package graphql.validation + +import graphql.parser.Parser +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.validation.SpecValidationSchema +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import graphql.validation.Validator +import spock.lang.Specification + +import static graphql.Scalars.GraphQLString +import static graphql.introspection.Introspection.DirectiveLocation.FIELD +import static graphql.schema.GraphQLNonNull.nonNull + +class ProvidedNonNullArgumentsTest extends Specification { + + def "not provided non null field argument results in error"() { + def query = """ + query getDogName { + dog { + doesKnowCommand + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.MissingFieldArgument } + } + + def "not provided and not defaulted non null field argument with error message"() { + def query = """ + query getDogName { + dog { + doesKnowCommand + } + } + """.stripIndent() + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors[0].validationErrorType == ValidationErrorType.MissingFieldArgument + validationErrors[0].message == "Validation error (MissingFieldArgument@[dog/doesKnowCommand]) : Missing field argument 'dogCommand'" + } + + def "all field arguments are provided results in no error"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "not provided not defaulted directive argument results in error"() { + def query = """ + query getDogName { + dog @nonNullDirective { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.MissingDirectiveArgument } + } + + def "not provided and not defaulted non null directive argument with error message"() { + def query = """ + query getDogName { + dog @nonNullDirective { + name + } + } + """.stripIndent() + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 1 + validationErrors[0].validationErrorType == ValidationErrorType.MissingDirectiveArgument + validationErrors[0].message == "Validation error (MissingDirectiveArgument@[dog]) : Missing directive argument 'arg1'" + } + + def "all directive arguments are provided results in no error"() { + def query = """ + query getDogName { + dog @nonNullDirective(arg1: "value") { + name + } + } + """ + when: + def validationErrors = validate(query) + + then: + validationErrors.empty + } + + def "provide the explicit value null is not valid for non null argument"() { + def query = """ + query getDogName { + dog { + doesKnowCommand(dogCommand: null) + } + } + """ + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.NullValueForNonNullArgument } + } + + def "provide the explicit value null is not valid for non null argument with error message"() { + def query = """ + query getDogName { + dog { + doesKnowCommand(dogCommand: null) + } + } + """.stripIndent() + when: + def validationErrors = validate(query) + + then: + !validationErrors.empty + validationErrors.size() == 2 + validationErrors[0].validationErrorType == ValidationErrorType.NullValueForNonNullArgument + validationErrors[0].message == "Validation error (NullValueForNonNullArgument@[dog/doesKnowCommand]) : Null value for non-null field argument 'dogCommand'" + } + + def "not provided but defaulted non null field argument is not an error"() { + def schema = GraphQLSchema.newSchema() + .query(GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("field") + .type(GraphQLString) + .argument(GraphQLArgument.newArgument() + .name("arg") + .type(nonNull(GraphQLString)) + .defaultValueProgrammatic("defaultVal"))) + .build()) + .build() + def document = new Parser().parseDocument('{ field }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + !errors.any { it.validationErrorType == ValidationErrorType.MissingFieldArgument } + } + + def "not provided but defaulted directive argument is not an error"() { + def directive = GraphQLDirective.newDirective() + .name("myDirective") + .validLocation(FIELD) + .argument(GraphQLArgument.newArgument() + .name("arg") + .type(nonNull(GraphQLString)) + .defaultValueProgrammatic("defaultVal")) + .build() + def schema = GraphQLSchema.newSchema() + .query(GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("field") + .type(GraphQLString)) + .build()) + .additionalDirective(directive) + .build() + def document = new Parser().parseDocument('{ field @myDirective }') + when: + def errors = new Validator().validateDocument(schema, document, Locale.ENGLISH) + then: + !errors.any { it.validationErrorType == ValidationErrorType.MissingDirectiveArgument } + } + + static List validate(String query) { + def document = new Parser().parseDocument(query) + return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) + } +} diff --git a/src/test/groovy/graphql/validation/RulesVisitorTest.groovy b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy index 2b0de3a993..079a0caaf4 100644 --- a/src/test/groovy/graphql/validation/RulesVisitorTest.groovy +++ b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy @@ -4,22 +4,25 @@ import graphql.TestUtil import graphql.i18n.I18n import graphql.language.Document import graphql.parser.Parser +import graphql.schema.GraphQLSchema import spock.lang.Specification +import java.util.function.Predicate + class RulesVisitorTest extends Specification { - AbstractRule simpleRule = Mock() - AbstractRule visitsSpreadsRule = Mock() - def setup() { - visitsSpreadsRule.isVisitFragmentSpreads() >> true - } + ValidationErrorCollector errorCollector = new ValidationErrorCollector() def traverse(String query) { + traverse(query, TestUtil.dummySchema, { rule -> true }) + } + + def traverse(String query, GraphQLSchema schema, Predicate rulePredicate) { Document document = new Parser().parseDocument(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) - ValidationContext validationContext = new ValidationContext(TestUtil.dummySchema, document, i18n) + ValidationContext validationContext = new ValidationContext(schema, document, i18n) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [simpleRule, visitsSpreadsRule])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, rulePredicate)) } def "RulesVisitor does not repeatedly spread directly recursive fragments leading to a stackoverflow"() { @@ -74,24 +77,19 @@ class RulesVisitorTest extends Specification { notThrown(StackOverflowError) } - def "RulesVisitor visits fragment definition with isVisitFragmentSpread rules once per operation"() { + def "OperationValidator visits fragment definitions per-operation for fragment-spread rules"() { given: def query = """ - fragment A on A { __typename } - fragment B on B { ...A } - fragment C on C { ...A ...B } - - query Q1 { ...A ...B ...C } - query Q2 { ...A ...B ...C } - """ + fragment HumanFields on __Type { fields(includeDeprecated: \$inc) { name } } + query Q1(\$inc: Boolean!) { __schema { queryType { ...HumanFields } } } + query Q2 { __schema { queryType { ...HumanFields } } } + """ when: - traverse(query) - + traverse(query, TestUtil.dummySchema, { r -> r == OperationValidationRule.NO_UNDEFINED_VARIABLES }) then: - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "A"}) - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "B"}) - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "C"}) + // Q2 has undefined variable $inc -> exactly 1 error + errorCollector.errors.size() == 1 + errorCollector.errors[0].validationErrorType == ValidationErrorType.UndefinedVariable } } - diff --git a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy b/src/test/groovy/graphql/validation/ScalarLeavesTest.groovy similarity index 60% rename from src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy rename to src/test/groovy/graphql/validation/ScalarLeavesTest.groovy index 3f5d413b90..c544f81b50 100644 --- a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy +++ b/src/test/groovy/graphql/validation/ScalarLeavesTest.groovy @@ -1,39 +1,30 @@ -package graphql.validation.rules +package graphql.validation -import graphql.Scalars -import graphql.language.Field -import graphql.language.SelectionSet import graphql.parser.Parser -import graphql.schema.GraphQLObjectType import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification -import static graphql.language.Field.newField - class ScalarLeavesTest extends Specification { - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - ValidationContext validationContext = Mock(ValidationContext) - ScalarLeaves scalarLeaves = new ScalarLeaves(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - def "subselection not allowed"() { - given: - Field field = newField("hello", SelectionSet.newSelectionSet([newField("world").build()]).build()).build() - validationContext.getOutputType() >> Scalars.GraphQLString + def query = """ + { + dog { + name { + something + } + } + } + """ when: - scalarLeaves.checkField(field) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.SubselectionNotAllowed) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.SubselectionNotAllowed } } def "subselection not allowed with error message"() { @@ -57,14 +48,17 @@ class ScalarLeavesTest extends Specification { } def "subselection required"() { - given: - Field field = newField("hello").build() - validationContext.getOutputType() >> GraphQLObjectType.newObject().name("objectType").build() + def query = """ + { + dog + } + """ when: - scalarLeaves.checkField(field) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.SubselectionRequired) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.SubselectionRequired } } def "subselection required with error message"() { diff --git a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy b/src/test/groovy/graphql/validation/SubscriptionUniqueRootFieldTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy rename to src/test/groovy/graphql/validation/SubscriptionUniqueRootFieldTest.groovy index 9b171f2256..dbf88344cf 100644 --- a/src/test/groovy/graphql/validation/rules/SubscriptionUniqueRootFieldTest.groovy +++ b/src/test/groovy/graphql/validation/SubscriptionUniqueRootFieldTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser import graphql.validation.SpecValidationSchema diff --git a/src/test/groovy/graphql/validation/rules/UniqueArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/UniqueArgumentNamesTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/UniqueArgumentNamesTest.groovy rename to src/test/groovy/graphql/validation/UniqueArgumentNamesTest.groovy index 4e76fc69a4..ee57499c48 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueArgumentNamesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser import graphql.validation.SpecValidationSchema diff --git a/src/test/groovy/graphql/validation/rules/UniqueDirectiveNamesPerLocationTest.groovy b/src/test/groovy/graphql/validation/UniqueDirectiveNamesPerLocationTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/UniqueDirectiveNamesPerLocationTest.groovy rename to src/test/groovy/graphql/validation/UniqueDirectiveNamesPerLocationTest.groovy index 107a140295..2c7defdc91 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueDirectiveNamesPerLocationTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueDirectiveNamesPerLocationTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser diff --git a/src/test/groovy/graphql/validation/rules/UniqueFragmentNamesTest.groovy b/src/test/groovy/graphql/validation/UniqueFragmentNamesTest.groovy similarity index 97% rename from src/test/groovy/graphql/validation/rules/UniqueFragmentNamesTest.groovy rename to src/test/groovy/graphql/validation/UniqueFragmentNamesTest.groovy index f3d049aba1..9a69b1f27c 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueFragmentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueFragmentNamesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.language.SourceLocation import graphql.parser.Parser diff --git a/src/test/groovy/graphql/validation/rules/UniqueObjectFieldNameTest.groovy b/src/test/groovy/graphql/validation/UniqueObjectFieldNameTest.groovy similarity index 96% rename from src/test/groovy/graphql/validation/rules/UniqueObjectFieldNameTest.groovy rename to src/test/groovy/graphql/validation/UniqueObjectFieldNameTest.groovy index f59a4e7c07..72b0421cf9 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueObjectFieldNameTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueObjectFieldNameTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser import graphql.validation.Validator diff --git a/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy b/src/test/groovy/graphql/validation/UniqueOperationNamesTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy rename to src/test/groovy/graphql/validation/UniqueOperationNamesTest.groovy index c5fa1eb3d2..637e07fe0b 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueOperationNamesTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueOperationNamesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.language.SourceLocation import graphql.parser.Parser diff --git a/src/test/groovy/graphql/validation/rules/UniqueVariableNamesTest.groovy b/src/test/groovy/graphql/validation/UniqueVariableNamesTest.groovy similarity index 97% rename from src/test/groovy/graphql/validation/rules/UniqueVariableNamesTest.groovy rename to src/test/groovy/graphql/validation/UniqueVariableNamesTest.groovy index 79e80c05ba..fb93376b7a 100644 --- a/src/test/groovy/graphql/validation/rules/UniqueVariableNamesTest.groovy +++ b/src/test/groovy/graphql/validation/UniqueVariableNamesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.parser.Parser diff --git a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/VariableDefaultValuesOfCorrectTypeTest.groovy similarity index 61% rename from src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy rename to src/test/groovy/graphql/validation/VariableDefaultValuesOfCorrectTypeTest.groovy index ad52085efa..943e6e143e 100644 --- a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/VariableDefaultValuesOfCorrectTypeTest.groovy @@ -1,44 +1,36 @@ -package graphql.validation.rules +package graphql.validation -import graphql.GraphQLContext import graphql.TestUtil -import graphql.i18n.I18n -import graphql.language.BooleanValue -import graphql.language.TypeName -import graphql.language.VariableDefinition -import graphql.validation.ValidationContext -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification -import static graphql.Scalars.GraphQLString - class VariableDefaultValuesOfCorrectTypeTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - VariableDefaultValuesOfCorrectType defaultValuesOfCorrectType = new VariableDefaultValuesOfCorrectType(validationContext, errorCollector) - I18n i18n = Mock(I18n) - - void setup() { - def context = GraphQLContext.getDefault() - validationContext.getGraphQLContext() >> context - validationContext.getI18n() >> i18n - validationContext.i18n(_, _) >> "test error message" - i18n.getLocale() >> Locale.ENGLISH - } - def "default value has wrong type"() { - given: - BooleanValue defaultValue = BooleanValue.newBooleanValue(false).build() - VariableDefinition variableDefinition = VariableDefinition.newVariableDefinition("var", TypeName.newTypeName("String").build(), defaultValue).build() - validationContext.getInputType() >> GraphQLString + setup: + def schema = ''' + type Query { + field(arg: String) : String + } + ''' + + def query = ''' + query($arg: String = false) { + field(arg: $arg) + } + ''' + + def graphQlSchema = TestUtil.schema(schema) + def document = TestUtil.parseQuery(query) + def validator = new Validator() + when: - defaultValuesOfCorrectType.checkVariableDefinition(variableDefinition) + def validationErrors = validator.validateDocument(graphQlSchema, document, Locale.ENGLISH) then: - errorCollector.containsValidationError(ValidationErrorType.BadValueForDefaultArg) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.BadValueForDefaultArg } } def "default value has wrong type with error message"() { @@ -47,7 +39,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { type User { id: String } - + type Query { getUsers(howMany: Int) : [User] } @@ -57,7 +49,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { query($howMany: Int = "NotANumber") { getUsers(howMany: $howMany) { id - } + } } ''' @@ -81,7 +73,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { type User { id: String } - + type Query { getUsers(howMany: Int) : [User] } @@ -91,7 +83,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { query($howMany: Int = "NotANumber") { getUsers(howMany: $howMany) { id - } + } } ''' @@ -108,4 +100,4 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { validationErrors[0].getValidationErrorType() == ValidationErrorType.BadValueForDefaultArg validationErrors[0].message == "Validierungsfehler (BadValueForDefaultArg) : Ungültiger Standardwert 'StringValue{value='NotANumber'}' für Typ 'Int'" } -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy b/src/test/groovy/graphql/validation/VariableTypesMatchTest.groovy similarity index 92% rename from src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy rename to src/test/groovy/graphql/validation/VariableTypesMatchTest.groovy index 5ccaff13dd..51a6cfa549 100644 --- a/src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy +++ b/src/test/groovy/graphql/validation/VariableTypesMatchTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.StarWarsSchema @@ -7,7 +7,8 @@ import graphql.i18n.I18n import graphql.parser.Parser import graphql.schema.GraphQLSchema import graphql.validation.LanguageTraversal -import graphql.validation.RulesVisitor +import graphql.validation.OperationValidationRule +import graphql.validation.OperationValidator import graphql.validation.ValidationContext import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType @@ -20,17 +21,17 @@ class VariableTypesMatchTest extends Specification { def document = Parser.parse(query) I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH) def validationContext = new ValidationContext(schema, document, i18n) - def variableTypesMatchRule = new VariableTypesMatch(validationContext, errorCollector) def languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [variableTypesMatchRule])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, + { rule -> rule == OperationValidationRule.VARIABLE_TYPES_MATCH })) } def "valid variables"() { given: def query = """ query Q(\$id: String!) { - human(id: \$id) { - __typename + human(id: \$id) { + __typename } } """ @@ -46,8 +47,8 @@ class VariableTypesMatchTest extends Specification { given: def query = """ query Q(\$id: String) { - human(id: \$id) { - __typename + human(id: \$id) { + __typename } } """ @@ -69,7 +70,7 @@ class VariableTypesMatchTest extends Specification { __typename } } - + query Invalid(\$xid: String) { ...QueryType } @@ -91,11 +92,11 @@ class VariableTypesMatchTest extends Specification { __typename } } - + query Valid(\$id: String!) { ... QueryType } - + query Invalid(\$id: String) { ... QueryType } @@ -117,11 +118,11 @@ class VariableTypesMatchTest extends Specification { __typename } } - + query Invalid(\$id: String) { ... QueryType } - + query Valid(\$id: String!) { ... QueryType } @@ -143,11 +144,11 @@ class VariableTypesMatchTest extends Specification { __typename } } - + query Invalid1(\$id: String) { ... QueryType } - + query Invalid2(\$id: Boolean) { ... QueryType } @@ -185,7 +186,7 @@ class VariableTypesMatchTest extends Specification { def query = ''' query Items( $limit: Int, $offset: Int) { items( - pagination: {limit: $limit, offset: $offset} + pagination: {limit: $limit, offset: $offset} ) } ''' @@ -214,7 +215,7 @@ class VariableTypesMatchTest extends Specification { def query = ''' query Items( $var : Pagination) { items( - pagination: $var + pagination: $var ) } ''' @@ -242,7 +243,7 @@ class VariableTypesMatchTest extends Specification { def query = ''' query Items( $var : Pagination = {limit: 1, offset: 1}) { items( - pagination: $var + pagination: $var ) } ''' diff --git a/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy b/src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy similarity index 58% rename from src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy rename to src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy index f0287ab40b..8d77e3d58a 100644 --- a/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy +++ b/src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy @@ -1,38 +1,42 @@ -package graphql.validation.rules +package graphql.validation -import graphql.StarWarsSchema import graphql.TestUtil -import graphql.language.ListType -import graphql.language.NonNullType -import graphql.language.TypeName -import graphql.language.VariableDefinition -import graphql.validation.ValidationContext -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType import graphql.validation.Validator import spock.lang.Specification class VariablesAreInputTypesTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - VariablesAreInputTypes variablesAreInputTypes = new VariablesAreInputTypes(validationContext, errorCollector) + def "the unmodified ast type is not a schema input type"() { + setup: + def schema = ''' + type Droid { + name: String + } - def setup() { - validationContext.i18n(_, _) >> "test error message" - } + type Query { + droid(id: String): Droid + } + ''' - def "the unmodified ast type is not a schema input type"() { - given: - def astType = new NonNullType(new ListType(new TypeName(StarWarsSchema.droidType.getName()))) - VariableDefinition variableDefinition = new VariableDefinition("var", astType) - validationContext.getSchema() >> StarWarsSchema.starWarsSchema + def query = ''' + query(\$var: [Droid]!) { + droid(id: "1") { + name + } + } + ''' + + def graphQlSchema = TestUtil.schema(schema) + def document = TestUtil.parseQuery(query) + def validator = new Validator() when: - variablesAreInputTypes.checkVariableDefinition(variableDefinition) + def validationErrors = validator.validateDocument(graphQlSchema, document, Locale.ENGLISH) then: - errorCollector.containsValidationError(ValidationErrorType.NonInputTypeOnVariable) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.NonInputTypeOnVariable } } def "when a variable is of type GraphQLObjectType then it should not throw ClassCastException and validate with errors"() { @@ -41,15 +45,15 @@ class VariablesAreInputTypesTest extends Specification { type User { id: String } - + input UserInput { id: String } - + type Mutation { createUser(user: UserInput): User } - + type Query { getUser: User } @@ -59,7 +63,7 @@ class VariablesAreInputTypesTest extends Specification { mutation createUser($user: User){ createUser(user: $user) { id - } + } } ''' diff --git a/src/test/groovy/graphql/validation/rules/VariablesTypesMatcherTest.groovy b/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/VariablesTypesMatcherTest.groovy rename to src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy index b05e425090..97eebfd612 100644 --- a/src/test/groovy/graphql/validation/rules/VariablesTypesMatcherTest.groovy +++ b/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.language.BooleanValue import graphql.language.StringValue diff --git a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy deleted file mode 100644 index 762a5788cb..0000000000 --- a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy +++ /dev/null @@ -1,397 +0,0 @@ -package graphql.validation.rules - -import graphql.GraphQLContext -import graphql.i18n.I18n -import graphql.language.Argument -import graphql.language.ArrayValue -import graphql.language.BooleanValue -import graphql.language.NullValue -import graphql.language.ObjectField -import graphql.language.ObjectValue -import graphql.language.StringValue -import graphql.language.VariableReference -import graphql.parser.Parser -import graphql.schema.GraphQLArgument -import graphql.schema.GraphQLInputObjectField -import graphql.schema.GraphQLInputObjectType -import graphql.schema.GraphQLList -import graphql.schema.GraphQLNonNull -import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import graphql.validation.Validator -import spock.lang.Specification - -import static graphql.Scalars.GraphQLBoolean -import static graphql.Scalars.GraphQLInt -import static graphql.Scalars.GraphQLString -import static graphql.StarWarsSchema.starWarsSchema - -class ArgumentsOfCorrectTypeTest extends Specification { - - ArgumentsOfCorrectType argumentsOfCorrectType - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - I18n i18n = Mock(I18n) - - def setup() { - argumentsOfCorrectType = new ArgumentsOfCorrectType(validationContext, errorCollector) - def context = GraphQLContext.getDefault() - validationContext.getGraphQLContext() >> context - validationContext.getI18n() >> i18n - validationContext.i18n(_, _) >> "test error message" - i18n.getLocale() >> Locale.ENGLISH - } - - def "error message uses locale of client (German), not server (English)"() { - def query = """ - query getDog { - dog @objectArgumentDirective(myObject: { id: "1" }) { - name - } - } - """ - def document = new Parser().parseDocument(query) - - when: - def validationErrors = new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.GERMAN) - - then: - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validierungsfehler (WrongType@[dog]) : Argument 'myObject' mit Wert 'ObjectValue{objectFields=[ObjectField{name='id', value=StringValue{value='1'}}]}' fehlen Pflichtfelder '[name]'" - } - - def "valid type results in no error"() { - given: - def variableReference = new VariableReference("ref") - def argumentLiteral = new Argument("arg", variableReference) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInt).build() - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.errors.isEmpty() - } - - def "invalid type results in error"() { - given: - def stringValue = new StringValue("string") - def argumentLiteral = new Argument("arg", stringValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLBoolean).build() - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "invalid type scalar results in error with message"() { - def query = """ - query getDog { - dog(arg1: 1) { - name - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'arg1' with value 'IntValue{value=1}' is not a valid 'String' - Expected an AST type of 'StringValue' but it was a 'IntValue'" - } - - def "invalid input object type results in error"() { - given: - def objectValue = new ObjectValue([new ObjectField("foo", new StringValue("string"))]) - def argumentLiteral = new Argument("arg", objectValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType").field(GraphQLInputObjectField.newInputObjectField().name("foo").type(GraphQLBoolean)).build()).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "invalid list object type results in error"() { - given: - - def validValue = new ObjectValue([new ObjectField("foo", new BooleanValue(true))]) - def invalidValue = new ObjectValue([new ObjectField("foo", new StringValue("string"))]) - def arrayValue = new ArrayValue([validValue, invalidValue]) - def argumentLiteral = new Argument("arg", arrayValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLList.list(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType").field(GraphQLInputObjectField.newInputObjectField().name("foo").type(GraphQLBoolean)).build())).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "invalid list inside object type results in error"() { - given: - - def validValue = new ObjectValue([new ObjectField("foo", new ArrayValue([new BooleanValue(true), new BooleanValue(false)]))]) - def invalidValue = new ObjectValue([new ObjectField("foo", new ArrayValue([new BooleanValue(true), new StringValue('string')]))]) - def arrayValue = new ArrayValue([invalidValue, validValue]) - def argumentLiteral = new Argument("arg", arrayValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLList.list(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType").field(GraphQLInputObjectField.newInputObjectField().name("foo").type(GraphQLList.list(GraphQLBoolean))).build())).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "invalid list simple type results in error"() { - given: - - def validValue = new BooleanValue(true) - def invalidValue = new StringValue("string") - def arrayValue = new ArrayValue([validValue, invalidValue]) - def argumentLiteral = new Argument("arg", arrayValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLList.list(GraphQLBoolean)).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "type missing fields results in error"() { - given: - def objectValue = new ObjectValue([new ObjectField("foo", new StringValue("string"))]) - def argumentLiteral = new Argument("arg", objectValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType") - .field(GraphQLInputObjectField.newInputObjectField() - .name("foo").type(GraphQLNonNull.nonNull(GraphQLString))) - .field(GraphQLInputObjectField.newInputObjectField() - .name("bar").type(GraphQLNonNull.nonNull(GraphQLString))) - .build()).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "type missing fields results in error with message"() { - def query = """ - query getDog { - dog @objectArgumentDirective(myObject: { id: "1" }) { - name - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'ObjectValue{objectFields=[ObjectField{name='id', value=StringValue{value='1'}}]}' is missing required fields '[name]'" - } - - def "type not object results in error"() { - given: - def objectValue = new StringValue("string") - def argumentLiteral = new Argument("arg", objectValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType") - .field(GraphQLInputObjectField.newInputObjectField() - .name("foo").type(GraphQLNonNull.nonNull(GraphQLString))) - .field(GraphQLInputObjectField.newInputObjectField() - .name("bar").type(GraphQLNonNull.nonNull(GraphQLString))) - .build()).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "invalid not object type results in error with message"() { - def query = """ - query getDog { - dog @objectArgumentDirective(myObject: 1) { - name - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'IntValue{value=1}' must be an object type" - } - - def "type null fields results in error"() { - given: - def objectValue = new ObjectValue([new ObjectField("foo", new StringValue("string")), new ObjectField("bar", NullValue.newNullValue().build())]) - def argumentLiteral = new Argument("arg", objectValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType") - .field(GraphQLInputObjectField.newInputObjectField() - .name("foo").type(GraphQLNonNull.nonNull(GraphQLString))) - .field(GraphQLInputObjectField.newInputObjectField() - .name("bar").type(GraphQLNonNull.nonNull(GraphQLString))) - .build()).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "type null results in error with message"() { - def query = """ - query getDog { - dog { - doesKnowCommand(dogCommand: null) - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 2 // First error is NullValueForNonNullArgument - validationErrors.get(1).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(1).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'NullValue{}' must not be null" - } - - def "type with extra fields results in error"() { - given: - def objectValue = new ObjectValue([new ObjectField("foo", new StringValue("string")), new ObjectField("bar", new StringValue("string")), new ObjectField("fooBar", new BooleanValue(true))]) - def argumentLiteral = new Argument("arg", objectValue) - def graphQLArgument = GraphQLArgument.newArgument().name("arg").type(GraphQLInputObjectType.newInputObject().name("ArgumentObjectType") - .field(GraphQLInputObjectField.newInputObjectField() - .name("foo").type(GraphQLNonNull.nonNull(GraphQLString))) - .field(GraphQLInputObjectField.newInputObjectField() - .name("bar").type(GraphQLNonNull.nonNull(GraphQLString))) - .build()).build() - - argumentsOfCorrectType.validationContext.getArgument() >> graphQLArgument - argumentsOfCorrectType.validationContext.getSchema() >> starWarsSchema - - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 - } - - def "type with extra fields results in error with message"() { - def query = """ - query getDog { - dog @objectArgumentDirective(myObject: { name: "Gary", extraField: "ShouldNotBeHere" }) { - name - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog]) : argument 'myObject' with value 'ObjectValue{objectFields=[ObjectField{name='name', value=StringValue{value='Gary'}}, ObjectField{name='extraField', value=StringValue{value='ShouldNotBeHere'}}]}' contains a field not in 'Input': 'extraField'" - } - - def "current null argument from context is no error"() { - given: - def stringValue = new StringValue("string") - def argumentLiteral = new Argument("arg", stringValue) - when: - argumentsOfCorrectType.checkArgument(argumentLiteral) - then: - argumentsOfCorrectType.getErrors().isEmpty() - } - - def "invalid enum type results in error with message"() { - def query = """ - query getDog { - dog { - doesKnowCommand(dogCommand: PRETTY) - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[dog/doesKnowCommand]) : argument 'dogCommand' with value 'EnumValue{name='PRETTY'}' is not a valid 'DogCommand' - Literal value not in allowable values for enum 'DogCommand' - 'EnumValue{name='PRETTY'}'" - } - - def "invalid @oneOf argument - has more than 1 key - case #why"() { - when: - def validationErrors = validate(query) - - then: - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.WrongType - validationErrors.get(0).message == "Validation error (WrongType@[oneOfField]) : Exactly one key must be specified for OneOf type 'oneOfInputType'." - - where: - why | query | _ - 'some variables' | - ''' - query q($v1 : String) { - oneOfField(oneOfArg : { a : $v1, b : "y" }) - } - ''' | _ - 'all variables' | - ''' - query q($v1 : String, $v2 : String) { - oneOfField(oneOfArg : { a : $v1, b : $v2 }) - } - ''' | _ - 'all literals' | - ''' - query q { - oneOfField(oneOfArg : { a : "x", b : "y" }) - } - ''' | _ - } - - static List validate(String query) { - def document = new Parser().parseDocument(query) - return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) - } -} diff --git a/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy b/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy deleted file mode 100644 index d648973849..0000000000 --- a/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy +++ /dev/null @@ -1,165 +0,0 @@ -package graphql.validation.rules - -import graphql.ExecutionInput -import graphql.GraphQL -import graphql.StarWarsSchema -import graphql.TestUtil -import graphql.language.FragmentDefinition -import graphql.language.InlineFragment -import graphql.language.TypeName -import graphql.validation.ValidationContext -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import spock.lang.Specification - -class FragmentsOnCompositeTypeTest extends Specification { - - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - FragmentsOnCompositeType fragmentsOnCompositeType = new FragmentsOnCompositeType(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - - def "inline fragment type condition must refer to a composite type"() { - given: - InlineFragment inlineFragment = InlineFragment.newInlineFragment().typeCondition(TypeName.newTypeName("String").build()).build() - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - - when: - fragmentsOnCompositeType.checkInlineFragment(inlineFragment) - - then: - errorCollector.containsValidationError(ValidationErrorType.InlineFragmentTypeConditionInvalid) - errorCollector.errors.size() == 1 - } - - def "should results in no error"(InlineFragment inlineFragment) { - given: - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - - when: - fragmentsOnCompositeType.checkInlineFragment(inlineFragment) - - then: - errorCollector.errors.isEmpty() - - where: - inlineFragment << [ - getInlineFragmentWithTypeConditionNull(), - getInlineFragmentWithConditionWithStrangeType(), - getInlineFragmentWithConditionWithRightType() - ] - } - - private InlineFragment getInlineFragmentWithTypeConditionNull() { - InlineFragment.newInlineFragment().build() - } - - private InlineFragment getInlineFragmentWithConditionWithStrangeType() { - InlineFragment.newInlineFragment().typeCondition(TypeName.newTypeName("StrangeType").build()).build() - } - - private InlineFragment getInlineFragmentWithConditionWithRightType() { - InlineFragment.newInlineFragment().typeCondition(TypeName.newTypeName("Character").build()).build() - } - - def "fragment type condition must refer to a composite type"() { - given: - FragmentDefinition fragmentDefinition = FragmentDefinition.newFragmentDefinition().name("fragment").typeCondition(TypeName.newTypeName("String").build()).build() - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - - when: - fragmentsOnCompositeType.checkFragmentDefinition(fragmentDefinition) - - then: - errorCollector.containsValidationError(ValidationErrorType.FragmentTypeConditionInvalid) - } - - def schema = TestUtil.schema(""" - type Query { - nothing: String - } - - type Mutation { - updateUDI(input: UDIInput!): UDIOutput - } - - type UDIOutput { - device: String - version: String - } - - input UDIInput { - device: String - version: String - } - """) - - def graphQL = GraphQL.newGraphQL(schema).build() - - - def "#1440 when fragment type condition is input type it should return validation error - not classCastException"() { - when: - def executionInput = ExecutionInput.newExecutionInput() - .query(''' - mutation UpdateUDI($input: UDIInput!) { - updateUDI(input: $input) { - ...fragOnInputType - __typename - } - } - - # fragment should only target composite types - fragment fragOnInputType on UDIInput { - device - version - __typename - } - - ''') - .variables([input: [device: 'device', version: 'version'] ]) - .build() - - def executionResult = graphQL.execute(executionInput) - - then: - - executionResult.data == null - executionResult.errors.size() == 1 - (executionResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.FragmentTypeConditionInvalid - (executionResult.errors[0] as ValidationError).message == "Validation error (FragmentTypeConditionInvalid@[fragOnInputType]) : Fragment type condition is invalid, must be on Object/Interface/Union" - } - - def "#1440 when inline fragment type condition is input type it should return validation error - not classCastException"() { - when: - def executionInput = ExecutionInput.newExecutionInput() - .query(''' - mutation UpdateUDI($input: UDIInput!) { - updateUDI(input: $input) { - # fragment should only target composite types - ... on UDIInput { - device - version - __typename - } - __typename - } - } - ''') - .variables([input: [device: 'device', version: 'version'] ]) - .build() - - def executionResult = graphQL.execute(executionInput) - - then: - - executionResult.data == null - executionResult.errors.size() == 1 - (executionResult.errors[0] as ValidationError).validationErrorType == ValidationErrorType.InlineFragmentTypeConditionInvalid - (executionResult.errors[0] as ValidationError).message == "Validation error (InlineFragmentTypeConditionInvalid@[updateUDI]) : Inline fragment type condition is invalid, must be on Object/Interface/Union" - } - -} diff --git a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy deleted file mode 100644 index 6d91157e18..0000000000 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ /dev/null @@ -1,145 +0,0 @@ -package graphql.validation.rules - -import graphql.introspection.Introspection -import graphql.language.Argument -import graphql.language.BooleanValue -import graphql.language.StringValue -import graphql.parser.Parser -import graphql.schema.GraphQLArgument -import graphql.schema.GraphQLDirective -import graphql.schema.GraphQLFieldDefinition -import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import graphql.validation.Validator -import spock.lang.Specification - -import static graphql.Scalars.GraphQLBoolean -import static graphql.Scalars.GraphQLString - -class KnownArgumentNamesTest extends Specification { - - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - KnownArgumentNames knownArgumentNames = new KnownArgumentNames(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - - def "unknown field argument"() { - given: - Argument argument = Argument.newArgument("unknownArg", StringValue.newStringValue("value").build()).build() - def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString) - .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLString).build()).build() - validationContext.getFieldDef() >> fieldDefinition - when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.containsValidationError(ValidationErrorType.UnknownArgument) - } - - def "known field argument"() { - given: - Argument argument = Argument.newArgument("knownArg", StringValue.newStringValue("value").build()).build() - def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString) - .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLString).build()).build() - validationContext.getFieldDef() >> fieldDefinition - when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.errors.isEmpty() - } - - def "unknown directive argument"() { - given: - Argument argument = Argument.newArgument("unknownArg", BooleanValue.newBooleanValue(true).build()).build() - def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString).build() - def directiveDefinition = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) - .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() - validationContext.getFieldDef() >> fieldDefinition - validationContext.getDirective() >> directiveDefinition - when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.containsValidationError(ValidationErrorType.UnknownDirective) - } - - def "known directive argument"() { - given: - Argument argument = Argument.newArgument("knownArg", BooleanValue.newBooleanValue(true).build()).build() - def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString).build() - def directiveDefinition = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) - .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() - validationContext.getFieldDef() >> fieldDefinition - validationContext.getDirective() >> directiveDefinition - when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.errors.isEmpty() - } - - def "directive argument not validated against field arguments"() { - given: - Argument argument = Argument.newArgument("unknownArg", BooleanValue.newBooleanValue(true).build()).build() - def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString) - .argument(GraphQLArgument.newArgument().name("unknownArg").type(GraphQLString).build()).build() - def directiveDefinition = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) - .argument(GraphQLArgument.newArgument().name("knownArg").type(GraphQLBoolean).build()).build() - validationContext.getFieldDef() >> fieldDefinition - validationContext.getDirective() >> directiveDefinition - when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.containsValidationError(ValidationErrorType.UnknownDirective) - } - - def "directive missing argument validation error with message"() { - def query = """ - query getDogName { - dog @dogDirective(notArgument: "value"){ - name - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.UnknownDirective - validationErrors.get(0).message == "Validation error (UnknownDirective@[dog]) : Unknown directive argument 'notArgument'" - } - - def "field missing argument validation error with message"() { - def query = """ - query getDog { - dog { - doesKnowCommand(dogCommand: SIT, notArgument: false) - } - } - """ - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors.get(0).getValidationErrorType() == ValidationErrorType.UnknownArgument - validationErrors.get(0).message == "Validation error (UnknownArgument@[dog/doesKnowCommand]) : Unknown field argument 'notArgument'" - } - - static List validate(String query) { - def document = new Parser().parseDocument(query) - return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) - } -} diff --git a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy deleted file mode 100644 index 859e71c858..0000000000 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ /dev/null @@ -1,237 +0,0 @@ -package graphql.validation.rules - -import graphql.introspection.Introspection -import graphql.language.Argument -import graphql.language.Directive -import graphql.language.Field -import graphql.language.NullValue -import graphql.language.StringValue -import graphql.parser.Parser -import graphql.schema.GraphQLArgument -import graphql.schema.GraphQLDirective -import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLNonNull -import graphql.validation.SpecValidationSchema -import graphql.validation.ValidationContext -import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector -import graphql.validation.ValidationErrorType -import graphql.validation.Validator -import spock.lang.Specification - -import static graphql.Scalars.GraphQLString - -class ProvidedNonNullArgumentsTest extends Specification { - - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - ProvidedNonNullArguments providedNonNullArguments = new ProvidedNonNullArguments(validationContext, errorCollector) - - def setup() { - validationContext.i18n(_, _) >> "test error message" - } - - def "not provided and not defaulted non null field argument"() { - given: - def fieldArg = GraphQLArgument.newArgument().name("arg") - .type(GraphQLNonNull.nonNull(GraphQLString)) - def fieldDef = GraphQLFieldDefinition.newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(fieldArg) - .build() - validationContext.getFieldDef() >> fieldDef - - def field = new Field("field") - - when: - providedNonNullArguments.checkField(field) - - then: - errorCollector.containsValidationError(ValidationErrorType.MissingFieldArgument) - } - - def "not provided and not defaulted non null field argument with error message"() { - def query = """ - query getDogName { - dog { - doesKnowCommand - } - } - """.stripIndent() - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors[0].validationErrorType == ValidationErrorType.MissingFieldArgument - validationErrors[0].message == "Validation error (MissingFieldArgument@[dog/doesKnowCommand]) : Missing field argument 'dogCommand'" - } - - def "not provided and but defaulted non null field argument"() { - given: - def fieldArg = GraphQLArgument.newArgument().name("arg") - .type(GraphQLNonNull.nonNull(GraphQLString)) - .defaultValueProgrammatic("defaultVal") - def fieldDef = GraphQLFieldDefinition.newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(fieldArg) - .build() - validationContext.getFieldDef() >> fieldDef - - def field = new Field("field") - - when: - providedNonNullArguments.checkField(field) - - then: - errorCollector.getErrors().isEmpty() - } - - def "all field arguments are provided"() { - given: - def fieldArg = GraphQLArgument.newArgument().name("arg") - .type(GraphQLNonNull.nonNull(GraphQLString)) - def fieldDef = GraphQLFieldDefinition.newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(fieldArg) - .build() - validationContext.getFieldDef() >> fieldDef - - def field = new Field("field", [new Argument("arg", new StringValue("hallo"))]) - - when: - providedNonNullArguments.checkField(field) - - then: - errorCollector.getErrors().isEmpty() - } - - def "not provided not defaulted directive argument"() { - given: - def directiveArg = GraphQLArgument.newArgument() - .name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) - def graphQLDirective = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.SCALAR) - .argument(directiveArg) - .build() - validationContext.getDirective() >> graphQLDirective - - def directive = new Directive("directive") - - when: - providedNonNullArguments.checkDirective(directive, []) - - then: - errorCollector.containsValidationError(ValidationErrorType.MissingDirectiveArgument) - } - - def "not provided and not defaulted non null directive argument with error message"() { - def query = """ - query getDogName { - dog @nonNullDirective { - name - } - } - """.stripIndent() - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 1 - validationErrors[0].validationErrorType == ValidationErrorType.MissingDirectiveArgument - validationErrors[0].message == "Validation error (MissingDirectiveArgument@[dog]) : Missing directive argument 'arg1'" - } - - def "not provided but defaulted directive argument"() { - given: - def directiveArg = GraphQLArgument.newArgument() - .name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) - .defaultValueProgrammatic("defaultVal") - def graphQLDirective = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.SCALAR) - .argument(directiveArg) - .build() - validationContext.getDirective() >> graphQLDirective - - def directive = new Directive("directive") - - when: - providedNonNullArguments.checkDirective(directive, []) - - then: - errorCollector.getErrors().isEmpty() - } - - def "all directive arguments are provided"() { - given: - def directiveArg = GraphQLArgument.newArgument().name("arg").type(GraphQLNonNull.nonNull(GraphQLString)) - def graphQLDirective = GraphQLDirective.newDirective() - .name("directive") - .validLocation(Introspection.DirectiveLocation.SCALAR) - .argument(directiveArg) - .build() - validationContext.getDirective() >> graphQLDirective - - def directive = new Directive("directive", [new Argument("arg", new StringValue("hallo"))]) - - - when: - providedNonNullArguments.checkDirective(directive, []) - - then: - errorCollector.getErrors().isEmpty() - } - - def "provide the explicit value null is not valid for non null argument"() { - given: - def fieldArg = GraphQLArgument.newArgument().name("arg") - .type(GraphQLNonNull.nonNull(GraphQLString)) - - def fieldDef = GraphQLFieldDefinition.newFieldDefinition() - .name("field") - .type(GraphQLString) - .argument(fieldArg) - .build() - - validationContext.getFieldDef() >> fieldDef - - def defaultNullArg = Argument.newArgument().name("arg").value(NullValue.newNullValue().build()).build() - def field = new Field("field", [defaultNullArg]) - - when: - providedNonNullArguments.checkField(field) - - then: - errorCollector.containsValidationError(ValidationErrorType.NullValueForNonNullArgument) - } - - def "provide the explicit value null is not valid for non null argument with error message"() { - def query = """ - query getDogName { - dog { - doesKnowCommand(dogCommand: null) - } - } - """.stripIndent() - when: - def validationErrors = validate(query) - - then: - !validationErrors.empty - validationErrors.size() == 2 - validationErrors[0].validationErrorType == ValidationErrorType.NullValueForNonNullArgument - validationErrors[0].message == "Validation error (NullValueForNonNullArgument@[dog/doesKnowCommand]) : Null value for non-null field argument 'dogCommand'" - } - - static List validate(String query) { - def document = new Parser().parseDocument(query) - return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) - } -}