graphql-php/tests/Validator/OverlappingFieldsCanBeMergedTest.php

1350 lines
36 KiB
PHP
Raw Normal View History

2015-07-15 20:05:46 +03:00
<?php
2018-09-02 14:08:49 +03:00
declare(strict_types=1);
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\Definition\InterfaceType;
2015-07-15 20:05:46 +03:00
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
2018-09-02 14:08:49 +03:00
use GraphQL\Type\Schema;
2015-07-15 20:05:46 +03:00
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
/**
* @see it('unique fields')
*/
public function testUniqueFields() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment uniqueFields on Dog {
name
nickname
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('identical fields')
*/
public function testIdenticalFields() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment mergeIdenticalFields on Dog {
name
name
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('identical fields with identical args')
*/
public function testIdenticalFieldsWithIdenticalArgs() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: SIT)
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('identical fields with identical directives')
*/
public function testIdenticalFieldsWithIdenticalDirectives() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment mergeSameFieldsWithSameDirectives on Dog {
name @include(if: true)
name @include(if: true)
2015-07-15 20:05:46 +03:00
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('different args with different aliases')
*/
public function testDifferentArgsWithDifferentAliases() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment differentArgsWithDifferentAliases on Dog {
knowsSit : doesKnowCommand(dogCommand: SIT)
knowsDown : doesKnowCommand(dogCommand: DOWN)
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('different directives with different aliases')
*/
public function testDifferentDirectivesWithDifferentAliases() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment differentDirectivesWithDifferentAliases on Dog {
nameIfTrue : name @include(if: true)
nameIfFalse : name @include(if: false)
2015-07-15 20:05:46 +03:00
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('different skip/include directives accepted')
*/
public function testDifferentSkipIncludeDirectivesAccepted() : void
{
// 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.
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment differentDirectivesWithDifferentAliases on Dog {
name @include(if: true)
name @include(if: false)
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('Same aliases with different field targets')
*/
public function testSameAliasesWithDifferentFieldTargets() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment sameAliasesWithDifferentFieldTargets on Dog {
fido : name
fido : nickname
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'fido',
'name and nickname are different fields'
),
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('Same aliases allowed on non-overlapping fields')
*/
public function testSameAliasesAllowedOnNonOverlappingFields() : void
{
// This is valid since no object can be both a "Dog" and a "Cat", thus
// these fields can never overlap.
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment sameAliasesWithDifferentFieldTargets on Pet {
... on Dog {
name
}
... on Cat {
name: nickname
}
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('Alias masking direct field access')
*/
public function testAliasMaskingDirectFieldAccess() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment aliasMaskingDirectFieldAccess on Dog {
name : nickname
name
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'name',
'nickname and name are different fields'
),
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('different args, second adds an argument')
*/
public function testDifferentArgsSecondAddsAnArgument() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
fragment conflictingArgs on Dog {
doesKnowCommand
2015-07-15 20:05:46 +03:00
doesKnowCommand(dogCommand: HEEL)
}
2018-09-02 14:08:49 +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
}
/**
* @see it('different args, second missing an argument')
*/
public function testDifferentArgsSecondMissingAnArgument() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand
}
',
[
FormattedError::create(
2018-09-02 14:08:49 +03:00
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'doesKnowCommand',
'they have differing arguments'
),
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
2018-09-02 14:08:49 +03:00
),
]
);
}
/**
* @see it('conflicting args')
*/
public function testConflictingArgs() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: HEEL)
2015-07-15 20:05:46 +03:00
}
2018-09-02 14:08:49 +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
}
/**
* @see it('allows different args where no conflict is possible')
*/
public function testAllowsDifferentArgsWhereNoConflictIsPossible() : void
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.
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment conflictingArgs on Pet {
... on Dog {
name(surname: true)
}
... on Cat {
name
}
2015-07-15 20:05:46 +03:00
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('encounters conflict in fragments')
*/
public function testEncountersConflictInFragments() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
...A
...B
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
[new SourceLocation(7, 9), new SourceLocation(10, 9)]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('reports each conflict once')
*/
public function testReportsEachConflictOnce() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
f1 {
...A
...B
}
f2 {
...B
...A
}
f3 {
...A
...B
x: c
}
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
[new SourceLocation(18, 9), new SourceLocation(21, 9)]
),
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'c and a are different fields'),
[new SourceLocation(14, 11), new SourceLocation(18, 9)]
),
FormattedError::create(
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
}
/**
* @see it('deep conflict')
*/
public function testDeepConflict() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
field {
x: a
},
field {
x: b
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'field',
[['x', 'a and b are different fields']]
),
[
new SourceLocation(3, 9),
new SourceLocation(4, 11),
new SourceLocation(6, 9),
new SourceLocation(7, 11),
]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('deep conflict with multiple issues')
*/
public function testDeepConflictWithMultipleIssues() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
field {
x: a
y: c
},
field {
x: b
y: d
}
}
2018-09-02 14:08:49 +03:00
',
[
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(4, 11),
new SourceLocation(5, 11),
new SourceLocation(7, 9),
new SourceLocation(8, 11),
new SourceLocation(9, 11),
]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('very deep conflict')
*/
public function testVeryDeepConflict() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
field {
deepField {
x: a
}
},
field {
deepField {
x: b
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'field',
[['deepField', [['x', 'a and b are different fields']]]]
),
[
new SourceLocation(3, 9),
new SourceLocation(4, 11),
new SourceLocation(5, 13),
new SourceLocation(8, 9),
new SourceLocation(9, 11),
new SourceLocation(10, 13),
]
),
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('reports deep conflict to nearest common ancestor')
*/
public function testReportsDeepConflictToNearestCommonAncestor() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
2015-07-15 20:05:46 +03:00
{
field {
deepField {
x: a
}
deepField {
x: b
}
},
field {
deepField {
y
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'deepField',
[['x', 'a and b are different fields']]
),
[
new SourceLocation(4, 11),
new SourceLocation(5, 13),
new SourceLocation(7, 11),
new SourceLocation(8, 13),
]
),
]
);
2015-07-15 20:05:46 +03:00
}
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
/**
* @see it('reports deep conflict to nearest common ancestor in fragments')
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
*/
public function testReportsDeepConflictToNearestCommonAncestorInFragments() : void
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
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
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
{
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
}
}
}
2018-09-02 14:08:49 +03:00
',
[
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),
]
),
]
);
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
}
/**
* @see it('reports deep conflict in nested fragments')
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
*/
public function testReportsDeepConflictInNestedFragments() : void
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
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRule(
new OverlappingFieldsCanBeMerged(),
'
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
{
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
}
2018-09-02 14:08:49 +03:00
',
[
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),
]
),
]
);
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
}
/**
* @see it('ignores unknown fragments')
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
*/
public function testIgnoresUnknownFragments() : void
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
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
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
{
field {
...Unknown
...Known
}
}
fragment Known on T {
field
...OtherUnknown
}
2018-09-02 14:08:49 +03:00
'
);
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
}
// Describe: return types must be unambiguous
/**
* @see it('conflicting return types which potentially overlap')
*/
public function testConflictingReturnTypesWhichPotentiallyOverlap() : void
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.
2018-09-02 14:08:49 +03:00
$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
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types Int and String!'
),
[
new SourceLocation(5, 15),
new SourceLocation(8, 15),
]
),
2018-09-02 14:08:49 +03:00
]
);
}
private function getSchema()
{
$StringBox = null;
$IntBox = null;
$SomeBox = null;
$SomeBox = new InterfaceType([
'name' => 'SomeBox',
2018-09-26 12:07:23 +03:00
'fields' => static function () use (&$SomeBox) {
2018-09-02 14:08:49 +03:00
return [
'deepBox' => ['type' => $SomeBox],
'unrelatedField' => ['type' => Type::string()],
];
},
]);
$StringBox = new ObjectType([
'name' => 'StringBox',
'interfaces' => [$SomeBox],
2018-09-26 12:07:23 +03:00
'fields' => static function () use (&$StringBox, &$IntBox) {
2018-09-02 14:08:49 +03:00
return [
'scalar' => ['type' => Type::string()],
'deepBox' => ['type' => $StringBox],
'unrelatedField' => ['type' => Type::string()],
'listStringBox' => ['type' => Type::listOf($StringBox)],
'stringBox' => ['type' => $StringBox],
'intBox' => ['type' => $IntBox],
];
},
]);
$IntBox = new ObjectType([
'name' => 'IntBox',
'interfaces' => [$SomeBox],
2018-09-26 12:07:23 +03:00
'fields' => static function () use (&$StringBox, &$IntBox) {
2018-09-02 14:08:49 +03:00
return [
'scalar' => ['type' => Type::int()],
'deepBox' => ['type' => $IntBox],
'unrelatedField' => ['type' => Type::string()],
'listStringBox' => ['type' => Type::listOf($StringBox)],
'stringBox' => ['type' => $StringBox],
'intBox' => ['type' => $IntBox],
];
},
]);
$NonNullStringBox1 = new InterfaceType([
'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([
'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],
],
]);
$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()],
],
]),
],
],
])),
],
],
]);
2018-09-26 12:07:23 +03:00
return new Schema([
2018-09-02 14:08:49 +03:00
'query' => new ObjectType([
'name' => 'QueryRoot',
'fields' => [
'someBox' => ['type' => $SomeBox],
'connection' => ['type' => $Connection],
],
]),
'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl],
]);
}
/**
* @see it('compatible return shapes on different return types')
*/
public function testCompatibleReturnShapesOnDifferentReturnTypes() : void
{
// 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.
2018-09-02 14:08:49 +03:00
$this->expectPassesRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on SomeBox {
deepBox {
unrelatedField
}
}
... on StringBox {
deepBox {
unrelatedField
}
}
}
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('disallows differing return types despite no overlap')
*/
public function testDisallowsDifferingReturnTypesDespiteNoOverlap() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
scalar
}
... on StringBox {
scalar
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types Int and String'
),
[
new SourceLocation(5, 15),
new SourceLocation(8, 15),
]
),
2018-09-02 14:08:49 +03:00
]
);
}
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
/**
* @see it('reports correctly when a non-exclusive follows an exclusive')
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
*/
public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive() : void
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
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
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
{
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
}
2018-09-02 14:08:49 +03:00
',
[
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),
]
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
),
2018-09-02 14:08:49 +03:00
]
);
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
}
/**
* @see it('disallows differing return type nullability despite no overlap')
*/
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on NonNullStringBox1 {
scalar
}
... on StringBox {
scalar
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'scalar',
'they return conflicting types String! and String'
),
[
new SourceLocation(5, 15),
new SourceLocation(8, 15),
]
),
2018-09-02 14:08:49 +03:00
]
);
}
/**
* @see it('disallows differing return type list despite no overlap')
*/
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
box: listStringBox {
scalar
}
}
... on StringBox {
box: stringBox {
scalar
}
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'box',
'they return conflicting types [StringBox] and StringBox'
),
[
new SourceLocation(5, 15),
new SourceLocation(10, 15),
]
),
2018-09-02 14:08:49 +03:00
]
);
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: listStringBox {
scalar
}
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'box',
'they return conflicting types StringBox and [StringBox]'
),
[
new SourceLocation(5, 15),
new SourceLocation(10, 15),
]
),
2018-09-02 14:08:49 +03:00
]
);
}
public function testDisallowsDifferingSubfields() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
box: stringBox {
val: scalar
val: unrelatedField
}
}
... on StringBox {
box: stringBox {
val: scalar
}
}
}
}
2018-09-02 14:08:49 +03:00
',
[
2018-09-02 14:08:49 +03:00
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'val',
'scalar and unrelatedField are different fields'
),
[
new SourceLocation(6, 17),
new SourceLocation(7, 17),
]
),
2018-09-02 14:08:49 +03:00
]
);
}
/**
* @see it('disallows differing deep return types despite no overlap')
*/
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: intBox {
scalar
}
}
}
}
2018-09-02 14:08:49 +03:00
',
[
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),
]
),
2018-09-02 14:08:49 +03:00
]
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('allows non-conflicting overlaping types')
*/
public function testAllowsNonConflictingOverlapingTypes() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
... on IntBox {
scalar: unrelatedField
}
... on StringBox {
scalar
}
}
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('same wrapped scalar return types')
*/
public function testSameWrappedScalarReturnTypes() : void
2015-07-15 20:05:46 +03:00
{
2018-09-02 14:08:49 +03:00
$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
}
}
}
2018-09-02 14:08:49 +03:00
'
);
2015-07-15 20:05:46 +03:00
}
/**
* @see it('allows inline typeless fragments')
*/
public function testAllowsInlineTypelessFragments() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
a
... {
a
}
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('compares deep types including list')
*/
public function testComparesDeepTypesIncludingList() : void
{
2018-09-02 14:08:49 +03:00
$this->expectFailsRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
connection {
...edgeID
edges {
node {
id: name
}
}
}
}
fragment edgeID on Connection {
edges {
node {
id
}
}
}
2018-09-02 14:08:49 +03:00
',
[
FormattedError::create(
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
'edges',
[['node', [['id', 'name and id are different fields']]]]
),
[
new SourceLocation(5, 13),
new SourceLocation(6, 15),
new SourceLocation(7, 17),
new SourceLocation(14, 11),
new SourceLocation(15, 13),
new SourceLocation(16, 15),
]
),
]
);
}
/**
* @see it('ignores unknown types')
*/
public function testIgnoresUnknownTypes() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRuleWithSchema(
$this->getSchema(),
new OverlappingFieldsCanBeMerged(),
'
{
someBox {
...on UnknownType {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('error message contains hint for alias conflict')
*/
public function testErrorMessageContainsHintForAliasConflict() : void
{
// 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');
2018-09-02 14:08:49 +03:00
$hint = 'Use different aliases on the fields to fetch both if this was intentional.';
2018-09-19 18:12:09 +03:00
self::assertStringEndsWith($hint, $error);
}
/**
* @see it('does not infinite loop on recursive fragment')
*/
public function testDoesNotInfiniteLoopOnRecursiveFragment() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment fragA on Human { name, relatives { name, ...fragA } }
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('does not infinite loop on immediately recursive fragment')
*/
public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment fragA on Human { name, ...fragA }
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('does not infinite loop on transitively recursive fragment')
*/
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment() : void
{
2018-09-02 14:08:49 +03:00
$this->expectPassesRule(
new OverlappingFieldsCanBeMerged(),
'
fragment fragA on Human { name, ...fragB }
fragment fragB on Human { name, ...fragC }
fragment fragC on Human { name, ...fragA }
2018-09-02 14:08:49 +03:00
'
);
}
/**
* @see it('find invalid case even with immediately recursive fragment')
*/
public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment() : void
{
2018-09-02 14:08:49 +03:00
$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),
]
2018-09-02 14:08:49 +03:00
),
]
2018-09-02 14:08:49 +03:00
);
2015-07-15 20:05:46 +03:00
}
}