Passed
Push — feature/issue-714-fix-self-and... ( 38aaf5 )
by Kyle
02:28
created

UndefinedVariable   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 316
Duplicated Lines 10.76 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 34
loc 316
rs 6.4799
c 0
b 0
f 0
wmc 54
lcom 1
cbo 9

15 Methods

Rating   Name   Duplication   Size   Complexity  
B apply() 0 21 6
A collect() 0 11 1
A collectGlobalStatements() 0 10 3
A collectExceptionCatches() 12 12 4
A collectListExpressions() 10 10 3
B collectForeachStatements() 0 18 7
A collectClosureParameters() 0 8 2
A checkVariableDefined() 0 6 2
A collectParameters() 0 12 2
A collectAssignments() 0 16 4
A collectPropertyPostfix() 12 12 4
A addVariableDefinition() 0 8 2
A getNode() 0 8 2
B getVariableImage() 0 28 9
A isNameAllowedInContext() 0 8 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like UndefinedVariable 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 UndefinedVariable, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of PHP Mess Detector.
4
 *
5
 * Copyright (c) Manuel Pichler <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * Licensed under BSD License
9
 * For full copyright and license information, please see the LICENSE file.
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @author Manuel Pichler <[email protected]>
13
 * @copyright Manuel Pichler. All rights reserved.
14
 * @license https://opensource.org/licenses/bsd-license.php BSD License
15
 * @link http://phpmd.org/
16
 */
17
18
namespace PHPMD\Rule\CleanCode;
19
20
use PDepend\Source\AST\ASTArray;
21
use PDepend\Source\AST\ASTArrayIndexExpression;
22
use PDepend\Source\AST\ASTMemberPrimaryPrefix;
23
use PDepend\Source\AST\ASTPropertyPostfix;
24
use PDepend\Source\AST\ASTUnaryExpression;
25
use PDepend\Source\AST\ASTVariable;
26
use PDepend\Source\AST\ASTVariableDeclarator;
27
use PDepend\Source\AST\State;
28
use PHPMD\AbstractNode;
29
use PHPMD\Node\AbstractCallableNode;
30
use PHPMD\Node\ASTNode;
31
use PHPMD\Node\MethodNode;
32
use PHPMD\Rule\AbstractLocalVariable;
33
use PHPMD\Rule\FunctionAware;
34
use PHPMD\Rule\MethodAware;
35
36
/**
37
 * This rule collects all undefined variables within a given function or method
38
 * that are used by any code in the analyzed source artifact.
39
 */
40
class UndefinedVariable extends AbstractLocalVariable implements FunctionAware, MethodAware
41
{
42
    /**
43
     * @var array Self reference class names.
44
     */
45
    protected $selfReferences = array('self', 'static');
46
47
    /**
48
     * Found variable images within a single method or function.
49
     *
50
     * @var array(string)
51
     */
52
    private $images = array();
53
54
    /**
55
     * This method checks that all local variables within the given function or
56
     * method are used at least one time.
57
     *
58
     * @param \PHPMD\AbstractNode $node
59
     * @return void
60
     */
61
    public function apply(AbstractNode $node)
62
    {
63
        $this->images = array();
64
65
        $this->collect($node);
66
67
        foreach ($node->findChildrenOfType('Class') as $class) {
68
            foreach ($class->getMethods() as $method) {
0 ignored issues
show
Documentation Bug introduced by
The method getMethods does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
69
                $this->collect(new MethodNode($method));
70
            }
71
        }
72
73
        foreach ($node->findChildrenOfType('Variable') as $variable) {
74
            if (!$this->isNotSuperGlobal($variable)) {
75
                $this->addVariableDefinition($variable);
0 ignored issues
show
Documentation introduced by
$variable is of type object<PHPMD\Node\ASTNode>, but the function expects a object<PDepend\Source\AS...\ASTVariableDeclarator>.

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...
76
            }
77
            if (!$this->checkVariableDefined($variable, $node)) {
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
78
                $this->addViolation($variable, array($this->getVariableImage($variable)));
0 ignored issues
show
Documentation introduced by
$variable is of type object<PHPMD\Node\ASTNode>, but the function expects a object<PDepend\Source\AS...\ASTVariableDeclarator>.

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...
79
            }
80
        }
81
    }
82
83
    /**
84
     * Collect variables defined inside a PHPMD entry node (such as MethodNode).
85
     *
86
     * @param AbstractNode $node
87
     */
88
    private function collect(AbstractNode $node)
89
    {
90
        $this->collectPropertyPostfix($node);
91
        $this->collectClosureParameters($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
92
        $this->collectForeachStatements($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
93
        $this->collectListExpressions($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
94
        $this->collectAssignments($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
95
        $this->collectParameters($node);
96
        $this->collectExceptionCatches($node);
0 ignored issues
show
Compatibility introduced by
$node of type object<PHPMD\AbstractNode> is not a sub-type of object<PHPMD\Node\AbstractCallableNode>. It seems like you assume a child class of the class PHPMD\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
97
        $this->collectGlobalStatements($node);
98
    }
99
100
    /**
101
     * Stores the given literal node in an global of found variables.
102
     *
103
     * @param \PHPMD\Node\AbstractNode $node
104
     * @return void
105
     */
106
    private function collectGlobalStatements(AbstractNode $node)
107
    {
108
        $globalStatements = $node->findChildrenOfType('GlobalStatement');
109
110
        foreach ($globalStatements as $globalStatement) {
111
            foreach ($globalStatement->getChildren() as $variable) {
0 ignored issues
show
Documentation Bug introduced by
The method getChildren does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
112
                $this->addVariableDefinition($variable);
113
            }
114
        }
115
    }
116
117
    /**
118
     * Stores the given literal node in an catch of found variables.
119
     *
120
     * @param \PHPMD\Node\AbstractCallableNode $node
121
     * @return void
122
     */
123 View Code Duplication
    private function collectExceptionCatches(AbstractCallableNode $node)
124
    {
125
        $catchStatements = $node->findChildrenOfType('CatchStatement');
126
127
        foreach ($catchStatements as $catchStatement) {
128
            foreach ($catchStatement->getChildren() as $children) {
0 ignored issues
show
Documentation Bug introduced by
The method getChildren does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
129
                if ($children instanceof ASTVariable) {
130
                    $this->addVariableDefinition($children);
131
                }
132
            }
133
        }
134
    }
135
136
    /**
137
     * Stores the given literal node in an internal list of found variables.
138
     *
139
     * @param \PHPMD\Node\AbstractCallableNode $node
140
     * @return void
141
     */
142 View Code Duplication
    private function collectListExpressions(AbstractCallableNode $node)
143
    {
144
        $lists = $node->findChildrenOfType('ListExpression');
145
146
        foreach ($lists as $listExpression) {
147
            foreach ($listExpression->getChildren() as $variable) {
0 ignored issues
show
Documentation Bug introduced by
The method getChildren does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
148
                $this->addVariableDefinition($variable);
149
            }
150
        }
151
    }
152
153
    /**
154
     * Stores the given literal node in an internal foreach of found variables.
155
     *
156
     * @param \PHPMD\Node\AbstractCallableNode $node
157
     * @return void
158
     */
159
    private function collectForeachStatements(AbstractCallableNode $node)
160
    {
161
        $foreachStatements = $node->findChildrenOfType('ForeachStatement');
162
163
        foreach ($foreachStatements as $foreachStatement) {
164
            foreach ($foreachStatement->getChildren() as $children) {
0 ignored issues
show
Documentation Bug introduced by
The method getChildren does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
165
                if ($children instanceof ASTVariable) {
166
                    $this->addVariableDefinition($children);
167
                } elseif ($children instanceof ASTUnaryExpression) {
168
                    foreach ($children->getChildren() as $refChildren) {
169
                        if ($refChildren instanceof ASTVariable) {
170
                            $this->addVariableDefinition($refChildren);
171
                        }
172
                    }
173
                }
174
            }
175
        }
176
    }
177
178
    /**
179
     * Stores the given literal node in an internal closure of found variables.
180
     *
181
     * @param \PHPMD\Node\AbstractCallableNode $node
182
     * @return void
183
     */
184
    private function collectClosureParameters(AbstractCallableNode $node)
185
    {
186
        $closures = $node->findChildrenOfType('Closure');
187
188
        foreach ($closures as $closure) {
189
            $this->collectParameters($closure);
190
        }
191
    }
192
193
    /**
194
     * Check if the given variable was defined in the current context before usage.
195
     *
196
     * @param \PHPMD\Node\ASTNode $variable
197
     * @param \PHPMD\Node\AbstractCallableNode $parentNode
198
     * @return bool
199
     */
200
    private function checkVariableDefined(ASTNode $variable, AbstractCallableNode $parentNode)
201
    {
202
        $image = $this->getVariableImage($variable);
0 ignored issues
show
Documentation introduced by
$variable is of type object<PHPMD\Node\ASTNode>, but the function expects a object<PDepend\Source\AS...\ASTVariableDeclarator>.

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...
203
204
        return isset($this->images[$image]) || $this->isNameAllowedInContext($parentNode, $variable);
205
    }
206
207
    /**
208
     * Collect parameter names of method/function.
209
     *
210
     * @param \PHPMD\Node\AbstractNode $node
211
     * @return void
212
     */
213
    private function collectParameters(AbstractNode $node)
214
    {
215
        // Get formal parameter container
216
        $parameters = $node->getFirstChildOfType('FormalParameters');
217
218
        // Now get all declarators in the formal parameters container
219
        $declarators = $parameters->findChildrenOfType('VariableDeclarator');
220
221
        foreach ($declarators as $declarator) {
222
            $this->addVariableDefinition($declarator);
223
        }
224
    }
225
226
    /**
227
     * Collect assignments of variables.
228
     *
229
     * @param \PHPMD\Node\AbstractCallableNode $node
230
     * @return void
231
     */
232
    private function collectAssignments(AbstractCallableNode $node)
233
    {
234
        foreach ($node->findChildrenOfType('AssignmentExpression') as $assignment) {
235
            $variable = $assignment->getChild(0);
236
237
            if ($variable->getNode() instanceof ASTArray) {
238
                foreach ($variable->findChildrenOfType('Variable') as $unpackedVariable) {
239
                    $this->addVariableDefinition($unpackedVariable);
0 ignored issues
show
Documentation introduced by
$unpackedVariable is of type object<PHPMD\Node\ASTNode>, but the function expects a object<PDepend\Source\AS...\ASTVariableDeclarator>.

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...
240
                }
241
242
                continue;
243
            }
244
245
            $this->addVariableDefinition($variable);
0 ignored issues
show
Documentation introduced by
$variable is of type object<PHPMD\Node\ASTNode>, but the function expects a object<PDepend\Source\AS...\ASTVariableDeclarator>.

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...
246
        }
247
    }
248
249
    /**
250
     * Collect postfix property.
251
     *
252
     * @param \PHPMD\Node\AbstractNode $node
253
     * @return void
254
     */
255 View Code Duplication
    private function collectPropertyPostfix(AbstractNode $node)
256
    {
257
        $properties = $node->findChildrenOfType('PropertyPostfix');
258
259
        foreach ($properties as $property) {
260
            foreach ($property->getChildren() as $children) {
0 ignored issues
show
Documentation Bug introduced by
The method getChildren does not exist on object<PHPMD\Node\ASTNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
261
                if ($children instanceof ASTVariable) {
262
                    $this->addVariableDefinition($children);
263
                }
264
            }
265
        }
266
    }
267
268
    /**
269
     * Add the variable to images.
270
     *
271
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
272
     * @return void
273
     */
274
    private function addVariableDefinition($variable)
275
    {
276
        $image = $this->getVariableImage($variable);
277
278
        if (!isset($this->images[$image])) {
279
            $this->images[$image] = $variable;
280
        }
281
    }
282
283
    /**
284
     * Return the PDepend node of ASTNode PHPMD node.
285
     *
286
     * Or return the input as is if it's not an ASTNode PHPMD node.
287
     *
288
     * @param mixed $node
289
     *
290
     * @return \PDepend\Source\AST\ASTArtifact|\PDepend\Source\AST\ASTNode
291
     */
292
    private function getNode($node)
293
    {
294
        if ($node instanceof ASTNode) {
295
            return $node->getNode();
296
        }
297
298
        return $node;
299
    }
300
301
    /**
302
     * Get the image of the given variable node.
303
     *
304
     * Prefix self:: and static:: properties with "::".
305
     *
306
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
307
     *
308
     * @return string
309
     */
310
    private function getVariableImage($variable)
311
    {
312
        $image = $variable->getImage();
313
314
        if ($image === '::') {
315
            return $image.$variable->getChild(1)->getImage();
316
        }
317
318
        $base = $variable;
319
        $parent = $this->getNode($variable->getParent());
320
321
        while ($parent && $parent instanceof ASTArrayIndexExpression && $parent->getChild(0) === $base->getNode()) {
322
            $base = $parent;
323
            $parent = $this->getNode($base->getParent());
324
        }
325
326
        if ($parent && $parent instanceof ASTPropertyPostfix) {
327
            $parent = $parent->getParent();
328
329
            if ($parent instanceof ASTMemberPrimaryPrefix &&
330
                in_array($parent->getChild(0)->getImage(), $this->selfReferences)
331
            ) {
332
                return "::$image";
333
            }
334
        }
335
336
        return $image;
337
    }
338
339
    /**
340
     * Checks if a short name is acceptable in the current context.
341
     *
342
     * @param \PHPMD\Node\AbstractCallableNode $node
343
     * @param \PHPMD\Node\ASTNode $variable
344
     *
345
     * @return boolean
346
     */
347
    private function isNameAllowedInContext(AbstractCallableNode $node, ASTNode $variable)
348
    {
349
        return (
350
            $node instanceof MethodNode &&
351
            $variable->getImage() === '$this' &&
352
            ($node->getModifiers() & State::IS_STATIC) === 0
0 ignored issues
show
Documentation Bug introduced by
The method getModifiers does not exist on object<PHPMD\Node\MethodNode>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
353
        );
354
    }
355
}
356