Passed
Push — feature/issue-601-unused-prope... ( c701c0...35ee65 )
by Kyle
12:49
created

AbstractLocalVariable::isPassedByReference()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 6
nop 1
dl 0
loc 24
rs 9.2248
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;
19
20
use PDepend\Source\AST\ASTArguments;
21
use PDepend\Source\AST\ASTArrayIndexExpression;
22
use PDepend\Source\AST\ASTFieldDeclaration;
23
use PDepend\Source\AST\ASTMemberPrimaryPrefix;
24
use PDepend\Source\AST\ASTPropertyPostfix;
25
use PDepend\Source\AST\ASTVariable;
26
use PDepend\Source\AST\ASTVariableDeclarator;
27
use PHPMD\AbstractNode;
28
use PHPMD\AbstractRule;
29
use PHPMD\Node\ASTNode;
30
use ReflectionException;
31
use ReflectionFunction;
32
33
/**
34
 * Base class for rules that rely on local variables.
35
 *
36
 * @since 0.2.6
37
 */
38
abstract class AbstractLocalVariable extends AbstractRule
39
{
40
    /**
41
     * @var array Self reference class names.
42
     */
43
    protected $selfReferences = array('self', 'static');
44
45
    /**
46
     * PHP super globals that are available in all php scopes, so that they
47
     * can never be unused local variables.
48
     *
49
     * @var array(string=>boolean)
50
     * @link http://php.net/manual/en/reserved.variables.php
51
     */
52
    private static $superGlobals = array(
53
        '$argc' => true,
54
        '$argv' => true,
55
        '$_COOKIE' => true,
56
        '$_ENV' => true,
57
        '$_FILES' => true,
58
        '$_GET' => true,
59
        '$_POST' => true,
60
        '$_REQUEST' => true,
61
        '$_SERVER' => true,
62
        '$_SESSION' => true,
63
        '$GLOBALS' => true,
64
        '$HTTP_RAW_POST_DATA' => true,
65
        '$php_errormsg' => true,
66
        '$http_response_header' => true,
67
    );
68
69
    /**
70
     * Tests if the given variable node represents a local variable or if it is
71
     * a static object property or something similar.
72
     *
73
     * @param \PHPMD\Node\ASTNode $variable The variable to check.
74
     * @return boolean
75
     */
76
    protected function isLocal(ASTNode $variable)
77
    {
78
        return (false === $variable->isThis()
0 ignored issues
show
Documentation Bug introduced by
The method isThis 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...
79
            && $this->isNotSuperGlobal($variable)
80
            && $this->isRegularVariable($variable)
81
        );
82
    }
83
84
    /**
85
     * Tests if the given variable represents one of the PHP super globals
86
     * that are available in scopes.
87
     *
88
     * @param \PHPMD\AbstractNode $variable
89
     * @return boolean
90
     */
91
    protected function isSuperGlobal(AbstractNode $variable)
92
    {
93
        return isset(self::$superGlobals[$variable->getImage()]);
94
    }
95
96
    /**
97
     * Tests if the given variable does not represent one of the PHP super globals
98
     * that are available in scopes.
99
     *
100
     * @param \PHPMD\AbstractNode $variable
101
     * @return boolean
102
     */
103
    protected function isNotSuperGlobal(AbstractNode $variable)
104
    {
105
        return !$this->isSuperGlobal($variable);
106
    }
107
108
    /**
109
     * Tests if the given variable node is a regular variable an not property
110
     * or method postfix.
111
     *
112
     * @param \PHPMD\Node\ASTNode $variable
113
     * @return boolean
114
     */
115
    protected function isRegularVariable(ASTNode $variable)
116
    {
117
        $node = $this->stripWrappedIndexExpression($variable);
118
        $parent = $node->getParent();
119
120
        if ($parent->isInstanceOf('PropertyPostfix')) {
121
            $primaryPrefix = $parent->getParent();
122
            if ($primaryPrefix->getParent()->isInstanceOf('MemberPrimaryPrefix')) {
123
                return !$primaryPrefix->getParent()->isStatic();
124
            }
125
126
            return ($parent->getChild(0)->getNode() !== $node->getNode()
127
                || !$primaryPrefix->isStatic()
128
            );
129
        }
130
131
        return true;
132
    }
133
134
    /**
135
     * Removes all index expressions that are wrapped around the given node
136
     * instance.
137
     *
138
     * @param \PHPMD\Node\ASTNode $node
139
     * @return \PHPMD\Node\ASTNode
140
     */
141
    protected function stripWrappedIndexExpression(ASTNode $node)
142
    {
143
        if (false === $this->isWrappedByIndexExpression($node)) {
144
            return $node;
145
        }
146
147
        $parent = $node->getParent();
148
        if ($parent->getChild(0)->getNode() === $node->getNode()) {
149
            return $this->stripWrappedIndexExpression($parent);
0 ignored issues
show
Bug introduced by
It seems like $parent defined by $node->getParent() on line 147 can be null; however, PHPMD\Rule\AbstractLocal...rappedIndexExpression() 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...
150
        }
151
152
        return $node;
153
    }
154
155
    /**
156
     * Tests if the given variable node os part of an index expression.
157
     *
158
     * @param \PHPMD\Node\ASTNode $node
159
     * @return boolean
160
     */
161
    protected function isWrappedByIndexExpression(ASTNode $node)
162
    {
163
        return ($node->getParent()->isInstanceOf('ArrayIndexExpression')
164
            || $node->getParent()->isInstanceOf('StringIndexExpression')
165
        );
166
    }
167
168
    /**
169
     * PHP is case insensitive so we should compare function names case
170
     * insensitive.
171
     *
172
     * @param \PHPMD\AbstractNode $node
173
     * @param string $name
174
     * @return boolean
175
     */
176
    protected function isFunctionNameEqual(AbstractNode $node, $name)
177
    {
178
        return (0 === strcasecmp(trim($node->getImage(), '\\'), $name));
179
    }
180
181
    /**
182
     * AST puts namespace prefix to global functions called from a namespace.
183
     * This method checks if the last part of function fully qualified name is equal to $name
184
     *
185
     * @param \PHPMD\AbstractNode $node
186
     * @param string $name
187
     * @return boolean
188
     */
189
    protected function isFunctionNameEndingWith(AbstractNode $node, $name)
190
    {
191
        $parts = explode('\\', trim($node->getImage(), '\\'));
192
193
        return (0 === strcasecmp(array_pop($parts), $name));
194
    }
195
196
    /**
197
     * Get the image of the given variable node.
198
     *
199
     * Prefix self:: and static:: properties with "::".
200
     *
201
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
202
     * @return string
203
     */
204
    protected function getVariableImage($variable)
205
    {
206
        $image = $variable->getImage();
207
208
        if (substr($image, 0, 1) === '$' && $this->getNode($variable->getParent()) instanceof ASTFieldDeclaration) {
209
            $image = "::$image";
210
        }
211
212
        if ($image === '::') {
213
            return $image.$variable->getChild(1)->getImage();
214
        }
215
216
        return $this->prependMemberPrimaryPrefix($image, $variable);
217
    }
218
219
    /**
220
     * Return the PDepend node of ASTNode PHPMD node.
221
     *
222
     * Or return the input as is if it's not an ASTNode PHPMD node.
223
     *
224
     * @param mixed $node
225
     * @return \PDepend\Source\AST\ASTArtifact|\PDepend\Source\AST\ASTNode
226
     */
227
    protected function getNode($node)
228
    {
229
        if ($node instanceof ASTNode) {
230
            return $node->getNode();
231
        }
232
233
        return $node;
234
    }
235
236
    /**
237
     * Return true if the given variable is passed by reference in a native PHP function.
238
     *
239
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
240
     * @return bool
241
     */
242
    protected function isPassedByReference($variable)
243
    {
244
        $parent = $this->getNode($variable->getParent());
245
246
        if ($parent && $parent instanceof ASTArguments) {
247
            $argumentPosition = array_search($this->getNode($variable), $parent->getChildren());
248
            $function = $this->getNode($parent->getParent());
249
            $functionName = $function->getImage();
0 ignored issues
show
Bug introduced by
The method getImage does only exist in PDepend\Source\AST\ASTNode, but not in PDepend\Source\AST\ASTArtifact.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
250
251
            try {
252
                $reflectionFunction = new ReflectionFunction($functionName);
253
                $parameters = $reflectionFunction->getParameters();
254
255
                if ($parameters[$argumentPosition]->isPassedByReference()) {
256
                    return true;
257
                }
258
            } catch (ReflectionException $exception) {
259
                // @TODO: Find a way to handle user-land functions
260
                // @TODO: Find a way to handle methods
261
            }
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * Prepend "::" if the variable has a ASTMemberPrimaryPrefix.
269
     *
270
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
271
     * @return string
272
     */
273
    private function prependMemberPrimaryPrefix($image, $variable)
274
    {
275
        $base = $this->getNode($variable);
276
        $parent = $this->getNode($base->getParent());
0 ignored issues
show
Bug introduced by
The method getParent does only exist in PDepend\Source\AST\ASTNode, but not in PDepend\Source\AST\ASTArtifact.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
277
278
        while ($parent && $parent instanceof ASTArrayIndexExpression && $parent->getChild(0) === $base) {
279
            $base = $parent;
280
            $parent = $this->getNode($base->getParent());
281
        }
282
283
        if ($parent && $parent instanceof ASTPropertyPostfix) {
284
            $parent = $parent->getParent();
285
286
            if ($parent instanceof ASTMemberPrimaryPrefix &&
287
                in_array($parent->getChild(0)->getImage(), $this->selfReferences)
288
            ) {
289
                return "::$image";
290
            }
291
        }
292
293
        return $image;
294
    }
295
}
296