Completed
Push — master ( 279f64...652b11 )
by Дмитрий
10:56
created

ClassDefinition::addInterface()   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 1
dl 0
loc 4
ccs 0
cts 3
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 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...
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\Const_[]
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 888
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
87
    {
88 888
        $this->name = $name;
89 888
        $this->statement = $statement;
90 888
        $this->type = $type;
91 888
    }
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 46
    public function addMethod(ClassMethod $classMethod, $overwrite = true)
99
    {
100 46
        if ($overwrite) {
101 46
            $this->methods[$classMethod->getName()] = $classMethod;
102 46
        } else {
103
            $name = $classMethod->getName();
104
            if (isset($this->methods[$name])) {
105 1
                return false;
106
            } else {
107
                $this->methods[$name] = $classMethod;
108
            }
109
        }
110
111 46
        return true;
112
    }
113
114
    /**
115
     * @param Node\Stmt\Property $property
116
     */
117 8
    public function addProperty(Node\Stmt\Property $property)
118
    {
119 8
        foreach ($property->props as $propertyDefinition) {
120 8
            $this->properties[$propertyDefinition->name] = $propertyDefinition;
121 8
        }
122
123 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 119. 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...
124 8
    }
125
126
    /**
127
     * @param Node\Stmt\ClassConst $const
128
     */
129 4
    public function addConst(Node\Stmt\ClassConst $const)
130
    {
131 4
        $this->constants[$const->consts[0]->name] = $const;
132 4
    }
133
134
    /**
135
     * @param Context $context
136
     * @return $this
137
     */
138 48
    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...
139
    {
140 48
        if ($this->compiled) {
141
            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...
142
        }
143
144 48
        $this->compiled = true;
145 48
        $context->setFilepath($this->filepath);
146 48
        $context->setScope($this);
147
148 48
        $context->getEventManager()->fire(
149 48
            Event\StatementBeforeCompile::EVENT_NAME,
150 48
            new Event\StatementBeforeCompile(
151 48
                $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...
152
                $context
153 48
            )
154 48
        );
155
156
        // Compile event for properties
157 48
        foreach ($this->properties as $property) {
158 7
            if (!$property->default) {
159 3
                continue;
160
            }
161
162
            // fire expression event for property default
163 6
            $context->getEventManager()->fire(
164 6
                Event\ExpressionBeforeCompile::EVENT_NAME,
165 6
                new Event\ExpressionBeforeCompile(
166 6
                    $property->default,
167
                    $context
168 6
                )
169 6
            );
170 48
        }
171
172
        // Compile event for PropertyProperty
173 48
        foreach ($this->properties as $property) {
174 7
            $context->getEventManager()->fire(
175 7
                Event\StatementBeforeCompile::EVENT_NAME,
176 7
                new Event\StatementBeforeCompile(
177 7
                    $property,
178
                    $context
179 7
                )
180 7
            );
181 48
        }
182
183
        // Compile event for constants
184 48
        foreach ($this->constants as $const) {
185 2
            $context->getEventManager()->fire(
186 2
                Event\StatementBeforeCompile::EVENT_NAME,
187 2
                new Event\StatementBeforeCompile(
188 2
                    $const,
189
                    $context
190 2
                )
191 2
            );
192 48
        }
193
194
        // Compiler event for property statements
195 48
        foreach ($this->propertyStatements as $prop) {
196 7
            $context->getEventManager()->fire(
197 7
                Event\StatementBeforeCompile::EVENT_NAME,
198 7
                new Event\StatementBeforeCompile(
199 7
                    $prop,
200
                    $context
201 7
                )
202 7
            );
203 48
        }
204
205
        // Compile each method
206 48
        foreach ($this->methods as $method) {
207 45
            $context->clearSymbols();
208
209 45
            if (!$method->isStatic()) {
210 44
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
211 44
                $thisPtr->incGets();
212 44
                $context->addVariable($thisPtr);
213 44
            }
214
215 45
            $method->compile($context);
216
217 45
            $symbols = $context->getSymbols();
218 45
            if (count($symbols) > 0) {
219 44
                foreach ($symbols as $name => $variable) {
220 44
                    if ($variable->isUnused()) {
221 13
                        $context->warning(
222 13
                            'unused-' . $variable->getSymbolType(),
223 13
                            sprintf(
224 13
                                'Unused ' . $variable->getSymbolType() . ' $%s in method %s()',
225 13
                                $variable->getName(),
226 13
                                $method->getName()
227 13
                            )
228 13
                        );
229 13
                    }
230 44
                }
231 44
            }
232 48
        }
233
234 48
        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...
235
    }
236
237
    /**
238
     * @param string $name
239
     * @param boolean|false $inherit
240
     * @return bool
241
     */
242 2
    public function hasMethod($name, $inherit = false)
243
    {
244 2
        if (isset($this->methods[$name])) {
245 2
            return true;
246
        }
247
248 1
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
249
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
250
            return $method && ($method->isPublic() || $method->isProtected());
251
        }
252
253 1
        return false;
254
    }
255
256
    /**
257
     * @param string $name
258
     * @param bool $inherit
259
     * @return bool
260
     */
261 2
    public function hasConst($name, $inherit = false)
262
    {
263 2
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasConst($name, $inherit)) {
264 1
            return true;
265
        }
266
267 2
        return isset($this->constants[$name]);
268
    }
269
270
    /**
271
     * @param $name
272
     * @param boolean|false $inherit
273
     * @return ClassMethod|null
274
     */
275 2
    public function getMethod($name, $inherit = false)
276
    {
277 2
        if (isset($this->methods[$name])) {
278 2
            return $this->methods[$name];
279
        }
280
281
        if ($inherit && $this->extendsClassDefinition) {
282
            return $this->extendsClassDefinition->getMethod($name, $inherit);
283
        }
284
285
        return null;
286
    }
287
288
    /**
289
     * @param $name
290
     * @param bool $inherit
291
     * @return bool
292
     */
293 1
    public function hasProperty($name, $inherit = false)
294
    {
295 1
        if (isset($this->properties[$name])) {
296 1
            return isset($this->properties[$name]);
297
        }
298
299 1
        return $inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasProperty($name, true);
300
    }
301
302
    /**
303
     * @param string $name
304
     * @param bool $inherit
305
     * @return Node\Stmt\PropertyProperty
306
     */
307
    public function getProperty($name, $inherit = false)
308
    {
309
        assert($this->hasProperty($name, $inherit));
310
311
        if (isset($this->properties[$name])) {
312
            return $this->properties[$name];
313
        }
314
315
        if ($inherit && $this->extendsClassDefinition) {
316
            return $this->extendsClassDefinition->getProperty($name, true);
317
        }
318
319
        return null;
320
    }
321
322
    /**
323
     * @param string $name
324
     * @param bool $inherit
325
     * @return Node\Stmt\Property
326
     */
327
    public function getPropertyStatement($name, $inherit = false)
328
    {
329
        if (isset($this->propertyStatements[$name])) {
330
            return $this->propertyStatements[$name];
331
        }
332
333
        if ($inherit && $this->extendsClassDefinition) {
334
            return $this->extendsClassDefinition->getPropertyStatement($name, true);
335
        }
336
337
        return null;
338
    }
339
340
    /**
341
     * @return string
342
     */
343 48
    public function getFilepath()
344
    {
345 48
        return $this->filepath;
346
    }
347
348
    /**
349
     * @param string $filepath
350
     */
351 48
    public function setFilepath($filepath)
352
    {
353 48
        $this->filepath = $filepath;
354 48
    }
355
356
    /**
357
     * @return bool
358
     */
359 1
    public function isAbstract()
360
    {
361 1
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_ABSTRACT);
362
    }
363
364
    /**
365
     * @return bool
366
     */
367 1
    public function isFinal()
368
    {
369 1
        return (bool) ($this->type & Node\Stmt\Class_::MODIFIER_FINAL);
370
    }
371
372
    /**
373
     * @param null|string $extendsClass
374
     */
375
    public function setExtendsClass($extendsClass)
376
    {
377
        $this->extendsClass = $extendsClass;
378
    }
379
380
    /**
381
     * @return null|ClassDefinition
382
     */
383
    public function getExtendsClassDefinition()
384
    {
385
        return $this->extendsClassDefinition;
386
    }
387
388
    /**
389
     * @param ClassDefinition $extendsClassDefinition
390
     */
391 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...
392
    {
393 1
        $this->extendsClassDefinition = $extendsClassDefinition;
394 1
    }
395
396
    /**
397
     * @param string $interface
398
     */
399
    public function addInterface($interface)
400
    {
401
        $this->interfaces[] = $interface;
402
    }
403
404
    /**
405
     * @return null|string
406
     */
407 48
    public function getExtendsClass()
408
    {
409 48
        return $this->extendsClass;
410
    }
411
412
    /**
413
     * @param TraitDefinition $definition
414
     * @param Node\Stmt\TraitUseAdaptation\Alias[] $adaptations
415
     */
416
    public function mergeTrait(TraitDefinition $definition, array $adaptations)
417
    {
418
        $methods = $definition->getMethods();
419
        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...
420
            foreach ($adaptations as $adaptation) {
421
                // We don't support Trait name for now
422
                if (!$adaptation->trait) {
423
                    $methodNameFromTrait = $adaptation->method;
424
                    if (isset($methods[$methodNameFromTrait])) {
425
                        /** @var ClassMethod $method Method from Trait */
426
                        $method = $methods[$methodNameFromTrait];
427
                        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...
428
                            || ($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...
429
                            // Don't modify original method from Trait
430
                            $method = clone $method;
431
                            $method->setName($adaptation->newName);
432
                            $method->setModifier($adaptation->newModifier);
433
434
                            $methods[$methodNameFromTrait] = $method;
435
                        }
436
                    }
437
                }
438
            }
439
440
            foreach ($methods as $method) {
441
                $this->addMethod($method, false);
442
            }
443
        }
444
    }
445
}
446