Passed
Push — master ( 682fcd...f00a38 )
by Julius
01:55
created

PhpDomainBuilder   F

Complexity

Total Complexity 94

Size/Duplication

Total Lines 392
Duplicated Lines 9.44 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
dl 37
loc 392
rs 1.5789
c 3
b 0
f 0
wmc 94

20 Methods

Rating   Name   Duplication   Size   Complexity  
A addUsedTraits() 0 7 3
A __construct() 0 4 1
A getNamespace() 0 2 1
B addPageHeader() 0 18 6
A addAfterIntroduction() 0 2 1
A addConstants() 0 6 4
A addProperties() 0 6 4
B getTypeForClass() 0 14 6
A typesToRst() 0 20 4
A beginPhpDomain() 0 5 2
A shouldRenderElement() 0 8 3
A addDocBlockDescription() 0 15 4
A addProperty() 0 11 4
A addMethods() 0 5 3
C addDocblockTag() 37 52 17
B addParent() 0 11 7
A endPhpDomain() 0 3 1
F addMethod() 0 64 18
A addConstant() 0 10 3
A getLink() 0 5 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PhpDomainBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PhpDomainBuilder, and based on these observations, apply Extract Interface, too.

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() . ' = ' . $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
        if ($docBlock !== null) {
229
            /** @var Param $param */
230
            foreach ($docBlock->getTagsByName('param') as $param) {
231
                $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

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

317
    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...
318
        $this->unindent();
319
        $this->addLine();
320
    }
321
322
    /**
323
     * @param Class_|Interface_|Trait_|Property|Method|Constant $element
324
     * @return $this
325
     */
326
    public function addDocBlockDescription($element) {
327
        if ($element === null) {
328
            return;
329
        }
330
        $docBlock = $element->getDocBlock();
331
        $this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element);
332
        if ($docBlock !== null && $docBlock->getSummary() !== '') {
333
            $this->addLine('.. rst-class:: phpdoc-description')->addLine();
334
            $this->indent();
335
            $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getSummary()))->addLine();
336
            $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getDescription()))->addLine();
337
            $this->unindent();
338
        }
339
        $this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element);
340
        return $this;
341
    }
342
343
    /**
344
     * @param string $tagName Name of the tag to parse
345
     * @param DocBlock $docBlock
346
     */
347
    protected function addDocblockTag($tagName, DocBlock $docBlock) {
348
        $tags = $docBlock->getTagsByName($tagName);
349
        switch ($tagName) {
350 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...
351
                if (count($tags) === 0) continue;
352
                /** @var Return_ $return */
353
                $return = $tags[0];
354
                $this->addMultiline(':Returns: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true);
355
                break;
356
            case 'var':
357
                if (count($tags) === 0) continue;
358
                /** @var DocBlock\Tags\Var_ $return */
359
                $return = $tags[0];
360
                $this->addMultiline(':Type: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true);
361
                break;
362 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...
363
                if (count($tags) === 0) continue;
364
                /** @var Throws $tag */
365
                foreach ($tags as $tag) {
366
                    $this->addMultiline(':Throws: ' . $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

366
                    $this->addMultiline(':Throws: ' . $tag->/** @scrutinizer ignore-call */ getType() . ' ' . RstBuilder::escape($tag->getDescription()), true);
Loading history...
367
                }
368
                break;
369 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...
370
                if (count($tags) === 0) continue;
371
                /** @var Since $return */
372
                $return = $tags[0];
373
                $this->addMultiline(':Since: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
374
                break;
375 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...
376
                if (count($tags) === 0) continue;
377
                /** @var Deprecated $return */
378
                $return = $tags[0];
379
                $this->addMultiline(':Deprecated: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
380
                break;
381 View Code Duplication
            case 'see':
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...
382
                if (count($tags) === 0) continue;
383
                /** @var See $return */
384
                $return = $tags[0];
385
                $this->addMultiline(':See: ' . $return->getReference() . ' ' . RstBuilder::escape($return->getDescription()), true);
386
                break;
387 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...
388
                if (count($tags) === 0) continue;
389
                /** @var DocBlock\Tags\BaseTag $return */
390
                $return = $tags[0];
391
                $this->addMultiline(':License: ' . RstBuilder::escape($return->getDescription()), true);
392
                break;
393
            case 'param':
394
                // param handling is done by subclasses since it is more that docbook parsing
395
                break;
396
            default:
397
                //echo 'Tag handling not defined for: ' . $tag . PHP_EOL;
398
                break;
399
        }
400
401
    }
402
403
    /**
404
     * @param string $typesString
405
     * @return bool|string
406
     */
407
    public static function typesToRst($typesString) {
408
        // http://docs.phpdoc.org/guides/types.html
409
        $whitelist = [
410
            'string', 'int', 'integer', 'float', 'bool', 'boolean', 'array', 'resource', 'null', 'callable',
411
            'mixed', 'void', 'object', 'false', 'true', 'self', 'static', '$this'
412
        ];
413
        $types = explode('|', $typesString);
414
        $result = '';
415
        /** @var string $type */
416
        foreach ($types as $type) {
417
            $type = str_replace('[]', '', $type);
418
            if (in_array($type, $whitelist, true)) {
419
                $result .= $type . ' | ';
420
                continue;
421
            }
422
            if (0 === strpos($type, '\\'))
423
                $type = substr($type, 1);
424
            $result .= ':any:`' . RstBuilder::escape($type) . '` | ';
425
        }
426
        return substr($result, 0, -3);
427
    }
428
429
    /**
430
     * @param Element $element
431
     * @return bool
432
     */
433
    public function shouldRenderElement(Element $element) {
434
        /** @var Extension $extension */
435
        foreach ($this->extensions as $extension) {
436
            if ($extension->shouldRenderElement($element) === false) {
437
                return false;
438
            }
439
        }
440
        return true;
441
    }
442
443
444
}