diff --git a/.travis.yml b/.travis.yml index aa33b344..87d98537 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,15 @@ matrix: - php: 7 env: - DEPS=latest + - php: 7.1 + env: + - DEPS=lowest + - php: 7.1 + env: + - DEPS=locked + - php: 7.1 + env: + - DEPS=latest - php: hhvm env: - DEPS=lowest @@ -65,7 +74,7 @@ notifications: before_install: - travis_retry composer self-update - - if [[ $TRAVIS_PHP_VERSION != "hhvm" && $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini ; fi + - if [[ $TRAVIS_PHP_VERSION != "hhvm" && $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || true ; fi install: - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi diff --git a/src/Generator/MethodGenerator.php b/src/Generator/MethodGenerator.php index bc489fd4..59af8221 100644 --- a/src/Generator/MethodGenerator.php +++ b/src/Generator/MethodGenerator.php @@ -9,6 +9,7 @@ namespace Zend\Code\Generator; +use ReflectionMethod; use Zend\Code\Reflection\MethodReflection; class MethodGenerator extends AbstractMemberGenerator @@ -389,12 +390,30 @@ private static function extractReturnTypeFromMethodReflection(MethodReflection $ return null; } - $returnTypeString = (string) $returnType; + if (! method_exists($returnType, 'getName')) { + return self::expandLiteralType((string) $returnType, $methodReflection); + } + + return ($returnType->allowsNull() ? '?' : '') + . self::expandLiteralType($returnType->getName(), $methodReflection); + } - if ('self' === strtolower($returnType)) { + /** + * @param string $literalReturnType + * @param ReflectionMethod $methodReflection + * + * @return string + */ + private static function expandLiteralType($literalReturnType, ReflectionMethod $methodReflection) + { + if ('self' === strtolower($literalReturnType)) { return $methodReflection->getDeclaringClass()->getName(); } - return $returnTypeString; + if ('parent' === strtolower($literalReturnType)) { + return $methodReflection->getDeclaringClass()->getParentClass()->getName(); + } + + return $literalReturnType; } } diff --git a/src/Generator/ParameterGenerator.php b/src/Generator/ParameterGenerator.php index 4c12642f..f5f2bc2a 100644 --- a/src/Generator/ParameterGenerator.php +++ b/src/Generator/ParameterGenerator.php @@ -9,6 +9,7 @@ namespace Zend\Code\Generator; +use ReflectionParameter; use Zend\Code\Reflection\ParameterReflection; class ParameterGenerator extends AbstractGenerator @@ -334,14 +335,12 @@ private static function extractFQCNTypeFromReflectionType(ParameterReflection $r return null; } - $typeString = (string) $type; - - if ('self' === strtolower($typeString)) { - // exceptional case: `self` must expand to the reflection type declaring class - return $reflectionParameter->getDeclaringClass()->getName(); + if (! method_exists($type, 'getName')) { + return self::expandLiteralParameterType((string) $type, $reflectionParameter); } - return $typeString; + return ($type->allowsNull() ? '?' : '') + . self::expandLiteralParameterType($type->getName(), $reflectionParameter); } /** @@ -368,6 +367,25 @@ private static function prePhp7ExtractFQCNTypeFromReflectionType(ParameterReflec return null; } + /** + * @param string $literalParameterType + * @param ReflectionParameter $reflectionParameter + * + * @return string + */ + private static function expandLiteralParameterType($literalParameterType, ReflectionParameter $reflectionParameter) + { + if ('self' === strtolower($literalParameterType)) { + return $reflectionParameter->getDeclaringClass()->getName(); + } + + if ('parent' === strtolower($literalParameterType)) { + return $reflectionParameter->getDeclaringClass()->getParentClass()->getName(); + } + + return $literalParameterType; + } + /** * @param string|null $type * diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 477355ad..327da067 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -23,19 +23,23 @@ final class TypeGenerator implements GeneratorInterface */ private $type; + /** + * @var bool + */ + private $nullable; + /** * @var string[] * * @link http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration */ - private static $internalPhpTypes = ['int', 'float', 'string', 'bool', 'array', 'callable']; + private static $internalPhpTypes = ['void', 'int', 'float', 'string', 'bool', 'array', 'callable', 'iterable']; - // @codingStandardsIgnoreStart /** * @var string a regex pattern to match valid class names or types */ - private static $validIdentifierMatcher = '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/'; - // @codingStandardsIgnoreEnd + private static $validIdentifierMatcher = '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*' + . '(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/'; /** * @param string $type @@ -46,7 +50,8 @@ final class TypeGenerator implements GeneratorInterface */ public static function fromTypeString($type) { - list($wasTrimmed, $trimmedType) = self::trimType($type); + list($nullable, $trimmedNullable) = self::trimNullable($type); + list($wasTrimmed, $trimmedType) = self::trimType($trimmedNullable); if (! preg_match(self::$validIdentifierMatcher, $trimmedType)) { throw new InvalidArgumentException(sprintf( @@ -65,9 +70,14 @@ public static function fromTypeString($type) )); } + if ($nullable && $isInternalPhpType && 'void' === strtolower($trimmedType)) { + throw new InvalidArgumentException(sprintf('Provided type "%s" cannot be nullable', $type)); + } + $instance = new self(); $instance->type = $trimmedType; + $instance->nullable = $nullable; $instance->isInternalPhpType = self::isInternalPhpType($trimmedType); return $instance; @@ -82,11 +92,13 @@ private function __construct() */ public function generate() { + $nullable = $this->nullable ? '?' : ''; + if ($this->isInternalPhpType) { - return strtolower($this->type); + return $nullable . strtolower($this->type); } - return '\\' . $this->type; + return $nullable . '\\' . $this->type; } /** @@ -94,14 +106,29 @@ public function generate() */ public function __toString() { - return ltrim($this->generate(), '\\'); + return ltrim($this->generate(), '?\\'); + } + + /** + * @param string $type + * + * @return bool[]|string[] ordered tuple, first key represents whether the type is nullable, second is the + * trimmed string + */ + private static function trimNullable($type) + { + if (0 === strpos($type, '?')) { + return [true, substr($type, 1)]; + } + + return [false, $type]; } /** * @param string $type * - * @return bool[]|int[] ordered tuple, first key represents whether the values was trimmed, second is the - * trimmed string + * @return bool[]|string[] ordered tuple, first key represents whether the values was trimmed, second is the + * trimmed string */ private static function trimType($type) { diff --git a/test/Generator/MethodGeneratorTest.php b/test/Generator/MethodGeneratorTest.php index 0d9d3ea9..fcb72dc1 100644 --- a/test/Generator/MethodGeneratorTest.php +++ b/test/Generator/MethodGeneratorTest.php @@ -14,7 +14,10 @@ use Zend\Code\Generator\ValueGenerator; use Zend\Code\Reflection\MethodReflection; use ZendTest\Code\TestAsset\ClassWithByRefReturnMethod; +use ZendTest\Code\TestAsset\EmptyClass; use ZendTest\Code\TestAsset\InternalHintsClass; +use ZendTest\Code\TestAsset\IterableHintsClass; +use ZendTest\Code\TestAsset\NullableReturnTypeHintedClass; use ZendTest\Code\TestAsset\ReturnTypeHintedClass; /** @@ -338,7 +341,8 @@ public function testFrom(string $className, string $methodName, string $expected public function returnTypeHintClassesProvider() { - return [ + $parameters = [ + [ReturnTypeHintedClass::class, 'voidReturn', 'void'], [ReturnTypeHintedClass::class, 'arrayReturn', 'array'], [ReturnTypeHintedClass::class, 'callableReturn', 'callable'], [ReturnTypeHintedClass::class, 'intReturn', 'int'], @@ -346,9 +350,33 @@ public function returnTypeHintClassesProvider() [ReturnTypeHintedClass::class, 'stringReturn', 'string'], [ReturnTypeHintedClass::class, 'boolReturn', 'bool'], [ReturnTypeHintedClass::class, 'selfReturn', '\\' . ReturnTypeHintedClass::class], + [ReturnTypeHintedClass::class, 'parentReturn', '\\' . EmptyClass::class], [ReturnTypeHintedClass::class, 'classReturn', '\\' . ReturnTypeHintedClass::class], [ReturnTypeHintedClass::class, 'otherClassReturn', '\\' . InternalHintsClass::class], + [NullableReturnTypeHintedClass::class, 'arrayReturn', '?array'], + [NullableReturnTypeHintedClass::class, 'callableReturn', '?callable'], + [NullableReturnTypeHintedClass::class, 'intReturn', '?int'], + [NullableReturnTypeHintedClass::class, 'floatReturn', '?float'], + [NullableReturnTypeHintedClass::class, 'stringReturn', '?string'], + [NullableReturnTypeHintedClass::class, 'boolReturn', '?bool'], + [NullableReturnTypeHintedClass::class, 'selfReturn', '?\\' . NullableReturnTypeHintedClass::class], + [NullableReturnTypeHintedClass::class, 'parentReturn', '?\\' . EmptyClass::class], + [NullableReturnTypeHintedClass::class, 'classReturn', '?\\' . NullableReturnTypeHintedClass::class], + [NullableReturnTypeHintedClass::class, 'otherClassReturn', '?\\' . InternalHintsClass::class], + [IterableHintsClass::class, 'iterableReturnValue', 'iterable'], + [IterableHintsClass::class, 'nullableIterableReturnValue', '?iterable'], ]; + + return array_filter( + $parameters, + function (array $parameter) { + return PHP_VERSION_ID >= 70100 + || ( + false === strpos($parameter[2], '?') + && ! in_array(strtolower($parameter[2]), ['void', 'iterable']) + ); + } + ); } /** diff --git a/test/Generator/ParameterGeneratorTest.php b/test/Generator/ParameterGeneratorTest.php index ebb2e558..8e4ca443 100644 --- a/test/Generator/ParameterGeneratorTest.php +++ b/test/Generator/ParameterGeneratorTest.php @@ -16,7 +16,11 @@ use ZendTest\Code\Generator\TestAsset\ParameterClass; use ZendTest\Code\TestAsset\ClassTypeHintedClass; use ZendTest\Code\TestAsset\DocBlockOnlyHintsClass; +use ZendTest\Code\TestAsset\EmptyClass; use ZendTest\Code\TestAsset\InternalHintsClass; +use ZendTest\Code\TestAsset\IterableHintsClass; +use ZendTest\Code\TestAsset\NullableHintsClass; +use ZendTest\Code\TestAsset\NullNullableDefaultHintsClass; use ZendTest\Code\TestAsset\VariadicParametersClass; /** @@ -359,7 +363,7 @@ public function validClassNameProvider() * * @requires PHP 7.0 * - * @dataProvider internalReflectionHintsProvider + * @dataProvider reflectionHintsProvider * * @param string $className * @param string $methodName @@ -373,13 +377,47 @@ public function testTypeHintFromReflection($className, $methodName, $parameterNa $parameterName )); - self::assertSame($expectedType, $parameter->getType()); + if (null === $expectedType) { + self::assertNull($parameter->getType()); + + return; + } + + self::assertSame(ltrim($expectedType, '?\\'), $parameter->getType()); + } + + /** + * @group zendframework/zend-code#29 + * + * @requires PHP 7.0 + * + * @dataProvider reflectionHintsProvider + * + * @param string $className + * @param string $methodName + * @param string $parameterName + * @param string|null $expectedType + */ + public function testTypeHintFromReflectionGeneratedCode($className, $methodName, $parameterName, $expectedType) + { + $parameter = ParameterGenerator::fromReflection(new ParameterReflection( + [$className, $methodName], + $parameterName + )); + + if (null === $expectedType) { + self::assertStringStartsWith('$' . $parameterName, $parameter->generate()); + + return; + } + + self::assertStringStartsWith($expectedType . ' $' . $parameterName, $parameter->generate()); } /** * @return string[][] */ - public function internalReflectionHintsProvider() + public function reflectionHintsProvider() { $parameters = [ [InternalHintsClass::class, 'arrayParameter', 'foo', 'array'], @@ -388,11 +426,40 @@ public function internalReflectionHintsProvider() [InternalHintsClass::class, 'floatParameter', 'foo', 'float'], [InternalHintsClass::class, 'stringParameter', 'foo', 'string'], [InternalHintsClass::class, 'boolParameter', 'foo', 'bool'], - [ClassTypeHintedClass::class, 'selfParameter', 'foo', ClassTypeHintedClass::class], - [ClassTypeHintedClass::class, 'classParameter', 'foo', ClassTypeHintedClass::class], - [ClassTypeHintedClass::class, 'otherClassParameter', 'foo', InternalHintsClass::class], - [ClassTypeHintedClass::class, 'closureParameter', 'foo', \Closure::class], - [ClassTypeHintedClass::class, 'importedClosureParameter', 'foo', \Closure::class], + [NullableHintsClass::class, 'arrayParameter', 'foo', '?array'], + [NullableHintsClass::class, 'callableParameter', 'foo', '?callable'], + [NullableHintsClass::class, 'intParameter', 'foo', '?int'], + [NullableHintsClass::class, 'floatParameter', 'foo', '?float'], + [NullableHintsClass::class, 'stringParameter', 'foo', '?string'], + [NullableHintsClass::class, 'boolParameter', 'foo', '?bool'], + [NullableHintsClass::class, 'selfParameter', 'foo', '?\\' . NullableHintsClass::class], + [NullableHintsClass::class, 'parentParameter', 'foo', '?\\' . EmptyClass::class], + [NullableHintsClass::class, 'nullableHintsClassParameter', 'foo', '?\\' . NullableHintsClass::class], + [NullNullableDefaultHintsClass::class, 'arrayParameter', 'foo', '?array'], + [NullNullableDefaultHintsClass::class, 'callableParameter', 'foo', '?callable'], + [NullNullableDefaultHintsClass::class, 'intParameter', 'foo', '?int'], + [NullNullableDefaultHintsClass::class, 'floatParameter', 'foo', '?float'], + [NullNullableDefaultHintsClass::class, 'stringParameter', 'foo', '?string'], + [NullNullableDefaultHintsClass::class, 'boolParameter', 'foo', '?bool'], + [ + NullNullableDefaultHintsClass::class, + 'selfParameter', + 'foo', + '?\\' . NullNullableDefaultHintsClass::class, + ], + [NullNullableDefaultHintsClass::class, 'parentParameter', 'foo', '?\\' . EmptyClass::class], + [ + NullNullableDefaultHintsClass::class, + 'nullableDefaultHintsClassParameter', + 'foo', + '?\\' . NullNullableDefaultHintsClass::class, + ], + [ClassTypeHintedClass::class, 'selfParameter', 'foo', '\\' . ClassTypeHintedClass::class], + [ClassTypeHintedClass::class, 'parentParameter', 'foo', '\\' . EmptyClass::class], + [ClassTypeHintedClass::class, 'classParameter', 'foo', '\\' . ClassTypeHintedClass::class], + [ClassTypeHintedClass::class, 'otherClassParameter', 'foo', '\\' . InternalHintsClass::class], + [ClassTypeHintedClass::class, 'closureParameter', 'foo', '\\' . \Closure::class], + [ClassTypeHintedClass::class, 'importedClosureParameter', 'foo', '\\' . \Closure::class], [DocBlockOnlyHintsClass::class, 'arrayParameter', 'foo', null], [DocBlockOnlyHintsClass::class, 'callableParameter', 'foo', null], [DocBlockOnlyHintsClass::class, 'intParameter', 'foo', null], @@ -402,17 +469,28 @@ public function internalReflectionHintsProvider() [DocBlockOnlyHintsClass::class, 'selfParameter', 'foo', null], [DocBlockOnlyHintsClass::class, 'classParameter', 'foo', null], [DocBlockOnlyHintsClass::class, 'otherClassParameter', 'foo', null], + [IterableHintsClass::class, 'iterableParameter', 'foo', 'iterable'], + [IterableHintsClass::class, 'nullableIterableParameter', 'foo', '?iterable'], + [IterableHintsClass::class, 'nullDefaultIterableParameter', 'foo', '?iterable'], ]; + $compatibleParameters = array_filter( + $parameters, + function (array $parameter) { + return PHP_VERSION_ID >= 70100 + || (false === strpos($parameter[3], '?') && 'iterable' !== strtolower($parameter[3])); + } + ); + // just re-organizing the keys so that the phpunit data set makes sense in errors: return array_combine( array_map( function (array $definition) { return $definition[0] . '#' . $definition[1]; }, - $parameters + $compatibleParameters ), - $parameters + $compatibleParameters ); } diff --git a/test/Generator/TypeGeneratorTest.php b/test/Generator/TypeGeneratorTest.php index 7e3483c8..8c060145 100644 --- a/test/Generator/TypeGeneratorTest.php +++ b/test/Generator/TypeGeneratorTest.php @@ -50,7 +50,7 @@ public function testStringCastFromValidTypeString(string $typeString, string $ex { $generator = TypeGenerator::fromTypeString($typeString); - self::assertSame(ltrim($expectedReturnType, '\\'), (string) $generator); + self::assertSame(ltrim($expectedReturnType, '?\\'), (string) $generator); } /** @@ -84,7 +84,7 @@ public function testRejectsInvalidTypeString(string $typeString) */ public function validTypeProvider() { - return [ + $valid = [ ['foo', '\\foo'], ['foo', '\\foo'], ['foo1', '\\foo1'], @@ -94,6 +94,9 @@ public function validTypeProvider() ['foo\\bar\\baz1', '\\foo\\bar\\baz1'], ['FOO', '\\FOO'], ['FOO1', '\\FOO1'], + ['void', 'void'], + ['Void', 'void'], + ['VOID', 'void'], ['array', 'array'], ['Array', 'array'], ['ARRAY', 'array'], @@ -112,6 +115,9 @@ public function validTypeProvider() ['bool', 'bool'], ['Bool', 'bool'], ['BOOL', 'bool'], + ['iterable', 'iterable'], + ['Iterable', 'iterable'], + ['ITERABLE', 'iterable'], ['object', '\\object'], ['Object', '\\Object'], ['OBJECT', '\\OBJECT'], @@ -122,9 +128,54 @@ public function validTypeProvider() ['Resource', '\\Resource'], ['RESOURCE', '\\RESOURCE'], ['foo_bar', '\\foo_bar'], + ['?foo', '?\\foo'], + ['?foo', '?\\foo'], + ['?foo1', '?\\foo1'], + ['?foo\\bar', '?\\foo\\bar'], + ['?a\\b\\c', '?\\a\\b\\c'], + ['?foo\\bar\\baz', '?\\foo\\bar\\baz'], + ['?foo\\bar\\baz1', '?\\foo\\bar\\baz1'], + ['?FOO', '?\\FOO'], + ['?FOO1', '?\\FOO1'], + ['?array', '?array'], + ['?Array', '?array'], + ['?ARRAY', '?array'], + ['?callable', '?callable'], + ['?Callable', '?callable'], + ['?CALLABLE', '?callable'], + ['?string', '?string'], + ['?String', '?string'], + ['?STRING', '?string'], + ['?int', '?int'], + ['?Int', '?int'], + ['?INT', '?int'], + ['?float', '?float'], + ['?Float', '?float'], + ['?FLOAT', '?float'], + ['?bool', '?bool'], + ['?Bool', '?bool'], + ['?BOOL', '?bool'], + ['?iterable', '?iterable'], + ['?Iterable', '?iterable'], + ['?ITERABLE', '?iterable'], + ['?object', '?\\object'], + ['?Object', '?\\Object'], + ['?OBJECT', '?\\OBJECT'], + ['?mixed', '?\\mixed'], + ['?Mixed', '?\\Mixed'], + ['?MIXED', '?\\MIXED'], + ['?resource', '?\\resource'], + ['?Resource', '?\\Resource'], + ['?RESOURCE', '?\\RESOURCE'], + ['?foo_bar', '?\\foo_bar'], ["\x80", "\\\x80"], ["\x80\\\x80", "\\\x80\\\x80"], ]; + + return array_combine( + array_map('reset', $valid), + $valid + ); } /** @@ -147,7 +198,7 @@ function (array $pair) { */ public function invalidTypeProvider() { - return [ + $invalid = [ [''], ['\\'], ['\\\\'], @@ -182,6 +233,20 @@ public function invalidTypeProvider() ['\\bool'], ['\\Bool'], ['\\BOOL'], + ['\\void'], + ['\\Void'], + ['\\VOID'], + ['?void'], + ['?Void'], + ['?VOID'], + ['\\iterable'], + ['\\Iterable'], + ['\\ITERABLE'], ]; + + return array_combine( + array_map('reset', $invalid), + $invalid + ); } } diff --git a/test/TestAsset/ClassTypeHintedClass.php b/test/TestAsset/ClassTypeHintedClass.php index 53281ff5..3fd6d876 100644 --- a/test/TestAsset/ClassTypeHintedClass.php +++ b/test/TestAsset/ClassTypeHintedClass.php @@ -4,12 +4,16 @@ use Closure; -class ClassTypeHintedClass +class ClassTypeHintedClass extends EmptyClass { public function selfParameter(self $foo) { } + public function parentParameter(parent $foo) + { + } + public function classParameter(ClassTypeHintedClass $foo) { } diff --git a/test/TestAsset/EmptyClass.php b/test/TestAsset/EmptyClass.php new file mode 100644 index 00000000..96330ae1 --- /dev/null +++ b/test/TestAsset/EmptyClass.php @@ -0,0 +1,7 @@ +