Completed
Push — master ( 2cf8eb...09dcac )
by Дмитрий
04:07
created

ClassDefinition::getMethods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 58
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
87
    {
88 58
        $this->name = $name;
89 58
        $this->statement = $statement;
90 58
        $this->type = $type;
91 58
    }
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 50
    public function addMethod(ClassMethod $classMethod, $overwrite = true)
99
    {
100 50
        if ($overwrite) {
101 50
            $this->methods[$classMethod->getName()] = $classMethod;
102 50
        } 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 50
        return true;
112
    }
113
114
    /**
115
     * @param Node\Stmt\Property $property
116
     */
117 7
    public function addProperty(Node\Stmt\Property $property)
118
    {
119 7
        foreach ($property->props as $propertyDefinition) {
120 7
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
121 7
            $this->propertyStatements[$propertyDefinition->name] = $property;
122 7
        }
123 7
    }
124
125
    /**
126
     * @param Node\Stmt\ClassConst $const
127
     */
128 4
    public function addConst(Node\Stmt\ClassConst $const)
129
    {
130 4
        $this->constants[$const->consts[0]->name] = $const;
131 4
    }
132
133
    /**
134
     * @param Context $context
135
     * @return $this
136
     */
137 52
    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 52
        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 52
        $this->compiled = true;
144 52
        $context->setFilepath($this->filepath);
145 52
        $context->setScope($this);
146
147 52
        $context->getEventManager()->fire(
148 52
            Event\StatementBeforeCompile::EVENT_NAME,
149 52
            new Event\StatementBeforeCompile(
150 52
                $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 52
            )
153 52
        );
154
155
        // Compile event for properties
156 52
        foreach ($this->properties as $property) {
157 6
            if (!$property->default) {
158 2
                continue;
159
            }
160
161
            // fire expression event for property default
162 6
            $context->getEventManager()->fire(
163 6
                Event\ExpressionBeforeCompile::EVENT_NAME,
164 6
                new Event\ExpressionBeforeCompile(
165 6
                    $property->default,
166
                    $context
167 6
                )
168 6
            );
169 52
        }
170
171
        // Compile event for PropertyProperty
172 52
        foreach ($this->properties as $property) {
173 6
            $context->getEventManager()->fire(
174 6
                Event\StatementBeforeCompile::EVENT_NAME,
175 6
                new Event\StatementBeforeCompile(
176 6
                    $property,
177
                    $context
178 6
                )
179 6
            );
180 52
        }
181
182
        // Compile event for constants
183 52
        foreach ($this->constants as $const) {
184 2
            $context->getEventManager()->fire(
185 2
                Event\StatementBeforeCompile::EVENT_NAME,
186 2
                new Event\StatementBeforeCompile(
187 2
                    $const,
188
                    $context
189 2
                )
190 2
            );
191 52
        }
192
193
        // Compiler event for property statements
194 52
        foreach ($this->propertyStatements as $prop) {
195 6
            $context->getEventManager()->fire(
196 6
                Event\StatementBeforeCompile::EVENT_NAME,
197 6
                new Event\StatementBeforeCompile(
198 6
                    $prop,
199
                    $context
200 6
                )
201 6
            );
202 52
        }
203
204
        // Compile each method
205 52
        foreach ($this->methods as $method) {
206 49
            $context->clearSymbols();
207
208 49
            if (!$method->isStatic()) {
209 48
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
210 48
                $thisPtr->incGets();
211 48
                $context->addVariable($thisPtr);
212 48
            }
213
214 49
            $method->compile($context);
215
216 49
            $symbols = $context->getSymbols();
217 49
            if (count($symbols) > 0) {
218 49
                foreach ($symbols as $name => $variable) {
219 49
                    if (!$variable->isGlobal() && $variable->isUnused()) {
220 12
                        $context->warning(
221 12
                            'unused-' . $variable->getSymbolType(),
222 12
                            sprintf(
223 12
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
224 12
                                $variable->getName(),
225 12
                                $method->getName()
226 12
                            )
227 12
                        );
228 12
                    }
229 49
                }
230 49
            }
231 52
        }
232
233 52
        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 4
    public function hasMethod($name, $inherit = false)
242
    {
243 4
        if (isset($this->methods[$name])) {
244 2
            return true;
245
        }
246
247 3
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
248 2
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
249 2
            return $method && ($method->isPublic() || $method->isProtected());
250
        }
251
252 1
        return false;
253
    }
254
255
    /**
256
     * @param string $name
257
     * @param bool $inherit
258
     * @return bool
259
     */
260 2
    public function hasConst($name, $inherit = false)
261
    {
262 2
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasConst($name, $inherit)) {
263 1
            return true;
264
        }
265
266 2
        return isset($this->constants[$name]);
267
    }
268
269
    /**
270
     * @param $name
271
     * @param boolean|false $inherit
272
     * @return ClassMethod|null
273
     */
274 4
    public function getMethod($name, $inherit = false)
275
    {
276 4
        if (isset($this->methods[$name])) {
277 2
            return $this->methods[$name];
278
        }
279
280 2
        if ($inherit && $this->extendsClassDefinition) {
281 2
            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 3
    public function hasProperty($name, $inherit = false, $isParent = true)
294
    {
295 3
        if (isset($this->properties[$name])) {
296 1
            if (!$isParent) {
297
                $property = $this->propertyStatements[$name];
298
                if ($property->isPrivate()) {
299
                    return false;
300
                }
301
            }
302
303 1
            return true;
304
        }
305
306 3
        if ($inherit && $this->extendsClassDefinition) {
307
            return $this->extendsClassDefinition->hasProperty($name, true, false);
308
        }
309
310 3
        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 52
    public function getFilepath()
353
    {
354 52
        return $this->filepath;
355
    }
356
357
    /**
358
     * @param string $filepath
359
     */
360 52
    public function setFilepath($filepath)
361
    {
362 52
        $this->filepath = $filepath;
363 52
    }
364
365
    /**
366
     * @return bool
367
     */
368 1
    public function isAbstract()
369
    {
370 1
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
371
    }
372
373
    /**
374
     * @return bool
375
     */
376 3
    public function isFinal()
377
    {
378 3
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_FINAL);
379
    }
380
381
    /**
382
     * @param null|string $extendsClass
383
     */
384 2
    public function setExtendsClass($extendsClass)
385
    {
386 2
        $this->extendsClass = $extendsClass;
387 2
    }
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 3
    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 3
        $this->extendsClassDefinition = $extendsClassDefinition;
403 3
    }
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 52
    public function getExtendsClass()
417
    {
418 52
        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
0 ignored issues
show
Bug Best Practice introduced by
The expression $adaptation->newName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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);
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