Completed
Push — feature/remove-php52-work-arou... ( 4600c9 )
by Juliette
05:06 queued 03:21
created

NewKeywordsSniff::isNotQuoted()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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 (isset($this->translateContentToToken[$content]) === false) {
208
                // Not one of the tokens we're looking for.
209
                return;
210
            }
211
212
            $tokenType = $this->translateContentToToken[$content];
213
        }
214
215
        /*
216
         * Special case: distinguish between `yield` and `yield from`.
217
         *
218
         * PHPCS currently (at least up to v 3.0.1) does not backfill for the
219
         * `yield` nor the `yield from` keywords.
220
         * See: https://github.com/squizlabs/PHP_CodeSniffer/issues/1524
221
         *
222
         * In PHP < 5.5, both `yield` as well as `from` are tokenized as T_STRING.
223
         * In PHP 5.5 - 5.6, `yield` is tokenized as T_YIELD and `from` as T_STRING,
224
         * but the `T_YIELD_FROM` token *is* defined in PHP.
225
         * In PHP 7.0+ both are tokenized as their respective token, however,
226
         * a multi-line "yield from" is tokenized as two tokens.
227
         */
228
        if ($tokenType === 'T_YIELD') {
229
            $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($end + 1), null, true);
230
            if ($tokens[$nextToken]['code'] === T_STRING
231
                && $tokens[$nextToken]['content'] === 'from'
232
            ) {
233
                $tokenType = 'T_YIELD_FROM';
234
                $end       = $nextToken;
235
            }
236
            unset($nextToken);
237
        }
238
239
        if ($tokenType === 'T_YIELD_FROM' && $tokens[($stackPtr - 1)]['type'] === 'T_YIELD_FROM') {
240
            // Multi-line "yield from", no need to report it twice.
241
            return;
242
        }
243
244
        if (isset($this->newKeywords[$tokenType]) === false) {
245
            return;
246
        }
247
248
        $nextToken = $phpcsFile->findNext(\PHP_CodeSniffer_Tokens::$emptyTokens, ($end + 1), null, true);
249
        $prevToken = $phpcsFile->findPrevious(\PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
250
251
252
        // Skip attempts to use keywords as functions or class names - the former
253
        // will be reported by ForbiddenNamesAsInvokedFunctionsSniff, whilst the
254
        // latter will be (partially) reported by the ForbiddenNames sniff.
255
        // Either type will result in false-positives when targetting lower versions
256
        // of PHP where the name was not reserved, unless we explicitly check for
257
        // them.
258
        if (($nextToken === false
259
                || $tokens[$nextToken]['type'] !== 'T_OPEN_PARENTHESIS')
260
            && ($prevToken === false
261
                || $tokens[$prevToken]['type'] !== 'T_CLASS'
262
                || $tokens[$prevToken]['type'] !== 'T_INTERFACE')
263
        ) {
264
            // Skip based on token scope condition.
265
            if (isset($this->newKeywords[$tokenType]['condition'])
266
                && call_user_func(array($this, $this->newKeywords[$tokenType]['condition']), $phpcsFile, $stackPtr) === true
267
            ) {
268
                return;
269
            }
270
271
            $itemInfo = array(
272
                'name'   => $tokenType,
273
            );
274
            $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
275
        }
276
277
    }//end process()
278
279
280
    /**
281
     * Get the relevant sub-array for a specific item from a multi-dimensional array.
282
     *
283
     * @param array $itemInfo Base information about the item.
284
     *
285
     * @return array Version and other information about the item.
286
     */
287
    public function getItemArray(array $itemInfo)
288
    {
289
        return $this->newKeywords[$itemInfo['name']];
290
    }
291
292
293
    /**
294
     * Get an array of the non-PHP-version array keys used in a sub-array.
295
     *
296
     * @return array
297
     */
298
    protected function getNonVersionArrayKeys()
299
    {
300
        return array(
301
            'description',
302
            'condition',
303
            'content',
304
        );
305
    }
306
307
308
    /**
309
     * Retrieve the relevant detail (version) information for use in an error message.
310
     *
311
     * @param array $itemArray Version and other information about the item.
312
     * @param array $itemInfo  Base information about the item.
313
     *
314
     * @return array
315
     */
316
    public function getErrorInfo(array $itemArray, array $itemInfo)
317
    {
318
        $errorInfo = parent::getErrorInfo($itemArray, $itemInfo);
319
        $errorInfo['description'] = $itemArray['description'];
320
321
        return $errorInfo;
322
323
    }
324
325
326
    /**
327
     * Allow for concrete child classes to filter the error data before it's passed to PHPCS.
328
     *
329
     * @param array $data      The error data array which was created.
330
     * @param array $itemInfo  Base information about the item this error message applied to.
331
     * @param array $errorInfo Detail information about an item this error message applied to.
332
     *
333
     * @return array
334
     */
335
    protected function filterErrorData(array $data, array $itemInfo, array $errorInfo)
336
    {
337
        $data[0] = $errorInfo['description'];
338
        return $data;
339
    }
340
341
342
    /**
343
     * Callback for the quoted heredoc identifier condition.
344
     *
345
     * A double quoted identifier will have the opening quote on position 3
346
     * in the string: `<<<"ID"`.
347
     *
348
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
349
     * @param int                  $stackPtr  The position of the current token in
350
     *                                        the stack passed in $tokens.
351
     *
352
     * @return bool
353
     */
354
    public function isNotQuoted(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
355
    {
356
        $tokens = $phpcsFile->getTokens();
357
        return ($tokens[$stackPtr]['content'][3] !== '"');
358
    }
359
360
361
}//end class
362