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
|
|
|
|
2016-10-21 12:39:57 +03:00
|
|
|
use GraphQL\Error\FormattedError;
|
2015-07-15 20:05:46 +03:00
|
|
|
use GraphQL\Language\SourceLocation;
|
2018-02-11 15:15:51 +03:00
|
|
|
use GraphQL\Type\Schema;
|
2016-04-25 16:29:17 +03:00
|
|
|
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
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('unique fields')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testUniqueFields() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged(), '
|
|
|
|
fragment uniqueFields on Dog {
|
|
|
|
name
|
|
|
|
nickname
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('identical fields')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testIdenticalFields() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment mergeIdenticalFields on Dog {
|
|
|
|
name
|
|
|
|
name
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('identical fields with identical args')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testIdenticalFieldsWithIdenticalArgs() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
|
|
|
|
doesKnowCommand(dogCommand: SIT)
|
|
|
|
doesKnowCommand(dogCommand: SIT)
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('identical fields with identical directives')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testIdenticalFieldsWithIdenticalDirectives() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment mergeSameFieldsWithSameDirectives on Dog {
|
2015-08-17 17:01:55 +03:00
|
|
|
name @include(if: true)
|
|
|
|
name @include(if: true)
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('different args with different aliases')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDifferentArgsWithDifferentAliases() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment differentArgsWithDifferentAliases on Dog {
|
|
|
|
knowsSit : doesKnowCommand(dogCommand: SIT)
|
|
|
|
knowsDown : doesKnowCommand(dogCommand: DOWN)
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('different directives with different aliases')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDifferentDirectivesWithDifferentAliases() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment differentDirectivesWithDifferentAliases on Dog {
|
2015-08-17 17:01:55 +03:00
|
|
|
nameIfTrue : name @include(if: true)
|
|
|
|
nameIfFalse : name @include(if: false)
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('different skip/include directives accepted')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDifferentSkipIncludeDirectivesAccepted() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('Same aliases with different field targets')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testSameAliasesWithDifferentFieldTargets() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment sameAliasesWithDifferentFieldTargets on Dog {
|
|
|
|
fido : name
|
|
|
|
fido : nickname
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('Same aliases allowed on non-overlapping fields')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testSameAliasesAllowedOnNonOverlappingFields() : void
|
2016-04-25 16:29:17 +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 sameAliasesWithDifferentFieldTargets on Pet {
|
|
|
|
... on Dog {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
... on Cat {
|
|
|
|
name: nickname
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('Alias masking direct field access')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testAliasMaskingDirectFieldAccess() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment aliasMaskingDirectFieldAccess on Dog {
|
|
|
|
name : nickname
|
|
|
|
name
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('different args, second adds an argument')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDifferentArgsSecondAddsAnArgument() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment conflictingArgs on Dog {
|
2016-04-25 16:29:17 +03:00
|
|
|
doesKnowCommand
|
2015-07-15 20:05:46 +03:00
|
|
|
doesKnowCommand(dogCommand: HEEL)
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('different args, second missing an argument')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDifferentArgsSecondMissingAnArgument() : void
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
fragment conflictingArgs on Dog {
|
|
|
|
doesKnowCommand(dogCommand: SIT)
|
|
|
|
doesKnowCommand
|
2015-08-17 17:01:55 +03:00
|
|
|
}
|
2016-04-25 16:29:17 +03:00
|
|
|
',
|
|
|
|
[
|
2015-08-17 17:01:55 +03:00
|
|
|
FormattedError::create(
|
2016-04-25 16:29:17 +03:00
|
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
2015-08-17 17:01:55 +03:00
|
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('conflicting args')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testConflictingArgs() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
fragment conflictingArgs on Dog {
|
|
|
|
doesKnowCommand(dogCommand: SIT)
|
|
|
|
doesKnowCommand(dogCommand: HEEL)
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
FormattedError::create(
|
|
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
2016-04-25 16:29:17 +03:00
|
|
|
[new SourceLocation(3,9), new SourceLocation(4,9)]
|
2015-07-15 20:05:46 +03:00
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('allows different args where no conflict is possible')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testAllowsDifferentArgsWhereNoConflictIsPossible() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +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
|
|
|
}
|
2016-04-25 16:29:17 +03:00
|
|
|
');
|
2015-07-15 20:05:46 +03:00
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('encounters conflict in fragments')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testEncountersConflictInFragments() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
...A
|
|
|
|
...B
|
|
|
|
}
|
|
|
|
fragment A on Type {
|
|
|
|
x: a
|
|
|
|
}
|
|
|
|
fragment B on Type {
|
|
|
|
x: b
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('reports each conflict once')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testReportsEachConflictOnce() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$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
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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)]
|
|
|
|
),
|
2015-08-17 17:01:55 +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 a are different fields'),
|
|
|
|
[new SourceLocation(14, 11), new SourceLocation(18, 9)]
|
2015-07-15 20:05:46 +03:00
|
|
|
),
|
2015-08-17 17:01:55 +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
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('deep conflict')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDeepConflict() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
x: a
|
|
|
|
},
|
|
|
|
field {
|
|
|
|
x: b
|
|
|
|
}
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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),
|
2016-04-25 16:29:17 +03:00
|
|
|
new SourceLocation(6,9),
|
2015-07-15 20:05:46 +03:00
|
|
|
new SourceLocation(7, 11)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('deep conflict with multiple issues')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDeepConflictWithMultipleIssues() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
x: a
|
|
|
|
y: c
|
|
|
|
},
|
|
|
|
field {
|
|
|
|
x: b
|
|
|
|
y: d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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),
|
2016-04-25 16:29:17 +03:00
|
|
|
new SourceLocation(7,9),
|
|
|
|
new SourceLocation(8,11),
|
2015-07-15 20:05:46 +03:00
|
|
|
new SourceLocation(9,11)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('very deep conflict')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testVeryDeepConflict() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
deepField {
|
|
|
|
x: a
|
|
|
|
}
|
|
|
|
},
|
|
|
|
field {
|
|
|
|
deepField {
|
|
|
|
x: b
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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),
|
2016-04-25 16:29:17 +03:00
|
|
|
new SourceLocation(8,9),
|
|
|
|
new SourceLocation(9,11),
|
2015-07-15 20:05:46 +03:00
|
|
|
new SourceLocation(10,13)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('reports deep conflict to nearest common ancestor')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testReportsDeepConflictToNearestCommonAncestor() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
deepField {
|
|
|
|
x: a
|
|
|
|
}
|
|
|
|
deepField {
|
|
|
|
x: b
|
|
|
|
}
|
|
|
|
},
|
|
|
|
field {
|
|
|
|
deepField {
|
|
|
|
y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
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),
|
2016-04-25 16:29:17 +03:00
|
|
|
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
|
|
|
/**
|
2018-08-31 11:55:14 +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
|
|
|
*/
|
2018-08-31 12:07:29 +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
|
|
|
{
|
|
|
|
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
...F
|
|
|
|
}
|
|
|
|
field {
|
|
|
|
...F
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment F on T {
|
|
|
|
deepField {
|
|
|
|
deeperField {
|
|
|
|
x: a
|
|
|
|
}
|
|
|
|
deeperField {
|
|
|
|
x: b
|
|
|
|
}
|
2018-02-11 19:58:48 +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
|
|
|
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),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +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
|
|
|
*/
|
2018-08-31 12:07:29 +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
|
|
|
{
|
|
|
|
$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),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +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
|
|
|
*/
|
2018-08-31 12:07:29 +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
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
{
|
|
|
|
field {
|
|
|
|
...Unknown
|
|
|
|
...Known
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment Known on T {
|
|
|
|
field
|
|
|
|
...OtherUnknown
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
// Describe: return types must be unambiguous
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('conflicting return types which potentially overlap')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testConflictingReturnTypesWhichPotentiallyOverlap() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +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-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +03:00
|
|
|
someBox {
|
2015-07-15 20:05:46 +03:00
|
|
|
...on IntBox {
|
|
|
|
scalar
|
|
|
|
}
|
2016-04-25 16:29:17 +03:00
|
|
|
...on NonNullStringBox1 {
|
2015-07-15 20:05:46 +03:00
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-04-25 16:29:17 +03:00
|
|
|
', [
|
|
|
|
FormattedError::create(
|
|
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
|
|
'scalar',
|
|
|
|
'they return conflicting types Int and String!'
|
|
|
|
),
|
|
|
|
[new SourceLocation(5, 15),
|
|
|
|
new SourceLocation(8, 15)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('compatible return shapes on different return types')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testCompatibleReturnShapesOnDifferentReturnTypes() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
// 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-02-11 15:15:51 +03:00
|
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
someBox {
|
|
|
|
... on SomeBox {
|
|
|
|
deepBox {
|
|
|
|
unrelatedField
|
|
|
|
}
|
|
|
|
}
|
|
|
|
... on StringBox {
|
|
|
|
deepBox {
|
|
|
|
unrelatedField
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('disallows differing return types despite no overlap')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDisallowsDifferingReturnTypesDespiteNoOverlap() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
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
|
|
|
/**
|
2018-08-31 11:55:14 +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
|
|
|
*/
|
2018-08-31 12:07:29 +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
|
|
|
{
|
|
|
|
$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),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('disallows differing return type nullability despite no overlap')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('disallows differing return type list despite no overlap')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDisallowsDifferingSubfields() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
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)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('disallows differing deep return types despite no overlap')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
someBox {
|
|
|
|
... on IntBox {
|
|
|
|
box: stringBox {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
... on StringBox {
|
|
|
|
box: intBox {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
', [
|
2015-08-17 17:01:55 +03:00
|
|
|
FormattedError::create(
|
2016-04-25 16:29:17 +03:00
|
|
|
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
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('allows non-conflicting overlaping types')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testAllowsNonConflictingOverlapingTypes() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
someBox {
|
|
|
|
... on IntBox {
|
|
|
|
scalar: unrelatedField
|
|
|
|
}
|
|
|
|
... on StringBox {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('same wrapped scalar return types')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testSameWrappedScalarReturnTypes() : void
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +03:00
|
|
|
someBox {
|
2015-07-15 20:05:46 +03:00
|
|
|
...on NonNullStringBox1 {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
...on NonNullStringBox2 {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('allows inline typeless fragments')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testAllowsInlineTypelessFragments() : void
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2016-04-25 16:29:17 +03:00
|
|
|
{
|
|
|
|
a
|
|
|
|
... {
|
|
|
|
a
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('compares deep types including list')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testComparesDeepTypesIncludingList() : void
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectFailsRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
|
|
|
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']]]]),
|
2015-08-17 17:01:55 +03:00
|
|
|
[
|
2016-04-25 16:29:17 +03:00
|
|
|
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),
|
2015-08-17 17:01:55 +03:00
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('ignores unknown types')
|
2016-04-25 16:29:17 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testIgnoresUnknownTypes() : void
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
2018-02-11 15:15:51 +03:00
|
|
|
$this->expectPassesRuleWithSchema($this->getSchema(), new OverlappingFieldsCanBeMerged, '
|
2015-08-17 17:01:55 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +03:00
|
|
|
someBox {
|
2015-08-17 17:01:55 +03:00
|
|
|
...on UnknownType {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
...on NonNullStringBox2 {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
2018-02-11 16:11:21 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('error message contains hint for alias conflict')
|
2018-02-11 16:11:21 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testErrorMessageContainsHintForAliasConflict() : void
|
2018-02-11 16:11:21 +03:00
|
|
|
{
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2018-02-11 19:58:48 +03:00
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('does not infinite loop on recursive fragment')
|
2018-02-11 19:58:48 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDoesNotInfiniteLoopOnRecursiveFragment() : void
|
2018-02-11 19:58:48 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment fragA on Human { name, relatives { name, ...fragA } }
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('does not infinite loop on immediately recursive fragment')
|
2018-02-11 19:58:48 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment() : void
|
2018-02-11 19:58:48 +03:00
|
|
|
{
|
|
|
|
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
|
|
|
fragment fragA on Human { name, ...fragA }
|
|
|
|
');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-08-31 11:55:14 +03:00
|
|
|
* @see it('does not infinite loop on transitively recursive fragment')
|
2018-02-11 19:58:48 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment() : void
|
2018-02-11 19:58:48 +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-08-31 11:55:14 +03:00
|
|
|
* @see it('find invalid case even with immediately recursive fragment')
|
2018-02-11 19:58:48 +03:00
|
|
|
*/
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment() : void
|
2018-02-11 19:58:48 +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-02-11 15:15:51 +03:00
|
|
|
private function getSchema()
|
2015-07-15 20:05:46 +03:00
|
|
|
{
|
2016-04-25 16:29:17 +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',
|
2016-04-25 16:29:17 +03:00
|
|
|
'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',
|
2016-04-25 16:29:17 +03:00
|
|
|
'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
|
|
|
]);
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
$NonNullStringBox1 = new InterfaceType([
|
2015-07-15 20:05:46 +03:00
|
|
|
'name' => 'NonNullStringBox1',
|
|
|
|
'fields' => [
|
|
|
|
'scalar' => [ 'type' => Type::nonNull(Type::string()) ]
|
|
|
|
]
|
|
|
|
]);
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
$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())]
|
|
|
|
]
|
|
|
|
]);
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
$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
|
|
|
]);
|
|
|
|
|
2015-08-17 17:01:55 +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()]
|
|
|
|
]
|
|
|
|
])
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]))
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]);
|
|
|
|
|
2016-04-25 16:29:17 +03:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|