Passed
Push — master ( 9e531d...2ae08f )
by Kyle
02:50 queued 12s
created

getReflectionFunctionByName()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 19
ccs 0
cts 0
cp 0
crap 20
rs 9.6333
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\ASTMemberPrimaryPrefix;
23
use PDepend\Source\AST\ASTPropertyPostfix;
24
use PDepend\Source\AST\ASTVariable;
25
use PDepend\Source\AST\ASTVariableDeclarator;
26
use PHPMD\AbstractNode;
27
use PHPMD\AbstractRule;
28
use PHPMD\Node\ASTNode;
29
use ReflectionException;
30
use ReflectionFunction;
31
32
/**
33
 * Base class for rules that rely on local variables.
34
 *
35
 * @since 0.2.6
36
 */
37
abstract class AbstractLocalVariable extends AbstractRule
38
{
39
    /**
40
     * @var array Self reference class names.
41
     */
42
    protected $selfReferences = array('self', 'static');
43
44
    /**
45
     * PHP super globals that are available in all php scopes, so that they
46
     * can never be unused local variables.
47
     *
48
     * @var array(string=>boolean)
49
     * @link http://php.net/manual/en/reserved.variables.php
50
     */
51
    protected static $superGlobals = array(
52
        '$argc' => true,
53
        '$argv' => true,
54
        '$_COOKIE' => true,
55
        '$_ENV' => true,
56
        '$_FILES' => true,
57
        '$_GET' => true,
58
        '$_POST' => true,
59
        '$_REQUEST' => true,
60
        '$_SERVER' => true,
61
        '$_SESSION' => true,
62 43
        '$GLOBALS' => true,
63
        '$HTTP_RAW_POST_DATA' => true,
64 43
        '$php_errormsg' => true,
65 43
        '$http_response_header' => true,
66 43
    );
67
68
    /**
69
     * Tests if the given variable node represents a local variable or if it is
70
     * a static object property or something similar.
71
     *
72
     * @param \PHPMD\Node\ASTNode $variable The variable to check.
73
     * @return boolean
74
     */
75
    protected function isLocal(ASTNode $variable)
76
    {
77 48
        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...
78
            && $this->isNotSuperGlobal($variable)
79 48
            && $this->isRegularVariable($variable)
80
        );
81
    }
82
83
    /**
84
     * Tests if the given variable represents one of the PHP super globals
85
     * that are available in scopes.
86
     *
87
     * @param \PHPMD\AbstractNode $variable
88
     * @return boolean
89 43
     */
90
    protected function isSuperGlobal(AbstractNode $variable)
91 43
    {
92 43
        return isset(self::$superGlobals[$variable->getImage()]);
93
    }
94 43
95 17
    /**
96 17
     * Tests if the given variable does not represent one of the PHP super globals
97 1
     * that are available in scopes.
98
     *
99 16
     * @param \PHPMD\AbstractNode $variable
100 16
     * @return boolean
101
     */
102
    protected function isNotSuperGlobal(AbstractNode $variable)
103 39
    {
104
        return !$this->isSuperGlobal($variable);
105
    }
106
107
    /**
108
     * Tests if the given variable node is a regular variable an not property
109
     * or method postfix.
110
     *
111
     * @param \PHPMD\Node\ASTNode $variable
112
     * @return boolean
113 43
     */
114
    protected function isRegularVariable(ASTNode $variable)
115 43
    {
116 43
        $node = $this->stripWrappedIndexExpression($variable);
117
        $parent = $node->getParent();
118
119 14
        if ($parent->isInstanceOf('PropertyPostfix')) {
120 14
            $primaryPrefix = $parent->getParent();
121 14
            if ($primaryPrefix->getParent()->isInstanceOf('MemberPrimaryPrefix')) {
122
                return !$primaryPrefix->getParent()->isStatic();
123 10
            }
124
125
            return ($parent->getChild(0)->getNode() !== $node->getNode()
126
                || !$primaryPrefix->isStatic()
127
            );
128
        }
129
130
        return true;
131
    }
132 43
133
    /**
134 43
     * Removes all index expressions that are wrapped around the given node
135 43
     * instance.
136
     *
137
     * @param \PHPMD\Node\ASTNode $node
138
     * @return \PHPMD\Node\ASTNode
139
     */
140
    protected function stripWrappedIndexExpression(ASTNode $node)
141
    {
142
        if (false === $this->isWrappedByIndexExpression($node)) {
143
            return $node;
144
        }
145
146
        $parent = $node->getParent();
147 8
        if ($parent->getChild(0)->getNode() === $node->getNode()) {
148
            return $this->stripWrappedIndexExpression($parent);
0 ignored issues
show
Bug introduced by
It seems like $parent defined by $node->getParent() on line 146 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...
149 8
        }
150
151
        return $node;
152
    }
153
154
    /**
155
     * Tests if the given variable node os part of an index expression.
156
     *
157
     * @param \PHPMD\Node\ASTNode $node
158
     * @return boolean
159
     */
160 12
    protected function isWrappedByIndexExpression(ASTNode $node)
161
    {
162 12
        return ($node->getParent()->isInstanceOf('ArrayIndexExpression')
163
            || $node->getParent()->isInstanceOf('StringIndexExpression')
164 12
        );
165
    }
166
167
    /**
168
     * PHP is case insensitive so we should compare function names case
169
     * insensitive.
170
     *
171
     * @param \PHPMD\AbstractNode $node
172
     * @param string $name
173
     * @return boolean
174
     */
175
    protected function isFunctionNameEqual(AbstractNode $node, $name)
176
    {
177
        return (0 === strcasecmp(trim($node->getImage(), '\\'), $name));
178
    }
179
180
    /**
181
     * AST puts namespace prefix to global functions called from a namespace.
182
     * This method checks if the last part of function fully qualified name is equal to $name
183
     *
184
     * @param \PHPMD\AbstractNode $node
185
     * @param string $name
186
     * @return boolean
187
     */
188
    protected function isFunctionNameEndingWith(AbstractNode $node, $name)
189
    {
190
        $parts = explode('\\', trim($node->getImage(), '\\'));
191
192
        return (0 === strcasecmp(array_pop($parts), $name));
193
    }
194
195
    /**
196
     * Get the image of the given variable node.
197
     *
198
     * Prefix self:: and static:: properties with "::".
199
     *
200
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
201
     * @return string
202
     */
203
    protected function getVariableImage($variable)
204
    {
205
        $image = $variable->getImage();
206
207
        if ($image === '::') {
208
            return $image.$variable->getChild(1)->getImage();
209
        }
210
211
        $base = $variable;
212
        $parent = $this->getNode($variable->getParent());
213
214
        while ($parent instanceof ASTArrayIndexExpression &&
215
            $base instanceof ASTNode &&
216
            $parent->getChild(0) === $base->getNode()
217
        ) {
218
            $base = $parent;
219
            $parent = $this->getNode($base->getParent());
220
        }
221
222
        if ($parent instanceof ASTPropertyPostfix) {
223
            $previousChildImage = $this->getParentMemberPrimaryPrefixImage($image, $parent);
224
225
            if (in_array($previousChildImage, $this->selfReferences, true)) {
226
                return "::$image";
227
            }
228
        }
229
230
        return $image;
231
    }
232
233
    protected function getParentMemberPrimaryPrefixImage($image, ASTPropertyPostfix $postfix)
234
    {
235
        do {
236
            $postfix = $postfix->getParent();
237
        } while ($postfix && $postfix->getChild(0) && $postfix->getChild(0)->getImage() === $image);
238
239
        $previousChildImage = $postfix->getChild(0)->getImage();
240
241
        if ($postfix instanceof ASTMemberPrimaryPrefix &&
242
            in_array($previousChildImage, $this->selfReferences)
243
        ) {
244
            return $previousChildImage;
245
        }
246
247
        return null;
248
    }
249
250
    /**
251
     * Return the PDepend node of ASTNode PHPMD node.
252
     *
253
     * Or return the input as is if it's not an ASTNode PHPMD node.
254
     *
255
     * @param mixed $node
256
     * @return \PDepend\Source\AST\ASTArtifact|\PDepend\Source\AST\ASTNode
257
     */
258
    protected function getNode($node)
259
    {
260
        if ($node instanceof ASTNode) {
261
            return $node->getNode();
262
        }
263
264
        return $node;
265
    }
266
267
    /**
268
     * Reflect function trying as namespaced function first, then global function.
269
     *
270
     * @param string $functionName
271
     * @return ReflectionFunction|null
272
     */
273
    private function getReflectionFunctionByName($functionName)
274
    {
275
        try {
276
            return new ReflectionFunction($functionName);
277
        } catch (ReflectionException $exception) {
278
            $chunks = explode('\\', $functionName);
279
280
            if (count($chunks) > 1) {
281
                try {
282
                    return new ReflectionFunction(end($chunks));
283
                } catch (ReflectionException $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
284
                }
285
                // @TODO: Find a way to handle user-land functions
286
                // @TODO: Find a way to handle methods
287
            }
288
        }
289
290
        return null;
291
    }
292
293
    /**
294
     * Return true if the given variable is passed by reference in a native PHP function.
295
     *
296
     * @param ASTVariable|ASTPropertyPostfix|ASTVariableDeclarator $variable
297
     * @return bool
298
     */
299
    protected function isPassedByReference($variable)
300
    {
301
        $parent = $this->getNode($variable->getParent());
302
303
        if (!($parent && $parent instanceof ASTArguments)) {
304
            return false;
305
        }
306
307
        $argumentPosition = array_search($this->getNode($variable), $parent->getChildren());
308
        $function = $this->getNode($parent->getParent());
309
        $functionParent = $this->getNode($function->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...
310
        $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...
311
312
        if ($functionParent instanceof ASTMemberPrimaryPrefix) {
313
            // @TODO: Find a way to handle methods
314
            return false;
315
        }
316
317
        $reflectionFunction = $this->getReflectionFunctionByName($functionName);
318
319
        if (!$reflectionFunction) {
320
            return false;
321
        }
322
323
        $parameters = $reflectionFunction->getParameters();
324
325
        return isset($parameters[$argumentPosition]) && $parameters[$argumentPosition]->isPassedByReference();
326
    }
327
}
328