From 34ab54aaca5d544f692225c77c53f6fc29b0c683 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Thu, 21 Jun 2012 22:17:30 +0200 Subject: [PATCH 1/6] Added support for namespace expansion of a DocBlock and via the DocBlock to its tags --- src/phpDocumentor/Reflection/DocBlock.php | 118 +++++++++++++++++- .../phpDocumentor/Reflection/DocBlockTest.php | 91 +++++++++++++- 2 files changed, 202 insertions(+), 7 deletions(-) diff --git a/src/phpDocumentor/Reflection/DocBlock.php b/src/phpDocumentor/Reflection/DocBlock.php index 9d5cfb6..782bfab 100644 --- a/src/phpDocumentor/Reflection/DocBlock.php +++ b/src/phpDocumentor/Reflection/DocBlock.php @@ -36,14 +36,36 @@ class DocBlock implements \Reflector */ protected $tags = array(); + /** @var string the current namespace */ + protected $namespace = '\\'; + + /** @var string[] List of namespace aliases => Fully Qualified Namespace */ + protected $namespace_aliases = array(); + /** * Parses the given docblock and populates the member fields. * - * @param string|\Reflector $docblock A docblock comment (including asterisks) + * The constructor may also receive namespace information such as the + * current namespace and aliases. This information is used in the + * {@link expandType()} method to transform a relative Type into a FQCN. + * + * For example the param and return tags use this to expand their type + * information. + * + * @param \Reflector|string $docblock A docblock comment (including asterisks) * or reflector supporting the getDocComment method. + * @param string $namespace The namespace where this DocBlock resides in; + * defaults to `\`. + * @param string[] $namespace_aliases a list of namespace aliases as + * provided by the `use` keyword; the key of the array is the alias name + * or last part of the alias array if no alias name is provided. + * + * @throws \InvalidArgumentException if the given argument does not have the + * getDocComment method. */ - public function __construct($docblock) - { + public function __construct( + $docblock, $namespace = '\\', $namespace_aliases = array() + ) { if (is_object($docblock)) { if (!method_exists($docblock, 'getDocComment')) { throw new \InvalidArgumentException( @@ -61,6 +83,9 @@ class DocBlock implements \Reflector $this->short_description = $short; $this->long_description = new DocBlock\LongDescription($long); $this->parseTags($tags); + + $this->namespace = $namespace; + $this->namespace_aliases = $namespace_aliases; } /** @@ -271,6 +296,93 @@ class DocBlock implements \Reflector return false; } + /** + * Tries to expand a type to it's full namespaced equivalent (FQCN). + * + * This method will take the given type and examine the current namespace + * and namespace aliases to see whether it should expand it into a FQCN + * as defined by the rules in PHP. + * + * @param string $type Type to expand into full namespaced + * equivalent. + * @param string[] $ignore_keywords Whether to ignore given keywords, when + * null it will use the default keywords: 'string', 'int', 'integer', + * 'bool', 'boolean', 'float', 'double', 'object', 'mixed', 'array', + * 'resource', 'void', 'null', 'callback', 'false', 'true'. + * Default value for this parameter is null. + * + * @return string + */ + public function expandType($type, $ignore_keywords = null) + { + if ($type === null) { + return null; + } + + if ($ignore_keywords === null) { + $ignore_keywords = array( + 'string', 'int', 'integer', 'bool', 'boolean', 'float', 'double', + 'object', 'mixed', 'array', 'resource', 'void', 'null', + 'callback', 'false', 'true' + ); + } + + $namespace = ''; + if ($this->namespace != 'default') { + $namespace = rtrim($this->namespace, '\\') . '\\'; + } + + $type = explode('|', $type); + foreach ($type as &$item) { + $item = trim($item); + + // add support for array notation + $is_array = false; + if (substr($item, -2) == '[]') { + $item = substr($item, 0, -2); + $is_array = true; + } + + if ((substr($item, 0, 1) != '\\') + && (!in_array(strtolower($item), $ignore_keywords)) + ) { + $type_parts = explode('\\', $item); + + // if the first part is the keyword 'namespace', replace it + // with the current namespace + if ($type_parts[0] == 'namespace') { + $type_parts[0] = $this->getNamespace(); + $item = implode('\\', $type_parts); + } + + // if the first segment is an alias; replace with full name + if (isset($this->namespace_aliases[$type_parts[0]])) { + $type_parts[0] = $this->namespace_aliases[$type_parts[0]]; + + $item = implode('\\', $type_parts); + } elseif (count($type_parts) == 1) { + // prefix the item with the namespace if there is only one + // part and no alias + $item = $namespace . $item; + } + } + + // full paths always start with a slash + if (isset($item[0]) && ($item[0] !== '\\') + && (!in_array(strtolower($item), $ignore_keywords)) + ) { + $item = '\\' . $item; + } + + // re-add the array notation markers + if ($is_array) { + $item .= '[]'; + } + } + + return implode('|', $type); + } + /** * Builds a string representation of this object. * diff --git a/tests/phpDocumentor/Reflection/DocBlockTest.php b/tests/phpDocumentor/Reflection/DocBlockTest.php index b2a07ee..32d248f 100644 --- a/tests/phpDocumentor/Reflection/DocBlockTest.php +++ b/tests/phpDocumentor/Reflection/DocBlockTest.php @@ -11,7 +11,8 @@ namespace phpDocumentor\Reflection; require_once __DIR__.'/../../../src/phpDocumentor/Reflection/DocBlock.php'; -require_once __DIR__.'/../../../src/phpDocumentor/Reflection/DocBlock/LongDescription.php'; +require_once __DIR__ + .'/../../../src/phpDocumentor/Reflection/DocBlock/LongDescription.php'; /** * Test class for phpDocumentor_Reflection_DocBlock @@ -38,7 +39,8 @@ DOCBLOCK; 'This is a short description.', $object->getShortDescription() ); $this->assertEquals( - 'This is a long description.', $object->getLongDescription()->getContents() + 'This is a long description.', + $object->getLongDescription()->getContents() ); $this->assertEquals(2, count($object->getTags())); $this->assertTrue($object->hasTag('see')); @@ -58,8 +60,89 @@ DOCBLOCK; 'This is a short description.', $object->getShortDescription() ); $this->assertEquals( - "This is a long description.\nThis is a continuation of the long description.", $object->getLongDescription()->getContents() - ); + "This is a long description.\nThis is a continuation of the long " + ."description.", $object->getLongDescription()->getContents() + ); + } + + /** + * Tests whether a type is expanded with the given namespace and that a + * keyword is not expanded. + * + * @covers \phpDocumentor\Reflection\DocBlock::expandType() + * + * @return void + */ + public function testExpandTypeUsingNamespace() + { + $docblock = new DocBlock('', '\My\Namespace'); + $this->assertEquals('\My\Namespace\Mine', $docblock->expandType('Mine')); + } + + /** + * Tests whether a type is expanded when no namespace is given. + * + * @covers \phpDocumentor\Reflection\DocBlock::expandType() + * + * @return void + */ + public function testExpandTypeWithoutNamespace() + { + $docblock = new DocBlock(''); + $this->assertEquals('\Mine', $docblock->expandType('Mine')); + } + + /** + * Tests whether a type is expanded with the given namespace when an alias + * is provided. + * + * @covers \phpDocumentor\Reflection\DocBlock::expandType() + * + * @return void + */ + public function testExpandTypeUsingNamespaceAlias() + { + $docblock = new DocBlock( + '', '\My\Namespace', array('Alias' => '\My\Namespace\Alias') + ); + + // first try a normal resolution without alias + $this->assertEquals( + '\My\Namespace\Al', $docblock->expandType('Al') + ); + + // try to use the alias + $this->assertEquals( + '\My\Namespace\Alias\Al', $docblock->expandType('Alias\Al') + ); + } + + /** + * Tests whether the keywords that should not be converted are not converted. + * + * @param string $keyword The keyword that is to be tested; this is provided + * by the dataprovider. + * + * @covers \phpDocumentor\Reflection\DocBlock::expandType() + * + * @dataProvider getNonExpandableKeywordsForExpandType + * + * @return void + */ + public function testThatExpandTypeDoesNotExpandAllKeywords($keyword) + { + $docblock = new DocBlock('', '\My\Namespace'); + $this->assertEquals($keyword, $docblock->expandType($keyword)); + } + + public function getNonExpandableKeywordsForExpandType() + { + return array( + array('string'), array('int'), array('integer'), array('bool'), + array('boolean'), array('float'), array('double'), array('object'), + array('mixed'), array('array'), array('resource'), array('void'), + array('null'), array('callback'), array('false'), array('true') + ); } } From fa9e0b637e9a28153b2883348e32441f1dbbc245 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Thu, 21 Jun 2012 22:51:29 +0200 Subject: [PATCH 2/6] Added type expansion for the @param tag --- src/phpDocumentor/Reflection/DocBlock/Tag.php | 2 +- .../Reflection/DocBlock/Tag/ParamTag.php | 7 +- .../Reflection/DocBlock/Tag/ParamTagTest.php | 118 ++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 tests/phpDocumentor/Reflection/DocBlock/Tag/ParamTagTest.php diff --git a/src/phpDocumentor/Reflection/DocBlock/Tag.php b/src/phpDocumentor/Reflection/DocBlock/Tag.php index 8016349..89bf708 100644 --- a/src/phpDocumentor/Reflection/DocBlock/Tag.php +++ b/src/phpDocumentor/Reflection/DocBlock/Tag.php @@ -33,7 +33,7 @@ class Tag implements \Reflector /** @var int line number of the tag */ protected $line_number = 0; - /** @var object docblock class */ + /** @var \phpDocumentor\Reflection\DocBlock docblock class */ protected $docblock; /** diff --git a/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php b/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php index 3aac2e0..4be3254 100644 --- a/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php +++ b/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php @@ -63,7 +63,12 @@ class ParamTag extends Tag public function getTypes() { $types = explode('|', $this->type); - array_walk($types, 'trim'); + foreach ($types as &$type) { + $type = !empty($type) && $this->docblock + ? $this->docblock->expandType($type) + : trim($type); + } + return $types; } diff --git a/tests/phpDocumentor/Reflection/DocBlock/Tag/ParamTagTest.php b/tests/phpDocumentor/Reflection/DocBlock/Tag/ParamTagTest.php new file mode 100644 index 0000000..5f99e25 --- /dev/null +++ b/tests/phpDocumentor/Reflection/DocBlock/Tag/ParamTagTest.php @@ -0,0 +1,118 @@ + + * @copyright Copyright (c) 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + */ + +namespace phpDocumentor\Reflection\DocBlock\Tag; + +require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php'; + +/** + * Test class for phpDocumentor_Reflection_DocBlock_Param. + * + * @author Mike van Riel + * @copyright Copyright (c) 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + */ +class ParamTagTest extends \PHPUnit_Framework_TestCase +{ + /** + * Test that the \phpDocumentor\Reflection\DocBlock\Tag\ParamTag can + * understand the param DocBlock. + * + * @param string $type + * @param string $content + * @param string $extracted_type + * @param string $extracted_variable_name + * @param string $extracted_description + * + * @covers \phpDocumentor\Reflection\DocBlock\Tag\ParamTag::__construct + * + * @dataProvider provideDataForConstructor + * + * @return void + */ + public function testConstructorParsesInputsIntoCorrectFields( + $type, $content, $extracted_type, $extracted_variable_name, + $extracted_description + ) { + $tag = new ParamTag($type, $content); + + $this->assertEquals($extracted_type, $tag->getTypes()); + $this->assertEquals($extracted_variable_name, $tag->getVariableName()); + $this->assertEquals($extracted_description, $tag->getDescription()); + } + + /** + * Tests whether the getTypes method correctly converts the given tags. + * + * @param string $type Type string to test + * @param string[] $expected Array of expected types + * + * @covers \phpDocumentor\Reflection\DocBlock\Tag\ParamTag::getTypes() + * + * @dataProvider provideTypesToExpand + */ + public function testExpandTypeIntoCorrectFcqn($type, $expected) + { + $docblock = new \phpDocumentor\Reflection\DocBlock( + '', '\My\Namespace', array('Alias' => '\My\Namespace\Aliasing') + ); + + $tag = new ParamTag('param', $type.' $my_type'); + $tag->setDocBlock($docblock); + $this->assertEquals($expected, $tag->getTypes()); + } + + /** + * Data provider for testConstructorParsesInputsIntoCorrectFields() + * + * @return array + */ + public function provideDataForConstructor() + { + return array( + array('param', 'int', array(''), '', 'int'), + array('param', '$bob', array(''), '$bob', ''), + array( + 'param', 'int Number of bobs', array('int'), '', + 'Number of bobs' + ), + array('param', 'int $bob', array('int'), '$bob', ''), + array( + 'param', 'int $bob Number of bobs', array('int'), '$bob', + 'Number of bobs' + ), + ); + } + + /** + * Returns the types and their expected values to test the retrieval of + * types. + * + * @return string[] + */ + public function provideTypesToExpand() + { + return array( + array('', array('')), + array(' ', array('')), + array('int', array('int')), + array('int ', array('int')), + array('string', array('string')), + array('DocBlock', array('\My\Namespace\DocBlock')), +// array(' DocBlock ', array('\My\Namespace\DocBlock')), FIXME + array('Alias\DocBlock', array('\My\Namespace\Aliasing\DocBlock')), + array( + 'DocBlock|Tag', + array('\My\Namespace\DocBlock', '\My\Namespace\Tag') + ), + array( + 'DocBlock|null', + array('\My\Namespace\DocBlock', 'null') + ), + ); + } +} From 0a90ca388197c768c4b026b2f11e392c9383f2c2 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Thu, 21 Jun 2012 23:00:13 +0200 Subject: [PATCH 3/6] Changed return tag to properly support expanding types --- .../Reflection/DocBlock/Tag/ReturnTag.php | 21 ----- .../Reflection/DocBlock/Tag/ReturnTagTest.php | 81 +++++++++++++++++++ 2 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 tests/phpDocumentor/Reflection/DocBlock/Tag/ReturnTagTest.php diff --git a/src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php b/src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php index 95a0c2d..88c631b 100644 --- a/src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php +++ b/src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php @@ -42,25 +42,4 @@ class ReturnTag extends ParamTag $this->description = implode(' ', $content); } - /** - * Returns the type of the variable. - * - * @return string - */ - public function getTypes() - { - $types = explode('|', $this->type); - array_walk($types, 'trim'); - return $types; - } - - /** - * Returns the type of the variable. - * - * @return string - */ - public function getType() - { - return $this->type; - } } diff --git a/tests/phpDocumentor/Reflection/DocBlock/Tag/ReturnTagTest.php b/tests/phpDocumentor/Reflection/DocBlock/Tag/ReturnTagTest.php new file mode 100644 index 0000000..1cc95be --- /dev/null +++ b/tests/phpDocumentor/Reflection/DocBlock/Tag/ReturnTagTest.php @@ -0,0 +1,81 @@ + + * @copyright Copyright (c) 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + */ + +namespace phpDocumentor\Reflection\DocBlock\Tag; + +require_once __DIR__ + . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php'; + +/** + * Test class for phpDocumentor_Reflection_DocBlock_ReturnTag. + * + * @author Mike van Riel + * @copyright Copyright (c) 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + */ +class ReturnTagTest extends ParamTagTest +{ + /** + * Test that the \phpDocumentor\Reflection\DocBlock\Tag\ReturnTag can + * understand the Return DocBlock. + * + * @param string $content + * @param string $extracted_type + * @param string $extracted_description + * + * @covers \phpDocumentor\Reflection\DocBlock\Tag\ReturnTag::__construct + * + * @dataProvider provideDataForConstructor + * + * @return void + */ + public function testConstructorParsesInputsIntoCorrectFields( + $content, $extracted_type, $extracted_description + ) { + $tag = new ReturnTag('return', $content); + + $this->assertEquals($extracted_type, $tag->getTypes()); + $this->assertEquals($extracted_description, $tag->getDescription()); + } + + /** + * Tests whether the getTypes method correctly converts the given tags. + * + * @param string $type Type string to test + * @param string[] $expected Array of expected types + * + * @covers \phpDocumentor\Reflection\DocBlock\Tag\ReturnTag::getTypes() + * + * @dataProvider provideTypesToExpand + * + * @return void + */ + public function testExpandTypeIntoCorrectFcqn($type, $expected) + { + $docblock = new \phpDocumentor\Reflection\DocBlock( + '', '\My\Namespace', array('Alias' => '\My\Namespace\Aliasing') + ); + + $tag = new ReturnTag('return', $type); + $tag->setDocBlock($docblock); + $this->assertEquals($expected, $tag->getTypes()); + } + + /** + * Data provider for testConstructorParsesInputsIntoCorrectFields() + * + * @return array + */ + public function provideDataForConstructor() + { + return array( + array('', array(''), ''), + array('int', array('int'), ''), + array('int Number of Bobs', array('int'), 'Number of Bobs'), + ); + } +} From 467101e53d22c1815cc54b39f043695798518bff Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Thu, 21 Jun 2012 23:03:48 +0200 Subject: [PATCH 4/6] Added command to set the DocBlock onto the tags so that type expansion may happen --- src/phpDocumentor/Reflection/DocBlock.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/phpDocumentor/Reflection/DocBlock.php b/src/phpDocumentor/Reflection/DocBlock.php index 782bfab..00958ac 100644 --- a/src/phpDocumentor/Reflection/DocBlock.php +++ b/src/phpDocumentor/Reflection/DocBlock.php @@ -217,7 +217,9 @@ class DocBlock implements \Reflector // create proper Tag objects foreach ($result as $key => $tag_line) { - $result[$key] = DocBlock\Tag::createInstance($tag_line); + $tag = DocBlock\Tag::createInstance($tag_line); + $tag->setDocBlock($this); + $result[$key] = $tag; } $this->tags = $result; From cb0246462f0fb7860d4da733488ca011c2d3b3fe Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Fri, 29 Jun 2012 23:04:12 +0200 Subject: [PATCH 5/6] Added 'global' to the list of special namespace names indicating global space and adding namespace resolution to the param tags 'getType()' method --- src/phpDocumentor/Reflection/DocBlock.php | 2 +- src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phpDocumentor/Reflection/DocBlock.php b/src/phpDocumentor/Reflection/DocBlock.php index 00958ac..e09fdf1 100644 --- a/src/phpDocumentor/Reflection/DocBlock.php +++ b/src/phpDocumentor/Reflection/DocBlock.php @@ -330,7 +330,7 @@ class DocBlock implements \Reflector } $namespace = ''; - if ($this->namespace != 'default') { + if ($this->namespace != 'default' && $this->namespace != 'global') { $namespace = rtrim($this->namespace, '\\') . '\\'; } diff --git a/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php b/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php index 4be3254..8214534 100644 --- a/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php +++ b/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php @@ -79,7 +79,7 @@ class ParamTag extends Tag */ public function getType() { - return $this->type; + return $this->docblock->expandType($this->type); } /** From b3d119fd24fd16415ddf1869d088704dded6d048 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Sat, 30 Jun 2012 13:38:18 +0200 Subject: [PATCH 6/6] Namespace resolution did not work as intended The algorithm to expand a class name into a FCQN contained seevral errors. These are now gone and expansion should work as expected --- src/phpDocumentor/Reflection/DocBlock.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/phpDocumentor/Reflection/DocBlock.php b/src/phpDocumentor/Reflection/DocBlock.php index e09fdf1..ac5b6a4 100644 --- a/src/phpDocumentor/Reflection/DocBlock.php +++ b/src/phpDocumentor/Reflection/DocBlock.php @@ -350,21 +350,12 @@ class DocBlock implements \Reflector ) { $type_parts = explode('\\', $item); - // if the first part is the keyword 'namespace', replace it - // with the current namespace - if ($type_parts[0] == 'namespace') { - $type_parts[0] = $this->getNamespace(); - $item = implode('\\', $type_parts); - } - // if the first segment is an alias; replace with full name if (isset($this->namespace_aliases[$type_parts[0]])) { $type_parts[0] = $this->namespace_aliases[$type_parts[0]]; - $item = implode('\\', $type_parts); - } elseif (count($type_parts) == 1) { - // prefix the item with the namespace if there is only one - // part and no alias + } else { + // otherwise prepend the current namespace $item = $namespace . $item; } }