Completed
Push — master ( e787ce...d6ad3a )
by
unknown
18:33
created

handleUnusedArguments()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 26
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 41
rs 8.5706
1
<?php
2
declare(strict_types = 1);
3
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use PhpParser\Node;
19
use PhpParser\Node\Expr\ConstFetch;
20
use PhpParser\Node\Expr\New_;
21
22
/**
23
 * Finds invocations to class constructors and the amount of passed arguments.
24
 * This matcher supports direct `new MyClass(123)` invocations as well as delegated
25
 * calls to `GeneralUtility::makeInstance(MyClass::class, 123)` using `GeneratorClassResolver`.
26
 *
27
 * These configuration property names are handled independently:
28
 * + numberOfMandatoryArguments
29
 * + maximumNumberOfArguments
30
 * + unusedArgumentNumbers
31
 *
32
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
33
 */
34
class ConstructorArgumentMatcher extends AbstractCoreMatcher
35
{
36
    protected const TOPIC_TYPE_REQUIRED = 'required';
37
    protected const TOPIC_TYPE_DROPPED = 'dropped';
38
    protected const TOPIC_TYPE_CALLED = 'called';
39
    protected const TOPIC_TYPE_UNUSED = 'unused';
40
41
    /**
42
     * Prepare $this->flatMatcherDefinitions once and validate config
43
     *
44
     * @param array $matcherDefinitions Incoming main configuration
45
     */
46
    public function __construct(array $matcherDefinitions)
47
    {
48
        $this->matcherDefinitions = $matcherDefinitions;
49
        $this->validateMatcherDefinitionsTopicRequirements([
50
            self::TOPIC_TYPE_REQUIRED => ['numberOfMandatoryArguments'],
51
            self::TOPIC_TYPE_DROPPED => ['maximumNumberOfArguments'],
52
            self::TOPIC_TYPE_CALLED => ['numberOfMandatoryArguments', 'maximumNumberOfArguments'],
53
            self::TOPIC_TYPE_UNUSED => ['unusedArgumentNumbers'],
54
        ]);
55
    }
56
57
    /**
58
     * Called by PhpParser.
59
     * Test for "->deprecated()" (weak match)
60
     *
61
     * @param Node $node
62
     */
63
    public function enterNode(Node $node)
64
    {
65
        if ($this->isFileIgnored($node) || $this->isLineIgnored($node)) {
66
            return;
67
        }
68
        $resolvedNode = $node->getAttribute(self::NODE_RESOLVED_AS, null) ?? $node;
69
        if (!$resolvedNode instanceof New_
70
            || !array_key_exists((string)$resolvedNode->class, $this->matcherDefinitions)
71
        ) {
72
            return;
73
        }
74
75
        // A method call is considered a match if it is not called with argument unpacking
76
        // and number of used arguments is lower than numberOfMandatoryArguments
77
        if ($this->isArgumentUnpackingUsed($resolvedNode->args)) {
78
            return;
79
        }
80
81
        // $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
82
        // $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
83
        $this->handleRequiredArguments($node, $resolvedNode);
84
        $this->handleDroppedArguments($node, $resolvedNode);
85
        $this->handleCalledArguments($node, $resolvedNode);
86
        $this->handleUnusedArguments($node, $resolvedNode);
87
    }
88
89
    /**
90
     * @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
91
     * @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
92
     * @return bool
93
     */
94
    protected function handleRequiredArguments(Node $node, Node $resolvedNode): bool
95
    {
96
        $className = (string)$resolvedNode->class;
0 ignored issues
show
Bug introduced by
Accessing class on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
97
        $candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_REQUIRED] ?? null;
98
        $mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
99
        $numberOfArguments = count($resolvedNode->args);
0 ignored issues
show
Bug introduced by
Accessing args on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
100
101
        if ($candidate === null || $numberOfArguments >= $mandatoryArguments) {
102
            return false;
103
        }
104
105
        $this->matches[] = [
106
            'restFiles' => $candidate['restFiles'],
107
            'line' => $node->getAttribute('startLine'),
108
            'message' => sprintf(
109
                '%s::__construct requires at least %d arguments (%d given).',
110
                $className,
111
                $mandatoryArguments,
112
                $numberOfArguments
113
            ),
114
            'indicator' => 'strong',
115
        ];
116
        return true;
117
    }
118
119
    /**
120
     * @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
121
     * @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
122
     * @return bool
123
     */
124
    protected function handleDroppedArguments(Node $node, Node $resolvedNode): bool
125
    {
126
        $className = (string)$resolvedNode->class;
0 ignored issues
show
Bug introduced by
Accessing class on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
127
        $candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_DROPPED] ?? null;
128
        $maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
129
        $numberOfArguments = count($resolvedNode->args);
0 ignored issues
show
Bug introduced by
Accessing args on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
130
131
        if ($candidate === null || $numberOfArguments <= $maximumArguments) {
132
            return false;
133
        }
134
135
        $this->matches[] = [
136
            'restFiles' => $candidate['restFiles'],
137
            'line' => $node->getAttribute('startLine'),
138
            'message' => sprintf(
139
                '%s::__construct supports only %d arguments (%d given).',
140
                $className,
141
                $maximumArguments,
142
                $numberOfArguments
143
            ),
144
            'indicator' => 'strong',
145
        ];
146
        return true;
147
    }
148
149
    /**
150
     * @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
151
     * @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
152
     * @return bool
153
     */
154
    protected function handleCalledArguments(Node $node, Node $resolvedNode): bool
155
    {
156
        $className = (string)$resolvedNode->class;
0 ignored issues
show
Bug introduced by
Accessing class on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
157
        $candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_CALLED] ?? null;
158
        $isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($resolvedNode->args);
0 ignored issues
show
Bug introduced by
Accessing args on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
159
        $mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
160
        $maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
161
        $numberOfArguments = count($resolvedNode->args);
162
163
        if ($candidate === null
164
            || !$isArgumentUnpackingUsed
165
            && ($numberOfArguments < $mandatoryArguments || $numberOfArguments > $maximumArguments)) {
166
            return false;
167
        }
168
169
        $this->matches[] = [
170
            'restFiles' => $candidate['restFiles'],
171
            'line' => $node->getAttribute('startLine'),
172
            'message' => sprintf(
173
                '%s::__construct being called (%d arguments given).',
174
                $className,
175
                $numberOfArguments
176
            ),
177
            'indicator' => 'weak',
178
        ];
179
        return true;
180
    }
181
182
    /**
183
     * @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
184
     * @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
185
     * @return bool
186
     */
187
    protected function handleUnusedArguments(Node $node, Node $resolvedNode): bool
188
    {
189
        $className = (string)$resolvedNode->class;
0 ignored issues
show
Bug introduced by
Accessing class on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
190
        $candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_UNUSED] ?? null;
191
        // values in array (if any) are actual position counts
192
        // e.g. `[2, 4]` refers to internal argument indexes `[1, 3]`
193
        $unusedArgumentPositions = $candidate['unusedArgumentNumbers'] ?? null;
194
195
        if ($candidate === null || empty($unusedArgumentPositions)) {
196
            return false;
197
        }
198
199
        $arguments = $resolvedNode->args;
0 ignored issues
show
Bug introduced by
Accessing args on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
200
        // keeping positions having argument values that are not null
201
        $unusedArgumentPositions = array_filter(
202
            $unusedArgumentPositions,
203
            function (int $position) use ($arguments) {
204
                $index = $position - 1;
205
                return isset($arguments[$index]->value)
206
                    && !$arguments[$index]->value instanceof ConstFetch
207
                    && (
208
                        !isset($arguments[$index]->value->name->name->parts[0])
209
                        || $arguments[$index]->value->name->name->parts[0] !== null
210
                    );
211
            }
212
        );
213
        if (empty($unusedArgumentPositions)) {
214
            return false;
215
        }
216
217
        $this->matches[] = [
218
            'restFiles' => $candidate['restFiles'],
219
            'line' => $node->getAttribute('startLine'),
220
            'message' => sprintf(
221
                '%s::__construct was called with argument positions %s not being null.',
222
                $className,
223
                implode(', ', $unusedArgumentPositions)
224
            ),
225
            'indicator' => 'strong',
226
        ];
227
        return true;
228
    }
229
230
    protected function validateMatcherDefinitionsTopicRequirements(array $topicRequirements): void
231
    {
232
        foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
233
            foreach ($topicRequirements as $topic => $requiredArrayKeys) {
234
                if (empty($matcherDefinition[$topic])) {
235
                    continue;
236
                }
237
                $this->validateMatcherDefinitionKeys($key, $matcherDefinition[$topic], $requiredArrayKeys);
238
            }
239
        }
240
    }
241
}
242