Passed
Push — master ( 646450...8b10c9 )
by Kyle
49s queued 11s
created

UndefinedVariable::collectProperties()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 6

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 12
ccs 1
cts 2
cp 0.5
crap 6
rs 9.8666
c 0
b 0
f 0
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\ASTClass;
22
use PDepend\Source\AST\ASTPropertyPostfix;
23
use PDepend\Source\AST\ASTUnaryExpression;
24
use PDepend\Source\AST\ASTVariable;
25
use PDepend\Source\AST\ASTVariableDeclarator;
26
use PDepend\Source\AST\State;
27
use PHPMD\AbstractNode;
28
use PHPMD\Node\AbstractCallableNode;
29
use PHPMD\Node\ASTNode;
30
use PHPMD\Node\MethodNode;
31
use PHPMD\Rule\AbstractLocalVariable;
32
use PHPMD\Rule\FunctionAware;
33
use PHPMD\Rule\MethodAware;
34
35
/**
36
 * This rule collects all undefined variables within a given function or method
37
 * that are used by any code in the analyzed source artifact.
38
 */
39
class UndefinedVariable extends AbstractLocalVariable implements FunctionAware, MethodAware
40
{
41
    /**
42
     * Found variable images within a single method or function.
43
     *
44
     * @var array(string)
45
     */
46
    private $images = array();
47
48
    /**
49
     * This method checks that all local variables within the given function or
50 6
     * method are used at least one time.
51
     *
52 6
     * @param \PHPMD\AbstractNode $node
53
     * @return void
54 6
     */
55 6
    public function apply(AbstractNode $node)
56 6
    {
57 6
        $this->images = array();
58 6
59 6
        if ($node instanceof MethodNode) {
60 6
            $this->collectProperties($this->getNode($node->getNode()->getParent()));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PDepend\Source\AST\ASTArtifact as the method getParent() does only exist in the following implementations of said interface: PDepend\Source\AST\ASTAnonymousClass, PDepend\Source\AST\ASTMethod.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
61 6
        }
62
63 6
        $this->collect($node);
64 6
65 1
        foreach ($node->findChildrenOfType('Class') as $class) {
66
            /** @var ASTClass $class */
67 6
68 4
            $this->collectProperties($class);
69
70
            foreach ($class->getMethods() as $method) {
71 6
                $this->collect(new MethodNode($method));
0 ignored issues
show
Compatibility introduced by
$method of type object<PDepend\Source\AST\ASTArtifact> is not a sub-type of object<PDepend\Source\AST\ASTMethod>. It seems like you assume a concrete implementation of the interface PDepend\Source\AST\ASTArtifact 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...
72
            }
73
        }
74
75
        foreach ($node->findChildrenOfType('Variable') as $variable) {
76
            if (!$this->isNotSuperGlobal($variable)) {
77
                $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...
78
            }
79 6
80
            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...
81 6
                $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...
82
            }
83 6
        }
84
    }
85
86
    /**
87
     * Collect variables defined inside a PHPMD entry node (such as MethodNode).
88 6
     *
89
     * @param AbstractNode $node
90
     */
91
    private function collect(AbstractNode $node)
92
    {
93
        $this->collectPropertyPostfix($node);
94
        $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...
95
        $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...
96 6
        $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...
97
        $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...
98 6
        $this->collectParameters($node);
99
        $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...
100 6
        $this->collectGlobalStatements($node);
101
    }
102
103
    private function collectProperties($node)
104
    {
105
        if (!($node instanceof ASTClass)) {
106
            return;
107 6
        }
108
109
        foreach ($node->getProperties() as $property) {
110
            if ($property->isStatic()) {
111
                $this->images['::'.$property->getName()] = $property;
112
            }
113
        }
114
    }
115 6
116
    /**
117 6
     * Stores the given literal node in an global of found variables.
118
     *
119 6
     * @param \PHPMD\Node\AbstractNode $node
120
     * @return void
121
     */
122
    private function collectGlobalStatements(AbstractNode $node)
123
    {
124 6
        $globalStatements = $node->findChildrenOfType('GlobalStatement');
125
126
        foreach ($globalStatements as $globalStatement) {
127
            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...
128
                $this->addVariableDefinition($variable);
129
            }
130
        }
131
    }
132
133 6
    /**
134
     * Stores the given literal node in an catch of found variables.
135 6
     *
136
     * @param \PHPMD\Node\AbstractCallableNode $node
137 6
     * @return void
138
     */
139 View Code Duplication
    private function collectExceptionCatches(AbstractCallableNode $node)
140
    {
141
        $catchStatements = $node->findChildrenOfType('CatchStatement');
142
143
        foreach ($catchStatements as $catchStatement) {
144 6
            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...
145
                if ($children instanceof ASTVariable) {
146
                    $this->addVariableDefinition($children);
147
                }
148
            }
149
        }
150
    }
151
152 6
    /**
153
     * Stores the given literal node in an internal list of found variables.
154 6
     *
155
     * @param \PHPMD\Node\AbstractCallableNode $node
156 6
     * @return void
157
     */
158 View Code Duplication
    private function collectListExpressions(AbstractCallableNode $node)
159 6
    {
160
        $lists = $node->findChildrenOfType('ListExpression');
161
162
        foreach ($lists as $listExpression) {
163
            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...
164
                $this->addVariableDefinition($variable);
165
            }
166
        }
167
    }
168 6
169
    /**
170 6
     * Stores the given literal node in an internal foreach of found variables.
171
     *
172
     * @param \PHPMD\Node\AbstractCallableNode $node
173
     * @return void
174
     */
175
    private function collectForeachStatements(AbstractCallableNode $node)
176
    {
177
        $foreachStatements = $node->findChildrenOfType('ForeachStatement');
178
179 6
        foreach ($foreachStatements as $foreachStatement) {
180
            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...
181
                if ($children instanceof ASTVariable) {
182 6
                    $this->addVariableDefinition($children);
183
                } elseif ($children instanceof ASTUnaryExpression) {
184
                    foreach ($children->getChildren() as $refChildren) {
185 6
                        if ($refChildren instanceof ASTVariable) {
186
                            $this->addVariableDefinition($refChildren);
187 6
                        }
188
                    }
189
                }
190 6
            }
191
        }
192
    }
193
194
    /**
195
     * Stores the given literal node in an internal closure of found variables.
196
     *
197
     * @param \PHPMD\Node\AbstractCallableNode $node
198 6
     * @return void
199
     */
200 6
    private function collectClosureParameters(AbstractCallableNode $node)
201 3
    {
202
        $closures = $node->findChildrenOfType('Closure');
203 3
204
        foreach ($closures as $closure) {
205 6
            $this->collectParameters($closure);
206
        }
207
    }
208
209
    /**
210
     * Check if the given variable was defined in the current context before usage.
211
     *
212
     * @param \PHPMD\Node\ASTNode $variable
213 6
     * @param \PHPMD\Node\AbstractCallableNode $parentNode
214
     * @return bool
215 6
     */
216
    private function checkVariableDefined(ASTNode $variable, AbstractCallableNode $parentNode)
217 6
    {
218 1
        $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...
219 1
220
        return isset($this->images[$image]) || $this->isNameAllowedInContext($parentNode, $variable);
221
    }
222
223
    /**
224 6
     * Collect parameter names of method/function.
225
     *
226
     * @param \PHPMD\Node\AbstractNode $node
227
     * @return void
228
     */
229
    private function collectParameters(AbstractNode $node)
230
    {
231
        // Get formal parameter container
232 4
        $parameters = $node->getFirstChildOfType('FormalParameters');
233
234 4
        // Now get all declarators in the formal parameters container
235 4
        $declarators = $parameters->findChildrenOfType('VariableDeclarator');
236
237 4
        foreach ($declarators as $declarator) {
238
            $this->addVariableDefinition($declarator);
239
        }
240
    }
241
242
    /**
243
     * Collect assignments of variables.
244
     *
245
     * @param \PHPMD\Node\AbstractCallableNode $node
246
     * @return void
247 5
     */
248
    private function collectAssignments(AbstractCallableNode $node)
249
    {
250 5
        foreach ($node->findChildrenOfType('AssignmentExpression') as $assignment) {
251 5
            $variable = $assignment->getChild(0);
252 5
253
            if ($variable->getNode() instanceof ASTArray) {
254
                foreach ($variable->findChildrenOfType('Variable') as $unpackedVariable) {
255
                    $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...
256
                }
257
258
                continue;
259
            }
260
261
            $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...
262
        }
263
    }
264
265
    /**
266
     * Collect postfix property.
267
     *
268
     * @param \PHPMD\Node\AbstractNode $node
269
     * @return void
270
     */
271 View Code Duplication
    private function collectPropertyPostfix(AbstractNode $node)
272
    {
273
        $properties = $node->findChildrenOfType('PropertyPostfix');
274
275
        foreach ($properties as $property) {
276
            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...
277
                if ($children instanceof ASTVariable) {
278
                    $this->addVariableDefinition($children);
279
                }
280
            }
281
        }
282
    }
283
284
    /**
285
     * Add the variable to images.
286
     *
287
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
288
     * @return void
289
     */
290
    private function addVariableDefinition($variable)
291
    {
292
        $image = $this->getVariableImage($variable);
293
294
        if (!isset($this->images[$image])) {
295
            $this->images[$image] = $variable;
296
        }
297
    }
298
299
    /**
300
     * Checks if a short name is acceptable in the current context.
301
     *
302
     * @param \PHPMD\Node\AbstractCallableNode $node
303
     * @param \PHPMD\Node\ASTNode $variable
304
     *
305
     * @return boolean
306
     */
307
    private function isNameAllowedInContext(AbstractCallableNode $node, ASTNode $variable)
308
    {
309
        return (
310
            $node instanceof MethodNode &&
311
            $variable->getImage() === '$this' &&
312
            ($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...
313
        );
314
    }
315
}
316