diff --git a/src/SignatureAssertionUtil.php b/src/SignatureAssertionUtil.php new file mode 100644 index 0000000000000000000000000000000000000000..d037290af92a7c0ed40e700f1fab7306772baa61 --- /dev/null +++ b/src/SignatureAssertionUtil.php @@ -0,0 +1,112 @@ +<?php declare(strict_types=1); +/** + * This file is part of assert-function-signature. + */ + +namespace Grifart\AssertFunction; + +final class SignatureAssertionUtil +{ + private function __construct() + { + } + + + public static function checkSignature(callable $function, array $parameters, ?string $expectedReturnType): void + { + $reflection = new \ReflectionFunction($function); + + // NUMBER OF PARAMETERS CHECK: + $numberOfParameters = $reflection->getNumberOfParameters(); + if($numberOfParameters !== count($parameters)) { + throw FunctionAssertionError::wrongNumberOrArguments($reflection, count($parameters), $numberOfParameters); + } + + // PARAMETER TYPES CHECK: + $i = 0; + /** @var string[] $parameters */ + foreach($parameters AS $parameter) { + assert(is_string($parameter)); + + $parameterReflection = $reflection->getParameters()[$i++]; + assert($parameterReflection instanceof \ReflectionParameter); + + self::checkFunctionParameter( + $reflection, + $parameterReflection, + ...self::parseType($parameter) + ); + } + + + // RETURN TYPE: + if($expectedReturnType !== NULL) { + if(!$reflection->hasReturnType()) { + throw FunctionAssertionError::missingReturnType($reflection, $expectedReturnType); + } + + if ($reflection->hasReturnType()) { + list($expectedReturnType, $expectingNullable) = self::parseType($expectedReturnType); + $returnTypeReflection = $reflection->getReturnType(); + + $actuallyNullable = $returnTypeReflection->allowsNull(); + if ($expectingNullable !== $actuallyNullable) { + throw FunctionAssertionError::wrongReturnTypeNullability( + $reflection, + $expectingNullable, + $actuallyNullable + ); + } + if ($expectedReturnType !== (string) $reflection->getReturnType()) { + throw FunctionAssertionError::wrongReturnType( + $reflection, + $expectedReturnType, + (string) $reflection->getReturnType() + ); + } + } + } + } + + private static function parseType(string $type): array + { + $cleanType = $type; // e.g. ?Namespace\Class + $optional = FALSE; + if ($type[0] === '?') { + $optional = TRUE; + $cleanType = substr($type, 1); // without leading '?' + } + return [$cleanType, $optional]; + } + + private static function checkFunctionParameter( + \ReflectionFunction $functionReflection, + \ReflectionParameter $parameterReflection, + string $expectedParameterType, + bool $expectedOptional + ): void + { + $reflectionType = $parameterReflection->getType(); + assert($reflectionType instanceof \ReflectionType); + + // checks: + $actuallyOptional = $reflectionType->allowsNull(); + if ($expectedOptional !== $actuallyOptional) { + throw FunctionAssertionError::parameterNullability( + $functionReflection, + $parameterReflection, + $expectedOptional, + $actuallyOptional + ); + } + if ($expectedParameterType !== (string) $reflectionType) { + throw FunctionAssertionError::wrongParameterType( + $functionReflection, + $parameterReflection, + $expectedParameterType, + $reflectionType + ); + } + } + +} diff --git a/src/exceptions.php b/src/exceptions.php index dcfe4f9dc827b5b989018a018b6059d85d8761e8..f1d214b8d8737e54f19abe2b65c033808d722b50 100644 --- a/src/exceptions.php +++ b/src/exceptions.php @@ -48,4 +48,44 @@ final class FunctionAssertionError extends \AssertionError { return implode('/', $pathParts); // normalized shortened readable path } + + + public static function parameterNullability(\ReflectionFunction $functionReflection, \ReflectionParameter $param, bool $expectedOptional, bool $actuallyOptional): self + { + return new self( + $functionReflection, + $expectedOptional + ? "Function should have parameter {$param->getName()} optional but it is not." + : "Function should have parameter {$param->getName()} required but it is not." + ); + } + + + public static function wrongParameterType(\ReflectionFunction $functionReflection, \ReflectionParameter $parameterReflection, string $expectedParameterType, \ReflectionType $actualParameterType): self + { + return new self( + $functionReflection, + "Wrong parameter type given in function {$parameterReflection}. {$expectedParameterType} expected. {$actualParameterType} defined in function." + ); + } + + + public static function wrongReturnTypeNullability(\ReflectionFunction $reflectionFunction, bool $expectingNullable, bool $actuallyNullable): self + { + return new self( + $reflectionFunction, + $expectingNullable + ? 'Wrong return type nullability. Expecting nullable, but was required.' + : 'Wrong return type nullability. Expecting required, but was nullable.' + ); + } + + + public static function wrongReturnType(\ReflectionFunction $reflection, string $expectedReturnType, string $actualReturnType): self + { + return new self( + $reflection, + "Expected return type of type '$expectedReturnType', but function declares '$actualReturnType'" + ); + } }; diff --git a/src/functions.php b/src/functions.php index 7eaf202f4f5e687bb599334eb61c483d13d80e14..54821f76e41c767bc035358282dc4632784dddfa 100644 --- a/src/functions.php +++ b/src/functions.php @@ -3,9 +3,7 @@ * Used to load functions. */ -// todo: use only assert( ... ) instead of exceptions; to be compatible without configuration with PHP env -// todo: [performance] separate code itself into autoloaded class -// todo: better assertion messages + throw FunctionAssertionError + catch in fn + pass it to assert -> compatible with PHP assert config +// todo: TESTS: better assertion messages + throw FunctionAssertionError + catch in fn + pass it to assert -> compatible with PHP assert config // todo: covariance and contra-variance namespace Grifart\AssertFunction; @@ -22,71 +20,16 @@ function nullable(string $classType): string function assertSignature(callable $function, array $parameters, ?string $expectedReturnType): void { - // todo: call assert(__impl()); - $reflection = new \ReflectionFunction($function); - - // NUMBER OF PARAMETERS CHECK: - $numberOfParameters = $reflection->getNumberOfParameters(); - assert( - $numberOfParameters === count($parameters), - FunctionAssertionError::wrongNumberOrArguments($reflection, count($parameters), $numberOfParameters) - ); - if ($numberOfParameters !== count($parameters)) { - return; - } - - - // PARAMETER TYPES CHECK: - $i = 0; - /** @var string[] $parameters */ - foreach($parameters AS $parameter) { - assert(is_string($parameter)); - - $parameterReflection = $reflection->getParameters()[$i++]; - assert($parameterReflection instanceof \ReflectionParameter); - - __checkFunctionParameter($parameterReflection, ...__parseType($parameter)); // todo: move to helper class - } - - - // RETURN TYPE: - if($expectedReturnType !== NULL) { - assert( - $reflection->hasReturnType(), - FunctionAssertionError::missingReturnType($reflection, $expectedReturnType) + try { + SignatureAssertionUtil::checkSignature( + $function, + $parameters, + $expectedReturnType ); - if ($reflection->hasReturnType()) { - list($_returnType, $_optional) = __parseType($expectedReturnType); - $returnTypeReflection = $reflection->getReturnType(); - - assert($_optional === $returnTypeReflection->allowsNull()); - assert($_returnType === (string) $reflection->getReturnType()); - } - } -} - -function __parseType(string $type): array -{ - $cleanType = $type; // e.g. ?Namespace\Class - $optional = FALSE; - if ($type[0] === '?') { - $optional = TRUE; - $cleanType = substr($type, 1); // without leading '?' + } catch (FunctionAssertionError $error) { + // todo: add callee position + assert(FALSE, $error); } - return [$cleanType, $optional]; -} - -function __checkFunctionParameter( - \ReflectionParameter $parameterReflection, - string $parameterType, - bool $optional -): void -{ - $reflectionType = $parameterReflection->getType(); - assert($reflectionType instanceof \ReflectionType); - // checks: - assert($optional === $reflectionType->allowsNull()); - assert($parameterType === (string) $reflectionType); }