Completed
Pull Request — master (#264)
by Enrico
11:14
created

ClassDefinition::addProperty()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 2
rs 9.4285
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 ClassDefinition
16
 * @package PHPSA\Definition
17
 */
18
class ClassDefinition extends ParentDefinition
0 ignored issues
show
Complexity introduced by
This class has a complexity of 64 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...
19
{
20
    /**
21
     * @var int
22
     */
23
    protected $type;
24
25
    /**
26
     * Class methods
27
     *
28
     * @var ClassMethod[]
29
     */
30
    protected $methods = [];
31
32
    /**
33
     * Class properties
34
     *
35
     * @var Node\Stmt\PropertyProperty[]
36
     */
37
    protected $properties = [];
38
39
    /**
40
     * Property Statements
41
     *
42
     * @var Node\Stmt\Property[]
43
     */
44
    protected $propertyStatements = [];
45
46
    /**
47
     * Class constants
48
     *
49
     * @var Node\Stmt\Const_[]
50
     */
51
    protected $constants = [];
52
53
    /**
54
     * @todo Use Finder
55
     *
56
     * @var string
57
     */
58
    protected $filepath;
59
60
    /**
61
     * @var Node\Stmt\Class_|null
62
     */
63
    protected $statement;
64
65
    /**
66
     * @var string|null
67
     */
68
    protected $extendsClass;
69
70
    /**
71
     * @var ClassDefinition|null
72
     */
73
    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...
74
75
    /**
76
     * @var array
77
     */
78
    protected $interfaces = [];
79
80
    /**
81
     * @param string $name
82
     * @param Node\Stmt\Class_ $statement
83
     * @param integer $type
84
     */
85 878
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
86
    {
87 878
        $this->name = $name;
88 878
        $this->statement = $statement;
89 878
        $this->type = $type;
90 878
    }
91
92
    /**
93
     * @param ClassMethod $classMethod
94
     * @param bool $overwrite Should we overwrite method if it already exists
95
     * @return bool Did we overwrite method?
96
     */
97 44
    public function addMethod(ClassMethod $classMethod, $overwrite = true)
98
    {
99 44
        if ($overwrite) {
100 44
            $this->methods[$classMethod->getName()] = $classMethod;
101 44
        } else {
102
            $name = $classMethod->getName();
103
            if (isset($this->methods[$name])) {
104
                return false;
105
            } else {
106
                $this->methods[$name] = $classMethod;
107
            }
108
        }
109
110 44
        return true;
111
    }
112
113
    /**
114
     * @param Node\Stmt\Property $property
115
     */
116 8
    public function addProperty(Node\Stmt\Property $property)
117
    {
118 8
        foreach ($property->props as $propertyDefinition) {
119 8
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
120 8
        }
121
122 8
        $this->propertyStatements[$propertyDefinition->name] = $property;
0 ignored issues
show
Bug introduced by
The variable $propertyDefinition seems to be defined by a foreach iteration on line 118. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
123 8
    }
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 46
    public function compile(Context $context)
0 ignored issues
show
Complexity introduced by
This operation has 432 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 46
        if ($this->compiled) {
140
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type documented by PHPSA\Definition\ClassDefinition::compile of type PHPSA\Definition\ClassDefinition.

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 46
        $this->compiled = true;
144 46
        $context->setFilepath($this->filepath);
145 46
        $context->setScope($this);
146
147 46
        $context->getEventManager()->fire(
148 46
            Event\StatementBeforeCompile::EVENT_NAME,
149 46
            new Event\StatementBeforeCompile(
150 46
                $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 46
            )
153 46
        );
154
155
        // Compile event for properties
156 46
        foreach ($this->properties as $property) {
157 7
            if (!$property->default) {
158 3
                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 46
        }
170
171
        // Compile event for PropertyProperty
172 46
        foreach ($this->properties as $property) {
173 7
            $context->getEventManager()->fire(
174 7
                Event\StatementBeforeCompile::EVENT_NAME,
175 7
                new Event\StatementBeforeCompile(
176 7
                    $property,
177
                    $context
178 7
                )
179 7
            );
180 46
        }
181
182
        // Compile event for constants
183 46
        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 46
        }
192
193
        // Compiler event for property statements
194 46
        foreach ($this->propertyStatements as $prop) {
195 7
            $context->getEventManager()->fire(
196 7
                Event\StatementBeforeCompile::EVENT_NAME,
197 7
                new Event\StatementBeforeCompile(
198 7
                    $prop,
199
                    $context
200 7
                )
201 7
            );
202 46
        }
203
204
        // Compile each method
205 46
        foreach ($this->methods as $method) {
206 43
            $context->clearSymbols();
207
208 43
            if (!$method->isStatic()) {
209 42
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
210 42
                $thisPtr->incGets();
211 42
                $context->addVariable($thisPtr);
212 42
            }
213
214 43
            $method->compile($context);
215
216 43
            $symbols = $context->getSymbols();
217 43
            if (count($symbols) > 0) {
218 42
                foreach ($symbols as $name => $variable) {
219 42
                    if ($variable->isUnused()) {
220 13
                        $context->warning(
221 13
                            'unused-' . $variable->getSymbolType(),
222 13
                            sprintf(
223 13
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
224 13
                                $variable->getName(),
225 13
                                $method->getName()
226 13
                            )
227 13
                        );
228 13
                    }
229 42
                }
230 42
            }
231 46
        }
232
233 46
        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 2
    public function hasMethod($name, $inherit = false)
242
    {
243 2
        if (isset($this->methods[$name])) {
244 2
            return true;
245
        }
246
247 1
        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 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 2
    public function getMethod($name, $inherit = false)
275
    {
276 2
        if (isset($this->methods[$name])) {
277 2
            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
     * @return bool
291
     */
292 1
    public function hasProperty($name, $inherit = false)
293
    {
294 1
        if (isset($this->properties[$name])) {
295 1
            return isset($this->properties[$name]);
296
        }
297
298 1
        return $inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasProperty($name, true);
299
    }
300
301
    /**
302
     * @param string $name
303
     * @param bool $inherit
304
     * @return Node\Stmt\PropertyProperty
305
     */
306
    public function getProperty($name, $inherit = false)
307
    {
308
        assert($this->hasProperty($name, $inherit));
309
310
        if (isset($this->properties[$name])) {
311
            return $this->properties[$name];
312
        }
313
314
        if ($inherit && $this->extendsClassDefinition) {
315
            return $this->extendsClassDefinition->getProperty($name, true);
316
        }
317
318
        return null;
319
    }
320
321
    /**
322
     * @param string $name
323
     * @param bool $inherit
324
     * @return Node\Stmt\Property
325
     */
326
    public function getPropertyStatement($name, $inherit = false)
327
    {
328
        if (isset($this->propertyStatements[$name])) {
329
            return $this->propertyStatements[$name];
330
        }
331
332
        if ($inherit && $this->extendsClassDefinition) {
333
            return $this->extendsClassDefinition->getPropertyStatement($name, true);
334
        }
335
336
        return null;
337
    }
338
339
    /**
340
     * @return string
341
     */
342 46
    public function getFilepath()
343
    {
344 46
        return $this->filepath;
345
    }
346
347
    /**
348
     * @param string $filepath
349
     */
350 46
    public function setFilepath($filepath)
351
    {
352 46
        $this->filepath = $filepath;
353 46
    }
354
355
    /**
356
     * @return bool
357
     */
358 1
    public function isAbstract()
359
    {
360 1
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
361
    }
362
363
    /**
364
     * @return bool
365
     */
366 1
    public function isFinal()
367
    {
368 1
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_FINAL);
369
    }
370
371
    /**
372
     * @param null|string $extendsClass
373
     */
374
    public function setExtendsClass($extendsClass)
375
    {
376
        $this->extendsClass = $extendsClass;
377
    }
378
379
    /**
380
     * @return null|ClassDefinition
381
     */
382
    public function getExtendsClassDefinition()
383
    {
384
        return $this->extendsClassDefinition;
385
    }
386
387
    /**
388
     * @param ClassDefinition $extendsClassDefinition
389
     */
390 1
    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...
391
    {
392 1
        $this->extendsClassDefinition = $extendsClassDefinition;
393 1
    }
394
395
    /**
396
     * @param string $interface
397
     */
398
    public function addInterface($interface)
399
    {
400
        $this->interfaces[] = $interface;
401
    }
402
403
    /**
404
     * @return null|string
405
     */
406 46
    public function getExtendsClass()
407
    {
408 46
        return $this->extendsClass;
409
    }
410
411
    /**
412
     * @param TraitDefinition $definition
413
     * @param Node\Stmt\TraitUseAdaptation\Alias[] $adaptations
414
     */
415
    public function mergeTrait(TraitDefinition $definition, array $adaptations)
416
    {
417
        $methods = $definition->getMethods();
418
        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...
419
            foreach ($adaptations as $adaptation) {
420
                // We don't support Trait name for now
421
                if (!$adaptation->trait) {
422
                    $methodNameFromTrait = $adaptation->method;
423
                    if (isset($methods[$methodNameFromTrait])) {
424
                        /** @var ClassMethod $method Method from Trait */
425
                        $method = $methods[$methodNameFromTrait];
426
                        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...
427
                            || ($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...
428
                            // Don't modify original method from Trait
429
                            $method = clone $method;
430
                            $method->setName($adaptation->newName);
431
                            $method->setModifier($adaptation->newModifier);
432
433
                            $methods[$methodNameFromTrait] = $method;
434
                        }
435
                    }
436
                }
437
            }
438
439
            foreach ($methods as $method) {
440
                $this->addMethod($method, false);
441
            }
442
        }
443
    }
444
}
445