Completed
Pull Request — master (#283)
by Enrico
04:27
created

ClassDefinition::addMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.7898

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 15
ccs 5
cts 9
cp 0.5556
crap 3.7898
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 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 string[]
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 56
    public function __construct($name, Node\Stmt\Class_ $statement = null, $type = 0)
87
    {
88 56
        $this->name = $name;
89 56
        $this->statement = $statement;
90 56
        $this->type = $type;
91 56
    }
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 48
    public function addMethod(ClassMethod $classMethod, $overwrite = true)
99
    {
100 48
        if ($overwrite) {
101 48
            $this->methods[$classMethod->getName()] = $classMethod;
102 48
        } 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 48
        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 50
    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 50
        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 50
        $this->compiled = true;
145 50
        $context->setFilepath($this->filepath);
146 50
        $context->setScope($this);
147
148 50
        $context->getEventManager()->fire(
149 50
            Event\StatementBeforeCompile::EVENT_NAME,
150 50
            new Event\StatementBeforeCompile(
151 50
                $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 50
            )
154 50
        );
155
156
        // Compile event for properties
157 50
        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 50
        }
171
172
        // Compile event for PropertyProperty
173 50
        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 50
        }
182
183
        // Compile event for constants
184 50
        foreach ($this->constants as $const) {
185 2
            $context->getEventManager()->fire(
186 2
                Event\StatementBeforeCompile::EVENT_NAME,
187 2
                new Event\StatementBeforeCompile(
188 2
                    $const,
0 ignored issues
show
Documentation introduced by
$const is of type string, but the function expects a object<PhpParser\Node\Stmt>.

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...
189
                    $context
190 2
                )
191 2
            );
192 50
        }
193
194
        // Compiler event for property statements
195 50
        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 50
        }
204
205
        // Compile each method
206 50
        foreach ($this->methods as $method) {
207 47
            $context->clearSymbols();
208
209 47
            if (!$method->isStatic()) {
210 46
                $thisPtr = new Variable('this', $this, CompiledExpression::OBJECT);
211 46
                $thisPtr->incGets();
212 46
                $context->addVariable($thisPtr);
213 46
            }
214
215 47
            $method->compile($context);
216
217 47
            $symbols = $context->getSymbols();
218 47
            if (count($symbols) > 0) {
219 46
                foreach ($symbols as $name => $variable) {
220 46
                    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 46
                }
231 46
            }
232 50
        }
233
234 50
        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 4
    public function hasMethod($name, $inherit = false)
243
    {
244 4
        if (isset($this->methods[$name])) {
245 2
            return true;
246
        }
247
248 3
        if ($inherit && $this->extendsClassDefinition && $this->extendsClassDefinition->hasMethod($name, $inherit)) {
249 2
            $method = $this->extendsClassDefinition->getMethod($name, $inherit);
250 2
            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 4
    public function getMethod($name, $inherit = false)
276
    {
277 4
        if (isset($this->methods[$name])) {
278 2
            return $this->methods[$name];
279
        }
280
281 2
        if ($inherit && $this->extendsClassDefinition) {
282 2
            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 50
    public function getFilepath()
344
    {
345 50
        return $this->filepath;
346
    }
347
348
    /**
349
     * @param string $filepath
350
     */
351 50
    public function setFilepath($filepath)
352
    {
353 50
        $this->filepath = $filepath;
354 50
    }
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 2
    public function setExtendsClass($extendsClass)
376
    {
377 2
        $this->extendsClass = $extendsClass;
378 2
    }
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 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...
392
    {
393 3
        $this->extendsClassDefinition = $extendsClassDefinition;
394 3
    }
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 50
    public function getExtendsClass()
408
    {
409 50
        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