diff --git a/src/DocBlock/Tags/Return_.php b/src/DocBlock/Tags/Return_.php index 1ae6632..6d60b1d 100644 --- a/src/DocBlock/Tags/Return_.php +++ b/src/DocBlock/Tags/Return_.php @@ -24,17 +24,12 @@ use function preg_split; /** * Reflection class for a {@}return tag in a Docblock. */ -final class Return_ extends BaseTag implements Factory\StaticMethod +final class Return_ extends TagWithType implements Factory\StaticMethod { - /** @var string */ - protected $name = 'return'; - - /** @var Type */ - private $type; - - public function __construct(Type $type, ?Description $description = null) + public function __construct(Type $type, Description $description = null) { - $this->type = $type; + $this->name = 'return'; + $this->type = $type; $this->description = $description; } @@ -50,7 +45,7 @@ final class Return_ extends BaseTag implements Factory\StaticMethod Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); - list($type, $description) = self::splitBodyIntoTypeAndTheRest($body); + list($type, $description) = self::extractTypeFromBody($body); $type = $typeResolver->resolve($type, $context); $description = $descriptionFactory->create($description, $context); @@ -58,41 +53,8 @@ final class Return_ extends BaseTag implements Factory\StaticMethod return new static($type, $description); } - /** - * Returns the type section of the variable. - */ - public function getType() : Type - { - return $this->type; - } - - public function __toString() : string + public function __toString() :string { return $this->type . ' ' . (string) $this->description; } - - private static function splitBodyIntoTypeAndTheRest(string $body) : array - { - $type = ''; - $nestingLevel = 0; - for ($i = 0; $i < strlen($body); $i++) { - $character = $body[$i]; - - if (trim($character) === '' && $nestingLevel === 0) { - break; - } - - $type .= $character; - if (in_array($character, ['<', '(', '[', '{'])) { - $nestingLevel++; - } - if (in_array($character, ['>', ')', ']', '}'])) { - $nestingLevel--; - } - } - - $description = trim(substr($body, strlen($type))); - - return [$type, $description]; - } } diff --git a/src/DocBlock/Tags/TagWithType.php b/src/DocBlock/Tags/TagWithType.php new file mode 100644 index 0000000..e75ea32 --- /dev/null +++ b/src/DocBlock/Tags/TagWithType.php @@ -0,0 +1,59 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\DocBlock\Tags; + +use phpDocumentor\Reflection\Type; + +abstract class TagWithType extends BaseTag +{ + /** @var Type */ + protected $type; + + /** + * Returns the type section of the variable. + * + * @return Type + */ + public function getType() + { + return $this->type; + } + + protected static function extractTypeFromBody(string $body) : array + { + $type = ''; + $nestingLevel = 0; + for ($i = 0; $i < strlen($body); $i++) { + $character = $body[$i]; + + if (trim($character) === '' && $nestingLevel === 0) { + break; + } + + $type .= $character; + if (in_array($character, ['<', '(', '[', '{'])) { + $nestingLevel++; + } + if (in_array($character, ['>', ')', ']', '}'])) { + $nestingLevel--; + } + } + + $description = trim(substr($body, strlen($type))); + + return [$type, $description]; + } +} diff --git a/src/DocBlock/Tags/Throws.php b/src/DocBlock/Tags/Throws.php index 1a2328d..dc86c63 100644 --- a/src/DocBlock/Tags/Throws.php +++ b/src/DocBlock/Tags/Throws.php @@ -24,17 +24,12 @@ use function preg_split; /** * Reflection class for a {@}throws tag in a Docblock. */ -final class Throws extends BaseTag implements Factory\StaticMethod +final class Throws extends TagWithType implements Factory\StaticMethod { - /** @var string */ - protected $name = 'throws'; - - /** @var Type */ - private $type; - - public function __construct(Type $type, ?Description $description = null) + public function __construct(Type $type, Description $description = null) { - $this->type = $type; + $this->name = 'throws'; + $this->type = $type; $this->description = $description; } @@ -50,23 +45,14 @@ final class Throws extends BaseTag implements Factory\StaticMethod Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); - $parts = preg_split('/\s+/Su', $body, 2); - Assert::isArray($parts); + list($type, $description) = self::extractTypeFromBody($body); - $type = $typeResolver->resolve($parts[0] ?? '', $context); - $description = $descriptionFactory->create($parts[1] ?? '', $context); + $type = $typeResolver->resolve($type, $context); + $description = $descriptionFactory->create($description, $context); return new static($type, $description); } - /** - * Returns the type section of the variable. - */ - public function getType() : Type - { - return $this->type; - } - public function __toString() : string { return (string) $this->type . ' ' . (string) $this->description; diff --git a/tests/unit/DocBlock/Tags/ThrowsTest.php b/tests/unit/DocBlock/Tags/ThrowsTest.php index b4dec5e..a5745e8 100644 --- a/tests/unit/DocBlock/Tags/ThrowsTest.php +++ b/tests/unit/DocBlock/Tags/ThrowsTest.php @@ -134,10 +134,10 @@ class ThrowsTest extends TestCase public function testFactoryMethod() : void { $descriptionFactory = m::mock(DescriptionFactory::class); - $resolver = new TypeResolver(); - $context = new Context(''); + $resolver = new TypeResolver(); + $context = new Context(''); - $type = new String_(); + $type = new String_(); $description = new Description('My Description'); $descriptionFactory->shouldReceive('create')->with('My Description', $context)->andReturn($description); @@ -148,6 +148,96 @@ class ThrowsTest extends TestCase $this->assertSame($description, $fixture->getDescription()); } + /** + * This test checks whether a braces in a Type are allowed. + * + * The advent of generics poses a few issues, one of them is that spaces can now be part of a type. In the past we + * could purely rely on spaces to split the individual parts of the body of a tag; but when there is a type in play + * we now need to check for braces. + * + * This test tests whether an error occurs demonstrating that the braces were taken into account; this test is still + * expected to produce an exception because the TypeResolver does not support generics. + * + * @covers ::create + * @uses \phpDocumentor\Reflection\DocBlock\Tags\Throws:: + * @uses \phpDocumentor\Reflection\DocBlock\DescriptionFactory + * @uses \phpDocumentor\Reflection\TypeResolver + * @uses \phpDocumentor\Reflection\DocBlock\Description + * @uses \phpDocumentor\Reflection\Types\String_ + * @uses \phpDocumentor\Reflection\Types\Context + */ + public function testFactoryMethodWithGenericWithSpace() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('"\array" is not a valid Fqsen.'); + + $descriptionFactory = m::mock(DescriptionFactory::class); + $resolver = new TypeResolver(); + $context = new Context(''); + + $description = new Description('My Description'); + $descriptionFactory->shouldReceive('create') + ->with('My Description', $context) + ->andReturn($description); + + Throws::create('array My Description', $resolver, $descriptionFactory, $context); + } + + /** + * @see self::testFactoryMethodWithGenericWithSpace() + * + * @covers ::create + * @uses \phpDocumentor\Reflection\DocBlock\Tags\Throws:: + * @uses \phpDocumentor\Reflection\DocBlock\DescriptionFactory + * @uses \phpDocumentor\Reflection\TypeResolver + * @uses \phpDocumentor\Reflection\DocBlock\Description + * @uses \phpDocumentor\Reflection\Types\String_ + * @uses \phpDocumentor\Reflection\Types\Context + */ + public function testFactoryMethodWithGenericWithSpaceAndAddedEmojisToVerifyMultiByteBehaviour() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('"\array😁" is not a valid Fqsen.'); + + $descriptionFactory = m::mock(DescriptionFactory::class); + $resolver = new TypeResolver(); + $context = new Context(''); + + $description = new Description('My Description'); + $descriptionFactory->shouldReceive('create') + ->with('My Description', $context) + ->andReturn($description); + + Throws::create('array😁 My Description', $resolver, $descriptionFactory, $context); + } + + /** + * @covers ::create + * @uses \phpDocumentor\Reflection\DocBlock\Tags\Throws:: + * @uses \phpDocumentor\Reflection\DocBlock\DescriptionFactory + * @uses \phpDocumentor\Reflection\TypeResolver + * @uses \phpDocumentor\Reflection\DocBlock\Description + * @uses \phpDocumentor\Reflection\Types\String_ + * @uses \phpDocumentor\Reflection\Types\Context + */ + public function testFactoryMethodWithEmojisToVerifyMultiByteBehaviour() + { + $descriptionFactory = m::mock(DescriptionFactory::class); + $resolver = new TypeResolver(); + $context = new Context(''); + + $description = new Description('My Description'); + $descriptionFactory->shouldReceive('create') + ->with('My Description', $context) + ->andReturn($description); + + $fixture = Throws::create('\My😁Class My Description', $resolver, $descriptionFactory, $context); + + $this->assertSame('\My😁Class My Description', (string) $fixture); + $this->assertEquals('\My😁Class', $fixture->getType()); + $this->assertSame($description, $fixture->getDescription()); + } + /** * @covers ::create */