mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-30 00:46:02 +03:00
1350 lines
36 KiB
PHP
1350 lines
36 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace GraphQL\Tests\Validator;
|
|
|
|
use GraphQL\Error\FormattedError;
|
|
use GraphQL\Language\SourceLocation;
|
|
use GraphQL\Type\Definition\InterfaceType;
|
|
use GraphQL\Type\Definition\ObjectType;
|
|
use GraphQL\Type\Definition\Type;
|
|
use GraphQL\Type\Schema;
|
|
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
|
|
|
class OverlappingFieldsCanBeMergedTest extends ValidatorTestCase
|
|
{
|
|
// Validate: Overlapping fields can be merged
|
|
/**
|
|
* @see it('unique fields')
|
|
*/
|
|
public function testUniqueFields() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment uniqueFields on Dog {
|
|
name
|
|
nickname
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('identical fields')
|
|
*/
|
|
public function testIdenticalFields() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment mergeIdenticalFields on Dog {
|
|
name
|
|
name
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('identical fields with identical args')
|
|
*/
|
|
public function testIdenticalFieldsWithIdenticalArgs() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand(dogCommand: SIT)
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('identical fields with identical directives')
|
|
*/
|
|
public function testIdenticalFieldsWithIdenticalDirectives() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment mergeSameFieldsWithSameDirectives on Dog {
|
|
name @include(if: true)
|
|
name @include(if: true)
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('different args with different aliases')
|
|
*/
|
|
public function testDifferentArgsWithDifferentAliases() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment differentArgsWithDifferentAliases on Dog {
|
|
knowsSit : doesKnowCommand(dogCommand: SIT)
|
|
knowsDown : doesKnowCommand(dogCommand: DOWN)
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('different directives with different aliases')
|
|
*/
|
|
public function testDifferentDirectivesWithDifferentAliases() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment differentDirectivesWithDifferentAliases on Dog {
|
|
nameIfTrue : name @include(if: true)
|
|
nameIfFalse : name @include(if: false)
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment differentDirectivesWithDifferentAliases on Dog {
|
|
name @include(if: true)
|
|
name @include(if: false)
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('Same aliases with different field targets')
|
|
*/
|
|
public function testSameAliasesWithDifferentFieldTargets() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment sameAliasesWithDifferentFieldTargets on Dog {
|
|
fido : name
|
|
fido : nickname
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'fido',
|
|
'name and nickname are different fields'
|
|
),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment sameAliasesWithDifferentFieldTargets on Pet {
|
|
... on Dog {
|
|
name
|
|
}
|
|
... on Cat {
|
|
name: nickname
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('Alias masking direct field access')
|
|
*/
|
|
public function testAliasMaskingDirectFieldAccess() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment aliasMaskingDirectFieldAccess on Dog {
|
|
name : nickname
|
|
name
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'name',
|
|
'nickname and name are different fields'
|
|
),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('different args, second adds an argument')
|
|
*/
|
|
public function testDifferentArgsSecondAddsAnArgument() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand
|
|
doesKnowCommand(dogCommand: HEEL)
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'doesKnowCommand',
|
|
'they have differing arguments'
|
|
),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('different args, second missing an argument')
|
|
*/
|
|
public function testDifferentArgsSecondMissingAnArgument() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'doesKnowCommand',
|
|
'they have differing arguments'
|
|
),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('conflicting args')
|
|
*/
|
|
public function testConflictingArgs() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment conflictingArgs on Dog {
|
|
doesKnowCommand(dogCommand: SIT)
|
|
doesKnowCommand(dogCommand: HEEL)
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'doesKnowCommand',
|
|
'they have differing arguments'
|
|
),
|
|
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('allows different args where no conflict is possible')
|
|
*/
|
|
public function testAllowsDifferentArgsWhereNoConflictIsPossible() : void
|
|
{
|
|
// 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
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('encounters conflict in fragments')
|
|
*/
|
|
public function testEncountersConflictInFragments() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
...A
|
|
...B
|
|
}
|
|
fragment A on Type {
|
|
x: a
|
|
}
|
|
fragment B on Type {
|
|
x: b
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
|
|
[new SourceLocation(7, 9), new SourceLocation(10, 9)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('reports each conflict once')
|
|
*/
|
|
public function testReportsEachConflictOnce() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
f1 {
|
|
...A
|
|
...B
|
|
}
|
|
f2 {
|
|
...B
|
|
...A
|
|
}
|
|
f3 {
|
|
...A
|
|
...B
|
|
x: c
|
|
}
|
|
}
|
|
fragment A on Type {
|
|
x: a
|
|
}
|
|
fragment B on Type {
|
|
x: b
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage('x', 'a and b are different fields'),
|
|
[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)]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('deep conflict')
|
|
*/
|
|
public function testDeepConflict() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
x: a
|
|
},
|
|
field {
|
|
x: b
|
|
}
|
|
}
|
|
',
|
|
[
|
|
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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('deep conflict with multiple issues')
|
|
*/
|
|
public function testDeepConflictWithMultipleIssues() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
x: a
|
|
y: c
|
|
},
|
|
field {
|
|
x: b
|
|
y: d
|
|
}
|
|
}
|
|
',
|
|
[
|
|
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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('very deep conflict')
|
|
*/
|
|
public function testVeryDeepConflict() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
deepField {
|
|
x: a
|
|
}
|
|
},
|
|
field {
|
|
deepField {
|
|
x: b
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'field',
|
|
[['deepField', [['x', 'a and b are different fields']]]]
|
|
),
|
|
[
|
|
new SourceLocation(3, 9),
|
|
new SourceLocation(4, 11),
|
|
new SourceLocation(5, 13),
|
|
new SourceLocation(8, 9),
|
|
new SourceLocation(9, 11),
|
|
new SourceLocation(10, 13),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('reports deep conflict to nearest common ancestor')
|
|
*/
|
|
public function testReportsDeepConflictToNearestCommonAncestor() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
deepField {
|
|
x: a
|
|
}
|
|
deepField {
|
|
x: b
|
|
}
|
|
},
|
|
field {
|
|
deepField {
|
|
y
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'deepField',
|
|
[['x', 'a and b are different fields']]
|
|
),
|
|
[
|
|
new SourceLocation(4, 11),
|
|
new SourceLocation(5, 13),
|
|
new SourceLocation(7, 11),
|
|
new SourceLocation(8, 13),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('reports deep conflict to nearest common ancestor in fragments')
|
|
*/
|
|
public function testReportsDeepConflictToNearestCommonAncestorInFragments() : void
|
|
{
|
|
$this->expectFailsRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
...F
|
|
}
|
|
field {
|
|
...F
|
|
}
|
|
}
|
|
fragment F on T {
|
|
deepField {
|
|
deeperField {
|
|
x: a
|
|
}
|
|
deeperField {
|
|
x: b
|
|
}
|
|
}
|
|
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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('reports deep conflict in nested fragments')
|
|
*/
|
|
public function testReportsDeepConflictInNestedFragments() : void
|
|
{
|
|
$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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('ignores unknown fragments')
|
|
*/
|
|
public function testIgnoresUnknownFragments() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
field {
|
|
...Unknown
|
|
...Known
|
|
}
|
|
}
|
|
fragment Known on T {
|
|
field
|
|
...OtherUnknown
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
// Describe: return types must be unambiguous
|
|
|
|
/**
|
|
* @see it('conflicting return types which potentially overlap')
|
|
*/
|
|
public function testConflictingReturnTypesWhichPotentiallyOverlap() : void
|
|
{
|
|
// This is invalid since an object could potentially be both the Object
|
|
// type IntBox and the interface type NonNullStringBox1. While that
|
|
// condition does not exist in the current schema, the schema could
|
|
// expand in the future to allow this. Thus it is invalid.
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
...on IntBox {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types Int and String!'
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
private function getSchema()
|
|
{
|
|
$StringBox = null;
|
|
$IntBox = null;
|
|
$SomeBox = null;
|
|
|
|
$SomeBox = new InterfaceType([
|
|
'name' => 'SomeBox',
|
|
'fields' => static function () use (&$SomeBox) {
|
|
return [
|
|
'deepBox' => ['type' => $SomeBox],
|
|
'unrelatedField' => ['type' => Type::string()],
|
|
];
|
|
},
|
|
]);
|
|
|
|
$StringBox = new ObjectType([
|
|
'name' => 'StringBox',
|
|
'interfaces' => [$SomeBox],
|
|
'fields' => static 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],
|
|
];
|
|
},
|
|
]);
|
|
|
|
$IntBox = new ObjectType([
|
|
'name' => 'IntBox',
|
|
'interfaces' => [$SomeBox],
|
|
'fields' => static 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],
|
|
];
|
|
},
|
|
]);
|
|
|
|
$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()],
|
|
],
|
|
]),
|
|
],
|
|
],
|
|
])),
|
|
],
|
|
],
|
|
]);
|
|
|
|
return new Schema([
|
|
'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.
|
|
$this->expectPassesRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on SomeBox {
|
|
deepBox {
|
|
unrelatedField
|
|
}
|
|
}
|
|
... on StringBox {
|
|
deepBox {
|
|
unrelatedField
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('disallows differing return types despite no overlap')
|
|
*/
|
|
public function testDisallowsDifferingReturnTypesDespiteNoOverlap() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
scalar
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types Int and String'
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('reports correctly when a non-exclusive follows an exclusive')
|
|
*/
|
|
public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive() : void
|
|
{
|
|
$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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('disallows differing return type nullability despite no overlap')
|
|
*/
|
|
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'scalar',
|
|
'they return conflicting types String! and String'
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(8, 15),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('disallows differing return type list despite no overlap')
|
|
*/
|
|
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: listStringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
'they return conflicting types [StringBox] and StringBox'
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(10, 15),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: listStringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
'they return conflicting types StringBox and [StringBox]'
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(10, 15),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
public function testDisallowsDifferingSubfields() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
val: scalar
|
|
val: unrelatedField
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: stringBox {
|
|
val: scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'val',
|
|
'scalar and unrelatedField are different fields'
|
|
),
|
|
[
|
|
new SourceLocation(6, 17),
|
|
new SourceLocation(7, 17),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('disallows differing deep return types despite no overlap')
|
|
*/
|
|
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
box: stringBox {
|
|
scalar
|
|
}
|
|
}
|
|
... on StringBox {
|
|
box: intBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
FormattedError::create(
|
|
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
|
'box',
|
|
[['scalar', 'they return conflicting types String and Int']]
|
|
),
|
|
[
|
|
new SourceLocation(5, 15),
|
|
new SourceLocation(6, 17),
|
|
new SourceLocation(10, 15),
|
|
new SourceLocation(11, 17),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('allows non-conflicting overlaping types')
|
|
*/
|
|
public function testAllowsNonConflictingOverlapingTypes() : void
|
|
{
|
|
$this->expectPassesRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
... on IntBox {
|
|
scalar: unrelatedField
|
|
}
|
|
... on StringBox {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('same wrapped scalar return types')
|
|
*/
|
|
public function testSameWrappedScalarReturnTypes() : void
|
|
{
|
|
$this->expectPassesRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
...on NonNullStringBox1 {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox2 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('allows inline typeless fragments')
|
|
*/
|
|
public function testAllowsInlineTypelessFragments() : void
|
|
{
|
|
$this->expectPassesRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
a
|
|
... {
|
|
a
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('compares deep types including list')
|
|
*/
|
|
public function testComparesDeepTypesIncludingList() : void
|
|
{
|
|
$this->expectFailsRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
connection {
|
|
...edgeID
|
|
edges {
|
|
node {
|
|
id: name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fragment edgeID on Connection {
|
|
edges {
|
|
node {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
',
|
|
[
|
|
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
|
|
{
|
|
$this->expectPassesRuleWithSchema(
|
|
$this->getSchema(),
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
{
|
|
someBox {
|
|
...on UnknownType {
|
|
scalar
|
|
}
|
|
...on NonNullStringBox2 {
|
|
scalar
|
|
}
|
|
}
|
|
}
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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');
|
|
$hint = 'Use different aliases on the fields to fetch both if this was intentional.';
|
|
|
|
self::assertStringEndsWith($hint, $error);
|
|
}
|
|
|
|
/**
|
|
* @see it('does not infinite loop on recursive fragment')
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnRecursiveFragment() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment fragA on Human { name, relatives { name, ...fragA } }
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('does not infinite loop on immediately recursive fragment')
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnImmeditelyRecursiveFragment() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment fragA on Human { name, ...fragA }
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('does not infinite loop on transitively recursive fragment')
|
|
*/
|
|
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragment() : void
|
|
{
|
|
$this->expectPassesRule(
|
|
new OverlappingFieldsCanBeMerged(),
|
|
'
|
|
fragment fragA on Human { name, ...fragB }
|
|
fragment fragB on Human { name, ...fragC }
|
|
fragment fragC on Human { name, ...fragA }
|
|
'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see it('find invalid case even with immediately recursive fragment')
|
|
*/
|
|
public function testFindInvalidCaseEvenWithImmediatelyRecursiveFragment() : void
|
|
{
|
|
$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),
|
|
]
|
|
),
|
|
]
|
|
);
|
|
}
|
|
}
|