Completed
Push — master ( f32f45...e7b229 )
by Arne
45:20 queued 19:37
created

UnitCollectingVisitor::processTraitUse()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 25
nc 12
nop 1
1
<?php
2
    /**
3
     * Copyright (c) 2010-2018 Arne Blankerts <[email protected]>
4
     * All rights reserved.
5
     *
6
     * Redistribution and use in source and binary forms, with or without modification,
7
     * are permitted provided that the following conditions are met:
8
     *
9
     *   * Redistributions of source code must retain the above copyright notice,
10
     *     this list of conditions and the following disclaimer.
11
     *
12
     *   * Redistributions in binary form must reproduce the above copyright notice,
13
     *     this list of conditions and the following disclaimer in the documentation
14
     *     and/or other materials provided with the distribution.
15
     *
16
     *   * Neither the name of Arne Blankerts nor the names of contributors
17
     *     may be used to endorse or promote products derived from this software
18
     *     without specific prior written permission.
19
     *
20
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
     * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT  * NOT LIMITED TO,
22
     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23
     * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS
24
     * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
25
     * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
     * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
     * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28
     * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
     * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
     * POSSIBILITY OF SUCH DAMAGE.
31
     *
32
     * @package    phpDox
33
     * @author     Arne Blankerts <[email protected]>
34
     * @copyright  Arne Blankerts <[email protected]>, All rights reserved.
35
     * @license    BSD License
36
     */
37
namespace TheSeer\phpDox\Collector\Backend {
38
39
    use PhpParser\Node\Expr;
40
    use PhpParser\Node\Expr\Array_;
41
    use PhpParser\Node\Expr\BinaryOp;
42
    use PhpParser\Node\Expr\ClassConstFetch;
43
    use PhpParser\Node\Expr\ConstFetch;
44
    use PhpParser\Node\Expr\UnaryMinus;
45
    use PhpParser\Node\Expr\UnaryPlus;
46
    use PhpParser\Node\Name\FullyQualified;
47
    use PhpParser\Node\NullableType;
48
    use PhpParser\Node\Scalar\DNumber;
49
    use PhpParser\Node\Scalar\LNumber;
50
    use PhpParser\Node\Scalar\MagicConst;
51
    use PhpParser\Node\Scalar\String_;
52
    use TheSeer\phpDox\Collector\AbstractUnitObject;
53
    use TheSeer\phpDox\Collector\AbstractVariableObject;
54
    use TheSeer\phpDox\Collector\InlineComment;
55
    use TheSeer\phpDox\Collector\MethodObject;
56
    use TheSeer\phpDox\DocBlock\Parser as DocBlockParser;
57
    use PhpParser\NodeVisitorAbstract;
58
    use PhpParser\Node\Stmt as NodeType;
59
60
    /**
61
     *
62
     */
63
    class UnitCollectingVisitor extends NodeVisitorAbstract {
64
65
        /**
66
         * @var \TheSeer\phpDox\DocBlock\Parser
67
         */
68
        private $docBlockParser;
69
70
        /**
71
         * @var array
72
         */
73
        private $aliasMap = array();
74
75
        /**
76
         * @var string
77
         */
78
        private $namespace = '\\';
79
80
        /**
81
         * @var ParseResult
82
         */
83
        private $result;
84
85
        /**
86
         * @var AbstractUnitObject
87
         */
88
        private $unit;
89
90
        private $modifier = array(
91
            NodeType\Class_::MODIFIER_PUBLIC    => 'public',
92
            NodeType\Class_::MODIFIER_PROTECTED => 'protected',
93
            NodeType\Class_::MODIFIER_PRIVATE   => 'private',
94
        );
95
96
        /**
97
         * @param \TheSeer\phpDox\DocBlock\Parser $parser
98
         * @param ParseResult                     $result
99
         */
100
        public function __construct(DocBlockParser $parser, ParseResult $result) {
101
            $this->docBlockParser = $parser;
102
            $this->result = $result;
103
        }
104
105
        /**
106
         * @param \PhpParser\Node $node
107
         *
108
         * @return int|null|\PhpParser\Node|void
109
         */
110
        public function enterNode(\PhpParser\Node $node) {
111
            if ($node instanceof NodeType\Namespace_ && $node->name != NULL) {
112
                $this->namespace = implode('\\', $node->name->parts);
113
                $this->aliasMap['::context'] = $this->namespace;
114
            } else if ($node instanceof NodeType\UseUse) {
115
                $this->aliasMap[$node->alias] = implode('\\', $node->name->parts);
116
            } else if ($node instanceof NodeType\Class_) {
117
                $this->aliasMap['::unit'] = (string)$node->namespacedName;
0 ignored issues
show
Bug introduced by Arne Blankerts
The property namespacedName does not seem to exist in PhpParser\Node\Stmt\Class_.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
118
                $this->unit = $this->result->addClass((string)$node->namespacedName);
119
                $this->processUnit($node);
120
                return;
121
            } else if ($node instanceof NodeType\Interface_) {
122
                $this->aliasMap['::unit'] = (string)$node->namespacedName;
0 ignored issues
show
Bug introduced by Arne Blankerts
The property namespacedName does not seem to exist in PhpParser\Node\Stmt\Interface_.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
123
                $this->unit = $this->result->addInterface((string)$node->namespacedName);
124
                $this->processUnit($node);
125
                return;
126
            } else if ($node instanceof NodeType\Trait_) {
127
                $this->aliasMap['::unit'] = (string)$node->namespacedName;
0 ignored issues
show
Bug introduced by Arne Blankerts
The property namespacedName does not seem to exist in PhpParser\Node\Stmt\Trait_.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
128
                $this->unit = $this->result->addTrait((string)$node->namespacedName);
129
                $this->processUnit($node);
130
                return;
131
            } else if ($node instanceof NodeType\Property) {
132
                $this->processProperty($node);
133
                return;
134
            } else if ($node instanceof NodeType\ClassMethod) {
135
                $this->processMethod($node);
136
                return;
137
            } elseif ($node instanceof NodeType\ClassConst) {
138
                $this->processClassConstant($node);
139
            } elseif ($node instanceof NodeType\TraitUse) {
140
                $this->processTraitUse($node);
141
            }
142
        }
143
144
        /**
145
         * @param \PhpParser\Node $node
146
         *
147
         * @return false|int|null|\PhpParser\Node|\PhpParser\Node[]|void
148
         */
149
        public function leaveNode(\PhpParser\Node $node) {
150
            if ($node instanceof NodeType\Class_
151
                || $node instanceof NodeType\Interface_
152
                || $node instanceof NodeType\Trait_) {
153
                $this->unit = NULL;
154
                return;
155
            }
156
        }
157
158
        /**
159
         * @param $node
160
         */
161
        private function processUnit($node) {
162
            $this->unit->setStartLine($node->getAttribute('startLine'));
163
            $this->unit->setEndLine($node->getAttribute('endLine'));
164
            if ($node instanceof NodeType\Class_) {
165
                $this->unit->setAbstract($node->isAbstract());
166
                $this->unit->setFinal($node->isFinal());
167
            } else {
168
                $this->unit->setAbstract(FALSE);
169
                $this->unit->setFinal(FALSE);
170
            }
171
172
            $docComment = $node->getDocComment();
173 View Code Duplication
            if ($docComment !== NULL) {
174
                $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
175
                $this->unit->setDocBlock($block);
176
            }
177
178
            if ($node->getType() != 'Stmt_Trait' && $node->extends !== NULL) {
179
                if (is_array($node->extends)) {
180
                    $extendsArray = $node->extends;
181
                    foreach ($extendsArray as $extends) {
182
                        $this->unit->addExtends(implode('\\', $extends->parts));
183
                    }
184
                } else {
185
                    $this->unit->addExtends(implode('\\', $node->extends->parts));
186
                }
187
            }
188
189
            if ($node->getType() === 'Stmt_Class') {
190
                foreach($node->implements as $implements) {
191
                    $this->unit->addImplements(implode('\\', $implements->parts));
192
                }
193
            }
194
        }
195
196
        private function processTraitUse(NodeType\TraitUse $node) {
197
            foreach($node->traits as $trait) {
198
                $traitUse = $this->unit->addTrait( (string)$trait );
199
                $traitUse->setStartLine($node->getAttribute('startLine'));
200
                $traitUse->setEndLine($node->getAttribute('endLine'));
201
            }
202
203
            foreach($node->adaptations as $adaptation) {
204
                if ($adaptation instanceof NodeType\TraitUseAdaptation\Alias) {
205
                    if ($adaptation->trait instanceof FullyQualified) {
206
                        $traitUse = $this->getTraitUse((string)$adaptation->trait);
207
                    } else if (count($node->traits) === 1) {
208
                        $traitUse = $this->getTraitUse( (string)$node->traits[0]);
209
                    } else {
210
                        $traitUse = $this->unit->getAmbiguousTraitUse();
211
                    }
212
213
                    $traitUse->addAlias(
214
                        $adaptation->method,
215
                        $adaptation->newName,
216
                        $adaptation->newModifier ? $this->modifier[$adaptation->newModifier] : NULL
217
                    );
218
219
                    continue;
220
                }
221
222
                if ($adaptation instanceof NodeType\TraitUseAdaptation\Precedence) {
223
                    $traitUse = $this->getTraitUse((string)$adaptation->insteadof[0]);
224
                    $traitUse->addExclude($adaptation->method);
225
226
                    continue;
227
                }
228
229
                throw new ParseErrorException(
230
                    sprintf('Unexpected adaption type %s', get_class($adaptation)),
231
                    ParseErrorException::UnexpectedExpr
232
                );
233
            }
234
235
        }
236
237
        private function getTraitUse($traitName) {
238
            if (!$this->unit->usesTrait($traitName)) {
239
                throw new ParseErrorException(
240
                    sprintf('Referenced trait "%s" not used', $traitName),
241
                    ParseErrorException::GeneralParseError
242
                );
243
            }
244
            return $this->unit->getTraitUse($traitName);
245
        }
246
247
        /**
248
         * @param NodeType\ClassMethod $node
249
         */
250
        private function processMethod(NodeType\ClassMethod $node) {
251
252
            /** @var $method \TheSeer\phpDox\Collector\MethodObject */
253
            $method = $this->unit->addMethod($node->name);
254
            $method->setStartLine($node->getAttribute('startLine'));
255
            $method->setEndLine($node->getAttribute('endLine'));
256
            $method->setAbstract($node->isAbstract());
257
            $method->setFinal($node->isFinal());
258
            $method->setStatic($node->isStatic());
259
260
            $this->processMethodReturnType($method, $node->getReturnType());
261
262
            $visibility = 'public';
263
            if ($node->isPrivate()) {
264
                $visibility = 'private';
265
            } elseif ($node->isProtected()) {
266
                $visibility = 'protected';
267
            }
268
            $method->setVisibility($visibility);
269
270
            $docComment = $node->getDocComment();
271 View Code Duplication
            if ($docComment !== NULL) {
272
                $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
273
                $method->setDocBlock($block);
274
            }
275
276
            $this->processMethodParams($method, $node->params);
277
278
            if ($node->stmts) {
279
                $this->processInlineComments($method, $node->stmts);
280
            }
281
        }
282
283
        private function processMethodReturnType(MethodObject $method, $returnType) {
284
            if ($returnType === null) {
285
                return;
286
            }
287
288
            if (in_array($returnType, ['void','float','int','string','bool','callable','array'])) {
289
                $returnTypeObject = $method->setReturnType($returnType);
290
                $returnTypeObject->setNullable(false);
291
                return;
292
            }
293
294
            if ($returnType instanceof \PhpParser\Node\Name\FullyQualified) {
295
                $returnTypeObject = $method->setReturnType($returnType->toString());
296
                $returnTypeObject->setNullable(false);
297
                return;
298
            }
299
300
            if ($returnType instanceof NullableType) {
301
                if ((string)$returnType->type === 'self') {
302
                    $returnTypeObject = $method->setReturnType($this->unit->getName());
303
                } else {
304
                    $returnTypeObject = $method->setReturnType($returnType->type);
0 ignored issues
show
Bug introduced by Arne Blankerts
It seems like $returnType->type can also be of type object<PhpParser\Node\Name>; however, TheSeer\phpDox\Collector...Object::setReturnType() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
305
                }
306
                $returnTypeObject->setNullable(true);
307
                return;
308
            }
309
310
            if ($returnType instanceof \PhpParser\Node\Name) {
311
                $returnTypeObject = $method->setReturnType(
312
                    $this->unit->getName()
313
                );
314
                $returnTypeObject->setNullable(false);
315
                return;
316
            }
317
318
            throw new ParseErrorException(
319
                sprintf("Unexpected return type definition %s",
320
                    get_class($returnType)
321
                ),ParseErrorException::UnexpectedExpr);
322
        }
323
324
        private function processInlineComments(MethodObject $method, array $stmts) {
325
            foreach($stmts as $stmt) {
326
                if ($stmt->hasAttribute('comments')) {
327
                    foreach($stmt->getAttribute('comments') as $comment) {
328
                        $inline = new InlineComment($comment->getLine(), $comment->getText());
329
                        if ($inline->getCount() != 0) {
330
                            $method->addInlineComment($inline);
331
                        }
332
                    }
333
                }
334
            }
335
        }
336
337
        /**
338
         * @param MethodObject $method
339
         * @param array                                $params
340
         */
341
        private function processMethodParams(MethodObject $method, array $params) {
342
            foreach($params as $param) {
343
                /** @var $param \PhpParser\Node\Param  */
344
                $parameter = $method->addParameter($param->name);
345
                $parameter->setByReference($param->byRef);
346
                $this->setVariableType($parameter, $param->type);
347
                $this->setVariableDefaultValue($parameter, $param->default);
348
            }
349
        }
350
351
        private function processClassConstant(NodeType\ClassConst $node) {
352
            $constNode = $node->consts[0];
353
            $const = $this->unit->addConstant($constNode->name);
354
355
            $resolved = $this->resolveExpressionValue($constNode->value);
356
357
            $const->setValue($resolved['value']);
358
            if (isset($resolved['constant'])) {
359
                $const->setConstantReference($resolved['constant']);
360
            }
361
            if (isset($resolved['type'])) {
362
                $const->setType($resolved['type']);
363
            }
364
365
            $docComment = $node->getDocComment();
366 View Code Duplication
            if ($docComment !== NULL) {
367
                $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
368
                $const->setDocBlock($block);
369
            }
370
        }
371
372
        private function processProperty(NodeType\Property $node) {
373
            $property = $node->props[0];
374
            $member = $this->unit->addMember($property->name);
375
            if ($node->props[0]->default) {
376
                $this->setVariableDefaultValue($member, $node->props[0]->default);
377
            }
378
            $visibility = 'public';
379
            if ($node->isPrivate()) {
380
                $visibility = 'private';
381
            } elseif ($node->isProtected()) {
382
                $visibility = 'protected';
383
            }
384
            $member->setVisibility($visibility);
385
            $member->setStatic($node->isStatic());
386
            $docComment = $node->getDocComment();
387 View Code Duplication
            if ($docComment !== NULL) {
388
                $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
389
                $member->setDocBlock($block);
390
            }
391
            $member->setLine($node->getLine());
392
        }
393
394
        private function setVariableType(AbstractVariableObject $variable, $type = NULL) {
395
            if ($type instanceof NullableType) {
396
                $variable->setNullable(true);
397
                $type = $type->type;
398
            }
399
400
            if ($type === NULL) {
401
                $variable->setType('{unknown}');
402
                return;
403
            }
404
405
            if ($variable->isInternalType($type)) {
406
                $variable->setType($type);
407
                return;
408
            }
409
410
            if ($type instanceof \PhpParser\Node\Name\FullyQualified) {
411
                $variable->setType( (string)$type);
412
                return;
413
            }
414
415
            $type = (string)$type;
416
            if (isset($this->aliasMap[$type])) {
417
                $type = $this->aliasMap[$type];
418
            } elseif ($type[0]!=='\\') {
419
                $type = $this->namespace . '\\' . $type;
420
            }
421
            $variable->setType($type);
422
        }
423
424
        private function resolveExpressionValue(Expr $expr) {
425
            if ($expr instanceof String_) {
426
                return array(
427
                    'type' => 'string',
428
                    'value' => $expr->getAttribute('originalValue')
429
                );
430
            }
431
432
            if ($expr instanceof LNumber ||
433
                $expr instanceof UnaryMinus ||
434
                $expr instanceof UnaryPlus) {
435
                return array(
436
                    'type' => 'integer',
437
                    'value' => $expr->getAttribute('originalValue')
438
                );
439
            }
440
441
            if ($expr instanceof DNumber) {
442
                return array(
443
                    'type' => 'float',
444
                    'value' => $expr->getAttribute('originalValue')
445
                );
446
            }
447
448
            if ($expr instanceof Array_) {
449
                return array(
450
                    'type' => 'array',
451
                    'value' => '' // @todo add array2xml?
452
                );
453
            }
454
455
            if ($expr instanceof ClassConstFetch) {
456
                return array(
457
                    'type' => '{unknown}',
458
                    'value' => '',
459
                    'constant' => implode('\\', $expr->class->parts) . '::' . $expr->name
460
                );
461
            }
462
463
            if ($expr instanceof ConstFetch) {
464
                $reference = implode('\\', $expr->name->parts);
465
                if (strtolower($reference) === 'null') {
466
                    return array(
467
                        'value' => 'NULL'
468
                    );
469
                }
470
                if (in_array(strtolower($reference), array('true', 'false'))) {
471
                    return array(
472
                        'type' => 'boolean',
473
                        'value' => $reference
474
                    );
475
                }
476
                return array(
477
                    'type' => '{unknown}',
478
                    'value' => '',
479
                    'constant' => implode('\\', $expr->name->parts)
480
                );
481
            }
482
483
            if ($expr instanceof MagicConst\Line) {
484
                return array(
485
                    'type' => 'integer',
486
                    'value' => '',
487
                    'constant' => $expr->getName()
488
                );
489
            }
490
491
            if ($expr instanceof MagicConst) {
492
                return array(
493
                    'type' => 'string',
494
                    'value' => '',
495
                    'constant' => $expr->getName()
496
                );
497
            }
498
499
            if ($expr instanceof BinaryOp) {
500
                $code = (new \PhpParser\PrettyPrinter\Standard)->prettyPrint([$expr]);
501
502
                return array(
503
                    'type' => 'expression',
504
                    'value' => substr($code,0,-1)
505
                );
506
507
            }
508
509
            $type = get_class($expr);
510
            $line = $expr->getLine();
511
            $file = $this->result->getFileName();
512
            throw new ParseErrorException("Unexpected expression type '$type' for value in line $line of file '$file'", ParseErrorException::UnexpectedExpr);
513
514
        }
515
516
        /**
517
         * @param AbstractVariableObject $variable
518
         * @param Expr                   $default
0 ignored issues
show
Documentation introduced by Arne Blankerts
Should the type for parameter $default not be null|Expr?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
519
         *
520
         * @throws ParseErrorException
521
         */
522
        private function setVariableDefaultValue(AbstractVariableObject $variable, Expr $default = NULL) {
523
            if ($default === NULL) {
524
                return;
525
            }
526
527
            $resolved = $this->resolveExpressionValue($default);
528
            $variable->setDefault($resolved['value']);
529
530
            if (isset($resolved['type'])) {
531
                $variable->setType($resolved['type']);
532
            }
533
534
            if (isset($resolved['constant'])) {
535
                $variable->setConstant($resolved['constant']);
536
            }
537
        }
538
    }
539
}
540