getNonVersionArrayKeys()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * \PHPCompatibility\Sniffs\PHP\NewExecutionDirectivesSniff.
4
 *
5
 * @category PHP
6
 * @package  PHPCompatibility
7
 * @author   Juliette Reinders Folmer <[email protected]>
8
 */
9
10
namespace PHPCompatibility\Sniffs\PHP;
11
12
use PHPCompatibility\AbstractNewFeatureSniff;
13
use PHPCompatibility\PHPCSHelper;
14
15
/**
16
 * \PHPCompatibility\Sniffs\PHP\NewExecutionDirectivesSniff.
17
 *
18
 * @category PHP
19
 * @package  PHPCompatibility
20
 * @author   Juliette Reinders Folmer <[email protected]>
21
 */
22
class NewExecutionDirectivesSniff extends AbstractNewFeatureSniff
23
{
24
25
    /**
26
     * A list of new execution directives
27
     *
28
     * The array lists : version number with false (not present) or true (present).
29
     * If the execution order is conditional, add the condition as a string to the version nr.
30
     * If's sufficient to list the first version where the execution directive appears.
31
     *
32
     * @var array(string => array(string => int|string|null))
33
     */
34
    protected $newDirectives = array(
35
        'ticks' => array(
36
            '3.1' => false,
37
            '4.0' => true,
38
            'valid_value_callback' => 'isNumeric',
39
        ),
40
        'encoding' => array(
41
            '5.2' => false,
42
            '5.3' => '--enable-zend-multibyte', // Directive ignored unless.
43
            '5.4' => true,
44
            'valid_value_callback' => 'validEncoding',
45
        ),
46
        'strict_types' => array(
47
            '5.6' => false,
48
            '7.0' => true,
49
            'valid_values' => array(1),
50
        ),
51
    );
52
53
54
    /**
55
     * Tokens to ignore when trying to find the value for the directive.
56
     *
57
     * @var array
58
     */
59
    protected $ignoreTokens = array();
60
61
62
    /**
63
     * Returns an array of tokens this test wants to listen for.
64
     *
65
     * @return array
66
     */
67
    public function register()
68
    {
69
        $this->ignoreTokens          = \PHP_CodeSniffer_Tokens::$emptyTokens;
70
        $this->ignoreTokens[T_EQUAL] = T_EQUAL;
71
72
        return array(T_DECLARE);
73
    }//end register()
74
75
76
    /**
77
     * Processes this test, when one of its tokens is encountered.
78
     *
79
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
80
     * @param int                   $stackPtr  The position of the current token in
81
     *                                         the stack passed in $tokens.
82
     *
83
     * @return void
84
     */
85
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
86
    {
87
        $tokens = $phpcsFile->getTokens();
88
89
        if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === true) {
90
            $openParenthesis  = $tokens[$stackPtr]['parenthesis_opener'];
91
            $closeParenthesis = $tokens[$stackPtr]['parenthesis_closer'];
92
        } else {
93
            if (version_compare(PHPCSHelper::getVersion(), '2.3.4', '>=')) {
94
                return;
95
            }
96
97
            // Deal with PHPCS 1.x which does not set the parenthesis properly for declare statements.
98
            $openParenthesis = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1), null, false, null, true);
99 View Code Duplication
            if ($openParenthesis === false || isset($tokens[$openParenthesis]['parenthesis_closer']) === false) {
100
                return;
101
            }
102
            $closeParenthesis = $tokens[$openParenthesis]['parenthesis_closer'];
103
        }
104
105
        $directivePtr = $phpcsFile->findNext(T_STRING, ($openParenthesis + 1), $closeParenthesis, false);
106
        if ($directivePtr === false) {
107
            return;
108
        }
109
110
        $directiveContent = $tokens[$directivePtr]['content'];
111
112
        if (isset($this->newDirectives[$directiveContent]) === false) {
113
            $error = 'Declare can only be used with the directives %s. Found: %s';
114
            $data  = array(
115
                implode(', ', array_keys($this->newDirectives)),
116
                $directiveContent,
117
            );
118
119
            $phpcsFile->addError($error, $stackPtr, 'InvalidDirectiveFound', $data);
120
121
        } else {
122
            // Check for valid directive for version.
123
            $itemInfo = array(
124
                'name'   => $directiveContent,
125
            );
126
            $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
127
128
            // Check for valid directive value.
129
            $valuePtr = $phpcsFile->findNext($this->ignoreTokens, $directivePtr + 1, $closeParenthesis, true);
130
            if ($valuePtr === false) {
131
                return;
132
            }
133
134
            $this->addWarningOnInvalidValue($phpcsFile, $valuePtr, $directiveContent);
135
        }
136
137
    }//end process()
138
139
140
    /**
141
     * Determine whether an error/warning should be thrown for an item based on collected information.
142
     *
143
     * @param array $errorInfo Detail information about an item.
144
     *
145
     * @return bool
146
     */
147
    protected function shouldThrowError(array $errorInfo)
148
    {
149
        return ($errorInfo['not_in_version'] !== '' || $errorInfo['conditional_version'] !== '');
150
    }
151
152
153
    /**
154
     * Get the relevant sub-array for a specific item from a multi-dimensional array.
155
     *
156
     * @param array $itemInfo Base information about the item.
157
     *
158
     * @return array Version and other information about the item.
159
     */
160
    public function getItemArray(array $itemInfo)
161
    {
162
        return $this->newDirectives[$itemInfo['name']];
163
    }
164
165
166
    /**
167
     * Get an array of the non-PHP-version array keys used in a sub-array.
168
     *
169
     * @return array
170
     */
171
    protected function getNonVersionArrayKeys()
172
    {
173
        return array(
174
            'valid_value_callback',
175
            'valid_values',
176
        );
177
    }
178
179
180
    /**
181
     * Retrieve the relevant detail (version) information for use in an error message.
182
     *
183
     * @param array $itemArray Version and other information about the item.
184
     * @param array $itemInfo  Base information about the item.
185
     *
186
     * @return array
187
     */
188
    public function getErrorInfo(array $itemArray, array $itemInfo)
189
    {
190
        $errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
191
        $errorInfo['conditional_version'] = '';
192
        $errorInfo['condition']           = '';
193
194
        $versionArray = $this->getVersionArray($itemArray);
195
196
        if (empty($versionArray) === false) {
197
            foreach ($versionArray as $version => $present) {
198
                if (is_string($present) === true && $this->supportsBelow($version) === true) {
199
                    // We cannot test for compilation option (ok, except by scraping the output of phpinfo...).
200
                    $errorInfo['conditional_version'] = $version;
201
                    $errorInfo['condition']           = $present;
202
                }
203
            }
204
        }
205
206
        return $errorInfo;
207
    }
208
209
210
    /**
211
     * Get the error message template for this sniff.
212
     *
213
     * @return string
214
     */
215
    protected function getErrorMsgTemplate()
216
    {
217
        return 'Directive '.parent::getErrorMsgTemplate();
218
    }
219
220
221
    /**
222
     * Generates the error or warning for this item.
223
     *
224
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
225
     * @param int                   $stackPtr  The position of the relevant token in
226
     *                                         the stack.
227
     * @param array                 $itemInfo  Base information about the item.
228
     * @param array                 $errorInfo Array with detail (version) information
229
     *                                         relevant to the item.
230
     *
231
     * @return void
232
     */
233
    public function addError(\PHP_CodeSniffer_File $phpcsFile, $stackPtr, array $itemInfo, array $errorInfo)
234
    {
235
        if ($errorInfo['not_in_version'] !== '') {
236
            parent::addError($phpcsFile, $stackPtr, $itemInfo, $errorInfo);
237
        } elseif ($errorInfo['conditional_version'] !== '') {
238
            $error     = 'Directive %s is present in PHP version %s but will be disregarded unless PHP is compiled with %s';
239
            $errorCode = $this->stringToErrorCode($itemInfo['name']).'WithConditionFound';
240
            $data      = array(
241
                $itemInfo['name'],
242
                $errorInfo['conditional_version'],
243
                $errorInfo['condition'],
244
            );
245
246
            $phpcsFile->addWarning($error, $stackPtr, $errorCode, $data);
247
        }
248
249
    }//end addError()
250
251
252
    /**
253
     * Generates a error or warning for this sniff.
254
     *
255
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
256
     * @param int                   $stackPtr  The position of the execution directive value
257
     *                                         in the token array.
258
     * @param string                $directive The directive.
259
     *
260
     * @return void
261
     */
262
    protected function addWarningOnInvalidValue(\PHP_CodeSniffer_File $phpcsFile, $stackPtr, $directive)
263
    {
264
        $tokens = $phpcsFile->getTokens();
265
266
        $value = $tokens[$stackPtr]['content'];
267
        if (in_array($tokens[$stackPtr]['code'], \PHP_CodeSniffer_Tokens::$stringTokens, true) === true) {
268
            $value = $this->stripQuotes($value);
269
        }
270
271
        $isError = false;
272
        if (isset($this->newDirectives[$directive]['valid_values'])) {
273
            if (in_array($value, $this->newDirectives[$directive]['valid_values']) === false) {
274
                $isError = true;
275
            }
276
        } elseif (isset($this->newDirectives[$directive]['valid_value_callback'])) {
277
            $valid = call_user_func(array($this, $this->newDirectives[$directive]['valid_value_callback']), $value);
278
            if ($valid === false) {
279
                $isError = true;
280
            }
281
        }
282
283
        if ($isError === true) {
284
            $error     = 'The execution directive %s does not seem to have a valid value. Please review. Found: %s';
285
            $errorCode = $this->stringToErrorCode($directive).'InvalidValueFound';
286
            $data      = array(
287
                $directive,
288
                $value,
289
            );
290
291
            $phpcsFile->addWarning($error, $stackPtr, $errorCode, $data);
292
        }
293
    }//end addWarningOnInvalidValue()
294
295
296
    /**
297
     * Check whether a value is numeric.
298
     *
299
     * Callback function to test whether the value for an execution directive is valid.
300
     *
301
     * @param mixed $value The value to test.
302
     *
303
     * @return bool
304
     */
305
    protected function isNumeric($value)
306
    {
307
        return is_numeric($value);
308
    }
309
310
311
    /**
312
     * Check whether a value is a valid encoding.
313
     *
314
     * Callback function to test whether the value for an execution directive is valid.
315
     *
316
     * @param mixed $value The value to test.
317
     *
318
     * @return bool
319
     */
320
    protected function validEncoding($value)
321
    {
322
        static $encodings;
323
        if (isset($encodings) === false && function_exists('mb_list_encodings')) {
324
            $encodings = mb_list_encodings();
325
        }
326
327
        if (empty($encodings) || is_array($encodings) === false) {
328
            // If we can't test the encoding, let it pass through.
329
            return true;
330
        }
331
332
        return in_array($value, $encodings, true);
333
    }
334
335
336
}//end class
337