Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHPCSUtils / 836885383

12 May 2021 - 14:15 coverage: 97.293%. First build
836885383

Pull #251

github

GitHub
<a href="https://github.com/PHPCSStandards/PHPCSUtils/commit/<a class=hub.com/PHPCSStandards/PHPCSUtils/commit/<a class="double-link" href="https://git"><a class=hub.com/PHPCSStandards/PHPCSUtils/commit/<a class="double-link" href="https://git"><a class=hub.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e366">db246f1a0<a href="https://github.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e366">&quot;&gt;&amp;lt;a href=&amp;quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/&lt;/a&gt;&lt;a class=&quot;double-link&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/&amp;lt;a class=&amp;quot;double-link&amp;quot; href=&amp;quot;https://git&quot;&gt;&amp;lt;a class=&lt;/a&gt;hub.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e366&quot;&gt;db246f1a0&lt;/a&gt;&lt;a href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e366&quot;&gt;&amp;lt;a href=&amp;quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba&lt;/a&gt;4199e366&quot;>&quot;&gt;Merge &lt;/a&gt;&lt;a class=&quot;double-link&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/<a class="double-link" href="https://github.com/PHPCSStandards/PHPCSUtils/commit/56388c57ecf097e23494442776b88277d5fa7718">56388c57e</a><a href="https://github.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e366">&amp;quot;&amp;gt;56388c57e&amp;lt;/a&amp;gt;&amp;quot;&amp;gt;56388c57e&amp;lt;/a&amp;gt;&amp;lt;a href=&amp;quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/db246f1a07dadd2d97a1da7aa6e5aeba4199e&lt;/a&gt;366&quot;&gt; into &lt;/a&gt;&lt;a class=&quot;double-link&quot; href=&quot;https://github.com/PHPCSStandards/PHPCSUtils/commit/<a class="double-link" href="https://github.com/PHPCSStandards/PHPCSUtils/commit/cb5f20cd4">cb5f20cd4">cb5f20cd4</a>
Pull Request #251: PHPCS 4.x | Handle closure use being a parenthesis owner

4 of 5 new or added lines in 2 files covered. (80.0%)

2732 of 2808 relevant lines covered (97.29%)

87.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.06
/PHPCSUtils/Utils/UseStatements.php
1
<?php
2
/**
3
 * PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4
 *
5
 * @package   PHPCSUtils
6
 * @copyright 2019-2020 PHPCSUtils Contributors
7
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
8
 * @link      https://github.com/PHPCSStandards/PHPCSUtils
9
 */
10

11
namespace PHPCSUtils\Utils;
12

13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Files\File;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHPCSUtils\BackCompat\BCTokens;
17
use PHPCSUtils\BackCompat\Helper;
18
use PHPCSUtils\Utils\Conditions;
19
use PHPCSUtils\Utils\Parentheses;
20

21
/**
22
 * Utility functions for examining use statements.
23
 *
24
 * @since 1.0.0
25
 */
26
class UseStatements
27
{
28

29
    /**
30
     * Determine what a T_USE token is used for.
31
     *
32
     * @since 1.0.0
33
     *
34
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
35
     * @param int                         $stackPtr  The position of the `T_USE` token.
36
     *
37
     * @return string Either `'closure'`, `'import'` or `'trait'`.
38
     *                An empty string will be returned if the token is used in an
39
     *                invalid context or if it couldn't be reliably determined what
40
     *                the `T_USE` token is used for. An empty string being returned will
41
     *                normally mean the code being examined contains a parse error.
42
     *
43
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
44
     *                                                      `T_USE` token.
45
     */
46
    public static function getType(File $phpcsFile, $stackPtr)
47
    {
48
        $tokens = $phpcsFile->getTokens();
224×
49

50
        if (isset($tokens[$stackPtr]) === false
224×
51
            || $tokens[$stackPtr]['code'] !== \T_USE
224×
52
        ) {
53
            throw new RuntimeException('$stackPtr must be of type T_USE');
8×
54
        }
55

56
        $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
216×
57
        if ($next === false) {
216×
58
            // Live coding or parse error.
59
            return '';
12×
60
        }
61

62
        // More efficient & simpler check for closure use in PHPCS 4.x.
63
        if (isset($tokens[$stackPtr]['parenthesis_owner'])
204×
64
            && $tokens[$stackPtr]['parenthesis_owner'] === $stackPtr
204×
65
        ) {
NEW
66
            return 'closure';
!
67
        }
68

69
        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
204×
70
        if ($prev !== false && $tokens[$prev]['code'] === \T_CLOSE_PARENTHESIS
204×
71
            && Parentheses::isOwnerIn($phpcsFile, $prev, \T_CLOSURE) === true
204×
72
        ) {
73
            return 'closure';
36×
74
        }
75

76
        $lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr);
168×
77
        if (($tokens[$lastCondition]['code'] === \T_CASE
168×
78
                || $tokens[$lastCondition]['code'] === \T_DEFAULT)
168×
79
            && \version_compare(Helper::getVersion(), '2.99.99', '<') === true
168×
80
            && Conditions::hasCondition($phpcsFile, $stackPtr, [\T_SWITCH]) === false
168×
81
        ) {
82
            $lastCondition = Conditions::getLastCondition($phpcsFile, $lastCondition);
!
83
        }
84

85
        if ($lastCondition === false || $tokens[$lastCondition]['code'] === \T_NAMESPACE) {
168×
86
            // Global or scoped namespace and not a closure use statement.
87
            return 'import';
84×
88
        }
89

90
        $traitScopes = BCTokens::ooScopeTokens();
84×
91
        // Only classes and traits can import traits.
92
        unset($traitScopes[\T_INTERFACE]);
84×
93

94
        if (isset($traitScopes[$tokens[$lastCondition]['code']]) === true) {
84×
95
            return 'trait';
72×
96
        }
97

98
        return '';
12×
99
    }
100

101
    /**
102
     * Determine whether a T_USE token represents a closure use statement.
103
     *
104
     * @since 1.0.0
105
     *
106
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
107
     * @param int                         $stackPtr  The position of the `T_USE` token.
108
     *
109
     * @return bool `TRUE` if the token passed is a closure use statement.
110
     *              `FALSE` if it's not.
111
     *
112
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
113
     *                                                      `T_USE` token.
114
     */
115
    public static function isClosureUse(File $phpcsFile, $stackPtr)
116
    {
117
        return (self::getType($phpcsFile, $stackPtr) === 'closure');
72×
118
    }
119

120
    /**
121
     * Determine whether a T_USE token represents a class/function/constant import use statement.
122
     *
123
     * @since 1.0.0
124
     *
125
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
126
     * @param int                         $stackPtr  The position of the `T_USE` token.
127
     *
128
     * @return bool `TRUE` if the token passed is an import use statement.
129
     *              `FALSE` if it's not.
130
     *
131
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
132
     *                                                      `T_USE` token.
133
     */
134
    public static function isImportUse(File $phpcsFile, $stackPtr)
135
    {
136
        return (self::getType($phpcsFile, $stackPtr) === 'import');
72×
137
    }
138

139
    /**
140
     * Determine whether a T_USE token represents a trait use statement.
141
     *
142
     * @since 1.0.0
143
     *
144
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
145
     * @param int                         $stackPtr  The position of the `T_USE` token.
146
     *
147
     * @return bool `TRUE` if the token passed is a trait use statement.
148
     *              `FALSE` if it's not.
149
     *
150
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
151
     *                                                      `T_USE` token.
152
     */
153
    public static function isTraitUse(File $phpcsFile, $stackPtr)
154
    {
155
        return (self::getType($phpcsFile, $stackPtr) === 'trait');
72×
156
    }
157

158
    /**
159
     * Split an import use statement into individual imports.
160
     *
161
     * Handles single import, multi-import and group-import use statements.
162
     *
163
     * @since 1.0.0
164
     * @since 1.0.0-alpha4 Added support for PHP 8.0 identifier name tokenization.
165
     *
166
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
167
     * @param int                         $stackPtr  The position in the stack of the `T_USE` token.
168
     *
169
     * @return array A multi-level array containing information about the use statement.
170
     *               The first level is `'name'`, `'function'` and `'const'`. These keys will always exist.
171
     *               If any statements are found for any of these categories, the second level
172
     *               will contain the alias/name as the key and the full original use name as the
173
     *               value for each of the found imports or an empty array if no imports were found
174
     *               in this use statement for a particular category.
175
     *
176
     *               For example, for this function group use statement:
177
     *               ```php
178
     *               use function Vendor\Package\{
179
     *                   LevelA\Name as Alias,
180
     *                   LevelB\Another_Name,
181
     *               };
182
     *               ```
183
     *               the return value would look like this:
184
     *               ```php
185
     *               array(
186
     *                 'name'     => array(),
187
     *                 'function' => array(
188
     *                   'Alias'        => 'Vendor\Package\LevelA\Name',
189
     *                   'Another_Name' => 'Vendor\Package\LevelB\Another_Name',
190
     *                 ),
191
     *                 'const'    => array(),
192
     *               )
193
     *               ```
194
     *
195
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
196
     *                                                      `T_USE` token.
197
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `T_USE` token is not for an import
198
     *                                                      use statement.
199
     */
200
    public static function splitImportUseStatement(File $phpcsFile, $stackPtr)
201
    {
202
        $tokens = $phpcsFile->getTokens();
92×
203

204
        if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_USE) {
92×
205
            throw new RuntimeException('$stackPtr must be of type T_USE');
8×
206
        }
207

208
        if (self::isImportUse($phpcsFile, $stackPtr) === false) {
84×
209
            throw new RuntimeException('$stackPtr must be an import use statement');
8×
210
        }
211

212
        $statements = [
213
            'name'     => [],
76×
214
            'function' => [],
215
            'const'    => [],
216
        ];
217

218
        $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
76×
219
        if ($endOfStatement === false) {
76×
220
            // Live coding or parse error.
221
            return $statements;
8×
222
        }
223

224
        $endOfStatement++;
68×
225

226
        $start     = true;
68×
227
        $useGroup  = false;
68×
228
        $hasAlias  = false;
68×
229
        $baseName  = '';
68×
230
        $name      = '';
68×
231
        $type      = '';
68×
232
        $fixedType = false;
68×
233
        $alias     = '';
68×
234

235
        for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
68×
236
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
68×
237
                continue;
68×
238
            }
239

240
            $tokenType = $tokens[$i]['type'];
68×
241

242
            /*
243
             * BC: Work round a tokenizer bug related to a parse error.
244
             *
245
             * If `function` or `const` is used as the alias, the semi-colon after it would
246
             * be tokenized as T_STRING.
247
             * For `function` this was fixed in PHPCS 2.8.0. For `const` the issue still exists
248
             * in PHPCS 3.5.2.
249
             *
250
             * Along the same lines, the `}` T_CLOSE_USE_GROUP would also be tokenized as T_STRING.
251
             */
252
            if ($tokenType === 'T_STRING') {
68×
253
                if ($tokens[$i]['content'] === ';') {
68×
254
                    $tokenType = 'T_SEMICOLON';
!
255
                } elseif ($tokens[$i]['content'] === '}') {
68×
256
                    $tokenType = 'T_CLOSE_USE_GROUP';
1×
257
                }
258
            }
259

260
            switch ($tokenType) {
261
                case 'T_STRING':
68×
262
                    // Only when either at the start of the statement or at the start of a new sub within a group.
263
                    if ($start === true && $fixedType === false) {
68×
264
                        $content = \strtolower($tokens[$i]['content']);
64×
265
                        if ($content === 'function'
266
                            || $content === 'const'
64×
267
                        ) {
268
                            $type  = $content;
38×
269
                            $start = false;
38×
270
                            if ($useGroup === false) {
38×
271
                                $fixedType = true;
36×
272
                            }
273

274
                            break;
38×
275
                        } else {
276
                            $type = 'name';
28×
277
                        }
278
                    }
279

280
                    $start = false;
68×
281

282
                    if ($hasAlias === false) {
68×
283
                        $name .= $tokens[$i]['content'];
68×
284
                    }
285

286
                    $alias = $tokens[$i]['content'];
68×
287

288
                    /*
289
                     * BC: work around PHPCS tokenizer issue in PHPCS < 3.5.7 where anything directly after
290
                     * a `function` or `const` keyword would be retokenized to `T_STRING`, including the
291
                     * PHP 8 identifier name tokens.
292
                     */
293
                    $hasSlash = \strrpos($tokens[$i]['content'], '\\');
68×
294
                    if ($hasSlash !== false) {
68×
295
                        $alias = \substr($tokens[$i]['content'], ($hasSlash + 1));
4×
296
                    }
297

298
                    break;
68×
299

300
                case 'T_NAME_QUALIFIED':
68×
301
                case 'T_NAME_FULLY_QUALIFIED': // This would be a parse error, but handle it anyway.
68×
302
                    // Only when either at the start of the statement or at the start of a new sub within a group.
303
                    if ($start === true && $fixedType === false) {
!
304
                        $type = 'name';
!
305
                    }
306

307
                    $start = false;
!
308

309
                    if ($hasAlias === false) {
!
310
                        $name .= $tokens[$i]['content'];
!
311
                    }
312

313
                    $alias = \substr($tokens[$i]['content'], (\strrpos($tokens[$i]['content'], '\\') + 1));
!
314
                    break;
!
315

316
                case 'T_AS':
68×
317
                    $hasAlias = true;
48×
318
                    break;
48×
319

320
                case 'T_OPEN_USE_GROUP':
68×
321
                    $start    = true;
20×
322
                    $useGroup = true;
20×
323
                    $baseName = $name;
20×
324
                    $name     = '';
20×
325
                    break;
20×
326

327
                case 'T_SEMICOLON':
68×
328
                case 'T_CLOSE_TAG':
68×
329
                case 'T_CLOSE_USE_GROUP':
68×
330
                case 'T_COMMA':
68×
331
                    if ($name !== '') {
68×
332
                        if ($useGroup === true) {
68×
333
                            $statements[$type][$alias] = $baseName . $name;
20×
334
                        } else {
335
                            $statements[$type][$alias] = $name;
48×
336
                        }
337
                    }
338

339
                    if ($tokenType !== 'T_COMMA') {
68×
340
                        break 2;
68×
341
                    }
342

343
                    // Reset.
344
                    $start    = true;
28×
345
                    $name     = '';
28×
346
                    $hasAlias = false;
28×
347
                    if ($fixedType === false) {
28×
348
                        $type = '';
12×
349
                    }
350
                    break;
28×
351

352
                case 'T_NS_SEPARATOR':
68×
353
                    $name .= $tokens[$i]['content'];
68×
354
                    break;
68×
355

356
                case 'T_FUNCTION':
20×
357
                case 'T_CONST':
16×
358
                    /*
359
                     * BC: Work around tokenizer bug in PHPCS < 3.4.1.
360
                     *
361
                     * `function`/`const` in `use function`/`use const` tokenized as T_FUNCTION/T_CONST
362
                     * instead of T_STRING when there is a comment between the keywords.
363
                     *
364
                     * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/2431
365
                     */
366
                    if ($start === true && $fixedType === false) {
12×
367
                        $type  = \strtolower($tokens[$i]['content']);
6×
368
                        $start = false;
6×
369
                        if ($useGroup === false) {
6×
370
                            $fixedType = true;
4×
371
                        }
372

373
                        break;
6×
374
                    }
375

376
                    $start = false;
6×
377

378
                    if ($hasAlias === false) {
6×
379
                        $name .= $tokens[$i]['content'];
4×
380
                    }
381

382
                    $alias = $tokens[$i]['content'];
6×
383
                    break;
6×
384

385
                /*
386
                 * Fall back in case reserved keyword is (illegally) used in name.
387
                 * Parse error, but not our concern.
388
                 */
389
                default:
390
                    if ($hasAlias === false) {
8×
391
                        $name .= $tokens[$i]['content'];
4×
392
                    }
393

394
                    $alias = $tokens[$i]['content'];
8×
395
                    break;
8×
396
            }
397
        }
398

399
        return $statements;
68×
400
    }
401

402
    /**
403
     * Split an import use statement into individual imports and merge it with an array of previously
404
     * seen import use statements.
405
     *
406
     * Beware: this method should only be used to combine the import use statements found in *one* file.
407
     * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
408
     *
409
     * In most cases when tempted to use this method, the {@see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff}
410
     * (upcoming) should be used instead.
411
     *
412
     * @see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff
413
     * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
414
     * @see \PHPCSUtils\Utils\UseStatements::mergeImportUseStatements()
415
     *
416
     * @since 1.0.0-alpha3
417
     *
418
     * @param \PHP_CodeSniffer\Files\File $phpcsFile             The file where this token was found.
419
     * @param int                         $stackPtr              The position in the stack of the `T_USE` token.
420
     * @param array                       $previousUseStatements The import `use` statements collected so far.
421
     *                                                           This should be either the output of a
422
     *                                                           previous call to this method or the output of
423
     *                                                           an earlier call to the
424
     *                                                           {@see UseStatements::splitImportUseStatement()}
425
     *                                                           method.
426
     *
427
     * @return array A multi-level array containing information about the current `use` statement combined with
428
     *               the previously collected `use` statement information.
429
     *               See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
430
     */
431
    public static function splitAndMergeImportUseStatement(File $phpcsFile, $stackPtr, $previousUseStatements)
432
    {
433
        try {
434
            $useStatements         = self::splitImportUseStatement($phpcsFile, $stackPtr);
20×
435
            $previousUseStatements = self::mergeImportUseStatements($previousUseStatements, $useStatements);
16×
436
        } catch (RuntimeException $e) {
12×
437
            // Not an import use statement.
438
        }
439

440
        return $previousUseStatements;
20×
441
    }
442

443
    /**
444
     * Merge two import use statement arrays.
445
     *
446
     * Beware: this method should only be used to combine the import use statements found in *one* file.
447
     * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
448
     *
449
     * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
450
     *
451
     * @since 1.0.0-alpha4
452
     *
453
     * @param array $previousUseStatements The import `use` statements collected so far.
454
     *                                     This should be either the output of a
455
     *                                     previous call to this method or the output of
456
     *                                     an earlier call to the
457
     *                                     {@see UseStatements::splitImportUseStatement()}
458
     *                                     method.
459
     * @param array $currentUseStatement   The parsed import `use` statements to merge with
460
     *                                     the previously collected use statements.
461
     *                                     This should be the output of a call to the
462
     *                                     {@see UseStatements::splitImportUseStatement()}
463
     *                                     method.
464
     *
465
     * @return array A multi-level array containing information about the current `use` statement combined with
466
     *               the previously collected `use` statement information.
467
     *               See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
468
     */
469
    public static function mergeImportUseStatements($previousUseStatements, $currentUseStatement)
470
    {
471
        if (isset($previousUseStatements['name']) === false) {
16×
472
            $previousUseStatements['name'] = $currentUseStatement['name'];
4×
473
        } else {
474
            $previousUseStatements['name'] += $currentUseStatement['name'];
12×
475
        }
476
        if (isset($previousUseStatements['function']) === false) {
16×
477
            $previousUseStatements['function'] = $currentUseStatement['function'];
4×
478
        } else {
479
            $previousUseStatements['function'] += $currentUseStatement['function'];
12×
480
        }
481
        if (isset($previousUseStatements['const']) === false) {
16×
482
            $previousUseStatements['const'] = $currentUseStatement['const'];
4×
483
        } else {
484
            $previousUseStatements['const'] += $currentUseStatement['const'];
12×
485
        }
486

487
        return $previousUseStatements;
16×
488
    }
489
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc