graphql-php/tests/Validator/OverlappingFieldsCanBeMergedTest.php

1147 lines
32 KiB
PHP
Raw Normal View History

2015-07-15 20:05:46 +03:00
<?php
2016-04-09 10:36:53 +03:00
namespace GraphQL\Tests\Validator;
2015-07-15 20:05:46 +03:00
use GraphQL\Error\FormattedError;
2015-07-15 20:05:46 +03:00
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\InterfaceType;
2015-07-15 20:05:46 +03:00
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
2018-07-29 18:43:10 +03:00
class OverlappingFieldsCanBeMergedTest extends ValidatorTestCase
2015-07-15 20:05:46 +03:00
{
// Validate: Overlapping fields can be merged
/**
* @it unique fields
*/
2015-07-15 20:05:46 +03:00
public function testUniqueFields()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged(), '
fragment uniqueFields on Dog {
name
nickname
}
');
}
/**
* @it identical fields
*/
2015-07-15 20:05:46 +03:00
public function testIdenticalFields()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment mergeIdenticalFields on Dog {
name
name
}
');
}
/**
* @it identical fields with identical args
*/
2015-07-15 20:05:46 +03:00
public function testIdenticalFieldsWithIdenticalArgs()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: SIT)
}
');
}
/**
* @it identical fields with identical directives
*/
2015-07-15 20:05:46 +03:00
public function testIdenticalFieldsWithIdenticalDirectives()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment mergeSameFieldsWithSameDirectives on Dog {
name @include(if: true)
name @include(if: true)
2015-07-15 20:05:46 +03:00
}
');
}
/**
* @it different args with different aliases
*/
2015-07-15 20:05:46 +03:00
public function testDifferentArgsWithDifferentAliases()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment differentArgsWithDifferentAliases on Dog {
knowsSit : doesKnowCommand(dogCommand: SIT)
knowsDown : doesKnowCommand(dogCommand: DOWN)
}
');
}
/**
* @it different directives with different aliases
*/
2015-07-15 20:05:46 +03:00
public function testDifferentDirectivesWithDifferentAliases()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment differentDirectivesWithDifferentAliases on Dog {
nameIfTrue : name @include(if: true)
nameIfFalse : name @include(if: false)
2015-07-15 20:05:46 +03:00
}
');
}
/**
* @it different skip/include directives accepted
*/
public function testDifferentSkipIncludeDirectivesAccepted()
{
// Note: Differing skip/include directives don't create an ambiguous return
// value and are acceptable in conditions where differing runtime values
// may have the same desired effect of including or skipping a field.
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment differentDirectivesWithDifferentAliases on Dog {
name @include(if: true)
name @include(if: false)
}
');
}
/**
* @it Same aliases with different field targets
*/
2015-07-15 20:05:46 +03:00
public function testSameAliasesWithDifferentFieldTargets()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment sameAliasesWithDifferentFieldTargets on Dog {
fido : name
fido : nickname
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('fido', 'name and nickname are different fields'),
2015-07-15 20:05:46 +03:00
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
)
]);
}
/**
* @it Same aliases allowed on non-overlapping fields
*/
public function testSameAliasesAllowedOnNonOverlappingFields()
{
// This is valid since no object can be both a "Dog" and a "Cat", thus
// these fields can never overlap.
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment sameAliasesWithDifferentFieldTargets on Pet {
... on Dog {
name
}
... on Cat {
name: nickname
}
}
');
}
/**
* @it Alias masking direct field access
*/
2015-07-15 20:05:46 +03:00
public function testAliasMaskingDirectFieldAccess()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment aliasMaskingDirectFieldAccess on Dog {
name : nickname
name
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'nickname and name are different fields'),
2015-07-15 20:05:46 +03:00
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
)
]);
}
/**
* @it different args, second adds an argument
*/
public function testDifferentArgsSecondAddsAnArgument()
2015-07-15 20:05:46 +03:00
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingArgs on Dog {
doesKnowCommand
2015-07-15 20:05:46 +03:00
doesKnowCommand(dogCommand: HEEL)
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
2015-07-15 20:05:46 +03:00
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
)
]);
}
/**
* @it different args, second missing an argument
*/
public function testDifferentArgsSecondMissingAnArgument()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand
}
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
)
]
);
}
/**
* @it conflicting args
*/
public function testConflictingArgs()
2015-07-15 20:05:46 +03:00
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: HEEL)
2015-07-15 20:05:46 +03:00
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
[new SourceLocation(3,9), new SourceLocation(4,9)]
2015-07-15 20:05:46 +03:00
)
]);
}
/**
* @it allows different args where no conflict is possible
*/
public function testAllowsDifferentArgsWhereNoConflictIsPossible()
2015-07-15 20:05:46 +03:00
{
// This is valid since no object can be both a "Dog" and a "Cat", thus
// these fields can never overlap.
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment conflictingArgs on Pet {
... on Dog {
name(surname: true)
}
... on Cat {
name
}
2015-07-15 20:05:46 +03:00
}
');
2015-07-15 20:05:46 +03:00
}
/**
* @it encounters conflict in fragments
*/
2015-07-15 20:05:46 +03:00
public function testEncountersConflictInFragments()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
...A
...B
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
2015-07-15 20:05:46 +03:00
[new SourceLocation(7, 9), new SourceLocation(10, 9)]
)
]);
}
/**
* @it reports each conflict once
*/
2015-07-15 20:05:46 +03:00
public function testReportsEachConflictOnce()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
f1 {
...A
...B
}
f2 {
...B
...A
}
f3 {
...A
...B
x: c
}
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
2015-07-15 20:05:46 +03:00
[new SourceLocation(18, 9), new SourceLocation(21, 9)]
),
FormattedError::create(
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and a are different fields'),
[new SourceLocation(14, 11), new SourceLocation(18, 9)]
2015-07-15 20:05:46 +03:00
),
FormattedError::create(
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and b are different fields'),
[new SourceLocation(14, 11), new SourceLocation(21, 9)]
2015-07-15 20:05:46 +03:00
)
]);
}
/**
* @it deep conflict
*/
2015-07-15 20:05:46 +03:00
public function testDeepConflict()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
x: a
},
field {
x: b
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['x', 'a and b are different fields']]),
2015-07-15 20:05:46 +03:00
[
new SourceLocation(3, 9),
new SourceLocation(4, 11),
new SourceLocation(6,9),
2015-07-15 20:05:46 +03:00
new SourceLocation(7, 11)
]
)
]);
}
/**
* @it deep conflict with multiple issues
*/
2015-07-15 20:05:46 +03:00
public function testDeepConflictWithMultipleIssues()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
x: a
y: c
},
field {
x: b
y: d
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [
2015-07-15 20:05:46 +03:00
['x', 'a and b are different fields'],
['y', 'c and d are different fields']
]),
[
new SourceLocation(3,9),
new SourceLocation(4,11),
new SourceLocation(5,11),
new SourceLocation(7,9),
new SourceLocation(8,11),
2015-07-15 20:05:46 +03:00
new SourceLocation(9,11)
]
)
]);
}
/**
* @it very deep conflict
*/
2015-07-15 20:05:46 +03:00
public function testVeryDeepConflict()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
deepField {
x: a
}
},
field {
deepField {
x: b
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]),
2015-07-15 20:05:46 +03:00
[
new SourceLocation(3,9),
new SourceLocation(4,11),
new SourceLocation(5,13),
new SourceLocation(8,9),
new SourceLocation(9,11),
2015-07-15 20:05:46 +03:00
new SourceLocation(10,13)
]
)
]);
}
/**
* @it reports deep conflict to nearest common ancestor
*/
2015-07-15 20:05:46 +03:00
public function testReportsDeepConflictToNearestCommonAncestor()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
deepField {
x: a
}
deepField {
x: b
}
},
field {
deepField {
y
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]),
2015-07-15 20:05:46 +03:00
[
new SourceLocation(4,11),
new SourceLocation(5,13),
new SourceLocation(7,11),
2015-07-15 20:05:46 +03:00
new SourceLocation(8,13)
]
)
]);
}
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
/**
* @it reports deep conflict to nearest common ancestor in fragments
*/
public function testReportsDeepConflictToNearestCommonAncestorInFragments()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
...F
}
field {
...F
}
}
fragment F on T {
deepField {
deeperField {
x: a
}
deeperField {
x: b
}
}
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
deepField {
deeperField {
y
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('deeperField', [['x', 'a and b are different fields']]),
[
new SourceLocation(12,11),
new SourceLocation(13,13),
new SourceLocation(15,11),
new SourceLocation(16,13),
]
)
]);
}
/**
* @it reports deep conflict in nested fragments
*/
public function testReportsDeepConflictInNestedFragments()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
{
field {
...F
}
field {
...I
}
}
fragment F on T {
x: a
...G
}
fragment G on T {
y: c
}
fragment I on T {
y: d
...J
}
fragment J on T {
x: b
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [
['x', 'a and b are different fields'],
['y', 'c and d are different fields'],
]),
[
new SourceLocation(3,9),
new SourceLocation(11,9),
new SourceLocation(15,9),
new SourceLocation(6,9),
new SourceLocation(22,9),
new SourceLocation(18,9),
]
)
]);
}
/**
* @it ignores unknown fragments
*/
public function testIgnoresUnknownFragments()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
{
field {
...Unknown
...Known
}
}
fragment Known on T {
field
...OtherUnknown
}
');
}
// Describe: return types must be unambiguous
/**
* @it conflicting return types which potentially overlap
*/
public function testConflictingReturnTypesWhichPotentiallyOverlap()
2015-07-15 20:05:46 +03:00
{
// This is invalid since an object could potentially be both the Object
// type IntBox and the interface type NonNullStringBox1. While that
// condition does not exist in the current schema, the schema could
// expand in the future to allow this. Thus it is invalid.
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
2015-07-15 20:05:46 +03:00
{
someBox {
2015-07-15 20:05:46 +03:00
...on IntBox {
scalar
}
...on NonNullStringBox1 {
2015-07-15 20:05:46 +03:00
scalar
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types Int and String!'
),
[new SourceLocation(5, 15),
new SourceLocation(8, 15)]
)
]);
}
/**
* @it compatible return shapes on different return types
*/
public function testCompatibleReturnShapesOnDifferentReturnTypes()
{
// In this case `deepBox` returns `SomeBox` in the first usage, and
// `StringBox` in the second usage. These return types are not the same!
// however this is valid because the return *shapes* are compatible.
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on SomeBox {
deepBox {
unrelatedField
}
}
... on StringBox {
deepBox {
unrelatedField
}
}
}
}
');
}
/**
* @it disallows differing return types despite no overlap
*/
public function testDisallowsDifferingReturnTypesDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
scalar
}
... on StringBox {
scalar
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types Int and String'
),
[ new SourceLocation(5, 15),
new SourceLocation(8, 15)]
)
]);
}
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
/**
* @it reports correctly when a non-exclusive follows an exclusive
*/
public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
deepBox {
...X
}
}
}
someBox {
... on StringBox {
deepBox {
...Y
}
}
}
memoed: someBox {
... on IntBox {
deepBox {
...X
}
}
}
memoed: someBox {
... on StringBox {
deepBox {
...Y
}
}
}
other: someBox {
...X
}
other: someBox {
...Y
}
}
fragment X on SomeBox {
scalar
}
fragment Y on SomeBox {
scalar: unrelatedField
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'other',
[['scalar', 'scalar and unrelatedField are different fields']]
),
[
new SourceLocation(31, 11),
new SourceLocation(39, 11),
new SourceLocation(34, 11),
new SourceLocation(42, 11),
]
)
]);
}
/**
* @it disallows differing return type nullability despite no overlap
*/
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on NonNullStringBox1 {
scalar
}
... on StringBox {
scalar
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types String! and String'
),
[new SourceLocation(5, 15),
new SourceLocation(8, 15)]
)
]);
}
/**
* @it disallows differing return type list despite no overlap
*/
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
box: listStringBox {
scalar
}
}
... on StringBox {
box: stringBox {
scalar
}
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'box',
'they return conflicting types [StringBox] and StringBox'
),
[new SourceLocation(5, 15),
new SourceLocation(10, 15)]
)
]);
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: listStringBox {
scalar
}
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'box',
'they return conflicting types StringBox and [StringBox]'
),
[new SourceLocation(5, 15),
new SourceLocation(10, 15)]
)
]);
}
public function testDisallowsDifferingSubfields()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
box: stringBox {
val: scalar
val: unrelatedField
}
}
... on StringBox {
box: stringBox {
val: scalar
}
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'val',
'scalar and unrelatedField are different fields'
),
[new SourceLocation(6, 17),
new SourceLocation(7, 17)]
)
]);
}
/**
* @it disallows differing deep return types despite no overlap
*/
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: intBox {
scalar
}
}
}
}
', [
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'box',
[ [ 'scalar', 'they return conflicting types String and Int' ] ]
),
[
new SourceLocation(5, 15),
new SourceLocation(6, 17),
new SourceLocation(10, 15),
new SourceLocation(11, 17)
]
2015-07-15 20:05:46 +03:00
)
]);
}
/**
* @it allows non-conflicting overlaping types
*/
public function testAllowsNonConflictingOverlapingTypes()
{
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
... on IntBox {
scalar: unrelatedField
}
... on StringBox {
scalar
}
}
}
');
}
/**
* @it same wrapped scalar return types
*/
2015-07-15 20:05:46 +03:00
public function testSameWrappedScalarReturnTypes()
{
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
2015-07-15 20:05:46 +03:00
{
someBox {
2015-07-15 20:05:46 +03:00
...on NonNullStringBox1 {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
');
}
/**
* @it allows inline typeless fragments
*/
public function testAllowsInlineTypelessFragments()
{
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
a
... {
a
}
}
');
}
/**
* @it compares deep types including list
*/
public function testComparesDeepTypesIncludingList()
{
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
connection {
...edgeID
edges {
node {
id: name
}
}
}
}
fragment edgeID on Connection {
edges {
node {
id
}
}
}
', [
FormattedError::create(
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
OverlappingFieldsCanBeMerged::fieldsConflictMessage('edges', [['node', [['id', 'name and id are different fields']]]]),
[
new SourceLocation(5, 13),
new SourceLocation(6, 15),
new SourceLocation(7, 17),
Validation: improving overlapping fields quality This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread. e.g. ```graphql { same: a same: b ...X } fragment X on T { same: c same: d } ``` In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization). **BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`. From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement. From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment. This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test. ref: graphql/graphql-js#386
2018-02-11 19:45:35 +03:00
new SourceLocation(14, 11),
new SourceLocation(15, 13),
new SourceLocation(16, 15),
]
)
]);
}
/**
* @it ignores unknown types
*/
public function testIgnoresUnknownTypes()
{
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
{
someBox {
...on UnknownType {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
');
}
/**
* @it error message contains hint for alias conflict
*/
public function testErrorMessageContainsHintForAliasConflict()
{
// The error template should end with a hint for the user to try using
// different aliases.
$error = OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields');
$hint = 'Use different aliases on the fields to fetch both if this was intentional.';
$this->assertStringEndsWith($hint, $error);
}
/**
* @it does not infinite loop on recursive fragment
*/
public function testDoesNotInfiniteLoopOnRecursiveFragment()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment fragA on Human { name, relatives { name, ...fragA } }
');
}
/**
* @it does not infinite loop on immediately recursive fragment
*/
public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment fragA on Human { name, ...fragA }
');
}
/**
* @it does not infinite loop on transitively recursive fragment
*/
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment()
{
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
fragment fragA on Human { name, ...fragB }
fragment fragB on Human { name, ...fragC }
fragment fragC on Human { name, ...fragA }
');
}
/**
* @it find invalid case even with immediately recursive fragment
*/
public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment()
{
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
fragment sameAliasesWithDifferentFieldTargets on Dob {
...sameAliasesWithDifferentFieldTargets
fido: name
fido: nickname
}
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'fido',
'name and nickname are different fields'
),
[
new SourceLocation(4, 9),
new SourceLocation(5, 9),
]
)
]);
}
private function getSchema()
2015-07-15 20:05:46 +03:00
{
$StringBox = null;
$IntBox = null;
$SomeBox = null;
$SomeBox = new InterfaceType([
'name' => 'SomeBox',
'fields' => function() use (&$SomeBox) {
return [
'deepBox' => ['type' => $SomeBox],
'unrelatedField' => ['type' => Type::string()]
];
}
]);
2015-07-15 20:05:46 +03:00
$StringBox = new ObjectType([
'name' => 'StringBox',
'interfaces' => [$SomeBox],
'fields' => function() use (&$StringBox, &$IntBox) {
return [
'scalar' => ['type' => Type::string()],
'deepBox' => ['type' => $StringBox],
'unrelatedField' => ['type' => Type::string()],
'listStringBox' => ['type' => Type::listOf($StringBox)],
'stringBox' => ['type' => $StringBox],
'intBox' => ['type' => $IntBox],
];
}
2015-07-15 20:05:46 +03:00
]);
$IntBox = new ObjectType([
'name' => 'IntBox',
'interfaces' => [$SomeBox],
'fields' => function() use (&$StringBox, &$IntBox) {
return [
'scalar' => ['type' => Type::int()],
'deepBox' => ['type' => $IntBox],
'unrelatedField' => ['type' => Type::string()],
'listStringBox' => ['type' => Type::listOf($StringBox)],
'stringBox' => ['type' => $StringBox],
'intBox' => ['type' => $IntBox],
];
}
2015-07-15 20:05:46 +03:00
]);
$NonNullStringBox1 = new InterfaceType([
2015-07-15 20:05:46 +03:00
'name' => 'NonNullStringBox1',
'fields' => [
'scalar' => [ 'type' => Type::nonNull(Type::string()) ]
]
]);
$NonNullStringBox1Impl = new ObjectType([
'name' => 'NonNullStringBox1Impl',
'interfaces' => [ $SomeBox, $NonNullStringBox1 ],
'fields' => [
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
'unrelatedField' => ['type' => Type::string() ],
'deepBox' => [ 'type' => $SomeBox ],
]
]);
$NonNullStringBox2 = new InterfaceType([
2015-07-15 20:05:46 +03:00
'name' => 'NonNullStringBox2',
'fields' => [
'scalar' => ['type' => Type::nonNull(Type::string())]
]
]);
$NonNullStringBox2Impl = new ObjectType([
'name' => 'NonNullStringBox2Impl',
'interfaces' => [ $SomeBox, $NonNullStringBox2 ],
'fields' => [
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
'unrelatedField' => [ 'type' => Type::string() ],
'deepBox' => [ 'type' => $SomeBox ],
]
2015-07-15 20:05:46 +03:00
]);
$Connection = new ObjectType([
'name' => 'Connection',
'fields' => [
'edges' => [
'type' => Type::listOf(new ObjectType([
'name' => 'Edge',
'fields' => [
'node' => [
'type' => new ObjectType([
'name' => 'Node',
'fields' => [
'id' => ['type' => Type::id()],
'name' => ['type' => Type::string()]
]
])
]
]
]))
]
]
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'QueryRoot',
'fields' => [
'someBox' => ['type' => $SomeBox],
'connection' => ['type' => $Connection]
]
]),
'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl]
]);
2015-07-15 20:05:46 +03:00
return $schema;
}
}