Passed
Push — master ( bbff4e...b34341 )
by Julius
01:48
created

PhpDomainBuilder::addDocBlockDescription()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 13
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 17
rs 8.8571
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Julius Härtl <[email protected]>
4
 *
5
 * @author Julius Härtl <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 *  This program is free software: you can redistribute it and/or modify
10
 *  it under the terms of the GNU Affero General Public License as
11
 *  published by the Free Software Foundation, either version 3 of the
12
 *  License, or (at your option) any later version.
13
 *
14
 *  This program is distributed in the hope that it will be useful,
15
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 *  GNU Affero General Public License for more details.
18
 *
19
 *  You should have received a copy of the GNU Affero General Public License
20
 *  along with this program. If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace JuliusHaertl\PHPDocToRst\Builder;
25
26
use JuliusHaertl\PHPDocToRst\Extension\Extension;
27
use phpDocumentor\Reflection\DocBlock;
28
use phpDocumentor\Reflection\DocBlock\Tags\Param;
29
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
30
use phpDocumentor\Reflection\DocBlock\Tags\See;
31
use phpDocumentor\Reflection\DocBlock\Tags\Since;
32
use phpDocumentor\Reflection\DocBlock\Tags\Throws;
33
use phpDocumentor\Reflection\Element;
34
use phpDocumentor\Reflection\Php\Argument;
35
use phpDocumentor\Reflection\Php\Class_;
36
use phpDocumentor\Reflection\Php\Constant;
37
use phpDocumentor\Reflection\Php\Function_;
38
use phpDocumentor\Reflection\Php\Interface_;
39
use phpDocumentor\Reflection\Php\Method;
40
use phpDocumentor\Reflection\Php\Property;
41
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
42
use phpDocumentor\Reflection\Php\Trait_;
43
44
/**
45
 * Class to build reStructuredText file with sphinxcontrib-phpdomain syntax
46
 *
47
 * @package JuliusHaertl\PHPDocToRst\Builder
48
 */
49
class PhpDomainBuilder extends RstBuilder {
50
51
    const SECTION_BEFORE_DESCRIPTION = self::class . '::SECTION_BEFORE_DESCRIPTION';
52
    const SECTION_AFTER_DESCRIPTION = self::class . '::SECTION_AFTER_DESCRIPTION';
53
    const SECTION_AFTER_TITLE = self::class . '::SECTION_AFTER_TITLE';
54
    const SECTION_AFTER_INTRODUCTION = self::class . '::SECTION_AFTER_INTRODUCTION';
55
56
    use ExtensionBuilder {
57
        ExtensionBuilder::__construct as private __extensionConstructor;
58
    }
59
60
    public function __construct($extensions) {
61
        $this->__extensionConstructor($extensions);
62
        $this->addMultiline('.. role:: php(code)' . PHP_EOL . ':language: php', true);
63
        $this->addLine();
64
    }
65
66
    /**
67
     * Strip element name from Fqsen to return the namespace only
68
     *
69
     * @param Element $element
70
     * @return mixed
71
     */
72
    public static function getNamespace(Element $element) {
73
        return substr($element->getFqsen(), 0, strlen($element->getFqsen())-strlen('\\'. $element->getName()));
74
        //return str_replace('\\' . $element->getName(), '', $element->getFqsen());
75
    }
76
77
    /**
78
     * Add namespace
79
     * @param Element $element
80
     */
81
    protected function addPageHeader(Element $element) {
82
        $this->addH1(self::escape($element->getName()))->addLine();
83
        if (self::getNamespace($element) !== '') {
84
            $this->beginPhpDomain('namespace', substr(self::getNamespace($element), 1), false);
85
        }
86
        if ($element instanceof Class_) {
87
            $modifiers = $element->isAbstract() ? ' abstract' : '';
88
            $modifiers = $element->isFinal() ? ' final' : $modifiers;
89
            if ($modifiers !== '') {
90
                $this->addLine('.. rst-class:: ' . $modifiers)->addLine();
91
            }
92
        }
93
94
        $this->callExtensions(self::SECTION_AFTER_TITLE, $element);
95
96
97
        $this->beginPhpDomain($this->getTypeForClass($element), $element->getName(), false);
98
        $this->addLine();
99
    }
100
101
    private function getTypeForClass($element) {
102
        switch (get_class($element)) {
103
            case Class_::class:
104
                return 'class';
105
            case Interface_::class:
106
                return 'interface';
107
            case Trait_::class:
108
                return 'trait';
109
            case Function_::class:
110
                return 'function';
111
            case Method::class:
112
                return 'method';
113
            default:
114
                return '';
115
        }
116
    }
117
118
    protected function addAfterIntroduction($element) {
119
        $this->callExtensions(self::SECTION_AFTER_INTRODUCTION, $element);
120
    }
121
122
123
    protected function addConstants($constants) {
124
        if (count($constants) > 0) {
125
            $this->addH2('Constants');
126
            foreach ($constants as $constant) {
127
                if ($this->shouldRenderElement($constant)) {
128
                    $this->addConstant($constant);
129
                }
130
            }
131
        }
132
    }
133
134
    /**
135
     * @param Constant $constant
136
     */
137
    private function addConstant(Constant $constant) {
138
        $this->beginPhpDomain('const', $constant->getName() . ' = ' . self::escape($constant->getValue()));
139
        $docBlock = $constant->getDocBlock();
140
        $this->addDocBlockDescription($constant);
141
        if ($docBlock) {
142
            foreach ($docBlock->getTags() as $tag) {
143
                $this->addDocblockTag($tag->getName(), $docBlock);
144
            }
145
        }
146
        $this->endPhpDomain();
147
    }
148
149
    /**
150
     * @param Property[] $properties
151
     */
152
    protected function addProperties($properties) {
153
        if (count($properties) > 0) {
154
            $this->addH2('Properties');
155
            foreach ($properties as $property) {
156
                if ($this->shouldRenderElement($property)) {
157
                    $this->addProperty($property);
158
                }
159
            }
160
        }
161
    }
162
163
    /**
164
     * @param Property $property
165
     */
166
    private function addProperty(Property $property) {
167
        $modifiers = $property->isStatic() ? '' : ' static' ;
168
        $this->beginPhpDomain('attr', $property->getVisibility() . $modifiers . ' ' . $property->getName());
169
        $docBlock = $property->getDocBlock();
170
        $this->addDocBlockDescription($property);
171
        if ($docBlock) {
172
            foreach ($docBlock->getTags() as $tag) {
173
                $this->addDocblockTag($tag->getName(), $docBlock);
174
            }
175
        }
176
        $this->endPhpDomain();
177
    }
178
179
    /**
180
     * @param Interface_|Class_ $element
181
     */
182
    protected function addParent($element) {
183
        if ($element instanceof Class_) {
184
            $parent = $element->getParent();
185
            if ($parent !== null) {
186
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('class', $parent) : '');
187
            }
188
        }
189
        if ($element instanceof Interface_) {
190
            $parents = $element->getParents();
191
            foreach ($parents as $parent) {
192
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('interface', $parent) : '');
193
            }
194
        }
195
    }
196
197
    /**
198
     * @param Class_|Trait_ $element
199
     */
200
    protected function addUsedTraits($element) {
201
        $usedTraits = '';
202
        foreach ($element->getUsedTraits() as $trait) {
203
            $usedTraits .= $this->getLink('trait', $trait) . ' ';
204
        }
205
        if ($usedTraits !== '') {
206
            $this->addFieldList('Used traits', $usedTraits);
207
        }
208
    }
209
210
    /**
211
     * @param $methods
212
     */
213
    protected function addMethods($methods) {
214
        if (count($methods) > 0) {
215
            $this->addH2('Methods');
216
            foreach ($methods as $method) {
217
                $this->addMethod($method);
218
            }
219
        }
220
    }
221
222
    private function addMethod(Method $method) {
223
        if (!$this->shouldRenderElement($method)) {
224
            return;
225
        }
226
        $docBlock = $method->getDocBlock();
227
        $params = [];
228
        $deprecated = [];
229 View Code Duplication
        if ($docBlock !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
230
            /** @var Param $param */
231
            foreach ($docBlock->getTagsByName('param') as $param) {
232
                $params[$param->getVariableName()] = $param;
0 ignored issues
show
Bug introduced by
The method getVariableName() does not exist on phpDocumentor\Reflection\DocBlock\Tag. It seems like you code against a sub-type of phpDocumentor\Reflection\DocBlock\Tag such as phpDocumentor\Reflection\DocBlock\Tags\Property or phpDocumentor\Reflection\DocBlock\Tags\Var_ or phpDocumentor\Reflection\DocBlock\Tags\Param or phpDocumentor\Reflection...lock\Tags\PropertyWrite or phpDocumentor\Reflection...Block\Tags\PropertyRead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

232
                $params[$param->/** @scrutinizer ignore-call */ getVariableName()] = $param;
Loading history...
233
            }
234
            $deprecated = $docBlock->getTagsByName('deprecated');
235
        }
236
        $args = '';
237
        /** @var Argument $argument */
238
        foreach ($method->getArguments() as $argument) {
239
            // This will work after https://github.com/phpDocumentor/Reflection/pull/109 is merged
240
            foreach ($argument->getTypes() as $type) {
241
                $args .= self::escape($type) . '|';
242
            }
243
            $args = substr($args, 0, -1) . ' ';
244
            if($argument->isVariadic()) {
245
                $args .= '...';
246
            }
247
            if($argument->isByReference()) {
248
                $args .= '&';
249
            }
250
            $args .= '$' . $argument->getName();
251
            $default = $argument->getDefault();
252
            if ($default !== null) {
253
                $default = $default === '' ? '""' : $default;
254
                $args .= '=' . self::escape($default);
255
            }
256
            $args .= ', ';
257
        }
258
        $args = substr($args, 0, -2);
259
260
        $modifiers = $method->getVisibility();
261
        $modifiers .= $method->isAbstract() ? ' abstract' : '';
262
        $modifiers .= $method->isFinal() ? ' final' : '';
263
        $modifiers .= $method->isStatic() ? ' static' : '';
264
        $deprecated = count($deprecated) > 0 ? ' deprecated' : '';
265
        $this->addLine('.. rst-class:: ' . $modifiers . $deprecated)->addLine();
266
        $this->indent();
267
        $this->beginPhpDomain('method', $modifiers . ' ' . $method->getName() . '(' . $args . ')');
268
        $this->addDocBlockDescription($method);
269
        $this->addLine();
270
        if (!empty($params)) {
271
            $parameterDetails = '';
272
            foreach ($method->getArguments() as $argument) {
273
                /** @var Param $param */
274
                $param = $params[$argument->getName()];
275
                if ($param !== null) {
276
                    $typString = $param->getType();
277
                    // Remove first \ to allow references
278
                    if (0 === strpos($typString, '\\')) {
279
                        $typString = substr($typString, 1);
280
                    }
281
                    $paramItem = '* ';
282
                    $paramItem .= '**$' . $argument->getName() . '** ';
283
                    if ($typString !== null) {
284
                        $paramItem .= '(' . self::typesToRst($typString) . ') ';
285
                    }
286
                    $paramItem .= ' ' . $param->getDescription();
287
                    $parameterDetails .= $paramItem . PHP_EOL;
288
                }
289
            }
290
            $this->addFieldList('Parameters', $parameterDetails);
291
        }
292
        if ($docBlock !== null) {
293
            foreach ($docBlock->getTags() as $tag) {
294
                $this->addDocblockTag($tag->getName(), $docBlock);
295
            }
296
        }
297
        $this->endPhpDomain('method');
298
        $this->unindent();
299
    }
300
301
    /**
302
     * @param $type string
303
     * @param $fqsen string
304
     * @return string
305
     */
306
    public static function getLink($type, $fqsen, $description='') {
307
        if($description !== '') {
308
            return ':php:' . $type . ':`' . RstBuilder::escape($description) . '<' . RstBuilder::escape(substr($fqsen, 1)) . '>`';
309
        }
310
        return ':php:' . $type . ':`' . RstBuilder::escape(substr($fqsen, 1)) . '`';
311
    }
312
313
    /**
314
     * @param $type string
315
     * @param $name string
316
     * @param $indent bool Should indent after the section started
317
     */
318
    public function beginPhpDomain($type, $name, $indent = true) {
319
        // FIXME: Add checks if it is properly ended
320
        $this->addLine('.. php:' . $type . ':: ' . $name)->addLine();
321
        if ($indent === true) {
322
            $this->indent();
323
        }
324
    }
325
326
    /**
327
     * @param string $type
328
     * @return $this
329
     */
330
    public function endPhpDomain($type = '') {
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

330
    public function endPhpDomain(/** @scrutinizer ignore-unused */ $type = '') {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
331
        $this->unindent();
332
        $this->addLine();
333
    }
334
335
    /**
336
     * @param Class_|Interface_|Trait_|Property|Method|Constant $element
337
     * @return $this
338
     */
339
    public function addDocBlockDescription($element) {
340
        if ($element === null) {
341
            return $this;
342
        }
343
        $docBlock = $element->getDocBlock();
344
        $this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element);
345
        if ($docBlock !== null && $docBlock->getSummary() !== '') {
346
            $this->addLine('.. rst-class:: phpdoc-description')->addLine();
347
            $this->indent();
348
            $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getSummary()))->addLine();
349
            if ((string)$docBlock->getDescription() !== '') {
350
                $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getDescription()))->addLine();
351
            }
352
            $this->unindent();
353
        }
354
        $this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element);
355
        return $this;
356
    }
357
358
    /**
359
     * @param string $tagName Name of the tag to parse
360
     * @param DocBlock $docBlock
361
     */
362
    protected function addDocblockTag($tagName, DocBlock $docBlock) {
363
        $tags = $docBlock->getTagsByName($tagName);
364
        switch ($tagName) {
365 View Code Duplication
            case 'return':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
366
                if (count($tags) === 0) continue;
367
                /** @var Return_ $return */
368
                $return = $tags[0];
369
                $this->addMultiline(':Returns: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true);
370
                break;
371
            case 'var':
372
                if (count($tags) === 0) continue;
373
                /** @var DocBlock\Tags\Var_ $return */
374
                $return = $tags[0];
375
                $this->addMultiline(':Type: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true);
376
                break;
377 View Code Duplication
            case 'throws':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
378
                if (count($tags) === 0) continue;
379
                /** @var Throws $tag */
380
                foreach ($tags as $tag) {
381
                    $this->addMultiline(':Throws: ' . self::typesToRst($tag->getType()) . ' ' . RstBuilder::escape($tag->getDescription()), true);
0 ignored issues
show
Bug introduced by
The method getType() does not exist on phpDocumentor\Reflection\DocBlock\Tag. It seems like you code against a sub-type of phpDocumentor\Reflection\DocBlock\Tag such as phpDocumentor\Reflection\DocBlock\Tags\Property or phpDocumentor\Reflection\DocBlock\Tags\Var_ or phpDocumentor\Reflection\DocBlock\Tags\Param or phpDocumentor\Reflection...lock\Tags\PropertyWrite or phpDocumentor\Reflection\DocBlock\Tags\Return_ or phpDocumentor\Reflection\DocBlock\Tags\Throws or phpDocumentor\Reflection...Block\Tags\PropertyRead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

381
                    $this->addMultiline(':Throws: ' . self::typesToRst($tag->/** @scrutinizer ignore-call */ getType()) . ' ' . RstBuilder::escape($tag->getDescription()), true);
Loading history...
382
                }
383
                break;
384 View Code Duplication
            case 'since':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
385
                if (count($tags) === 0) continue;
386
                /** @var Since $return */
387
                $return = $tags[0];
388
                $this->addMultiline(':Since: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
389
                break;
390 View Code Duplication
            case 'deprecated':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
                if (count($tags) === 0) continue;
392
                /** @var Deprecated $return */
393
                $return = $tags[0];
394
                $this->addMultiline(':Deprecated: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
395
                break;
396
            case 'see':
397
                if (count($tags) === 0) continue;
398
                /** @var See $return */
399
                $return = $tags[0];
400
                $this->addMultiline(':See: ' . self::typesToRst($return->getReference()) . ' ' . RstBuilder::escape($return->getDescription()), true);
401
                break;
402 View Code Duplication
            case 'license':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
403
                if (count($tags) === 0) continue;
404
                /** @var DocBlock\Tags\BaseTag $return */
405
                $return = $tags[0];
406
                $this->addMultiline(':License: ' . RstBuilder::escape($return->getDescription()), true);
407
                break;
408
            case 'param':
409
                // param handling is done by subclasses since it is more that docbook parsing
410
                break;
411
            default:
412
                //echo 'Tag handling not defined for: ' . $tag . PHP_EOL;
413
                break;
414
        }
415
416
    }
417
418
    /**
419
     * @param string $typesString
420
     * @return bool|string
421
     */
422
    public static function typesToRst($typesString) {
423
        // http://docs.phpdoc.org/guides/types.html
424
        $whitelist = [
425
            'string', 'int', 'integer', 'float', 'bool', 'boolean', 'array', 'resource', 'null', 'callable',
426
            'mixed', 'void', 'object', 'false', 'true', 'self', 'static', '$this'
427
        ];
428
        $types = explode('|', $typesString);
429
        $result = '';
430
        /** @var string $type */
431
        foreach ($types as $typeFull) {
432
            $type = str_replace('[]', '', $typeFull);
433
            if (in_array($type, $whitelist, true)) {
434
                $result .= $typeFull . ' | ';
435
                continue;
436
            }
437
            if (0 === strpos($type, '\\'))
438
                $type = substr($type, 1);
439
            $result .= ':any:`' . RstBuilder::escape($typeFull) . ' <' . RstBuilder::escape($type) . '>` | ';
440
        }
441
        return substr($result, 0, -3);
442
    }
443
444
    /**
445
     * @param Element $element
446
     * @return bool
447
     */
448
    public function shouldRenderElement(Element $element) {
449
        /** @var Extension $extension */
450
        foreach ($this->extensions as $extension) {
451
            if ($extension->shouldRenderElement($element) === false) {
452
                return false;
453
            }
454
        }
455
        return true;
456
    }
457
458
459
}