|  | 
|  | 1 | +<?php | 
|  | 2 | +/** | 
|  | 3 | + * PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers. | 
|  | 4 | + * | 
|  | 5 | + * @package   PHPCSUtils | 
|  | 6 | + * @copyright 2025 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\Files\File; | 
|  | 14 | +use PHP_CodeSniffer\Util\Tokens; | 
|  | 15 | +use PHPCSUtils\Exceptions\OutOfBoundsStackPtr; | 
|  | 16 | +use PHPCSUtils\Exceptions\TypeError; | 
|  | 17 | +use PHPCSUtils\Exceptions\UnexpectedTokenType; | 
|  | 18 | +use PHPCSUtils\Internal\Cache; | 
|  | 19 | +use PHPCSUtils\Tokens\Collections; | 
|  | 20 | + | 
|  | 21 | +/** | 
|  | 22 | + * Utility functions to retrieve information related to attributes. | 
|  | 23 | + * | 
|  | 24 | + * @since 1.2.0 | 
|  | 25 | + */ | 
|  | 26 | +final class AttributeBlock | 
|  | 27 | +{ | 
|  | 28 | + | 
|  | 29 | +    /** | 
|  | 30 | +     * Retrieve information on each attribute instantiation within an attribute block. | 
|  | 31 | +     * | 
|  | 32 | +     * @since 1.2.0 | 
|  | 33 | +     * | 
|  | 34 | +     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. | 
|  | 35 | +     * @param int                         $stackPtr  The position of the T_ATTRIBUTE (attribute opener) token. | 
|  | 36 | +     * | 
|  | 37 | +     * @return array<array<string, int|string|false>> | 
|  | 38 | +     *               A multi-dimentional array with information on each attribute instantiation in the block. | 
|  | 39 | +     *               The information gathered about each attribute instantiation is in the following format: | 
|  | 40 | +     *               ```php | 
|  | 41 | +     *               array( | 
|  | 42 | +     *                 'name'        => string,    // The full name of the attribute being instantiated. | 
|  | 43 | +     *                                             // This will be name as passed without namespace resolution. | 
|  | 44 | +     *                 'name_token'  => int,       // The stack pointer to the last token in the attribute name. | 
|  | 45 | +     *                                             // Pro-tip: this token can be passed on to the methods in the | 
|  | 46 | +     *                                             // {@see PassedParameters} class to retrieve the | 
|  | 47 | +     *                                             // parameters passed to the attribute constructor. | 
|  | 48 | +     *                 'start'       => int,       // The stack pointer to the first token in the attribute instantiation. | 
|  | 49 | +     *                                             // Note: this may be a leading whitespace/comment token. | 
|  | 50 | +     *                 'end'         => int,       // The stack pointer to the last token in the attribute instantiation. | 
|  | 51 | +     *                                             // Note: this may be a trailing whitespace/comment token. | 
|  | 52 | +     *                 'comma_token' => int|false, // The stack pointer to the comma after the attribute instantiation | 
|  | 53 | +     *                                             // or FALSE if this is the last attribute and there is no comma. | 
|  | 54 | +     *               ) | 
|  | 55 | +     *               ``` | 
|  | 56 | +     *               If no attributes are found, an empty array will be returned. | 
|  | 57 | +     * | 
|  | 58 | +     * @throws \PHPCSUtils\Exceptions\TypeError           If the $stackPtr parameter is not an integer. | 
|  | 59 | +     * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. | 
|  | 60 | +     * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_ATTRIBUTE` token. | 
|  | 61 | +     */ | 
|  | 62 | +    public static function getAttributes(File $phpcsFile, $stackPtr) | 
|  | 63 | +    { | 
|  | 64 | +        $tokens = $phpcsFile->getTokens(); | 
|  | 65 | + | 
|  | 66 | +        if (\is_int($stackPtr) === false) { | 
|  | 67 | +            throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); | 
|  | 68 | +        } | 
|  | 69 | + | 
|  | 70 | +        if (isset($tokens[$stackPtr]) === false) { | 
|  | 71 | +            throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); | 
|  | 72 | +        } | 
|  | 73 | + | 
|  | 74 | +        if ($tokens[$stackPtr]['code'] !== \T_ATTRIBUTE) { | 
|  | 75 | +            throw UnexpectedTokenType::create(2, '$stackPtr', 'T_ATTRIBUTE', $tokens[$stackPtr]['type']); | 
|  | 76 | +        } | 
|  | 77 | + | 
|  | 78 | +        if (isset($tokens[$stackPtr]['attribute_closer']) === false) { | 
|  | 79 | +            return []; | 
|  | 80 | +        } | 
|  | 81 | + | 
|  | 82 | +        if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { | 
|  | 83 | +            return Cache::get($phpcsFile, __METHOD__, $stackPtr); | 
|  | 84 | +        } | 
|  | 85 | + | 
|  | 86 | +        $opener = $stackPtr; | 
|  | 87 | +        $closer = $tokens[$stackPtr]['attribute_closer']; | 
|  | 88 | + | 
|  | 89 | +        $attributes  = []; | 
|  | 90 | +        $currentName = ''; | 
|  | 91 | +        $nameToken   = null; | 
|  | 92 | +        $start       = ($opener + 1); | 
|  | 93 | + | 
|  | 94 | +        for ($i = ($opener + 1); $i <= $closer; $i++) { | 
|  | 95 | +            // Skip over potentially large docblocks. | 
|  | 96 | +            if ($tokens[$i]['code'] === \T_DOC_COMMENT_OPEN_TAG | 
|  | 97 | +                && isset($tokens[$i]['comment_closer']) | 
|  | 98 | +            ) { | 
|  | 99 | +                $i = $tokens[$i]['comment_closer']; | 
|  | 100 | +                continue; | 
|  | 101 | +            } | 
|  | 102 | + | 
|  | 103 | +            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) { | 
|  | 104 | +                continue; | 
|  | 105 | +            } | 
|  | 106 | + | 
|  | 107 | +            if (isset(Collections::namespacedNameTokens()[$tokens[$i]['code']])) { | 
|  | 108 | +                $currentName .= $tokens[$i]['content']; | 
|  | 109 | +                $nameToken    = $i; | 
|  | 110 | +                continue; | 
|  | 111 | +            } | 
|  | 112 | + | 
|  | 113 | +            if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS | 
|  | 114 | +                && isset($tokens[$i]['parenthesis_closer']) === true | 
|  | 115 | +            ) { | 
|  | 116 | +                // Skip over whatever is passed to the Attribute constructor. | 
|  | 117 | +                $i = $tokens[$i]['parenthesis_closer']; | 
|  | 118 | +                continue; | 
|  | 119 | +            } | 
|  | 120 | + | 
|  | 121 | +            if ($tokens[$i]['code'] === \T_COMMA | 
|  | 122 | +                || $i === $closer | 
|  | 123 | +            ) { | 
|  | 124 | +                // We've reached the end of the name. | 
|  | 125 | +                if ($currentName === '') { | 
|  | 126 | +                    // Parse error. Stop parsing this attribute block. | 
|  | 127 | +                    break; | 
|  | 128 | +                } | 
|  | 129 | + | 
|  | 130 | +                $attributes[] = [ | 
|  | 131 | +                    'name'        => $currentName, | 
|  | 132 | +                    'name_token'  => $nameToken, | 
|  | 133 | +                    'start'       => $start, | 
|  | 134 | +                    'end'         => ($i - 1), | 
|  | 135 | +                    'comma_token' => ($tokens[$i]['code'] === \T_COMMA ? $i : false), | 
|  | 136 | +                ]; | 
|  | 137 | + | 
|  | 138 | +                if ($i === $closer) { | 
|  | 139 | +                    break; | 
|  | 140 | +                } | 
|  | 141 | + | 
|  | 142 | +                // Check if there are more tokens before the attribute closer. | 
|  | 143 | +                // Prevents atrtibute blocks with trailing comma's from setting an extra attribute. | 
|  | 144 | +                $hasNext = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), $closer, true); | 
|  | 145 | +                if ($hasNext === false) { | 
|  | 146 | +                    break; | 
|  | 147 | +                } | 
|  | 148 | + | 
|  | 149 | +                // Prepare for the next attribute instantiation. | 
|  | 150 | +                $currentName = ''; | 
|  | 151 | +                $nameToken   = null; | 
|  | 152 | +                $start       = ($i + 1); | 
|  | 153 | +            } | 
|  | 154 | +        } | 
|  | 155 | + | 
|  | 156 | +        Cache::set($phpcsFile, __METHOD__, $stackPtr, $attributes); | 
|  | 157 | +        return $attributes; | 
|  | 158 | +    } | 
|  | 159 | + | 
|  | 160 | +    /** | 
|  | 161 | +     * Count the number of attributes being instantiated in an attribute block. | 
|  | 162 | +     * | 
|  | 163 | +     * @since 1.2.0 | 
|  | 164 | +     * | 
|  | 165 | +     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. | 
|  | 166 | +     * @param int                         $stackPtr  The position of the T_ATTRIBUTE (attribute opener) token. | 
|  | 167 | +     * | 
|  | 168 | +     * @return int | 
|  | 169 | +     * | 
|  | 170 | +     * @throws \PHPCSUtils\Exceptions\TypeError           If the $stackPtr parameter is not an integer. | 
|  | 171 | +     * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. | 
|  | 172 | +     * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_ATTRIBUTE` token. | 
|  | 173 | +     */ | 
|  | 174 | +    public static function countAttributes(File $phpcsFile, $stackPtr) | 
|  | 175 | +    { | 
|  | 176 | +        return \count(self::getAttributes($phpcsFile, $stackPtr)); | 
|  | 177 | +    } | 
|  | 178 | +} | 
0 commit comments