ClassDefinition   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 13

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 444
ccs 0
cts 156
cp 0
rs 2.96
c 0
b 0
f 0
wmc 68
lcom 3
cbo 13

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getProperty() 0 12 4
A getPropertyStatement() 0 12 4
A getExtendsClassDefinition() 0 4 1
A addInterface() 0 4 1
B mergeTrait() 0 29 9
A getMethods() 0 4 1
A __construct() 0 6 1
A addMethod() 0 15 3
A addProperty() 0 7 2
A addConst() 0 4 1
D compile() 0 98 13
B hasMethod() 0 13 7
A hasConst() 0 8 4
A getMethod() 0 12 4
A hasProperty() 0 19 6
A getFilepath() 0 4 1
A setFilepath() 0 4 1
A isAbstract() 0 4 1
A isFinal() 0 4 1
A setExtendsClass() 0 4 1
A setExtendsClassDefinition() 0 4 1
A getExtendsClass() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ClassDefinition 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ClassDefinition, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Patsura Dmitry https://github.com/ovr <[email protected]>
4
 */
5
6
namespace PHPSA\Definition;
7
8
use PHPSA\CompiledExpression;
9
use PHPSA\Context;
10
use PhpParser\Node;
11
use PHPSA\Variable;
12
use PHPSA\Compiler\Event;
13
14
/**
15
 * Class Definition
16
 *
17
 * @package PHPSA\Definition
18
 */
19
class ClassDefinition extends ParentDefinition
0 ignored issues
show
Complexity introduced by
This class has a complexity of 68 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

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

Loading history...
20
{
21
    /**
22
     * @var int
23
     */
24
    protected $type;
25
26
    /**
27
     * Class methods
28
     *
29
     * @var ClassMethod[]
30
     */
31
    protected $methods = [];
32
33
    /**
34
     * Class properties
35
     *
36
     * @var Node\Stmt\PropertyProperty[]
37
     */
38
    protected $properties = [];
39
40
    /**
41
     * Property Statements
42
     *
43
     * @var Node\Stmt\Property[]
44
     */
45
    protected $propertyStatements = [];
46
47
    /**
48
     * Class constants
49
     *
50
     * @var Node\Stmt\ClassConst[]
51
     */
52
    protected $constants = [];
53
54
    /**
55
     * @todo Use Finder
56
     *
57
     * @var string
58
     */
59
    protected $filepath;
60
61
    /**
62
     * @var Node\Stmt\Class_|null
63
     */
64
    protected $statement;
65
66
    /**
67
     * @var string|null
68
     */
69
    protected $extendsClass;
70
71
    /**
72
     * @var ClassDefinition|null
73
     */
74
    protected $extendsClassDefinition;
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $extendsClassDefinition exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
75
76
    /**
77
     * @var array
78
     */
79
    protected $interfaces = [];
80
81
    /**
82
     * @param string $name
83
     * @param Node\Stmt\Class_ $statement
84
     * @param integer $type
85
     */
86
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
87
    {
88
        $this->name = $name;
89
        $this->statement = $statement;
90
        $this->type = $type;
91
    }
92
93
    /**
94
     * @param ClassMethod $classMethod
95
     * @param bool $overwrite Should we overwrite method if it already exists
96
     * @return bool Did we overwrite method?
97
     */
98
    public function addMethod(ClassMethod $classMethod, $overwrite = true)
99
    {
100
        if ($overwrite) {
101
            $this->methods[$classMethod->getName()] = $classMethod;
102
        } else {
103
            $name = $classMethod->getName();
104
            if (isset($this->methods[$name])) {
105
                return false;
106
            } else {
107
                $this->methods[$name] = $classMethod;
108
            }
109
        }
110
111
        return true;
112
    }
113
114
    /**
115
     * @param Node\Stmt\Property $property
116
     */
117
    public function addProperty(Node\Stmt\Property $property)
118
    {
119
        foreach ($property->props as $propertyDefinition) {
120
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
121
            $this->propertyStatements[$propertyDefinition->name] = $property;
122
        }
123
    }
124
125
    /**
126
     * @param Node\Stmt\ClassConst $const
127
     */
128
    public function addConst(Node\Stmt\ClassConst $const)
129
    {
130
        $this->constants[$const->consts[0]->name] = $const;
131
    }
132
133
    /**
134
     * @param Context $context
135
     * @return $this
136
     */
137
    public function compile(Context $context)
0 ignored issues
show
Complexity introduced by
This operation has 528 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
138
    {
139
        if ($this->compiled) {
140
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (PHPSA\Definition\ClassDefinition) is incompatible with the return type declared by the abstract method PHPSA\Definition\AbstractDefinition::compile of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
141
        }
142
143
        $this->compiled = true;
144
        $context->setFilepath($this->filepath);
145
        $context->setScope($this);
146
147
        $context->getEventManager()->fire(
148
            Event\StatementBeforeCompile::EVENT_NAME,
149
            new Event\StatementBeforeCompile(
150
                $this->statement,
0 ignored issues
show
Bug introduced by
It seems like $this->statement can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
151
                $context
152
            )
153
        );
154
155
        // Compile event for properties
156
        foreach ($this->properties as $property) {
157
            if (!$property->default) {
158
                continue;
159
            }
160
161
            // fire expression event for property default
162
            $context->getEventManager()->fire(
163
                Event\ExpressionBeforeCompile::EVENT_NAME,
164
                new Event\ExpressionBeforeCompile(
165
                    $property->default,
166
                    $context
167
                )
168
            );
169
        }
170
171
        // Compile event for PropertyProperty
172
        foreach ($this->properties as $property) {
173
            $context->getEventManager()->fire(
174
                Event\StatementBeforeCompile::EVENT_NAME,
175
                new Event\StatementBeforeCompile(
176
                    $property,
177
                    $context
178
                )
179
            );
180
        }
181
182
        // Compile event for constants
183
        foreach ($this->constants as $const) {
184
            $context->getEventManager()->fire(
185
                Event\StatementBeforeCompile::EVENT_NAME,
186
                new Event\StatementBeforeCompile(
187
                    $const,
188
                    $context
189
                )
190
            );
191
        }
192
193
        // Compiler event for property statements
194
        foreach ($this->propertyStatements as $prop) {
195
            $context->getEventManager()->fire(
196
                Event\StatementBeforeCompile::EVENT_NAME,
197
                new Event\StatementBeforeCompile(
198
                    $prop,
199
                    $context
200
                )
201
            );
202
        }
203
204
        // Compile each method
205
        foreach ($this->methods as $method) {
206
            $context->clearSymbols();
207
208
            if (!$method->isStatic()) {
209
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
210
                $thisPtr->incGets();
211
                $context->addVariable($thisPtr);
212
            }
213
214
            $method->compile($context);
215
216
            $symbols = $context->getSymbols();
217
            if (count($symbols) > 0) {
218
                foreach ($symbols as $name => $variable) {
219
                    if (!$variable->isGlobal() && $variable->isUnused()) {
220
                        $context->warning(
221
                            'unused-' . $variable->getSymbolType(),
222
                            sprintf(
223
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
224
                                $variable->getName(),
225
                                $method->getName()
226
                            )
227
                        );
228
                    }
229
                }
230
            }
231
        }
232
233
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (PHPSA\Definition\ClassDefinition) is incompatible with the return type declared by the abstract method PHPSA\Definition\AbstractDefinition::compile of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
234
    }
235
236
    /**
237
     * @param string $name
238
     * @param boolean|false $inherit
239
     * @return bool
240
     */
241
    public function hasMethod($name, $inherit = false)
242
    {
243
        if (isset($this->methods[$name])) {
244
            return true;
245
        }
246
247
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
248
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
249
            return $method && ($method->isPublic() || $method->isProtected());
250
        }
251
252
        return false;
253
    }
254
255
    /**
256
     * @param string $name
257
     * @param bool $inherit
258
     * @return bool
259
     */
260
    public function hasConst($name, $inherit = false)
261
    {
262
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasConst($name, $inherit)) {
263
            return true;
264
        }
265
266
        return isset($this->constants[$name]);
267
    }
268
269
    /**
270
     * @param $name
271
     * @param boolean|false $inherit
272
     * @return ClassMethod|null
273
     */
274
    public function getMethod($name, $inherit = false)
275
    {
276
        if (isset($this->methods[$name])) {
277
            return $this->methods[$name];
278
        }
279
280
        if ($inherit && $this->extendsClassDefinition) {
281
            return $this->extendsClassDefinition->getMethod($name, $inherit);
282
        }
283
284
        return null;
285
    }
286
287
    /**
288
     * @param $name
289
     * @param bool $inherit
290
     * @param bool $isParent It's an internal parameter
291
     * @return bool
292
     */
293
    public function hasProperty($name, $inherit = false, $isParent = true)
294
    {
295
        if (isset($this->properties[$name])) {
296
            if (!$isParent) {
297
                $property = $this->propertyStatements[$name];
298
                if ($property->isPrivate()) {
299
                    return false;
300
                }
301
            }
302
303
            return true;
304
        }
305
306
        if ($inherit && $this->extendsClassDefinition) {
307
            return $this->extendsClassDefinition->hasProperty($name, true, false);
308
        }
309
310
        return false;
311
    }
312
313
    /**
314
     * @param string $name
315
     * @param bool $inherit
316
     * @return Node\Stmt\PropertyProperty|null
317
     */
318
    public function getProperty($name, $inherit = false)
319
    {
320
        if (isset($this->properties[$name])) {
321
            return $this->properties[$name];
322
        }
323
324
        if ($inherit && $this->extendsClassDefinition) {
325
            return $this->extendsClassDefinition->getProperty($name, true);
326
        }
327
328
        return null;
329
    }
330
331
    /**
332
     * @param string $name
333
     * @param bool $inherit
334
     * @return Node\Stmt\Property|null
335
     */
336
    public function getPropertyStatement($name, $inherit = false)
337
    {
338
        if (isset($this->propertyStatements[$name])) {
339
            return $this->propertyStatements[$name];
340
        }
341
342
        if ($inherit && $this->extendsClassDefinition) {
343
            return $this->extendsClassDefinition->getPropertyStatement($name, true);
344
        }
345
346
        return null;
347
    }
348
349
    /**
350
     * @return string
351
     */
352
    public function getFilepath()
353
    {
354
        return $this->filepath;
355
    }
356
357
    /**
358
     * @param string $filepath
359
     */
360
    public function setFilepath($filepath)
361
    {
362
        $this->filepath = $filepath;
363
    }
364
365
    /**
366
     * @return bool
367
     */
368
    public function isAbstract()
369
    {
370
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
371
    }
372
373
    /**
374
     * @return bool
375
     */
376
    public function isFinal()
377
    {
378
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_FINAL);
379
    }
380
381
    /**
382
     * @param null|string $extendsClass
383
     */
384
    public function setExtendsClass($extendsClass)
385
    {
386
        $this->extendsClass = $extendsClass;
387
    }
388
389
    /**
390
     * @return null|ClassDefinition
391
     */
392
    public function getExtendsClassDefinition()
393
    {
394
        return $this->extendsClassDefinition;
395
    }
396
397
    /**
398
     * @param ClassDefinition $extendsClassDefinition
399
     */
400
    public function setExtendsClassDefinition(ClassDefinition $extendsClassDefinition)
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $extendsClassDefinition exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
401
    {
402
        $this->extendsClassDefinition = $extendsClassDefinition;
403
    }
404
405
    /**
406
     * @param string $interface
407
     */
408
    public function addInterface($interface)
409
    {
410
        $this->interfaces[] = $interface;
411
    }
412
413
    /**
414
     * @return null|string
415
     */
416
    public function getExtendsClass()
417
    {
418
        return $this->extendsClass;
419
    }
420
421
    /**
422
     * @param TraitDefinition $definition
423
     * @param Node\Stmt\TraitUseAdaptation\Alias[] $adaptations
424
     */
425
    public function mergeTrait(TraitDefinition $definition, array $adaptations)
426
    {
427
        $methods = $definition->getMethods();
428
        if ($methods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods of type PHPSA\Definition\ClassMethod[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
429
            foreach ($adaptations as $adaptation) {
430
                // We don't support Trait name for now
431
                if (!$adaptation->trait) {
432
                    $methodNameFromTrait = $adaptation->method;
433
                    if (isset($methods[$methodNameFromTrait])) {
434
                        /** @var ClassMethod $method Method from Trait */
435
                        $method = $methods[$methodNameFromTrait];
436
                        if ($adaptation->newName
437
                            || ($adaptation->newModifier && $method->getModifier() != $adaptation->newModifier)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $adaptation->newModifier of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
438
                            // Don't modify original method from Trait
439
                            $method = clone $method;
440
                            $method->setName($adaptation->newName);
0 ignored issues
show
Documentation introduced by
$adaptation->newName is of type null|object<PhpParser\Node\Identifier>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
441
                            $method->setModifier($adaptation->newModifier);
442
443
                            $methods[$methodNameFromTrait] = $method;
444
                        }
445
                    }
446
                }
447
            }
448
449
            foreach ($methods as $method) {
450
                $this->addMethod($method, false);
451
            }
452
        }
453
    }
454
455
    /**
456
     * @return ClassMethod[]
457
     */
458
    public function getMethods()
459
    {
460
        return $this->methods;
461
    }
462
}
463