Change behaviour of @param parsing

In issue report phpDocumentor/phpDocumentor2#620 @bobef reported that when he
used just a Type as content of the @param that it would be recognized as
description instead of the Type.

According to the unit tests this is correct behaviour but after reviewing the
pattern of the output his version is more consistent. As such I have altered
the behaviour to act as following:

If only one word is found after an @param (word means white-space bounded
series of characters) then interpret that as the type and not description.

During this item several issues in unit tests were fixed and a new 'Type'
Collection was introduced that is capable of expanding types based on a given
namespace and series of aliases.
This should be re-used in phpDocumentor's Transformer as a duplication exists
there with the expanding of the Types.

Please note: the suggested format by @bobef is not valid according to the
PHPDoc Standard but is provided for convenience.
This commit is contained in:
Mike van Riel
2012-11-03 22:14:43 +01:00
parent b3641c0e69
commit 0701e60839
17 changed files with 541 additions and 116 deletions
+2 -1
View File
@@ -1 +1,2 @@
.idea
.idea
vendor
+1 -1
View File
@@ -10,6 +10,6 @@
"dflydev/markdown": "1.0.*"
},
"autoload": {
"psr-0": {"phpDocumentor\\Reflection": "src/"}
"psr-0": {"phpDocumentor": ["src/"]}
}
}
Generated
+67
View File
@@ -0,0 +1,67 @@
{
"hash": "89c7387ec02d39de3c07849e525f95e4",
"packages": [
{
"name": "dflydev/markdown",
"version": "v1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-markdown.git",
"reference": "v1.0.2"
},
"dist": {
"type": "zip",
"url": "https://github.com/dflydev/dflydev-markdown/zipball/v1.0.2",
"reference": "v1.0.2",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"time": "2012-01-15 19:36:37",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"dflydev\\markdown": "src"
}
},
"license": [
"New BSD License"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "[email protected]",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "[email protected]",
"homepage": "http://beausimensen.com"
},
{
"name": "Michel Fortin",
"homepage": "http://michelf.com"
},
{
"name": "John Gruber",
"homepage": "http://daringfireball.net"
}
],
"description": "PHP Markdown & Extra",
"homepage": "http://github.com/dflydev/dflydev-markdown",
"keywords": [
"markdown"
]
}
],
"packages-dev": null,
"aliases": [
],
"minimum-stability": "stable",
"stability-flags": [
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit colors="true" strict="true">
<phpunit colors="true" strict="true" bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="phpDocumentor\Reflection\DocBlock">
<directory>./tests/</directory>
+13 -1
View File
@@ -327,7 +327,7 @@ class DocBlock implements \Reflector
);
}
$namespace = '';
$namespace = '\\';
if ($this->namespace != 'default' && $this->namespace != 'global') {
$namespace = rtrim($this->namespace, '\\') . '\\';
}
@@ -398,4 +398,16 @@ class DocBlock implements \Reflector
return 'Not yet implemented';
}
/**
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
public function getNamespaceAliases()
{
return $this->namespace_aliases;
}
}
@@ -23,7 +23,7 @@ use phpDocumentor\Reflection\DocBlock\Tag;
class ParamTag extends Tag
{
/** @var string */
protected $type = null;
protected $type = '';
/**
* @var string
@@ -33,7 +33,7 @@ class ParamTag extends Tag
/**
* Parses a tag and populates the member variables.
*
* @param string $type Tag identifier for this tag (should be 'return')
* @param string $type Tag identifier for this tag (should be 'param')
* @param string $content Contents for this tag.
*/
public function __construct($type, $content)
@@ -42,13 +42,13 @@ class ParamTag extends Tag
$this->content = $content;
$content = preg_split('/\s+/u', $content);
// if there is only 1, it is either a piece of content or a variable name
if (count($content) > 1) {
// if the first item that is encountered is not a variable; it is a type
if (isset($content[0]) && (strlen($content[0]) > 0) && ($content[0][0] !== '$')) {
$this->type = array_shift($content);
}
// if the next item starts with a $ it must be the variable name
if ((strlen($content[0]) > 0) && ($content[0][0] == '$')) {
if (isset($content[0]) && (strlen($content[0]) > 0) && ($content[0][0] == '$')) {
$this->variableName = array_shift($content);
}
@@ -62,14 +62,13 @@ class ParamTag extends Tag
*/
public function getTypes()
{
$types = explode('|', $this->type);
foreach ($types as &$type) {
$type = !empty($type) && $this->docblock
? $this->docblock->expandType($type)
: trim($type);
}
$types = new \phpDocumentor\Reflection\DocBlock\Type\Collection(
array($this->type),
$this->docblock ? $this->docblock->getNamespace() : null,
$this->docblock ? $this->docblock->getNamespaceAliases() : array()
);
return array_values(array_unique($types, SORT_REGULAR));
return $types->getArrayCopy();
}
/**
@@ -79,7 +78,7 @@ class ParamTag extends Tag
*/
public function getType()
{
return $this->docblock->expandType($this->type);
return implode('|', $this->getTypes());
}
/**
@@ -0,0 +1,248 @@
<?php
namespace phpDocumentor\Reflection\DocBlock\Type;
class Collection extends \ArrayObject
{
/** @var string Definition of the OR operator for types */
const OPERATOR_OR = '|';
/** @var string Definition of the ARRAY operator for types */
const OPERATOR_ARRAY = '[]';
/** @var string Definition of the NAMESPACE operator in PHP */
const OPERATOR_NAMESPACE = '\\';
/** @var string[] List of recognized keywords */
protected $keywords = array(
'string', 'int', 'integer', 'bool', 'boolean', 'float', 'double',
'object', 'mixed', 'array', 'resource', 'void', 'null',
'callback', 'callable', 'false', 'true', 'self', '$this', 'static'
);
/**
* Current namespace of the invoking location.
*
* This string is used to prepend to type with a relative location.
* May also be 'default' or 'global', in which case they are ignored.
*
* @var string
*/
protected $namespace = '\\';
/**
* Associative array of alias => namespace pairs.
*
* @var string[]
*/
protected $namespace_aliases = array();
/**
* Registers the namespace and aliases; uses that to add and expand the
* given types.
*
* @param string[] $types Array containing a list of types
* to add to this container.
* @param string|null $namespace The namespace where the types in
* this container are relative to; used to expand any relative
* namespaces.
* @param string[] $namespace_aliases An array containing alias => FQNN
* pairs that are used in the resolving process.
*/
public function __construct(
array $types = array(), $namespace = null,
array $namespace_aliases = array()
) {
// only set the namespace if overridden
if (is_string($namespace)) {
$this->setNamespace($namespace);
}
$this->namespace_aliases = $namespace_aliases;
foreach($types as $type) {
$this->add($type);
}
}
/**
* Returns the namespace name used to expand relative namespaces with.
*
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Sets the namespace which is used to resolve types with relative
* namespaces.
*
* Unless the name of the current namespace if default or global we make
* sure the namespace is succeeded with a '\'. This makes it clear for
* the processing functions that a leading slash is present.
*
* @param string $namespace
*
* @return void
*/
public function setNamespace($namespace)
{
if ($namespace != 'default' && $namespace != 'global') {
$namespace = self::OPERATOR_NAMESPACE
. trim($namespace, self::OPERATOR_NAMESPACE)
. self::OPERATOR_NAMESPACE;
} else {
$namespace = '\\';
}
$this->namespace = $namespace;
}
/**
* Returns the list of namespace aliases used to expand the typed with.
*
* @return string[] An associative array of Alias => Fully Qualified
* Namespace Names.
*/
public function getNamespaceAliases()
{
return $this->namespace_aliases;
}
/**
* Sets the namespace aliases to expand the added types.
*
* @param string[] $namespace_aliases An associative array of Alias => Fully
* Qualified Namespace Names.
*
* @return void
*/
public function setNamespaceAliases($namespace_aliases)
{
$this->namespace_aliases = $namespace_aliases;
}
/**
* Adds a new type to the collection and expands it if it contains a
* relative namespace.
*
* If a class in the type contains a relative namespace than this collection
* will try to expand that into a FQCN.
*
* @param string $type A 'Type' as defined in the phpDocumentor
* documentation.
*
* @throws \InvalidArgumentException if a non-string argument is passed.
*
* @see http://phpdoc.org/docs/latest/for-users/types.html for the
* definition of a type.
*
* @return void
*/
public function add($type)
{
if (!is_string($type)) {
throw new \InvalidArgumentException(
'A type should be represented by a string, received: '
.var_export($type, true)
);
}
// separate the type by the OR operator
$type_parts = explode(self::OPERATOR_OR, $type);
foreach($type_parts as $part) {
$expanded_type = $this->expand($part);
if ($expanded_type) {
$this[] = $expanded_type;
}
}
}
/**
* Analyzes the given type and returns the FQCN variant.
*
* When a type is provided this method checks whether it is not a keyword or
* Fully Qualified Class Name. If so it will use the given namespace and
* aliases to expand the type to a FQCN representation.
*
* This method only works as expected if the namespace and aliases are set;
* no dynamic reflection is being performed here.
*
* @uses getNamespace to determine with what to prefix the type name.
* @uses getNamespaceAliases to check whether the first part of the relative
* type name should not be replaced with another namespace.
*
* @param string $type The relative or absolute type.
*
* @return string
*/
protected function expand($type)
{
$type = trim($type);
if (!$type) {
return '';
}
if ($this->isTypeAnArray($type)) {
return $this->expand(substr($type, 0, -2)).self::OPERATOR_ARRAY;
}
if ($this->isRelativeType($type) && !$this->isTypeAKeyword($type)) {
$type_parts = explode(self::OPERATOR_NAMESPACE, $type);
// if the first segment is not an alias; prepend namespace name and
// return
if (!isset($this->namespace_aliases[$type_parts[0]])) {
return $this->getNamespace() . $type;
}
$type_parts[0] = $this->namespace_aliases[$type_parts[0]];
$type = implode(self::OPERATOR_NAMESPACE, $type_parts);
}
return $type;
}
/**
* Detects whether the given type represents an array.
*
* @param string $type A relative or absolute type as defined in the
* phpDocumentor documentation.
*
* @return bool
*/
protected function isTypeAnArray($type)
{
return (substr($type, -2) == self::OPERATOR_ARRAY);
}
/**
* Detects whether the given type represents a PHPDoc keyword.
*
* @param string $type A relative or absolute type as defined in the
* phpDocumentor documentation.
*
* @return bool
*/
protected function isTypeAKeyword($type)
{
return in_array(strtolower($type), $this->keywords);
}
/**
* Detects whether the given type represents a relative or absolute path.
*
* This method will detect keywords as being absolute; even though they are
* not preceeded by a namespace separator.
*
* @param string $type A relative or absolute type as defined in the
* phpDocumentor documentation.
*
* @return bool
*/
protected function isRelativeType($type)
{
return ($type[0] !== self::OPERATOR_NAMESPACE)
|| $this->isTypeAKeyword($type);
}
}
@@ -8,10 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__.'/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag.php';
require_once __DIR__.'/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/SeeTag.php';
require_once __DIR__.'/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/CoversTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_Tag_Covers
*
@@ -8,8 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/LinkTag.php';
/**
* Test class for \phpDocumentor\Reflection\DocBlock\Tag\LinkTag
*
@@ -8,9 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php';
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/MethodTag.php';
/**
* Test class for \phpDocumentor\Reflection\DocBlock\Tag\MethodTag
*
@@ -8,8 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_Param.
*
@@ -45,27 +43,6 @@ class ParamTagTest extends \PHPUnit_Framework_TestCase
$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()
*
@@ -74,8 +51,8 @@ class ParamTagTest extends \PHPUnit_Framework_TestCase
public function provideDataForConstructor()
{
return array(
array('param', 'int', array(''), '', 'int'),
array('param', '$bob', array(''), '$bob', ''),
array('param', 'int', array('int'), '', ''),
array('param', '$bob', array(), '$bob', ''),
array(
'param', 'int Number of bobs', array('int'), '',
'Number of bobs'
@@ -87,32 +64,4 @@ class ParamTagTest extends \PHPUnit_Framework_TestCase
),
);
}
/**
* 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')
),
);
}
}
@@ -8,9 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__
. '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_ReturnTag.
*
@@ -42,29 +39,6 @@ class ReturnTagTest extends ParamTagTest
$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()
*
@@ -73,7 +47,7 @@ class ReturnTagTest extends ParamTagTest
public function provideDataForConstructor()
{
return array(
array('', array(''), ''),
array('', array(), ''),
array('int', array('int'), ''),
array('int Number of Bobs', array('int'), 'Number of Bobs'),
);
@@ -8,8 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/SeeTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_Tag_See
*
@@ -8,8 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/UsesTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_Tag_Uses
*
@@ -8,8 +8,6 @@
namespace phpDocumentor\Reflection\DocBlock\Tag;
require_once __DIR__ . '/../../../../../src/phpDocumentor/Reflection/DocBlock/Tag/VarTag.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock_Tag_Link
*
@@ -0,0 +1,194 @@
<?php
namespace phpDocumentor\Reflection\DocBlock\Type;
/**
* Test class for phpDocumentor_Reflection_DocBlock
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection
*/
class CollectionTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::__construct
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespace
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespaceAliases
*/
public function testConstruct()
{
$collection = new Collection();
$this->assertCount(0, $collection);
$this->assertEquals('\\', $collection->getNamespace());
$this->assertCount(0, $collection->getNamespaceAliases());
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::__construct
*/
public function testConstructWithTypes()
{
$collection = new Collection(array('integer', 'string'));
$this->assertCount(2, $collection);
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::__construct
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespace
*/
public function testConstructWithNamespace()
{
$collection = new Collection(array(), '\My\Space');
$this->assertEquals('\My\Space\\', $collection->getNamespace());
$collection = new Collection(array(), 'My\Space');
$this->assertEquals('\My\Space\\', $collection->getNamespace());
$collection = new Collection(array(), null);
$this->assertEquals('\\', $collection->getNamespace());
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::__construct
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespaceAliases
*/
public function testConstructWithNamespaceAliases()
{
$fixture = array('a' => 'b');
$collection = new Collection(array(), null, $fixture);
$this->assertEquals($fixture, $collection->getNamespaceAliases());
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::setNamespace
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespace
*/
public function testSetAndGetNamespace()
{
$collection = new Collection();
$this->assertEquals('\\', $collection->getNamespace());
$collection->setNamespace('My');
$this->assertEquals('\My\\', $collection->getNamespace());
$collection->setNamespace('\My');
$this->assertEquals('\My\\', $collection->getNamespace());
$collection->setNamespace('\My\\');
$this->assertEquals('\My\\', $collection->getNamespace());
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::setNamespaceAliases
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::getNamespaceAliases
*/
public function testSetAndGetNamespaceAliases()
{
$collection = new Collection();
$this->assertEmpty($collection->getNamespaceAliases());
$collection->setNamespaceAliases(array('My'));
$this->assertEquals(array('My'), $collection->getNamespaceAliases());
}
/**
* @param $fixture
* @param $expected
*
* @dataProvider provideTypesToExpand
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::add
*/
public function testAdd($fixture, $expected)
{
$collection = new Collection();
$collection->setNamespace('\My\Space');
$collection->setNamespaceAliases(array('Alias' => '\My\Space\Aliasing'));
$collection->add($fixture);
$this->assertSame($expected, $collection->getArrayCopy());
}
/**
* @param $fixture
* @param $expected
*
* @dataProvider provideTypesToExpandWithoutNamespace
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::add
*/
public function testAddWithoutNamespace($fixture, $expected)
{
$collection = new Collection();
$collection->setNamespaceAliases(array('Alias' => '\My\Space\Aliasing'));
$collection->add($fixture);
$this->assertSame($expected, $collection->getArrayCopy());
}
/**
* @covers phpDocumentor\Reflection\DocBlock\Type\Collection::add
* @expectedException InvalidArgumentException
*/
public function testAddWithInvalidArgument()
{
$collection = new Collection();
$collection->add(array());
}
/**
* Returns the types and their expected values to test the retrieval of
* types.
*
* @param string $method Name of the method consuming this data provider.
* @param string $namespace Name of the namespace to user as basis.
*
* @return string[]
*/
public function provideTypesToExpand($method, $namespace = '\My\Space\\')
{
return array(
array('', array()),
array(' ', array()),
array('int', array('int')),
array('int ', array('int')),
array('string', array('string')),
array('DocBlock', array($namespace.'DocBlock')),
array('DocBlock[]', array($namespace.'DocBlock[]')),
array(' DocBlock ', array($namespace.'DocBlock')),
array('\My\Space\DocBlock', array('\My\Space\DocBlock')),
array('Alias\DocBlock', array('\My\Space\Aliasing\DocBlock')),
array(
'DocBlock|Tag',
array($namespace .'DocBlock', $namespace .'Tag')
),
array(
'DocBlock|null',
array($namespace.'DocBlock', 'null')
),
array(
'\My\Space\DocBlock|Tag',
array('\My\Space\DocBlock', $namespace.'Tag')
),
array(
'DocBlock[]|null',
array($namespace.'DocBlock[]', 'null')
),
array(
'DocBlock[]|int[]',
array($namespace.'DocBlock[]', 'int[]')
),
);
}
/**
* Returns the types and their expected values to test the retrieval of
* types when no namespace is available.
*
* @param string $method Name of the method consuming this data provider.
* @param string $namespace Name of the namespace to user as basis.
*
* @return string[]
*/
public function provideTypesToExpandWithoutNamespace($method)
{
return $this->provideTypesToExpand($method, '\\');
}
}
@@ -10,10 +10,6 @@
namespace phpDocumentor\Reflection;
require_once __DIR__.'/../../../src/phpDocumentor/Reflection/DocBlock.php';
require_once __DIR__
.'/../../../src/phpDocumentor/Reflection/DocBlock/LongDescription.php';
/**
* Test class for phpDocumentor_Reflection_DocBlock
*