From 17520876d86f3ab2d83156927bf7b65f59bd7063 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder Date: Thu, 15 Feb 2018 17:19:53 +0100 Subject: [PATCH] Update some validators to latest upstream version This includes: graphql/graphql-js#1147 graphql/graphql-js#355 This also fixes two bugs in the Schema - types that were not found where still added to the typeMap - InputObject args should not be searched for types. --- src/Language/AST/FragmentDefinitionNode.php | 2 +- src/Type/Schema.php | 6 +- src/Utils/TypeInfo.php | 7 +- src/Utils/Utils.php | 61 ++++++++++ src/Validator/Rules/FieldsOnCorrectType.php | 122 +++++++++++++++++--- src/Validator/Rules/KnownArgumentNames.php | 78 +++++++------ src/Validator/Rules/KnownTypeNames.php | 36 ++++-- tests/Utils/QuotedOrListTest.php | 65 +++++++++++ tests/Utils/SuggestionListTest.php | 45 ++++++++ tests/Validator/FieldsOnCorrectTypeTest.php | 107 ++++++++++++----- tests/Validator/KnownArgumentNamesTest.php | 48 ++++++-- tests/Validator/KnownTypeNamesTest.php | 12 +- tests/Validator/ValidationTest.php | 2 +- 13 files changed, 485 insertions(+), 106 deletions(-) create mode 100644 tests/Utils/QuotedOrListTest.php create mode 100644 tests/Utils/SuggestionListTest.php diff --git a/src/Language/AST/FragmentDefinitionNode.php b/src/Language/AST/FragmentDefinitionNode.php index 4543b03..14cf662 100644 --- a/src/Language/AST/FragmentDefinitionNode.php +++ b/src/Language/AST/FragmentDefinitionNode.php @@ -13,7 +13,7 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H /** * Note: fragment variable definitions are experimental and may be changed * or removed in the future. - * + * * @var VariableDefinitionNode[]|NodeList */ public $variableDefinitions; diff --git a/src/Type/Schema.php b/src/Type/Schema.php index b68ef12..3e8b16b 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -224,7 +224,11 @@ class Schema public function getType($name) { if (!isset($this->resolvedTypes[$name])) { - $this->resolvedTypes[$name] = $this->loadType($name); + $type = $this->loadType($name); + if (!$type) { + return null; + } + $this->resolvedTypes[$name] = $type; } return $this->resolvedTypes[$name]; } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 34a48cb..843a433 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -122,7 +122,7 @@ class TypeInfo if ($type instanceof ObjectType) { $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); } - if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { + if ($type instanceof ObjectType || $type instanceof InterfaceType) { foreach ((array) $type->getFields() as $fieldName => $field) { if (!empty($field->args)) { $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); @@ -131,6 +131,11 @@ class TypeInfo $nestedTypes[] = $field->getType(); } } + if ($type instanceof InputObjectType) { + foreach ((array) $type->getFields() as $fieldName => $field) { + $nestedTypes[] = $field->getType(); + } + } foreach ($nestedTypes as $type) { $typeMap = self::extractTypes($type, $typeMap); } diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index c000c80..853fbf1 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -471,4 +471,65 @@ class Utils } }; } + + + /** + * @param string[] $items + * @return string + */ + public static function quotedOrList(array $items) + { + $items = array_map(function($item) { return "\"$item\""; }, $items); + return self::orList($items); + } + + public static function orList(array $items) + { + if (!$items) { + throw new \LogicException('items must not need to be empty.'); + } + $selected = array_slice($items, 0, 5); + $selectedLength = count($selected); + $firstSelected = $selected[0]; + + if ($selectedLength === 1) { + return $firstSelected; + } + + return array_reduce( + range(1, $selectedLength - 1), + function ($list, $index) use ($selected, $selectedLength) { + return $list. + ($selectedLength > 2 && $index !== $selectedLength - 1? ', ' : ' ') . + ($index === $selectedLength - 1 ? 'or ' : '') . + $selected[$index]; + }, + $firstSelected + ); + } + + /** + * Given an invalid input string and a list of valid options, returns a filtered + * list of valid options sorted based on their similarity with the input. + * + * @param string $input + * @param array $options + * @return string[] + */ + public static function suggestionList($input, array $options) + { + $optionsByDistance = []; + $inputThreshold = mb_strlen($input) / 2; + foreach ($options as $option) { + $distance = levenshtein($input, $option); + $threshold = max($inputThreshold, mb_strlen($option) / 2, 1); + if ($distance <= $threshold) { + $optionsByDistance[$option] = $distance; + } + } + + asort($optionsByDistance); + + return array_keys($optionsByDistance); + } } diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index 26ee748..7d052ae 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -4,27 +4,27 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error\Error; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; class FieldsOnCorrectType extends AbstractValidationRule { - static function undefinedFieldMessage($field, $type, array $suggestedTypes = []) + static function undefinedFieldMessage($fieldName, $type, array $suggestedTypeNames, array $suggestedFieldNames) { - $message = 'Cannot query field "' . $field . '" on type "' . $type.'".'; + $message = 'Cannot query field "' . $fieldName . '" on type "' . $type.'".'; - $maxLength = 5; - $count = count($suggestedTypes); - if ($count > 0) { - $suggestions = array_slice($suggestedTypes, 0, $maxLength); - $suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; }); - $suggestions = implode(', ', $suggestions); - - if ($count > $maxLength) { - $suggestions .= ', and ' . ($count - $maxLength) . ' other types'; - } - $message .= " However, this field exists on $suggestions."; - $message .= ' Perhaps you meant to use an inline fragment?'; + if ($suggestedTypeNames) { + $suggestions = Utils::quotedOrList($suggestedTypeNames); + $message .= " Did you mean to use an inline fragment on $suggestions?"; + } else if ($suggestedFieldNames) { + $suggestions = Utils::quotedOrList($suggestedFieldNames); + $message .= " Did you mean {$suggestions}?"; } return $message; } @@ -37,8 +37,32 @@ class FieldsOnCorrectType extends AbstractValidationRule if ($type) { $fieldDef = $context->getFieldDef(); if (!$fieldDef) { + // This isn't valid. Let's find suggestions, if any. + $schema = $context->getSchema(); + $fieldName = $node->name->value; + // First determine if there are any suggested types to condition on. + $suggestedTypeNames = $this->getSuggestedTypeNames( + $schema, + $type, + $fieldName + ); + // If there are no suggested types, then perhaps this was a typo? + $suggestedFieldNames = $suggestedTypeNames + ? [] + : $this->getSuggestedFieldNames( + $schema, + $type, + $fieldName + ); + + // Report an error, including helpful suggestions. $context->reportError(new Error( - static::undefinedFieldMessage($node->name->value, $type->name), + static::undefinedFieldMessage( + $node->name->value, + $type->name, + $suggestedTypeNames, + $suggestedFieldNames + ), [$node] )); } @@ -46,4 +70,72 @@ class FieldsOnCorrectType extends AbstractValidationRule } ]; } + + /** + * Go through all of the implementations of type, as well as the interfaces + * that they implement. If any of those types include the provided field, + * suggest them, sorted by how often the type is referenced, starting + * with Interfaces. + * + * @param Schema $schema + * @param $type + * @param string $fieldName + * @return array + */ + private function getSuggestedTypeNames(Schema $schema, $type, $fieldName) + { + if (Type::isAbstractType($type)) { + $suggestedObjectTypes = []; + $interfaceUsageCount = []; + + foreach($schema->getPossibleTypes($type) as $possibleType) { + $fields = $possibleType->getFields(); + if (!isset($fields[$fieldName])) { + continue; + } + // This object type defines this field. + $suggestedObjectTypes[] = $possibleType->name; + foreach($possibleType->getInterfaces() as $possibleInterface) { + $fields = $possibleInterface->getFields(); + if (!isset($fields[$fieldName])) { + continue; + } + // This interface type defines this field. + $interfaceUsageCount[$possibleInterface->name] = + !isset($interfaceUsageCount[$possibleInterface->name]) + ? 0 + : $interfaceUsageCount[$possibleInterface->name] + 1; + } + } + + // Suggest interface types based on how common they are. + arsort($interfaceUsageCount); + $suggestedInterfaceTypes = array_keys($interfaceUsageCount); + + // Suggest both interface and object types. + return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes); + } + + // Otherwise, must be an Object type, which does not have possible fields. + return []; + } + + /** + * For the field name provided, determine if there are any similar field names + * that may be the result of a typo. + * + * @param Schema $schema + * @param $type + * @param string $fieldName + * @return array|string[] + */ + private function getSuggestedFieldNames(Schema $schema, $type, $fieldName) + { + if ($type instanceof ObjectType || $type instanceof InterfaceType) { + $possibleFieldNames = array_keys($type->getFields()); + return Utils::suggestionList($fieldName, $possibleFieldNames); + } + // Otherwise, must be a Union type, which does not define fields. + return []; + } } diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index 78ee3f9..15a77ab 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -7,56 +7,68 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; +/** + * Known argument names + * + * A GraphQL field is only valid if all supplied arguments are defined by + * that field. + */ class KnownArgumentNames extends AbstractValidationRule { - public static function unknownArgMessage($argName, $fieldName, $type) + public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs) { - return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\"."; + $message = "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$typeName\"."; + if ($suggestedArgs) { + $message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?'; + } + return $message; } - public static function unknownDirectiveArgMessage($argName, $directiveName) + public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs) { - return "Unknown argument \"$argName\" on directive \"@$directiveName\"."; + $message = "Unknown argument \"$argName\" on directive \"@$directiveName\"."; + if ($suggestedArgs) { + $message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?'; + } + return $message; } public function getVisitor(ValidationContext $context) { return [ NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { - $argumentOf = $ancestors[count($ancestors) - 1]; - if ($argumentOf->kind === NodeKind::FIELD) { - $fieldDef = $context->getFieldDef(); - - if ($fieldDef) { - $fieldArgDef = null; - foreach ($fieldDef->args as $arg) { - if ($arg->name === $node->name->value) { - $fieldArgDef = $arg; - break; - } - } - if (!$fieldArgDef) { - $parentType = $context->getParentType(); - Utils::invariant($parentType); + $argDef = $context->getArgument(); + if (!$argDef) { + $argumentOf = $ancestors[count($ancestors) - 1]; + if ($argumentOf->kind === NodeKind::FIELD) { + $fieldDef = $context->getFieldDef(); + $parentType = $context->getParentType(); + if ($fieldDef && $parentType) { $context->reportError(new Error( - self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), + self::unknownArgMessage( + $node->name->value, + $fieldDef->name, + $parentType->name, + Utils::suggestionList( + $node->name->value, + array_map(function ($arg) { return $arg->name; }, $fieldDef->args) + ) + ), [$node] )); } - } - } else if ($argumentOf->kind === NodeKind::DIRECTIVE) { - $directive = $context->getDirective(); - if ($directive) { - $directiveArgDef = null; - foreach ($directive->args as $arg) { - if ($arg->name === $node->name->value) { - $directiveArgDef = $arg; - break; - } - } - if (!$directiveArgDef) { + } else if ($argumentOf->kind === NodeKind::DIRECTIVE) { + $directive = $context->getDirective(); + if ($directive) { $context->reportError(new Error( - self::unknownDirectiveArgMessage($node->name->value, $directive->name), + self::unknownDirectiveArgMessage( + $node->name->value, + $directive->name, + Utils::suggestionList( + $node->name->value, + array_map(function ($arg) { return $arg->name; }, $directive->args) + ) + ), [$node] )); } diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 71fa60a..47065c1 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -1,35 +1,55 @@ $skip, NodeKind::INTERFACE_TYPE_DEFINITION => $skip, NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, - - NodeKind::NAMED_TYPE => function(NamedTypeNode $node, $key) use ($context) { + NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) { + $schema = $context->getSchema(); $typeName = $node->name->value; - $type = $context->getSchema()->getType($typeName); + $type = $schema->getType($typeName); if (!$type) { - $context->reportError(new Error(self::unknownTypeMessage($typeName), [$node])); + $context->reportError(new Error( + self::unknownTypeMessage( + $typeName, + Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) + ), [$node]) + ); } } ]; diff --git a/tests/Utils/QuotedOrListTest.php b/tests/Utils/QuotedOrListTest.php new file mode 100644 index 0000000..861388b --- /dev/null +++ b/tests/Utils/QuotedOrListTest.php @@ -0,0 +1,65 @@ +setExpectedException(\LogicException::class); + Utils::quotedOrList([]); + } + + /** + * @it Returns single quoted item + */ + public function testReturnsSingleQuotedItem() + { + $this->assertEquals( + '"A"', + Utils::quotedOrList(['A']) + ); + } + + /** + * @it Returns two item list + */ + public function testReturnsTwoItemList() + { + $this->assertEquals( + '"A" or "B"', + Utils::quotedOrList(['A', 'B']) + ); + } + + /** + * @it Returns comma separated many item list + */ + public function testReturnsCommaSeparatedManyItemList() + { + $this->assertEquals( + '"A", "B" or "C"', + Utils::quotedOrList(['A', 'B', 'C']) + ); + } + + /** + * @it Limits to five items + */ + public function testLimitsToFiveItems() + { + $this->assertEquals( + '"A", "B", "C", "D" or "E"', + Utils::quotedOrList(['A', 'B', 'C', 'D', 'E', 'F']) + ); + } +} diff --git a/tests/Utils/SuggestionListTest.php b/tests/Utils/SuggestionListTest.php new file mode 100644 index 0000000..73797f7 --- /dev/null +++ b/tests/Utils/SuggestionListTest.php @@ -0,0 +1,45 @@ +assertEquals( + Utils::suggestionList('', ['a']), + ['a'] + ); + } + + /** + * @it Returns empty array when there are no options + */ + public function testReturnsEmptyArrayWhenThereAreNoOptions() + { + $this->assertEquals( + Utils::suggestionList('input', []), + [] + ); + } + + /** + * @it Returns options sorted based on similarity + */ + public function testReturnsOptionsSortedBasedOnSimilarity() + { + $this->assertEquals( + Utils::suggestionList('abc', ['a', 'ab', 'abc']), + ['abc', 'ab'] + ); + } +} diff --git a/tests/Validator/FieldsOnCorrectTypeTest.php b/tests/Validator/FieldsOnCorrectTypeTest.php index 7d4b78b..f59bf6c 100644 --- a/tests/Validator/FieldsOnCorrectTypeTest.php +++ b/tests/Validator/FieldsOnCorrectTypeTest.php @@ -97,8 +97,10 @@ class FieldsOnCorrectTypeTest extends TestCase } } }', - [ $this->undefinedField('unknown_pet_field', 'Pet', [], 3, 9), - $this->undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ] + [ + $this->undefinedField('unknown_pet_field', 'Pet', [], [], 3, 9), + $this->undefinedField('unknown_cat_field', 'Cat', [], [], 5, 13) + ] ); } @@ -111,7 +113,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment fieldNotDefined on Dog { meowVolume }', - [$this->undefinedField('meowVolume', 'Dog', [], 3, 9)] + [$this->undefinedField('meowVolume', 'Dog', [], ['barkVolume'], 3, 9)] ); } @@ -126,7 +128,7 @@ class FieldsOnCorrectTypeTest extends TestCase deeper_unknown_field } }', - [$this->undefinedField('unknown_field', 'Dog', [], 3, 9)] + [$this->undefinedField('unknown_field', 'Dog', [], [], 3, 9)] ); } @@ -141,7 +143,7 @@ class FieldsOnCorrectTypeTest extends TestCase unknown_field } }', - [$this->undefinedField('unknown_field', 'Pet', [], 4, 11)] + [$this->undefinedField('unknown_field', 'Pet', [], [], 4, 11)] ); } @@ -156,7 +158,7 @@ class FieldsOnCorrectTypeTest extends TestCase meowVolume } }', - [$this->undefinedField('meowVolume', 'Dog', [], 4, 11)] + [$this->undefinedField('meowVolume', 'Dog', [], ['barkVolume'], 4, 11)] ); } @@ -169,7 +171,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment aliasedFieldTargetNotDefined on Dog { volume : mooVolume }', - [$this->undefinedField('mooVolume', 'Dog', [], 3, 9)] + [$this->undefinedField('mooVolume', 'Dog', [], ['barkVolume'], 3, 9)] ); } @@ -182,7 +184,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment aliasedLyingFieldTargetNotDefined on Dog { barkVolume : kawVolume }', - [$this->undefinedField('kawVolume', 'Dog', [], 3, 9)] + [$this->undefinedField('kawVolume', 'Dog', [], ['barkVolume'], 3, 9)] ); } @@ -195,7 +197,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment notDefinedOnInterface on Pet { tailLength }', - [$this->undefinedField('tailLength', 'Pet', [], 3, 9)] + [$this->undefinedField('tailLength', 'Pet', [], [], 3, 9)] ); } @@ -208,8 +210,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment definedOnImplementorsButNotInterface on Pet { nickname }', - //[$this->undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9)] - [$this->undefinedField('nickname', 'Pet', [ ], 3, 9)] + [$this->undefinedField('nickname', 'Pet', ['Dog', 'Cat'], ['name'], 3, 9)] ); } @@ -234,7 +235,7 @@ class FieldsOnCorrectTypeTest extends TestCase fragment directFieldSelectionOnUnion on CatOrDog { directField }', - [$this->undefinedField('directField', 'CatOrDog', [], 3, 9)] + [$this->undefinedField('directField', 'CatOrDog', [], [], 3, 9)] ); } @@ -247,8 +248,14 @@ class FieldsOnCorrectTypeTest extends TestCase fragment definedOnImplementorsQueriedOnUnion on CatOrDog { name }', - //[$this->undefinedField('name', 'CatOrDog', [ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ], 3, 9)] - [$this->undefinedField('name', 'CatOrDog', [ ], 3, 9)] + [$this->undefinedField( + 'name', + 'CatOrDog', + ['Being', 'Pet', 'Canine', 'Dog', 'Cat'], + [], + 3, + 9 + )] ); } @@ -273,38 +280,78 @@ class FieldsOnCorrectTypeTest extends TestCase */ public function testWorksWithNoSuggestions() { - $this->assertEquals('Cannot query field "T" on type "f".', FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [])); + $this->assertEquals('Cannot query field "f" on type "T".', FieldsOnCorrectType::undefinedFieldMessage('f', 'T', [], [])); } /** - * @it Works with no small numbers of suggestions + * @it Works with no small numbers of type suggestions */ - public function testWorksWithNoSmallNumbersOfSuggestions() + public function testWorksWithNoSmallNumbersOfTypeSuggestions() { - $expected = 'Cannot query field "T" on type "f". ' . - 'However, this field exists on "A", "B". ' . - 'Perhaps you meant to use an inline fragment?'; + $expected = 'Cannot query field "f" on type "T". ' . + 'Did you mean to use an inline fragment on "A" or "B"?'; - $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B' ])); + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('f', 'T', ['A', 'B'], [])); } /** - * @it Works with lots of suggestions + * @it Works with no small numbers of field suggestions */ - public function testWorksWithLotsOfSuggestions() + public function testWorksWithNoSmallNumbersOfFieldSuggestions() { - $expected = 'Cannot query field "T" on type "f". ' . - 'However, this field exists on "A", "B", "C", "D", "E", ' . - 'and 1 other types. ' . - 'Perhaps you meant to use an inline fragment?'; + $expected = 'Cannot query field "f" on type "T". ' . + 'Did you mean "z" or "y"?'; - $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ])); + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('f', 'T', [], ['z', 'y'])); } - private function undefinedField($field, $type, $suggestions, $line, $column) + /** + * @it Only shows one set of suggestions at a time, preferring types + */ + public function testOnlyShowsOneSetOfSuggestionsAtATimePreferringTypes() + { + $expected = 'Cannot query field "f" on type "T". ' . + 'Did you mean to use an inline fragment on "A" or "B"?'; + + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('f', 'T', ['A', 'B'], ['z', 'y'])); + } + + /** + * @it Limits lots of type suggestions + */ + public function testLimitsLotsOfTypeSuggestions() + { + $expected = 'Cannot query field "f" on type "T". ' . + 'Did you mean to use an inline fragment on "A", "B", "C", "D" or "E"?'; + + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage( + 'f', + 'T', + ['A', 'B', 'C', 'D', 'E', 'F'], + [] + )); + } + + /** + * @it Limits lots of field suggestions + */ + public function testLimitsLotsOfFieldSuggestions() + { + $expected = 'Cannot query field "f" on type "T". ' . + 'Did you mean "z", "y", "x", "w" or "v"?'; + + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage( + 'f', + 'T', + [], + ['z', 'y', 'x', 'w', 'v', 'u'] + )); + } + + private function undefinedField($field, $type, $suggestedTypes, $suggestedFields, $line, $column) { return FormattedError::create( - FieldsOnCorrectType::undefinedFieldMessage($field, $type, $suggestions), + FieldsOnCorrectType::undefinedFieldMessage($field, $type, $suggestedTypes, $suggestedFields), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/KnownArgumentNamesTest.php b/tests/Validator/KnownArgumentNamesTest.php index 80ced66..84b7e38 100644 --- a/tests/Validator/KnownArgumentNamesTest.php +++ b/tests/Validator/KnownArgumentNamesTest.php @@ -112,7 +112,21 @@ class KnownArgumentNamesTest extends TestCase dog @skip(unless: true) } ', [ - $this->unknownDirectiveArg('unless', 'skip', 3, 19), + $this->unknownDirectiveArg('unless', 'skip', [], 3, 19), + ]); + } + + /** + * @it misspelled directive args are reported + */ + public function testMisspelledDirectiveArgsAreReported() + { + $this->expectFailsRule(new KnownArgumentNames, ' + { + dog @skip(iff: true) + } + ', [ + $this->unknownDirectiveArg('iff', 'skip', ['if'], 3, 19), ]); } @@ -126,7 +140,21 @@ class KnownArgumentNamesTest extends TestCase doesKnowCommand(unknown: true) } ', [ - $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 25), + $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', [],3, 25), + ]); + } + + /** + * @it misspelled arg name is reported + */ + public function testMisspelledArgNameIsReported() + { + $this->expectFailsRule(new KnownArgumentNames, ' + fragment invalidArgName on Dog { + doesKnowCommand(dogcommand: true) + } + ', [ + $this->unknownArg('dogcommand', 'doesKnowCommand', 'Dog', ['dogCommand'],3, 25), ]); } @@ -140,8 +168,8 @@ class KnownArgumentNamesTest extends TestCase doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) } ', [ - $this->unknownArg('whoknows', 'doesKnowCommand', 'Dog', 3, 25), - $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 55), + $this->unknownArg('whoknows', 'doesKnowCommand', 'Dog', [], 3, 25), + $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 3, 55), ]); } @@ -164,23 +192,23 @@ class KnownArgumentNamesTest extends TestCase } } ', [ - $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', 4, 27), - $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', 9, 31), + $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 4, 27), + $this->unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 9, 31), ]); } - private function unknownArg($argName, $fieldName, $typeName, $line, $column) + private function unknownArg($argName, $fieldName, $typeName, $suggestedArgs, $line, $column) { return FormattedError::create( - KnownArgumentNames::unknownArgMessage($argName, $fieldName, $typeName), + KnownArgumentNames::unknownArgMessage($argName, $fieldName, $typeName, $suggestedArgs), [new SourceLocation($line, $column)] ); } - private function unknownDirectiveArg($argName, $directiveName, $line, $column) + private function unknownDirectiveArg($argName, $directiveName, $suggestedArgs, $line, $column) { return FormattedError::create( - KnownArgumentNames::unknownDirectiveArgMessage($argName, $directiveName), + KnownArgumentNames::unknownDirectiveArgMessage($argName, $directiveName, $suggestedArgs), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/KnownTypeNamesTest.php b/tests/Validator/KnownTypeNamesTest.php index 8bdab66..f8aba61 100644 --- a/tests/Validator/KnownTypeNamesTest.php +++ b/tests/Validator/KnownTypeNamesTest.php @@ -42,9 +42,9 @@ class KnownTypeNamesTest extends TestCase name } ', [ - $this->unknownType('JumbledUpLetters', 2, 23), - $this->unknownType('Badger', 5, 25), - $this->unknownType('Peettt', 8, 29) + $this->unknownType('JumbledUpLetters', [], 2, 23), + $this->unknownType('Badger', [], 5, 25), + $this->unknownType('Peettt', ['Pet'], 8, 29) ]); } @@ -70,14 +70,14 @@ class KnownTypeNamesTest extends TestCase } } ', [ - $this->unknownType('NotInTheSchema', 12, 23), + $this->unknownType('NotInTheSchema', [], 12, 23), ]); } - private function unknownType($typeName, $line, $column) + private function unknownType($typeName, $suggestedTypes, $line, $column) { return FormattedError::create( - KnownTypeNames::unknownTypeMessage($typeName), + KnownTypeNames::unknownTypeMessage($typeName, $suggestedTypes), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/ValidationTest.php b/tests/Validator/ValidationTest.php index 7c7fc09..6cab583 100644 --- a/tests/Validator/ValidationTest.php +++ b/tests/Validator/ValidationTest.php @@ -58,7 +58,7 @@ Expected type \"Invalid\", found \"bad value\"; Invalid scalar is always invalid $query = '{invalid}'; $expectedError = [ - 'message' => 'Cannot query field "invalid" on type "QueryRoot".', + 'message' => 'Cannot query field "invalid" on type "QueryRoot". Did you mean "invalidArg"?', 'locations' => [ ['line' => 1, 'column' => 2] ] ]; $this->expectFailsCompleteValidation($query, [$expectedError]);