From 8884d2d1e48677342560e3b45537bca9ba99fa25 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 28 Jan 2026 09:40:42 +1000 Subject: [PATCH 1/9] Consolidate 31 validation rule classes into single OperationValidator Replace the AbstractRule + RulesVisitor + 31 individual rule class architecture with a single OperationValidator class that implements DocumentVisitor directly. BREAKING CHANGE: The rule filtering predicate type changes from Predicate> to Predicate in Validator, ParseAndValidate, and GraphQL. Code that filters validation rules by class reference must migrate to the new OperationValidationRule enum. - Create OperationValidationRule enum with 31 values (one per rule) - Create OperationValidator implementing DocumentVisitor with all validation logic consolidated from the 31 rule classes - Simplify Validator to create OperationValidator directly - Update ParseAndValidate and GraphQL predicate types - Delete AbstractRule, RulesVisitor, and all 31 rule classes (keep VariablesTypesMatcher as utility) - Convert all test files from mock-based rule instantiation to integration tests or OperationValidator with rule predicates - Update JMH benchmarks Co-Authored-By: Claude Opus 4.5 --- .../OverlappingFieldValidationBenchmark.java | 10 +- ...OverlappingFieldValidationPerformance.java | 10 +- src/main/java/graphql/GraphQL.java | 3 +- src/main/java/graphql/ParseAndValidate.java | 9 +- .../java/graphql/validation/AbstractRule.java | 211 --- .../validation/OperationValidationRule.java | 47 + .../validation/OperationValidator.java | 1599 +++++++++++++++++ .../java/graphql/validation/RulesVisitor.java | 197 -- .../java/graphql/validation/Validator.java | 132 +- .../rules/ArgumentsOfCorrectType.java | 38 - .../validation/rules/DeferDirectiveLabel.java | 66 - .../rules/DeferDirectiveOnRootLevel.java | 58 - .../rules/DeferDirectiveOnValidOperation.java | 84 - .../rules/ExecutableDefinitions.java | 53 - .../validation/rules/FieldsOnCorrectType.java | 35 - .../rules/FragmentsOnCompositeType.java | 46 - .../validation/rules/KnownArgumentNames.java | 45 - .../validation/rules/KnownDirectives.java | 75 - .../validation/rules/KnownFragmentNames.java | 28 - .../validation/rules/KnownOperationTypes.java | 48 - .../validation/rules/KnownTypeNames.java | 27 - .../rules/LoneAnonymousOperation.java | 48 - .../validation/rules/NoFragmentCycles.java | 118 -- .../rules/NoUndefinedVariables.java | 50 - .../validation/rules/NoUnusedFragments.java | 85 - .../validation/rules/NoUnusedVariables.java | 55 - .../rules/OverlappingFieldsCanBeMerged.java | 404 ----- .../rules/PossibleFragmentSpreads.java | 94 - .../rules/ProvidedNonNullArguments.java | 88 - .../validation/rules/ScalarLeaves.java | 39 - .../rules/SubscriptionUniqueRootField.java | 72 - .../validation/rules/UniqueArgumentNames.java | 66 - .../UniqueDirectiveNamesPerLocation.java | 79 - .../validation/rules/UniqueFragmentNames.java | 40 - .../rules/UniqueObjectFieldName.java | 34 - .../rules/UniqueOperationNames.java | 44 - .../validation/rules/UniqueVariableNames.java | 48 - .../VariableDefaultValuesOfCorrectType.java | 33 - .../validation/rules/VariableTypesMatch.java | 84 - .../rules/VariablesAreInputTypes.java | 35 - .../graphql/ParseAndValidateTest.groovy | 10 +- .../validation/RulesVisitorTest.groovy | 29 +- .../rules/ArgumentsOfCorrectTypeTest.groovy | 247 +-- .../rules/DeferDirectiveLabelTest.groovy | 72 +- .../DeferDirectiveOnRootLevelTest.groovy | 115 +- .../DeferDirectiveOnValidOperationTest.groovy | 120 +- .../rules/FieldsOnCorrectTypeTest.groovy | 54 +- .../rules/FragmentsOnCompositeTypeTest.groovy | 151 +- .../rules/KnownArgumentNamesTest.groovy | 115 +- .../rules/KnownDirectivesTest.groovy | 89 +- .../rules/KnownFragmentNamesTest.groovy | 24 +- .../rules/KnownTypeNamesTest.groovy | 25 +- .../rules/NoFragmentCyclesTest.groovy | 14 +- .../rules/NoUndefinedVariablesTest.groovy | 20 +- .../rules/NoUnusedFragmentsTest.groovy | 45 +- .../rules/NoUnusedVariablesTest.groovy | 10 +- .../OverlappingFieldsCanBeMergedTest.groovy | 24 +- .../rules/PossibleFragmentSpreadsTest.groovy | 13 +- .../rules/ProvidedNonNullArgumentsTest.groovy | 195 +- .../validation/rules/ScalarLeavesTest.groovy | 42 +- ...iableDefaultValuesOfCorrectTypeTest.groovy | 55 +- .../rules/VariableTypesMatchTest.groovy | 35 +- .../rules/VariablesAreInputTypesTest.groovy | 50 +- 63 files changed, 2247 insertions(+), 3644 deletions(-) delete mode 100644 src/main/java/graphql/validation/AbstractRule.java create mode 100644 src/main/java/graphql/validation/OperationValidationRule.java create mode 100644 src/main/java/graphql/validation/OperationValidator.java delete mode 100644 src/main/java/graphql/validation/RulesVisitor.java delete mode 100644 src/main/java/graphql/validation/rules/ArgumentsOfCorrectType.java delete mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveLabel.java delete mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveOnRootLevel.java delete mode 100644 src/main/java/graphql/validation/rules/DeferDirectiveOnValidOperation.java delete mode 100644 src/main/java/graphql/validation/rules/ExecutableDefinitions.java delete mode 100644 src/main/java/graphql/validation/rules/FieldsOnCorrectType.java delete mode 100644 src/main/java/graphql/validation/rules/FragmentsOnCompositeType.java delete mode 100644 src/main/java/graphql/validation/rules/KnownArgumentNames.java delete mode 100644 src/main/java/graphql/validation/rules/KnownDirectives.java delete mode 100644 src/main/java/graphql/validation/rules/KnownFragmentNames.java delete mode 100644 src/main/java/graphql/validation/rules/KnownOperationTypes.java delete mode 100644 src/main/java/graphql/validation/rules/KnownTypeNames.java delete mode 100644 src/main/java/graphql/validation/rules/LoneAnonymousOperation.java delete mode 100644 src/main/java/graphql/validation/rules/NoFragmentCycles.java delete mode 100644 src/main/java/graphql/validation/rules/NoUndefinedVariables.java delete mode 100644 src/main/java/graphql/validation/rules/NoUnusedFragments.java delete mode 100644 src/main/java/graphql/validation/rules/NoUnusedVariables.java delete mode 100644 src/main/java/graphql/validation/rules/OverlappingFieldsCanBeMerged.java delete mode 100644 src/main/java/graphql/validation/rules/PossibleFragmentSpreads.java delete mode 100644 src/main/java/graphql/validation/rules/ProvidedNonNullArguments.java delete mode 100644 src/main/java/graphql/validation/rules/ScalarLeaves.java delete mode 100644 src/main/java/graphql/validation/rules/SubscriptionUniqueRootField.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueArgumentNames.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueDirectiveNamesPerLocation.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueFragmentNames.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueObjectFieldName.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueOperationNames.java delete mode 100644 src/main/java/graphql/validation/rules/UniqueVariableNames.java delete mode 100644 src/main/java/graphql/validation/rules/VariableDefaultValuesOfCorrectType.java delete mode 100644 src/main/java/graphql/validation/rules/VariableTypesMatch.java delete mode 100644 src/main/java/graphql/validation/rules/VariablesAreInputTypes.java 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 5c0bd83494..f27d84ab2c 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,7 +599,7 @@ 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); + 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); diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index 0ecb17947d..f29fa09f4b 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -6,6 +6,7 @@ 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; @@ -88,7 +89,7 @@ public static ParseAndValidateResult parse(@NonNull ExecutionInput executionInpu * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Locale locale) { - return validate(graphQLSchema, parsedDocument, ruleClass -> true, locale); + return validate(graphQLSchema, parsedDocument, rule -> true, locale); } /** @@ -100,7 +101,7 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument) { - return validate(graphQLSchema, parsedDocument, ruleClass -> true, Locale.getDefault()); + return validate(graphQLSchema, parsedDocument, rule -> true, Locale.getDefault()); } /** @@ -113,7 +114,7 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * * @return a result object that indicates how this operation went */ - public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Predicate> rulePredicate, @NonNull 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); } @@ -127,7 +128,7 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * * @return a result object that indicates how this operation went */ - public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull 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/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..49adf55bf7 --- /dev/null +++ b/src/main/java/graphql/validation/OperationValidationRule.java @@ -0,0 +1,47 @@ +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}. + */ +@PublicApi +@NullMarked +public enum OperationValidationRule { + EXECUTABLE_DEFINITIONS, + ARGUMENTS_OF_CORRECT_TYPE, + FIELDS_ON_CORRECT_TYPE, + FRAGMENTS_ON_COMPOSITE_TYPE, + KNOWN_ARGUMENT_NAMES, + KNOWN_DIRECTIVES, + KNOWN_FRAGMENT_NAMES, + KNOWN_TYPE_NAMES, + NO_FRAGMENT_CYCLES, + NO_UNDEFINED_VARIABLES, + NO_UNUSED_FRAGMENTS, + NO_UNUSED_VARIABLES, + OVERLAPPING_FIELDS_CAN_BE_MERGED, + POSSIBLE_FRAGMENT_SPREADS, + PROVIDED_NON_NULL_ARGUMENTS, + SCALAR_LEAVES, + VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE, + VARIABLES_ARE_INPUT_TYPES, + VARIABLE_TYPES_MATCH, + LONE_ANONYMOUS_OPERATION, + UNIQUE_OPERATION_NAMES, + UNIQUE_FRAGMENT_NAMES, + UNIQUE_DIRECTIVE_NAMES_PER_LOCATION, + UNIQUE_ARGUMENT_NAMES, + UNIQUE_VARIABLE_NAMES, + SUBSCRIPTION_UNIQUE_ROOT_FIELD, + UNIQUE_OBJECT_FIELD_NAME, + DEFER_DIRECTIVE_ON_ROOT_LEVEL, + DEFER_DIRECTIVE_ON_VALID_OPERATION, + DEFER_DIRECTIVE_LABEL, + KNOWN_OPERATION_TYPES, +} diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java new file mode 100644 index 0000000000..6529fab837 --- /dev/null +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -0,0 +1,1599 @@ +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.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.validation.rules.VariablesTypesMatcher; +import graphql.util.StringKit; + +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.LinkedList; +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.util.FpKit.filterSet; +import static graphql.util.FpKit.groupingBy; +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. + */ +@Internal +@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 (from RulesVisitor) --- + private boolean operationScope = false; + private int fragmentSpreadVisitDepth = 0; + 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 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 --- + // fragmentSpreadVisitDepth == 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 + + public OperationValidator(ValidationContext validationContext, ValidationErrorCollector errorCollector, Predicate rulePredicate) { + this.validationContext = validationContext; + this.errorCollector = errorCollector; + this.validationUtil = new ValidationUtil(); + this.rulePredicate = rulePredicate; + prepareFragmentSpreadsMap(); + } + + private boolean isRuleEnabled(OperationValidationRule rule) { + return rulePredicate.test(rule); + } + + /** + * True when we are NOT inside a manually-traversed fragment definition. + * Non-fragment-spread rules run in this context. + */ + private boolean shouldRunNonFragmentSpreadChecks() { + return fragmentSpreadVisitDepth == 0; + } + + /** + * True when we are inside an operation scope (including manual fragment traversal). + * Fragment-spread-visiting rules (NoUndefinedVariables, NoUnusedVariables, VariableTypesMatch, + * DeferDirectiveOnRootLevel, DeferDirectiveOnValidOperation) run in this context. + */ + private boolean shouldRunFragmentSpreadChecks() { + return operationScope; + } + + // ==================== DocumentVisitor ==================== + + @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(); + } + } + + // ==================== Error Reporting (from AbstractRule) ==================== + + 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, 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 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) { + return (validationContext != null && + validationContext.getGraphQLContext() != null || + validationContext.getGraphQLContext().get(key) != null || + ((Boolean) validationContext.getGraphQLContext().get(key))); + } + + // ==================== Dispatch Methods ==================== + + private void checkDocument(Document document) { + // ExecutableDefinitions + if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { + validateExecutableDefinitions(document); + } + // UniqueDirectiveNamesPerLocation - no-op on document in original + } + + private void checkArgument(Argument argument) { + if (shouldRunNonFragmentSpreadChecks()) { + // ArgumentsOfCorrectType + if (isRuleEnabled(OperationValidationRule.ARGUMENTS_OF_CORRECT_TYPE)) { + validateArgumentsOfCorrectType(argument); + } + // KnownArgumentNames + if (isRuleEnabled(OperationValidationRule.KNOWN_ARGUMENT_NAMES)) { + validateKnownArgumentNames(argument); + } + } + // Fragment spread visiting rules that check arguments - none currently + if (shouldRunFragmentSpreadChecks()) { + // ArgumentsOfCorrectType also needs to run during fragment spread traversal + if (isRuleEnabled(OperationValidationRule.ARGUMENTS_OF_CORRECT_TYPE)) { + // Only run if we're in the fragment spread depth (these are the fragment-spread-visiting rules) + // Actually, ArgumentsOfCorrectType is NOT a fragment spread visiting rule, so only run it + // when shouldRunNonFragmentSpreadChecks(). Already handled above. + } + } + } + + private void checkTypeName(TypeName typeName) { + if (shouldRunNonFragmentSpreadChecks()) { + if (isRuleEnabled(OperationValidationRule.KNOWN_TYPE_NAMES)) { + validateKnownTypeNames(typeName); + } + } + } + + private void checkVariableDefinition(VariableDefinition variableDefinition) { + // These are all non-fragment-spread rules + if (shouldRunNonFragmentSpreadChecks()) { + if (isRuleEnabled(OperationValidationRule.VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE)) { + validateVariableDefaultValuesOfCorrectType(variableDefinition); + } + if (isRuleEnabled(OperationValidationRule.VARIABLES_ARE_INPUT_TYPES)) { + validateVariablesAreInputTypes(variableDefinition); + } + } + // NoUndefinedVariables: track defined variables (fragment-spread-visiting) + if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { + definedVariableNames.add(variableDefinition.getName()); + } + // NoUnusedVariables: track definitions (fragment-spread-visiting) + if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + unusedVars_variableDefinitions.add(variableDefinition); + } + // VariableTypesMatch: track definitions (fragment-spread-visiting) + if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { + if (variableDefinitionMap != null) { + variableDefinitionMap.put(variableDefinition.getName(), variableDefinition); + } + } + } + + private void checkField(Field field) { + if (shouldRunNonFragmentSpreadChecks()) { + 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 (shouldRunNonFragmentSpreadChecks()) { + 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) { + // Non-fragment-spread rules + if (shouldRunNonFragmentSpreadChecks()) { + 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); + } + } + // Fragment-spread-visiting rules for directives + if (shouldRunFragmentSpreadChecks()) { + 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) { + // Non-fragment-spread checks on the spread itself + if (shouldRunNonFragmentSpreadChecks()) { + 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()); + fragmentSpreadVisitDepth++; + new LanguageTraversal(ancestors).traverse(fragment, this); + fragmentSpreadVisitDepth--; + } + } + } + + private void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { + if (shouldRunNonFragmentSpreadChecks()) { + 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 (shouldRunNonFragmentSpreadChecks()) { + 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); + } + } + + // Fragment-spread-visiting rules: reset per operation + 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) { + // Fragment-spread-visiting rules + if (shouldRunFragmentSpreadChecks()) { + if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { + validateNoUndefinedVariables(variableReference); + } + if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { + validateVariableTypesMatch(variableReference); + } + } + // NoUnusedVariables also visits fragment spreads + if (shouldRunFragmentSpreadChecks()) { + 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 (shouldRunNonFragmentSpreadChecks()) { + if (isRuleEnabled(OperationValidationRule.UNIQUE_OBJECT_FIELD_NAME)) { + validateUniqueObjectFieldName(objectValue); + } + } + } + + // ==================== Leave Methods ==================== + + private void leaveOperationDefinition(OperationDefinition operationDefinition) { + // fragments should be revisited for each operation + visitedFragmentSpreads.clear(); + operationScope = false; + + // NoUnusedVariables: check on leave + 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) { + // NoUnusedFragments + if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + validateNoUnusedFragments(); + } + // LoneAnonymousOperation cleanup + if (isRuleEnabled(OperationValidationRule.LONE_ANONYMOUS_OPERATION)) { + hasAnonymousOp = false; + } + } + + // ==================== Validation Rule Implementations ==================== + + // --- 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) { + 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(FragmentCycle, Collections.singletonList(fragmentDefinition), message); + } + } + } + + private Map> buildTransitiveSpreads(LinkedList path, Map> transitiveSpreads) { + String name = path.peekFirst(); + 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; + } + LinkedList childPath = new LinkedList<>(path); + childPath.add(0, 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() { + 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); + } + } + + // --- OverlappingFieldsCanBeMerged --- + private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationDefinition) { + overlappingFieldsImpl(operationDefinition.getSelectionSet(), validationContext.getOutputType()); + } + + private void overlappingFieldsImpl(SelectionSet selectionSet, GraphQLOutputType graphQLOutputType) { + Map> fieldMap = new LinkedHashMap<>(); + 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, 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, 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, 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 = validationContext.getSchema().getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, field.getName()); + fieldType = fieldDefinition != null ? fieldDefinition.getType() : null; + } + fieldMap.get(responseName).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) { + 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); + } + } + + // --- 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) { + return validationContext.getSchema().getImplementations((GraphQLInterfaceType) type); + } 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; + 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); + } + } + } + } + + private void validateProvidedNonNullArguments_directive(Directive directive) { + GraphQLDirective graphQLDirective = validationContext.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; + } + + // --- 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/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/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/validation/RulesVisitorTest.groovy b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy index 2b0de3a993..d023cc18ef 100644 --- a/src/test/groovy/graphql/validation/RulesVisitorTest.groovy +++ b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy @@ -7,19 +7,15 @@ import graphql.parser.Parser import spock.lang.Specification class RulesVisitorTest extends Specification { - AbstractRule simpleRule = Mock() - AbstractRule visitsSpreadsRule = Mock() - def setup() { - visitsSpreadsRule.isVisitFragmentSpreads() >> true - } + ValidationErrorCollector errorCollector = new ValidationErrorCollector() 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) LanguageTraversal languageTraversal = new LanguageTraversal() - languageTraversal.traverse(document, new RulesVisitor(validationContext, [simpleRule, visitsSpreadsRule])) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, { rule -> true })) } def "RulesVisitor does not repeatedly spread directly recursive fragments leading to a stackoverflow"() { @@ -73,25 +69,4 @@ class RulesVisitorTest extends Specification { then: notThrown(StackOverflowError) } - - def "RulesVisitor visits fragment definition with isVisitFragmentSpread rules once per operation"() { - 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 } - """ - - when: - traverse(query) - - then: - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "A"}) - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "B"}) - 2 * visitsSpreadsRule.checkFragmentDefinition({it.name == "C"}) - } } - diff --git a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy index 7e8210955e..739aa86417 100644 --- a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy @@ -1,55 +1,20 @@ 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 - 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) @@ -64,28 +29,34 @@ class ArgumentsOfCorrectTypeTest extends Specification { } 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 + def query = """ + query getDog(\$cmd: DogCommand!) { + dog { + doesKnowCommand(dogCommand: \$cmd) + } + } + """ when: - argumentsOfCorrectType.checkArgument(argumentLiteral) + def validationErrors = validate(query) + then: - errorCollector.errors.isEmpty() + validationErrors.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 + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: "notAnEnum") + } + } + """ when: - argumentsOfCorrectType.checkArgument(argumentLiteral) + def validationErrors = validate(query) + then: - errorCollector.containsValidationError(ValidationErrorType.WrongType) - errorCollector.errors.size() == 1 + !validationErrors.isEmpty() + validationErrors.any { it.validationErrorType == ValidationErrorType.WrongType } } def "invalid type scalar results in error with message"() { @@ -93,7 +64,7 @@ class ArgumentsOfCorrectTypeTest extends Specification { query getDog { dog(arg1: 1) { name - } + } } """ when: @@ -106,103 +77,12 @@ class ArgumentsOfCorrectTypeTest extends Specification { 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: @@ -215,31 +95,12 @@ class ArgumentsOfCorrectTypeTest extends Specification { 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: @@ -252,33 +113,12 @@ class ArgumentsOfCorrectTypeTest extends Specification { 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: @@ -291,33 +131,12 @@ class ArgumentsOfCorrectTypeTest extends Specification { 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: @@ -330,22 +149,12 @@ class ArgumentsOfCorrectTypeTest extends Specification { 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: diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy b/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy index 2e3975269d..38076e62e8 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy @@ -1,13 +1,13 @@ package graphql.validation.rules 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,19 +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 + 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"() { @@ -43,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() @@ -60,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() @@ -89,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() @@ -113,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() @@ -144,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() @@ -162,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() @@ -194,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() @@ -210,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/rules/DeferDirectiveOnRootLevelTest.groovy index ec65ec1937..dbc407255e 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy @@ -5,7 +5,8 @@ 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/rules/DeferDirectiveOnValidOperationTest.groovy index 1430b7743b..2d78539e7d 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy +++ b/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy @@ -5,7 +5,8 @@ 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/FieldsOnCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy index 8bbb8052e3..857f9edc18 100644 --- a/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy @@ -1,63 +1,35 @@ package graphql.validation.rules -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 "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/rules/FragmentsOnCompositeTypeTest.groovy b/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy index ccd918cbdb..02d0325e35 100644 --- a/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy @@ -2,94 +2,101 @@ 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.parser.Parser +import graphql.validation.SpecValidationSchema import graphql.validation.ValidationError -import graphql.validation.ValidationErrorCollector import graphql.validation.ValidationErrorType +import graphql.validation.Validator import spock.lang.Specification class FragmentsOnCompositeTypeTest extends Specification { - ValidationContext validationContext = Mock(ValidationContext) - ValidationErrorCollector errorCollector = new ValidationErrorCollector() - FragmentsOnCompositeType fragmentsOnCompositeType = new FragmentsOnCompositeType(validationContext, errorCollector) - 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 - + def query = """ + { + dog { + ... on String { + name + } + } + } + """ when: - fragmentsOnCompositeType.checkInlineFragment(inlineFragment) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.InlineFragmentTypeConditionInvalid) - errorCollector.errors.size() == 1 + validationErrors.any { it.validationErrorType == ValidationErrorType.InlineFragmentTypeConditionInvalid } } - def "should results in no error"(InlineFragment inlineFragment) { - given: - validationContext.getSchema() >> StarWarsSchema.starWarsSchema - + def "should result in no error for inline fragment without type condition"() { + def query = """ + { + dog { + ... { + name + } + } + } + """ when: - fragmentsOnCompositeType.checkInlineFragment(inlineFragment) + def validationErrors = validate(query) then: - errorCollector.errors.isEmpty() - - where: - inlineFragment << [ - getInlineFragmentWithTypeConditionNull(), - getInlineFragmentWithConditionWithStrangeType(), - getInlineFragmentWithConditionWithRightType() - ] + validationErrors.empty } - private InlineFragment getInlineFragmentWithTypeConditionNull() { - InlineFragment.newInlineFragment().build() - } - - private InlineFragment getInlineFragmentWithConditionWithStrangeType() { - InlineFragment.newInlineFragment().typeCondition(TypeName.newTypeName("StrangeType").build()).build() - } + def "should result in no error for inline fragment with composite type condition"() { + def query = """ + { + dog { + ... on Pet { + name + } + } + } + """ + when: + def validationErrors = validate(query) - private InlineFragment getInlineFragmentWithConditionWithRightType() { - InlineFragment.newInlineFragment().typeCondition(TypeName.newTypeName("Character").build()).build() + then: + validationErrors.empty } 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 - + def query = """ + { + dog { + ...frag + } + } + fragment frag on String { + length + } + """ when: - fragmentsOnCompositeType.checkFragmentDefinition(fragmentDefinition) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.FragmentTypeConditionInvalid) + 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 + device: String version: String } """) @@ -100,21 +107,21 @@ class FragmentsOnCompositeTypeTest extends Specification { 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 - } + .query(''' + mutation UpdateUDI($input: UDIInput!) { + updateUDI(input: $input) { + ...fragOnInputType + __typename + } } - + # fragment should only target composite types - fragment fragOnInputType on UDIInput { + fragment fragOnInputType on UDIInput { device - version - __typename - } - + version + __typename + } + ''') .variables([input: [device: 'device', version: 'version'] ]) .build() @@ -132,17 +139,17 @@ class FragmentsOnCompositeTypeTest extends Specification { 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) { + .query(''' + mutation UpdateUDI($input: UDIInput!) { + updateUDI(input: $input) { # fragment should only target composite types - ... on UDIInput { + ... on UDIInput { device - version - __typename - } - __typename - } + version + __typename + } + __typename + } } ''') .variables([input: [device: 'device', version: 'version'] ]) @@ -158,4 +165,8 @@ class FragmentsOnCompositeTypeTest extends Specification { (executionResult.errors[0] as ValidationError).message == "Validation error (InlineFragmentTypeConditionInvalid@[updateUDI]) : Inline fragment type condition is invalid, must be on Object/Interface/Union" } + 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/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy index e437b43eda..06151e2efb 100644 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy @@ -1,101 +1,74 @@ 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 "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 + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT, unknownArg: false) + } + } + """ when: - knownArgumentNames.checkArgument(argument) + def validationErrors = validate(query) + then: - errorCollector.containsValidationError(ValidationErrorType.UnknownArgument) + !validationErrors.empty + validationErrors.any { it.validationErrorType == 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 + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + """ when: - knownArgumentNames.checkArgument(argument) + def validationErrors = validate(query) + then: - errorCollector.errors.isEmpty() + validationErrors.empty } 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 + def query = """ + query getDogName { + dog @dogDirective(unknownArg: "value") { + name + } + } + """ when: - knownArgumentNames.checkArgument(argument) - then: - errorCollector.containsValidationError(ValidationErrorType.UnknownDirective) - } + def validationErrors = validate(query) - 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() + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.UnknownDirective } } - 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 + def "known directive argument results in no error"() { + def query = """ + query getDogName { + dog { + name + } + } + """ when: - knownArgumentNames.checkArgument(argument) + def validationErrors = validate(query) + then: - errorCollector.containsValidationError(ValidationErrorType.UnknownDirective) + validationErrors.empty } def "directive missing argument validation error with message"() { @@ -103,7 +76,7 @@ class KnownArgumentNamesTest extends Specification { query getDogName { dog @dogDirective(notArgument: "value"){ name - } + } } """ when: @@ -121,7 +94,7 @@ class KnownArgumentNamesTest extends Specification { query getDog { dog { doesKnowCommand(dogCommand: SIT, notArgument: false) - } + } } """ when: diff --git a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy index 8ac2b0d037..fe3a083a13 100644 --- a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy @@ -2,11 +2,12 @@ package graphql.validation.rules 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,14 +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 + 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) } @@ -34,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) @@ -53,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() @@ -72,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() @@ -92,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) @@ -113,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) @@ -129,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) @@ -161,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() @@ -182,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() @@ -203,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() @@ -225,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() @@ -237,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) @@ -257,7 +230,7 @@ class KnownDirectivesTest extends Specification { def "invalid directive on SUBSCRIPTION"() { def spec = ''' subscription sub @queryDirective{ - field + field } ''' @@ -275,7 +248,7 @@ class KnownDirectivesTest extends Specification { def "unknown directive on SUBSCRIPTION"() { def spec = ''' subscription sub @unknownDirective{ - field + field } ''' @@ -293,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/rules/KnownFragmentNamesTest.groovy index 5a56b13514..bc04e0aafe 100644 --- a/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy @@ -1,30 +1,28 @@ package graphql.validation.rules -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 "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 '() { @@ -32,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/rules/KnownTypeNamesTest.groovy index fb84d8739d..e0356757d5 100644 --- a/src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy @@ -1,31 +1,30 @@ package graphql.validation.rules -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 "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/NoFragmentCyclesTest.groovy b/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy index ea3e1807f7..737b2dd1bf 100644 --- a/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy @@ -5,11 +5,11 @@ 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/rules/NoUndefinedVariablesTest.groovy index 90f9976a4d..35e7f70e3d 100644 --- a/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy @@ -5,7 +5,8 @@ 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/rules/NoUnusedFragmentsTest.groovy index 3fde31a3fb..352418ded5 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy @@ -1,11 +1,13 @@ package graphql.validation.rules +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,13 +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 + 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"() { @@ -47,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) { @@ -81,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() @@ -93,6 +93,7 @@ class NoUnusedFragmentsTest extends Specification { def "contains unknown fragments"() { + given: def query = """ query Foo { human(id: 4) { @@ -122,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) @@ -167,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) @@ -183,7 +178,7 @@ class NoUnusedFragmentsTest extends Specification { query getDogName { dog { name - } + } } fragment dogFragment on Dog { barkVolume } """.stripIndent() @@ -201,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/rules/NoUnusedVariablesTest.groovy index 9b7d0dc8de..a1cd22c5f6 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy @@ -5,7 +5,8 @@ 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/rules/OverlappingFieldsCanBeMergedTest.groovy index d35ec38084..80c71591ba 100644 --- a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy +++ b/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy @@ -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/rules/PossibleFragmentSpreadsTest.groovy index 87c465fd38..2f49c941bd 100644 --- a/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy @@ -4,7 +4,8 @@ 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/rules/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy index 9a11066973..f720e30b12 100644 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy @@ -1,50 +1,28 @@ 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 "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") - + def "not provided non null field argument results in error"() { + def query = """ + query getDogName { + dog { + doesKnowCommand + } + } + """ when: - providedNonNullArguments.checkField(field) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.MissingFieldArgument) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.MissingFieldArgument } } def "not provided and not defaulted non null field argument with error message"() { @@ -52,7 +30,7 @@ class ProvidedNonNullArgumentsTest extends Specification { query getDogName { dog { doesKnowCommand - } + } } """.stripIndent() when: @@ -65,65 +43,35 @@ class ProvidedNonNullArgumentsTest extends Specification { 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"))]) - + def "all field arguments are provided results in no error"() { + def query = """ + query getDog { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + """ when: - providedNonNullArguments.checkField(field) + def validationErrors = validate(query) then: - errorCollector.getErrors().isEmpty() + validationErrors.empty } - 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") - + def "not provided not defaulted directive argument results in error"() { + def query = """ + query getDogName { + dog @nonNullDirective { + name + } + } + """ when: - providedNonNullArguments.checkDirective(directive, []) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.MissingDirectiveArgument) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.MissingDirectiveArgument } } def "not provided and not defaulted non null directive argument with error message"() { @@ -131,7 +79,7 @@ class ProvidedNonNullArgumentsTest extends Specification { query getDogName { dog @nonNullDirective { name - } + } } """.stripIndent() when: @@ -144,68 +92,35 @@ class ProvidedNonNullArgumentsTest extends Specification { 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"))]) - - + def "all directive arguments are provided results in no error"() { + def query = """ + query getDogName { + dog @nonNullDirective(arg1: "value") { + name + } + } + """ when: - providedNonNullArguments.checkDirective(directive, []) + def validationErrors = validate(query) then: - errorCollector.getErrors().isEmpty() + validationErrors.empty } 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]) - + def query = """ + query getDogName { + dog { + doesKnowCommand(dogCommand: null) + } + } + """ when: - providedNonNullArguments.checkField(field) + def validationErrors = validate(query) then: - errorCollector.containsValidationError(ValidationErrorType.NullValueForNonNullArgument) + !validationErrors.empty + validationErrors.any { it.validationErrorType == ValidationErrorType.NullValueForNonNullArgument } } def "provide the explicit value null is not valid for non null argument with error message"() { @@ -213,7 +128,7 @@ class ProvidedNonNullArgumentsTest extends Specification { query getDogName { dog { doesKnowCommand(dogCommand: null) - } + } } """.stripIndent() when: diff --git a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy b/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy index 14934a4846..d7f6c6c2d0 100644 --- a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy @@ -1,35 +1,30 @@ package graphql.validation.rules -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 "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"() { @@ -53,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/VariableDefaultValuesOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy index 29f132d411..49ec51887f 100644 --- a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy @@ -1,43 +1,36 @@ package graphql.validation.rules -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) + def "default value has wrong type"() { + setup: + def schema = ''' + type Query { + field(arg: String) : String + } + ''' - void setup() { - def context = GraphQLContext.getDefault() - validationContext.getGraphQLContext() >> context - validationContext.getI18n() >> i18n - i18n.getLocale() >> Locale.ENGLISH - } + def query = ''' + query($arg: String = false) { + field(arg: $arg) + } + ''' + + def graphQlSchema = TestUtil.schema(schema) + def document = TestUtil.parseQuery(query) + def validator = new Validator() - 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 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"() { @@ -46,7 +39,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { type User { id: String } - + type Query { getUsers(howMany: Int) : [User] } @@ -56,7 +49,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { query($howMany: Int = "NotANumber") { getUsers(howMany: $howMany) { id - } + } } ''' @@ -80,7 +73,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { type User { id: String } - + type Query { getUsers(howMany: Int) : [User] } @@ -90,7 +83,7 @@ class VariableDefaultValuesOfCorrectTypeTest extends Specification { query($howMany: Int = "NotANumber") { getUsers(howMany: $howMany) { id - } + } } ''' @@ -107,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/rules/VariableTypesMatchTest.groovy index 5ccaff13dd..6816f59abd 100644 --- a/src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy +++ b/src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy @@ -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/rules/VariablesAreInputTypesTest.groovy index b01eebabc3..70896af279 100644 --- a/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy @@ -1,34 +1,42 @@ package graphql.validation.rules -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"() { - given: - def astType = new NonNullType(new ListType(new TypeName(StarWarsSchema.droidType.getName()))) - VariableDefinition variableDefinition = new VariableDefinition("var", astType) - validationContext.getSchema() >> StarWarsSchema.starWarsSchema + setup: + def schema = ''' + type Droid { + name: String + } + + type Query { + droid(id: String): Droid + } + ''' + + 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"() { @@ -37,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 } @@ -55,7 +63,7 @@ class VariablesAreInputTypesTest extends Specification { mutation createUser($user: User){ createUser(user: $user) { id - } + } } ''' From b3e3f3e942331ce68cc7de914b4fcdb8f983c2c3 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 28 Jan 2026 10:10:19 +1000 Subject: [PATCH 2/9] Restore 10 test coverage gaps lost during validation refactor Add integration tests replacing removed mock-based tests: - Per-operation fragment visiting (RulesVisitorTest) - Invalid input object field, list, nested list, and simple list types (ArgumentsOfCorrectTypeTest) - Null value for non-null input object field (ArgumentsOfCorrectTypeTest) - Unknown type on inline fragment not triggering composite type error (FragmentsOnCompositeTypeTest) - Directive argument scope isolation (KnownArgumentNamesTest) - Defaulted non-null field and directive arguments (ProvidedNonNullArgumentsTest) Co-Authored-By: Claude Opus 4.5 --- .../validation/RulesVisitorTest.groovy | 27 +++++++- .../rules/ArgumentsOfCorrectTypeTest.groovy | 62 +++++++++++++++++++ .../rules/FragmentsOnCompositeTypeTest.groovy | 17 +++++ .../rules/KnownArgumentNamesTest.groovy | 14 +++++ .../rules/ProvidedNonNullArgumentsTest.groovy | 54 ++++++++++++++++ 5 files changed, 172 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/validation/RulesVisitorTest.groovy b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy index d023cc18ef..079a0caaf4 100644 --- a/src/test/groovy/graphql/validation/RulesVisitorTest.groovy +++ b/src/test/groovy/graphql/validation/RulesVisitorTest.groovy @@ -4,18 +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 { 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 OperationValidator(validationContext, errorCollector, { rule -> true })) + languageTraversal.traverse(document, new OperationValidator(validationContext, errorCollector, rulePredicate)) } def "RulesVisitor does not repeatedly spread directly recursive fragments leading to a stackoverflow"() { @@ -69,4 +76,20 @@ class RulesVisitorTest extends Specification { then: notThrown(StackOverflowError) } + + def "OperationValidator visits fragment definitions per-operation for fragment-spread rules"() { + given: + def query = """ + fragment HumanFields on __Type { fields(includeDeprecated: \$inc) { name } } + + query Q1(\$inc: Boolean!) { __schema { queryType { ...HumanFields } } } + query Q2 { __schema { queryType { ...HumanFields } } } + """ + when: + traverse(query, TestUtil.dummySchema, { r -> r == OperationValidationRule.NO_UNDEFINED_VARIABLES }) + then: + // 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/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy index 739aa86417..132ddd537e 100644 --- a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy @@ -1,5 +1,6 @@ package graphql.validation.rules +import graphql.TestUtil import graphql.parser.Parser import graphql.validation.SpecValidationSchema import graphql.validation.ValidationError @@ -198,6 +199,67 @@ class ArgumentsOfCorrectTypeTest extends Specification { ''' | _ } + 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/FragmentsOnCompositeTypeTest.groovy b/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy index 02d0325e35..09d80c4302 100644 --- a/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy +++ b/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy @@ -165,6 +165,23 @@ class FragmentsOnCompositeTypeTest extends Specification { (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/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy index 06151e2efb..020f8356fd 100644 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy @@ -107,6 +107,20 @@ class KnownArgumentNamesTest extends Specification { 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/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy index f720e30b12..f21f45b50d 100644 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ b/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy @@ -1,12 +1,21 @@ package graphql.validation.rules 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"() { @@ -141,6 +150,51 @@ class ProvidedNonNullArgumentsTest extends Specification { 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) From 1f6d39d400df18b61806262ee5ee8a5271b1d4f5 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 28 Jan 2026 10:58:03 +1000 Subject: [PATCH 3/9] Move VariablesTypesMatcher and all validation tests from rules sub-package to validation package The rules sub-package is no longer needed since all 31 rule classes were consolidated into OperationValidator. Move the remaining utility class (VariablesTypesMatcher) and all 32 test files into the parent graphql.validation package, then delete the empty rules directories. Also remove stale JSpecifyAnnotationsCheck entries for deleted rule classes and update comments referencing the old package. Co-Authored-By: Claude Opus 4.5 --- src/main/java/graphql/execution/MergedField.java | 2 +- src/main/java/graphql/normalized/ENFMerger.java | 2 +- .../java/graphql/normalized/nf/NormalizedFieldsMerger.java | 2 +- src/main/java/graphql/validation/OperationValidator.java | 1 - .../validation/{rules => }/VariablesTypesMatcher.java | 2 +- .../groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy | 5 +---- .../validation/{rules => }/ArgumentsOfCorrectTypeTest.groovy | 2 +- .../validation/{rules => }/DeferDirectiveLabelTest.groovy | 2 +- .../{rules => }/DeferDirectiveOnRootLevelTest.groovy | 2 +- .../{rules => }/DeferDirectiveOnValidOperationTest.groovy | 2 +- .../validation/{rules => }/ExecutableDefinitionsTest.groovy | 2 +- .../validation/{rules => }/FieldsOnCorrectTypeTest.groovy | 2 +- .../{rules => }/FragmentsOnCompositeTypeTest.groovy | 2 +- src/test/groovy/graphql/validation/{rules => }/Harness.java | 2 +- .../validation/{rules => }/KnownArgumentNamesTest.groovy | 2 +- .../validation/{rules => }/KnownDirectivesTest.groovy | 2 +- .../validation/{rules => }/KnownFragmentNamesTest.groovy | 2 +- .../graphql/validation/{rules => }/KnownTypeNamesTest.groovy | 2 +- .../validation/{rules => }/LoneAnonymousOperationTest.groovy | 2 +- .../validation/{rules => }/NoFragmentCyclesTest.groovy | 2 +- .../validation/{rules => }/NoUndefinedVariablesTest.groovy | 2 +- .../validation/{rules => }/NoUnusedFragmentsTest.groovy | 2 +- .../validation/{rules => }/NoUnusedVariablesTest.groovy | 2 +- .../{rules => }/OverlappingFieldsCanBeMergedTest.groovy | 2 +- .../{rules => }/PossibleFragmentSpreadsTest.groovy | 2 +- .../{rules => }/ProvidedNonNullArgumentsTest.groovy | 2 +- .../graphql/validation/{rules => }/ScalarLeavesTest.groovy | 2 +- .../{rules => }/SubscriptionUniqueRootFieldTest.groovy | 2 +- .../validation/{rules => }/UniqueArgumentNamesTest.groovy | 2 +- .../{rules => }/UniqueDirectiveNamesPerLocationTest.groovy | 2 +- .../validation/{rules => }/UniqueFragmentNamesTest.groovy | 2 +- .../validation/{rules => }/UniqueObjectFieldNameTest.groovy | 2 +- .../validation/{rules => }/UniqueOperationNamesTest.groovy | 2 +- .../validation/{rules => }/UniqueVariableNamesTest.groovy | 2 +- .../VariableDefaultValuesOfCorrectTypeTest.groovy | 2 +- .../validation/{rules => }/VariableTypesMatchTest.groovy | 2 +- .../validation/{rules => }/VariablesAreInputTypesTest.groovy | 2 +- .../validation/{rules => }/VariablesTypesMatcherTest.groovy | 2 +- 38 files changed, 37 insertions(+), 41 deletions(-) rename src/main/java/graphql/validation/{rules => }/VariablesTypesMatcher.java (98%) rename src/test/groovy/graphql/validation/{rules => }/ArgumentsOfCorrectTypeTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/DeferDirectiveLabelTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/DeferDirectiveOnRootLevelTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/DeferDirectiveOnValidOperationTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/ExecutableDefinitionsTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/FieldsOnCorrectTypeTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/FragmentsOnCompositeTypeTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/Harness.java (99%) rename src/test/groovy/graphql/validation/{rules => }/KnownArgumentNamesTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/KnownDirectivesTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/KnownFragmentNamesTest.groovy (97%) rename src/test/groovy/graphql/validation/{rules => }/KnownTypeNamesTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/LoneAnonymousOperationTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/NoFragmentCyclesTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/NoUndefinedVariablesTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/NoUnusedFragmentsTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/NoUnusedVariablesTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/OverlappingFieldsCanBeMergedTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/PossibleFragmentSpreadsTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/ProvidedNonNullArgumentsTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/ScalarLeavesTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/SubscriptionUniqueRootFieldTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/UniqueArgumentNamesTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/UniqueDirectiveNamesPerLocationTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/UniqueFragmentNamesTest.groovy (97%) rename src/test/groovy/graphql/validation/{rules => }/UniqueObjectFieldNameTest.groovy (96%) rename src/test/groovy/graphql/validation/{rules => }/UniqueOperationNamesTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/UniqueVariableNamesTest.groovy (97%) rename src/test/groovy/graphql/validation/{rules => }/VariableDefaultValuesOfCorrectTypeTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/VariableTypesMatchTest.groovy (99%) rename src/test/groovy/graphql/validation/{rules => }/VariablesAreInputTypesTest.groovy (98%) rename src/test/groovy/graphql/validation/{rules => }/VariablesTypesMatcherTest.groovy (98%) 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/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index 6529fab837..28abac463b 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -56,7 +56,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.InputValueWithState; -import graphql.validation.rules.VariablesTypesMatcher; import graphql.util.StringKit; import java.util.ArrayList; 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/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy index 95dc0cc435..86cb61c957 100644 --- a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -325,10 +325,7 @@ class JSpecifyAnnotationsCheck extends Specification { "graphql.util.TreeTransformerUtil", "graphql.validation.ValidationError", "graphql.validation.ValidationErrorClassification", - "graphql.validation.ValidationErrorType", - "graphql.validation.rules.DeferDirectiveLabel", - "graphql.validation.rules.DeferDirectiveOnRootLevel", - "graphql.validation.rules.DeferDirectiveOnValidOperation" + "graphql.validation.ValidationErrorType" ] as Set def "ensure all public API and experimental API classes have @NullMarked annotation"() { diff --git a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy b/src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy rename to src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.groovy index 132ddd537e..a48fae4fe0 100644 --- a/src/test/groovy/graphql/validation/rules/ArgumentsOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/ArgumentsOfCorrectTypeTest.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/DeferDirectiveLabelTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy index 38076e62e8..d1eac2e903 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveLabelTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveLabelTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy index dbc407255e..10fc35007c 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnRootLevelTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveOnRootLevelTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy b/src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy rename to src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy index 2d78539e7d..1e160ddc16 100644 --- a/src/test/groovy/graphql/validation/rules/DeferDirectiveOnValidOperationTest.groovy +++ b/src/test/groovy/graphql/validation/DeferDirectiveOnValidOperationTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExperimentalApi import graphql.i18n.I18n 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 99% rename from src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy rename to src/test/groovy/graphql/validation/FieldsOnCorrectTypeTest.groovy index 857f9edc18..1919679c9f 100644 --- a/src/test/groovy/graphql/validation/rules/FieldsOnCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/FieldsOnCorrectTypeTest.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/FragmentsOnCompositeTypeTest.groovy b/src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy rename to src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy index 09d80c4302..d614c6d419 100644 --- a/src/test/groovy/graphql/validation/rules/FragmentsOnCompositeTypeTest.groovy +++ b/src/test/groovy/graphql/validation/FragmentsOnCompositeTypeTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.ExecutionInput import graphql.GraphQL 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/rules/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/KnownArgumentNamesTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy rename to src/test/groovy/graphql/validation/KnownArgumentNamesTest.groovy index 020f8356fd..1fececc10a 100644 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownArgumentNamesTest.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/KnownDirectivesTest.groovy b/src/test/groovy/graphql/validation/KnownDirectivesTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy rename to src/test/groovy/graphql/validation/KnownDirectivesTest.groovy index fe3a083a13..4c37fce407 100644 --- a/src/test/groovy/graphql/validation/rules/KnownDirectivesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownDirectivesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.StarWarsSchema import graphql.TestUtil diff --git a/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy b/src/test/groovy/graphql/validation/KnownFragmentNamesTest.groovy similarity index 97% rename from src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy rename to src/test/groovy/graphql/validation/KnownFragmentNamesTest.groovy index bc04e0aafe..026305afee 100644 --- a/src/test/groovy/graphql/validation/rules/KnownFragmentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownFragmentNamesTest.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/KnownTypeNamesTest.groovy b/src/test/groovy/graphql/validation/KnownTypeNamesTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy rename to src/test/groovy/graphql/validation/KnownTypeNamesTest.groovy index e0356757d5..92b40e2b95 100644 --- a/src/test/groovy/graphql/validation/rules/KnownTypeNamesTest.groovy +++ b/src/test/groovy/graphql/validation/KnownTypeNamesTest.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/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 99% rename from src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy rename to src/test/groovy/graphql/validation/NoFragmentCyclesTest.groovy index 737b2dd1bf..b54ad740bc 100644 --- a/src/test/groovy/graphql/validation/rules/NoFragmentCyclesTest.groovy +++ b/src/test/groovy/graphql/validation/NoFragmentCyclesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy b/src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy rename to src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy index 35e7f70e3d..62ddeb7dd2 100644 --- a/src/test/groovy/graphql/validation/rules/NoUndefinedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/NoUndefinedVariablesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy b/src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy rename to src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy index 352418ded5..157bd38c10 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedFragmentsTest.groovy +++ b/src/test/groovy/graphql/validation/NoUnusedFragmentsTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy b/src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy rename to src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy index a1cd22c5f6..4717f3810f 100644 --- a/src/test/groovy/graphql/validation/rules/NoUnusedVariablesTest.groovy +++ b/src/test/groovy/graphql/validation/NoUnusedVariablesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.i18n.I18n diff --git a/src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy b/src/test/groovy/graphql/validation/OverlappingFieldsCanBeMergedTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/OverlappingFieldsCanBeMergedTest.groovy rename to src/test/groovy/graphql/validation/OverlappingFieldsCanBeMergedTest.groovy index 80c71591ba..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 diff --git a/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy b/src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy rename to src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy index 2f49c941bd..4571a14c4b 100644 --- a/src/test/groovy/graphql/validation/rules/PossibleFragmentSpreadsTest.groovy +++ b/src/test/groovy/graphql/validation/PossibleFragmentSpreadsTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.i18n.I18n import graphql.language.Document diff --git a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy b/src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy rename to src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy index f21f45b50d..2cb36463ad 100644 --- a/src/test/groovy/graphql/validation/rules/ProvidedNonNullArgumentsTest.groovy +++ b/src/test/groovy/graphql/validation/ProvidedNonNullArgumentsTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.parser.Parser import graphql.schema.GraphQLArgument diff --git a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy b/src/test/groovy/graphql/validation/ScalarLeavesTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy rename to src/test/groovy/graphql/validation/ScalarLeavesTest.groovy index d7f6c6c2d0..c544f81b50 100644 --- a/src/test/groovy/graphql/validation/rules/ScalarLeavesTest.groovy +++ b/src/test/groovy/graphql/validation/ScalarLeavesTest.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/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 98% rename from src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy rename to src/test/groovy/graphql/validation/VariableDefaultValuesOfCorrectTypeTest.groovy index 49ec51887f..943e6e143e 100644 --- a/src/test/groovy/graphql/validation/rules/VariableDefaultValuesOfCorrectTypeTest.groovy +++ b/src/test/groovy/graphql/validation/VariableDefaultValuesOfCorrectTypeTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.validation.ValidationErrorType diff --git a/src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy b/src/test/groovy/graphql/validation/VariableTypesMatchTest.groovy similarity index 99% rename from src/test/groovy/graphql/validation/rules/VariableTypesMatchTest.groovy rename to src/test/groovy/graphql/validation/VariableTypesMatchTest.groovy index 6816f59abd..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 diff --git a/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy b/src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy similarity index 98% rename from src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy rename to src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy index 70896af279..8d77e3d58a 100644 --- a/src/test/groovy/graphql/validation/rules/VariablesAreInputTypesTest.groovy +++ b/src/test/groovy/graphql/validation/VariablesAreInputTypesTest.groovy @@ -1,4 +1,4 @@ -package graphql.validation.rules +package graphql.validation import graphql.TestUtil import graphql.validation.ValidationErrorType 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 From 142bad7f288056e700af965f471173e4d4ff59a6 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 29 Jan 2026 14:49:35 +1000 Subject: [PATCH 4/9] Improve OperationValidator documentation and naming - Rename methods for clarity: - shouldRunNonFragmentSpreadChecks() -> shouldRunDocumentLevelRules() - shouldRunFragmentSpreadChecks() -> shouldRunOperationScopedRules() - fragmentSpreadVisitDepth -> fragmentRetraversalDepth - Add comprehensive class Javadoc explaining: - Two independent state variables and their three valid combinations - Visual matrices showing when each rule category runs - Step-by-step traversal example with state at each step - Add detailed Javadoc to OperationValidationRule enum categorizing all rules by their traversal behavior - Simplify code by removing duplicate checks and redundant comments Co-authored-by: Cursor --- .../validation/OperationValidationRule.java | 150 ++++++++++- .../validation/OperationValidator.java | 239 +++++++++++++----- 2 files changed, 320 insertions(+), 69 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidationRule.java b/src/main/java/graphql/validation/OperationValidationRule.java index 49adf55bf7..d5645aa5af 100644 --- a/src/main/java/graphql/validation/OperationValidationRule.java +++ b/src/main/java/graphql/validation/OperationValidationRule.java @@ -9,39 +9,179 @@ * *

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, - NO_UNDEFINED_VARIABLES, + + /** All defined fragments must be used by at least one operation. */ NO_UNUSED_FRAGMENTS, - NO_UNUSED_VARIABLES, + + /** 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, - VARIABLE_TYPES_MATCH, + + /** 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_ON_ROOT_LEVEL, - DEFER_DIRECTIVE_ON_VALID_OPERATION, + + /** 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 index 28abac463b..ba947ea75f 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -132,6 +132,134 @@ * 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 @SuppressWarnings("rawtypes") @@ -143,9 +271,12 @@ public class OperationValidator implements DocumentVisitor { private final ValidationUtil validationUtil; private final Predicate rulePredicate; - // --- Traversal context (from RulesVisitor) --- + // --- Traversal context --- + /** True when currently processing within an operation definition. */ private boolean operationScope = false; - private int fragmentSpreadVisitDepth = 0; + /** 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 --- @@ -190,7 +321,7 @@ public class OperationValidator implements DocumentVisitor { private final FieldCollector fieldCollector = new FieldCollector(); // --- Track whether we're in a context where fragment spread rules should run --- - // fragmentSpreadVisitDepth == 0 means we're NOT inside a manually-traversed fragment => run non-fragment-spread checks + // 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 public OperationValidator(ValidationContext validationContext, ValidationErrorCollector errorCollector, Predicate rulePredicate) { @@ -206,24 +337,37 @@ private boolean isRuleEnabled(OperationValidationRule rule) { } /** - * True when we are NOT inside a manually-traversed fragment definition. - * Non-fragment-spread rules run in this context. + * 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 shouldRunNonFragmentSpreadChecks() { - return fragmentSpreadVisitDepth == 0; + private boolean shouldRunDocumentLevelRules() { + return fragmentRetraversalDepth == 0; } /** - * True when we are inside an operation scope (including manual fragment traversal). - * Fragment-spread-visiting rules (NoUndefinedVariables, NoUnusedVariables, VariableTypesMatch, - * DeferDirectiveOnRootLevel, DeferDirectiveOnValidOperation) run in this context. + * 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 shouldRunFragmentSpreadChecks() { + private boolean shouldRunOperationScopedRules() { return operationScope; } - // ==================== DocumentVisitor ==================== - @Override public void enter(Node node, List ancestors) { validationContext.getTraversalContext().enter(node, ancestors); @@ -272,8 +416,6 @@ public void leave(Node node, List ancestors) { } } - // ==================== Error Reporting (from AbstractRule) ==================== - private void addError(ValidationErrorType validationErrorType, Collection> locations, String description) { List locationList = new ArrayList<>(); for (Node node : locations) { @@ -328,40 +470,26 @@ private Boolean isExperimentalApiKeyEnabled(String key) { ((Boolean) validationContext.getGraphQLContext().get(key))); } - // ==================== Dispatch Methods ==================== - private void checkDocument(Document document) { // ExecutableDefinitions - if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { + if (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { validateExecutableDefinitions(document); } - // UniqueDirectiveNamesPerLocation - no-op on document in original } private void checkArgument(Argument argument) { - if (shouldRunNonFragmentSpreadChecks()) { - // ArgumentsOfCorrectType + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.ARGUMENTS_OF_CORRECT_TYPE)) { validateArgumentsOfCorrectType(argument); } - // KnownArgumentNames if (isRuleEnabled(OperationValidationRule.KNOWN_ARGUMENT_NAMES)) { validateKnownArgumentNames(argument); } } - // Fragment spread visiting rules that check arguments - none currently - if (shouldRunFragmentSpreadChecks()) { - // ArgumentsOfCorrectType also needs to run during fragment spread traversal - if (isRuleEnabled(OperationValidationRule.ARGUMENTS_OF_CORRECT_TYPE)) { - // Only run if we're in the fragment spread depth (these are the fragment-spread-visiting rules) - // Actually, ArgumentsOfCorrectType is NOT a fragment spread visiting rule, so only run it - // when shouldRunNonFragmentSpreadChecks(). Already handled above. - } - } } private void checkTypeName(TypeName typeName) { - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.KNOWN_TYPE_NAMES)) { validateKnownTypeNames(typeName); } @@ -369,8 +497,7 @@ private void checkTypeName(TypeName typeName) { } private void checkVariableDefinition(VariableDefinition variableDefinition) { - // These are all non-fragment-spread rules - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE)) { validateVariableDefaultValuesOfCorrectType(variableDefinition); } @@ -378,15 +505,12 @@ private void checkVariableDefinition(VariableDefinition variableDefinition) { validateVariablesAreInputTypes(variableDefinition); } } - // NoUndefinedVariables: track defined variables (fragment-spread-visiting) if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { definedVariableNames.add(variableDefinition.getName()); } - // NoUnusedVariables: track definitions (fragment-spread-visiting) - if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + if (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { unusedVars_variableDefinitions.add(variableDefinition); } - // VariableTypesMatch: track definitions (fragment-spread-visiting) if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { if (variableDefinitionMap != null) { variableDefinitionMap.put(variableDefinition.getName(), variableDefinition); @@ -395,7 +519,7 @@ private void checkVariableDefinition(VariableDefinition variableDefinition) { } private void checkField(Field field) { - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.FIELDS_ON_CORRECT_TYPE)) { validateFieldsOnCorrectType(field); } @@ -415,7 +539,7 @@ private void checkField(Field field) { } private void checkInlineFragment(InlineFragment inlineFragment) { - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.FRAGMENTS_ON_COMPOSITE_TYPE)) { validateFragmentsOnCompositeType_inline(inlineFragment); } @@ -429,8 +553,7 @@ private void checkInlineFragment(InlineFragment inlineFragment) { } private void checkDirective(Directive directive, List ancestors) { - // Non-fragment-spread rules - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.KNOWN_DIRECTIVES)) { validateKnownDirectives(directive, ancestors); } @@ -444,8 +567,7 @@ private void checkDirective(Directive directive, List ancestors) { validateDeferDirectiveLabel(directive); } } - // Fragment-spread-visiting rules for directives - if (shouldRunFragmentSpreadChecks()) { + if (shouldRunOperationScopedRules()) { if (isRuleEnabled(OperationValidationRule.DEFER_DIRECTIVE_ON_ROOT_LEVEL)) { validateDeferDirectiveOnRootLevel(directive); } @@ -456,8 +578,7 @@ private void checkDirective(Directive directive, List ancestors) { } private void checkFragmentSpread(FragmentSpread node, List ancestors) { - // Non-fragment-spread checks on the spread itself - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.KNOWN_FRAGMENT_NAMES)) { validateKnownFragmentNames(node); } @@ -477,15 +598,15 @@ private void checkFragmentSpread(FragmentSpread node, List ancestors) { FragmentDefinition fragment = validationContext.getFragment(node.getName()); if (fragment != null && !visitedFragmentSpreads.contains(node.getName())) { visitedFragmentSpreads.add(node.getName()); - fragmentSpreadVisitDepth++; + fragmentRetraversalDepth++; new LanguageTraversal(ancestors).traverse(fragment, this); - fragmentSpreadVisitDepth--; + fragmentRetraversalDepth--; } } } private void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.FRAGMENTS_ON_COMPOSITE_TYPE)) { validateFragmentsOnCompositeType_definition(fragmentDefinition); } @@ -509,7 +630,7 @@ private void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { private void checkOperationDefinition(OperationDefinition operationDefinition) { operationScope = true; - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED)) { validateOverlappingFieldsCanBeMerged(operationDefinition); } @@ -537,7 +658,7 @@ private void checkOperationDefinition(OperationDefinition operationDefinition) { } } - // Fragment-spread-visiting rules: reset per operation + // Reset operation-scoped rule state if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { definedVariableNames.clear(); } @@ -551,17 +672,13 @@ private void checkOperationDefinition(OperationDefinition operationDefinition) { } private void checkVariable(VariableReference variableReference) { - // Fragment-spread-visiting rules - if (shouldRunFragmentSpreadChecks()) { + if (shouldRunOperationScopedRules()) { if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { validateNoUndefinedVariables(variableReference); } if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { validateVariableTypesMatch(variableReference); } - } - // NoUnusedVariables also visits fragment spreads - if (shouldRunFragmentSpreadChecks()) { if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { unusedVars_usedVariables.add(variableReference.getName()); } @@ -573,21 +690,18 @@ private void checkSelectionSet(SelectionSet selectionSet) { } private void checkObjectValue(ObjectValue objectValue) { - if (shouldRunNonFragmentSpreadChecks()) { + if (shouldRunDocumentLevelRules()) { if (isRuleEnabled(OperationValidationRule.UNIQUE_OBJECT_FIELD_NAME)) { validateUniqueObjectFieldName(objectValue); } } } - // ==================== Leave Methods ==================== - private void leaveOperationDefinition(OperationDefinition operationDefinition) { // fragments should be revisited for each operation visitedFragmentSpreads.clear(); operationScope = false; - // NoUnusedVariables: check on leave if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { for (VariableDefinition variableDefinition : unusedVars_variableDefinitions) { if (!unusedVars_usedVariables.contains(variableDefinition.getName())) { @@ -608,8 +722,7 @@ private void leaveFragmentDefinition() { } private void documentFinished(Document document) { - // NoUnusedFragments - if (shouldRunNonFragmentSpreadChecks() && isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + if (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { validateNoUnusedFragments(); } // LoneAnonymousOperation cleanup @@ -618,8 +731,6 @@ private void documentFinished(Document document) { } } - // ==================== Validation Rule Implementations ==================== - // --- ExecutableDefinitions --- private void validateExecutableDefinitions(Document document) { document.getDefinitions().forEach(definition -> { From 72b28280b6a7ffae648c3657c4bec757f8a8d20e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 29 Jan 2026 19:34:06 +1000 Subject: [PATCH 5/9] Remove redundant shouldRunDocumentLevelRules() checks These checks are always true in methods that are only called at document level (depth=0): - checkDocument(): Document node visited once at start - checkOperationDefinition(): Operations are never inside fragments - checkVariableDefinition(): Variable definitions only exist in operations - documentFinished(): Called when leaving Document Co-authored-by: Cursor --- .../validation/OperationValidator.java | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index ba947ea75f..d8cb9f417c 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -471,8 +471,7 @@ private Boolean isExperimentalApiKeyEnabled(String key) { } private void checkDocument(Document document) { - // ExecutableDefinitions - if (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { + if (isRuleEnabled(OperationValidationRule.EXECUTABLE_DEFINITIONS)) { validateExecutableDefinitions(document); } } @@ -497,18 +496,16 @@ private void checkTypeName(TypeName typeName) { } private void checkVariableDefinition(VariableDefinition variableDefinition) { - if (shouldRunDocumentLevelRules()) { - if (isRuleEnabled(OperationValidationRule.VARIABLE_DEFAULT_VALUES_OF_CORRECT_TYPE)) { - validateVariableDefaultValuesOfCorrectType(variableDefinition); - } - if (isRuleEnabled(OperationValidationRule.VARIABLES_ARE_INPUT_TYPES)) { - validateVariablesAreInputTypes(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 (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_VARIABLES)) { unusedVars_variableDefinitions.add(variableDefinition); } if (isRuleEnabled(OperationValidationRule.VARIABLE_TYPES_MATCH)) { @@ -630,35 +627,31 @@ private void checkFragmentDefinition(FragmentDefinition fragmentDefinition) { private void checkOperationDefinition(OperationDefinition operationDefinition) { operationScope = true; - if (shouldRunDocumentLevelRules()) { - 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.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); } - - // Reset operation-scoped rule state if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) { definedVariableNames.clear(); } @@ -722,10 +715,9 @@ private void leaveFragmentDefinition() { } private void documentFinished(Document document) { - if (shouldRunDocumentLevelRules() && isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { + if (isRuleEnabled(OperationValidationRule.NO_UNUSED_FRAGMENTS)) { validateNoUnusedFragments(); } - // LoneAnonymousOperation cleanup if (isRuleEnabled(OperationValidationRule.LONE_ANONYMOUS_OPERATION)) { hasAnonymousOp = false; } From e0c5f0bc2a9cee2efef0fcaef59f0882fbc6fe72 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 29 Jan 2026 19:55:16 +1000 Subject: [PATCH 6/9] Add JSpecify @NullMarked annotations to ValidationContext and OperationValidator ValidationContext: - Add @NullMarked at class level - Add @Nullable to methods that can return null based on traversal state: getFragment, getParentType, getInputType, getDefaultValue, getFieldDef, getDirective, getArgument, getOutputType, getQueryPath OperationValidator: - Add @NullMarked at class level - Add @Nullable to methods that can return null: getQueryPath, requireSameNameAndArguments, findArgumentByName, requireSameOutputTypeShape - Add @Nullable to parameters that can be null: addError (SourceLocation), mkNotSameTypeError (typeA, typeB), overlappingFieldsImpl, overlappingFields_collectFields, overlappingFields_collectFieldsForInlineFragment, overlappingFields_collectFieldsForField, sameType, sameArguments - Add @Nullable to fields that can be null: variableDefinitionMap, FieldAndType.graphQLType Co-authored-by: Cursor --- .../validation/OperationValidator.java | 35 ++++++++++--------- .../graphql/validation/ValidationContext.java | 22 ++++++------ 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index d8cb9f417c..9ce1040f7a 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -57,6 +57,8 @@ 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; @@ -262,6 +264,7 @@ * @see OperationValidationRule */ @Internal +@NullMarked @SuppressWarnings("rawtypes") public class OperationValidator implements DocumentVisitor { @@ -297,7 +300,7 @@ public class OperationValidator implements DocumentVisitor { // --- State: VariableTypesMatch --- private final VariablesTypesMatcher variablesTypesMatcher = new VariablesTypesMatcher(); - private Map variableDefinitionMap; + private @Nullable Map variableDefinitionMap; // --- State: OverlappingFieldsCanBeMerged --- private final Set> sameResponseShapeChecked = new LinkedHashSet<>(); @@ -427,7 +430,7 @@ private void addError(ValidationErrorType validationErrorType, Collection getQueryPath() { + private @Nullable List getQueryPath() { return validationContext.getQueryPath(); } @@ -985,7 +988,7 @@ private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationD overlappingFieldsImpl(operationDefinition.getSelectionSet(), validationContext.getOutputType()); } - private void overlappingFieldsImpl(SelectionSet selectionSet, GraphQLOutputType graphQLOutputType) { + private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLOutputType graphQLOutputType) { Map> fieldMap = new LinkedHashMap<>(); Set visitedFragments = new LinkedHashSet<>(); overlappingFields_collectFields(fieldMap, selectionSet, graphQLOutputType, visitedFragments); @@ -999,7 +1002,7 @@ private void overlappingFieldsImpl(SelectionSet selectionSet, GraphQLOutputType } } - private void overlappingFields_collectFields(Map> fieldMap, SelectionSet selectionSet, GraphQLType parentType, Set visitedFragments) { + 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); @@ -1020,7 +1023,7 @@ private void overlappingFields_collectFieldsForFragmentSpread(Map> fieldMap, Set visitedFragments, GraphQLType parentType, InlineFragment inlineFragment) { + private void overlappingFields_collectFieldsForInlineFragment(Map> fieldMap, Set visitedFragments, @Nullable GraphQLType parentType, InlineFragment inlineFragment) { GraphQLType graphQLType; if (inlineFragment.getTypeCondition() == null) { graphQLType = parentType; @@ -1030,7 +1033,7 @@ private void overlappingFields_collectFieldsForInlineFragment(Map> fieldMap, GraphQLType parentType, Field field) { + private void overlappingFields_collectFieldsForField(Map> fieldMap, @Nullable GraphQLType parentType, Field field) { String responseName = field.getResultKey(); if (!fieldMap.containsKey(responseName)) { fieldMap.put(responseName, new LinkedHashSet<>()); @@ -1120,7 +1123,7 @@ private boolean isInterfaceOrUnion(GraphQLType type) { return type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType; } - private Conflict requireSameNameAndArguments(ImmutableList path, Set fieldAndTypes) { + private @Nullable Conflict requireSameNameAndArguments(ImmutableList path, Set fieldAndTypes) { if (fieldAndTypes.size() <= 1) { return null; } @@ -1151,8 +1154,8 @@ private String pathToString(ImmutableList path) { return String.join("/", path); } - private boolean sameArguments(List arguments1, List arguments2) { - if (arguments1.size() != arguments2.size()) { + private boolean sameArguments(List arguments1, @Nullable List arguments2) { + if (arguments2 == null || arguments1.size() != arguments2.size()) { return false; } for (Argument argument : arguments1) { @@ -1167,7 +1170,7 @@ private boolean sameArguments(List arguments1, List argument return true; } - private Argument findArgumentByName(String name, List arguments) { + private @Nullable Argument findArgumentByName(String name, List arguments) { for (Argument argument : arguments) { if (argument.getName().equals(name)) { return argument; @@ -1176,7 +1179,7 @@ private Argument findArgumentByName(String name, List arguments) { return null; } - private Conflict requireSameOutputTypeShape(ImmutableList path, Set fieldAndTypes) { + private @Nullable Conflict requireSameOutputTypeShape(ImmutableList path, Set fieldAndTypes) { if (fieldAndTypes.size() <= 1) { return null; } @@ -1223,14 +1226,14 @@ private Conflict requireSameOutputTypeShape(ImmutableList path, Set path, List fields, GraphQLType typeA, GraphQLType typeB) { + 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(GraphQLType type1, GraphQLType type2) { + private boolean sameType(@Nullable GraphQLType type1, @Nullable GraphQLType type2) { if (type1 == null || type2 == null) { return true; } @@ -1239,10 +1242,10 @@ private boolean sameType(GraphQLType type1, GraphQLType type2) { private static class FieldAndType { final Field field; - final GraphQLType graphQLType; + final @Nullable GraphQLType graphQLType; final GraphQLType parentType; - public FieldAndType(Field field, GraphQLType graphQLType, GraphQLType parentType) { + public FieldAndType(Field field, @Nullable GraphQLType graphQLType, GraphQLType parentType) { this.field = field; this.graphQLType = graphQLType; this.parentType = parentType; 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(); } From b65e8c3e83a0a87e5517e109b360bef60c7e0f92 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 29 Jan 2026 20:03:02 +1000 Subject: [PATCH 7/9] Add JSpecify @NullMarked annotations to TraversalContext - Add @NullMarked at class level - Add @Nullable to fields: directive, argument - Add @Nullable to public methods that can return null: getOutputType, getParentType, getInputType, getDefaultValue, getFieldDef, getQueryPath, getDirective, getArgument - Add @Nullable to private methods that can return null: lastElement, find, getFieldDef(schema, parentType, field), getNullableType - Add @Nullable to parameters that accept null: addOutputType, addParentType, addInputType, addDefaultValue, addFieldDef, getNullableType Co-authored-by: Cursor --- .../graphql/validation/TraversalContext.java | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/main/java/graphql/validation/TraversalContext.java b/src/main/java/graphql/validation/TraversalContext.java index d9d93af7f1..9a70f72465 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) { @@ -187,7 +189,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 +247,7 @@ private boolean isEmpty(String name) { return name == null || name.isEmpty(); } - private GraphQLNullableType getNullableType(GraphQLType type) { + private @Nullable GraphQLNullableType getNullableType(@Nullable GraphQLType type) { return (GraphQLNullableType) (isNonNull(type) ? unwrapOne(type) : type); } @@ -253,15 +255,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 +277,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(); From 7a4e1fb15049bc4a1c11bed304691d35edfea7b4 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 15 Feb 2026 20:20:58 +1000 Subject: [PATCH 8/9] Optimize validation hot paths to reduce collection overhead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace HashMap/LinkedHashMap allocations with linear scans for small argument lists, single-pass partitioning in groupByCommonParents, pre-sized maps, computeIfAbsent, HashSet for fragment tracking, and ArrayList over LinkedList. Short-circuit isRuleEnabled when all rules are enabled. ~12% improvement on ValidatorBenchmark (largeSchema4: 7.86→6.94ms, manyFragments: 5.83→5.16ms). Co-Authored-By: Claude Opus 4.6 --- .../validation/OperationValidator.java | 108 +++++++++++------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index 0381bba0f6..872ba2044b 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -69,7 +69,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.LinkedList; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -90,8 +90,7 @@ 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.ValidationError.newValidationError; import static graphql.validation.ValidationErrorType.BadValueForDefaultArg; import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; @@ -328,16 +327,28 @@ public class OperationValidator implements DocumentVisitor { // 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 rulePredicate.test(rule); + return allRulesEnabled || rulePredicate.test(rule); } /** @@ -909,8 +920,8 @@ public void leave(Node node, List path) { } private void validateNoFragmentCycles(FragmentDefinition fragmentDefinition) { - LinkedList path = new LinkedList<>(); - path.add(0, fragmentDefinition.getName()); + ArrayList path = new ArrayList<>(); + path.add(fragmentDefinition.getName()); Map> transitiveSpreads = buildTransitiveSpreads(path, new HashMap<>()); for (Map.Entry> entry : transitiveSpreads.entrySet()) { @@ -921,8 +932,8 @@ private void validateNoFragmentCycles(FragmentDefinition fragmentDefinition) { } } - private Map> buildTransitiveSpreads(LinkedList path, Map> transitiveSpreads) { - String name = path.peekFirst(); + private Map> buildTransitiveSpreads(ArrayList path, Map> transitiveSpreads) { + String name = path.get(path.size() - 1); if (transitiveSpreads.containsKey(name)) { return transitiveSpreads; } @@ -942,8 +953,8 @@ private Map> buildTransitiveSpreads(LinkedList path, if (path.contains(child) || transitiveSpreads.containsKey(child)) { continue; } - LinkedList childPath = new LinkedList<>(path); - childPath.add(0, child); + ArrayList childPath = new ArrayList<>(path); + childPath.add(child); buildTransitiveSpreads(childPath, transitiveSpreads); } return transitiveSpreads; @@ -959,7 +970,7 @@ private void validateNoUndefinedVariables(VariableReference variableReference) { // --- NoUnusedFragments --- private void validateNoUnusedFragments() { - List allUsedFragments = new ArrayList<>(); + Set allUsedFragments = new HashSet<>(); for (List fragmentsInOneOperation : fragmentsUsedDirectlyInOperation) { for (String fragment : fragmentsInOneOperation) { collectUsedFragmentsInDefinition(allUsedFragments, fragment); @@ -973,9 +984,8 @@ private void validateNoUnusedFragments() { } } - private void collectUsedFragmentsInDefinition(List result, String fragmentName) { - if (result.contains(fragmentName)) return; - result.add(fragmentName); + private void collectUsedFragmentsInDefinition(Set result, String fragmentName) { + if (!result.add(fragmentName)) return; List spreadList = spreadsInDefinition.get(fragmentName); if (spreadList == null) { return; @@ -991,7 +1001,7 @@ private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationD } private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLOutputType graphQLOutputType) { - Map> fieldMap = new LinkedHashMap<>(); + Map> fieldMap = new LinkedHashMap<>(selectionSet.getSelections().size()); Set visitedFragments = new LinkedHashSet<>(); overlappingFields_collectFields(fieldMap, selectionSet, graphQLOutputType, visitedFragments); List conflicts = findConflicts(fieldMap); @@ -1037,9 +1047,6 @@ private void overlappingFields_collectFieldsForInlineFragment(Map> fieldMap, @Nullable GraphQLType parentType, Field field) { String responseName = field.getResultKey(); - if (!fieldMap.containsKey(responseName)) { - fieldMap.put(responseName, new LinkedHashSet<>()); - } GraphQLOutputType fieldType = null; GraphQLUnmodifiedType unwrappedParent = parentType != null ? unwrapAll(parentType) : null; if (unwrappedParent instanceof GraphQLFieldsContainer) { @@ -1050,7 +1057,7 @@ private void overlappingFields_collectFieldsForField(Map new LinkedHashSet<>()).add(new FieldAndType(field, fieldType, unwrappedParent)); } private List findConflicts(Map> fieldMap) { @@ -1109,17 +1116,38 @@ private void sameForCommonParentsByName(Map> fieldMap, } 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); + // 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; } @@ -1349,10 +1377,10 @@ private boolean isValidTargetCompositeType(GraphQLType type) { private void validateProvidedNonNullArguments_field(Field field) { GraphQLFieldDefinition fieldDef = validationContext.getFieldDef(); if (fieldDef == null) return; - Map argumentMap = argumentMap(field.getArguments()); + List providedArguments = field.getArguments(); for (GraphQLArgument graphQLArgument : fieldDef.getArguments()) { - Argument argument = argumentMap.get(graphQLArgument.getName()); + Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); boolean nonNullType = isNonNull(graphQLArgument.getType()); boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); if (argument == null && nonNullType && noDefaultValue) { @@ -1372,10 +1400,10 @@ private void validateProvidedNonNullArguments_field(Field field) { private void validateProvidedNonNullArguments_directive(Directive directive) { GraphQLDirective graphQLDirective = validationContext.getDirective(); if (graphQLDirective == null) return; - Map argumentMap = argumentMap(directive.getArguments()); + List providedArguments = directive.getArguments(); for (GraphQLArgument graphQLArgument : graphQLDirective.getArguments()) { - Argument argument = argumentMap.get(graphQLArgument.getName()); + Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); boolean nonNullType = isNonNull(graphQLArgument.getType()); boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); if (argument == null && nonNullType && noDefaultValue) { @@ -1385,12 +1413,14 @@ private void validateProvidedNonNullArguments_directive(Directive directive) { } } - private Map argumentMap(List arguments) { - Map result = new LinkedHashMap<>(); - for (Argument argument : arguments) { - result.put(argument.getName(), argument); + 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 result; + return null; } // --- ScalarLeaves --- From 051f73ddc446654389fbbbf94a259085b3db5ef0 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 15 Feb 2026 22:57:37 +1000 Subject: [PATCH 9/9] Remove unnecessary null checks on getCodeRegistry() GraphQLSchema.getCodeRegistry() is no longer @Nullable, so these guards are unnecessary. Co-Authored-By: Claude Opus 4.6 --- src/main/java/graphql/validation/OperationValidator.java | 7 ++----- src/main/java/graphql/validation/TraversalContext.java | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index 872ba2044b..3e32d5d67c 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -1051,11 +1051,8 @@ private void overlappingFields_collectFieldsForField(Map new LinkedHashSet<>()).add(new FieldAndType(field, fieldType, unwrappedParent)); } diff --git a/src/main/java/graphql/validation/TraversalContext.java b/src/main/java/graphql/validation/TraversalContext.java index bfc3e7dcc9..a8ce012f1d 100644 --- a/src/main/java/graphql/validation/TraversalContext.java +++ b/src/main/java/graphql/validation/TraversalContext.java @@ -180,7 +180,7 @@ private void enterImpl(ObjectField objectField) { GraphQLUnmodifiedType objectType = currentInputType != null ? unwrapAll(currentInputType) : null; GraphQLInputType inputType = null; GraphQLInputObjectField inputField = null; - if (objectType instanceof GraphQLInputObjectType && schema.getCodeRegistry() != null) { + if (objectType instanceof GraphQLInputObjectType) { GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) objectType; inputField = schema.getCodeRegistry().getFieldVisibility().getFieldDefinition(inputObjectType, objectField.getName()); if (inputField != null) { @@ -344,7 +344,7 @@ private void addFieldDef(@Nullable GraphQLFieldDefinition fieldDefinition) { parentType instanceof GraphQLUnionType)) { return schema.getIntrospectionTypenameFieldDefinition(); } - if (parentType instanceof GraphQLFieldsContainer && schema.getCodeRegistry() != null) { + if (parentType instanceof GraphQLFieldsContainer) { return schema.getCodeRegistry().getFieldVisibility().getFieldDefinition((GraphQLFieldsContainer) parentType, field.getName()); } return null;