Completed
Push — master ( a5b1eb...d550ab )
by Julius
04:40
created

PhpDomainBuilder::addAfterIntroduction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
106
            case Interface_::class:
107
                return 'interface';
108
                break;
109
            case Trait_::class:
110
                return 'trait';
111
                break;
112
            case Function_::class:
113
                return 'function';
114
                break;
115
            case Method::class:
116
                return 'method';
117
            default:
118
                return '';
119
        }
120
    }
121
122
    protected function addAfterIntroduction($element) {
123
        $this->callExtensions(self::SECTION_AFTER_INTRODUCTION, $element);
124
    }
125
126
127
    protected function addConstants($constants) {
128
        if (count($constants) > 0) {
129
            $this->addH2('Constants');
130
            foreach ($constants as $constant) {
131
                if ($this->shouldRenderElement($constant)) {
132
                    $this->addConstant($constant);
133
                }
134
            }
135
        }
136
    }
137
138
    /**
139
     * @param Constant $constant
140
     */
141 View Code Duplication
    private function addConstant(Constant $constant) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
142
        $this->beginPhpDomain('const', $constant->getName() . ' = ' . $constant->getValue());
143
        $docBlock = $constant->getDocBlock();
144
        $this->addDocBlockDescription($constant);
145
        if ($docBlock) {
146
            foreach ($docBlock->getTags() as $tag) {
147
                $this->addDocblockTag($tag->getName(), $docBlock);
148
            }
149
        }
150
        $this->endPhpDomain();
151
    }
152
153
    /**
154
     * @param Property[] $properties
155
     */
156
    protected function addProperties($properties) {
157
        if (count($properties) > 0) {
158
            $this->addH2('Properties');
159
            foreach ($properties as $property) {
160
                if ($this->shouldRenderElement($property)) {
161
                    $this->addProperty($property);
162
                }
163
            }
164
        }
165
    }
166
167
    /**
168
     * @param Property $property
169
     */
170 View Code Duplication
    private function addProperty(Property $property) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
171
        $this->beginPhpDomain('attr', $property->getName());
172
        $docBlock = $property->getDocBlock();
173
        $this->addDocBlockDescription($property);
174
        if ($docBlock) {
175
            foreach ($docBlock->getTags() as $tag) {
176
                $this->addDocblockTag($tag->getName(), $docBlock);
177
            }
178
        }
179
        $this->endPhpDomain();
180
    }
181
182
    /**
183
     * @param Interface_|Class_|Trait_ $element
184
     */
185
    protected function addParent($element) {
186 View Code Duplication
        if ($element instanceof Class_) {
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...
187
            $parent = $element->getParent();
188
            if ($parent !== null) {
189
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('class', $parent) : '');
190
            }
191
        }
192 View Code Duplication
        if ($element instanceof Trait_) {
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...
193
            $parent = $element->getParent();
1 ignored issue
show
Bug introduced by
The method getParent() does not exist on phpDocumentor\Reflection\Php\Trait_. ( Ignorable by Annotation )

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

193
            $parent = $element->/** @scrutinizer ignore-call */ getParent();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getParent() does not exist on phpDocumentor\Reflection\Php\Interface_. Did you maybe mean getParents()? ( Ignorable by Annotation )

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

193
            $parent = $element->/** @scrutinizer ignore-call */ getParent();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
194
            if ($parent !== null) {
195
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('trait', $parent) : '');
196
            }
197
        }
198
        if ($element instanceof Interface_) {
199
            $parents = $element->getParents();
200
            foreach ($parents as $parent) {
201
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('interface', $parent) : '');
202
            }
203
        }
204
    }
205
206
    /**
207
     * @param Class_|Trait_ $element
208
     */
209
    protected function addUsedTraits($element) {
210
        $usedTraits = '';
211
        foreach ($element->getUsedTraits() as $trait) {
212
            $usedTraits .= $this->getLink('trait', $trait) . ' ';
213
        }
214
        if ($usedTraits !== '') {
215
            $this->addFieldList('Used traits', $usedTraits);
216
        }
217
    }
218
219
    protected function addMethods($methods) {
220
        if (count($methods) > 0) {
221
            $this->addH2('Methods');
222
            foreach ($methods as $method) {
223
                $this->addMethod($method);
224
            }
225
        }
226
    }
227
228
    private function addMethod(Method $method) {
229
        if (!$this->shouldRenderElement($method)) {
230
            return;
231
        }
232
        $docBlock = $method->getDocBlock();
233
        $params = [];
234
        if ($docBlock !== null) {
235
            /** @var Param $param */
236
            foreach ($docBlock->getTagsByName('param') as $param) {
237
                $params[$param->getVariableName()] = $param;
1 ignored issue
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

237
                $params[$param->/** @scrutinizer ignore-call */ getVariableName()] = $param;
Loading history...
238
            }
239
        }
240
        $args = '';
241
        /** @var Argument $argument */
242
        foreach ($method->getArguments() as $argument) {
243
            // TODO: defaults, types
244
            $args .= ' $' . $argument->getName() . ', ';
245
        }
246
        $args = substr($args, 0, -2);
247
248
        $modifiers = $method->getVisibility();
249
        $modifiers .= $method->isAbstract() ? ' abstract' : '';
250
        $modifiers .= $method->isFinal() ? ' final' : '';
251
        $modifiers .= $method->isStatic() ? ' static' : '';
252
        $this->addLine('.. rst-class:: ' . $modifiers)->addLine();
253
        $this->indent();
254
        $this->beginPhpDomain('method', $method->getName() . '(' . $args . ')');
255
        $this->addDocBlockDescription($method);
256
        $this->addLine();
257 View Code Duplication
        if (!empty($params)) {
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...
258
            foreach ($method->getArguments() as $argument) {
259
                /** @var Param $param */
260
                $param = $params[$argument->getName()];
261
                if ($param !== null) $this->addMultiline(':param ' . self::escape($param->getType()) . ' $' . $argument->getName() . ': ' . $param->getDescription(), true);
262
            }
263
        }
264
        $this->endPhpDomain('method');
265
        $this->unindent();
266
    }
267
268
    /**
269
     * @param $type string
270
     * @param $fqsen string
271
     * @return string
272
     */
273
    public static function getLink($type, $fqsen, $description='') {
274
        if($description !== '') {
275
            return ':php:' . $type . ':`' . RstBuilder::escape($description) . '<' . RstBuilder::escape(substr($fqsen, 1)) . '>`';
276
        }
277
        return ':php:' . $type . ':`' . RstBuilder::escape(substr($fqsen, 1)) . '`';
278
    }
279
280
    /**
281
     * @param $type string
282
     * @param $name string
283
     * @param $indent bool Should indent after the section started
284
     */
285
    public function beginPhpDomain($type, $name, $indent = true) {
286
        // FIXME: Add checks if it is properly ended
287
        $this->addLine('.. php:' . $type . ':: ' . $name)->addLine();
288
        if ($indent === true) {
289
            $this->indent();
290
        }
291
    }
292
293
    /**
294
     * @param string $type
295
     */
296
    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

296
    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...
297
        $this->unindent();
298
        $this->addLine();
299
    }
300
301
    /**
302
     * @param Class_|Interface_|Trait_|Property|Method|Constant $element
303
     * @return $this
304
     */
305
    public function addDocBlockDescription($element) {
306
        if ($element === null) {
307
            return;
308
        }
309
        $docBlock = $element->getDocBlock();
310
        $this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element);
311
        if ($docBlock !== null && $docBlock->getSummary() !== '') {
312
            $this->addLine('.. rst-class:: phpdoc-description')->addLine();
313
            $this->addLine('::')->addLine();
314
            $this->indent();
315
            $this->addMultiline($docBlock->getSummary())->addLine();
316
            $this->addMultiline($docBlock->getDescription())->addLine();
317
            $this->unindent();
318
        }
319
        $this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element);
320
        return $this;
321
    }
322
323
    /**
324
     * @param string $tagName Name of the tag to parse
325
     * @param DocBlock $docBlock
326
     */
327
    protected function addDocblockTag($tagName, DocBlock $docBlock) {
328
        $tags = $docBlock->getTagsByName($tagName);
329
        switch ($tagName) {
330 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...
331
                if (count($tags) === 0) continue;
332
                /** @var Return_ $return */
333
                $return = $tags[0];
334
                $this->addMultiline(':Returns: ' . $return->getType() . ' ' . RstBuilder::escape($return->getDescription()), true);
335
                break;
336 View Code Duplication
            case 'var':
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...
337
                if (count($tags) === 0) continue;
338
                /** @var DocBlock\Tags\Var_ $return */
339
                $return = $tags[0];
340
                $this->addMultiline(':Type: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true);
341
                break;
342 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...
343
                if (count($tags) === 0) continue;
344
                /** @var Throws $tag */
345
                foreach ($tags as $tag) {
346
                    $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

346
                    $this->addMultiline(':Throws: ' . $tag->/** @scrutinizer ignore-call */ getType() . ' ' . RstBuilder::escape($tag->getDescription()), true);
Loading history...
347
                }
348
                break;
349 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...
350
                if (count($tags) === 0) continue;
351
                /** @var Since $return */
352
                $return = $tags[0];
353
                $this->addMultiline(':Since: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
354
                break;
355 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...
356
                if (count($tags) === 0) continue;
357
                /** @var Deprecated $return */
358
                $return = $tags[0];
359
                $this->addMultiline(':Deprecated: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true);
360
                break;
361 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...
362
                if (count($tags) === 0) continue;
363
                /** @var See $return */
364
                $return = $tags[0];
365
                $this->addMultiline(':See: ' . $return->getReference() . ' ' . RstBuilder::escape($return->getDescription()), true);
366
                break;
367 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...
368
                if (count($tags) === 0) continue;
369
                /** @var DocBlock\Tags\BaseTag $return */
370
                $return = $tags[0];
371
                $this->addMultiline(':License: ' . RstBuilder::escape($return->getDescription()), true);
372
                break;
373
            case 'param':
374
                // param handling is done by subclasses since it is more that docbook parsing
375
                break;
376
            default:
377
                //echo 'Tag handling not defined for: ' . $tag . PHP_EOL;
378
                break;
379
        }
380
381
    }
382
383
    public static function typesToRst($types) {
384
        // http://docs.phpdoc.org/guides/types.html
385
        $whitelist = [
386
            'string', 'int', 'integer', 'float', 'bool', 'boolean', 'array', 'resource', 'null', 'callable',
387
            'mixed', 'void', 'object', 'false', 'true', 'self', 'static', '$this'
388
        ];
389
        $types = explode('|', $types);
390
        $result = '';
391
        /** @var string $type */
392
        foreach ($types as $type) {
393
            $type = str_replace('[]', '', $type);
394
            if (in_array($type, $whitelist)) {
395
                $result .= $type . ' | ';
396
                continue;
397
            }
398
            if (0 === strpos($type, '\\'))
399
                $type = substr($type, 1);
400
            // we could use :any: here but resolve_any_xref is not implemented by sphinxcontrib.phpdomain
401
            // FIXME: once https://github.com/markstory/sphinxcontrib-phpdomain/pull/14 is merged
402
            // $result .= ':any:`' . RstBuilder::escape($type) . '` | ';
403
            $result .= '`' . RstBuilder::escape($type) . '` | ';
404
        }
405
        return substr($result, 0, -3);
406
    }
407
408
    public function shouldRenderElement(Element $element) {
409
        /** @var Extension $extension */
410
        foreach ($this->extensions as $extension) {
411
            if ($extension->shouldRenderElement($element) === false) {
412
                return false;
413
            }
414
        }
415
        return true;
416
    }
417
418
419
}