Completed
Pull Request — master (#102)
by Aleksei
14:20
created

DescriptionFactory   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 159
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 13
c 2
b 0
f 0
lcom 1
cbo 2
dl 0
loc 159
ccs 43
cts 43
cp 1
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A create() 0 6 1
B lex() 0 39 2
A parse() 0 21 3
B removeSuperfluousStartingWhitespace() 0 32 6
1
<?php
2
/**
3
 * This file is part of phpDocumentor.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @copyright 2010-2015 Mike van Riel<[email protected]>
9
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
10
 * @link      http://phpdoc.org
11
 */
12
13
namespace phpDocumentor\Reflection\DocBlock;
14
15
use phpDocumentor\Reflection\Types\Context as TypeContext;
16
17
/**
18
 * Creates a new Description object given a body of text.
19
 *
20
 * Descriptions in phpDocumentor are somewhat complex entities as they can contain one or more tags inside their
21
 * body that can be replaced with a readable output. The replacing is done by passing a Formatter object to the
22
 * Description object's `render` method.
23
 *
24
 * In addition to the above does a Description support two types of escape sequences:
25
 *
26
 * 1. `{@}` to escape the `@` character to prevent it from being interpreted as part of a tag, i.e. `{{@}link}`
27
 * 2. `{}` to escape the `}` character, this can be used if you want to use the `}` character in the description
28
 *    of an inline tag.
29
 *
30
 * If a body consists of multiple lines then this factory will also remove any superfluous whitespace at the beginning
31
 * of each line while maintaining any indentation that is used. This will prevent formatting parsers from tripping
32
 * over unexpected spaces as can be observed with tag descriptions.
33
 */
34
class DescriptionFactory
35
{
36
    /** @var TagFactory */
37
    private $tagFactory;
38
39
    /**
40
     * Initializes this factory with the means to construct (inline) tags.
41
     *
42
     * @param TagFactory $tagFactory
43
     */
44 9
    public function __construct(TagFactory $tagFactory)
45
    {
46 9
        $this->tagFactory = $tagFactory;
47 9
    }
48
49
    /**
50
     * Returns the parsed text of this description.
51
     *
52
     * @param string $contents
53
     * @param TypeContext $context
54
     *
55
     * @return Description
56
     */
57 9
    public function create($contents, TypeContext $context = null)
58
    {
59 9
        list($text, $tags) = $this->parse($this->lex($contents), $context);
0 ignored issues
show
Bug introduced by
It seems like $context can be null; however, parse() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
60
61 9
        return new Description($text, $tags);
62
    }
63
64
    /**
65
     * Strips the contents from superfluous whitespace and splits the description into a series of tokens.
66
     *
67
     * @param string $contents
68
     *
69
     * @return string[] A series of tokens of which the description text is composed.
70
     */
71 9
    private function lex($contents)
72
    {
73 9
        $contents = $this->removeSuperfluousStartingWhitespace($contents);
74
75
        // performance optimalization; if there is no inline tag, don't bother splitting it up.
76 9
        if (strpos($contents, '{@') === false) {
77 6
            return [$contents];
78
        }
79
80 3
        return preg_split(
81
            '/\{
82
                # "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally.
83
                (?!@\})
84
                # We want to capture the whole tag line, but without the inline tag delimiters.
85
                (\@
86
                    # Match everything up to the next delimiter.
87
                    [^{}]*
88
                    # Nested inline tag content should not be captured, or it will appear in the result separately.
89
                    (?:
90
                        # Match nested inline tags.
91
                        (?:
92
                            # Because we did not catch the tag delimiters earlier, we must be explicit with them here.
93
                            # Notice that this also matches "{}", as a way to later introduce it as an escape sequence.
94
                            \{(?1)?\}
95
                            |
96
                            # Make sure we match hanging "{".
97
                            \{
98
                        )
99
                        # Match content after the nested inline tag.
100
                        [^{}]*
101
                    )* # If there are more inline tags, match them as well. We use "*" since there may not be any
102
                       # nested inline tags.
103
                )
104 3
            \}/Sux',
105 3
            $contents,
106 3
            null,
107
            PREG_SPLIT_DELIM_CAPTURE
108 3
        );
109
    }
110
111
    /**
112
     * Parses the stream of tokens in to a new set of tokens containing Tags.
113
     *
114
     * @param string[] $tokens
115
     * @param TypeContext $context
116
     *
117
     * @return string[]|Tag[]
118
     */
119 9
    private function parse($tokens, TypeContext $context)
120
    {
121 9
        $count = count($tokens);
122 9
        $tagCount = 0;
123 9
        $tags  = [];
124
125 9
        for ($i = 1; $i < $count; $i += 2) {
126 2
            $tags[] = $this->tagFactory->create($tokens[$i], $context);
127 2
            $tokens[$i] = '%' . ++$tagCount . '$s';
128 2
        }
129
130
        //In order to allow "literal" inline tags, the otherwise invalid
131
        //sequence "{@}" is changed to "@", and "{}" is changed to "}".
132
        //"%" is escaped to "%%" because of vsprintf.
133
        //See unit tests for examples.
134 9
        for ($i = 0; $i < $count; $i += 2) {
135 9
            $tokens[$i] = str_replace(['{@}', '{}', '%'], ['@', '}', '%%'], $tokens[$i]);
136 9
        }
137
138 9
        return [implode('', $tokens), $tags];
139
    }
140
141
    /**
142
     * Removes the superfluous from a multi-line description.
143
     *
144
     * When a description has more than one line then it can happen that the second and subsequent lines have an
145
     * additional indentation. This is commonly in use with tags like this:
146
     *
147
     *     {@}since 1.1.0 This is an example
148
     *         description where we have an
149
     *         indentation in the second and
150
     *         subsequent lines.
151
     *
152
     * If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent
153
     * lines and this may cause rendering issues when, for example, using a Markdown converter.
154
     *
155
     * @param string $contents
156
     *
157
     * @return string
158
     */
159 9
    private function removeSuperfluousStartingWhitespace($contents)
160
    {
161 9
        $lines = explode("\n", $contents);
162
163
        // if there is only one line then we don't have lines with superfluous whitespace and
164
        // can use the contents as-is
165 9
        if (count($lines) <= 1) {
166 8
            return $contents;
167
        }
168
169
        // determine how many whitespace characters need to be stripped
170 1
        $startingSpaceCount = 9999999;
171 1
        for ($i = 1; $i < count($lines); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
172
            // lines with a no length do not count as they are not indented at all
173 1
            if (strlen(trim($lines[$i])) === 0) {
174 1
                continue;
175
            }
176
177
            // determine the number of prefixing spaces by checking the difference in line length before and after
178
            // an ltrim
179 1
            $startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i])));
180 1
        }
181
182
        // strip the number of spaces from each line
183 1
        if ($startingSpaceCount > 0) {
184 1
            for ($i = 1; $i < count($lines); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
185 1
                $lines[$i] = substr($lines[$i], $startingSpaceCount);
186 1
            }
187 1
        }
188
189 1
        return implode("\n", $lines);
190
    }
191
192
}
193