NewKeywordsSniff::getErrorInfo()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * \PHPCompatibility\Sniffs\PHP\NewKeywordsSniff.
4
 *
5
 * @category  PHP
6
 * @package   PHPCompatibility
7
 * @author    Wim Godden <[email protected]>
8
 * @copyright 2013 Cu.be Solutions bvba
9
 */
10
11
namespace PHPCompatibility\Sniffs\PHP;
12
13
use PHPCompatibility\AbstractNewFeatureSniff;
14
15
/**
16
 * \PHPCompatibility\Sniffs\PHP\NewKeywordsSniff.
17
 *
18
 * @category  PHP
19
 * @package   PHPCompatibility
20
 * @author    Wim Godden <[email protected]>
21
 * @copyright 2013 Cu.be Solutions bvba
22
 */
23
class NewKeywordsSniff extends AbstractNewFeatureSniff
24
{
25
26
    /**
27
     * A list of new keywords, not present in older versions.
28
     *
29
     * The array lists : version number with false (not present) or true (present).
30
     * If's sufficient to list the last version which did not contain the keyword.
31
     *
32
     * Description will be used as part of the error message.
33
     * Condition is the name of a callback method within this class or the parent class
34
     * which checks whether the token complies with a certain condition.
35
     * The callback function will be passed the $phpcsFile and the $stackPtr.
36
     * The callback function should return `true` if the condition is met and the
37
     * error should *not* be thrown.
38
     *
39
     * @var array(string => array(string => int|string|null))
40
     */
41
    protected $newKeywords = array(
42
        'T_HALT_COMPILER' => array(
43
            '5.0'         => false,
44
            '5.1'         => true,
45
            'description' => '"__halt_compiler" keyword',
46
        ),
47
        'T_CONST' => array(
48
            '5.2'         => false,
49
            '5.3'         => true,
50
            'description' => '"const" keyword',
51
            'condition'   => 'isClassConstant', // Keyword is only new when not in class context.
52
        ),
53
        'T_CALLABLE' => array(
54
            '5.3'         => false,
55
            '5.4'         => true,
56
            'description' => '"callable" keyword',
57
            'content'     => 'callable',
58
        ),
59
        'T_DIR' => array(
60
            '5.2'         => false,
61
            '5.3'         => true,
62
            'description' => '__DIR__ magic constant',
63
            'content'     => '__DIR__',
64
        ),
65
        'T_GOTO' => array(
66
            '5.2'         => false,
67
            '5.3'         => true,
68
            'description' => '"goto" keyword',
69
            'content'     => 'goto',
70
        ),
71
        'T_INSTEADOF' => array(
72
            '5.3'         => false,
73
            '5.4'         => true,
74
            'description' => '"insteadof" keyword (for traits)',
75
            'content'     => 'insteadof',
76
        ),
77
        'T_NAMESPACE' => array(
78
            '5.2'         => false,
79
            '5.3'         => true,
80
            'description' => '"namespace" keyword',
81
            'content'     => 'namespace',
82
        ),
83
        'T_NS_C' => array(
84
            '5.2'         => false,
85
            '5.3'         => true,
86
            'description' => '__NAMESPACE__ magic constant',
87
            'content'     => '__NAMESPACE__',
88
        ),
89
        'T_USE' => array(
90
            '5.2'         => false,
91
            '5.3'         => true,
92
            'description' => '"use" keyword (for traits/namespaces/anonymous functions)',
93
        ),
94
        'T_START_NOWDOC' => array(
95
            '5.2'         => false,
96
            '5.3'         => true,
97
            'description' => 'nowdoc functionality',
98
        ),
99
        'T_END_NOWDOC' => array(
100
            '5.2'         => false,
101
            '5.3'         => true,
102
            'description' => 'nowdoc functionality',
103
        ),
104
        'T_START_HEREDOC' => array(
105
            '5.2'         => false,
106
            '5.3'         => true,
107
            'description' => '(Double) quoted Heredoc identifier',
108
            'condition'   => 'isNotQuoted', // Heredoc is only new with quoted identifier.
109
        ),
110
        'T_TRAIT' => array(
111
            '5.3'         => false,
112
            '5.4'         => true,
113
            'description' => '"trait" keyword',
114
            'content'     => 'trait',
115
        ),
116
        'T_TRAIT_C' => array(
117
            '5.3'         => false,
118
            '5.4'         => true,
119
            'description' => '__TRAIT__ magic constant',
120
            'content'     => '__TRAIT__',
121
        ),
122
        // The specifics for distinguishing between 'yield' and 'yield from' are dealt
123
        // with in the translation logic.
124
        // This token has to be placed above the `T_YIELD` token in this array to allow for this.
125
        'T_YIELD_FROM' => array(
126
            '5.6'         => false,
127
            '7.0'         => true,
128
            'description' => '"yield from" keyword (for generators)',
129
            'content'     => 'yield',
130
        ),
131
        'T_YIELD' => array(
132
            '5.4'         => false,
133
            '5.5'         => true,
134
            'description' => '"yield" keyword (for generators)',
135
            'content'     => 'yield',
136
        ),
137
        'T_FINALLY' => array(
138
            '5.4'         => false,
139
            '5.5'         => true,
140
            'description' => '"finally" keyword (in exception handling)',
141
            'content'     => 'finally',
142
        ),
143
    );
144
145
    /**
146
     * Translation table for T_STRING tokens.
147
     *
148
     * Will be set up from the register() method.
149
     *
150
     * @var array(string => string)
151
     */
152
    protected $translateContentToToken = array();
153
154
155
    /**
156
     * Returns an array of tokens this test wants to listen for.
157
     *
158
     * @return array
159
     */
160
    public function register()
161
    {
162
        $tokens    = array();
163
        $translate = array();
164
        foreach ($this->newKeywords as $token => $versions) {
165
            if (defined($token)) {
166
                $tokens[] = constant($token);
167
            }
168
            if (isset($versions['content'])) {
169
                $translate[$versions['content']] = $token;
170
            }
171
        }
172
173
        /*
174
         * Deal with tokens not recognized by the PHP version the sniffer is run
175
         * under and (not correctly) compensated for by PHPCS.
176
         */
177
        if (empty($translate) === false) {
178
            $this->translateContentToToken = $translate;
179
            $tokens[] = T_STRING;
180
        }
181
182
        return $tokens;
183
184
    }//end register()
185
186
187
    /**
188
     * Processes this test, when one of its tokens is encountered.
189
     *
190
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
191
     * @param int                   $stackPtr  The position of the current token in
192
     *                                         the stack passed in $tokens.
193
     *
194
     * @return void
195
     */
196
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
197
    {
198
        $tokens    = $phpcsFile->getTokens();
199
        $tokenType = $tokens[$stackPtr]['type'];
200
201
        // Allow for dealing with multi-token keywords, like "yield from".
202
        $end = $stackPtr;
203
204
        // Translate T_STRING token if necessary.
205
        if ($tokens[$stackPtr]['type'] === 'T_STRING') {
206
            $content = $tokens[$stackPtr]['content'];
207
            if (strpos($content, '__') !== 0) {
208
                $content = strtolower($tokens[$stackPtr]['content']);
209
            }
210
211
            if (isset($this->translateContentToToken[$content]) === false) {
212
                // Not one of the tokens we're looking for.
213
                return;
214
            }
215
216
            $tokenType = $this->translateContentToToken[$content];
217
        }
218
219
        /*
220
         * Special case: distinguish between `yield` and `yield from`.
221
         *
222
         * PHPCS currently (at least up to v 3.0.1) does not backfill for the
223
         * `yield` nor the `yield from` keywords.
224
         * See: https://github.com/squizlabs/PHP_CodeSniffer/issues/1524
225
         *
226
         * In PHP < 5.5, both `yield` as well as `from` are tokenized as T_STRING.
227
         * In PHP 5.5 - 5.6, `yield` is tokenized as T_YIELD and `from` as T_STRING,
228
         * but the `T_YIELD_FROM` token *is* defined in PHP.
229
         * In PHP 7.0+ both are tokenized as their respective token, however,
230
         * a multi-line "yield from" is tokenized as two tokens.
231
         */
232
        if ($tokenType === 'T_YIELD') {
233
            $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($end + 1), null, true);
234
            if ($tokens[$nextToken]['code'] === T_STRING
235
                && $tokens[$nextToken]['content'] === 'from'
236
            ) {
237
                $tokenType = 'T_YIELD_FROM';
238
                $end       = $nextToken;
239
            }
240
            unset($nextToken);
241
        }
242
243
        if ($tokenType === 'T_YIELD_FROM' && $tokens[($stackPtr - 1)]['type'] === 'T_YIELD_FROM') {
244
            // Multi-line "yield from", no need to report it twice.
245
            return;
246
        }
247
248
        if (isset($this->newKeywords[$tokenType]) === false) {
249
            return;
250
        }
251
252
        $nextToken = $phpcsFile->findNext(\PHP_CodeSniffer_Tokens::$emptyTokens, ($end + 1), null, true);
253
        $prevToken = $phpcsFile->findPrevious(\PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
254
255 View Code Duplication
        if ($prevToken !== false
256
            && ($tokens[$prevToken]['code'] === T_DOUBLE_COLON
257
            || $tokens[$prevToken]['code'] === T_OBJECT_OPERATOR)
258
        ) {
259
            // Class property of the same name as one of the keywords. Ignore.
260
            return;
261
        }
262
263
        // Skip attempts to use keywords as functions or class names - the former
264
        // will be reported by ForbiddenNamesAsInvokedFunctionsSniff, whilst the
265
        // latter will be (partially) reported by the ForbiddenNames sniff.
266
        // Either type will result in false-positives when targetting lower versions
267
        // of PHP where the name was not reserved, unless we explicitly check for
268
        // them.
269
        if (($nextToken === false
270
                || $tokens[$nextToken]['type'] !== 'T_OPEN_PARENTHESIS')
271
            && ($prevToken === false
272
                || $tokens[$prevToken]['type'] !== 'T_CLASS'
273
                || $tokens[$prevToken]['type'] !== 'T_INTERFACE')
274
        ) {
275
            // Skip based on token scope condition.
276
            if (isset($this->newKeywords[$tokenType]['condition'])
277
                && call_user_func(array($this, $this->newKeywords[$tokenType]['condition']), $phpcsFile, $stackPtr) === true
278
            ) {
279
                return;
280
            }
281
282
            $itemInfo = array(
283
                'name'   => $tokenType,
284
            );
285
            $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
286
        }
287
288
    }//end process()
289
290
291
    /**
292
     * Get the relevant sub-array for a specific item from a multi-dimensional array.
293
     *
294
     * @param array $itemInfo Base information about the item.
295
     *
296
     * @return array Version and other information about the item.
297
     */
298
    public function getItemArray(array $itemInfo)
299
    {
300
        return $this->newKeywords[$itemInfo['name']];
301
    }
302
303
304
    /**
305
     * Get an array of the non-PHP-version array keys used in a sub-array.
306
     *
307
     * @return array
308
     */
309
    protected function getNonVersionArrayKeys()
310
    {
311
        return array(
312
            'description',
313
            'condition',
314
            'content',
315
        );
316
    }
317
318
319
    /**
320
     * Retrieve the relevant detail (version) information for use in an error message.
321
     *
322
     * @param array $itemArray Version and other information about the item.
323
     * @param array $itemInfo  Base information about the item.
324
     *
325
     * @return array
326
     */
327
    public function getErrorInfo(array $itemArray, array $itemInfo)
328
    {
329
        $errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
330
        $errorInfo['description'] = $itemArray['description'];
331
332
        return $errorInfo;
333
334
    }
335
336
337
    /**
338
     * Allow for concrete child classes to filter the error data before it's passed to PHPCS.
339
     *
340
     * @param array $data      The error data array which was created.
341
     * @param array $itemInfo  Base information about the item this error message applied to.
342
     * @param array $errorInfo Detail information about an item this error message applied to.
343
     *
344
     * @return array
345
     */
346
    protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
347
    {
348
        $data[0] = $errorInfo['description'];
349
        return $data;
350
    }
351
352
353
    /**
354
     * Callback for the quoted heredoc identifier condition.
355
     *
356
     * A double quoted identifier will have the opening quote on position 3
357
     * in the string: `<<<"ID"`.
358
     *
359
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
360
     * @param int                  $stackPtr  The position of the current token in
361
     *                                        the stack passed in $tokens.
362
     *
363
     * @return bool
364
     */
365
    public function isNotQuoted(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
366
    {
367
        $tokens = $phpcsFile->getTokens();
368
        return ($tokens[$stackPtr]['content'][3] !== '"');
369
    }
370
371
372
}//end class
373