Passed
Push — 3.x ( b9e5df...58f741 )
by Anders
21:52 queued 18:38
created

UnusedFormalParameter::isInheritedSignature()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 13
nc 6
nop 1
dl 0
loc 27
ccs 14
cts 14
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of PHP Mess Detector.
5
 *
6
 * Copyright (c) Manuel Pichler <[email protected]>.
7
 * All rights reserved.
8
 *
9
 * Licensed under BSD License
10
 * For full copyright and license information, please see the LICENSE file.
11
 * Redistributions of files must retain the above copyright notice.
12
 *
13
 * @author Manuel Pichler <[email protected]>
14
 * @copyright Manuel Pichler. All rights reserved.
15
 * @license https://opensource.org/licenses/bsd-license.php BSD License
16
 * @link http://phpmd.org/
17
 */
18
19
namespace PHPMD\Rule;
20
21
use OutOfBoundsException;
22
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use PDepend\Source\AST\AbstractASTCallable;
24
use PDepend\Source\AST\ASTClassOrInterfaceRecursiveInheritanceException;
25
use PDepend\Source\AST\ASTCompoundVariable;
26
use PDepend\Source\AST\ASTExpression;
27
use PDepend\Source\AST\ASTFormalParameter;
28
use PDepend\Source\AST\ASTFormalParameters;
29
use PDepend\Source\AST\ASTFunctionPostfix;
30
use PDepend\Source\AST\ASTLiteral;
31
use PDepend\Source\AST\ASTNode as PDependNode;
32
use PDepend\Source\AST\ASTVariableDeclarator;
33
use PHPMD\AbstractNode;
34
use PHPMD\Node\AbstractCallableNode;
35
use PHPMD\Node\MethodNode;
36
use ReflectionException;
37
use ReflectionMethod;
38
use RuntimeException;
39
40
/**
41
 * This rule collects all formal parameters of a given function or method that
42
 * are not used in a statement of the artifact's body.
43
 *
44
 * @SuppressWarnings("PMD.CouplingBetweenObjects")
45
 */
46
final class UnusedFormalParameter extends AbstractLocalVariable implements FunctionAware, MethodAware
47
{
48
    /**
49
     * Collected ast nodes.
50
     *
51
     * @var array<string, AbstractNode<ASTVariableDeclarator>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, AbstractNo...ASTVariableDeclarator>> at position 4 could not be parsed: Expected '>' at position 4, but found 'AbstractNode'.
Loading history...
52
     */
53
    private array $nodes = [];
54
55
    /**
56
     * This method checks that all parameters of a given function or method are
57
     * used at least one time within the artifacts body.
58
     *
59
     * @throws ReflectionException
60
     * @throws RuntimeException
61
     */
62 42
    public function apply(AbstractNode $node): void
63
    {
64 42
        if (!$node instanceof AbstractCallableNode) {
65
            return;
66
        }
67
68 42
        if ($this->isAbstractMethod($node)) {
69 2
            return;
70
        }
71
72
        // Magic methods should be ignored as invalid declarations are picked up by PHP.
73 40
        if ($this->isMagicMethod($node)) {
74 1
            return;
75
        }
76
77 39
        if ($this->isInheritedSignature($node)) {
78 3
            return;
79
        }
80
81 36
        if ($this->isNotDeclaration($node)) {
82 3
            return;
83
        }
84
85 33
        $this->nodes = [];
86
87 33
        $this->collectParameters($node);
88 33
        $this->removeUsedParameters($node);
89
90 33
        foreach ($this->nodes as $node) {
91 10
            $this->addViolation($node, [$node->getImage()]);
92
        }
93
    }
94
95
    /**
96
     * PHP is case insensitive so we should compare function names case
97
     * insensitive.
98
     *
99
     * @param AbstractNode<PDependNode> $node
100
     */
101 8
    private function isFunctionNameEqual(AbstractNode $node, string $name): bool
102
    {
103 8
        return (0 === strcasecmp(trim($node->getImage(), '\\'), $name));
104
    }
105
106
    /**
107
     * Returns <b>true</b> when the given node is an abstract method.
108
     *
109
     * @param AbstractCallableNode<AbstractASTCallable> $node
110
     */
111 42
    private function isAbstractMethod(AbstractCallableNode $node): bool
112
    {
113 42
        if ($node instanceof MethodNode) {
114 38
            return $node->isAbstract();
115
        }
116
117 4
        return false;
118
    }
119
120
    /**
121
     * Returns <b>true</b> when the given node is method with signature declared as inherited using
122
     * \Override attribute or the {@inheritDoc} annotation.
123
     *
124
     * @param AbstractCallableNode<AbstractASTCallable> $node
125
     * @throws ReflectionException
126
     * @throws RuntimeException
127
     */
128 39
    private function isInheritedSignature(AbstractCallableNode $node): bool
129
    {
130 39
        if (!$node instanceof MethodNode) {
131 4
            return false;
132
        }
133
134 35
        $comment = $node->getComment();
0 ignored issues
show
Bug introduced by
The method getComment() does not exist on PHPMD\Node\MethodNode. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

134
        /** @scrutinizer ignore-call */ 
135
        $comment = $node->getComment();
Loading history...
135
136 35
        if ($comment && preg_match('/@inheritdoc/i', $comment)) {
137 2
            return true;
138
        }
139
140 33
        if (\PHP_VERSION_ID < 80300 || !class_exists($node->getParentType()->getFullQualifiedName())) {
141 31
            return false;
142
        }
143
144
        // Remove the "()" at the end of method's name.
145 2
        $methodName = substr($node->getFullQualifiedName(), 0, -2);
146 2
        $reflectionMethod = new ReflectionMethod($methodName);
147
148 2
        foreach ($reflectionMethod->getAttributes() as $reflectionAttribute) {
149 1
            if ($reflectionAttribute->getName() === Override::class) {
150 1
                return true;
151
            }
152
        }
153
154 1
        return false;
155
    }
156
157
    /**
158
     * Returns <b>true</b> when the given node is a magic method signature
159
     *
160
     * @param AbstractCallableNode<AbstractASTCallable> $node
161
     */
162 40
    private function isMagicMethod(AbstractCallableNode $node): bool
163
    {
164 40
        if (!($node instanceof MethodNode)) {
165 4
            return false;
166
        }
167
168 36
        static $magicMethodRegExp = null;
169
170 36
        if ($magicMethodRegExp === null) {
171 3
            $magicMethodRegExp = '/__(?:' . implode('|', [
172 3
                'call',
173 3
                'callStatic',
174 3
                'get',
175 3
                'set',
176 3
                'isset',
177 3
                'unset',
178 3
                'set_state',
179 3
            ]) . ')/i';
180
        }
181
182 36
        return preg_match($magicMethodRegExp, $node->getName()) === 1;
183
    }
184
185
    /**
186
     * Tests if the given <b>$node</b> is a method and if this method is also
187
     * the initial declaration.
188
     *
189
     * @param AbstractCallableNode<AbstractASTCallable> $node
190
     * @throws ASTClassOrInterfaceRecursiveInheritanceException
191
     * @since 1.2.1
192
     */
193 36
    private function isNotDeclaration(AbstractCallableNode $node): bool
194
    {
195 36
        if ($node instanceof MethodNode) {
196 32
            return !$node->isDeclaration();
197
        }
198
199 4
        return false;
200
    }
201
202
    /**
203
     * This method extracts all parameters for the given function or method node
204
     * and it stores the parameter images in the <b>$_images</b> property.
205
     *
206
     * @param AbstractCallableNode<AbstractASTCallable> $node
207
     */
208 33
    private function collectParameters(AbstractCallableNode $node): void
209
    {
210
        // First collect the formal parameters containers
211 33
        foreach ($node->findChildrenOfType(ASTFormalParameters::class) as $parameters) {
212 33
            $parent = $parameters->getParentOfType(AbstractASTCallable::class);
213 33
            if ($parent?->getNode() !== $node->getNode()) {
214 1
                continue;
215
            }
216
217
            // Now get all declarators in the formal parameters container
218 33
            $declarators = $parameters->findChildrenOfType(ASTVariableDeclarator::class);
219
220 33
            foreach ($declarators as $declarator) {
221 32
                $this->nodes[$declarator->getImage()] = $declarator;
222
            }
223
        }
224
    }
225
226
    /**
227
     * This method collects all local variables in the body of the currently
228
     * analyzed method or function and removes those parameters that are
229
     * referenced by one of the collected variables.
230
     *
231
     * @param AbstractCallableNode<AbstractASTCallable> $node
232
     * @throws OutOfBoundsException
233
     */
234 33
    private function removeUsedParameters(AbstractCallableNode $node): void
235
    {
236 33
        $this->removeRegularVariables($node);
237 33
        $this->removeCompoundVariables($node);
238 33
        $this->removeVariablesUsedByFuncGetArgs($node);
239 33
        $this->removePropertyPromotionVariables($node);
240
    }
241
242
    /**
243
     * Removes all the regular variables from a given node
244
     *
245
     * @param AbstractCallableNode<AbstractASTCallable> $node The node to remove the regular variables from.
246
     * @throws OutOfBoundsException
247
     */
248 33
    private function removeRegularVariables(AbstractCallableNode $node): void
249
    {
250 33
        $variables = $node->findChildrenOfTypeVariable();
251
252 33
        foreach ($variables as $variable) {
253 19
            if ($this->isRegularVariable($variable)) {
254 18
                unset($this->nodes[$variable->getImage()]);
255
            }
256
        }
257
    }
258
259
    /**
260
     * Removes all the compound variables from a given node
261
     *
262
     * Such as
263
     *
264
     * <code>
265
     * // ------
266
     * Foo::${BAR}();
267
     * // ------
268
     *
269
     * // ------
270
     * Foo::$${BAR}();
271
     * // ------
272
     * </code>
273
     *
274
     * @param AbstractCallableNode<AbstractASTCallable> $node The node to remove the compound variables from.
275
     */
276 33
    private function removeCompoundVariables(AbstractCallableNode $node): void
277
    {
278 33
        $compoundVariables = $node->findChildrenOfType(ASTCompoundVariable::class);
279
280 33
        foreach ($compoundVariables as $compoundVariable) {
281 5
            $variablePrefix = $compoundVariable->getImage();
282
283 5
            foreach ($compoundVariable->findChildrenOfType(ASTExpression::class) as $child) {
284 5
                $variableImage = $variablePrefix . $child->getImage();
285
286 5
                if (isset($this->nodes[$variableImage])) {
287 2
                    unset($this->nodes[$variableImage]);
288
                }
289
            }
290
        }
291
    }
292
293
    /**
294
     * Removes all the variables from a given node, if func_get_args() is called within
295
     *
296
     * If the given method calls func_get_args() then all parameters are automatically referenced.
297
     *
298
     * @param AbstractCallableNode<AbstractASTCallable> $node The node to remove the referenced variables from.
299
     */
300 33
    private function removeVariablesUsedByFuncGetArgs(AbstractCallableNode $node): void
301
    {
302 33
        $functionCalls = $node->findChildrenOfType(ASTFunctionPostfix::class);
303
304 33
        foreach ($functionCalls as $functionCall) {
305 8
            if ($this->isFunctionNameEqual($functionCall, 'func_get_args')) {
306 2
                $this->nodes = [];
307
            }
308
309 8
            if ($this->isFunctionNameEndingWith($functionCall, 'compact')) {
310 6
                foreach ($functionCall->findChildrenOfType(ASTLiteral::class) as $literal) {
311 6
                    unset($this->nodes['$' . trim($literal->getImage(), '"\'')]);
312
                }
313
            }
314
        }
315
    }
316
317
    /**
318
     * Removes all the property promotion parameters from a given node
319
     *
320
     * @param AbstractCallableNode<AbstractASTCallable> $node The node to remove the property promotion parameters from.
321
     */
322 33
    private function removePropertyPromotionVariables(AbstractCallableNode $node): void
323
    {
324 33
        if (! $node instanceof MethodNode) {
325 4
            return;
326
        }
327 29
        if ($node->getImage() !== '__construct') {
328 28
            return;
329
        }
330
331 1
        foreach ($node->findChildrenOfType(ASTFormalParameter::class) as $parameter) {
332 1
            if ($parameter->isPromoted()) {
333 1
                $variable = $parameter->getFirstChildOfType(ASTVariableDeclarator::class);
334 1
                if ($variable !== null) {
335 1
                    unset($this->nodes[$variable->getImage()]);
336
                }
337
            }
338
        }
339
    }
340
}
341