├── CHANGELOG.md ├── LICENSE ├── PHPCSUtils ├── AbstractSniffs │ └── AbstractArrayDeclarationSniff.php ├── BackCompat │ ├── BCFile.php │ ├── BCTokens.php │ └── Helper.php ├── Exceptions │ ├── InvalidTokenArray.php │ ├── LogicException.php │ ├── MissingArgumentError.php │ ├── OutOfBoundsStackPtr.php │ ├── RuntimeException.php │ ├── TestFileNotFound.php │ ├── TestMarkerNotFound.php │ ├── TestTargetNotFound.php │ ├── TypeError.php │ ├── UnexpectedTokenType.php │ └── ValueError.php ├── Fixers │ └── SpacesFixer.php ├── Internal │ ├── Cache.php │ ├── IsShortArrayOrList.php │ ├── IsShortArrayOrListWithCache.php │ ├── NoFileCache.php │ └── StableCollections.php ├── TestUtils │ ├── ConfigDouble.php │ └── UtilityMethodTestCase.php ├── Tokens │ ├── Collections.php │ └── TokenHelper.php ├── Utils │ ├── Arrays.php │ ├── Conditions.php │ ├── Constants.php │ ├── Context.php │ ├── ControlStructures.php │ ├── FileInfo.php │ ├── FilePath.php │ ├── FunctionDeclarations.php │ ├── GetTokensAsString.php │ ├── Lists.php │ ├── MessageHelper.php │ ├── Namespaces.php │ ├── NamingConventions.php │ ├── Numbers.php │ ├── ObjectDeclarations.php │ ├── Operators.php │ ├── Orthography.php │ ├── Parentheses.php │ ├── PassedParameters.php │ ├── Scopes.php │ ├── TextStrings.php │ ├── TypeString.php │ ├── UseStatements.php │ └── Variables.php └── ruleset.xml ├── README.md ├── composer.json └── phpcsutils-autoload.php /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /PHPCSUtils/BackCompat/BCTokens.php: -------------------------------------------------------------------------------- 1 | `PHPCSUtils\BackCompat\BCTokens::emptyTokens()` 38 | * - `PHP_CodeSniffer\Util\Tokens::$operators` => `PHPCSUtils\BackCompat\BCTokens::operators()` 39 | * - ... etc 40 | * 41 | * The order of the tokens in the arrays may differ between the PHPCS native token arrays and 42 | * the token arrays returned by this class. 43 | * 44 | * @since 1.0.0 45 | * 46 | * @method static array arithmeticTokens() Tokens that represent arithmetic operators. 47 | * @method static array assignmentTokens() Tokens that represent assignments. 48 | * @method static array blockOpeners() Tokens that open code blocks. 49 | * @method static array booleanOperators() Tokens that perform boolean operations. 50 | * @method static array bracketTokens() Tokens that represent brackets and parenthesis. 51 | * @method static array castTokens() Tokens that represent type casting. 52 | * @method static array commentTokens() Tokens that are comments. 53 | * @method static array comparisonTokens() Tokens that represent comparison operator. 54 | * @method static array contextSensitiveKeywords() Tokens representing context sensitive keywords 55 | * in PHP. 56 | * @method static array emptyTokens() Tokens that don't represent code. 57 | * @method static array equalityTokens() Tokens that represent equality comparisons. 58 | * @method static array heredocTokens() Tokens that make up a heredoc string. 59 | * @method static array includeTokens() Tokens that include files. 60 | * @method static array magicConstants() Tokens representing PHP magic constants. 61 | * @method static array methodPrefixes() Tokens that can prefix a method name. 62 | * @method static array ooScopeTokens() Tokens that open class and object scopes. 63 | * @method static array operators() Tokens that perform operations. 64 | * @method static array parenthesisOpeners() Token types that open parenthesis. 65 | * @method static array phpcsCommentTokens() Tokens that are comments containing PHPCS 66 | * instructions. 67 | * @method static array scopeModifiers() Tokens that represent scope modifiers. 68 | * @method static array scopeOpeners() Tokens that are allowed to open scopes. 69 | * @method static array stringTokens() Tokens that represent strings. 70 | * Note that `T_STRING`s are NOT represented in this 71 | * list as this list is about _text_ strings. 72 | * @method static array textStringTokens() Tokens that represent text strings. 73 | */ 74 | final class BCTokens 75 | { 76 | 77 | /** 78 | * Handle calls to (undeclared) methods for token arrays which haven't received any 79 | * changes since PHPCS 3.13.0. 80 | * 81 | * @since 1.0.0 82 | * 83 | * @param string $name The name of the method which has been called. 84 | * @param array $args Any arguments passed to the method. 85 | * Unused as none of the methods take arguments. 86 | * 87 | * @return array Token array 88 | * 89 | * @throws \PHPCSUtils\Exceptions\InvalidTokenArray When an invalid token array is requested. 90 | */ 91 | public static function __callStatic($name, $args) 92 | { 93 | if (isset(Tokens::${$name})) { 94 | return Tokens::${$name}; 95 | } 96 | 97 | // Unknown token array requested. 98 | throw InvalidTokenArray::create($name); 99 | } 100 | 101 | /** 102 | * Tokens that represent the names of called functions. 103 | * 104 | * Retrieve the PHPCS function name tokens array in a cross-version compatible manner. 105 | * 106 | * Changelog for the PHPCS native array: 107 | * - Introduced in PHPCS 2.3.3. 108 | * - PHPCS 4.0.0: `T_NAME_QUALIFIED`, `T_NAME_FULLY_QUALIFIED` and `T_NAME_RELATIVE` added to the array. 109 | * 110 | * @see \PHP_CodeSniffer\Util\Tokens::$functionNameTokens Original array. 111 | * 112 | * @since 1.0.0 113 | * 114 | * @return array Token array. 115 | */ 116 | public static function functionNameTokens() 117 | { 118 | $tokens = Tokens::$functionNameTokens; 119 | $tokens += Collections::nameTokens(); 120 | 121 | return $tokens; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /PHPCSUtils/BackCompat/Helper.php: -------------------------------------------------------------------------------- 1 | config` property should work 63 | * in PHPCS 3.x and higher. 64 | * 65 | * @return bool Whether the setting of the data was successfull. 66 | * 67 | * @throws \PHPCSUtils\Exceptions\MissingArgumentError When using PHPCS 4.x and not passing the $config parameter. 68 | */ 69 | public static function setConfigData($key, $value, $temp = false, $config = null) 70 | { 71 | if (isset($config) === true) { 72 | // PHPCS 3.x and 4.x. 73 | return $config->setConfigData($key, $value, $temp); 74 | } 75 | 76 | if (\version_compare(self::getVersion(), '3.99.99', '>') === true) { 77 | throw MissingArgumentError::create(4, '$config', 'when running on PHPCS 4.x'); 78 | } 79 | 80 | // PHPCS 3.x. 81 | return Config::setConfigData($key, $value, $temp); 82 | } 83 | 84 | /** 85 | * Get the value of a single PHP_CodeSniffer config key. 86 | * 87 | * @see Helper::getCommandLineData() Alternative for the same which is more reliable 88 | * if the `$phpcsFile` object is available. 89 | * 90 | * @since 1.0.0 91 | * 92 | * @param string $key The name of the config value. 93 | * 94 | * @return string|null 95 | */ 96 | public static function getConfigData($key) 97 | { 98 | return Config::getConfigData($key); 99 | } 100 | 101 | /** 102 | * Get the value of a CLI overrulable single PHP_CodeSniffer config key. 103 | * 104 | * Use this for config keys which can be set in the `CodeSniffer.conf` file, 105 | * on the command-line or in a ruleset. 106 | * 107 | * @since 1.0.0 108 | * 109 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. 110 | * @param string $key The name of the config value. 111 | * 112 | * @return string|null 113 | */ 114 | public static function getCommandLineData(File $phpcsFile, $key) 115 | { 116 | if (isset($phpcsFile->config->{$key})) { 117 | return $phpcsFile->config->{$key}; 118 | } 119 | 120 | return null; 121 | } 122 | 123 | /** 124 | * Get the applicable tab width as passed to PHP_CodeSniffer from the 125 | * command-line or the ruleset. 126 | * 127 | * @since 1.0.0 128 | * 129 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. 130 | * 131 | * @return int Tab width. Defaults to the PHPCS native default of 4. 132 | */ 133 | public static function getTabWidth(File $phpcsFile) 134 | { 135 | $tabWidth = self::getCommandLineData($phpcsFile, 'tabWidth'); 136 | if ($tabWidth > 0) { 137 | return (int) $tabWidth; 138 | } 139 | 140 | return self::DEFAULT_TABWIDTH; 141 | } 142 | 143 | /** 144 | * Get the applicable (file) encoding as passed to PHP_CodeSniffer from the 145 | * command-line or the ruleset. 146 | * 147 | * @since 1.0.0 148 | * @since 1.0.10 The `File` type declaration has been removed from the parameter declaration. 149 | * 150 | * @param \PHP_CodeSniffer\Files\File|null $phpcsFile Optional. The current file being processed. 151 | * 152 | * @return string Encoding. Defaults to the PHPCS native default, which is 'utf-8' 153 | * for PHPCS 3.x. 154 | */ 155 | public static function getEncoding($phpcsFile = null) 156 | { 157 | $default = 'utf-8'; 158 | 159 | if ($phpcsFile instanceof File) { 160 | // Most reliable. 161 | $encoding = self::getCommandLineData($phpcsFile, 'encoding'); 162 | if ($encoding === null) { 163 | $encoding = $default; 164 | } 165 | 166 | return $encoding; 167 | } 168 | 169 | // Less reliable. 170 | $encoding = self::getConfigData('encoding'); 171 | if ($encoding === null) { 172 | $encoding = $default; 173 | } 174 | 175 | return $encoding; 176 | } 177 | 178 | /** 179 | * Check whether the "--ignore-annotations" option is in effect. 180 | * 181 | * @since 1.0.0 182 | * @since 1.0.10 The `File` type declaration has been removed from the parameter declaration. 183 | * 184 | * @param \PHP_CodeSniffer\Files\File|null $phpcsFile Optional. The current file being processed. 185 | * 186 | * @return bool `TRUE` if annotations should be ignored, `FALSE` otherwise. 187 | */ 188 | public static function ignoreAnnotations($phpcsFile = null) 189 | { 190 | if (isset($phpcsFile) 191 | && $phpcsFile instanceof File 192 | && isset($phpcsFile->config->annotations) 193 | ) { 194 | return ! $phpcsFile->config->annotations; 195 | } 196 | 197 | $annotations = Config::getConfigData('annotations'); 198 | if (isset($annotations)) { 199 | return ! $annotations; 200 | } 201 | 202 | return false; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /PHPCSUtils/Exceptions/InvalidTokenArray.php: -------------------------------------------------------------------------------- 1 | getTokens(); 106 | 107 | /* 108 | * Validate the received function input. 109 | */ 110 | 111 | if (\is_int($stackPtr) === false) { 112 | throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); 113 | } 114 | 115 | if (\is_int($secondPtr) === false) { 116 | throw TypeError::create(3, '$secondPtr', 'integer', $secondPtr); 117 | } 118 | 119 | if (isset($tokens[$stackPtr]) === false) { 120 | throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); 121 | } 122 | 123 | if (isset($tokens[$secondPtr]) === false) { 124 | throw OutOfBoundsStackPtr::create(3, '$secondPtr', $secondPtr); 125 | } 126 | 127 | if ($tokens[$stackPtr]['code'] === \T_WHITESPACE) { 128 | throw UnexpectedTokenType::create(2, '$stackPtr', 'any, except whitespace', 'T_WHITESPACE'); 129 | } 130 | 131 | if ($tokens[$secondPtr]['code'] === \T_WHITESPACE) { 132 | throw UnexpectedTokenType::create(3, '$secondPtr', 'any, except whitespace', 'T_WHITESPACE'); 133 | } 134 | 135 | $expected = false; 136 | if ($expectedSpaces === 'newline') { 137 | $expected = $expectedSpaces; 138 | } elseif (\is_int($expectedSpaces) === true && $expectedSpaces >= 0) { 139 | $expected = $expectedSpaces; 140 | } elseif (\is_string($expectedSpaces) === true && Numbers::isDecimalInt($expectedSpaces) === true) { 141 | $expected = (int) $expectedSpaces; 142 | } 143 | 144 | if ($expected === false) { 145 | $message = \sprintf('should be either "newline", 0 or a positive integer; %s given', $expectedSpaces); 146 | throw ValueError::create(4, '$expectedSpaces', $message); 147 | } 148 | 149 | $ptrA = $stackPtr; 150 | $ptrB = $secondPtr; 151 | if ($stackPtr > $secondPtr) { 152 | $ptrA = $secondPtr; 153 | $ptrB = $stackPtr; 154 | } 155 | 156 | $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($ptrA + 1), null, true); 157 | if ($nextNonEmpty !== false && $nextNonEmpty < $ptrB) { 158 | throw LogicException::create( 159 | 'The $stackPtr and the $secondPtr token must be adjacent tokens separated only' 160 | . ' by whitespace and/or comments' 161 | ); 162 | } 163 | 164 | /* 165 | * Determine how many spaces are between the two tokens. 166 | */ 167 | 168 | $found = 0; 169 | $foundPhrase = 'no spaces'; 170 | if ($tokens[$ptrA]['line'] !== $tokens[$ptrB]['line']) { 171 | $found = 'newline'; 172 | $foundPhrase = 'a new line'; 173 | if (($tokens[$ptrA]['line'] + 1) !== $tokens[$ptrB]['line']) { 174 | $foundPhrase = 'multiple new lines'; 175 | } 176 | } elseif (($ptrA + 1) !== $ptrB) { 177 | if ($tokens[($ptrA + 1)]['code'] === \T_WHITESPACE) { 178 | $found = $tokens[($ptrA + 1)]['length']; 179 | $foundPhrase = $found . (($found === 1) ? ' space' : ' spaces'); 180 | } else { 181 | $found = 'non-whitespace tokens'; 182 | $foundPhrase = 'non-whitespace tokens'; 183 | } 184 | } 185 | 186 | if ($metricName !== '') { 187 | $phpcsFile->recordMetric($stackPtr, $metricName, $foundPhrase); 188 | } 189 | 190 | if ($found === $expected) { 191 | return; 192 | } 193 | 194 | /* 195 | * Handle the violation message. 196 | */ 197 | 198 | $expectedPhrase = 'no space'; 199 | if ($expected === 'newline') { 200 | $expectedPhrase = 'a new line'; 201 | } elseif ($expected === 1) { 202 | $expectedPhrase = $expected . ' space'; 203 | } elseif ($expected > 1) { 204 | $expectedPhrase = $expected . ' spaces'; 205 | } 206 | 207 | $fixable = true; 208 | $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($ptrA + 1), null, true); 209 | if ($nextNonWhitespace !== $ptrB) { 210 | // Comment found between the tokens and we don't know where it should go, so don't auto-fix. 211 | $fixable = false; 212 | } 213 | 214 | if ($found === 'newline' 215 | && $tokens[$ptrA]['code'] === \T_COMMENT 216 | && \substr($tokens[$ptrA]['content'], -2) !== '*/' 217 | ) { 218 | /* 219 | * $ptrA is a slash-style trailing comment, removing the new line would comment out 220 | * the code, so don't auto-fix. 221 | */ 222 | $fixable = false; 223 | } 224 | 225 | $method = 'add'; 226 | $method .= ($fixable === true) ? 'Fixable' : ''; 227 | $method .= ($errorType === 'error') ? 'Error' : 'Warning'; 228 | 229 | $recorded = $phpcsFile->$method( 230 | $errorTemplate, 231 | $stackPtr, 232 | $errorCode, 233 | [$expectedPhrase, $foundPhrase], 234 | $errorSeverity 235 | ); 236 | 237 | if ($fixable === false || $recorded === false) { 238 | return; 239 | } 240 | 241 | /* 242 | * Fix the violation. 243 | */ 244 | 245 | $phpcsFile->fixer->beginChangeset(); 246 | 247 | /* 248 | * Remove existing whitespace. No need to check if it's whitespace as otherwise the fixer 249 | * wouldn't have kicked in. 250 | */ 251 | for ($i = ($ptrA + 1); $i < $ptrB; $i++) { 252 | $phpcsFile->fixer->replaceToken($i, ''); 253 | } 254 | 255 | // If necessary: add the correct amount whitespace. 256 | if ($expected !== 0) { 257 | if ($expected === 'newline') { 258 | $phpcsFile->fixer->addContent($ptrA, $phpcsFile->eolChar); 259 | } else { 260 | $replacement = $tokens[$ptrA]['content'] . \str_repeat(' ', $expected); 261 | $phpcsFile->fixer->replaceToken($ptrA, $replacement); 262 | } 263 | } 264 | 265 | $phpcsFile->fixer->endChangeset(); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /PHPCSUtils/Internal/Cache.php: -------------------------------------------------------------------------------- 1 | >>> 72 | * Format: $cache[$loop][$fileName][$key][$id] = mixed $value; 73 | */ 74 | private static $cache = []; 75 | 76 | /** 77 | * Check whether a result has been cached for a certain utility function. 78 | * 79 | * @since 1.0.0 80 | * 81 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 82 | * @param string $key The key to identify a particular set of results. 83 | * It is recommended to pass __METHOD__ to this parameter. 84 | * @param int|string $id Unique identifier for these results. 85 | * Generally speaking this will be the $stackPtr passed 86 | * to the utility function, but it can also something else, 87 | * like a serialization of args passed to a function or an 88 | * md5 hash of an input. 89 | * 90 | * @return bool 91 | */ 92 | public static function isCached(File $phpcsFile, $key, $id) 93 | { 94 | if (self::$enabled === false) { 95 | return false; 96 | } 97 | 98 | $fileName = $phpcsFile->getFilename(); 99 | $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; 100 | 101 | return isset(self::$cache[$loop][$fileName][$key]) 102 | && \array_key_exists($id, self::$cache[$loop][$fileName][$key]); 103 | } 104 | 105 | /** 106 | * Retrieve a previously cached result for a certain utility function. 107 | * 108 | * @since 1.0.0 109 | * 110 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 111 | * @param string $key The key to identify a particular set of results. 112 | * It is recommended to pass __METHOD__ to this parameter. 113 | * @param int|string $id Unique identifier for these results. 114 | * Generally speaking this will be the $stackPtr passed 115 | * to the utility function, but it can also something else, 116 | * like a serialization of args passed to a function or an 117 | * md5 hash of an input. 118 | * 119 | * @return mixed 120 | */ 121 | public static function get(File $phpcsFile, $key, $id) 122 | { 123 | if (self::$enabled === false) { 124 | return null; 125 | } 126 | 127 | $fileName = $phpcsFile->getFilename(); 128 | $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; 129 | 130 | if (isset(self::$cache[$loop][$fileName][$key]) 131 | && \array_key_exists($id, self::$cache[$loop][$fileName][$key]) 132 | ) { 133 | return self::$cache[$loop][$fileName][$key][$id]; 134 | } 135 | 136 | return null; 137 | } 138 | 139 | /** 140 | * Retrieve all previously cached results for a certain utility function and a certain file. 141 | * 142 | * @since 1.0.0 143 | * 144 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 145 | * @param string $key The key to identify a particular set of results. 146 | * It is recommended to pass __METHOD__ to this parameter. 147 | * 148 | * @return array 149 | */ 150 | public static function getForFile(File $phpcsFile, $key) 151 | { 152 | if (self::$enabled === false) { 153 | return []; 154 | } 155 | 156 | $fileName = $phpcsFile->getFilename(); 157 | $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; 158 | 159 | if (isset(self::$cache[$loop][$fileName]) 160 | && \array_key_exists($key, self::$cache[$loop][$fileName]) 161 | ) { 162 | return self::$cache[$loop][$fileName][$key]; 163 | } 164 | 165 | return []; 166 | } 167 | 168 | /** 169 | * Cache the result for a certain utility function. 170 | * 171 | * @since 1.0.0 172 | * 173 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 174 | * @param string $key The key to identify a particular set of results. 175 | * It is recommended to pass __METHOD__ to this parameter. 176 | * @param int|string $id Unique identifier for these results. 177 | * Generally speaking this will be the $stackPtr passed 178 | * to the utility function, but it can also something else, 179 | * like a serialization of args passed to a function or an 180 | * md5 hash of an input. 181 | * @param mixed $value An arbitrary value to write to the cache. 182 | * 183 | * @return mixed 184 | */ 185 | public static function set(File $phpcsFile, $key, $id, $value) 186 | { 187 | if (self::$enabled === false) { 188 | return; 189 | } 190 | 191 | $fileName = $phpcsFile->getFilename(); 192 | $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; 193 | 194 | /* 195 | * If this is a phpcbf run and we've reached the next loop, clear the cache 196 | * of all previous loops to free up memory. 197 | */ 198 | if (isset(self::$cache[$loop]) === false 199 | && empty(self::$cache) === false 200 | ) { 201 | self::clear(); 202 | } 203 | 204 | self::$cache[$loop][$fileName][$key][$id] = $value; 205 | } 206 | 207 | /** 208 | * Clear the cache. 209 | * 210 | * @since 1.0.0 211 | * 212 | * @return void 213 | */ 214 | public static function clear() 215 | { 216 | self::$cache = []; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /PHPCSUtils/Internal/IsShortArrayOrListWithCache.php: -------------------------------------------------------------------------------- 1 | > 62 | */ 63 | private $tokens; 64 | 65 | /** 66 | * The current stack pointer. 67 | * 68 | * @since 1.0.0 69 | * 70 | * @var int 71 | */ 72 | private $stackPtr; 73 | 74 | /** 75 | * Stack pointer to the open bracket. 76 | * 77 | * @since 1.0.0 78 | * 79 | * @var int 80 | */ 81 | private $opener; 82 | 83 | /** 84 | * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short array construct 85 | * and not a short list. 86 | * 87 | * This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be 88 | * PHPCS cross-version compatible as the short array tokenizing has been plagued by 89 | * a number of bugs over time. 90 | * 91 | * @since 1.0.0 92 | * 93 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 94 | * @param int $stackPtr The position of the short array bracket token. 95 | * 96 | * @return bool `TRUE` if the token passed is the open/close bracket of a short array. 97 | * `FALSE` if the token is a short list bracket, a plain square bracket 98 | * or not one of the accepted tokens. 99 | */ 100 | public static function isShortArray(File $phpcsFile, $stackPtr) 101 | { 102 | return (self::getType($phpcsFile, $stackPtr) === IsShortArrayOrList::SHORT_ARRAY); 103 | } 104 | 105 | /** 106 | * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short list construct 107 | * in contrast to a short array. 108 | * 109 | * This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be 110 | * PHPCS cross-version compatible as the short array tokenizing has been plagued by 111 | * a number of bugs over time, which affects the short list determination. 112 | * 113 | * @since 1.0.0 114 | * 115 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 116 | * @param int $stackPtr The position of the short array bracket token. 117 | * 118 | * @return bool `TRUE` if the token passed is the open/close bracket of a short list. 119 | * `FALSE` if the token is a short array bracket or plain square bracket 120 | * or not one of the accepted tokens. 121 | */ 122 | public static function isShortList(File $phpcsFile, $stackPtr) 123 | { 124 | return (self::getType($phpcsFile, $stackPtr) === IsShortArrayOrList::SHORT_LIST); 125 | } 126 | 127 | /** 128 | * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short array or short list construct. 129 | * 130 | * This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be 131 | * PHPCS cross-version compatible as the short array tokenizing has been plagued by 132 | * a number of bugs over time, which affects the short list determination. 133 | * 134 | * @since 1.0.0 135 | * 136 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 137 | * @param int $stackPtr The position of the short array bracket token. 138 | * 139 | * @return string|false The type of construct this bracket was determined to be. 140 | * Either 'short array', 'short list' or 'square brackets'. 141 | * Or FALSE if this was not a bracket token. 142 | */ 143 | public static function getType(File $phpcsFile, $stackPtr) 144 | { 145 | return (new self($phpcsFile, $stackPtr))->process(); 146 | } 147 | 148 | /** 149 | * Constructor. 150 | * 151 | * @since 1.0.0 152 | * 153 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 154 | * @param int $stackPtr The position of the short array bracket token. 155 | * 156 | * @return void 157 | */ 158 | private function __construct(File $phpcsFile, $stackPtr) 159 | { 160 | $this->phpcsFile = $phpcsFile; 161 | $this->tokens = $phpcsFile->getTokens(); 162 | $this->stackPtr = $stackPtr; 163 | } 164 | 165 | /** 166 | * Determine whether a T_[OPEN|CLOSE}_[SHORT_ARRAY|SQUARE_BRACKET] token is a short array 167 | * or short list construct using previously cached results whenever possible. 168 | * 169 | * @since 1.0.0 170 | * 171 | * @return string|false The type of construct this bracket was determined to be. 172 | * Either 'short array', 'short list' or 'square brackets'. 173 | * Or FALSE is this was not a bracket token. 174 | */ 175 | private function process() 176 | { 177 | if ($this->isValidStackPtr() === false) { 178 | return false; 179 | } 180 | 181 | $this->opener = $this->getOpener(); 182 | 183 | /* 184 | * Check the cache in case we've seen this token before. 185 | */ 186 | $type = $this->getFromCache(); 187 | if ($type !== false) { 188 | return $type; 189 | } 190 | 191 | /* 192 | * If we've not seen the token before, try and solve it and cache the results. 193 | * 194 | * Make sure to safeguard against unopened/unclosed square brackets (parse error), 195 | * which should always be regarded as real square brackets. 196 | */ 197 | $type = IsShortArrayOrList::SQUARE_BRACKETS; 198 | if (isset($this->tokens[$this->stackPtr]['bracket_opener'], $this->tokens[$this->stackPtr]['bracket_closer'])) { 199 | $solver = new IsShortArrayOrList($this->phpcsFile, $this->opener); 200 | $type = $solver->solve(); 201 | } 202 | 203 | $this->updateCache($type); 204 | 205 | return $type; 206 | } 207 | 208 | /** 209 | * Verify the passed token could potentially be a short array or short list token. 210 | * 211 | * @since 1.0.0 212 | * 213 | * @return bool 214 | */ 215 | private function isValidStackPtr() 216 | { 217 | return (isset($this->tokens[$this->stackPtr]) === true 218 | && isset(StableCollections::$shortArrayListTokensBC[$this->tokens[$this->stackPtr]['code']]) === true); 219 | } 220 | 221 | /** 222 | * Get the stack pointer to the short array/list opener. 223 | * 224 | * @since 1.0.0 225 | * 226 | * @return int 227 | */ 228 | private function getOpener() 229 | { 230 | $opener = $this->stackPtr; 231 | if (isset($this->tokens[$this->stackPtr]['bracket_opener'])) { 232 | $opener = $this->tokens[$this->stackPtr]['bracket_opener']; 233 | } 234 | 235 | return $opener; 236 | } 237 | 238 | /** 239 | * Retrieve the bracket "type" of a token from the cache. 240 | * 241 | * @since 1.0.0 242 | * 243 | * @return string|false The previously determined type (which could be an empty string) 244 | * or FALSE if no cache entry was found for this token. 245 | */ 246 | private function getFromCache() 247 | { 248 | if (Cache::isCached($this->phpcsFile, self::CACHE_KEY, $this->opener) === true) { 249 | return Cache::get($this->phpcsFile, self::CACHE_KEY, $this->opener); 250 | } 251 | 252 | return false; 253 | } 254 | 255 | /** 256 | * Update the cache with information about a particular bracket token. 257 | * 258 | * @since 1.0.0 259 | * 260 | * @param string $type The type this bracket has been determined to be. 261 | * Either 'short array', 'short list' or 'square brackets'. 262 | * 263 | * @return void 264 | */ 265 | private function updateCache($type) 266 | { 267 | Cache::set($this->phpcsFile, self::CACHE_KEY, $this->opener, $type); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /PHPCSUtils/Internal/NoFileCache.php: -------------------------------------------------------------------------------- 1 | > Format: $cache[$key][$id] = mixed $value; 68 | */ 69 | private static $cache = []; 70 | 71 | /** 72 | * Check whether a result has been cached for a certain utility function. 73 | * 74 | * @since 1.0.0 75 | * 76 | * @param string $key The key to identify a particular set of results. 77 | * It is recommended to pass `__METHOD__` to this parameter. 78 | * @param int|string $id Unique identifier for these results. 79 | * It is recommended for this to be a serialization of arguments passed 80 | * to the function or an md5 hash of an input. 81 | * 82 | * @return bool 83 | */ 84 | public static function isCached($key, $id) 85 | { 86 | return self::$enabled === true && isset(self::$cache[$key]) && \array_key_exists($id, self::$cache[$key]); 87 | } 88 | 89 | /** 90 | * Retrieve a previously cached result for a certain utility function. 91 | * 92 | * @since 1.0.0 93 | * 94 | * @param string $key The key to identify a particular set of results. 95 | * It is recommended to pass `__METHOD__` to this parameter. 96 | * @param int|string $id Unique identifier for these results. 97 | * It is recommended for this to be a serialization of arguments passed 98 | * to the function or an md5 hash of an input. 99 | * 100 | * @return mixed 101 | */ 102 | public static function get($key, $id) 103 | { 104 | if (self::$enabled === true && isset(self::$cache[$key]) && \array_key_exists($id, self::$cache[$key])) { 105 | return self::$cache[$key][$id]; 106 | } 107 | 108 | return null; 109 | } 110 | 111 | /** 112 | * Retrieve all previously cached results for a certain utility function. 113 | * 114 | * @since 1.0.0 115 | * 116 | * @param string $key The key to identify a particular set of results. 117 | * It is recommended to pass `__METHOD__` to this parameter. 118 | * 119 | * @return array 120 | */ 121 | public static function getForKey($key) 122 | { 123 | if (self::$enabled === true && \array_key_exists($key, self::$cache)) { 124 | return self::$cache[$key]; 125 | } 126 | 127 | return []; 128 | } 129 | 130 | /** 131 | * Cache the result for a certain utility function. 132 | * 133 | * @since 1.0.0 134 | * 135 | * @param string $key The key to identify a particular set of results. 136 | * It is recommended to pass `__METHOD__` to this parameter. 137 | * @param int|string $id Unique identifier for these results. 138 | * It is recommended for this to be a serialization of arguments passed 139 | * to the function or an md5 hash of an input. 140 | * @param mixed $value An arbitrary value to write to the cache. 141 | * 142 | * @return mixed 143 | */ 144 | public static function set($key, $id, $value) 145 | { 146 | if (self::$enabled === false) { 147 | return; 148 | } 149 | 150 | self::$cache[$key][$id] = $value; 151 | } 152 | 153 | /** 154 | * Clear the cache. 155 | * 156 | * @since 1.0.0 157 | * 158 | * @return void 159 | */ 160 | public static function clear() 161 | { 162 | self::$cache = []; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /PHPCSUtils/Internal/StableCollections.php: -------------------------------------------------------------------------------- 1 | 50 | */ 51 | public static $shortArrayListOpenTokensBC = [ 52 | \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, 53 | \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, 54 | ]; 55 | 56 | /** 57 | * Tokens which are used for short lists. 58 | * 59 | * This array will ALWAYS include the `T_OPEN_SQUARE_BRACKET` and `T_CLOSE_SQUARE_BRACKET` tokens 60 | * to allow for handling intermittent tokenizer issues related to the retokenization to `T_OPEN_SHORT_ARRAY`. 61 | * 62 | * @internal 63 | * @ignore This array is only for internal use by PHPCSUtils and is not part of the public API. 64 | * 65 | * @since 1.0.2 66 | * 67 | * @var array 68 | */ 69 | public static $shortArrayListTokensBC = [ 70 | \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, 71 | \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, 72 | \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, 73 | \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, 74 | ]; 75 | } 76 | -------------------------------------------------------------------------------- /PHPCSUtils/TestUtils/ConfigDouble.php: -------------------------------------------------------------------------------- 1 | $cliArgs An array of values gathered from CLI args. 57 | * @param bool $skipSettingStandard Whether to skip setting a standard to prevent 58 | * the Config class trying to auto-discover a ruleset file. 59 | * Should only be set to `true` for tests which actually test 60 | * the ruleset auto-discovery. 61 | * Note: there is no need to set this to `true` when a standard 62 | * is being passed via the `$cliArgs`. Those settings will always 63 | * respected. 64 | * Defaults to `false`. Will result in the standard being set 65 | * to "PSR1" if not provided via `$cliArgs`. 66 | * @param bool $skipSettingReportWidth Whether to skip setting a report-width to prevent 67 | * the Config class trying to auto-discover the screen width. 68 | * Should only be set to `true` for tests which actually test 69 | * the screen width auto-discovery. 70 | * Note: there is no need to set this to `true` when a report-width 71 | * is being passed via the `$cliArgs`. Those settings will always 72 | * respected. 73 | * Defaults to `false`. Will result in the reportWidth being set 74 | * to "80" if not provided via `$cliArgs`. 75 | * 76 | * @return void 77 | */ 78 | public function __construct(array $cliArgs = [], $skipSettingStandard = false, $skipSettingReportWidth = false) 79 | { 80 | $this->skipSettingStandard = $skipSettingStandard; 81 | $this->phpcsVersion = Helper::getVersion(); 82 | 83 | $this->resetSelectProperties(); 84 | $this->preventReadingCodeSnifferConfFile(); 85 | 86 | parent::__construct($cliArgs); 87 | 88 | if ($skipSettingReportWidth !== true) { 89 | $this->preventAutoDiscoveryScreenWidth(); 90 | } 91 | } 92 | 93 | /** 94 | * Ensures the static properties in the Config class are reset to their default values 95 | * when the ConfigDouble is no longer used. 96 | * 97 | * @since 1.1.0 98 | * 99 | * @return void 100 | */ 101 | public function __destruct() 102 | { 103 | $this->setStaticConfigProperty('overriddenDefaults', []); 104 | $this->setStaticConfigProperty('executablePaths', []); 105 | $this->setStaticConfigProperty('configData', null); 106 | $this->setStaticConfigProperty('configDataFile', null); 107 | } 108 | 109 | /** 110 | * Sets the command line values and optionally prevents a file system search for a custom ruleset. 111 | * 112 | * {@internal Note: `array` type declaration can't be added as the parent class does not have a type declaration 113 | * for the parameter in the original method.} 114 | * 115 | * @since 1.1.0 116 | * 117 | * @param array $args An array of command line arguments to set. 118 | * 119 | * @return void 120 | */ 121 | public function setCommandLineValues($args) 122 | { 123 | parent::setCommandLineValues($args); 124 | 125 | if ($this->skipSettingStandard !== true) { 126 | $this->preventSearchingForRuleset(); 127 | } 128 | } 129 | 130 | /** 131 | * Reset a few properties on the Config class to their default values. 132 | * 133 | * @since 1.1.0 134 | * 135 | * @return void 136 | */ 137 | private function resetSelectProperties() 138 | { 139 | $this->setStaticConfigProperty('overriddenDefaults', []); 140 | $this->setStaticConfigProperty('executablePaths', []); 141 | } 142 | 143 | /** 144 | * Prevent the values in a potentially available user-specific `CodeSniffer.conf` file 145 | * from influencing the tests. 146 | * 147 | * This also prevents some file system calls which can influence the test runtime. 148 | * 149 | * @since 1.1.0 150 | * 151 | * @return void 152 | */ 153 | private function preventReadingCodeSnifferConfFile() 154 | { 155 | $this->setStaticConfigProperty('configData', []); 156 | $this->setStaticConfigProperty('configDataFile', ''); 157 | } 158 | 159 | /** 160 | * Prevent searching for a custom ruleset by setting a standard, but only if the test 161 | * being run doesn't set a standard itself. 162 | * 163 | * This also prevents some file system calls which can influence the test runtime. 164 | * 165 | * The standard being set is the smallest one available so the ruleset initialization 166 | * will be the fastest possible. 167 | * 168 | * @since 1.1.0 169 | * 170 | * @return void 171 | */ 172 | private function preventSearchingForRuleset() 173 | { 174 | $overriddenDefaults = $this->getStaticConfigProperty('overriddenDefaults'); 175 | if (isset($overriddenDefaults['standards']) === false) { 176 | $this->standards = ['PSR1']; 177 | $overriddenDefaults['standards'] = true; 178 | } 179 | 180 | self::setStaticConfigProperty('overriddenDefaults', $overriddenDefaults); 181 | } 182 | 183 | /** 184 | * Prevent a call to stty to figure out the screen width, but only if the test being run 185 | * doesn't set a report width itself. 186 | * 187 | * @since 1.1.0 188 | * 189 | * @return void 190 | */ 191 | private function preventAutoDiscoveryScreenWidth() 192 | { 193 | $settings = $this->getSettings(); 194 | if ($settings['reportWidth'] === 'auto') { 195 | $this->reportWidth = self::DEFAULT_REPORT_WIDTH; 196 | } 197 | } 198 | 199 | /** 200 | * Helper function to retrieve the value of a private static property on the Config class. 201 | * 202 | * Note: As of PHPCS 4.0, the "overriddenDefaults" property is no longer static, but this method 203 | * will still handle this property. 204 | * 205 | * @since 1.1.0 206 | * 207 | * @param string $name The name of the property to retrieve. 208 | * 209 | * @return mixed 210 | */ 211 | public function getStaticConfigProperty($name) 212 | { 213 | $property = new ReflectionProperty('PHP_CodeSniffer\Config', $name); 214 | $property->setAccessible(true); 215 | 216 | if ($name === 'overriddenDefaults' && \version_compare($this->phpcsVersion, '3.99.99', '>')) { 217 | return $property->getValue($this); 218 | } 219 | 220 | return $property->getValue(); 221 | } 222 | 223 | /** 224 | * Helper function to set the value of a private static property on the Config class. 225 | * 226 | * Note: As of PHPCS 4.0, the "overriddenDefaults" property is no longer static, but this method 227 | * will still handle this property. 228 | * 229 | * @since 1.1.0 230 | * 231 | * @param string $name The name of the property to set. 232 | * @param mixed $value The value to set the property to. 233 | * 234 | * @return void 235 | */ 236 | public function setStaticConfigProperty($name, $value) 237 | { 238 | $property = new ReflectionProperty('PHP_CodeSniffer\Config', $name); 239 | $property->setAccessible(true); 240 | 241 | if ($name === 'overriddenDefaults' && \version_compare($this->phpcsVersion, '3.99.99', '>')) { 242 | $property->setValue($this, $value); 243 | } else { 244 | $property->setValue(null, $value); 245 | } 246 | 247 | $property->setAccessible(false); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /PHPCSUtils/Tokens/TokenHelper.php: -------------------------------------------------------------------------------- 1 | = 9.3 (which uses PHP-Parser), 29 | * this logic breaks because PHP-Parser also polyfills tokens. 30 | * This method takes potentially polyfilled tokens from PHP-Parser into account and will regard the token 31 | * as undefined if it was declared by PHP-Parser. 32 | * 33 | * Note: this method only _needs_ to be used for PHP native tokens, not for PHPCS specific tokens. 34 | * Also, realistically, it only needs to be used for tokens introduced in PHP in recent versions (PHP 7.4 and up). 35 | * Having said that, the method _will_ also work correctly when a name of a PHPCS native token is passed or 36 | * of an older PHP native token. 37 | * 38 | * {@internal PHP native tokens have a positive integer value. PHPCS polyfilled tokens are strings. 39 | * PHP-Parser polyfilled tokens will always have a negative integer value < 0, which is how 40 | * these are filtered out.} 41 | * 42 | * @link https://github.com/sebastianbergmann/php-code-coverage/issues/798 PHP-Code-Coverage#798 43 | * @link https://github.com/nikic/PHP-Parser/blob/master/lib/PhpParser/Lexer.php PHP-Parser Lexer code 44 | * 45 | * @since 1.0.0 46 | * 47 | * @param string $name The token name. 48 | * 49 | * @return bool 50 | */ 51 | public static function tokenExists($name) 52 | { 53 | return (\defined($name) && (\is_int(\constant($name)) === false || \constant($name) > 0)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Arrays.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | private static $doubleArrowTargets = [ 37 | \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, 38 | 39 | // Nested arrays. 40 | \T_ARRAY => \T_ARRAY, 41 | \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, 42 | 43 | // Inline function, control structures and other things to skip over. 44 | \T_LIST => \T_LIST, 45 | \T_FN => \T_FN, 46 | \T_MATCH => \T_MATCH, 47 | \T_ATTRIBUTE => \T_ATTRIBUTE, 48 | ]; 49 | 50 | /** 51 | * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short array construct 52 | * and not a short list. 53 | * 54 | * This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be 55 | * PHPCS cross-version compatible as the short array tokenizing has been plagued by 56 | * a number of bugs over time. 57 | * 58 | * @since 1.0.0 59 | * 60 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 61 | * @param int $stackPtr The position of the short array bracket token. 62 | * 63 | * @return bool `TRUE` if the token passed is the open/close bracket of a short array. 64 | * `FALSE` if the token is a short list bracket, a plain square bracket 65 | * or not one of the accepted tokens. 66 | */ 67 | public static function isShortArray(File $phpcsFile, $stackPtr) 68 | { 69 | return IsShortArrayOrListWithCache::isShortArray($phpcsFile, $stackPtr); 70 | } 71 | 72 | /** 73 | * Find the array opener and closer based on a T_ARRAY or T_OPEN_SHORT_ARRAY token. 74 | * 75 | * This method also accepts `T_OPEN_SQUARE_BRACKET` tokens to allow it to be 76 | * PHPCS cross-version compatible as the short array tokenizing has been plagued by 77 | * a number of bugs over time, which affects the short array determination. 78 | * 79 | * @since 1.0.0 80 | * 81 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 82 | * @param int $stackPtr The position of the `T_ARRAY` or `T_OPEN_SHORT_ARRAY` 83 | * token in the stack. 84 | * @param true|null $isShortArray Short-circuit the short array check for `T_OPEN_SHORT_ARRAY` 85 | * tokens if it isn't necessary. 86 | * Efficiency tweak for when this has already been established, 87 | * i.e. when encountering a nested array while walking the 88 | * tokens in an array. 89 | * Use with care. 90 | * 91 | * @return array|false An array with the token pointers; or `FALSE` if this is not a 92 | * (short) array token or if the opener/closer could not be determined. 93 | * The format of the array return value is: 94 | * ```php 95 | * array( 96 | * 'opener' => integer, // Stack pointer to the array open bracket. 97 | * 'closer' => integer, // Stack pointer to the array close bracket. 98 | * ) 99 | * ``` 100 | */ 101 | public static function getOpenClose(File $phpcsFile, $stackPtr, $isShortArray = null) 102 | { 103 | $tokens = $phpcsFile->getTokens(); 104 | 105 | // Is this one of the tokens this function handles ? 106 | if (isset($tokens[$stackPtr]) === false 107 | || isset(Collections::arrayOpenTokensBC()[$tokens[$stackPtr]['code']]) === false 108 | ) { 109 | return false; 110 | } 111 | 112 | switch ($tokens[$stackPtr]['code']) { 113 | case \T_ARRAY: 114 | if (isset($tokens[$stackPtr]['parenthesis_opener'])) { 115 | $opener = $tokens[$stackPtr]['parenthesis_opener']; 116 | 117 | if (isset($tokens[$opener]['parenthesis_closer'])) { 118 | $closer = $tokens[$opener]['parenthesis_closer']; 119 | } 120 | } 121 | break; 122 | 123 | case \T_OPEN_SHORT_ARRAY: 124 | case \T_OPEN_SQUARE_BRACKET: 125 | if ($isShortArray === true || self::isShortArray($phpcsFile, $stackPtr) === true) { 126 | $opener = $stackPtr; 127 | $closer = $tokens[$stackPtr]['bracket_closer']; 128 | } 129 | break; 130 | } 131 | 132 | if (isset($opener, $closer)) { 133 | return [ 134 | 'opener' => $opener, 135 | 'closer' => $closer, 136 | ]; 137 | } 138 | 139 | return false; 140 | } 141 | 142 | /** 143 | * Get the stack pointer position of the double arrow within an array item. 144 | * 145 | * Expects to be passed the array item start and end tokens as retrieved via 146 | * {@see \PHPCSUtils\Utils\PassedParameters::getParameters()}. 147 | * 148 | * @since 1.0.0 149 | * 150 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being examined. 151 | * @param int $start Stack pointer to the start of the array item. 152 | * @param int $end Stack pointer to the last token in the array item. 153 | * 154 | * @return int|false Stack pointer to the double arrow if this array item has a key; or `FALSE` otherwise. 155 | * 156 | * @throws \PHPCSUtils\Exceptions\TypeError If the $start or $end parameters are not integers. 157 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the tokens passed do not exist in the $phpcsFile. 158 | * @throws \PHPCSUtils\Exceptions\LogicException If $end pointer is before the $start pointer. 159 | */ 160 | public static function getDoubleArrowPtr(File $phpcsFile, $start, $end) 161 | { 162 | $tokens = $phpcsFile->getTokens(); 163 | 164 | if (\is_int($start) === false) { 165 | throw TypeError::create(2, '$start', 'integer', $start); 166 | } 167 | 168 | if (\is_int($end) === false) { 169 | throw TypeError::create(3, '$end', 'integer', $end); 170 | } 171 | 172 | if (isset($tokens[$start]) === false) { 173 | throw OutOfBoundsStackPtr::create(2, '$start', $start); 174 | } 175 | 176 | if (isset($tokens[$end]) === false) { 177 | throw OutOfBoundsStackPtr::create(3, '$end', $end); 178 | } 179 | 180 | if ($start > $end) { 181 | throw LogicException::create( 182 | \sprintf('The $start token must be before the $end token. Received: $start %d, $end %d', $start, $end) 183 | ); 184 | } 185 | 186 | $cacheId = "$start-$end"; 187 | if (Cache::isCached($phpcsFile, __METHOD__, $cacheId) === true) { 188 | return Cache::get($phpcsFile, __METHOD__, $cacheId); 189 | } 190 | 191 | $targets = self::$doubleArrowTargets; 192 | $targets += Collections::closedScopes(); 193 | 194 | $doubleArrow = ($start - 1); 195 | $returnValue = false; 196 | ++$end; 197 | do { 198 | $doubleArrow = $phpcsFile->findNext( 199 | $targets, 200 | ($doubleArrow + 1), 201 | $end 202 | ); 203 | 204 | if ($doubleArrow === false) { 205 | break; 206 | } 207 | 208 | if ($tokens[$doubleArrow]['code'] === \T_DOUBLE_ARROW) { 209 | $returnValue = $doubleArrow; 210 | break; 211 | } 212 | 213 | // Skip over closed scopes which may contain foreach structures or generators. 214 | if ((isset(Collections::closedScopes()[$tokens[$doubleArrow]['code']]) === true 215 | || $tokens[$doubleArrow]['code'] === \T_FN 216 | || $tokens[$doubleArrow]['code'] === \T_MATCH) 217 | && isset($tokens[$doubleArrow]['scope_closer']) === true 218 | ) { 219 | $doubleArrow = $tokens[$doubleArrow]['scope_closer']; 220 | continue; 221 | } 222 | 223 | // Skip over attributes which may contain arrays as a passed parameters. 224 | if ($tokens[$doubleArrow]['code'] === \T_ATTRIBUTE 225 | && isset($tokens[$doubleArrow]['attribute_closer']) 226 | ) { 227 | $doubleArrow = $tokens[$doubleArrow]['attribute_closer']; 228 | continue; 229 | } 230 | 231 | // Skip over potentially keyed long lists. 232 | if ($tokens[$doubleArrow]['code'] === \T_LIST 233 | && isset($tokens[$doubleArrow]['parenthesis_closer']) 234 | ) { 235 | $doubleArrow = $tokens[$doubleArrow]['parenthesis_closer']; 236 | continue; 237 | } 238 | 239 | // Start of nested long/short array. 240 | break; 241 | } while ($doubleArrow < $end); 242 | 243 | Cache::set($phpcsFile, __METHOD__, $cacheId, $returnValue); 244 | return $returnValue; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Conditions.php: -------------------------------------------------------------------------------- 1 | $types Optional. The type(s) of tokens to search for. 45 | * @param bool $first Optional. Whether to search for the first (outermost) 46 | * (`true`) or the last (innermost) condition (`false`) of 47 | * the specified type(s). 48 | * 49 | * @return int|false Integer stack pointer to the condition; or `FALSE` if the token 50 | * does not have the condition or has no conditions at all. 51 | */ 52 | public static function getCondition(File $phpcsFile, $stackPtr, $types = [], $first = true) 53 | { 54 | $tokens = $phpcsFile->getTokens(); 55 | 56 | // Check for the existence of the token. 57 | if (isset($tokens[$stackPtr]) === false) { 58 | return false; 59 | } 60 | 61 | // Make sure the token has conditions. 62 | if (empty($tokens[$stackPtr]['conditions'])) { 63 | return false; 64 | } 65 | 66 | $types = (array) $types; 67 | $conditions = $tokens[$stackPtr]['conditions']; 68 | 69 | if (empty($types) === true) { 70 | // No types specified, just return the first/last condition pointer. 71 | if ($first === false) { 72 | \end($conditions); 73 | } else { 74 | \reset($conditions); 75 | } 76 | 77 | return \key($conditions); 78 | } 79 | 80 | if ($first === false) { 81 | $conditions = \array_reverse($conditions, true); 82 | } 83 | 84 | foreach ($conditions as $ptr => $type) { 85 | if (isset($tokens[$ptr]) === true 86 | && \in_array($type, $types, true) === true 87 | ) { 88 | // We found a token with the required type. 89 | return $ptr; 90 | } 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Determine if the passed token has a condition of one of the passed types. 98 | * 99 | * This method is not significantly different from the PHPCS native version. 100 | * 101 | * @see \PHP_CodeSniffer\Files\File::hasCondition() Original source. 102 | * @see \PHPCSUtils\BackCompat\BCFile::hasCondition() Cross-version compatible version of the original. 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 token we are checking. 108 | * @param int|string|array $types The type(s) of tokens to search for. 109 | * 110 | * @return bool 111 | */ 112 | public static function hasCondition(File $phpcsFile, $stackPtr, $types) 113 | { 114 | return (self::getCondition($phpcsFile, $stackPtr, $types) !== false); 115 | } 116 | 117 | /** 118 | * Return the position of the first (outermost) condition of a certain type for the passed token. 119 | * 120 | * If no types are specified, the first condition for the token, independently of type, 121 | * will be returned. 122 | * 123 | * @since 1.0.0 124 | * 125 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 126 | * @param int $stackPtr The position of the token we are checking. 127 | * @param int|string|array $types Optional. The type(s) of tokens to search for. 128 | * 129 | * @return int|false Integer stack pointer to the condition; or `FALSE` if the token 130 | * does not have the condition or has no conditions at all. 131 | */ 132 | public static function getFirstCondition(File $phpcsFile, $stackPtr, $types = []) 133 | { 134 | return self::getCondition($phpcsFile, $stackPtr, $types, true); 135 | } 136 | 137 | /** 138 | * Return the position of the last (innermost) condition of a certain type for the passed token. 139 | * 140 | * If no types are specified, the last condition for the token, independently of type, 141 | * will be returned. 142 | * 143 | * @since 1.0.0 144 | * 145 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 146 | * @param int $stackPtr The position of the token we are checking. 147 | * @param int|string|array $types Optional. The type(s) of tokens to search for. 148 | * 149 | * @return int|false Integer stack pointer to the condition; or `FALSE` if the token 150 | * does not have the condition or has no conditions at all. 151 | */ 152 | public static function getLastCondition(File $phpcsFile, $stackPtr, $types = []) 153 | { 154 | return self::getCondition($phpcsFile, $stackPtr, $types, false); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Constants.php: -------------------------------------------------------------------------------- 1 | Array with information about the constant declaration. 41 | * The format of the return value is: 42 | * ```php 43 | * array( 44 | * 'scope' => string, // Public, private, or protected. 45 | * 'scope_token' => integer|false, // The stack pointer to the scope keyword or 46 | * // FALSE if the scope was not explicitly specified. 47 | * 'is_final' => boolean, // TRUE if the final keyword was found. 48 | * 'final_token' => integer|false, // The stack pointer to the final keyword 49 | * // or FALSE if the const is not declared final. 50 | * 'type' => string, // The type of the const (empty if no type specified). 51 | * 'type_token' => integer|false, // The stack pointer to the start of the type 52 | * // or FALSE if there is no type. 53 | * 'type_end_token' => integer|false, // The stack pointer to the end of the type 54 | * // or FALSE if there is no type. 55 | * 'nullable_type' => boolean, // TRUE if the type is preceded by the 56 | * // nullability operator. 57 | * 'name_token' => integer, // The stack pointer to the constant name. 58 | * // Note: for group declarations this points to the 59 | * // name of the first constant. 60 | * 'equal_token' => integer, // The stack pointer to the equal sign. 61 | * // Note: for group declarations this points to the 62 | * // equal sign of the first constant. 63 | * ); 64 | * ``` 65 | * 66 | * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. 67 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. 68 | * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CONST` token. 69 | * @throws \PHPCSUtils\Exceptions\ValueError If the specified position is not an OO constant. 70 | */ 71 | public static function getProperties(File $phpcsFile, $stackPtr) 72 | { 73 | $tokens = $phpcsFile->getTokens(); 74 | 75 | if (\is_int($stackPtr) === false) { 76 | throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); 77 | } 78 | 79 | if (isset($tokens[$stackPtr]) === false) { 80 | throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); 81 | } 82 | 83 | if ($tokens[$stackPtr]['code'] !== \T_CONST) { 84 | throw UnexpectedTokenType::create(2, '$stackPtr', 'T_CONST', $tokens[$stackPtr]['type']); 85 | } 86 | 87 | if (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) { 88 | throw ValueError::create(2, '$stackPtr', 'must be the pointer to an OO constant'); 89 | } 90 | 91 | if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { 92 | return Cache::get($phpcsFile, __METHOD__, $stackPtr); 93 | } 94 | 95 | $find = [\T_EQUAL, \T_SEMICOLON, \T_OPEN_CURLY_BRACKET, \T_CLOSE_CURLY_BRACKET, \T_CLOSE_TAG]; 96 | $assignmentPtr = $phpcsFile->findNext($find, ($stackPtr + 1)); 97 | if ($assignmentPtr === false || $tokens[$assignmentPtr]['code'] !== \T_EQUAL) { 98 | // Probably a parse error. Don't cache the result. 99 | throw ValueError::create(2, '$stackPtr', 'must be the pointer to an OO constant'); 100 | } 101 | 102 | $namePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentPtr - 1), ($stackPtr + 1), true); 103 | 104 | $returnValue = [ 105 | 'scope' => 'public', 106 | 'scope_token' => false, 107 | 'is_final' => false, 108 | 'final_token' => false, 109 | 'type' => '', 110 | 'type_token' => false, 111 | 'type_end_token' => false, 112 | 'nullable_type' => false, 113 | 'name_token' => $namePtr, 114 | 'equal_token' => $assignmentPtr, 115 | ]; 116 | 117 | for ($i = ($stackPtr - 1);; $i--) { 118 | // Skip over potentially large docblocks. 119 | if ($tokens[$i]['code'] === \T_DOC_COMMENT_CLOSE_TAG 120 | && isset($tokens[$i]['comment_opener']) 121 | ) { 122 | $i = $tokens[$i]['comment_opener']; 123 | continue; 124 | } 125 | 126 | if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { 127 | continue; 128 | } 129 | 130 | switch ($tokens[$i]['code']) { 131 | case \T_PUBLIC: 132 | $returnValue['scope'] = 'public'; 133 | $returnValue['scope_token'] = $i; 134 | break; 135 | 136 | case \T_PROTECTED: 137 | $returnValue['scope'] = 'protected'; 138 | $returnValue['scope_token'] = $i; 139 | break; 140 | 141 | case \T_PRIVATE: 142 | $returnValue['scope'] = 'private'; 143 | $returnValue['scope_token'] = $i; 144 | break; 145 | 146 | case \T_FINAL: 147 | $returnValue['is_final'] = true; 148 | $returnValue['final_token'] = $i; 149 | break; 150 | 151 | default: 152 | // Any other token means that the start of the statement has been reached. 153 | break 2; 154 | } 155 | } 156 | 157 | $type = ''; 158 | $typeToken = false; 159 | $typeEndToken = false; 160 | $constantTypeTokens = Collections::constantTypeTokens(); 161 | 162 | // Now, let's check for a type. 163 | for ($i = ($stackPtr + 1); $i < $namePtr; $i++) { 164 | if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { 165 | continue; 166 | } 167 | 168 | if ($tokens[$i]['code'] === \T_NULLABLE) { 169 | $returnValue['nullable_type'] = true; 170 | continue; 171 | } 172 | 173 | if (isset($constantTypeTokens[$tokens[$i]['code']]) === true) { 174 | $typeEndToken = $i; 175 | if ($typeToken === false) { 176 | $typeToken = $i; 177 | } 178 | 179 | $type .= $tokens[$i]['content']; 180 | } 181 | } 182 | 183 | if ($type !== '' && $returnValue['nullable_type'] === true) { 184 | $type = '?' . $type; 185 | } 186 | 187 | $returnValue['type'] = $type; 188 | $returnValue['type_token'] = $typeToken; 189 | $returnValue['type_end_token'] = $typeEndToken; 190 | 191 | Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue); 192 | return $returnValue; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Context.php: -------------------------------------------------------------------------------- 1 | getTokens(); 100 | 101 | // Check for the existence of the token. 102 | if (isset($tokens[$stackPtr]) === false) { 103 | return false; 104 | } 105 | 106 | if (isset($tokens[$stackPtr]['attribute_opener'], $tokens[$stackPtr]['attribute_closer']) === false) { 107 | return false; 108 | } 109 | 110 | return ($stackPtr !== $tokens[$stackPtr]['attribute_opener'] 111 | && $stackPtr !== $tokens[$stackPtr]['attribute_closer']); 112 | } 113 | 114 | /** 115 | * Check whether an arbitrary token is in a foreach condition and if so, in which part: 116 | * before or after the "as". 117 | * 118 | * @since 1.0.0 119 | * 120 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 121 | * @param int $stackPtr The position of the token we are checking. 122 | * 123 | * @return string|false String `'beforeAs'`, `'as'` or `'afterAs'` when the token is within 124 | * a `foreach` condition. 125 | * `FALSE` in all other cases, including for parse errors. 126 | */ 127 | public static function inForeachCondition(File $phpcsFile, $stackPtr) 128 | { 129 | $tokens = $phpcsFile->getTokens(); 130 | 131 | // Check for the existence of the token. 132 | if (isset($tokens[$stackPtr]) === false) { 133 | return false; 134 | } 135 | 136 | $foreach = Parentheses::getLastOwner($phpcsFile, $stackPtr, \T_FOREACH); 137 | if ($foreach === false) { 138 | return false; 139 | } 140 | 141 | $opener = $tokens[$foreach]['parenthesis_opener']; 142 | $closer = $tokens[$foreach]['parenthesis_closer']; 143 | 144 | for ($i = ($opener + 1); $i < $closer; $i++) { 145 | // Skip past a short array declaration in the "before as" part. 146 | if (isset($tokens[$i]['bracket_closer'])) { 147 | $i = $tokens[$i]['bracket_closer']; 148 | continue; 149 | } 150 | 151 | // Skip past a long array declaration in the "before as" part. 152 | if (isset($tokens[$i]['parenthesis_closer'])) { 153 | $i = $tokens[$i]['parenthesis_closer']; 154 | continue; 155 | } 156 | 157 | if ($tokens[$i]['code'] === \T_AS) { 158 | $asPtr = $i; 159 | break; 160 | } 161 | } 162 | 163 | if (isset($asPtr) === false) { 164 | // Parse error or live coding. 165 | return false; 166 | } 167 | 168 | if ($asPtr === $stackPtr) { 169 | return 'as'; 170 | } 171 | 172 | if ($stackPtr < $asPtr) { 173 | return 'beforeAs'; 174 | } 175 | 176 | return 'afterAs'; 177 | } 178 | 179 | /** 180 | * Check whether an arbitrary token is in a for condition and if so, in which part: 181 | * the first, second or third expression. 182 | * 183 | * Note: the semicolons separating the conditions are regarded as belonging with the 184 | * expression before it. 185 | * 186 | * @since 1.0.0 187 | * 188 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 189 | * @param int $stackPtr The position of the token we are checking. 190 | * 191 | * @return string|false String `'expr1'`, `'expr2'` or `'expr3'` when the token is within 192 | * a `for` condition. 193 | * `FALSE` in all other cases, including for parse errors. 194 | */ 195 | public static function inForCondition(File $phpcsFile, $stackPtr) 196 | { 197 | $tokens = $phpcsFile->getTokens(); 198 | 199 | // Check for the existence of the token. 200 | if (isset($tokens[$stackPtr]) === false) { 201 | return false; 202 | } 203 | 204 | $for = Parentheses::getLastOwner($phpcsFile, $stackPtr, \T_FOR); 205 | if ($for === false) { 206 | return false; 207 | } 208 | 209 | $semicolons = []; 210 | $count = 0; 211 | $opener = $tokens[$for]['parenthesis_opener']; 212 | $closer = $tokens[$for]['parenthesis_closer']; 213 | $level = $tokens[$for]['level']; 214 | $parens = 1; 215 | 216 | if (isset($tokens[$for]['nested_parenthesis'])) { 217 | $parens = (\count($tokens[$for]['nested_parenthesis']) + 1); 218 | } 219 | 220 | for ($i = ($opener + 1); $i < $closer; $i++) { 221 | if ($tokens[$i]['code'] !== \T_SEMICOLON) { 222 | continue; 223 | } 224 | 225 | if ($tokens[$i]['level'] !== $level 226 | || \count($tokens[$i]['nested_parenthesis']) !== $parens 227 | ) { 228 | // Disregard semicolons at lower nesting/condition levels. 229 | continue; 230 | } 231 | 232 | ++$count; 233 | $semicolons[$count] = $i; 234 | } 235 | 236 | if ($count !== 2) { 237 | return false; 238 | } 239 | 240 | foreach ($semicolons as $key => $ptr) { 241 | if ($stackPtr <= $ptr) { 242 | return 'expr' . $key; 243 | } 244 | } 245 | 246 | return 'expr3'; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/ControlStructures.php: -------------------------------------------------------------------------------- 1 | getTokens(); 55 | 56 | // Check for the existence of the token. 57 | if (isset($tokens[$stackPtr]) === false 58 | || isset(Collections::controlStructureTokens()[$tokens[$stackPtr]['code']]) === false 59 | ) { 60 | return false; 61 | } 62 | 63 | // Handle `else if`. 64 | if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === false) { 65 | $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 66 | if ($next !== false && $tokens[$next]['code'] === \T_IF) { 67 | $stackPtr = $next; 68 | } 69 | } 70 | 71 | /* 72 | * The scope markers are set. This is the simplest situation. 73 | */ 74 | if (isset($tokens[$stackPtr]['scope_opener']) === true) { 75 | if ($allowEmpty === true) { 76 | return true; 77 | } 78 | 79 | // Check whether the body is empty. 80 | $start = ($tokens[$stackPtr]['scope_opener'] + 1); 81 | $end = $phpcsFile->numTokens; 82 | if (isset($tokens[$stackPtr]['scope_closer']) === true) { 83 | $end = $tokens[$stackPtr]['scope_closer']; 84 | } 85 | 86 | $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true); 87 | if ($nextNonEmpty !== false) { 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | /* 95 | * Control structure without scope markers. 96 | * Either single line statement or inline control structure. 97 | * 98 | * - Single line statement doesn't have a body and is therefore always empty. 99 | * - Inline control structure has to have a body and can never be empty. 100 | * 101 | * This code also needs to take live coding into account where a scope opener is found, but 102 | * no scope closer. 103 | */ 104 | $searchStart = ($stackPtr + 1); 105 | if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { 106 | $searchStart = ($tokens[$stackPtr]['parenthesis_closer'] + 1); 107 | } 108 | 109 | $nextNonEmpty = $phpcsFile->findNext( 110 | Tokens::$emptyTokens, 111 | $searchStart, 112 | null, 113 | true 114 | ); 115 | if ($nextNonEmpty === false 116 | || $tokens[$nextNonEmpty]['code'] === \T_SEMICOLON 117 | || $tokens[$nextNonEmpty]['code'] === \T_CLOSE_TAG 118 | ) { 119 | // Parse error or single line statement. 120 | return false; 121 | } 122 | 123 | if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_CURLY_BRACKET) { 124 | if ($allowEmpty === true) { 125 | return true; 126 | } 127 | 128 | // Unrecognized scope opener due to parse error. 129 | $nextNext = $phpcsFile->findNext( 130 | Tokens::$emptyTokens, 131 | ($nextNonEmpty + 1), 132 | null, 133 | true 134 | ); 135 | 136 | if ($nextNext === false) { 137 | return false; 138 | } 139 | 140 | return true; 141 | } 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * Check whether an IF or ELSE token is part of an "else if". 148 | * 149 | * @since 1.0.0 150 | * 151 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 152 | * @param int $stackPtr The position of the token we are checking. 153 | * 154 | * @return bool 155 | */ 156 | public static function isElseIf(File $phpcsFile, $stackPtr) 157 | { 158 | $tokens = $phpcsFile->getTokens(); 159 | 160 | // Check for the existence of the token. 161 | if (isset($tokens[$stackPtr]) === false) { 162 | return false; 163 | } 164 | 165 | if ($tokens[$stackPtr]['code'] === \T_ELSEIF) { 166 | return true; 167 | } 168 | 169 | if ($tokens[$stackPtr]['code'] !== \T_ELSE && $tokens[$stackPtr]['code'] !== \T_IF) { 170 | return false; 171 | } 172 | 173 | if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === true) { 174 | return false; 175 | } 176 | 177 | switch ($tokens[$stackPtr]['code']) { 178 | case \T_ELSE: 179 | $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 180 | if ($next !== false && $tokens[$next]['code'] === \T_IF) { 181 | return true; 182 | } 183 | break; 184 | 185 | case \T_IF: 186 | $previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 187 | if ($previous !== false && $tokens[$previous]['code'] === \T_ELSE) { 188 | return true; 189 | } 190 | break; 191 | } 192 | 193 | return false; 194 | } 195 | 196 | /** 197 | * Retrieve the exception(s) being caught in a CATCH condition. 198 | * 199 | * @since 1.0.0 200 | * 201 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 202 | * @param int $stackPtr The position of the token we are checking. 203 | * 204 | * @return array> 205 | * Array with information about the caught Exception(s). 206 | * The returned array will contain the following information for 207 | * each caught exception: 208 | * ```php 209 | * 0 => array( 210 | * 'type' => string, // The type declaration for the exception being caught. 211 | * 'type_token' => integer, // The stack pointer to the start of the type declaration. 212 | * 'type_end_token' => integer, // The stack pointer to the end of the type declaration. 213 | * ) 214 | * ``` 215 | * In case of an invalid catch structure, the array may be empty. 216 | * 217 | * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. 218 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. 219 | * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CATCH` token. 220 | */ 221 | public static function getCaughtExceptions(File $phpcsFile, $stackPtr) 222 | { 223 | $tokens = $phpcsFile->getTokens(); 224 | 225 | if (\is_int($stackPtr) === false) { 226 | throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); 227 | } 228 | 229 | if (isset($tokens[$stackPtr]) === false) { 230 | throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); 231 | } 232 | 233 | if ($tokens[$stackPtr]['code'] !== \T_CATCH) { 234 | throw UnexpectedTokenType::create(2, '$stackPtr', 'T_CATCH', $tokens[$stackPtr]['type']); 235 | } 236 | 237 | if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) { 238 | return []; 239 | } 240 | 241 | $opener = $tokens[$stackPtr]['parenthesis_opener']; 242 | $closer = $tokens[$stackPtr]['parenthesis_closer']; 243 | $exceptions = []; 244 | 245 | $foundName = ''; 246 | $firstToken = null; 247 | $lastToken = null; 248 | 249 | for ($i = ($opener + 1); $i <= $closer; $i++) { 250 | if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) { 251 | continue; 252 | } 253 | 254 | if (isset(Collections::namespacedNameTokens()[$tokens[$i]['code']]) === false) { 255 | // Add the current exception to the result array if one was found. 256 | if ($foundName !== '') { 257 | $exceptions[] = [ 258 | 'type' => $foundName, 259 | 'type_token' => $firstToken, 260 | 'type_end_token' => $lastToken, 261 | ]; 262 | } 263 | 264 | if ($tokens[$i]['code'] === \T_BITWISE_OR) { 265 | // Multi-catch. Reset and continue. 266 | $foundName = ''; 267 | $firstToken = null; 268 | $lastToken = null; 269 | continue; 270 | } 271 | 272 | break; 273 | } 274 | 275 | if (isset($firstToken) === false) { 276 | $firstToken = $i; 277 | } 278 | 279 | $foundName .= $tokens[$i]['content']; 280 | $lastToken = $i; 281 | } 282 | 283 | return $exceptions; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/FileInfo.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | private static $bomDefinitions = [ 33 | 'UTF-8' => 'efbbbf', 34 | 'UTF-16 (BE)' => 'feff', 35 | 'UTF-16 (LE)' => 'fffe', 36 | ]; 37 | 38 | /** 39 | * Determine whether the file under scan has a byte-order mark at the start. 40 | * 41 | * Inspired by similar code being used in a couple of PHPCS native sniffs. 42 | * 43 | * @since 1.1.0 44 | * 45 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 46 | * 47 | * @return string|false Name of the type of BOM found or FALSE when the file does not start with a BOM. 48 | */ 49 | public static function hasByteOrderMark(File $phpcsFile) 50 | { 51 | $tokens = $phpcsFile->getTokens(); 52 | 53 | if ($tokens[0]['code'] !== \T_INLINE_HTML) { 54 | return false; 55 | } 56 | 57 | foreach (self::$bomDefinitions as $bomName => $expectedBomHex) { 58 | $bomByteLength = (int) (\strlen($expectedBomHex) / 2); 59 | $htmlBomHex = \bin2hex(\substr($tokens[0]['content'], 0, $bomByteLength)); 60 | if ($htmlBomHex === $expectedBomHex) { 61 | return $bomName; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | 68 | /** 69 | * Determine whether the file under scan has a shebang line at the start. 70 | * 71 | * @since 1.1.0 72 | * 73 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 74 | * 75 | * @return bool 76 | */ 77 | public static function hasSheBang(File $phpcsFile) 78 | { 79 | $tokens = $phpcsFile->getTokens(); 80 | if ($tokens[0]['code'] !== \T_INLINE_HTML) { 81 | return false; 82 | } 83 | 84 | $start = 0; 85 | $hasByteOrderMark = self::hasByteOrderMark($phpcsFile); 86 | if ($hasByteOrderMark !== false) { 87 | $start = (int) (\strlen(self::$bomDefinitions[$hasByteOrderMark]) / 2); 88 | } 89 | 90 | return (\substr($tokens[0]['content'], $start, 2) === '#!'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/FilePath.php: -------------------------------------------------------------------------------- 1 | getFileName()); 48 | if ($fileName !== 'STDIN') { 49 | $fileName = self::normalizeAbsolutePath($fileName); 50 | } 51 | 52 | return \trim($fileName); 53 | } 54 | 55 | /** 56 | * Check whether the input was received via STDIN. 57 | * 58 | * @since 1.1.0 59 | * 60 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 61 | * 62 | * @return bool 63 | */ 64 | public static function isStdin(File $phpcsFile) 65 | { 66 | return (self::getName($phpcsFile) === 'STDIN'); 67 | } 68 | 69 | /** 70 | * Normalize an absolute path to forward slashes and to include a trailing slash for directories. 71 | * 72 | * @since 1.1.0 73 | * 74 | * @param string $path Absolute file or directory path. 75 | * 76 | * @return string 77 | */ 78 | public static function normalizeAbsolutePath($path) 79 | { 80 | return self::trailingSlashIt(self::normalizeDirectorySeparators($path)); 81 | } 82 | 83 | /** 84 | * Normalize all directory separators to be a forward slash. 85 | * 86 | * {@internal We cannot rely on the OS on which PHPCS is being run to determine the 87 | * the expected slashes, as the file name could also come from a text string in a 88 | * tokenized file or have been set by an IDE...} 89 | * 90 | * @since 1.1.0 91 | * 92 | * @param string $path File or directory path. 93 | * 94 | * @return string 95 | */ 96 | public static function normalizeDirectorySeparators($path) 97 | { 98 | return \strtr((string) $path, '\\', '/'); 99 | } 100 | 101 | /** 102 | * Ensure that a directory path ends on a trailing slash. 103 | * 104 | * Includes safeguard against adding a trailing slash to path ending on a file name. 105 | * 106 | * @since 1.1.0 107 | * 108 | * @param string $path File or directory path. 109 | * 110 | * @return string 111 | */ 112 | public static function trailingSlashIt($path) 113 | { 114 | if (\is_string($path) === false || $path === '') { 115 | return ''; 116 | } 117 | 118 | $extension = ''; 119 | $lastChar = \substr($path, -1); 120 | if ($lastChar !== '/' && $lastChar !== '\\') { 121 | // This may be a file, check if it has a file extension. 122 | $extension = \pathinfo($path, \PATHINFO_EXTENSION); 123 | } 124 | 125 | if ($extension !== '') { 126 | return $path; 127 | } 128 | 129 | return \rtrim((string) $path, '/\\') . '/'; 130 | } 131 | 132 | /** 133 | * Check whether one file/directory path starts with another path. 134 | * 135 | * Recommended to be used only when both paths are absolute. 136 | * 137 | * Note: this function does not normalize paths prior to comparing them. 138 | * If this is needed, normalization should be done prior to passing 139 | * the `$haystack` and `$needle` parameters to this function. 140 | * 141 | * Also note that this function does a case-sensitive comparison as most OS-es are case-sensitive. 142 | * 143 | * @since 1.1.0 144 | * 145 | * @param string $haystack Path to examine. 146 | * @param string $needle Partial path which the haystack path should start with. 147 | * 148 | * @return bool 149 | */ 150 | public static function startsWith($haystack, $needle) 151 | { 152 | return (\strncmp($haystack, $needle, \strlen($needle)) === 0); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/GetTokensAsString.php: -------------------------------------------------------------------------------- 1 | getTokens(); 223 | 224 | if (\is_int($start) === false) { 225 | throw TypeError::create(2, '$start', 'integer', $start); 226 | } 227 | 228 | if (isset($tokens[$start]) === false) { 229 | throw OutOfBoundsStackPtr::create(2, '$start', $start); 230 | } 231 | 232 | if (\is_int($end) === false || $end < $start) { 233 | return ''; 234 | } 235 | 236 | $str = ''; 237 | if ($end >= $phpcsFile->numTokens) { 238 | $end = ($phpcsFile->numTokens - 1); 239 | } 240 | 241 | $lastAdded = null; 242 | for ($i = $start; $i <= $end; $i++) { 243 | if ($stripComments === true && isset(Tokens::$commentTokens[$tokens[$i]['code']])) { 244 | continue; 245 | } 246 | 247 | if ($stripWhitespace === true && $tokens[$i]['code'] === \T_WHITESPACE) { 248 | continue; 249 | } 250 | 251 | if ($compact === true && $tokens[$i]['code'] === \T_WHITESPACE) { 252 | if (isset($lastAdded) === false || $tokens[$lastAdded]['code'] !== \T_WHITESPACE) { 253 | $str .= ' '; 254 | $lastAdded = $i; 255 | } 256 | continue; 257 | } 258 | 259 | // If tabs are being converted to spaces by the tokenizer, the 260 | // original content should be used instead of the converted content. 261 | if ($origContent === true && isset($tokens[$i]['orig_content']) === true) { 262 | $str .= $tokens[$i]['orig_content']; 263 | } else { 264 | $str .= $tokens[$i]['content']; 265 | } 266 | 267 | $lastAdded = $i; 268 | } 269 | 270 | return $str; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/MessageHelper.php: -------------------------------------------------------------------------------- 1 | addError($message, $stackPtr, $code, $data, $severity); 54 | } 55 | 56 | return $phpcsFile->addWarning($message, $stackPtr, $code, $data, $severity); 57 | } 58 | 59 | /** 60 | * Add a PHPCS message to the output stack as either a fixable warning or a fixable error. 61 | * 62 | * @since 1.0.0 63 | * 64 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 65 | * @param string $message The message. 66 | * @param int $stackPtr The position of the token 67 | * the message relates to. 68 | * @param bool $isError Whether to report the message as an 69 | * 'error' or 'warning'. 70 | * Defaults to true (error). 71 | * @param string $code The error code for the message. 72 | * Defaults to 'Found'. 73 | * @param scalar[] $data Optional input for the data replacements. 74 | * @param int $severity Optional. Severity level. Defaults to 0 which will 75 | * translate to the PHPCS default severity level. 76 | * 77 | * @return bool 78 | */ 79 | public static function addFixableMessage( 80 | File $phpcsFile, 81 | $message, 82 | $stackPtr, 83 | $isError = true, 84 | $code = 'Found', 85 | array $data = [], 86 | $severity = 0 87 | ) { 88 | if ($isError === true) { 89 | return $phpcsFile->addFixableError($message, $stackPtr, $code, $data, $severity); 90 | } 91 | 92 | return $phpcsFile->addFixableWarning($message, $stackPtr, $code, $data, $severity); 93 | } 94 | 95 | /** 96 | * Convert an arbitrary text string to an alphanumeric string with underscores. 97 | * 98 | * Pre-empt issues in XML and PHP when arbitrary strings are being used as error codes. 99 | * 100 | * @since 1.0.0 101 | * 102 | * @param string $text Arbitrary text string intended to be used in an error code. 103 | * @param bool $strtolower Whether or not to convert the text string to lowercase. 104 | * 105 | * @return string 106 | */ 107 | public static function stringToErrorcode($text, $strtolower = false) 108 | { 109 | $text = \preg_replace('`[^a-z0-9_]`i', '_', $text); 110 | 111 | if ($strtolower === true) { 112 | $text = \strtolower($text); 113 | } 114 | 115 | return $text; 116 | } 117 | 118 | /** 119 | * Make the whitespace escape codes used in an arbitrary text string visible. 120 | * 121 | * At times, it is useful to show a code snippet in an error message. 122 | * If such a code snippet contains new lines and/or tab or space characters, those would be 123 | * displayed as-is in the command-line report, often breaking the layout of the report 124 | * or making the report harder to read. 125 | * 126 | * This method will convert these characters to their escape codes, making them visible in the 127 | * display string without impacting the report layout. 128 | * 129 | * @see \PHPCSUtils\Utils\GetTokensToString Methods to retrieve a multi-token code snippet. 130 | * @see \PHP_CodeSniffer\Util\Common\prepareForOutput() Similar PHPCS native method. 131 | * 132 | * @since 1.0.0 133 | * 134 | * @param string $text Arbitrary text string. 135 | * 136 | * @return string 137 | */ 138 | public static function showEscapeChars($text) 139 | { 140 | $search = ["\n", "\r", "\t"]; 141 | $replace = ['\n', '\r', '\t']; 142 | 143 | return \str_replace($search, $replace, $text); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/NamingConventions.php: -------------------------------------------------------------------------------- 1 | Note: for variable names, the leading dollar sign - `$` - needs to be 67 | * removed prior to passing the name to this method. 68 | * 69 | * @return bool 70 | */ 71 | public static function isValidIdentifierName($name) 72 | { 73 | if (\is_string($name) === false || $name === '' || \strpos($name, ' ') !== false) { 74 | return false; 75 | } 76 | 77 | return (\preg_match(self::PHP_LABEL_REGEX, $name) === 1); 78 | } 79 | 80 | /** 81 | * Check if two arbitrary identifier names will be seen as the same in PHP. 82 | * 83 | * This method should not be used for variable or constant names, but *should* be used 84 | * when comparing namespace, class/trait/interface and function names. 85 | * 86 | * Variable and constant names in PHP are case-sensitive, except for constants explicitely 87 | * declared case-insensitive using the third parameter for 88 | * {@link https://www.php.net/function.define `define()`}. 89 | * 90 | * All other names are case-insensitive for the most part, but as it's PHP, not completely. 91 | * Basically ASCII chars used are case-insensitive, but anything from 0x80 up is case-sensitive. 92 | * 93 | * This method takes this case-(in)sensitivity into account when comparing identifier names. 94 | * 95 | * Note: this method does not check whether the passed names would be valid for identifiers! 96 | * The {@see \PHPCSUtils\Utils\NamingConventions::isValidIdentifierName()} method should be used 97 | * to verify that, if necessary. 98 | * 99 | * @since 1.0.0 100 | * 101 | * @param string $nameA The first identifier name. 102 | * @param string $nameB The second identifier name. 103 | * 104 | * @return bool `TRUE` if these names would be considered the same in PHP; `FALSE` otherwise. 105 | */ 106 | public static function isEqual($nameA, $nameB) 107 | { 108 | // Simple quick check first. 109 | if ($nameA === $nameB) { 110 | return true; 111 | } 112 | 113 | // Comparing via strcasecmp will only compare ASCII letters case-insensitively. 114 | return (\strcasecmp($nameA, $nameB) === 0); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Numbers.php: -------------------------------------------------------------------------------- 1 | [0-9](?:[0-9_]*[0-9])?) 88 | | 89 | (?P((?:[0-9](?:[0-9_]*[0-9])?)*\.(?P>LNUM)|(?P>LNUM)\.(?:[0-9](?:[0-9_]*[0-9])?)*)) 90 | ) 91 | [e][+-]?(?P>LNUM) 92 | ) 93 | | 94 | (?P>DNUM) 95 | | 96 | (?:0|[1-9](?:[0-9_]*[0-9])?) 97 | )$ 98 | `ixD'; 99 | 100 | /** 101 | * Retrieve information about a number token. 102 | * 103 | * Helper function to deal with numeric literals, potentially with underscore separators 104 | * and/or explicit octal notation. 105 | * 106 | * @since 1.0.0 107 | * 108 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 109 | * @param int $stackPtr The position of a T_LNUMBER or T_DNUMBER token. 110 | * 111 | * @return array An array with information about the number. 112 | * The format of the array return value is: 113 | * ```php 114 | * array( 115 | * 'orig_content' => string, // The original content of the token(s); 116 | * 'content' => string, // The content, underscore(s) removed; 117 | * 'code' => int, // The token code of the number, either 118 | * // T_LNUMBER or T_DNUMBER. 119 | * 'type' => string, // The token type, either 'T_LNUMBER' 120 | * // or 'T_DNUMBER'. 121 | * 'decimal' => string, // The decimal value of the number; 122 | * 'last_token' => int, // The stackPtr to the last token which was 123 | * // part of the number. 124 | * // At this time, this will be always be the original 125 | * // stackPtr. This may change in the future if 126 | * // new numeric syntaxes would be added to PHP. 127 | * ) 128 | * ``` 129 | * 130 | * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. 131 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. 132 | * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_LNUMBER` or `T_DNUMBER` token. 133 | */ 134 | public static function getCompleteNumber(File $phpcsFile, $stackPtr) 135 | { 136 | $tokens = $phpcsFile->getTokens(); 137 | 138 | if (\is_int($stackPtr) === false) { 139 | throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); 140 | } 141 | 142 | if (isset($tokens[$stackPtr]) === false) { 143 | throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); 144 | } 145 | 146 | if ($tokens[$stackPtr]['code'] !== \T_LNUMBER && $tokens[$stackPtr]['code'] !== \T_DNUMBER) { 147 | throw UnexpectedTokenType::create(2, '$stackPtr', 'T_LNUMBER or T_DNUMBER', $tokens[$stackPtr]['type']); 148 | } 149 | 150 | $content = $tokens[$stackPtr]['content']; 151 | return [ 152 | 'orig_content' => $content, 153 | 'content' => \str_replace('_', '', $content), 154 | 'code' => $tokens[$stackPtr]['code'], 155 | 'type' => $tokens[$stackPtr]['type'], 156 | 'decimal' => self::getDecimalValue($content), 157 | 'last_token' => $stackPtr, 158 | ]; 159 | } 160 | 161 | /** 162 | * Get the decimal number value of a numeric string. 163 | * 164 | * Takes PHP 7.4 numeric literal separators and explicit octal literals in numbers into account. 165 | * 166 | * @since 1.0.0 167 | * 168 | * @param string $textString Arbitrary text string. 169 | * This text string should be the (combined) token content of 170 | * one or more tokens which together represent a number in PHP. 171 | * 172 | * @return string|false Decimal number as a string or `FALSE` if the passed parameter 173 | * was not a numeric string. 174 | * > Note: floating point numbers with exponent will not be expanded, 175 | * but returned as-is. 176 | */ 177 | public static function getDecimalValue($textString) 178 | { 179 | if (\is_string($textString) === false || $textString === '') { 180 | return false; 181 | } 182 | 183 | /* 184 | * Remove potential PHP 7.4 numeric literal separators. 185 | * 186 | * {@internal While the is..() functions also do this, this is still needed 187 | * here to allow the hexdec(), bindec() functions to work correctly and for 188 | * the decimal/float to return a cross-version compatible decimal value.} 189 | */ 190 | $textString = \str_replace('_', '', $textString); 191 | 192 | if (self::isDecimalInt($textString) === true) { 193 | return $textString; 194 | } 195 | 196 | if (self::isHexidecimalInt($textString) === true) { 197 | return (string) \hexdec($textString); 198 | } 199 | 200 | if (self::isBinaryInt($textString) === true) { 201 | return (string) \bindec($textString); 202 | } 203 | 204 | if (self::isOctalInt($textString) === true) { 205 | return (string) \octdec($textString); 206 | } 207 | 208 | if (self::isFloat($textString) === true) { 209 | return $textString; 210 | } 211 | 212 | return false; 213 | } 214 | 215 | /** 216 | * Verify whether the contents of an arbitrary string represents a decimal integer. 217 | * 218 | * Takes PHP 7.4 numeric literal separators in numbers into account in the regex. 219 | * 220 | * @since 1.0.0 221 | * 222 | * @param string $textString Arbitrary string. 223 | * 224 | * @return bool 225 | */ 226 | public static function isDecimalInt($textString) 227 | { 228 | if (\is_string($textString) === false || $textString === '') { 229 | return false; 230 | } 231 | 232 | return (\preg_match(self::REGEX_DECIMAL_INT, $textString) === 1); 233 | } 234 | 235 | /** 236 | * Verify whether the contents of an arbitrary string represents a hexidecimal integer. 237 | * 238 | * Takes PHP 7.4 numeric literal separators in numbers into account in the regex. 239 | * 240 | * @since 1.0.0 241 | * 242 | * @param string $textString Arbitrary string. 243 | * 244 | * @return bool 245 | */ 246 | public static function isHexidecimalInt($textString) 247 | { 248 | if (\is_string($textString) === false || $textString === '') { 249 | return false; 250 | } 251 | 252 | return (\preg_match(self::REGEX_HEX_INT, $textString) === 1); 253 | } 254 | 255 | /** 256 | * Verify whether the contents of an arbitrary string represents a binary integer. 257 | * 258 | * Takes PHP 7.4 numeric literal separators in numbers into account in the regex. 259 | * 260 | * @since 1.0.0 261 | * 262 | * @param string $textString Arbitrary string. 263 | * 264 | * @return bool 265 | */ 266 | public static function isBinaryInt($textString) 267 | { 268 | if (\is_string($textString) === false || $textString === '') { 269 | return false; 270 | } 271 | 272 | return (\preg_match(self::REGEX_BINARY_INT, $textString) === 1); 273 | } 274 | 275 | /** 276 | * Verify whether the contents of an arbitrary string represents an octal integer. 277 | * 278 | * Takes PHP 7.4 numeric literal separators and explicit octal literals in numbers into account in the regex. 279 | * 280 | * @since 1.0.0 281 | * 282 | * @param string $textString Arbitrary string. 283 | * 284 | * @return bool 285 | */ 286 | public static function isOctalInt($textString) 287 | { 288 | if (\is_string($textString) === false || $textString === '') { 289 | return false; 290 | } 291 | 292 | return (\preg_match(self::REGEX_OCTAL_INT, $textString) === 1); 293 | } 294 | 295 | /** 296 | * Verify whether the contents of an arbitrary string represents a floating point number. 297 | * 298 | * Takes PHP 7.4 numeric literal separators in numbers into account in the regex. 299 | * 300 | * @since 1.0.0 301 | * 302 | * @param string $textString Arbitrary string. 303 | * 304 | * @return bool 305 | */ 306 | public static function isFloat($textString) 307 | { 308 | if (\is_string($textString) === false || $textString === '') { 309 | return false; 310 | } 311 | 312 | return (\preg_match(self::REGEX_FLOAT, $textString) === 1); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Operators.php: -------------------------------------------------------------------------------- 1 | Note: value is irrelevant, only key is used. 39 | */ 40 | private static $extraUnaryIndicators = [ 41 | \T_STRING_CONCAT => true, 42 | \T_RETURN => true, 43 | \T_EXIT => true, 44 | \T_CONTINUE => true, 45 | \T_BREAK => true, 46 | \T_ECHO => true, 47 | \T_PRINT => true, 48 | \T_YIELD => true, 49 | \T_COMMA => true, 50 | \T_OPEN_PARENTHESIS => true, 51 | \T_OPEN_SQUARE_BRACKET => true, 52 | \T_OPEN_SHORT_ARRAY => true, 53 | \T_OPEN_CURLY_BRACKET => true, 54 | \T_COLON => true, 55 | \T_CASE => true, 56 | \T_FN_ARROW => true, 57 | \T_MATCH_ARROW => true, 58 | ]; 59 | 60 | /** 61 | * Determine if the passed token is a reference operator. 62 | * 63 | * Main differences with the PHPCS version: 64 | * - Defensive coding against incorrect calls to this method. 65 | * 66 | * @see \PHP_CodeSniffer\Files\File::isReference() Original source. 67 | * @see \PHPCSUtils\BackCompat\BCFile::isReference() Cross-version compatible version of the original. 68 | * 69 | * @since 1.0.0 70 | * 71 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 72 | * @param int $stackPtr The position of the `T_BITWISE_AND` token. 73 | * 74 | * @return bool `TRUE` if the specified token position represents a reference. 75 | * `FALSE` if the token represents a bitwise operator. 76 | */ 77 | public static function isReference(File $phpcsFile, $stackPtr) 78 | { 79 | $tokens = $phpcsFile->getTokens(); 80 | 81 | if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_BITWISE_AND) { 82 | return false; 83 | } 84 | 85 | $tokenBefore = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 86 | 87 | if (isset(Collections::functionDeclarationTokens()[$tokens[$tokenBefore]['code']]) === true) { 88 | // Function returns a reference. 89 | return true; 90 | } 91 | 92 | if ($tokens[$tokenBefore]['code'] === \T_DOUBLE_ARROW) { 93 | // Inside a foreach loop or array assignment, this is a reference. 94 | return true; 95 | } 96 | 97 | if ($tokens[$tokenBefore]['code'] === \T_AS) { 98 | // Inside a foreach loop, this is a reference. 99 | return true; 100 | } 101 | 102 | if (isset(Tokens::$assignmentTokens[$tokens[$tokenBefore]['code']]) === true) { 103 | // This is directly after an assignment. It's a reference. Even if 104 | // it is part of an operation, the other tests will handle it. 105 | return true; 106 | } 107 | 108 | $tokenAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 109 | 110 | if ($tokens[$tokenAfter]['code'] === \T_NEW) { 111 | return true; 112 | } 113 | 114 | $lastOpener = Parentheses::getLastOpener($phpcsFile, $stackPtr); 115 | if ($lastOpener !== false) { 116 | $lastOwner = Parentheses::getOwner($phpcsFile, $lastOpener); 117 | 118 | if (isset(Collections::functionDeclarationTokens()[$tokens[$lastOwner]['code']]) === true 119 | // As of PHPCS 4.x, `T_USE` is a parenthesis owner. 120 | || $tokens[$lastOwner]['code'] === \T_USE 121 | ) { 122 | $params = FunctionDeclarations::getParameters($phpcsFile, $lastOwner); 123 | foreach ($params as $param) { 124 | if ($param['reference_token'] === $stackPtr) { 125 | // Function parameter declared to be passed by reference. 126 | return true; 127 | } 128 | } 129 | } 130 | } 131 | 132 | /* 133 | * Pass by reference in function calls, assign by reference in arrays and 134 | * closure use by reference in PHPCS 3.x. 135 | */ 136 | if ($tokens[$tokenBefore]['code'] === \T_OPEN_PARENTHESIS 137 | || $tokens[$tokenBefore]['code'] === \T_COMMA 138 | || $tokens[$tokenBefore]['code'] === \T_OPEN_SHORT_ARRAY 139 | ) { 140 | if ($tokens[$tokenAfter]['code'] === \T_VARIABLE) { 141 | return true; 142 | } else { 143 | $skip = Tokens::$emptyTokens; 144 | $skip += Collections::namespacedNameTokens(); 145 | $skip += Collections::ooHierarchyKeywords(); 146 | $skip[] = \T_DOUBLE_COLON; 147 | 148 | $nextSignificantAfter = $phpcsFile->findNext( 149 | $skip, 150 | ($stackPtr + 1), 151 | null, 152 | true 153 | ); 154 | if ($tokens[$nextSignificantAfter]['code'] === \T_VARIABLE) { 155 | return true; 156 | } 157 | } 158 | } 159 | 160 | return false; 161 | } 162 | 163 | /** 164 | * Determine whether a T_MINUS/T_PLUS token is a unary operator. 165 | * 166 | * @since 1.0.0 167 | * 168 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 169 | * @param int $stackPtr The position of the plus/minus token. 170 | * 171 | * @return bool `TRUE` if the token passed is a unary operator. 172 | * `FALSE` otherwise, i.e. if the token is an arithmetic operator, 173 | * or if the token is not a `T_PLUS`/`T_MINUS` token. 174 | */ 175 | public static function isUnaryPlusMinus(File $phpcsFile, $stackPtr) 176 | { 177 | $tokens = $phpcsFile->getTokens(); 178 | 179 | if (isset($tokens[$stackPtr]) === false 180 | || ($tokens[$stackPtr]['code'] !== \T_PLUS 181 | && $tokens[$stackPtr]['code'] !== \T_MINUS) 182 | ) { 183 | return false; 184 | } 185 | 186 | $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 187 | if ($next === false) { 188 | // Live coding or parse error. 189 | return false; 190 | } 191 | 192 | if (isset(Tokens::$operators[$tokens[$next]['code']]) === true) { 193 | // Next token is an operator, so this is not a unary. 194 | return false; 195 | } 196 | 197 | $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 198 | 199 | /* 200 | * Check the preceeding token for an indication that this is not an arithmetic operation. 201 | */ 202 | if (isset(Tokens::$operators[$tokens[$prev]['code']]) === true 203 | || isset(Tokens::$comparisonTokens[$tokens[$prev]['code']]) === true 204 | || isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true 205 | || isset(Tokens::$assignmentTokens[$tokens[$prev]['code']]) === true 206 | || isset(Tokens::$castTokens[$tokens[$prev]['code']]) === true 207 | || isset(Collections::ternaryOperators()[$tokens[$prev]['code']]) === true 208 | || isset(self::$extraUnaryIndicators[$tokens[$prev]['code']]) === true 209 | ) { 210 | return true; 211 | } 212 | 213 | return false; 214 | } 215 | 216 | /** 217 | * Determine whether a ternary is a short ternary/elvis operator, i.e. without "middle". 218 | * 219 | * @since 1.0.0 220 | * 221 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 222 | * @param int $stackPtr The position of the ternary then/else 223 | * operator in the stack. 224 | * 225 | * @return bool `TRUE` if short ternary; or `FALSE` otherwise. 226 | */ 227 | public static function isShortTernary(File $phpcsFile, $stackPtr) 228 | { 229 | $tokens = $phpcsFile->getTokens(); 230 | if (isset($tokens[$stackPtr]) === false) { 231 | return false; 232 | } 233 | 234 | if ($tokens[$stackPtr]['code'] === \T_INLINE_THEN) { 235 | $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 236 | if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_INLINE_ELSE) { 237 | return true; 238 | } 239 | } 240 | 241 | if ($tokens[$stackPtr]['code'] === \T_INLINE_ELSE) { 242 | $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 243 | if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['code'] === \T_INLINE_THEN) { 244 | return true; 245 | } 246 | } 247 | 248 | // Not a ternary operator token. 249 | return false; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Orthography.php: -------------------------------------------------------------------------------- 1 | An orthography is a set of conventions for writing a language. It includes norms of spelling, 19 | * > hyphenation, capitalization, word breaks, emphasis, and punctuation. 20 | * > Source: https://en.wikipedia.org/wiki/Orthography 21 | * 22 | * @since 1.0.0 23 | */ 24 | final class Orthography 25 | { 26 | 27 | /** 28 | * Characters which are considered terminal points for a sentence. 29 | * 30 | * @link https://www.thepunctuationguide.com/terminal-points.html Punctuation guide on terminal points. 31 | * 32 | * @since 1.0.0 33 | * 34 | * @var string 35 | */ 36 | const TERMINAL_POINTS = '.?!'; 37 | 38 | /** 39 | * Check if the first character of an arbitrary text string is a capital letter. 40 | * 41 | * Letter characters which do not have a concept of lower/uppercase will 42 | * be accepted as correctly capitalized. 43 | * 44 | * @since 1.0.0 45 | * 46 | * @param string $textString The text string to examine. 47 | * This can be the contents of a text string token, 48 | * but also, for instance, a comment text. 49 | * Potential text delimiter quotes should be stripped 50 | * off a text string before passing it to this method. 51 | * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. 52 | * 53 | * @return bool `TRUE` when the first character is a capital letter or a letter 54 | * which doesn't have a concept of capitalization. 55 | * `FALSE` otherwise, including for non-letter characters. 56 | */ 57 | public static function isFirstCharCapitalized($textString) 58 | { 59 | $textString = \ltrim($textString); 60 | return (\preg_match('`^[\p{Lu}\p{Lt}\p{Lo}]`u', $textString) > 0); 61 | } 62 | 63 | /** 64 | * Check if the first character of an arbitrary text string is a lowercase letter. 65 | * 66 | * @since 1.0.0 67 | * 68 | * @param string $textString The text string to examine. 69 | * This can be the contents of a text string token, 70 | * but also, for instance, a comment text. 71 | * Potential text delimiter quotes should be stripped 72 | * off a text string before passing it to this method. 73 | * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. 74 | * 75 | * @return bool `TRUE` when the first character is a lowercase letter. 76 | * `FALSE` otherwise, including for letters which don't have a concept of 77 | * capitalization and for non-letter characters. 78 | */ 79 | public static function isFirstCharLowercase($textString) 80 | { 81 | $textString = \ltrim($textString); 82 | return (\preg_match('`^\p{Ll}`u', $textString) > 0); 83 | } 84 | 85 | /** 86 | * Check if the last character of an arbitrary text string is a valid punctuation character. 87 | * 88 | * @since 1.0.0 89 | * 90 | * @param string $textString The text string to examine. 91 | * This can be the contents of a text string token, 92 | * but also, for instance, a comment text. 93 | * Potential text delimiter quotes should be stripped 94 | * off a text string before passing it to this method. 95 | * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. 96 | * @param string $allowedChars Characters which are considered valid punctuation 97 | * to end the text string. 98 | * Defaults to `'.?!'`, i.e. a full stop, question mark 99 | * or exclamation mark. 100 | * 101 | * @return bool 102 | */ 103 | public static function isLastCharPunctuation($textString, $allowedChars = self::TERMINAL_POINTS) 104 | { 105 | $encoding = Helper::getEncoding(); 106 | $textString = \rtrim($textString); 107 | 108 | if (\function_exists('iconv_substr') === true) { 109 | $lastChar = \iconv_substr($textString, -1, 1, $encoding); 110 | } else { 111 | $lastChar = \substr($textString, -1); 112 | } 113 | 114 | if (\function_exists('iconv_strpos') === true) { 115 | return (\iconv_strpos($allowedChars, $lastChar, 0, $encoding) !== false); 116 | } else { 117 | return (\strpos($allowedChars, $lastChar) !== false); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/Scopes.php: -------------------------------------------------------------------------------- 1 | $validScopes Array of token constants representing 37 | * the scopes considered valid. 38 | * 39 | * @return int|false Integer stack pointer to the valid direct scope; or `FALSE` if 40 | * no valid direct scope was found. 41 | */ 42 | public static function validDirectScope(File $phpcsFile, $stackPtr, $validScopes) 43 | { 44 | $ptr = Conditions::getLastCondition($phpcsFile, $stackPtr); 45 | 46 | if ($ptr !== false) { 47 | $tokens = $phpcsFile->getTokens(); 48 | $validScopes = (array) $validScopes; 49 | 50 | if (\in_array($tokens[$ptr]['code'], $validScopes, true) === true) { 51 | return $ptr; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /** 59 | * Check whether a T_CONST token is a class/interface/trait/enum constant declaration. 60 | * 61 | * @since 1.0.0 62 | * 63 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 64 | * @param int $stackPtr The position in the stack of the 65 | * `T_CONST` token to verify. 66 | * 67 | * @return bool 68 | */ 69 | public static function isOOConstant(File $phpcsFile, $stackPtr) 70 | { 71 | $tokens = $phpcsFile->getTokens(); 72 | 73 | if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CONST) { 74 | return false; 75 | } 76 | 77 | if (self::validDirectScope($phpcsFile, $stackPtr, Collections::ooConstantScopes()) !== false) { 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | 84 | /** 85 | * Check whether a T_VARIABLE token is a class/trait property declaration. 86 | * 87 | * @since 1.0.0 88 | * 89 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 90 | * @param int $stackPtr The position in the stack of the 91 | * `T_VARIABLE` token to verify. 92 | * 93 | * @return bool 94 | */ 95 | public static function isOOProperty(File $phpcsFile, $stackPtr) 96 | { 97 | $tokens = $phpcsFile->getTokens(); 98 | 99 | if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_VARIABLE) { 100 | return false; 101 | } 102 | 103 | $scopePtr = self::validDirectScope($phpcsFile, $stackPtr, Collections::ooPropertyScopes()); 104 | if ($scopePtr !== false) { 105 | // Make sure it's not a method parameter. 106 | $deepestOpen = Parentheses::getLastOpener($phpcsFile, $stackPtr); 107 | if ($deepestOpen === false 108 | || $deepestOpen < $scopePtr 109 | || Parentheses::isOwnerIn($phpcsFile, $deepestOpen, \T_FUNCTION) === false 110 | ) { 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | /** 119 | * Check whether a T_FUNCTION token is a class/interface/trait/enum method declaration. 120 | * 121 | * @since 1.0.0 122 | * 123 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 124 | * @param int $stackPtr The position in the stack of the 125 | * `T_FUNCTION` token to verify. 126 | * 127 | * @return bool 128 | */ 129 | public static function isOOMethod(File $phpcsFile, $stackPtr) 130 | { 131 | $tokens = $phpcsFile->getTokens(); 132 | 133 | if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_FUNCTION) { 134 | return false; 135 | } 136 | 137 | if (self::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens) !== false) { 138 | return true; 139 | } 140 | 141 | return false; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /PHPCSUtils/Utils/TextStrings.php: -------------------------------------------------------------------------------- 1 | [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:\??->(?P>varname)|\[[^\]\'"\s]+\])?`'; 54 | 55 | /** 56 | * Get the complete contents of a - potentially multi-line - text string. 57 | * 58 | * PHPCS tokenizes multi-line text strings with a single token for each line. 59 | * This method can be used to retrieve the text string as it would be received and 60 | * processed in PHP itself. 61 | * 62 | * This method is particularly useful for sniffs which examine the contents of text strings, 63 | * where the content matching might result in false positives/false negatives if the text 64 | * were to be examined line by line. 65 | * 66 | * @since 1.0.0 67 | * 68 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 69 | * @param int $stackPtr Pointer to the first text string token 70 | * of a - potentially multi-line - text string 71 | * or to a Nowdoc/Heredoc opener. 72 | * @param bool $stripQuotes Optional. Whether to strip text delimiter 73 | * quotes off the resulting text string. 74 | * Defaults to `true`. 75 | * 76 | * @return string The contents of the complete text string. 77 | * 78 | * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. 79 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. 80 | * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a valid text string token. 81 | * @throws \PHPCSUtils\Exceptions\ValueError If the specified token is not the _first_ token in 82 | * a text string. 83 | */ 84 | public static function getCompleteTextString(File $phpcsFile, $stackPtr, $stripQuotes = true) 85 | { 86 | $tokens = $phpcsFile->getTokens(); 87 | $end = self::getEndOfCompleteTextString($phpcsFile, $stackPtr); 88 | 89 | $stripNewline = false; 90 | if ($tokens[$stackPtr]['code'] === \T_START_HEREDOC || $tokens[$stackPtr]['code'] === \T_START_NOWDOC) { 91 | $stripQuotes = false; 92 | $stripNewline = true; 93 | $stackPtr = ($stackPtr + 1); 94 | } 95 | 96 | $contents = GetTokensAsString::normal($phpcsFile, $stackPtr, $end); 97 | 98 | if ($stripNewline === true) { 99 | // Heredoc/nowdoc: strip the new line at the end of the string to emulate how PHP sees the string. 100 | $contents = \rtrim($contents, "\r\n"); 101 | } 102 | 103 | if ($stripQuotes === true) { 104 | return self::stripQuotes($contents); 105 | } 106 | 107 | return $contents; 108 | } 109 | 110 | /** 111 | * Get the stack pointer to the end of a - potentially multi-line - text string. 112 | * 113 | * @see \PHPCSUtils\Utils\TextStrings::getCompleteTextString() Retrieve the contents of a complete - potentially 114 | * multi-line - text string. 115 | * 116 | * @since 1.0.0 117 | * 118 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. 119 | * @param int $stackPtr Pointer to the first text string token 120 | * of a - potentially multi-line - text string 121 | * or to a Nowdoc/Heredoc opener. 122 | * 123 | * @return int Stack pointer to the last token in the text string. 124 | * 125 | * @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer. 126 | * @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile. 127 | * @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a valid text string token. 128 | * @throws \PHPCSUtils\Exceptions\ValueError If the specified token is not the _first_ token in 129 | * a text string. 130 | */ 131 | public static function getEndOfCompleteTextString(File $phpcsFile, $stackPtr) 132 | { 133 | $tokens = $phpcsFile->getTokens(); 134 | 135 | if (\is_int($stackPtr) === false) { 136 | throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr); 137 | } 138 | 139 | if (isset($tokens[$stackPtr]) === false) { 140 | throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr); 141 | } 142 | 143 | if (isset(Collections::textStringStartTokens()[$tokens[$stackPtr]['code']]) === false) { 144 | $acceptedTokens = 'T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING or T_DOUBLE_QUOTED_STRING'; 145 | throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']); 146 | } 147 | 148 | // Must be the start of a text string token. 149 | if (isset(Tokens::$stringTokens[$tokens[$stackPtr]['code']]) === true) { 150 | $prev = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true); 151 | if ($tokens[$stackPtr]['code'] === $tokens[$prev]['code']) { 152 | throw ValueError::create(2, '$stackPtr', 'must be the start of the text string'); 153 | } 154 | } 155 | 156 | if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { 157 | return Cache::get($phpcsFile, __METHOD__, $stackPtr); 158 | } 159 | 160 | switch ($tokens[$stackPtr]['code']) { 161 | case \T_START_HEREDOC: 162 | $targetType = \T_HEREDOC; 163 | $current = ($stackPtr + 1); 164 | break; 165 | 166 | case \T_START_NOWDOC: 167 | $targetType = \T_NOWDOC; 168 | $current = ($stackPtr + 1); 169 | break; 170 | 171 | default: 172 | $targetType = $tokens[$stackPtr]['code']; 173 | $current = $stackPtr; 174 | break; 175 | } 176 | 177 | while (isset($tokens[$current]) && $tokens[$current]['code'] === $targetType) { 178 | ++$current; 179 | } 180 | 181 | $lastPtr = ($current - 1); 182 | 183 | Cache::set($phpcsFile, __METHOD__, $stackPtr, $lastPtr); 184 | return $lastPtr; 185 | } 186 | 187 | /** 188 | * Strip text delimiter quotes from an arbitrary text string. 189 | * 190 | * Intended for use with the "content" of a `T_CONSTANT_ENCAPSED_STRING` / `T_DOUBLE_QUOTED_STRING`. 191 | * 192 | * - Prevents stripping mis-matched quotes. 193 | * - Prevents stripping quotes from the textual content of the text string. 194 | * 195 | * @since 1.0.0 196 | * 197 | * @param string $textString The raw text string. 198 | * 199 | * @return string Text string without quotes around it. 200 | */ 201 | public static function stripQuotes($textString) 202 | { 203 | return \preg_replace('`^([\'"])(.*)\1$`Ds', '$2', $textString); 204 | } 205 | 206 | /** 207 | * Get the embedded variables/expressions from an arbitrary string. 208 | * 209 | * Note: this function gets the complete variables/expressions _as they are embedded_, 210 | * i.e. including potential curly brace wrappers, array access, method calls etc. 211 | * 212 | * @since 1.0.0 213 | * 214 | * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. 215 | * 216 | * @return array Array of encountered variable names/expressions with the offset at which 217 | * the variable/expression was found in the string, as the key. 218 | */ 219 | public static function getEmbeds($text) 220 | { 221 | return self::getStripEmbeds($text)['embeds']; 222 | } 223 | 224 | /** 225 | * Strip embedded variables/expressions from an arbitrary string. 226 | * 227 | * @since 1.0.0 228 | * 229 | * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. 230 | * 231 | * @return string String without variables/expressions in it. 232 | */ 233 | public static function stripEmbeds($text) 234 | { 235 | return self::getStripEmbeds($text)['remaining']; 236 | } 237 | 238 | /** 239 | * Split an arbitrary text string into embedded variables/expressions and remaining text. 240 | * 241 | * PHP contains four types of embedding syntaxes: 242 | * 1. Directly embedding variables ("$foo"); 243 | * 2. Braces outside the variable ("{$foo}"); 244 | * 3. Braces after the dollar sign ("${foo}"); 245 | * 4. Variable variables ("${expr}", equivalent to (string) ${expr}). 246 | * 247 | * Type 3 and 4 are deprecated as of PHP 8.2 and will be removed in PHP 9.0. 248 | * 249 | * This method handles all types of embeds, including recognition of whether an embed is escaped or not. 250 | * 251 | * @link https://www.php.net/language.types.string#language.types.string.parsing PHP Manual on string parsing 252 | * @link https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation PHP RFC on deprecating select 253 | * string interpolation syntaxes 254 | * 255 | * @since 1.0.0 256 | * 257 | * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. 258 | * 259 | * @return array Array containing two values: 260 | * 1. An array containing a string representation of each embed encountered. 261 | * The keys in this array are the integer offset within the original string 262 | * where the embed was found. 263 | * 2. The textual contents, embeds stripped out of it. 264 | * The format of the array return value is: 265 | * ```php 266 | * array( 267 | * 'embeds' => array, 268 | * 'remaining' => string, 269 | * ) 270 | * ``` 271 | */ 272 | public static function getStripEmbeds($text) 273 | { 274 | if (\strpos($text, '$') === false) { 275 | return [ 276 | 'embeds' => [], 277 | 'remaining' => $text, 278 | ]; 279 | } 280 | 281 | $textHash = \md5($text); 282 | if (NoFileCache::isCached(__METHOD__, $textHash) === true) { 283 | return NoFileCache::get(__METHOD__, $textHash); 284 | } 285 | 286 | $offset = 0; 287 | $strLen = \strlen($text); // Use iconv ? 288 | $stripped = ''; 289 | $variables = []; 290 | 291 | while (\preg_match(self::START_OF_EMBED, $text, $matches, \PREG_OFFSET_CAPTURE, $offset) === 1) { 292 | $stripped .= \substr($text, $offset, ($matches[2][1] - $offset)); 293 | 294 | $matchedExpr = $matches[2][0]; 295 | $matchedOffset = $matches[2][1]; 296 | $braces = \substr_count($matchedExpr, '{'); 297 | $newOffset = $matchedOffset + \strlen($matchedExpr); 298 | 299 | if ($braces === 0) { 300 | /* 301 | * Type 1: simple variable embed. 302 | * Regex will always return a match due to the look ahead in the above regex. 303 | */ 304 | \preg_match(self::TYPE1_EMBED_AFTER_DOLLAR, $text, $endMatch, 0, $newOffset); 305 | $matchedExpr .= $endMatch[0]; 306 | $variables[$matchedOffset] = $matchedExpr; 307 | $offset = $newOffset + \strlen($endMatch[0]); 308 | continue; 309 | } 310 | 311 | for (; $newOffset < $strLen; $newOffset++) { 312 | if ($text[$newOffset] === '{') { 313 | ++$braces; 314 | continue; 315 | } 316 | 317 | if ($text[$newOffset] === '}') { 318 | --$braces; 319 | if ($braces === 0) { 320 | $matchedExpr = \substr($text, $matchedOffset, (1 + $newOffset - $matchedOffset)); 321 | $variables[$matchedOffset] = $matchedExpr; 322 | $offset = ($newOffset + 1); 323 | break; 324 | } 325 | } 326 | } 327 | } 328 | 329 | if ($offset < $strLen) { 330 | // Add the end of the string. 331 | $stripped .= \substr($text, $offset); 332 | } 333 | 334 | $returnValue = [ 335 | 'embeds' => $variables, 336 | 'remaining' => $stripped, 337 | ]; 338 | 339 | NoFileCache::set(__METHOD__, $textHash, $returnValue); 340 | return $returnValue; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /PHPCSUtils/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Utility methods for external PHPCS standards. 4 | 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "phpcsstandards/phpcsutils", 3 | "description" : "A suite of utility functions for use with PHP_CodeSniffer", 4 | "type" : "phpcodesniffer-standard", 5 | "keywords" : [ "phpcs", "phpcbf", "standards", "static analysis", "php_codesniffer", "phpcs3", "tokens", "utility", "phpcodesniffer-standard" ], 6 | "license" : "LGPL-3.0-or-later", 7 | "homepage": "https://phpcsutils.com/", 8 | "authors" : [ 9 | { 10 | "name" : "Juliette Reinders Folmer", 11 | "role" : "lead", 12 | "homepage" : "https://github.com/jrfnl" 13 | }, 14 | { 15 | "name" : "Contributors", 16 | "homepage" : "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" 17 | } 18 | ], 19 | "support" : { 20 | "issues" : "https://github.com/PHPCSStandards/PHPCSUtils/issues", 21 | "source" : "https://github.com/PHPCSStandards/PHPCSUtils", 22 | "docs" : "https://phpcsutils.com/", 23 | "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy" 24 | }, 25 | "require" : { 26 | "php" : ">=5.4", 27 | "squizlabs/php_codesniffer" : "^3.13.0 || 4.0.x-dev@dev", 28 | "dealerdirect/phpcodesniffer-composer-installer" : "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0" 29 | }, 30 | "require-dev" : { 31 | "ext-filter": "*", 32 | "phpcsstandards/phpcsdevcs": "^1.1.6", 33 | "php-parallel-lint/php-parallel-lint": "^1.4.0", 34 | "php-parallel-lint/php-console-highlighter": "^1.0", 35 | "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" 36 | }, 37 | "minimum-stability": "dev", 38 | "prefer-stable": true, 39 | "autoload": { 40 | "classmap": ["PHPCSUtils/"] 41 | }, 42 | "autoload-dev" : { 43 | "psr-4": { 44 | "PHPCSUtils\\Tests\\": "Tests/", 45 | "PHPCSUtils\\GHPages\\": ".github/GHPages/" 46 | } 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-stable": "1.x-dev", 51 | "dev-develop": "1.x-dev" 52 | } 53 | }, 54 | "scripts" : { 55 | "lint-lt72": [ 56 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php --exclude vendor --exclude .git --exclude .github/GHPages" 57 | ], 58 | "lint": [ 59 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" 60 | ], 61 | "checkcs": [ 62 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" 63 | ], 64 | "fixcs": [ 65 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" 66 | ], 67 | "test": [ 68 | "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" 69 | ], 70 | "test10": [ 71 | "@php ./vendor/phpunit/phpunit/phpunit -c phpunit10.xml.dist --no-coverage" 72 | ], 73 | "coverage": [ 74 | "@php ./vendor/phpunit/phpunit/phpunit" 75 | ], 76 | "coverage10": [ 77 | "@php ./vendor/phpunit/phpunit/phpunit -c phpunit10.xml.dist" 78 | ], 79 | "coverage-local": [ 80 | "@php ./vendor/phpunit/phpunit/phpunit --coverage-html ./build/coverage-html" 81 | ], 82 | "coverage-local10": [ 83 | "@php ./vendor/phpunit/phpunit/phpunit -c phpunit10.xml.dist --coverage-html ./build/coverage-html" 84 | ] 85 | }, 86 | "config": { 87 | "allow-plugins": { 88 | "dealerdirect/phpcodesniffer-composer-installer": true 89 | }, 90 | "lock": false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /phpcsutils-autoload.php: -------------------------------------------------------------------------------- 1 | = 3.1.0 and uses the PHPCS 8 | * native unit test framework, this file does not need to be included. 9 | * 10 | * - If an external standard uses its own unit test setup, this file should 11 | * be included from the unit test bootstrap file. 12 | * 13 | * - If an external standard uses the PHPCSUtils {@see PHPCSUtils\TestUtils\UtilityMethodTestCase} 14 | * class to test their own utility methods, this file should be included from 15 | * the unit test bootstrap file. 16 | * 17 | * @package PHPCSUtils 18 | * @copyright 2019-2020 PHPCSUtils Contributors 19 | * @license https://opensource.org/licenses/LGPL-3.0 LGPL3 20 | * @link https://github.com/PHPCSStandards/PHPCSUtils 21 | * 22 | * @since 1.0.0 23 | */ 24 | 25 | if (defined('PHPCSUTILS_AUTOLOAD') === false) { 26 | /* 27 | * Register an autoloader. 28 | * 29 | * External PHPCS standards which have their own unit test suite 30 | * should include this file in their test runner bootstrap. 31 | */ 32 | spl_autoload_register(function ($fqClassName) { 33 | // Only try & load our own classes. 34 | if (stripos($fqClassName, 'PHPCSUtils\\') !== 0) { 35 | return; 36 | } 37 | 38 | $file = realpath(__DIR__) . DIRECTORY_SEPARATOR . strtr($fqClassName, '\\', DIRECTORY_SEPARATOR) . '.php'; 39 | 40 | if (file_exists($file)) { 41 | include_once $file; 42 | } 43 | }); 44 | 45 | define('PHPCSUTILS_AUTOLOAD', true); 46 | } 47 | 48 | if (defined('PHPCSUTILS_PHPUNIT_ALIASES_SET') === false) { 49 | /* 50 | * Alias the PHPUnit 4/5 TestCase class to its PHPUnit 6+ name. 51 | * 52 | * This allows both the PHPCSUtils native unit tests as well as the 53 | * `UtilityMethodTestCase` class to work cross-version with PHPUnit 54 | * below 6.x and above. 55 | * 56 | * {@internal The `class_exists` wrappers are needed to play nice with 57 | * PHPUnit bootstrap files of external standards which may be creating 58 | * cross-version compatibility in a similar manner.}} 59 | */ 60 | if (class_exists('PHPUnit_Framework_TestCase') === true 61 | && class_exists('PHPUnit\Framework\TestCase') === false 62 | ) { 63 | class_alias('PHPUnit_Framework_TestCase', 'PHPUnit\Framework\TestCase'); 64 | } 65 | 66 | define('PHPCSUTILS_PHPUNIT_ALIASES_SET', true); 67 | } 68 | --------------------------------------------------------------------------------