diff --git a/composer.json b/composer.json index 3cfdf384863d2230dac948ae39bf5eaf4e009ec0..0dcea46a21c1f528333c818b6d25d793f5efcefb 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,9 @@ "psr-4": { "Grifart\\AssertFunction\\": "src" }, + "classmap": [ + "src/exceptions.php" + ], "files": [ "src/functions.php" ] @@ -26,7 +29,10 @@ "autoload-dev": { "psr-4": { "Grifart\\AssertFunction\\": "tests" - } + }, + "files": [ + "src/exceptions.php" + ] } } diff --git a/src/exceptions.php b/src/exceptions.php new file mode 100644 index 0000000000000000000000000000000000000000..ab8b81dee6d4795d8be3dc0d20b7c7b7989b2994 --- /dev/null +++ b/src/exceptions.php @@ -0,0 +1,38 @@ +<?php declare(strict_types=1); +namespace Grifart\AssertFunction; + +use Throwable; + +final class AssertFunctionException extends \LogicException { + + /** @internal use named constructors instead */ + public function __construct($message, \Throwable $previous = NULL) + { + parent::__construct($message, 0, $previous); + } + + private static function location(\ReflectionFunction $reflection): string + { + return $reflection->getFileName() . ':' . $reflection->getStartLine() . '-' . $reflection->getEndLine() . ' '; + } + + + public static function cannotStartReflection(\ReflectionException $exception): self + { + return new self('Cannot start reflection for given function.', 0, $exception); + } + + + public static function wrongNumberOrArguments(\ReflectionFunction $reflection, int $numberOfParameters, int $actualCount): self + { + return new self(self::location($reflection) . "Given wrong number of parameters. Expected $numberOfParameters, $actualCount given."); + } + + + public static function missingReturnType(\ReflectionFunction $reflection, string $type): self + { + return new self(self::location($reflection) . "Function is required to have return type of type $type."); + } +}; + +abstract class RuntimeException extends \RuntimeException {}; diff --git a/src/functions.php b/src/functions.php index aca22c98e3a7b639bee91a14ce1b8b0279aa7846..ed9a64f4d1851cae0630c5223e142ac282b8d70f 100644 --- a/src/functions.php +++ b/src/functions.php @@ -3,6 +3,11 @@ * 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 +// todo: covariance and contra-variance + namespace Grifart\AssertFunction; function parameters(string ...$params): array @@ -15,7 +20,68 @@ function optional(string $classType): string return '?' . $classType; } -function assertFunction(callable $fn, array $parameters, string $returnType): void +function assertFunction(callable $function, array $parameters, ?string $expectedReturnType): void +{ + try { + $reflection = new \ReflectionFunction($function); + } catch (\ReflectionException $exception) { + throw AssertFunctionException::cannotStartReflection($exception); + } + + // PARAMETERS CHECK: + $numberOfParameters = $reflection->getNumberOfParameters(); + if ($numberOfParameters !== count($parameters)) { + throw AssertFunctionException::wrongNumberOrArguments($reflection, count($parameters), $numberOfParameters); + } + + $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) { + + if (!$reflection->hasReturnType()) { + throw AssertFunctionException::missingReturnType($reflection, $expectedReturnType); + } + + list($_returnType, $_optional) = __parseType($expectedReturnType); + $returnTypeReflection = $reflection->getReturnType(); + + assert($_optional === $returnTypeReflection->allowsNull()); + assert($_returnType === (string) $reflection->getReturnType()); + } +} + +function __parseType(string $type): array +{ + $cleanType = $type; + $optional = FALSE; + if ($type[0] === '?') { + $optional = TRUE; + $cleanType = substr($type, 1); // without leading ? + } + 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); } diff --git a/tests/fn.assertFunction.allOptional.phpt b/tests/fn.assertFunction.allOptional.phpt new file mode 100644 index 0000000000000000000000000000000000000000..8438042821bd373885f7a15e5c454c614cc502d5 --- /dev/null +++ b/tests/fn.assertFunction.allOptional.phpt @@ -0,0 +1,32 @@ +<?php declare(strict_types=1); +namespace MyTestNamespace; +require __DIR__ . '/bootstrap.php'; +require __DIR__ . '/testClasses.php'; +use function Grifart\AssertFunction\{assertFunction, optional, parameters}; +use Tester\Assert; + +$fn = function(?T1 $t1, ?T2 $t2): ?T3 {return new T3;}; +assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), optional(T3::class)); + +// missing optional check +Assert::exception(function () use ($fn) { + assertFunction($fn, parameters(T1::class, optional(T2::class)), optional(T3::class)); +}, \AssertionError::class /* todo: assert message */); + +Assert::exception(function () use ($fn) { + assertFunction($fn, parameters(optional(T1::class), T2::class), optional(T3::class)); +}, \AssertionError::class /* todo: assert message */); + +Assert::exception(function () use ($fn) { + assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), T3::class); +}, \AssertionError::class /* todo: assert message */); + +// Wrong type return type +Assert::exception(function () use ($fn) { + assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), optional(T2::class)); +}, \AssertionError::class /* todo: assert message */); + +// Wrong parameter type +Assert::exception(function () use ($fn) { + assertFunction($fn, parameters(optional(T1::class), optional(T1::class)), optional(T3::class)); +}, \AssertionError::class /* todo: assert message */); diff --git a/tests/fn.assertFunction.allRequired.phpt b/tests/fn.assertFunction.allRequired.phpt new file mode 100644 index 0000000000000000000000000000000000000000..05f7078b00c66961c1a98765bbb886999b20db33 --- /dev/null +++ b/tests/fn.assertFunction.allRequired.phpt @@ -0,0 +1,20 @@ +<?php declare(strict_types=1); +namespace MyTestNamespace; +require __DIR__ . '/bootstrap.php'; +require __DIR__ . '/testClasses.php'; +use function Grifart\AssertFunction\{assertFunction, optional, parameters}; +use Tester\Assert; + + +$f1 = function(T1 $t1, T2 $t2): T3 {return new T3;}; +assertFunction($f1, parameters(T1::class, T2::class), T3::class); + +// Optional by accident: parameter +Assert::exception(function () use ($f1) { + assertFunction($f1, parameters(optional(T1::class), T2::class), T3::class); +}, \AssertionError::class); + +// Optional by accident: return type +Assert::exception(function () use ($f1) { + assertFunction($f1, parameters(T1::class, T2::class), optional(T3::class)); +}, \AssertionError::class); diff --git a/tests/fn.assertFunction.wrongNumberOfArguments.phpt b/tests/fn.assertFunction.wrongNumberOfArguments.phpt new file mode 100644 index 0000000000000000000000000000000000000000..198a8673d5ee705834366697f745c97f51d4ff79 --- /dev/null +++ b/tests/fn.assertFunction.wrongNumberOfArguments.phpt @@ -0,0 +1,18 @@ +<?php declare(strict_types=1); +namespace MyTestNamespace; +require __DIR__ . '/bootstrap.php'; +require __DIR__ . '/testClasses.php'; +use function Grifart\AssertFunction\{assertFunction, optional, parameters}; +use Tester\Assert; + + +$f1 = function(T1 $t1, T2 $t2): T3 {return new T3;}; + +// Wrong number of arguments +Assert::exception(function () use ($f1) { + assertFunction($f1, parameters(T1::class), T3::class); +}, \AssertionError::class); + +Assert::exception(function () use ($f1) { + assertFunction($f1, parameters(T1::class, T2::class, T1::class), T3::class); +}, \AssertionError::class); diff --git a/tests/php.ini b/tests/php.ini new file mode 100644 index 0000000000000000000000000000000000000000..a7a9ac23525c4a41d72aebc3e9bab522ba78875c --- /dev/null +++ b/tests/php.ini @@ -0,0 +1,7 @@ +# http://php.net/manual/en/function.assert.php +# default +zend.assertions = 1 + +# required by tests +assert.exception = 1 + diff --git a/tests/testClasses.php b/tests/testClasses.php new file mode 100644 index 0000000000000000000000000000000000000000..0bbb77d4acf1cb82644e99a7ea9544a4c1da800c --- /dev/null +++ b/tests/testClasses.php @@ -0,0 +1,7 @@ +<?php declare(strict_types=1); + +namespace MyTestNamespace; + +class T1 {}; +class T2 {}; +class T3 {};