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

validateMatcherDefinitionKeys()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
c 0
b 0
f 0
nc 6
nop 3
dl 0
loc 24
rs 9.4555
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\Stmt\Class_;
20
use PhpParser\NodeVisitorAbstract;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface;
23
24
/**
25
 * Single "core matcher" classes extend from this.
26
 * It brings a set of protected methods to help single matcher classes doing common stuff.
27
 * This abstract extends the nikic/php-parser NodeVisitorAbstract which implements the main
28
 * parser interface, and it implements the TYPO3 specific CodeScannerInterface to retrieve matches.
29
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
30
 */
31
abstract class AbstractCoreMatcher extends NodeVisitorAbstract implements CodeScannerInterface
32
{
33
    public const NODE_RESOLVED_AS = 'nodeResolvedAs';
34
35
    /**
36
     * Incoming main configuration array.
37
     *
38
     * @var array
39
     */
40
    protected $matcherDefinitions = [];
41
42
    /**
43
     * @var array List of accumulated matches
44
     */
45
    protected $matches = [];
46
47
    /**
48
     * Helper property containing an array derived from $this->matcherDefinitions
49
     * created in __construct() if needed.
50
     *
51
     * @var array
52
     */
53
    protected $flatMatcherDefinitions = [];
54
55
    /**
56
     * @var int Helper variable for ignored line detection
57
     */
58
    protected $currentCodeLine = 0;
59
60
    /**
61
     * @var bool True if line with $lastIgnoredLineNumber is ignored
62
     */
63
    protected $isCurrentLineIgnored = false;
64
65
    /**
66
     * @var bool True if the entire file is ignored due to a @extensionScannerIgnoreFile class comment
67
     */
68
    protected $isFullFileIgnored = false;
69
70
    /**
71
     * Return list of matches after processing
72
     *
73
     * @return array
74
     */
75
    public function getMatches(): array
76
    {
77
        return $this->matches;
78
    }
79
80
    /**
81
     * Some matcher need specific keys in the array definition to work properly.
82
     * This method is called typically in __construct() of a matcher to
83
     * verify these are given.
84
     * This method is a measure against broken core configuration. It should be
85
     * pretty quick and is only called in __construct() once, no kitten should be harmed.
86
     *
87
     * This method works on $this->matcherDefinitions.
88
     *
89
     * @param array $requiredArrayKeys List of required keys for single matchers
90
     * @throws \RuntimeException
91
     */
92
    protected function validateMatcherDefinitions(array $requiredArrayKeys = [])
93
    {
94
        foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
95
            $this->validateMatcherDefinitionKeys($key, $matcherDefinition, $requiredArrayKeys);
96
        }
97
    }
98
99
    protected function validateMatcherDefinitionKeys(string $key, array $matcherDefinition, array $requiredArrayKeys = []): void
100
    {
101
        // Each config must point to at least one .rst file
102
        if (empty($matcherDefinition['restFiles'])) {
103
            throw new \InvalidArgumentException(
104
                'Each configuration must have at least one referenced "restFiles" entry. Offending key: ' . $key,
105
                1500496068
106
            );
107
        }
108
        foreach ($matcherDefinition['restFiles'] as $file) {
109
            if (empty($file)) {
110
                throw new \InvalidArgumentException(
111
                    'Empty restFiles definition',
112
                    1500735983
113
                );
114
            }
115
        }
116
        // Config broken if not all required array keys are specified in config
117
        $sharedArrays = array_intersect(array_keys($matcherDefinition), $requiredArrayKeys);
118
        if (count($sharedArrays) !== count($requiredArrayKeys)) {
119
            $missingKeys = array_diff($requiredArrayKeys, array_keys($matcherDefinition));
120
            throw new \InvalidArgumentException(
121
                'Required matcher definitions missing: ' . implode(', ', $missingKeys) . ' offending key: ' . $key,
122
                1500492001
123
            );
124
        }
125
    }
126
127
    /**
128
     * Initialize helper lookup array $this->flatMatcherDefinitions.
129
     * For class\name->foo matcherDefinitions, it creates a helper array
130
     * containing only the method name as array keys for "weak" matches.
131
     *
132
     * If methods with the same name from different classes are defined,
133
     * a "candidate" array is created containing details of single possible
134
     * matches for further analysis.
135
     *
136
     * @throws \RuntimeException
137
     */
138
    protected function initializeFlatMatcherDefinitions()
139
    {
140
        $methodNameArray = [];
141
        foreach ($this->matcherDefinitions as $classAndMethod => $details) {
142
            $method = GeneralUtility::trimExplode('::', $classAndMethod);
143
            if (count($method) !== 2) {
144
                $method = GeneralUtility::trimExplode('->', $classAndMethod);
145
            }
146
            if (count($method) !== 2) {
147
                throw new \RuntimeException(
148
                    'Keys in $this->matcherDefinitions must have a Class\Name->method or Class\Name::method structure',
149
                    1500557309
150
                );
151
            }
152
            $method = $method[1];
153
            if (!array_key_exists($method, $methodNameArray)) {
154
                $methodNameArray[$method]['candidates'] = [];
155
            }
156
            $methodNameArray[$method]['candidates'][] = $details;
157
        }
158
        $this->flatMatcherDefinitions = $methodNameArray;
159
    }
160
161
    /**
162
     * Test if one argument is given as "...$someArray".
163
     * If so, it kinda defeats any "argument count" approach.
164
     *
165
     * @param array $arguments List of arguments
166
     * @return bool
167
     */
168
    protected function isArgumentUnpackingUsed(array $arguments = []): bool
169
    {
170
        foreach ($arguments as $arg) {
171
            if ($arg->unpack === true) {
172
                return true;
173
            }
174
        }
175
        return false;
176
    }
177
178
    /**
179
     * Returns true if a comment before a statement is
180
     * marked as "@extensionScannerIgnoreLine"
181
     *
182
     * @param Node $node
183
     * @return bool
184
     */
185
    protected function isLineIgnored(Node $node): bool
186
    {
187
        // Early return if this line is marked as ignored
188
        $startLineOfNode = $node->getAttribute('startLine');
189
        if ($startLineOfNode === $this->currentCodeLine) {
190
            return $this->isCurrentLineIgnored;
191
        }
192
193
        $currentLineIsIgnored = false;
194
        if ($startLineOfNode !== $this->currentCodeLine) {
195
            $this->currentCodeLine = $startLineOfNode;
196
            // First node of a new line may contain the annotation
197
            $comments = $node->getAttribute('comments');
198
            if (!empty($comments)) {
199
                foreach ($comments as $comment) {
200
                    if (strpos($comment->getText(), '@extensionScannerIgnoreLine') !== false) {
201
                        $this->isCurrentLineIgnored = true;
202
                        $currentLineIsIgnored = true;
203
                        break;
204
                    }
205
                }
206
            }
207
        }
208
        return $currentLineIsIgnored;
209
    }
210
211
    /**
212
     * Return true if the node is ignored since the entire file is ignored.
213
     * Sets ignore status if a class node is given having the annotation.
214
     *
215
     * @param Node $node
216
     * @return bool
217
     */
218
    protected function isFileIgnored(Node $node): bool
219
    {
220
        if ($this->isFullFileIgnored) {
221
            return true;
222
        }
223
        $currentFileIsIgnored = false;
224
        if ($node instanceof Class_) {
225
            $comments = $node->getAttribute('comments');
226
            if (!empty($comments)) {
227
                foreach ($comments as $comment) {
228
                    if (strpos($comment->getText(), '@extensionScannerIgnoreFile') !== false) {
229
                        $this->isFullFileIgnored = true;
230
                        $currentFileIsIgnored = true;
231
                        break;
232
                    }
233
                }
234
            }
235
        }
236
        return $currentFileIsIgnored;
237
    }
238
}
239