UnitCollectingVisitor   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 88
eloc 270
c 8
b 0
f 0
dl 0
loc 506
ccs 0
cts 366
cp 0
rs 2

15 Methods

Rating   Name   Duplication   Size   Complexity  
A processInlineComments() 0 8 5
B enterNode() 0 48 11
B processUnit() 0 34 9
B processTraitUse() 0 38 8
A __construct() 0 3 1
B processMethodReturnType() 0 45 7
A processClassConstant() 0 21 4
A getTraitUse() 0 9 2
A leaveNode() 0 7 4
A processMethodParams() 0 8 2
A processMethod() 0 32 5
A processProperty() 0 23 5
C resolveExpressionValue() 0 92 14
B setVariableType() 0 32 7
A setVariableDefaultValue() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like UnitCollectingVisitor 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 UnitCollectingVisitor, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types = 1);
2
namespace TheSeer\phpDox\Collector\Backend;
3
4
use PhpParser\Node\Expr;
5
use PhpParser\Node\Expr\Array_;
6
use PhpParser\Node\Expr\BinaryOp;
7
use PhpParser\Node\Expr\ClassConstFetch;
8
use PhpParser\Node\Expr\ConstFetch;
9
use PhpParser\Node\Expr\UnaryMinus;
10
use PhpParser\Node\Expr\UnaryPlus;
11
use PhpParser\Node\Name\FullyQualified;
12
use PhpParser\Node\NullableType;
13
use PhpParser\Node\Scalar\DNumber;
14
use PhpParser\Node\Scalar\LNumber;
15
use PhpParser\Node\Scalar\MagicConst;
16
use PhpParser\Node\Scalar\String_;
17
use PhpParser\Node\Stmt as NodeType;
18
use PhpParser\NodeVisitorAbstract;
19
use TheSeer\phpDox\Collector\AbstractUnitObject;
20
use TheSeer\phpDox\Collector\AbstractVariableObject;
21
use TheSeer\phpDox\Collector\InlineComment;
22
use TheSeer\phpDox\Collector\MethodObject;
23
use TheSeer\phpDox\DocBlock\Parser as DocBlockParser;
24
use TheSeer\phpDox\TypeAwareInterface;
25
use TheSeer\phpDox\TypeAwareTrait;
26
27
class UnitCollectingVisitor extends NodeVisitorAbstract implements TypeAwareInterface {
28
    use TypeAwareTrait;
29
30
    /**
31
     * @var \TheSeer\phpDox\DocBlock\Parser
32
     */
33
    private $docBlockParser;
34
35
    /**
36
     * @var array
37
     */
38
    private $aliasMap = [];
39
40
    /**
41
     * @var string
42
     */
43
    private $namespace = '\\';
44
45
    /**
46
     * @var ParseResult
47
     */
48
    private $result;
49
50
    /**
51
     * @var AbstractUnitObject
52
     */
53
    private $unit;
54
55
    private $modifier = [
56
        NodeType\Class_::MODIFIER_PUBLIC    => 'public',
57
        NodeType\Class_::MODIFIER_PROTECTED => 'protected',
58
        NodeType\Class_::MODIFIER_PRIVATE   => 'private',
59
    ];
60
61
    public function __construct(DocBlockParser $parser, ParseResult $result) {
62
        $this->docBlockParser = $parser;
63
        $this->result         = $result;
64
    }
65
66
    /**
67
     * @return null|int|\PhpParser\Node|void
68
     */
69
    public function enterNode(\PhpParser\Node $node) {
70
        if ($node instanceof NodeType\Namespace_ && $node->name != null) {
71
            $this->namespace             = \implode('\\', $node->name->parts);
72
            $this->aliasMap['::context'] = $this->namespace;
73
        } else {
74
            if ($node instanceof NodeType\UseUse) {
75
                $this->aliasMap[$node->getAlias()->name] = \implode('\\', $node->name->parts);
76
            } else {
77
                if ($node instanceof NodeType\Class_) {
78
                    $this->aliasMap['::unit'] = (string)$node->namespacedName;
79
                    $this->unit               = $this->result->addClass((string)$node->namespacedName);
80
                    $this->processUnit($node);
81
82
                    return;
83
                }
84
85
                if ($node instanceof NodeType\Interface_) {
86
                    $this->aliasMap['::unit'] = (string)$node->namespacedName;
87
                    $this->unit               = $this->result->addInterface((string)$node->namespacedName);
88
                    $this->processUnit($node);
89
90
                    return;
91
                }
92
93
                if ($node instanceof NodeType\Trait_) {
94
                    $this->aliasMap['::unit'] = (string)$node->namespacedName;
95
                    $this->unit               = $this->result->addTrait((string)$node->namespacedName);
96
                    $this->processUnit($node);
97
98
                    return;
99
                }
100
101
                if ($node instanceof NodeType\Property) {
102
                    $this->processProperty($node);
103
104
                    return;
105
                }
106
107
                if ($node instanceof NodeType\ClassMethod) {
108
                    $this->processMethod($node);
109
110
                    return;
111
                }
112
113
                if ($node instanceof NodeType\ClassConst) {
114
                    $this->processClassConstant($node);
115
                } elseif ($node instanceof NodeType\TraitUse) {
116
                    $this->processTraitUse($node);
117
                }
118
            }
119
        }
120
    }
121
122
    /**
123
     * @return null|false|int|\PhpParser\Node|\PhpParser\Node[]|void
124
     */
125
    public function leaveNode(\PhpParser\Node $node) {
126
        if ($node instanceof NodeType\Class_
127
            || $node instanceof NodeType\Interface_
128
            || $node instanceof NodeType\Trait_) {
129
            $this->unit = null;
130
131
            return;
132
        }
133
    }
134
135
    /**
136
     * @param $node
137
     */
138
    private function processUnit($node): void {
139
        $this->unit->setStartLine($node->getAttribute('startLine'));
140
        $this->unit->setEndLine($node->getAttribute('endLine'));
141
142
        if ($node instanceof NodeType\Class_) {
143
            $this->unit->setAbstract($node->isAbstract());
144
            $this->unit->setFinal($node->isFinal());
145
        } else {
146
            $this->unit->setAbstract(false);
147
            $this->unit->setFinal(false);
148
        }
149
150
        $docComment = $node->getDocComment();
151
152
        if ($docComment !== null) {
153
            $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
154
            $this->unit->setDocBlock($block);
155
        }
156
157
        if ($node->getType() != 'Stmt_Trait' && $node->extends !== null) {
158
            if (\is_array($node->extends)) {
159
                $extendsArray = $node->extends;
160
161
                foreach ($extendsArray as $extends) {
162
                    $this->unit->addExtends(\implode('\\', $extends->parts));
163
                }
164
            } else {
165
                $this->unit->addExtends(\implode('\\', $node->extends->parts));
166
            }
167
        }
168
169
        if ($node->getType() === 'Stmt_Class') {
170
            foreach ($node->implements as $implements) {
171
                $this->unit->addImplements(\implode('\\', $implements->parts));
172
            }
173
        }
174
    }
175
176
    private function processTraitUse(NodeType\TraitUse $node): void {
177
        foreach ($node->traits as $trait) {
178
            $traitUse = $this->unit->addTrait((string)$trait);
179
            $traitUse->setStartLine($node->getAttribute('startLine'));
180
            $traitUse->setEndLine($node->getAttribute('endLine'));
181
        }
182
183
        foreach ($node->adaptations as $adaptation) {
184
            if ($adaptation instanceof NodeType\TraitUseAdaptation\Alias) {
185
                if ($adaptation->trait instanceof FullyQualified) {
186
                    $traitUse = $this->getTraitUse((string)$adaptation->trait);
187
                } else {
188
                    if (\count($node->traits) === 1) {
189
                        $traitUse = $this->getTraitUse((string)$node->traits[0]);
190
                    } else {
191
                        $traitUse = $this->unit->getAmbiguousTraitUse();
192
                    }
193
                }
194
195
                $traitUse->addAlias(
196
                    $adaptation->method,
197
                    $adaptation->newName,
198
                    $adaptation->newModifier ? $this->modifier[$adaptation->newModifier] : null
199
                );
200
201
                continue;
202
            }
203
204
            if ($adaptation instanceof NodeType\TraitUseAdaptation\Precedence) {
205
                $traitUse = $this->getTraitUse((string)$adaptation->insteadof[0]);
206
                $traitUse->addExclude($adaptation->method);
207
208
                continue;
209
            }
210
211
            throw new ParseErrorException(
212
                \sprintf('Unexpected adaption type %s', \get_class($adaptation)),
213
                ParseErrorException::UnexpectedExpr
214
            );
215
        }
216
    }
217
218
    private function getTraitUse($traitName) {
219
        if (!$this->unit->usesTrait($traitName)) {
220
            throw new ParseErrorException(
221
                \sprintf('Referenced trait "%s" not used', $traitName),
222
                ParseErrorException::GeneralParseError
223
            );
224
        }
225
226
        return $this->unit->getTraitUse($traitName);
227
    }
228
229
    private function processMethod(NodeType\ClassMethod $node): void {
230
231
        /** @var $method \TheSeer\phpDox\Collector\MethodObject */
232
        $method = $this->unit->addMethod($node->name);
233
        $method->setStartLine($node->getAttribute('startLine'));
234
        $method->setEndLine($node->getAttribute('endLine'));
235
        $method->setAbstract($node->isAbstract());
236
        $method->setFinal($node->isFinal());
237
        $method->setStatic($node->isStatic());
238
239
        $this->processMethodReturnType($method, $node->getReturnType());
240
241
        $visibility = 'public';
242
243
        if ($node->isPrivate()) {
244
            $visibility = 'private';
245
        } elseif ($node->isProtected()) {
246
            $visibility = 'protected';
247
        }
248
        $method->setVisibility($visibility);
249
250
        $docComment = $node->getDocComment();
0 ignored issues
show
Bug introduced by Arne Blankerts
Are you sure the assignment to $docComment is correct as $node->getDocComment() targeting PhpParser\NodeAbstract::getDocComment() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
251
252
        if ($docComment !== null) {
0 ignored issues
show
introduced by Arne Blankerts
The condition $docComment !== null is always false.
Loading history...
253
            $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
254
            $method->setDocBlock($block);
255
        }
256
257
        $this->processMethodParams($method, $node->params);
258
259
        if ($node->stmts) {
260
            $this->processInlineComments($method, $node->stmts);
261
        }
262
    }
263
264
    private function processMethodReturnType(MethodObject $method, $returnType): void {
265
        if ($returnType === null) {
266
            return;
267
        }
268
269
        if ($this->isBuiltInType((string)$returnType, self::TYPE_RETURN)) {
270
            $returnTypeObject = $method->setReturnType($returnType);
271
            $returnTypeObject->setNullable(false);
272
273
            return;
274
        }
275
276
        if ($returnType instanceof \PhpParser\Node\Name\FullyQualified) {
277
            $returnTypeObject = $method->setReturnType($returnType->toString());
278
            $returnTypeObject->setNullable(false);
279
280
            return;
281
        }
282
283
        if ($returnType instanceof NullableType) {
284
            if ((string)$returnType->type === 'self') {
285
                $returnTypeObject = $method->setReturnType($this->unit->getName());
286
            } else {
287
                $returnTypeObject = $method->setReturnType($returnType->type);
288
            }
289
            $returnTypeObject->setNullable(true);
290
291
            return;
292
        }
293
294
        if ($returnType instanceof \PhpParser\Node\Name) {
295
            $returnTypeObject = $method->setReturnType(
296
                $this->unit->getName()
297
            );
298
            $returnTypeObject->setNullable(false);
299
300
            return;
301
        }
302
303
        throw new ParseErrorException(
304
            \sprintf(
305
                'Unexpected return type definition %s',
306
                \get_class($returnType)
307
            ),
308
            ParseErrorException::UnexpectedExpr
309
        );
310
    }
311
312
    private function processInlineComments(MethodObject $method, array $stmts): void {
313
        foreach ($stmts as $stmt) {
314
            if ($stmt->hasAttribute('comments')) {
315
                foreach ($stmt->getAttribute('comments') as $comment) {
316
                    $inline = new InlineComment($comment->getLine(), $comment->getText());
317
318
                    if ($inline->getCount() != 0) {
319
                        $method->addInlineComment($inline);
320
                    }
321
                }
322
            }
323
        }
324
    }
325
326
    private function processMethodParams(MethodObject $method, array $params): void {
327
        foreach ($params as $param) {
328
            /** @var $param \PhpParser\Node\Param */
329
            $parameter = $method->addParameter($param->var->name);
0 ignored issues
show
Bug introduced by Arne Blankerts
It seems like $param->var->name can also be of type PhpParser\Node\Expr; however, parameter $name of TheSeer\phpDox\Collector...dObject::addParameter() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

329
            $parameter = $method->addParameter(/** @scrutinizer ignore-type */ $param->var->name);
Loading history...
Bug introduced by Arne Blankerts
The property name does not seem to exist on PhpParser\Node\Expr\Error.
Loading history...
330
            $parameter->setByReference($param->byRef);
331
            $parameter->setVariadic($param->variadic);
332
            $this->setVariableType($parameter, $param->type);
333
            $this->setVariableDefaultValue($parameter, $param->default);
334
        }
335
    }
336
337
    private function processClassConstant(NodeType\ClassConst $node): void {
338
        $constNode = $node->consts[0];
339
        $const     = $this->unit->addConstant($constNode->name);
340
341
        $resolved = $this->resolveExpressionValue($constNode->value);
342
343
        $const->setValue($resolved['value']);
344
345
        if (isset($resolved['constant'])) {
346
            $const->setConstantReference($resolved['constant']);
347
        }
348
349
        if (isset($resolved['type'])) {
350
            $const->setType($resolved['type']);
351
        }
352
353
        $docComment = $node->getDocComment();
0 ignored issues
show
Bug introduced by Arne Blankerts
Are you sure the assignment to $docComment is correct as $node->getDocComment() targeting PhpParser\NodeAbstract::getDocComment() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
354
355
        if ($docComment !== null) {
0 ignored issues
show
introduced by Arne Blankerts
The condition $docComment !== null is always false.
Loading history...
356
            $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
357
            $const->setDocBlock($block);
358
        }
359
    }
360
361
    private function processProperty(NodeType\Property $node): void {
362
        $property = $node->props[0];
363
        $member   = $this->unit->addMember($property->name);
364
365
        if ($node->props[0]->default) {
366
            $this->setVariableDefaultValue($member, $node->props[0]->default);
367
        }
368
        $visibility = 'public';
369
370
        if ($node->isPrivate()) {
371
            $visibility = 'private';
372
        } elseif ($node->isProtected()) {
373
            $visibility = 'protected';
374
        }
375
        $member->setVisibility($visibility);
376
        $member->setStatic($node->isStatic());
377
        $docComment = $node->getDocComment();
0 ignored issues
show
Bug introduced by Arne Blankerts
Are you sure the assignment to $docComment is correct as $node->getDocComment() targeting PhpParser\NodeAbstract::getDocComment() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
378
379
        if ($docComment !== null) {
0 ignored issues
show
introduced by Arne Blankerts
The condition $docComment !== null is always false.
Loading history...
380
            $block = $this->docBlockParser->parse($docComment, $this->aliasMap);
381
            $member->setDocBlock($block);
382
        }
383
        $member->setLine($node->getLine());
384
    }
385
386
    private function setVariableType(AbstractVariableObject $variable, $type = null): void {
387
        if ($type instanceof NullableType) {
388
            $variable->setNullable(true);
389
            $type = $type->type;
390
        }
391
392
        if ($type === null) {
393
            $variable->setType('{unknown}');
394
395
            return;
396
        }
397
398
        if ($variable->isInternalType($type)) {
399
            $variable->setType($type);
400
401
            return;
402
        }
403
404
        if ($type instanceof \PhpParser\Node\Name\FullyQualified) {
405
            $variable->setType((string)$type);
406
407
            return;
408
        }
409
410
        $type = (string)$type;
411
412
        if (isset($this->aliasMap[$type])) {
413
            $type = $this->aliasMap[$type];
414
        } elseif ($type[0] !== '\\') {
415
            $type = $this->namespace . '\\' . $type;
416
        }
417
        $variable->setType($type);
418
    }
419
420
    private function resolveExpressionValue(Expr $expr) {
421
        if ($expr instanceof String_) {
422
            return [
423
                'type'  => 'string',
424
                'value' => $expr->getAttribute('originalValue')
425
            ];
426
        }
427
428
        if ($expr instanceof LNumber ||
429
            $expr instanceof UnaryMinus ||
430
            $expr instanceof UnaryPlus) {
431
            return [
432
                'type'  => 'integer',
433
                'value' => $expr->getAttribute('originalValue')
434
            ];
435
        }
436
437
        if ($expr instanceof DNumber) {
438
            return [
439
                'type'  => 'float',
440
                'value' => $expr->getAttribute('originalValue')
441
            ];
442
        }
443
444
        if ($expr instanceof Array_) {
445
            return [
446
                'type'  => 'array',
447
                'value' => '' // @todo add array2xml?
448
            ];
449
        }
450
451
        if ($expr instanceof ClassConstFetch) {
452
            return [
453
                'type'     => '{unknown}',
454
                'value'    => '',
455
                'constant' => \implode('\\', $expr->class->parts) . '::' . $expr->name
0 ignored issues
show
Bug introduced by Arne Blankerts
Are you sure $expr->name of type PhpParser\Node\Expr\Erro...pParser\Node\Identifier can be used in concatenation? ( Ignorable by Annotation )

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

455
                'constant' => \implode('\\', $expr->class->parts) . '::' . /** @scrutinizer ignore-type */ $expr->name
Loading history...
456
            ];
457
        }
458
459
        if ($expr instanceof ConstFetch) {
460
            $reference = \implode('\\', $expr->name->parts);
461
462
            if (\strtolower($reference) === 'null') {
463
                return [
464
                    'value' => 'NULL'
465
                ];
466
            }
467
468
            if (\in_array(\strtolower($reference), ['true', 'false'])) {
469
                return [
470
                    'type'  => 'boolean',
471
                    'value' => $reference
472
                ];
473
            }
474
475
            return [
476
                'type'     => '{unknown}',
477
                'value'    => '',
478
                'constant' => \implode('\\', $expr->name->parts)
479
            ];
480
        }
481
482
        if ($expr instanceof MagicConst\Line) {
483
            return [
484
                'type'     => 'integer',
485
                'value'    => '',
486
                'constant' => $expr->getName()
487
            ];
488
        }
489
490
        if ($expr instanceof MagicConst) {
491
            return [
492
                'type'     => 'string',
493
                'value'    => '',
494
                'constant' => $expr->getName()
495
            ];
496
        }
497
498
        if ($expr instanceof BinaryOp) {
499
            $code = (new \PhpParser\PrettyPrinter\Standard)->prettyPrint([$expr]);
500
501
            return [
502
                'type'  => 'expression',
503
                'value' => $code
504
            ];
505
        }
506
507
        $type = \get_class($expr);
508
        $line = $expr->getLine();
509
        $file = $this->result->getFileName();
510
511
        throw new ParseErrorException("Unexpected expression type '$type' for value in line $line of file '$file'", ParseErrorException::UnexpectedExpr);
512
    }
513
514
    /**
515
     * @param Expr $default
516
     *
517
     * @throws ParseErrorException
518
     */
519
    private function setVariableDefaultValue(AbstractVariableObject $variable, Expr $default = null): void {
520
        if ($default === null) {
521
            return;
522
        }
523
524
        $resolved = $this->resolveExpressionValue($default);
525
        $variable->setDefault($resolved['value']);
526
527
        if (isset($resolved['type'])) {
528
            $variable->setType($resolved['type']);
529
        }
530
531
        if (isset($resolved['constant'])) {
532
            $variable->setConstant($resolved['constant']);
533
        }
534
    }
535
}
536