Completed
Push — master ( f048ee...64228b )
by
unknown
24:15 queued 06:49
created

ConstructorArgumentMatcher::enterNode()   B

Complexity

Conditions 9
Paths 4

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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