Completed
Push — master ( b73a2b...b21eaf )
by Jaap
25:34 queued 15:36
created

src/DocBlockFactory.php (2 issues)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of phpDocumentor.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * @link      http://phpdoc.org
12
 */
13
14
namespace phpDocumentor\Reflection;
15
16
use InvalidArgumentException;
17
use LogicException;
18
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
19
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
20
use phpDocumentor\Reflection\DocBlock\TagFactory;
21
use phpDocumentor\Reflection\DocBlock\Tags\Factory\StaticMethod;
22
use Webmozart\Assert\Assert;
23
use function array_shift;
24
use function count;
25
use function explode;
26
use function is_object;
27
use function method_exists;
28
use function preg_match;
29
use function preg_replace;
30
use function str_replace;
31
use function strpos;
32
use function substr;
33
use function trim;
34
35 13
final class DocBlockFactory implements DocBlockFactoryInterface
36
{
37 13
    /** @var DocBlock\DescriptionFactory */
38 13
    private $descriptionFactory;
39 13
40
    /** @var DocBlock\TagFactory */
41
    private $tagFactory;
42
43
    /**
44
     * Initializes this factory with the required subcontractors.
45
     */
46
    public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory)
47
    {
48 1
        $this->descriptionFactory = $descriptionFactory;
49
        $this->tagFactory         = $tagFactory;
50 1
    }
51 1
52 1
    /**
53
     * Factory method for easy instantiation.
54 1
     *
55 1
     * @param array<class-string<StaticMethod>> $additionalTags
0 ignored issues
show
The doc-type array<class-string<StaticMethod>> could not be parsed: Unknown type name "class-string" at position 6. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
56
     */
57 1
    public static function createInstance(array $additionalTags = []) : self
58 1
    {
59
        $fqsenResolver      = new FqsenResolver();
60
        $tagFactory         = new StandardTagFactory($fqsenResolver);
61
        $descriptionFactory = new DescriptionFactory($tagFactory);
62 1
63
        $tagFactory->addService($descriptionFactory);
64
        $tagFactory->addService(new TypeResolver($fqsenResolver));
65
66
        $docBlockFactory = new self($descriptionFactory, $tagFactory);
67
        foreach ($additionalTags as $tagName => $tagHandler) {
68
            $docBlockFactory->registerTagHandler($tagName, $tagHandler);
69
        }
70
71
        return $docBlockFactory;
72
    }
73 12
74
    /**
75 12
     * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the
76 1
     *                                getDocComment method (such as a ReflectionClass object).
77
     */
78
    public function create($docblock, ?Types\Context $context = null, ?Location $location = null) : DocBlock
79
    {
80
        if (is_object($docblock)) {
81 1
            if (!method_exists($docblock, 'getDocComment')) {
82
                $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
83
84 12
                throw new InvalidArgumentException($exceptionMessage);
85
            }
86 12
87 10
            $docblock = $docblock->getDocComment();
88
        }
89
90 12
        Assert::stringNotEmpty($docblock);
91 12
92
        if ($context === null) {
93 12
            $context = new Types\Context('');
94 12
        }
95 12
96 12
        $parts = $this->splitDocBlock($this->stripDocComment($docblock));
97 2
98 12
        [$templateMarker, $summary, $description, $tags] = $parts;
99 12
100 12
        return new DocBlock(
101 12
            $summary,
102 12
            $description ? $this->descriptionFactory->create($description, $context) : null,
103
            $this->parseTagBlock($tags, $context),
104
            $context,
105
            $location,
106
            $templateMarker === '#@+',
107
            $templateMarker === '#@-'
108
        );
109
    }
110
111
    /**
112
     * @param class-string<StaticMethod> $handler
0 ignored issues
show
The doc-type class-string<StaticMethod> could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
113
     */
114
    public function registerTagHandler(string $tagName, string $handler) : void
115
    {
116
        $this->tagFactory->registerTagHandler($tagName, $handler);
117
    }
118 12
119
    /**
120 12
     * Strips the asterisks from the DocBlock comment.
121
     *
122
     * @param string $comment String containing the comment text.
123 12
     */
124 2
    private function stripDocComment(string $comment) : string
125
    {
126
        $comment = preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment);
127 12
        Assert::string($comment);
128
        $comment = trim($comment);
129
130
        // reg ex above is not able to remove */ from a single line docblock
131
        if (substr($comment, -2) === '*/') {
132
            $comment = trim(substr($comment, 0, -2));
133
        }
134
135
        return str_replace(["\r\n", "\r"], "\n", $comment);
136
    }
137
138
    // phpcs:disable
139
    /**
140 12
     * Splits the DocBlock into a template marker, summary, description and block of tags.
141
     *
142
     * @param string $comment Comment to split into the sub-parts.
143
     *
144
     * @return string[] containing the template marker (if any), summary, description and a string containing the tags.
145 12
     *
146
     * @author Mike van Riel <[email protected]> for extending the regex with template marker support.
147
     *
148
     * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split.
149
     */
150 12
    private function splitDocBlock(string $comment) : array
151
    {
152
        // phpcs:enable
153
        // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This
154
        // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the
155
        // performance impact of running a regular expression
156
        if (strpos($comment, '@') === 0) {
157
            return ['', '', '', $comment];
158
        }
159
160
        // clears all extra horizontal whitespace from the line endings to prevent parsing issues
161
        $comment = preg_replace('/\h*$/Sum', '', $comment);
162
        Assert::string($comment);
163
        /*
164
         * Splits the docblock into a template marker, summary, description and tags section.
165
         *
166 12
         * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may
167 12
         *   occur after it and will be stripped).
168
         * - The short description is started from the first character until a dot is encountered followed by a
169
         *   newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing
170
         *   errors). This is optional.
171
         * - The long description, any character until a new line is encountered followed by an @ and word
172
         *   characters (a tag). This is optional.
173
         * - Tags; the remaining characters
174
         *
175
         * Big thanks to RichardJ for contributing this Regular Expression
176
         */
177
        preg_match(
178
            '/
179
            \A
180
            # 1. Extract the template marker
181
            (?:(\#\@\+|\#\@\-)\n?)?
182
183
            # 2. Extract the summary
184
            (?:
185
              (?! @\pL ) # The summary may not start with an @
186
              (
187
                [^\n.]+
188
                (?:
189
                  (?! \. \n | \n{2} )     # End summary upon a dot followed by newline or two newlines
190
                  [\n.]* (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line
191
                  [^\n.]+                 # Include anything else
192
                )*
193
                \.?
194
              )?
195
            )
196
197
            # 3. Extract the description
198
            (?:
199
              \s*        # Some form of whitespace _must_ precede a description because a summary must be there
200
              (?! @\pL ) # The description may not start with an @
201
              (
202 12
                [^\n]+
203 12
                (?: \n+
204
                  (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line
205 12
                  [^\n]+            # Include anything else
206
                )*
207 12
              )
208 10
            )?
209
210
            # 4. Extract the tags (anything that follows)
211 12
            (\s+ [\s\S]*)? # everything that follows
212
            /ux',
213
            $comment,
214
            $matches
215
        );
216
        array_shift($matches);
217
218
        while (count($matches) < 4) {
219
            $matches[] = '';
220
        }
221
222 12
        return $matches;
223
    }
224 12
225 12
    /**
226 10
     * Creates the tag objects.
227
     *
228
     * @param string        $tags    Tag block to parse.
229 2
     * @param Types\Context $context Context of the parsed Tag
230 2
     *
231 2
     * @return DocBlock\Tag[]
232
     */
233
    private function parseTagBlock(string $tags, Types\Context $context) : array
234 2
    {
235
        $tags = $this->filterTagBlock($tags);
236
        if ($tags === null) {
237
            return [];
238
        }
239
240
        $result = [];
241
        $lines  = $this->splitTagBlockIntoTagLines($tags);
242 2
        foreach ($lines as $key => $tagLine) {
243
            $result[$key] = $this->tagFactory->create(trim($tagLine), $context);
244 2
        }
245 2
246 2
        return $result;
247 2
    }
248
249 2
    /**
250
     * @return string[]
251
     */
252
    private function splitTagBlockIntoTagLines(string $tags) : array
253 2
    {
254
        $result = [];
255
        foreach (explode("\n", $tags) as $tagLine) {
256
            if (isset($tagLine[0]) && ($tagLine[0] === '@')) {
257
                $result[] = $tagLine;
258
            } else {
259
                $result[count($result) - 1] .= "\n" . $tagLine;
260 12
            }
261
        }
262 12
263 12
        return $result;
264 10
    }
265
266
    private function filterTagBlock(string $tags) : ?string
267 2
    {
268
        $tags = trim($tags);
269
        if (!$tags) {
270
            return null;
271
        }
272
273
        if ($tags[0] !== '@') {
274
            // @codeCoverageIgnoreStart
275 2
            // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that
276
            // we didn't foresee.
277
278
            throw new LogicException('A tag block started with text instead of an at-sign(@): ' . $tags);
279
280
            // @codeCoverageIgnoreEnd
281
        }
282
283
        return $tags;
284
    }
285
}
286