From d31a33e846085b8531563573bc4271c398462764 Mon Sep 17 00:00:00 2001 From: Vasil Rangelov Date: Sun, 4 Nov 2012 21:58:49 +0200 Subject: [PATCH 1/3] Implemented nested inline tag parsing; Added unit tests for LongDescription.php; Added the "src" folder as white listed for code coverage in the PHPUnit configuration; Fixed the @covers annotation inside the CoversTagTest.php (isn't this ironic?). --- phpunit.xml.dist | 5 + .../Reflection/DocBlock/LongDescription.php | 33 +++- .../DocBlock/LongDescriptionTest.php | 177 ++++++++++++++++++ .../Reflection/DocBlock/Tag/CoversTagTest.php | 2 +- 4 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b0a9ddf..f67ad2a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,4 +6,9 @@ ./tests/ + + + ./src/ + + diff --git a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php index 7fb8fa7..e119f56 100644 --- a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php +++ b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php @@ -61,8 +61,37 @@ class LongDescription implements \Reflector { if (null === $this->parsedContents) { $this->parsedContents = preg_split( - '/\{(\@.*?)\}/uS', $this->contents, - null, PREG_SPLIT_DELIM_CAPTURE + '/\{ + # We want the whole tag line, but without the inline tag + # delimiters. + (\@ + # The content should not be captured, or it will appear + # in the result separately. + (?: + # Match nested inline tags. + # Because we did not catch the tag delimiters + # earlier, we must be explicit with them here. + \{(?1)?\} + | + # "{@}" is not a valid inline tag. This ensures that + # having it occur inside an inline tag does not trip + # us up. + \{\@\} + | + # If we are not dealing with a nested inline tag, + # get the character, as long as it is not a closing + # tag delimiter. + # This is an alternative way of non-greedy matching. + [^\}] + )+ # We need to keep doing these checks for every + # character, since we never know where an inline tag + # is going to start at. The "+" ensures we are not + # treating "{@}" as a valid inline tag. + ) + \}/xuS', + $this->contents, + null, + PREG_SPLIT_DELIM_CAPTURE ); for ($i=1, $l = count($this->parsedContents); $i<$l; $i += 2) { $this->parsedContents[$i] = Tag::createInstance( diff --git a/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php b/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php new file mode 100644 index 0000000..f0676b3 --- /dev/null +++ b/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php @@ -0,0 +1,177 @@ + + * @copyright 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\DocBlock; + +/** + * Test class for phpDocumentor\Reflection\DocBlock\LongDescription + * + * @author Vasil Rangelov + * @copyright 2010-2011 Mike van Riel / Naenius. (http://www.naenius.com) + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ +class LongDescriptionTest extends \PHPUnit_Framework_TestCase +{ + public function testConstruct() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(1, $parsedContents); + $this->assertSame($fixture, $parsedContents[0]); + } + + public function testInlineTagParsing() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + $this->assertSame('This is text for a ', $parsedContents[0]); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag\LinkTag', + $parsedContents[1] + ); + $this->assertSame( + ' that uses inline +tags.', + $parsedContents[2] + ); + } + + public function testInlineTagAtStartParsing() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + + $this->assertSame('', $parsedContents[0]); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag\LinkTag', + $parsedContents[1] + ); + $this->assertSame( + ' is text for a description that uses inline +tags.', + $parsedContents[2] + ); + } + + public function testNestedInlineTagParsing() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + + $this->assertSame( + 'This is text for a description with ', + $parsedContents[0] + ); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag', + $parsedContents[1] + ); + $this->assertSame('.', $parsedContents[2]); + } + + public function testEmptyInlineTag() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(1, $parsedContents); + $this->assertSame($fixture, $parsedContents[0]); + } + + public function testNestedEmptyInlineTag() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + $this->assertSame( + 'This is text for a description with an ', + $parsedContents[0] + ); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag', + $parsedContents[1] + ); + $this->assertSame('.', $parsedContents[2]); + } + + public function testInlineTagDelimiters() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(1, $parsedContents); + $this->assertSame($fixture, $parsedContents[0]); + } + + public function testNestedInlineTagDelimiters() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + $this->assertSame( + 'This is text for a description with ', + $parsedContents[0] + ); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag', + $parsedContents[1] + ); + $this->assertSame('.', $parsedContents[2]); + } +} \ No newline at end of file diff --git a/tests/phpDocumentor/Reflection/DocBlock/Tag/CoversTagTest.php b/tests/phpDocumentor/Reflection/DocBlock/Tag/CoversTagTest.php index dfcd353..a059d9a 100644 --- a/tests/phpDocumentor/Reflection/DocBlock/Tag/CoversTagTest.php +++ b/tests/phpDocumentor/Reflection/DocBlock/Tag/CoversTagTest.php @@ -26,7 +26,7 @@ class CoversTagTest extends \PHPUnit_Framework_TestCase * @param string $exContent * @param string $exReference * - * @covers \phpDocumentor\Reflection\DocBlock\Tag\Covers::__construct + * @covers \phpDocumentor\Reflection\DocBlock\Tag\CoversTag::__construct * @dataProvider provideDataForConstuctor * * @return void From f7457e64bf5f958b061846fe259ac9afdffca214 Mon Sep 17 00:00:00 2001 From: Vasil Rangelov Date: Sun, 4 Nov 2012 23:04:19 +0200 Subject: [PATCH 2/3] Added escape sequences that allow "literal" inline tags in descriptions. --- .../Reflection/DocBlock/LongDescription.php | 16 +++- .../DocBlock/LongDescriptionTest.php | 88 ++++++++++++++++--- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php index e119f56..a5445a3 100644 --- a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php +++ b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php @@ -71,11 +71,14 @@ class LongDescription implements \Reflector # Match nested inline tags. # Because we did not catch the tag delimiters # earlier, we must be explicit with them here. + # Notice that this also matches "{}", as a way to + # later introduce it as an escape sequence. \{(?1)?\} | # "{@}" is not a valid inline tag. This ensures that # having it occur inside an inline tag does not trip - # us up. + # us up. While this is required in any event, notice + # that this is also later an escape sequence. \{\@\} | # If we are not dealing with a nested inline tag, @@ -98,6 +101,17 @@ class LongDescription implements \Reflector $this->parsedContents[$i] ); } + + //In order to allow "literal" inline tags, the otherwise invalid + //sequence "{@}" is changed to "@", and "{}" is changed to "}". + //See unit tests for examples. + for ($i=0, $l = count($this->parsedContents); $i<$l; $i += 2) { + $this->parsedContents[$i] = str_replace( + array('{@}', '{}'), + array('@', '}'), + $this->parsedContents[$i] + ); + } } return $this->parsedContents; } diff --git a/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php b/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php index f0676b3..5fe23c6 100644 --- a/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php +++ b/tests/phpDocumentor/Reflection/DocBlock/LongDescriptionTest.php @@ -103,12 +103,21 @@ LONGDESC; $parsedContents[1] ); $this->assertSame('.', $parsedContents[2]); + + $parsedDescription = $parsedContents[1]->getParsedDescription(); + $this->assertCount(3, $parsedDescription); + $this->assertSame("inline tag with\n", $parsedDescription[0]); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag\LinkTag', + $parsedDescription[1] + ); + $this->assertSame(' in it', $parsedDescription[2]); } - - public function testEmptyInlineTag() + + public function testLiteralOpeningDelimiter() { $fixture = <<assertSame($fixture, $object->getContents()); @@ -118,11 +127,11 @@ LONGDESC; $this->assertSame($fixture, $parsedContents[0]); } - public function testNestedEmptyInlineTag() + public function testNestedLiteralOpeningDelimiter() { $fixture = <<assertSame($fixture, $object->getContents()); @@ -130,7 +139,7 @@ LONGDESC; $parsedContents = $object->getParsedContents(); $this->assertCount(3, $parsedContents); $this->assertSame( - 'This is text for a description with an ', + 'This is text for a description containing ', $parsedContents[0] ); $this->assertInstanceOf( @@ -138,9 +147,15 @@ LONGDESC; $parsedContents[1] ); $this->assertSame('.', $parsedContents[2]); + + $this->assertSame( + array('inline tag that has { that +is literal'), + $parsedContents[1]->getParsedDescription() + ); } - public function testInlineTagDelimiters() + public function testLiteralClosingDelimiter() { $fixture = <<getParsedContents(); $this->assertCount(1, $parsedContents); - $this->assertSame($fixture, $parsedContents[0]); + $this->assertSame( + 'This is text for a description with } that is not a tag.', + $parsedContents[0] + ); } - public function testNestedInlineTagDelimiters() + public function testNestedLiteralClosingDelimiter() { $fixture = <<assertSame('.', $parsedContents[2]); + + $this->assertSame( + array('inline tag with } that is not an +inline tag'), + $parsedContents[1]->getParsedDescription() + ); + } + + public function testInlineTagEscapingSequence() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(1, $parsedContents); + $this->assertSame( + 'This is text for a description with literal {@link}.', + $parsedContents[0] + ); + } + + public function testNestedInlineTagEscapingSequence() + { + $fixture = <<assertSame($fixture, $object->getContents()); + + $parsedContents = $object->getParsedContents(); + $this->assertCount(3, $parsedContents); + $this->assertSame( + 'This is text for a description with an ', + $parsedContents[0] + ); + $this->assertInstanceOf( + __NAMESPACE__ . '\Tag', + $parsedContents[1] + ); + $this->assertSame('.', $parsedContents[2]); + + $this->assertSame( + array('inline tag with literal +{@link} in it'), + $parsedContents[1]->getParsedDescription() + ); } } \ No newline at end of file From 6bc655072fbb13e4401fcd1f399f7c9b86ca9c29 Mon Sep 17 00:00:00 2001 From: Vasil Rangelov Date: Mon, 5 Nov 2012 17:50:11 +0200 Subject: [PATCH 3/3] Improved performance, thanks to @nikic's new regex. --- .../Reflection/DocBlock/LongDescription.php | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php index a5445a3..efbbb15 100644 --- a/src/phpDocumentor/Reflection/DocBlock/LongDescription.php +++ b/src/phpDocumentor/Reflection/DocBlock/LongDescription.php @@ -62,36 +62,35 @@ class LongDescription implements \Reflector if (null === $this->parsedContents) { $this->parsedContents = preg_split( '/\{ - # We want the whole tag line, but without the inline tag - # delimiters. + # "{@}" is not a valid inline tag. This ensures that + # we do not treat it as one, but treat it literally. + (?!@\}) + # We want to capture the whole tag line, but without the + # inline tag delimiters. (\@ - # The content should not be captured, or it will appear - # in the result separately. + # Match everything up to the next delimiter. + [^{}]* + # Nested inline tag content should not be captured, or + # it will appear in the result separately. (?: # Match nested inline tags. - # Because we did not catch the tag delimiters - # earlier, we must be explicit with them here. - # Notice that this also matches "{}", as a way to - # later introduce it as an escape sequence. - \{(?1)?\} - | - # "{@}" is not a valid inline tag. This ensures that - # having it occur inside an inline tag does not trip - # us up. While this is required in any event, notice - # that this is also later an escape sequence. - \{\@\} - | - # If we are not dealing with a nested inline tag, - # get the character, as long as it is not a closing - # tag delimiter. - # This is an alternative way of non-greedy matching. - [^\}] - )+ # We need to keep doing these checks for every - # character, since we never know where an inline tag - # is going to start at. The "+" ensures we are not - # treating "{@}" as a valid inline tag. + (?: + # Because we did not catch the tag delimiters + # earlier, we must be explicit with them here. + # Notice that this also matches "{}", as a way + # to later introduce it as an escape sequence. + \{(?1)?\} + | + # Make sure we match hanging "{". + \{ + ) + # Match content after the nested inline tag. + [^{}]* + )* # If there are more inline tags, match them as well. + # We use "*" since there may not be any nested inline + # tags. ) - \}/xuS', + \}/Sux', $this->contents, null, PREG_SPLIT_DELIM_CAPTURE