Skip to content

Commit

Permalink
SPEC/BUG: Ambiguity with null variable values and default values (fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
vladar committed Aug 14, 2019
1 parent 953178f commit bf4e7d4
Show file tree
Hide file tree
Showing 18 changed files with 659 additions and 395 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## Unreleased
- **BREAKING:** Removal of `VariablesDefaultValueAllowed` validation rule. All variables may now specify a default value.
- **BREAKING:** renamed `ProvidedNonNullArguments` to `ProvidedRequiredArguments` (no longer require values to be provided to non-null arguments which provide a default value).
- Add schema validation: Input Objects must not contain non-nullable circular references (#492)
- Added retrieving query complexity once query has been completed (#316)
- Allow input types to be passed in from variables using \stdClass instead of associative arrays (#535)
Expand Down
180 changes: 110 additions & 70 deletions src/Executor/Values.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,48 +67,9 @@ public static function getVariableValues(Schema $schema, $varDefNodes, array $in
/** @var InputType|Type $varType */
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);

if (Type::isInputType($varType)) {
if (array_key_exists($varName, $inputs)) {
$value = $inputs[$varName];
$coerced = Value::coerceValue($value, $varType, $varDefNode);
/** @var Error[] $coercionErrors */
$coercionErrors = $coerced['errors'];
if (empty($coercionErrors)) {
$coercedValues[$varName] = $coerced['value'];
} else {
$messagePrelude = sprintf(
'Variable "$%s" got invalid value %s; ',
$varName,
Utils::printSafeJson($value)
);

foreach ($coercionErrors as $error) {
$errors[] = new Error(
$messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
);
}
}
} else {
if ($varType instanceof NonNull) {
$errors[] = new Error(
sprintf(
'Variable "$%s" of required type "%s" was not provided.',
$varName,
$varType
),
[$varDefNode]
);
} elseif ($varDefNode->defaultValue !== null) {
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
}
}
} else {
if (! Type::isInputType($varType)) {
// Must use input types for variables. This should be caught during
// validation, however is checked again here for safety.
$errors[] = new Error(
sprintf(
'Variable "$%s" expected value of type "%s" which cannot be used as an input type.',
Expand All @@ -117,6 +78,61 @@ public static function getVariableValues(Schema $schema, $varDefNodes, array $in
),
[$varDefNode->type]
);
} else {
$hasValue = array_key_exists($varName, $inputs);
$value = $hasValue ? $inputs[$varName] : Utils::undefined();

if (! $hasValue && $varDefNode->defaultValue) {
// If no value was provided to a variable with a default value,
// use the default value.
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
} elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) {
// If no value or a nullish value was provided to a variable with a
// non-null type (required), produce an error.
$errors[] = new Error(
sprintf(
$hasValue
? 'Variable "$%s" of non-null type "%s" must not be null.'
: 'Variable "$%s" of required type "%s" was not provided.',
$varName,
Utils::printSafe($varType)
),
[$varDefNode]
);
} elseif ($hasValue) {
if ($value === null) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$varName] = null;
} else {
// Otherwise, a non-null value was provided, coerce it to the expected
// type or report an error if coercion fails.
$coerced = Value::coerceValue($value, $varType, $varDefNode);
/** @var Error[] $coercionErrors */
$coercionErrors = $coerced['errors'];
if ($coercionErrors) {
$messagePrelude = sprintf(
'Variable "$%s" got invalid value %s; ',
$varName,
Utils::printSafeJson($value)
);

foreach ($coercionErrors as $error) {
$errors[] = new Error(
$messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
);
}
} else {
$coercedValues[$varName] = $coerced['value'];
}
}
}
}
}

Expand Down Expand Up @@ -208,47 +224,71 @@ public static function getArgumentValuesForMap($fieldDefinition, $argumentValueM
$argType = $argumentDefinition->getType();
$argumentValueNode = $argumentValueMap[$name] ?? null;

if ($argumentValueNode === null) {
if ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
if ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
$hasValue = $variableValues ? array_key_exists($variableName, $variableValues) : false;
$isNull = $hasValue ? $variableValues[$variableName] === null : false;
} else {
$hasValue = $argumentValueNode !== null;
$isNull = $argumentValueNode instanceof NullValueNode;
}

if (! $hasValue && $argumentDefinition->defaultValueExists()) {
// If no argument was provided where the definition has a default value,
// use the default value.
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) {
// If no argument or a null value was provided to an argument with a
// non-null type (required), produce a field error.
if ($isNull) {
throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
'Argument "' . $name . '" of non-null type ' .
'"' . Utils::printSafe($argType) . '" must not be null.',
$referenceNode
);
}
} elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;

if ($variableValues !== null && array_key_exists($variableName, $variableValues)) {
// Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName];
} elseif ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
if ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.',
[$argumentValueNode]
);
}
} else {
$valueNode = $argumentValueNode;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if (Utils::isInvalid($coercedValue)) {
// Note: ValuesOfCorrectType validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new Error(
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[$argumentValueNode]
);

throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
$referenceNode
);
} elseif ($hasValue) {
if ($argumentValueNode instanceof NullValueNode) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$name] = null;
} elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
Utils::invariant($variableValues !== null, 'Must exist for hasValue to be true.');
// Note: This does no further checking that this variable is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName] ?? null;
} else {
$valueNode = $argumentValueNode;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if (Utils::isInvalid($coercedValue)) {
// Note: ValuesOfCorrectType validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new Error(
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[$argumentValueNode]
);
}
$coercedValues[$name] = $coercedValue;
}
$coercedValues[$name] = $coercedValue;
}
}

Expand Down
11 changes: 8 additions & 3 deletions src/Utils/AST.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,14 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v
return $undefined;
}

// Note: we're not doing any checking that this variable is correct. We're
// assuming that this query has been validated and the variable usage here
// is of the correct type.
$variableValue = $variables[$variableName] ?? null;
if ($variableValue === null && $type instanceof NonNull) {
return $undefined; // Invalid: intentionally return no value.
}

// Note: This does no further checking that this variable is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
return $variables[$variableName];
}

Expand Down
47 changes: 36 additions & 11 deletions src/Utils/TypeInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class TypeInfo
/** @var SplStack<FieldDefinition> */
private $fieldDefStack;

/** @var SplStack<mixed> */
private $defaultValueStack;

/** @var Directive */
private $directive;

Expand All @@ -78,11 +81,13 @@ class TypeInfo
*/
public function __construct(Schema $schema, $initialType = null)
{
$this->schema = $schema;
$this->typeStack = [];
$this->parentTypeStack = [];
$this->inputTypeStack = [];
$this->fieldDefStack = [];
$this->schema = $schema;
$this->typeStack = [];
$this->parentTypeStack = [];
$this->inputTypeStack = [];
$this->fieldDefStack = [];
$this->defaultValueStack = [];

if (! $initialType) {
return;
}
Expand Down Expand Up @@ -322,6 +327,7 @@ public function enter(Node $node)
$fieldOrDirective = $this->getDirective() ?: $this->getFieldDef();
$argDef = $argType = null;
if ($fieldOrDirective) {
/** @var FieldArgument $argDef */
$argDef = Utils::find(
$fieldOrDirective->args,
static function ($arg) use ($node) {
Expand All @@ -332,28 +338,33 @@ static function ($arg) use ($node) {
$argType = $argDef->getType();
}
}
$this->argument = $argDef;
$this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
$this->argument = $argDef;
$this->defaultValueStack[] = $argDef && $argDef->defaultValueExists() ? $argDef->defaultValue : Utils::undefined();
$this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
break;

case $node instanceof ListValueNode:
$listType = Type::getNullableType($this->getInputType());
$itemType = $listType instanceof ListOfType
$listType = Type::getNullableType($this->getInputType());
$itemType = $listType instanceof ListOfType
? $listType->getWrappedType()
: $listType;
$this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
// List positions never have a default value.
$this->defaultValueStack[] = Utils::undefined();
$this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
break;

case $node instanceof ObjectFieldNode:
$objectType = Type::getNamedType($this->getInputType());
$fieldType = null;
$inputField = null;
$inputFieldType = null;
if ($objectType instanceof InputObjectType) {
$tmp = $objectType->getFields();
$inputField = $tmp[$node->name->value] ?? null;
$inputFieldType = $inputField ? $inputField->getType() : null;
}
$this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null;
$this->defaultValueStack[] = $inputField && $inputField->defaultValueExists() ? $inputField->defaultValue : Utils::undefined();
$this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null;
break;

case $node instanceof EnumValueNode:
Expand Down Expand Up @@ -456,6 +467,18 @@ public function getFieldDef()
return null;
}

/**
* @return mixed|null
*/
public function getDefaultValue()
{
if (! empty($this->defaultValueStack)) {
return $this->defaultValueStack[count($this->defaultValueStack) - 1];
}

return null;
}

/**
* @return ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null
*/
Expand Down Expand Up @@ -494,10 +517,12 @@ public function leave(Node $node)
break;
case $node instanceof ArgumentNode:
$this->argument = null;
array_pop($this->defaultValueStack);
array_pop($this->inputTypeStack);
break;
case $node instanceof ListValueNode:
case $node instanceof ObjectFieldNode:
array_pop($this->defaultValueStack);
array_pop($this->inputTypeStack);
break;
case $node instanceof EnumValueNode:
Expand Down
6 changes: 2 additions & 4 deletions src/Validator/DocumentValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
Expand All @@ -43,7 +43,6 @@
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
use Throwable;
use function array_filter;
Expand Down Expand Up @@ -160,8 +159,7 @@ public static function defaultRules()
KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
ValuesOfCorrectType::class => new ValuesOfCorrectType(),
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
ProvidedRequiredArguments::class => new ProvidedRequiredArguments(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
Expand Down
Loading

0 comments on commit bf4e7d4

Please sign in to comment.