├── LICENSE ├── README.md ├── VariableAnalysis ├── Lib │ ├── Constants.php │ ├── EnumInfo.php │ ├── ForLoopInfo.php │ ├── Helpers.php │ ├── ScopeInfo.php │ ├── ScopeManager.php │ ├── ScopeType.php │ └── VariableInfo.php ├── Sniffs │ └── CodeAnalysis │ │ └── VariableAnalysisSniff.php ├── ruleset.xml └── ruleset.xml.example ├── composer.json ├── psalm-autoloader.php └── psalm.xml /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Licence Agreement 2 | ----------------------------------------------------------------------- 3 | 4 | This software is available to you under the BSD license, 5 | available in the LICENSE file accompanying this software. 6 | You may obtain a copy of the License at 7 | 8 | http://www.opensource.org/licenses/bsd-license.php 9 | 10 | ----------------------------------------------------------------------- 11 | 12 | Copyright (c) 2011, Sam Graham 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are 17 | met: 18 | 19 | * Redistributions of source code must retain the above copyright 20 | notice, this list of conditions and the following disclaimer. 21 | * Redistributions in binary form must reproduce the above copyright 22 | notice, this list of conditions and the following disclaimer in the 23 | documentation and/or other materials provided with the 24 | distribution. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 32 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | 38 | ----------------------------------------------------------------------- 39 | 40 | Portions of this sofware derived from work Copyright (c) 2010, Monotek d.o.o, 41 | released under a BSD License available at: 42 | 43 | http://www.opensource.org/licenses/bsd-license.php 44 | 45 | ----------------------------------------------------------------------- 46 | 47 | Portions of this software derived from work Copyright (c), 2006 Squiz 48 | Pty Ltd (ABN 77 084 670 600), available under the following license: 49 | 50 | BSD Licence Agreement 51 | ----------------------------------------------------------------------- 52 | 53 | This software is available to you under the BSD license, 54 | available in the LICENSE file accompanying this software. 55 | You may obtain a copy of the License at 56 | 57 | http://matrix.squiz.net/developer/tools/php_cs/licence 58 | 59 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 60 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 61 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 62 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 63 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 64 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 65 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 66 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 67 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 68 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 69 | 70 | Copyright (c), 2006 Squiz Pty Ltd (ABN 77 084 670 600). 71 | All rights reserved. 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP_CodeSniffer VariableAnalysis 2 | 3 | [![CS and QA Build Status](https://github.com/sirbrillig/phpcs-variable-analysis/actions/workflows/csqa.yml/badge.svg)](https://github.com/sirbrillig/phpcs-variable-analysis/actions/workflows/csqa.yml) 4 | [![Test Build Status](https://github.com/sirbrillig/phpcs-variable-analysis/actions/workflows/test.yml/badge.svg)](https://github.com/sirbrillig/phpcs-variable-analysis/actions/workflows/test.yml) 5 | [![Coverage Status](https://coveralls.io/repos/github/sirbrillig/phpcs-variable-analysis/badge.svg)](https://coveralls.io/github/sirbrillig/phpcs-variable-analysis) 6 | 7 | Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. 8 | 9 | - Warns if variables are used without being defined. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable`) 10 | - Warns if variables are used inside `unset()` without being defined. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedUnsetVariable`) 11 | - Warns if variables are set or declared but never used. (Sniff code: `VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable`) 12 | - Warns if `$this`, `self::$static_member`, `static::$static_member` is used outside class scope. (Sniff codes: `VariableAnalysis.CodeAnalysis.VariableAnalysis.SelfOutsideClass` or `VariableAnalysis.CodeAnalysis.VariableAnalysis.StaticOutsideClass`) 13 | 14 | ## Installation 15 | 16 | ### Requirements 17 | 18 | VariableAnalysis requires PHP 5.4 or higher and [PHP CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) version 3.5.6 or higher. 19 | 20 | ### With PHPCS Composer Installer 21 | 22 | This is the easiest method. 23 | 24 | First, install [phpcodesniffer-composer-installer](https://github.com/PHPCSStandards/composer-installer) for your project if you have not already. This will also install PHPCS. 25 | 26 | ``` 27 | composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true 28 | composer require --dev dealerdirect/phpcodesniffer-composer-installer 29 | ``` 30 | 31 | Then install these standards. 32 | 33 | ``` 34 | composer require --dev sirbrillig/phpcs-variable-analysis 35 | ``` 36 | 37 | You can then include the sniffs by adding a line like the following to [your phpcs.xml file](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file). 38 | 39 | ``` 40 | 41 | ``` 42 | 43 | It should just work after that! 44 | 45 | ### Standalone 46 | 47 | 1. Install PHP_CodeSniffer (PHPCS) by following its [installation instructions](https://github.com/PHPCSStandards/PHP_CodeSniffer#installation) (via Composer, Phar file, PEAR, or Git checkout). 48 | 49 | Do ensure that PHP_CodeSniffer's version matches our [requirements](#requirements). 50 | 51 | 2. Install VariableAnalysis. Download either the zip or tar.gz file from [the VariableAnalysis latest release page](https://github.com/sirbrillig/phpcs-variable-analysis/releases/latest). Expand the file and rename the resulting directory to `phpcs-variable-analysis`. Move the directory to a place where you'd like to keep all your PHPCS standards. 52 | 53 | 3. Add the paths of the newly installed standards to the [PHP_CodeSniffer installed_paths configuration](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-installed-standard-paths). The following command should append the new standards to your existing standards (be sure to supply the actual paths to the directories you created above). 54 | 55 | phpcs --config-set installed_paths "$(phpcs --config-show|grep installed_paths|awk '{ print $2 }'),/path/to/phpcs-variable-analysis" 56 | 57 | If you do not have any other standards installed, you can do this more easily (again, be sure to supply the actual paths): 58 | 59 | phpcs --config-set installed_paths /path/to/phpcs-variable-analysis 60 | 61 | ## Customization 62 | 63 | There's a variety of options to customize the behaviour of VariableAnalysis, take a look at the included ruleset.xml.example for commented examples of a configuration. 64 | 65 | The available options are as follows: 66 | 67 | - `allowUnusedFunctionParameters` (bool, default `false`): if set to true, function arguments will never be marked as unused. 68 | - `allowUnusedCaughtExceptions` (bool, default `true`): if set to true, caught Exception variables will never be marked as unused. 69 | - `allowUnusedParametersBeforeUsed` (bool, default `true`): if set to true, unused function arguments will be ignored if they are followed by used function arguments. 70 | - `allowUnusedVariablesBeforeRequire` (bool, default `false`): if set to true, variables defined before a `require`, `require_once`, `include`, or `include_once` will not be marked as unused. They may be intended for the required file. 71 | - `allowUndefinedVariablesInFileScope` (bool, default `false`): if set to true, undefined variables in the file's top-level scope will never be marked as undefined. This can be useful for template files which use many global variables defined elsewhere. 72 | - `allowUnusedVariablesInFileScope` (bool, default `false`): if set to true, unused variables in the file's top-level scope will never be marked as unused. This can be helpful when defining a lot of global variables to be used elsewhere. 73 | - `validUnusedVariableNames` (string, default `null`): a space-separated list of names of placeholder variables that you want to ignore from unused variable warnings. For example, to ignore the variables `$junk` and `$unused`, this could be set to `'junk unused'`. 74 | - `ignoreUnusedRegexp` (string, default `null`): a PHP regexp string (note that this requires explicit delimiters) for variables that you want to ignore from unused variable warnings. For example, to ignore the variables `$_junk` and `$_unused`, this could be set to `'/^_/'`. 75 | - `validUndefinedVariableNames` (string, default `null`): a space-separated list of names of placeholder variables that you want to ignore from undefined variable warnings. For example, to ignore the variables `$post` and `$undefined`, this could be set to `'post undefined'`. This can be used in combination with `validUndefinedVariableRegexp`. 76 | - `validUndefinedVariableRegexp` (string, default `null`): a PHP regexp string (note that this requires explicit delimiters) for variables that you want to ignore from undefined variable warnings. For example, to ignore the variables `$post` and `$undefined`, this could be set to `'/^(post|undefined)$/'`. This can be used in combination with `validUndefinedVariableNames`. 77 | - `allowUnusedForeachVariables` (bool, default `true`): if set to true, unused values from the `key => value` syntax in a `foreach` loop will never be marked as unused. 78 | - `sitePassByRefFunctions` (string, default `null`): a list of custom functions which pass in variables to be initialized by reference (eg `preg_match()`) and therefore should not require those variables to be defined ahead of time. The list is space separated and each entry is of the form `functionName:1,2`. The function name comes first followed by a colon and a comma-separated list of argument numbers (starting from 1) which should be considered variable definitions. The special value `...` in the arguments list will cause all arguments after the last number to be considered variable definitions. 79 | - `allowWordPressPassByRefFunctions` (bool, default `false`): if set to true, a list of common WordPress pass-by-reference functions will be added to the list of PHP ones so that passing undefined variables to these functions (to be initialized by reference) will be allowed. 80 | 81 | To set these these options, you must use XML in your ruleset. For details, see the [phpcs customizable sniff properties page](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties). Here is an example that ignores all variables that start with an underscore: 82 | 83 | ```xml 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | ## See Also 92 | 93 | - [ImportDetection](https://github.com/sirbrillig/phpcs-import-detection): A set of phpcs sniffs to look for unused or unimported symbols. 94 | - [phpcs-changed](https://github.com/sirbrillig/phpcs-changed): Run phpcs on files, but only report warnings/errors from lines which were changed. 95 | 96 | ## Original 97 | 98 | This was forked from the excellent work in https://github.com/illusori/PHP_Codesniffer-VariableAnalysis 99 | 100 | ## Contributing 101 | 102 | Please open issues or PRs on this repository. 103 | 104 | Any changes should be accompanied by tests and should pass linting and static analysis. Please use phpdoc (rather than actual types) for declaring types since this must run in PHP 5.4. 105 | 106 | To run tests, make sure composer is installed, then run: 107 | 108 | ``` 109 | composer install # you only need to do this once 110 | composer test 111 | ``` 112 | 113 | To run linting, use: 114 | 115 | ``` 116 | composer lint 117 | ``` 118 | 119 | To run static analysis, use: 120 | 121 | ``` 122 | composer phpstan 123 | ``` 124 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/Constants.php: -------------------------------------------------------------------------------- 1 | > 17 | */ 18 | public static function getPassByReferenceFunctions() 19 | { 20 | return [ 21 | '__soapCall' => [5], 22 | 'addFunction' => [3], 23 | 'addTask' => [3], 24 | 'addTaskBackground' => [3], 25 | 'addTaskHigh' => [3], 26 | 'addTaskHighBackground' => [3], 27 | 'addTaskLow' => [3], 28 | 'addTaskLowBackground' => [3], 29 | 'addTaskStatus' => [2], 30 | 'apc_dec' => [3], 31 | 'apc_fetch' => [2], 32 | 'apc_inc' => [3], 33 | 'apcu_dec' => [3], 34 | 'apcu_fetch' => [2], 35 | 'apcu_inc' => [3], 36 | 'areConfusable' => [3], 37 | 'arsort' => [1], 38 | 'asort' => [1], 39 | 'bindColumn' => [2], 40 | 'bindParam' => [2], 41 | 'bind_param' => [2, 3, '...'], 42 | 'bind_result' => [1, 2, '...'], 43 | 'call_user_method' => [2], 44 | 'call_user_method_array' => [2], 45 | 'curl_multi_exec' => [2], 46 | 'curl_multi_info_read' => [2], 47 | 'current' => [1], 48 | 'dbplus_curr' => [2], 49 | 'dbplus_first' => [2], 50 | 'dbplus_info' => [3], 51 | 'dbplus_last' => [2], 52 | 'dbplus_next' => [2], 53 | 'dbplus_prev' => [2], 54 | 'dbplus_tremove' => [3], 55 | 'dns_get_record' => [3, 4], 56 | 'domxml_open_file' => [3], 57 | 'domxml_open_mem' => [3], 58 | 'each' => [1], 59 | 'enchant_dict_quick_check' => [3], 60 | 'end' => [1], 61 | 'ereg' => [3], 62 | 'eregi' => [3], 63 | 'exec' => [2, 3], 64 | 'exif_thumbnail' => [1, 2, 3], 65 | 'expect_expectl' => [3], 66 | 'extract' => [1], 67 | 'filter' => [3], 68 | 'flock' => [2,3], 69 | 'fscanf' => [2, 3, '...'], 70 | 'fsockopen' => [3, 4], 71 | 'ftp_alloc' => [3], 72 | 'get' => [2, 3], 73 | 'getByKey' => [4], 74 | 'getMulti' => [2], 75 | 'getMultiByKey' => [3], 76 | 'getimagesize' => [2], 77 | 'getmxrr' => [2, 3], 78 | 'gnupg_decryptverify' => [3], 79 | 'gnupg_verify' => [4], 80 | 'grapheme_extract' => [5], 81 | 'headers_sent' => [1, 2], 82 | 'http_build_url' => [4], 83 | 'http_get' => [3], 84 | 'http_head' => [3], 85 | 'http_negotiate_charset' => [2], 86 | 'http_negotiate_content_type' => [2], 87 | 'http_negotiate_language' => [2], 88 | 'http_post_data' => [4], 89 | 'http_post_fields' => [5], 90 | 'http_put_data' => [4], 91 | 'http_put_file' => [4], 92 | 'http_put_stream' => [4], 93 | 'http_request' => [5], 94 | 'isSuspicious' => [2], 95 | 'is_callable' => [3], 96 | 'key' => [1], 97 | 'krsort' => [1], 98 | 'ksort' => [1], 99 | 'ldap_get_option' => [3], 100 | 'ldap_parse_reference' => [3], 101 | 'ldap_parse_result' => [3, 4, 5, 6], 102 | 'localtime' => [2], 103 | 'm_completeauthorizations' => [2], 104 | 'maxdb_stmt_bind_param' => [3, 4, '...'], 105 | 'maxdb_stmt_bind_result' => [2, 3, '...'], 106 | 'mb_convert_variables' => [3, 4, '...'], 107 | 'mb_parse_str' => [2], 108 | 'mqseries_back' => [2, 3], 109 | 'mqseries_begin' => [3, 4], 110 | 'mqseries_close' => [4, 5], 111 | 'mqseries_cmit' => [2, 3], 112 | 'mqseries_conn' => [2, 3, 4], 113 | 'mqseries_connx' => [2, 3, 4, 5], 114 | 'mqseries_disc' => [2, 3], 115 | 'mqseries_get' => [3, 4, 5, 6, 7, 8, 9], 116 | 'mqseries_inq' => [6, 8, 9, 10], 117 | 'mqseries_open' => [2, 4, 5, 6], 118 | 'mqseries_put' => [3, 4, 6, 7], 119 | 'mqseries_put1' => [2, 3, 4, 6, 7], 120 | 'mqseries_set' => [9, 10], 121 | 'msg_receive' => [3, 5, 8], 122 | 'msg_send' => [6], 123 | 'mssql_bind' => [3], 124 | 'natcasesort' => [1], 125 | 'natsort' => [1], 126 | 'ncurses_color_content' => [2, 3, 4], 127 | 'ncurses_getmaxyx' => [2, 3], 128 | 'ncurses_getmouse' => [1], 129 | 'ncurses_getyx' => [2, 3], 130 | 'ncurses_instr' => [1], 131 | 'ncurses_mouse_trafo' => [1, 2], 132 | 'ncurses_mousemask' => [2], 133 | 'ncurses_pair_content' => [2, 3], 134 | 'ncurses_wmouse_trafo' => [2, 3], 135 | 'newt_button_bar' => [1], 136 | 'newt_form_run' => [2], 137 | 'newt_get_screen_size' => [1, 2], 138 | 'newt_grid_get_size' => [2, 3], 139 | 'newt_reflow_text' => [5, 6], 140 | 'newt_win_entries' => [7], 141 | 'newt_win_menu' => [8], 142 | 'next' => [1], 143 | 'oci_bind_array_by_name' => [3], 144 | 'oci_bind_by_name' => [3], 145 | 'oci_define_by_name' => [3], 146 | 'oci_fetch_all' => [2], 147 | 'ocifetchinto' => [2], 148 | 'odbc_fetch_into' => [2], 149 | 'openssl_csr_export' => [2], 150 | 'openssl_csr_new' => [2], 151 | 'openssl_open' => [2], 152 | 'openssl_pkcs12_export' => [2], 153 | 'openssl_pkcs12_read' => [2], 154 | 'openssl_pkey_export' => [2], 155 | 'openssl_private_decrypt' => [2], 156 | 'openssl_private_encrypt' => [2], 157 | 'openssl_public_decrypt' => [2], 158 | 'openssl_public_encrypt' => [2], 159 | 'openssl_random_pseudo_bytes' => [2], 160 | 'openssl_seal' => [2, 3], 161 | 'openssl_sign' => [2], 162 | 'openssl_x509_export' => [2], 163 | 'ovrimos_fetch_into' => [2], 164 | 'parse' => [2,3], 165 | 'parseCurrency' => [2, 3], 166 | 'parse_str' => [2], 167 | 'parsekit_compile_file' => [2], 168 | 'parsekit_compile_string' => [2], 169 | 'passthru' => [2], 170 | 'pcntl_sigprocmask' => [3], 171 | 'pcntl_sigtimedwait' => [2], 172 | 'pcntl_sigwaitinfo' => [2], 173 | 'pcntl_wait' => [1], 174 | 'pcntl_waitpid' => [2], 175 | 'pfsockopen' => [3, 4], 176 | 'php_check_syntax' => [2], 177 | 'poll' => [1, 2, 3], 178 | 'preg_filter' => [5], 179 | 'preg_match' => [3], 180 | 'preg_match_all' => [3], 181 | 'preg_replace' => [5], 182 | 'preg_replace_callback' => [5], 183 | 'prev' => [1], 184 | 'proc_open' => [3], 185 | 'query' => [3], 186 | 'queryExec' => [2], 187 | 'reset' => [1], 188 | 'rsort' => [1], 189 | 'settype' => [1], 190 | 'shuffle' => [1], 191 | 'similar_text' => [3], 192 | 'socket_create_pair' => [4], 193 | 'socket_getpeername' => [2, 3], 194 | 'socket_getsockname' => [2, 3], 195 | 'socket_recv' => [2], 196 | 'socket_recvfrom' => [2, 5, 6], 197 | 'socket_select' => [1, 2, 3], 198 | 'sort' => [1], 199 | 'sortWithSortKeys' => [1], 200 | 'sqlite_exec' => [3], 201 | 'sqlite_factory' => [3], 202 | 'sqlite_open' => [3], 203 | 'sqlite_popen' => [3], 204 | 'sqlite_query' => [4], 205 | 'sqlite_unbuffered_query' => [4], 206 | 'sscanf' => [3, '...'], 207 | 'str_ireplace' => [4], 208 | 'str_replace' => [4], 209 | 'stream_open' => [4], 210 | 'stream_select' => [1, 2, 3], 211 | 'stream_socket_accept' => [3], 212 | 'stream_socket_client' => [2, 3], 213 | 'stream_socket_recvfrom' => [4], 214 | 'stream_socket_server' => [2, 3], 215 | 'system' => [2], 216 | 'uasort' => [1], 217 | 'uksort' => [1], 218 | 'unbufferedQuery' => [3], 219 | 'usort' => [1], 220 | 'wincache_ucache_dec' => [3], 221 | 'wincache_ucache_get' => [2], 222 | 'wincache_ucache_inc' => [3], 223 | 'xdiff_string_merge3' => [4], 224 | 'xdiff_string_patch' => [4], 225 | 'xml_parse_into_struct' => [3, 4], 226 | 'xml_set_object' => [2], 227 | 'xmlrpc_decode_request' => [2], 228 | 'xmlrpc_set_type' => [1], 229 | 'xslt_set_object' => [2], 230 | 'yaml_parse' => [3], 231 | 'yaml_parse_file' => [3], 232 | 'yaml_parse_url' => [3], 233 | 'yaz_ccl_parse' => [3], 234 | 'yaz_hits' => [2], 235 | 'yaz_scan_result' => [2], 236 | 'yaz_wait' => [1], 237 | ]; 238 | } 239 | 240 | /** 241 | * @return array> 242 | */ 243 | public static function getWordPressPassByReferenceFunctions() 244 | { 245 | return [ 246 | 'wp_parse_str' => [2], 247 | 'wp_cache_get' => [4], 248 | ]; 249 | } 250 | 251 | /** 252 | * A regexp for matching variable names in double-quoted strings. 253 | * 254 | * @return string 255 | */ 256 | public static function getDoubleQuotedVarRegexp() 257 | { 258 | return '|(?enumIndex = $enumIndex; 42 | $this->blockStart = $blockStart; 43 | $this->blockEnd = $blockEnd; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/ForLoopInfo.php: -------------------------------------------------------------------------------- 1 | 79 | */ 80 | public $incrementVariables = []; 81 | 82 | /** 83 | * @param int $forIndex 84 | * @param int $blockStart 85 | * @param int $blockEnd 86 | * @param int $initStart 87 | * @param int $initEnd 88 | * @param int $conditionStart 89 | * @param int $conditionEnd 90 | * @param int $incrementStart 91 | * @param int $incrementEnd 92 | */ 93 | public function __construct( 94 | $forIndex, 95 | $blockStart, 96 | $blockEnd, 97 | $initStart, 98 | $initEnd, 99 | $conditionStart, 100 | $conditionEnd, 101 | $incrementStart, 102 | $incrementEnd 103 | ) { 104 | $this->forIndex = $forIndex; 105 | $this->blockStart = $blockStart; 106 | $this->blockEnd = $blockEnd; 107 | $this->initStart = $initStart; 108 | $this->initEnd = $initEnd; 109 | $this->conditionStart = $conditionStart; 110 | $this->conditionEnd = $conditionEnd; 111 | $this->incrementStart = $incrementStart; 112 | $this->incrementEnd = $incrementEnd; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/Helpers.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function getPossibleEndOfFileTokens() 20 | { 21 | return array_merge( 22 | array_values(Tokens::$emptyTokens), 23 | [ 24 | T_INLINE_HTML, 25 | T_CLOSE_TAG, 26 | ] 27 | ); 28 | } 29 | 30 | /** 31 | * @param int|bool $value 32 | * 33 | * @return ?int 34 | */ 35 | public static function getIntOrNull($value) 36 | { 37 | return is_int($value) ? $value : null; 38 | } 39 | 40 | /** 41 | * Find the position of the square bracket containing the token at $stackPtr, 42 | * if any. 43 | * 44 | * @param File $phpcsFile 45 | * @param int $stackPtr 46 | * 47 | * @return ?int 48 | */ 49 | public static function findContainingOpeningSquareBracket(File $phpcsFile, $stackPtr) 50 | { 51 | // Find the previous bracket within this same statement. 52 | $previousStatementPtr = self::getPreviousStatementPtr($phpcsFile, $stackPtr); 53 | $openBracketPosition = self::getIntOrNull($phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $stackPtr - 1, $previousStatementPtr)); 54 | if (empty($openBracketPosition)) { 55 | return null; 56 | } 57 | // Make sure we are inside the pair of brackets we found. 58 | $tokens = $phpcsFile->getTokens(); 59 | $openBracketToken = $tokens[$openBracketPosition]; 60 | if (empty($openBracketToken) || empty($tokens[$openBracketToken['bracket_closer']])) { 61 | return null; 62 | } 63 | $closeBracketPosition = $openBracketToken['bracket_closer']; 64 | if (empty($closeBracketPosition)) { 65 | return null; 66 | } 67 | if ($stackPtr > $closeBracketPosition) { 68 | return null; 69 | } 70 | return $openBracketPosition; 71 | } 72 | 73 | /** 74 | * @param File $phpcsFile 75 | * @param int $stackPtr 76 | * 77 | * @return int 78 | */ 79 | public static function getPreviousStatementPtr(File $phpcsFile, $stackPtr) 80 | { 81 | $result = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1); 82 | return is_bool($result) ? 1 : $result; 83 | } 84 | 85 | /** 86 | * @param File $phpcsFile 87 | * @param int $stackPtr 88 | * 89 | * @return ?int 90 | */ 91 | public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr) 92 | { 93 | $tokens = $phpcsFile->getTokens(); 94 | if (isset($tokens[$stackPtr]['nested_parenthesis'])) { 95 | /** 96 | * @var array 97 | */ 98 | $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']); 99 | return (int)end($openPtrs); 100 | } 101 | return null; 102 | } 103 | 104 | /** 105 | * @param array{conditions: (int|string)[], content: string} $token 106 | * 107 | * @return bool 108 | */ 109 | public static function areAnyConditionsAClass(array $token) 110 | { 111 | $conditions = $token['conditions']; 112 | $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT]; 113 | if (defined('T_ENUM')) { 114 | $classlikeCodes[] = T_ENUM; 115 | } 116 | $classlikeCodes[] = 'PHPCS_T_ENUM'; 117 | foreach (array_reverse($conditions, true) as $scopeCode) { 118 | if (in_array($scopeCode, $classlikeCodes, true)) { 119 | return true; 120 | } 121 | } 122 | return false; 123 | } 124 | 125 | /** 126 | * Return true if the token conditions are within a function before they are 127 | * within a class. 128 | * 129 | * @param array{conditions: (int|string)[], content: string} $token 130 | * 131 | * @return bool 132 | */ 133 | public static function areConditionsWithinFunctionBeforeClass(array $token) 134 | { 135 | $conditions = $token['conditions']; 136 | $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT]; 137 | if (defined('T_ENUM')) { 138 | $classlikeCodes[] = T_ENUM; 139 | } 140 | $classlikeCodes[] = 'PHPCS_T_ENUM'; 141 | foreach (array_reverse($conditions, true) as $scopeCode) { 142 | if (in_array($scopeCode, $classlikeCodes)) { 143 | return false; 144 | } 145 | if ($scopeCode === T_FUNCTION) { 146 | return true; 147 | } 148 | } 149 | return false; 150 | } 151 | 152 | /** 153 | * Return true if the token conditions are within an IF/ELSE/ELSEIF block 154 | * before they are within a class or function. 155 | * 156 | * @param (int|string)[] $conditions 157 | * 158 | * @return int|string|null 159 | */ 160 | public static function getClosestConditionPositionIfBeforeOtherConditions(array $conditions) 161 | { 162 | $conditionsInsideOut = array_reverse($conditions, true); 163 | if (empty($conditions)) { 164 | return null; 165 | } 166 | $scopeCode = reset($conditionsInsideOut); 167 | $conditionalCodes = [ 168 | T_IF, 169 | T_ELSE, 170 | T_ELSEIF, 171 | ]; 172 | if (in_array($scopeCode, $conditionalCodes, true)) { 173 | return key($conditionsInsideOut); 174 | } 175 | return null; 176 | } 177 | 178 | /** 179 | * @param File $phpcsFile 180 | * @param int $stackPtr 181 | * 182 | * @return bool 183 | */ 184 | public static function isTokenFunctionParameter(File $phpcsFile, $stackPtr) 185 | { 186 | return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr)); 187 | } 188 | 189 | /** 190 | * Return true if the token is inside the arguments of a function call. 191 | * 192 | * For example, the variable `$foo` in `doSomething($foo)` is inside the 193 | * arguments to the call to `doSomething()`. 194 | * 195 | * @param File $phpcsFile 196 | * @param int $stackPtr 197 | * 198 | * @return bool 199 | */ 200 | public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stackPtr) 201 | { 202 | return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr)); 203 | } 204 | 205 | /** 206 | * Find the index of the function keyword for a token in a function 207 | * definition's parameters. 208 | * 209 | * Does not work for tokens inside the "use". 210 | * 211 | * Will also work for the parenthesis that make up the function definition's 212 | * parameters list. 213 | * 214 | * For arguments inside a function call, rather than a definition, use 215 | * `getFunctionIndexForFunctionCallArgument`. 216 | * 217 | * @param File $phpcsFile 218 | * @param int $stackPtr 219 | * 220 | * @return ?int 221 | */ 222 | public static function getFunctionIndexForFunctionParameter(File $phpcsFile, $stackPtr) 223 | { 224 | $tokens = $phpcsFile->getTokens(); 225 | $token = $tokens[$stackPtr]; 226 | if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') { 227 | $startOfArguments = $stackPtr; 228 | } elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') { 229 | if (empty($token['parenthesis_opener'])) { 230 | return null; 231 | } 232 | $startOfArguments = $token['parenthesis_opener']; 233 | } else { 234 | if (empty($token['nested_parenthesis'])) { 235 | return null; 236 | } 237 | $startingParenthesis = array_keys($token['nested_parenthesis']); 238 | $startOfArguments = end($startingParenthesis); 239 | } 240 | 241 | if (! is_int($startOfArguments)) { 242 | return null; 243 | } 244 | 245 | $nonFunctionTokenTypes = Tokens::$emptyTokens; 246 | $nonFunctionTokenTypes[] = T_STRING; 247 | $nonFunctionTokenTypes[] = T_BITWISE_AND; 248 | $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true)); 249 | if (! is_int($functionPtr)) { 250 | return null; 251 | } 252 | $functionToken = $tokens[$functionPtr]; 253 | 254 | $functionTokenTypes = [ 255 | T_FUNCTION, 256 | T_CLOSURE, 257 | ]; 258 | if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) { 259 | return null; 260 | } 261 | return $functionPtr; 262 | } 263 | 264 | /** 265 | * @param File $phpcsFile 266 | * @param int $stackPtr 267 | * 268 | * @return bool 269 | */ 270 | public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr) 271 | { 272 | return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr)); 273 | } 274 | 275 | /** 276 | * Find the token index of the "use" for a token inside a function use import 277 | * 278 | * @param File $phpcsFile 279 | * @param int $stackPtr 280 | * 281 | * @return ?int 282 | */ 283 | public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr) 284 | { 285 | $tokens = $phpcsFile->getTokens(); 286 | 287 | $nonUseTokenTypes = Tokens::$emptyTokens; 288 | $nonUseTokenTypes[] = T_VARIABLE; 289 | $nonUseTokenTypes[] = T_ELLIPSIS; 290 | $nonUseTokenTypes[] = T_COMMA; 291 | $nonUseTokenTypes[] = T_BITWISE_AND; 292 | $openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true)); 293 | if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) { 294 | return null; 295 | } 296 | 297 | $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true)); 298 | if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) { 299 | return null; 300 | } 301 | return $usePtr; 302 | } 303 | 304 | /** 305 | * Return the index of a function's name token from inside the function. 306 | * 307 | * $stackPtr must be inside the function body or parameters for this to work. 308 | * 309 | * @param File $phpcsFile 310 | * @param int $stackPtr 311 | * 312 | * @return ?int 313 | */ 314 | public static function findFunctionCall(File $phpcsFile, $stackPtr) 315 | { 316 | $tokens = $phpcsFile->getTokens(); 317 | 318 | $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr); 319 | if (is_int($openPtr)) { 320 | // First non-whitespace thing and see if it's a T_STRING function name 321 | $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true); 322 | if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) { 323 | return $functionPtr; 324 | } 325 | } 326 | return null; 327 | } 328 | 329 | /** 330 | * @param File $phpcsFile 331 | * @param int $stackPtr 332 | * 333 | * @return array> 334 | */ 335 | public static function findFunctionCallArguments(File $phpcsFile, $stackPtr) 336 | { 337 | $tokens = $phpcsFile->getTokens(); 338 | 339 | // Slight hack: also allow this to find args for array constructor. 340 | if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) { 341 | // Assume $stackPtr is something within the brackets, find our function call 342 | $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr); 343 | if ($stackPtr === null) { 344 | return []; 345 | } 346 | } 347 | 348 | // $stackPtr is the function name, find our brackets after it 349 | $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true); 350 | if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) { 351 | return []; 352 | } 353 | 354 | if (!isset($tokens[$openPtr]['parenthesis_closer'])) { 355 | return []; 356 | } 357 | $closePtr = $tokens[$openPtr]['parenthesis_closer']; 358 | 359 | $argPtrs = []; 360 | $lastPtr = $openPtr; 361 | $lastArgComma = $openPtr; 362 | $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr); 363 | while (is_int($nextPtr)) { 364 | if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) { 365 | // Comma is at our level of brackets, it's an argument delimiter. 366 | $range = range($lastArgComma + 1, $nextPtr - 1); 367 | $range = array_filter($range, function ($element) { 368 | return is_int($element); 369 | }); 370 | array_push($argPtrs, $range); 371 | $lastArgComma = $nextPtr; 372 | } 373 | $lastPtr = $nextPtr; 374 | $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr); 375 | } 376 | $range = range($lastArgComma + 1, $closePtr - 1); 377 | $range = array_filter($range, function ($element) { 378 | return is_int($element); 379 | }); 380 | array_push($argPtrs, $range); 381 | 382 | return $argPtrs; 383 | } 384 | 385 | /** 386 | * @param File $phpcsFile 387 | * @param int $stackPtr 388 | * 389 | * @return ?int 390 | */ 391 | public static function getNextAssignPointer(File $phpcsFile, $stackPtr) 392 | { 393 | $tokens = $phpcsFile->getTokens(); 394 | 395 | // Is the next non-whitespace an assignment? 396 | $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true); 397 | if (is_int($nextPtr) 398 | && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']]) 399 | // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`. 400 | && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW 401 | ) { 402 | return $nextPtr; 403 | } 404 | return null; 405 | } 406 | 407 | /** 408 | * @param string $varName 409 | * 410 | * @return string 411 | */ 412 | public static function normalizeVarName($varName) 413 | { 414 | $result = preg_replace('/[{}$]/', '', $varName); 415 | return $result ? $result : $varName; 416 | } 417 | 418 | /** 419 | * @param File $phpcsFile 420 | * @param int $stackPtr 421 | * @param string $varName (optional) if it differs from the normalized 'content' of the token at $stackPtr 422 | * 423 | * @return ?int 424 | */ 425 | public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null) 426 | { 427 | $tokens = $phpcsFile->getTokens(); 428 | $token = $tokens[$stackPtr]; 429 | $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']); 430 | 431 | $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr); 432 | 433 | if (!is_null($enclosingScopeIndex)) { 434 | $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); 435 | $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex); 436 | if ($isTokenInsideArrowFunctionBody) { 437 | // Get the list of variables defined by the arrow function 438 | // If this matches any of them, the scope is the arrow function, 439 | // otherwise, it uses the enclosing scope. 440 | if ($arrowFunctionIndex) { 441 | $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex); 442 | self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames); 443 | if (in_array($varName, $variableNames, true)) { 444 | return $arrowFunctionIndex; 445 | } 446 | } 447 | } 448 | } 449 | 450 | return $enclosingScopeIndex; 451 | } 452 | 453 | /** 454 | * Return the variable names and positions of each variable targetted by a `compact()` call. 455 | * 456 | * @param File $phpcsFile 457 | * @param int $stackPtr 458 | * @param array> $arguments The stack pointers of each argument; see findFunctionCallArguments 459 | * 460 | * @return array each variable's firstRead position and its name; other VariableInfo properties are not set! 461 | */ 462 | public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments) 463 | { 464 | $tokens = $phpcsFile->getTokens(); 465 | $variablePositionsAndNames = []; 466 | 467 | foreach ($arguments as $argumentPtrs) { 468 | $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) { 469 | return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false; 470 | })); 471 | if (empty($argumentPtrs)) { 472 | continue; 473 | } 474 | if (!isset($tokens[$argumentPtrs[0]])) { 475 | continue; 476 | } 477 | $argumentFirstToken = $tokens[$argumentPtrs[0]]; 478 | if ($argumentFirstToken['code'] === T_ARRAY) { 479 | // It's an array argument, recurse. 480 | $arrayArguments = self::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]); 481 | $variablePositionsAndNames = array_merge($variablePositionsAndNames, self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments)); 482 | continue; 483 | } 484 | if (count($argumentPtrs) > 1) { 485 | // Complex argument, we can't handle it, ignore. 486 | continue; 487 | } 488 | if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) { 489 | // Single-quoted string literal, ie compact('whatever'). 490 | // Substr is to strip the enclosing single-quotes. 491 | $varName = substr($argumentFirstToken['content'], 1, -1); 492 | $variable = new VariableInfo($varName); 493 | $variable->firstRead = $argumentPtrs[0]; 494 | $variablePositionsAndNames[] = $variable; 495 | continue; 496 | } 497 | if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) { 498 | // Double-quoted string literal. 499 | $regexp = Constants::getDoubleQuotedVarRegexp(); 500 | if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) { 501 | // Bail if the string needs variable expansion, that's runtime stuff. 502 | continue; 503 | } 504 | // Substr is to strip the enclosing double-quotes. 505 | $varName = substr($argumentFirstToken['content'], 1, -1); 506 | $variable = new VariableInfo($varName); 507 | $variable->firstRead = $argumentPtrs[0]; 508 | $variablePositionsAndNames[] = $variable; 509 | continue; 510 | } 511 | } 512 | return $variablePositionsAndNames; 513 | } 514 | 515 | /** 516 | * Return the token index of the scope start for a token 517 | * 518 | * For a variable within a function body, or a variable within a function 519 | * definition argument list, this will return the function keyword's index. 520 | * 521 | * For a variable within a "use" import list within a function definition, 522 | * this will return the enclosing scope, not the function keyword. This is 523 | * important to note because the "use" keyword performs double-duty, defining 524 | * variables for the function's scope, and consuming the variables in the 525 | * enclosing scope. Use `getUseIndexForUseImport` to determine if this 526 | * token needs to be treated as a "use". 527 | * 528 | * For a variable within an arrow function definition argument list, 529 | * this will return the arrow function's keyword index. 530 | * 531 | * For a variable in an arrow function body, this will return the enclosing 532 | * function's index, which may be incorrect. 533 | * 534 | * Since a variable in an arrow function's body may be imported from the 535 | * enclosing scope, it's important to test to see if the variable is in an 536 | * arrow function and also check its enclosing scope separately. 537 | * 538 | * @param File $phpcsFile 539 | * @param int $stackPtr 540 | * 541 | * @return ?int 542 | */ 543 | public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr) 544 | { 545 | $tokens = $phpcsFile->getTokens(); 546 | $allowedTypes = [ 547 | T_VARIABLE, 548 | T_DOUBLE_QUOTED_STRING, 549 | T_HEREDOC, 550 | T_STRING, 551 | ]; 552 | if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) { 553 | throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}"); 554 | } 555 | 556 | $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr); 557 | if (is_int($startOfTokenScope) && $startOfTokenScope > 0) { 558 | return $startOfTokenScope; 559 | } 560 | 561 | // If there is no "conditions" array, this is a function definition argument. 562 | if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) { 563 | $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr); 564 | if (! is_int($functionPtr)) { 565 | throw new \Exception("Function index not found for function argument index {$stackPtr}"); 566 | } 567 | return $functionPtr; 568 | } 569 | 570 | self::debug('Cannot find function scope for variable at', $stackPtr); 571 | return $startOfTokenScope; 572 | } 573 | 574 | /** 575 | * Return the token index of the scope start for a variable token 576 | * 577 | * This will only work for a variable within a function's body. Otherwise, 578 | * see `findVariableScope`, which is more complex. 579 | * 580 | * Note that if used on a variable in an arrow function, it will return the 581 | * enclosing function's scope, which may be incorrect. 582 | * 583 | * @param File $phpcsFile 584 | * @param int $stackPtr 585 | * 586 | * @return ?int 587 | */ 588 | private static function getStartOfTokenScope(File $phpcsFile, $stackPtr) 589 | { 590 | $tokens = $phpcsFile->getTokens(); 591 | $token = $tokens[$stackPtr]; 592 | 593 | $inClass = false; 594 | $conditions = isset($token['conditions']) ? $token['conditions'] : []; 595 | $functionTokenTypes = [ 596 | T_FUNCTION, 597 | T_CLOSURE, 598 | ]; 599 | foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) { 600 | if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) { 601 | return $scopePtr; 602 | } 603 | if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) { 604 | $inClass = true; 605 | } 606 | } 607 | 608 | if ($inClass) { 609 | // If this is inside a class and not inside a function, this is either a 610 | // class member variable definition, or a function argument. If it is a 611 | // variable definition, it has no scope on its own (it can only be used 612 | // with an object reference). If it is a function argument, we need to do 613 | // more work (see `findVariableScopeExceptArrowFunctions`). 614 | return null; 615 | } 616 | 617 | // If we can't find a scope, let's use the first token of the file. 618 | return 0; 619 | } 620 | 621 | /** 622 | * @param File $phpcsFile 623 | * @param int $stackPtr 624 | * 625 | * @return bool 626 | */ 627 | public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr) 628 | { 629 | $tokens = $phpcsFile->getTokens(); 630 | $token = $tokens[$stackPtr]; 631 | $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : []; 632 | if (empty($openParenIndices)) { 633 | return false; 634 | } 635 | $openParenPtr = $openParenIndices[0]; 636 | return self::isArrowFunction($phpcsFile, $openParenPtr - 1); 637 | } 638 | 639 | /** 640 | * @param File $phpcsFile 641 | * @param int $stackPtr 642 | * @param int $enclosingScopeIndex 643 | * 644 | * @return ?int 645 | */ 646 | public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex) 647 | { 648 | $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex); 649 | if (! is_int($arrowFunctionIndex)) { 650 | return null; 651 | } 652 | $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex); 653 | if (! $arrowFunctionInfo) { 654 | return null; 655 | } 656 | 657 | // We found the closest arrow function before this token. If the token is 658 | // within the scope of that arrow function, then return it. 659 | if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) { 660 | return $arrowFunctionIndex; 661 | } 662 | 663 | // If the token is after the scope of the closest arrow function, we may 664 | // still be inside the scope of a nested arrow function, so we need to 665 | // search further back until we are certain there are no more arrow 666 | // functions. 667 | if ($stackPtr > $arrowFunctionInfo['scope_closer']) { 668 | return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex); 669 | } 670 | 671 | return null; 672 | } 673 | 674 | /** 675 | * Move back from the stackPtr to the start of the enclosing scope until we 676 | * find a 'fn' token that starts an arrow function, returning the index of 677 | * that token. Returns null if there are no arrow functions before stackPtr. 678 | * 679 | * Note that this does not guarantee that stackPtr is inside the arrow 680 | * function scope we find! 681 | * 682 | * @param File $phpcsFile 683 | * @param int $stackPtr 684 | * @param int $enclosingScopeIndex 685 | * 686 | * @return ?int 687 | */ 688 | private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex) 689 | { 690 | $tokens = $phpcsFile->getTokens(); 691 | for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) { 692 | $token = $tokens[$index]; 693 | if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) { 694 | return $index; 695 | } 696 | } 697 | return null; 698 | } 699 | 700 | /** 701 | * @param File $phpcsFile 702 | * @param int $stackPtr 703 | * 704 | * @return bool 705 | */ 706 | public static function isArrowFunction(File $phpcsFile, $stackPtr) 707 | { 708 | $tokens = $phpcsFile->getTokens(); 709 | if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) { 710 | return true; 711 | } 712 | if ($tokens[$stackPtr]['content'] !== 'fn') { 713 | return false; 714 | } 715 | // Make sure next non-space token is an open parenthesis 716 | $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); 717 | if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) { 718 | return false; 719 | } 720 | // Find the associated close parenthesis 721 | $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer']; 722 | // Make sure the next token is a fat arrow 723 | $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true); 724 | if (! is_int($fatArrowIndex)) { 725 | return false; 726 | } 727 | if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') { 728 | return false; 729 | } 730 | return true; 731 | } 732 | 733 | /** 734 | * Find the opening and closing scope positions for an arrow function if the 735 | * given position is the start of the arrow function (the `fn` keyword 736 | * token). 737 | * 738 | * Returns null if the passed token is not an arrow function keyword. 739 | * 740 | * If the token is an arrow function keyword, the scope opener is returned as 741 | * the provided position. 742 | * 743 | * @param File $phpcsFile 744 | * @param int $stackPtr 745 | * 746 | * @return ?array 747 | */ 748 | public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr) 749 | { 750 | $tokens = $phpcsFile->getTokens(); 751 | if ($tokens[$stackPtr]['content'] !== 'fn') { 752 | return null; 753 | } 754 | // Make sure next non-space token is an open parenthesis 755 | $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); 756 | if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) { 757 | return null; 758 | } 759 | // Find the associated close parenthesis 760 | $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer']; 761 | // Make sure the next token is a fat arrow or a return type 762 | $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true); 763 | if (! is_int($fatArrowIndex)) { 764 | return null; 765 | } 766 | if ( 767 | $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && 768 | $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' && 769 | $tokens[$fatArrowIndex]['code'] !== T_COLON 770 | ) { 771 | return null; 772 | } 773 | 774 | // Find the scope closer 775 | $scopeCloserIndex = null; 776 | $foundCurlyPairs = 0; 777 | $foundArrayPairs = 0; 778 | $foundParenPairs = 0; 779 | $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1; 780 | $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile); 781 | for ($index = $arrowBodyStart; $index < $lastToken; $index++) { 782 | $token = $tokens[$index]; 783 | if (empty($token['code'])) { 784 | $scopeCloserIndex = $index; 785 | break; 786 | } 787 | 788 | $code = $token['code']; 789 | 790 | // A semicolon is always a closer. 791 | if ($code === T_SEMICOLON) { 792 | $scopeCloserIndex = $index; 793 | break; 794 | } 795 | 796 | // Track pair opening tokens. 797 | if ($code === T_OPEN_CURLY_BRACKET) { 798 | $foundCurlyPairs += 1; 799 | continue; 800 | } 801 | if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) { 802 | $foundArrayPairs += 1; 803 | continue; 804 | } 805 | if ($code === T_OPEN_PARENTHESIS) { 806 | $foundParenPairs += 1; 807 | continue; 808 | } 809 | 810 | // A pair closing is only an arrow func closer if there was no matching opening token. 811 | if ($code === T_CLOSE_CURLY_BRACKET) { 812 | if ($foundCurlyPairs === 0) { 813 | $scopeCloserIndex = $index; 814 | break; 815 | } 816 | $foundCurlyPairs -= 1; 817 | continue; 818 | } 819 | if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) { 820 | if ($foundArrayPairs === 0) { 821 | $scopeCloserIndex = $index; 822 | break; 823 | } 824 | $foundArrayPairs -= 1; 825 | continue; 826 | } 827 | if ($code === T_CLOSE_PARENTHESIS) { 828 | if ($foundParenPairs === 0) { 829 | $scopeCloserIndex = $index; 830 | break; 831 | } 832 | $foundParenPairs -= 1; 833 | continue; 834 | } 835 | 836 | // A comma is a closer only if we are not inside an opening token. 837 | if ($code === T_COMMA) { 838 | if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) { 839 | $scopeCloserIndex = $index; 840 | break; 841 | } 842 | continue; 843 | } 844 | } 845 | 846 | if (! is_int($scopeCloserIndex)) { 847 | return null; 848 | } 849 | 850 | return [ 851 | 'scope_opener' => $stackPtr, 852 | 'scope_closer' => $scopeCloserIndex, 853 | ]; 854 | } 855 | 856 | /** 857 | * Determine if a token is a list opener for list assignment/destructuring. 858 | * 859 | * The index provided can be either the opening square brace of a short list 860 | * assignment like the first character of `[$a] = $b;` or the `list` token of 861 | * an expression like `list($a) = $b;` or the opening parenthesis of that 862 | * expression. 863 | * 864 | * @param File $phpcsFile 865 | * @param int $listOpenerIndex 866 | * 867 | * @return bool 868 | */ 869 | private static function isListAssignment(File $phpcsFile, $listOpenerIndex) 870 | { 871 | $tokens = $phpcsFile->getTokens(); 872 | // Match `[$a] = $b;` except for when the previous token is a parenthesis. 873 | if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) { 874 | return true; 875 | } 876 | // Match `list($a) = $b;` 877 | if ($tokens[$listOpenerIndex]['code'] === T_LIST) { 878 | return true; 879 | } 880 | 881 | // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then 882 | // match that too. 883 | if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) { 884 | $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true); 885 | if ( 886 | isset($tokens[$previousTokenPtr]) 887 | && $tokens[$previousTokenPtr]['code'] === T_LIST 888 | ) { 889 | return true; 890 | } 891 | return true; 892 | } 893 | 894 | // If the list opener token is a square bracket that is preceeded by a 895 | // close parenthesis that has an owner which is a scope opener, then this 896 | // is a list assignment and not an array access. 897 | // 898 | // Match `if (true) [$a] = $b;` 899 | if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) { 900 | $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true); 901 | if ( 902 | isset($tokens[$previousTokenPtr]) 903 | && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS 904 | && isset($tokens[$previousTokenPtr]['parenthesis_owner']) 905 | && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']]) 906 | ) { 907 | return true; 908 | } 909 | } 910 | 911 | return false; 912 | } 913 | 914 | /** 915 | * Return a list of indices for variables assigned within a list assignment. 916 | * 917 | * The index provided can be either the opening square brace of a short list 918 | * assignment like the first character of `[$a] = $b;` or the `list` token of 919 | * an expression like `list($a) = $b;` or the opening parenthesis of that 920 | * expression. 921 | * 922 | * @param File $phpcsFile 923 | * @param int $listOpenerIndex 924 | * 925 | * @return ?array 926 | */ 927 | public static function getListAssignments(File $phpcsFile, $listOpenerIndex) 928 | { 929 | $tokens = $phpcsFile->getTokens(); 930 | self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]); 931 | 932 | // First find the end of the list 933 | $closePtr = null; 934 | if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) { 935 | $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer']; 936 | } 937 | if (isset($tokens[$listOpenerIndex]['bracket_closer'])) { 938 | $closePtr = $tokens[$listOpenerIndex]['bracket_closer']; 939 | } 940 | if (! $closePtr) { 941 | return null; 942 | } 943 | 944 | // Find the assignment (equals sign) which, if this is a list assignment, should be the next non-space token 945 | $assignPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true); 946 | 947 | // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment 948 | if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) { 949 | // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index) 950 | $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : []; 951 | // There's no record of nested brackets for short lists; we'll have to find the parent ourselves 952 | if (empty($parents)) { 953 | $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex); 954 | if (is_int($parentSquareBracketPtr)) { 955 | // Make sure that the parent is really a parent by checking that its 956 | // closing index is outside of the current bracket's closing index. 957 | $parentSquareBracketToken = $tokens[$parentSquareBracketPtr]; 958 | $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer']; 959 | if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) { 960 | self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}"); 961 | // Collect the opening index, but we don't actually need the closing paren index so just make that 0 962 | $parents = [$parentSquareBracketPtr => 0]; 963 | } 964 | } 965 | } 966 | // If we have no parents, this is not a nested assignment and therefore is not an assignment 967 | if (empty($parents)) { 968 | return null; 969 | } 970 | 971 | // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion) 972 | $isNestedAssignment = null; 973 | $parentListOpener = array_keys(array_reverse($parents, true))[0]; 974 | $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener); 975 | if ($isNestedAssignment === null) { 976 | return null; 977 | } 978 | } 979 | 980 | $variablePtrs = []; 981 | 982 | $currentPtr = $listOpenerIndex; 983 | $variablePtr = 0; 984 | while ($currentPtr < $closePtr && is_int($variablePtr)) { 985 | $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr); 986 | if (is_int($variablePtr)) { 987 | $variablePtrs[] = $variablePtr; 988 | } 989 | ++$currentPtr; 990 | } 991 | 992 | if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) { 993 | return null; 994 | } 995 | 996 | return $variablePtrs; 997 | } 998 | 999 | /** 1000 | * @param File $phpcsFile 1001 | * @param int $stackPtr 1002 | * 1003 | * @return string[] 1004 | */ 1005 | public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr) 1006 | { 1007 | $tokens = $phpcsFile->getTokens(); 1008 | $arrowFunctionToken = $tokens[$stackPtr]; 1009 | $variableNames = []; 1010 | self::debug('looking for variables in arrow function token', $arrowFunctionToken); 1011 | for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) { 1012 | $token = $tokens[$index]; 1013 | if ($token['code'] === T_VARIABLE) { 1014 | $variableNames[] = self::normalizeVarName($token['content']); 1015 | } 1016 | } 1017 | self::debug('found these variables in arrow function token', $variableNames); 1018 | return $variableNames; 1019 | } 1020 | 1021 | /** 1022 | * @return void 1023 | */ 1024 | public static function debug() 1025 | { 1026 | $messages = func_get_args(); 1027 | if (! defined('PHP_CODESNIFFER_VERBOSITY')) { 1028 | return; 1029 | } 1030 | if (PHP_CODESNIFFER_VERBOSITY <= 3) { 1031 | return; 1032 | } 1033 | $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:'; 1034 | foreach ($messages as $message) { 1035 | if (is_string($message) || is_numeric($message)) { 1036 | $output .= ' "' . $message . '"'; 1037 | continue; 1038 | } 1039 | $output .= PHP_EOL . var_export($message, true) . PHP_EOL; 1040 | } 1041 | $output .= PHP_EOL; 1042 | echo $output; 1043 | } 1044 | 1045 | /** 1046 | * @param string $pattern 1047 | * @param string $value 1048 | * 1049 | * @return string[] 1050 | */ 1051 | public static function splitStringToArray($pattern, $value) 1052 | { 1053 | if (empty($pattern)) { 1054 | return []; 1055 | } 1056 | $result = preg_split($pattern, $value); 1057 | return is_array($result) ? $result : []; 1058 | } 1059 | 1060 | /** 1061 | * @param string $varName 1062 | * 1063 | * @return bool 1064 | */ 1065 | public static function isVariableANumericVariable($varName) 1066 | { 1067 | return is_numeric(substr($varName, 0, 1)); 1068 | } 1069 | 1070 | /** 1071 | * @param File $phpcsFile 1072 | * @param int $stackPtr 1073 | * 1074 | * @return bool 1075 | */ 1076 | public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr) 1077 | { 1078 | $tokens = $phpcsFile->getTokens(); 1079 | $nonFunctionTokenTypes = Tokens::$emptyTokens; 1080 | $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS; 1081 | $nonFunctionTokenTypes[] = T_INLINE_HTML; 1082 | $nonFunctionTokenTypes[] = T_CLOSE_TAG; 1083 | $nonFunctionTokenTypes[] = T_VARIABLE; 1084 | $nonFunctionTokenTypes[] = T_ELLIPSIS; 1085 | $nonFunctionTokenTypes[] = T_COMMA; 1086 | $nonFunctionTokenTypes[] = T_STRING; 1087 | $nonFunctionTokenTypes[] = T_BITWISE_AND; 1088 | $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true)); 1089 | $elseTokenTypes = [ 1090 | T_ELSE, 1091 | T_ELSEIF, 1092 | ]; 1093 | if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) { 1094 | return true; 1095 | } 1096 | return false; 1097 | } 1098 | 1099 | /** 1100 | * @param File $phpcsFile 1101 | * @param int $stackPtr 1102 | * 1103 | * @return bool 1104 | */ 1105 | public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr) 1106 | { 1107 | $tokens = $phpcsFile->getTokens(); 1108 | $token = $tokens[$stackPtr]; 1109 | $conditions = isset($token['conditions']) ? $token['conditions'] : []; 1110 | $elseTokenTypes = [ 1111 | T_ELSE, 1112 | T_ELSEIF, 1113 | ]; 1114 | foreach (array_reverse($conditions, true) as $scopeCode) { 1115 | if (in_array($scopeCode, $elseTokenTypes, true)) { 1116 | return true; 1117 | } 1118 | } 1119 | 1120 | // Some else body code will not have conditions because it is inline (no 1121 | // curly braces) so we have to look in other ways. 1122 | $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1); 1123 | if (! is_int($previousSemicolonPtr)) { 1124 | $previousSemicolonPtr = 0; 1125 | } 1126 | $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr); 1127 | if (is_int($elsePtr)) { 1128 | return true; 1129 | } 1130 | 1131 | return false; 1132 | } 1133 | 1134 | /** 1135 | * @param File $phpcsFile 1136 | * @param int $stackPtr 1137 | * 1138 | * @return int[] 1139 | */ 1140 | public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr) 1141 | { 1142 | $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1); 1143 | if (! is_int($currentElsePtr)) { 1144 | throw new \Exception("Cannot find expected else at {$stackPtr}"); 1145 | } 1146 | 1147 | $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1); 1148 | if (! is_int($ifPtr)) { 1149 | throw new \Exception("Cannot find if for else at {$stackPtr}"); 1150 | } 1151 | $blockIndices = [$ifPtr]; 1152 | 1153 | $previousElseIfPtr = $currentElsePtr; 1154 | do { 1155 | $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr); 1156 | if (is_int($elseIfPtr)) { 1157 | $blockIndices[] = $elseIfPtr; 1158 | $previousElseIfPtr = $elseIfPtr; 1159 | } 1160 | } while (is_int($elseIfPtr)); 1161 | 1162 | return $blockIndices; 1163 | } 1164 | 1165 | /** 1166 | * @param int $needle 1167 | * @param int $scopeStart 1168 | * @param int $scopeEnd 1169 | * 1170 | * @return bool 1171 | */ 1172 | public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd) 1173 | { 1174 | return ($needle > $scopeStart && $needle < $scopeEnd); 1175 | } 1176 | 1177 | /** 1178 | * @param File $phpcsFile 1179 | * @param int $scopeStartIndex 1180 | * 1181 | * @return int 1182 | */ 1183 | public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex) 1184 | { 1185 | $tokens = $phpcsFile->getTokens(); 1186 | $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0; 1187 | 1188 | if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) { 1189 | $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex); 1190 | $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex; 1191 | } 1192 | 1193 | if ($scopeStartIndex === 0) { 1194 | $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile); 1195 | } 1196 | return $scopeCloserIndex; 1197 | } 1198 | 1199 | /** 1200 | * @param File $phpcsFile 1201 | * 1202 | * @return int 1203 | */ 1204 | public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile) 1205 | { 1206 | $tokens = $phpcsFile->getTokens(); 1207 | foreach (array_reverse($tokens, true) as $index => $token) { 1208 | if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) { 1209 | return $index; 1210 | } 1211 | } 1212 | self::debug('no non-empty token found for end of file'); 1213 | return 0; 1214 | } 1215 | 1216 | /** 1217 | * @param VariableInfo $varInfo 1218 | * @param ScopeInfo $scopeInfo 1219 | * 1220 | * @return bool 1221 | */ 1222 | public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo) 1223 | { 1224 | $foundVarPosition = false; 1225 | foreach ($scopeInfo->variables as $variable) { 1226 | if ($variable === $varInfo) { 1227 | $foundVarPosition = true; 1228 | continue; 1229 | } 1230 | if (! $foundVarPosition) { 1231 | continue; 1232 | } 1233 | if ($variable->scopeType !== ScopeType::PARAM) { 1234 | continue; 1235 | } 1236 | if ($variable->firstRead) { 1237 | return true; 1238 | } 1239 | } 1240 | return false; 1241 | } 1242 | 1243 | /** 1244 | * @param File $phpcsFile 1245 | * @param VariableInfo $varInfo 1246 | * @param ScopeInfo $scopeInfo 1247 | * 1248 | * @return bool 1249 | */ 1250 | public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo) 1251 | { 1252 | $requireTokens = [ 1253 | T_REQUIRE, 1254 | T_REQUIRE_ONCE, 1255 | T_INCLUDE, 1256 | T_INCLUDE_ONCE, 1257 | ]; 1258 | $indexToStartSearch = $varInfo->firstDeclared; 1259 | if (! empty($varInfo->firstInitialized)) { 1260 | $indexToStartSearch = $varInfo->firstInitialized; 1261 | } 1262 | $tokens = $phpcsFile->getTokens(); 1263 | $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null; 1264 | if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) { 1265 | return false; 1266 | } 1267 | $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch); 1268 | if (is_int($requireTokenIndex)) { 1269 | return true; 1270 | } 1271 | return false; 1272 | } 1273 | 1274 | /** 1275 | * Find the index of the function keyword for a token in a function call's arguments 1276 | * 1277 | * For the variable `$foo` in the expression `doSomething($foo)`, this will 1278 | * return the index of the `doSomething` token. 1279 | * 1280 | * @param File $phpcsFile 1281 | * @param int $stackPtr 1282 | * 1283 | * @return ?int 1284 | */ 1285 | public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr) 1286 | { 1287 | $tokens = $phpcsFile->getTokens(); 1288 | $token = $tokens[$stackPtr]; 1289 | if (empty($token['nested_parenthesis'])) { 1290 | return null; 1291 | } 1292 | /** 1293 | * @var array 1294 | */ 1295 | $startingParenthesis = array_keys($token['nested_parenthesis']); 1296 | $startOfArguments = end($startingParenthesis); 1297 | if (! is_int($startOfArguments)) { 1298 | return null; 1299 | } 1300 | 1301 | $nonFunctionTokenTypes = Tokens::$emptyTokens; 1302 | $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true)); 1303 | if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) { 1304 | return null; 1305 | } 1306 | if ( 1307 | $tokens[$functionPtr]['content'] === 'function' 1308 | || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr)) 1309 | ) { 1310 | // If there is a function/fn keyword before the beginning of the parens, 1311 | // this is a function definition and not a function call. 1312 | return null; 1313 | } 1314 | if (! empty($tokens[$functionPtr]['scope_opener'])) { 1315 | // If the alleged function name has a scope, this is not a function call. 1316 | return null; 1317 | } 1318 | 1319 | $functionNameType = $tokens[$functionPtr]['code']; 1320 | if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) { 1321 | // If the alleged function name is not a variable or a string, this is 1322 | // not a function call. 1323 | return null; 1324 | } 1325 | 1326 | if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) { 1327 | // If the variable is inside a different scope than the function name, 1328 | // the function call doesn't apply to the variable. 1329 | return null; 1330 | } 1331 | 1332 | return $functionPtr; 1333 | } 1334 | 1335 | /** 1336 | * @param File $phpcsFile 1337 | * @param int $stackPtr 1338 | * 1339 | * @return bool 1340 | */ 1341 | public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr) 1342 | { 1343 | $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr); 1344 | if (! is_int($functionIndex)) { 1345 | return false; 1346 | } 1347 | $tokens = $phpcsFile->getTokens(); 1348 | if (! isset($tokens[$functionIndex])) { 1349 | return false; 1350 | } 1351 | $allowedFunctionNames = [ 1352 | 'isset', 1353 | 'empty', 1354 | ]; 1355 | if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) { 1356 | return true; 1357 | } 1358 | return false; 1359 | } 1360 | 1361 | /** 1362 | * @param File $phpcsFile 1363 | * @param int $stackPtr 1364 | * 1365 | * @return bool 1366 | */ 1367 | public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr) 1368 | { 1369 | $tokens = $phpcsFile->getTokens(); 1370 | $nonFunctionTokenTypes = Tokens::$emptyTokens; 1371 | 1372 | $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true)); 1373 | if (! is_int($arrayPushOperatorIndex1)) { 1374 | return false; 1375 | } 1376 | if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') { 1377 | return false; 1378 | } 1379 | 1380 | $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true)); 1381 | if (! is_int($arrayPushOperatorIndex2)) { 1382 | return false; 1383 | } 1384 | if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') { 1385 | return false; 1386 | } 1387 | 1388 | $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true)); 1389 | if (! is_int($arrayPushOperatorIndex3)) { 1390 | return false; 1391 | } 1392 | if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') { 1393 | return false; 1394 | } 1395 | 1396 | return true; 1397 | } 1398 | 1399 | /** 1400 | * @param File $phpcsFile 1401 | * @param int $stackPtr 1402 | * 1403 | * @return bool 1404 | */ 1405 | public static function isVariableInsideUnset(File $phpcsFile, $stackPtr) 1406 | { 1407 | $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr); 1408 | if (! is_int($functionIndex)) { 1409 | return false; 1410 | } 1411 | $tokens = $phpcsFile->getTokens(); 1412 | if (! isset($tokens[$functionIndex])) { 1413 | return false; 1414 | } 1415 | if ($tokens[$functionIndex]['content'] === 'unset') { 1416 | return true; 1417 | } 1418 | return false; 1419 | } 1420 | 1421 | /** 1422 | * @param File $phpcsFile 1423 | * @param int $stackPtr 1424 | * 1425 | * @return bool 1426 | */ 1427 | public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr) 1428 | { 1429 | $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1); 1430 | if (! is_int($previousStatementPtr)) { 1431 | $previousStatementPtr = 1; 1432 | } 1433 | $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr); 1434 | if (is_int($previousTokenPtr)) { 1435 | return true; 1436 | } 1437 | return false; 1438 | } 1439 | 1440 | /** 1441 | * @param File $phpcsFile 1442 | * @param int $stackPtr 1443 | * 1444 | * @return bool 1445 | */ 1446 | public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr) 1447 | { 1448 | // Is the next non-whitespace an assignment? 1449 | $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr); 1450 | if (! is_int($assignPtr)) { 1451 | return false; 1452 | } 1453 | 1454 | // Is this a variable variable? If so, it's not an assignment to the current variable. 1455 | if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) { 1456 | self::debug('found variable variable'); 1457 | return false; 1458 | } 1459 | return true; 1460 | } 1461 | 1462 | /** 1463 | * @param File $phpcsFile 1464 | * @param int $stackPtr 1465 | * 1466 | * @return bool 1467 | */ 1468 | public static function isTokenVariableVariable(File $phpcsFile, $stackPtr) 1469 | { 1470 | $tokens = $phpcsFile->getTokens(); 1471 | 1472 | $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 1473 | if ($prev === false) { 1474 | return false; 1475 | } 1476 | if ($tokens[$prev]['code'] === T_DOLLAR) { 1477 | return true; 1478 | } 1479 | if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) { 1480 | return false; 1481 | } 1482 | 1483 | $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true); 1484 | if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) { 1485 | return true; 1486 | } 1487 | return false; 1488 | } 1489 | 1490 | /** 1491 | * @param File $phpcsFile 1492 | * @param int $stackPtr 1493 | * 1494 | * @return EnumInfo|null 1495 | */ 1496 | public static function makeEnumInfo(File $phpcsFile, $stackPtr) 1497 | { 1498 | $tokens = $phpcsFile->getTokens(); 1499 | $token = $tokens[$stackPtr]; 1500 | 1501 | if (isset($token['scope_opener'])) { 1502 | $blockStart = $token['scope_opener']; 1503 | $blockEnd = $token['scope_closer']; 1504 | } else { 1505 | // Enums before phpcs could detect them do not have scopes so we have to 1506 | // find them ourselves. 1507 | 1508 | $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1); 1509 | if (! is_int($blockStart)) { 1510 | return null; 1511 | } 1512 | $blockEnd = $tokens[$blockStart]['bracket_closer']; 1513 | } 1514 | 1515 | return new EnumInfo( 1516 | $stackPtr, 1517 | $blockStart, 1518 | $blockEnd 1519 | ); 1520 | } 1521 | 1522 | /** 1523 | * @param File $phpcsFile 1524 | * @param int $stackPtr 1525 | * 1526 | * @return ForLoopInfo 1527 | */ 1528 | public static function makeForLoopInfo(File $phpcsFile, $stackPtr) 1529 | { 1530 | $tokens = $phpcsFile->getTokens(); 1531 | $token = $tokens[$stackPtr]; 1532 | $forIndex = $stackPtr; 1533 | $blockStart = $token['parenthesis_closer']; 1534 | if (isset($token['scope_opener'])) { 1535 | $blockStart = $token['scope_opener']; 1536 | $blockEnd = $token['scope_closer']; 1537 | } else { 1538 | // Some for loop blocks will not have scope positions because it they are 1539 | // inline (no curly braces) so we have to find the end of their scope by 1540 | // looking for the end of the next statement. 1541 | $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']); 1542 | if (! is_int($nextSemicolonIndex)) { 1543 | $nextSemicolonIndex = $token['parenthesis_closer'] + 1; 1544 | } 1545 | $blockEnd = $nextSemicolonIndex; 1546 | } 1547 | $initStart = intval($token['parenthesis_opener']) + 1; 1548 | $initEnd = null; 1549 | $conditionStart = null; 1550 | $conditionEnd = null; 1551 | $incrementStart = null; 1552 | $incrementEnd = $token['parenthesis_closer'] - 1; 1553 | 1554 | $semicolonCount = 0; 1555 | $forLoopLevel = $tokens[$forIndex]['level']; 1556 | $forLoopNestedParensCount = 1; 1557 | 1558 | if (isset($tokens[$forIndex]['nested_parenthesis'])) { 1559 | $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1; 1560 | } 1561 | 1562 | for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) { 1563 | if ($tokens[$i]['code'] !== T_SEMICOLON) { 1564 | continue; 1565 | } 1566 | 1567 | if ($tokens[$i]['level'] !== $forLoopLevel) { 1568 | continue; 1569 | } 1570 | 1571 | if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) { 1572 | continue; 1573 | } 1574 | 1575 | switch ($semicolonCount) { 1576 | case 0: 1577 | $initEnd = $i; 1578 | $conditionStart = $initEnd + 1; 1579 | break; 1580 | case 1: 1581 | $conditionEnd = $i; 1582 | $incrementStart = $conditionEnd + 1; 1583 | break; 1584 | } 1585 | $semicolonCount += 1; 1586 | } 1587 | 1588 | if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) { 1589 | throw new \Exception("Cannot parse for loop at position {$forIndex}"); 1590 | } 1591 | 1592 | return new ForLoopInfo( 1593 | $forIndex, 1594 | $blockStart, 1595 | $blockEnd, 1596 | $initStart, 1597 | $initEnd, 1598 | $conditionStart, 1599 | $conditionEnd, 1600 | $incrementStart, 1601 | $incrementEnd 1602 | ); 1603 | } 1604 | 1605 | /** 1606 | * @param int $stackPtr 1607 | * @param array $forLoops 1608 | * @return ForLoopInfo|null 1609 | */ 1610 | public static function getForLoopForIncrementVariable($stackPtr, $forLoops) 1611 | { 1612 | foreach ($forLoops as $forLoop) { 1613 | if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) { 1614 | return $forLoop; 1615 | } 1616 | } 1617 | return null; 1618 | } 1619 | 1620 | /** 1621 | * Return true if the token looks like constructor promotion. 1622 | * 1623 | * Call on a parameter variable token only. 1624 | * 1625 | * @param File $phpcsFile 1626 | * @param int $stackPtr 1627 | * 1628 | * @return bool 1629 | */ 1630 | public static function isConstructorPromotion(File $phpcsFile, $stackPtr) 1631 | { 1632 | // If we are not in a function's parameters, this is not promotion. 1633 | $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr); 1634 | if (! $functionIndex) { 1635 | return false; 1636 | } 1637 | 1638 | $tokens = $phpcsFile->getTokens(); 1639 | 1640 | // Move backwards from the token, ignoring whitespace, typehints, and the 1641 | // 'readonly' keyword, and return true if the previous token is a 1642 | // visibility keyword (eg: `public`). 1643 | for ($i = $stackPtr - 1; $i > $functionIndex; $i--) { 1644 | if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) { 1645 | return true; 1646 | } 1647 | if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) { 1648 | continue; 1649 | } 1650 | if ($tokens[$i]['content'] === 'readonly') { 1651 | continue; 1652 | } 1653 | if (self::isTokenPartOfTypehint($phpcsFile, $i)) { 1654 | continue; 1655 | } 1656 | return false; 1657 | } 1658 | return false; 1659 | } 1660 | 1661 | /** 1662 | * If looking at a function call token, return a string for the full function 1663 | * name including any inline namespace. 1664 | * 1665 | * So for example, if the call looks like `\My\Namespace\doSomething($bar)` 1666 | * and `$stackPtr` refers to `doSomething`, this will return 1667 | * `\My\Namespace\doSomething`. 1668 | * 1669 | * @param File $phpcsFile 1670 | * @param int $stackPtr 1671 | * 1672 | * @return string|null 1673 | */ 1674 | public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr) 1675 | { 1676 | $tokens = $phpcsFile->getTokens(); 1677 | 1678 | if (! isset($tokens[$stackPtr])) { 1679 | return null; 1680 | } 1681 | $startOfScope = self::findVariableScope($phpcsFile, $stackPtr); 1682 | $functionName = $tokens[$stackPtr]['content']; 1683 | 1684 | // Move backwards from the token, collecting namespace separators and 1685 | // strings, until we encounter whitespace or something else. 1686 | $partOfNamespace = [T_NS_SEPARATOR, T_STRING]; 1687 | for ($i = $stackPtr - 1; $i > $startOfScope; $i--) { 1688 | if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) { 1689 | break; 1690 | } 1691 | $functionName = "{$tokens[$i]['content']}{$functionName}"; 1692 | } 1693 | return $functionName; 1694 | } 1695 | 1696 | /** 1697 | * Return false if the token is definitely not part of a typehint 1698 | * 1699 | * @param File $phpcsFile 1700 | * @param int $stackPtr 1701 | * 1702 | * @return bool 1703 | */ 1704 | private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr) 1705 | { 1706 | $tokens = $phpcsFile->getTokens(); 1707 | $token = $tokens[$stackPtr]; 1708 | if ($token['code'] === 'PHPCS_T_NULLABLE') { 1709 | return true; 1710 | } 1711 | if ($token['code'] === T_NS_SEPARATOR) { 1712 | return true; 1713 | } 1714 | if ($token['code'] === T_STRING) { 1715 | return true; 1716 | } 1717 | if ($token['code'] === T_TRUE) { 1718 | return true; 1719 | } 1720 | if ($token['code'] === T_FALSE) { 1721 | return true; 1722 | } 1723 | if ($token['code'] === T_NULL) { 1724 | return true; 1725 | } 1726 | if ($token['content'] === '|') { 1727 | return true; 1728 | } 1729 | if (in_array($token['code'], Tokens::$emptyTokens)) { 1730 | return true; 1731 | } 1732 | return false; 1733 | } 1734 | 1735 | /** 1736 | * Return true if the token is inside a typehint 1737 | * 1738 | * @param File $phpcsFile 1739 | * @param int $stackPtr 1740 | * 1741 | * @return bool 1742 | */ 1743 | public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr) 1744 | { 1745 | $tokens = $phpcsFile->getTokens(); 1746 | 1747 | if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) { 1748 | return false; 1749 | } 1750 | 1751 | // Examine every following token, ignoring everything that might be part of 1752 | // a typehint. If we find a variable at the end, this is part of a 1753 | // typehint. 1754 | $i = $stackPtr; 1755 | while (true) { 1756 | $i += 1; 1757 | if (! isset($tokens[$i])) { 1758 | return false; 1759 | } 1760 | if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) { 1761 | return ($tokens[$i]['code'] === T_VARIABLE); 1762 | } 1763 | } 1764 | } 1765 | 1766 | /** 1767 | * Return true if the token is inside an abstract class. 1768 | * 1769 | * @param File $phpcsFile 1770 | * @param int $stackPtr 1771 | * 1772 | * @return bool 1773 | */ 1774 | public static function isInAbstractClass(File $phpcsFile, $stackPtr) 1775 | { 1776 | $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS); 1777 | if (! is_int($classIndex)) { 1778 | return false; 1779 | } 1780 | $classProperties = $phpcsFile->getClassProperties($classIndex); 1781 | return $classProperties['is_abstract']; 1782 | } 1783 | 1784 | /** 1785 | * Return true if the function body is empty or contains only `return;` 1786 | * 1787 | * @param File $phpcsFile 1788 | * @param int $stackPtr The index of the function keyword. 1789 | * 1790 | * @return bool 1791 | */ 1792 | public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr) 1793 | { 1794 | $tokens = $phpcsFile->getTokens(); 1795 | if ($tokens[$stackPtr]['code'] !== T_FUNCTION) { 1796 | return false; 1797 | } 1798 | $functionScopeStart = $tokens[$stackPtr]['scope_opener']; 1799 | $functionScopeEnd = $tokens[$stackPtr]['scope_closer']; 1800 | $tokensToIgnore = array_merge( 1801 | Tokens::$emptyTokens, 1802 | [ 1803 | T_RETURN, 1804 | T_SEMICOLON, 1805 | T_OPEN_CURLY_BRACKET, 1806 | T_CLOSE_CURLY_BRACKET, 1807 | ] 1808 | ); 1809 | for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) { 1810 | if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) { 1811 | return false; 1812 | } 1813 | } 1814 | return true; 1815 | } 1816 | } 1817 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/ScopeInfo.php: -------------------------------------------------------------------------------- 1 | scopeStartIndex = $scopeStartIndex; 38 | $this->scopeEndIndex = $scopeEndIndex; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/ScopeManager.php: -------------------------------------------------------------------------------- 1 | > 19 | */ 20 | private $scopes = []; 21 | 22 | /** 23 | * Add a scope's start and end index to our record for the file. 24 | * 25 | * @param File $phpcsFile 26 | * @param int $scopeStartIndex 27 | * 28 | * @return ScopeInfo 29 | */ 30 | public function recordScopeStartAndEnd(File $phpcsFile, $scopeStartIndex) 31 | { 32 | $scopeEndIndex = Helpers::getScopeCloseForScopeOpen($phpcsFile, $scopeStartIndex); 33 | $filename = $phpcsFile->getFilename(); 34 | if (! isset($this->scopes[$filename])) { 35 | $this->scopes[$filename] = []; 36 | } 37 | Helpers::debug('recording scope for file', $filename, 'start/end', $scopeStartIndex, $scopeEndIndex); 38 | $scope = new ScopeInfo($scopeStartIndex, $scopeEndIndex); 39 | $this->scopes[$filename][$scopeStartIndex] = $scope; 40 | return $scope; 41 | } 42 | 43 | /** 44 | * Return the scopes for a file. 45 | * 46 | * @param string $filename 47 | * 48 | * @return ScopeInfo[] 49 | */ 50 | public function getScopesForFilename($filename) 51 | { 52 | if (empty($this->scopes[$filename])) { 53 | return []; 54 | } 55 | return array_values($this->scopes[$filename]); 56 | } 57 | 58 | /** 59 | * Return the scope for a scope start index. 60 | * 61 | * @param string $filename 62 | * @param int $scopeStartIndex 63 | * 64 | * @return ScopeInfo|null 65 | */ 66 | public function getScopeForScopeStart($filename, $scopeStartIndex) 67 | { 68 | if (empty($this->scopes[$filename][$scopeStartIndex])) { 69 | return null; 70 | } 71 | return $this->scopes[$filename][$scopeStartIndex]; 72 | } 73 | 74 | /** 75 | * Find scopes closed by a scope close index. 76 | * 77 | * @param string $filename 78 | * @param int $scopeEndIndex 79 | * 80 | * @return ScopeInfo[] 81 | */ 82 | public function getScopesForScopeEnd($filename, $scopeEndIndex) 83 | { 84 | $scopePairsForFile = $this->getScopesForFilename($filename); 85 | $scopeIndicesThisCloses = array_reduce( 86 | $scopePairsForFile, 87 | /** 88 | * @param ScopeInfo[] $found 89 | * @param ScopeInfo $scope 90 | * 91 | * @return ScopeInfo[] 92 | */ 93 | function ($found, $scope) use ($scopeEndIndex) { 94 | if (! is_int($scope->scopeEndIndex)) { 95 | Helpers::debug('No scope closer found for scope start', $scope->scopeStartIndex); 96 | return $found; 97 | } 98 | 99 | if ($scopeEndIndex === $scope->scopeEndIndex) { 100 | $found[] = $scope; 101 | } 102 | return $found; 103 | }, 104 | [] 105 | ); 106 | return $scopeIndicesThisCloses; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /VariableAnalysis/Lib/ScopeType.php: -------------------------------------------------------------------------------- 1 | 93 | */ 94 | public static $scopeTypeDescriptions = [ 95 | ScopeType::LOCAL => 'variable', 96 | ScopeType::PARAM => 'function parameter', 97 | ScopeType::STATICSCOPE => 'static variable', 98 | ScopeType::GLOBALSCOPE => 'global variable', 99 | ScopeType::BOUND => 'bound variable', 100 | ]; 101 | 102 | /** 103 | * @param string $varName 104 | */ 105 | public function __construct($varName) 106 | { 107 | $this->name = $varName; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | private $forLoops = []; 35 | 36 | /** 37 | * A list of enum blocks, keyed by the index of their first token in this file. 38 | * 39 | * @var array 40 | */ 41 | private $enums = []; 42 | 43 | /** 44 | * A list of custom functions which pass in variables to be initialized by 45 | * reference (eg `preg_match()`) and therefore should not require those 46 | * variables to be defined ahead of time. The list is space separated and 47 | * each entry is of the form `functionName:1,2`. The function name comes 48 | * first followed by a colon and a comma-separated list of argument numbers 49 | * (starting from 1) which should be considered variable definitions. The 50 | * special value `...` in the arguments list will cause all arguments after 51 | * the last number to be considered variable definitions. 52 | * 53 | * @var string|null 54 | */ 55 | public $sitePassByRefFunctions = null; 56 | 57 | /** 58 | * If set, allows common WordPress pass-by-reference functions in addition to 59 | * the standard PHP ones. 60 | * 61 | * @var bool 62 | */ 63 | public $allowWordPressPassByRefFunctions = false; 64 | 65 | /** 66 | * Allow exceptions in a catch block to be unused without warning. 67 | * 68 | * @var bool 69 | */ 70 | public $allowUnusedCaughtExceptions = true; 71 | 72 | /** 73 | * Allow function parameters to be unused without provoking unused-var warning. 74 | * 75 | * @var bool 76 | */ 77 | public $allowUnusedFunctionParameters = false; 78 | 79 | /** 80 | * If set, ignores undefined variables in the file scope (the top-level 81 | * scope of a file). 82 | * 83 | * @var bool 84 | */ 85 | public $allowUndefinedVariablesInFileScope = false; 86 | 87 | /** 88 | * If set, ignores unused variables in the file scope (the top-level 89 | * scope of a file). 90 | * 91 | * @var bool 92 | */ 93 | public $allowUnusedVariablesInFileScope = false; 94 | 95 | /** 96 | * A space-separated list of names of placeholder variables that you want to 97 | * ignore from unused variable warnings. For example, to ignore the variables 98 | * `$junk` and `$unused`, this could be set to `'junk unused'`. 99 | * 100 | * @var string|null 101 | */ 102 | public $validUnusedVariableNames = null; 103 | 104 | /** 105 | * A PHP regexp string for variables that you want to ignore from unused 106 | * variable warnings. For example, to ignore the variables `$_junk` and 107 | * `$_unused`, this could be set to `'/^_/'`. 108 | * 109 | * @var string|null 110 | */ 111 | public $ignoreUnusedRegexp = null; 112 | 113 | /** 114 | * A space-separated list of names of placeholder variables that you want to 115 | * ignore from undefined variable warnings. For example, to ignore the variables 116 | * `$post` and `$undefined`, this could be set to `'post undefined'`. 117 | * 118 | * @var string|null 119 | */ 120 | public $validUndefinedVariableNames = null; 121 | 122 | /** 123 | * A PHP regexp string for variables that you want to ignore from undefined 124 | * variable warnings. For example, to ignore the variables `$_junk` and 125 | * `$_unused`, this could be set to `'/^_/'`. 126 | * 127 | * @var string|null 128 | */ 129 | public $validUndefinedVariableRegexp = null; 130 | 131 | /** 132 | * Allows unused arguments in a function definition if they are 133 | * followed by an argument which is used. 134 | * 135 | * @var bool 136 | */ 137 | public $allowUnusedParametersBeforeUsed = true; 138 | 139 | /** 140 | * If set to true, unused values from the `key => value` syntax 141 | * in a `foreach` loop will never be marked as unused. 142 | * 143 | * @var bool 144 | */ 145 | public $allowUnusedForeachVariables = true; 146 | 147 | /** 148 | * If set to true, unused variables in a function before a require or import 149 | * statement will not be marked as unused because they may be used in the 150 | * required file. 151 | * 152 | * @var bool 153 | */ 154 | public $allowUnusedVariablesBeforeRequire = false; 155 | 156 | /** 157 | * A cache for getPassByReferenceFunctions 158 | * 159 | * @var array>|null 160 | */ 161 | private $passByRefFunctionsCache = null; 162 | 163 | public function __construct() 164 | { 165 | $this->scopeManager = new ScopeManager(); 166 | } 167 | 168 | /** 169 | * Decide which tokens to scan. 170 | * 171 | * @return (int|string)[] 172 | */ 173 | public function register() 174 | { 175 | $types = [ 176 | T_VARIABLE, 177 | T_DOUBLE_QUOTED_STRING, 178 | T_HEREDOC, 179 | T_CLOSE_CURLY_BRACKET, 180 | T_FUNCTION, 181 | T_CLOSURE, 182 | T_STRING, 183 | T_COMMA, 184 | T_SEMICOLON, 185 | T_CLOSE_PARENTHESIS, 186 | T_FOR, 187 | T_ENDFOR, 188 | ]; 189 | if (defined('T_FN')) { 190 | $types[] = T_FN; 191 | } 192 | if (defined('T_ENUM')) { 193 | $types[] = T_ENUM; 194 | } 195 | return $types; 196 | } 197 | 198 | /** 199 | * @param string $functionName 200 | * 201 | * @return array 202 | */ 203 | private function getPassByReferenceFunction($functionName) 204 | { 205 | $passByRefFunctions = $this->getPassByReferenceFunctions(); 206 | return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : []; 207 | } 208 | 209 | /** 210 | * @return array> 211 | */ 212 | private function getPassByReferenceFunctions() 213 | { 214 | if (! is_null($this->passByRefFunctionsCache)) { 215 | return $this->passByRefFunctionsCache; 216 | } 217 | $passByRefFunctions = Constants::getPassByReferenceFunctions(); 218 | if (!empty($this->sitePassByRefFunctions)) { 219 | $lines = Helpers::splitStringToArray('/\s+/', trim($this->sitePassByRefFunctions)); 220 | foreach ($lines as $line) { 221 | list ($function, $args) = explode(':', $line); 222 | $passByRefFunctions[$function] = explode(',', $args); 223 | } 224 | } 225 | if ($this->allowWordPressPassByRefFunctions) { 226 | $passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions()); 227 | } 228 | $this->passByRefFunctionsCache = $passByRefFunctions; 229 | return $passByRefFunctions; 230 | } 231 | 232 | /** 233 | * Scan and process a token. 234 | * 235 | * This is the main processing function of the sniff. Will run on every token 236 | * for which `register()` returns true. 237 | * 238 | * @param File $phpcsFile 239 | * @param int $stackPtr 240 | * 241 | * @return void 242 | */ 243 | public function process(File $phpcsFile, $stackPtr) 244 | { 245 | $tokens = $phpcsFile->getTokens(); 246 | 247 | $scopeStartTokenTypes = [ 248 | T_FUNCTION, 249 | T_CLOSURE, 250 | ]; 251 | 252 | $token = $tokens[$stackPtr]; 253 | 254 | // Cache the current PHPCS File in an instance variable so it can be more 255 | // easily accessed in other places which aren't passed the object. 256 | if ($this->currentFile !== $phpcsFile) { 257 | $this->currentFile = $phpcsFile; 258 | $this->forLoops = []; 259 | $this->enums = []; 260 | } 261 | 262 | // Add the global scope for the current file to our scope indexes. 263 | $scopesForFilename = $this->scopeManager->getScopesForFilename($phpcsFile->getFilename()); 264 | if (empty($scopesForFilename)) { 265 | $this->scopeManager->recordScopeStartAndEnd($phpcsFile, 0); 266 | } 267 | 268 | // Find and process variables to perform two jobs: to record variable 269 | // definition or use, and to report variables as undefined if they are used 270 | // without having been first defined. 271 | if ($token['code'] === T_VARIABLE) { 272 | $this->processVariable($phpcsFile, $stackPtr); 273 | } 274 | 275 | // Report variables defined but not used in the current scope as unused 276 | // variables if the current token closes scopes. 277 | $this->searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr); 278 | 279 | // Scan variables that were postponed because they exist in the increment 280 | // expression of a for loop if the current token closes a loop. 281 | $this->processClosingForLoopsAt($phpcsFile, $stackPtr); 282 | 283 | if ($token['code'] === T_VARIABLE) { 284 | return; 285 | } 286 | 287 | if (($token['code'] === T_DOUBLE_QUOTED_STRING) || ($token['code'] === T_HEREDOC)) { 288 | $this->processVariableInString($phpcsFile, $stackPtr); 289 | return; 290 | } 291 | if (($token['code'] === T_STRING) && ($token['content'] === 'compact')) { 292 | $this->processCompact($phpcsFile, $stackPtr); 293 | return; 294 | } 295 | 296 | // Record for loop boundaries so we can delay scanning the third for loop 297 | // expression until after the loop has been scanned. 298 | if ($token['code'] === T_FOR) { 299 | $this->recordForLoop($phpcsFile, $stackPtr); 300 | return; 301 | } 302 | 303 | // Record enums so we can detect them even before phpcs was able to. 304 | if ($token['content'] === 'enum') { 305 | $enumInfo = Helpers::makeEnumInfo($phpcsFile, $stackPtr); 306 | // The token might not actually be an enum so let's avoid returning if 307 | // it's not. 308 | if ($enumInfo) { 309 | $this->enums[$stackPtr] = $enumInfo; 310 | return; 311 | } 312 | } 313 | 314 | // If the current token is a call to `get_defined_vars()`, consider that a 315 | // usage of all variables in the current scope. 316 | if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) { 317 | Helpers::debug('get_defined_vars is being called'); 318 | $this->markAllVariablesRead($phpcsFile, $stackPtr); 319 | return; 320 | } 321 | 322 | // If the current token starts a scope, record that scope's start and end 323 | // indexes so that we can determine if variables in that scope are defined 324 | // and/or used. 325 | if ( 326 | in_array($token['code'], $scopeStartTokenTypes, true) || 327 | Helpers::isArrowFunction($phpcsFile, $stackPtr) 328 | ) { 329 | Helpers::debug('found scope condition', $token); 330 | $this->scopeManager->recordScopeStartAndEnd($phpcsFile, $stackPtr); 331 | return; 332 | } 333 | } 334 | 335 | /** 336 | * Record the boundaries of a for loop. 337 | * 338 | * @param File $phpcsFile 339 | * @param int $stackPtr 340 | * 341 | * @return void 342 | */ 343 | private function recordForLoop($phpcsFile, $stackPtr) 344 | { 345 | $this->forLoops[$stackPtr] = Helpers::makeForLoopInfo($phpcsFile, $stackPtr); 346 | } 347 | 348 | /** 349 | * Find scopes closed by a token and process their variables. 350 | * 351 | * Calls `processScopeClose()` for each closed scope. 352 | * 353 | * @param File $phpcsFile 354 | * @param int $stackPtr 355 | * 356 | * @return void 357 | */ 358 | private function searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr) 359 | { 360 | $scopeIndicesThisCloses = $this->scopeManager->getScopesForScopeEnd($phpcsFile->getFilename(), $stackPtr); 361 | 362 | $tokens = $phpcsFile->getTokens(); 363 | $token = $tokens[$stackPtr]; 364 | $line = $token['line']; 365 | foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) { 366 | Helpers::debug('found closing scope at index', $stackPtr, 'line', $line, 'for scopes starting at:', $scopeIndexThisCloses->scopeStartIndex); 367 | $this->processScopeClose($phpcsFile, $scopeIndexThisCloses->scopeStartIndex); 368 | } 369 | } 370 | 371 | /** 372 | * Scan variables that were postponed because they exist in the increment expression of a for loop. 373 | * 374 | * @param File $phpcsFile 375 | * @param int $stackPtr 376 | * 377 | * @return void 378 | */ 379 | private function processClosingForLoopsAt($phpcsFile, $stackPtr) 380 | { 381 | $forLoopsThisCloses = []; 382 | foreach ($this->forLoops as $forLoop) { 383 | if ($forLoop->blockEnd === $stackPtr) { 384 | $forLoopsThisCloses[] = $forLoop; 385 | } 386 | } 387 | 388 | foreach ($forLoopsThisCloses as $forLoop) { 389 | foreach ($forLoop->incrementVariables as $varIndex => $varInfo) { 390 | Helpers::debug('processing delayed for loop increment variable at', $varIndex, $varInfo); 391 | $this->processVariable($phpcsFile, $varIndex, ['ignore-for-loops' => true]); 392 | } 393 | } 394 | } 395 | 396 | /** 397 | * Return true if the token is a call to `get_defined_vars()`. 398 | * 399 | * @param File $phpcsFile 400 | * @param int $stackPtr 401 | * 402 | * @return bool 403 | */ 404 | protected function isGetDefinedVars(File $phpcsFile, $stackPtr) 405 | { 406 | $tokens = $phpcsFile->getTokens(); 407 | $token = $tokens[$stackPtr]; 408 | if (! $token || $token['content'] !== 'get_defined_vars') { 409 | return false; 410 | } 411 | // Make sure this is a function call 412 | $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 413 | if (! $parenPointer || $tokens[$parenPointer]['code'] !== T_OPEN_PARENTHESIS) { 414 | return false; 415 | } 416 | return true; 417 | } 418 | 419 | /** 420 | * @return string 421 | */ 422 | protected function getFilename() 423 | { 424 | return $this->currentFile ? $this->currentFile->getFilename() : 'unknown file'; 425 | } 426 | 427 | /** 428 | * @param int $currScope 429 | * 430 | * @return ScopeInfo 431 | */ 432 | protected function getOrCreateScopeInfo($currScope) 433 | { 434 | $scope = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope); 435 | if (! $scope) { 436 | if (! $this->currentFile) { 437 | throw new \Exception('Cannot create scope info; current file is not set.'); 438 | } 439 | $scope = $this->scopeManager->recordScopeStartAndEnd($this->currentFile, $currScope); 440 | } 441 | return $scope; 442 | } 443 | 444 | /** 445 | * @param string $varName 446 | * @param int $currScope 447 | * 448 | * @return VariableInfo|null 449 | */ 450 | protected function getVariableInfo($varName, $currScope) 451 | { 452 | $scopeInfo = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope); 453 | return ($scopeInfo && isset($scopeInfo->variables[$varName])) ? $scopeInfo->variables[$varName] : null; 454 | } 455 | 456 | /** 457 | * Returns variable data for a variable at an index. 458 | * 459 | * The variable will also be added to the list of variables stored in its 460 | * scope so that its use or non-use can be reported when those scopes end by 461 | * `processScopeClose()`. 462 | * 463 | * @param string $varName 464 | * @param int $currScope 465 | * 466 | * @return VariableInfo 467 | */ 468 | protected function getOrCreateVariableInfo($varName, $currScope) 469 | { 470 | Helpers::debug("getOrCreateVariableInfo: starting for '{$varName}'"); 471 | $scopeInfo = $this->getOrCreateScopeInfo($currScope); 472 | if (isset($scopeInfo->variables[$varName])) { 473 | Helpers::debug("getOrCreateVariableInfo: found variable for '{$varName}'", $scopeInfo->variables[$varName]); 474 | return $scopeInfo->variables[$varName]; 475 | } 476 | Helpers::debug("getOrCreateVariableInfo: creating a new variable for '{$varName}' in scope", $scopeInfo); 477 | $scopeInfo->variables[$varName] = new VariableInfo($varName); 478 | $validUnusedVariableNames = (empty($this->validUnusedVariableNames)) 479 | ? [] 480 | : Helpers::splitStringToArray('/\s+/', trim($this->validUnusedVariableNames)); 481 | $validUndefinedVariableNames = (empty($this->validUndefinedVariableNames)) 482 | ? [] 483 | : Helpers::splitStringToArray('/\s+/', trim($this->validUndefinedVariableNames)); 484 | if (in_array($varName, $validUnusedVariableNames)) { 485 | $scopeInfo->variables[$varName]->ignoreUnused = true; 486 | } 487 | if (! empty($this->ignoreUnusedRegexp) && preg_match($this->ignoreUnusedRegexp, $varName) === 1) { 488 | $scopeInfo->variables[$varName]->ignoreUnused = true; 489 | } 490 | if ($scopeInfo->scopeStartIndex === 0 && $this->allowUndefinedVariablesInFileScope) { 491 | $scopeInfo->variables[$varName]->ignoreUndefined = true; 492 | } 493 | if (in_array($varName, $validUndefinedVariableNames)) { 494 | $scopeInfo->variables[$varName]->ignoreUndefined = true; 495 | } 496 | if (! empty($this->validUndefinedVariableRegexp) && preg_match($this->validUndefinedVariableRegexp, $varName) === 1) { 497 | $scopeInfo->variables[$varName]->ignoreUndefined = true; 498 | } 499 | Helpers::debug("getOrCreateVariableInfo: scope for '{$varName}' is now", $scopeInfo); 500 | return $scopeInfo->variables[$varName]; 501 | } 502 | 503 | /** 504 | * Record that a variable has been defined and assigned a value. 505 | * 506 | * If a variable has been defined within a scope, it will not be marked as 507 | * undefined when that variable is later used. If it is not used, it will be 508 | * marked as unused when that scope ends. 509 | * 510 | * Sometimes it's possible to assign something to a variable without 511 | * definining it (eg: assignment to a reference); in that case, use 512 | * `markVariableAssignmentWithoutInitialization()`. 513 | * 514 | * @param string $varName 515 | * @param int $stackPtr 516 | * @param int $currScope 517 | * 518 | * @return void 519 | */ 520 | protected function markVariableAssignment($varName, $stackPtr, $currScope) 521 | { 522 | Helpers::debug('markVariableAssignment: starting for', $varName); 523 | $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope); 524 | Helpers::debug('markVariableAssignment: marked as assigned without initialization', $varName); 525 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 526 | if (isset($varInfo->firstInitialized) && ($varInfo->firstInitialized <= $stackPtr)) { 527 | Helpers::debug('markVariableAssignment: variable is already initialized', $varName); 528 | return; 529 | } 530 | $varInfo->firstInitialized = $stackPtr; 531 | Helpers::debug('markVariableAssignment: marked as initialized', $varName); 532 | } 533 | 534 | /** 535 | * Record that a variable has been assigned a value. 536 | * 537 | * Does not record that a variable has been defined, which is the usual state 538 | * of affairs. For that, use `markVariableAssignment()`. 539 | * 540 | * This is useful for assignments to references. 541 | * 542 | * @param string $varName 543 | * @param int $stackPtr 544 | * @param int $currScope 545 | * 546 | * @return void 547 | */ 548 | protected function markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope) 549 | { 550 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 551 | 552 | // Is the variable referencing another variable? If so, mark that variable used also. 553 | if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) { 554 | Helpers::debug('markVariableAssignmentWithoutInitialization: considering marking referenced variable assigned', $varName); 555 | // Don't do this if the referenced variable does not exist; eg: if it's going to be bound at runtime like in array_walk 556 | if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) { 557 | Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName); 558 | $this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope); 559 | } else { 560 | Helpers::debug('markVariableAssignmentWithoutInitialization: not marking referenced variable assigned', $varName); 561 | } 562 | } else { 563 | Helpers::debug('markVariableAssignmentWithoutInitialization: not considering referenced variable', $varName); 564 | } 565 | 566 | if (empty($varInfo->scopeType)) { 567 | $varInfo->scopeType = ScopeType::LOCAL; 568 | } 569 | $varInfo->allAssignments[] = $stackPtr; 570 | } 571 | 572 | /** 573 | * Record that a variable has been defined within a scope. 574 | * 575 | * @param string $varName 576 | * @param ScopeType::PARAM|ScopeType::BOUND|ScopeType::LOCAL|ScopeType::GLOBALSCOPE|ScopeType::STATICSCOPE $scopeType 577 | * @param ?string $typeHint 578 | * @param int $stackPtr 579 | * @param int $currScope 580 | * @param ?bool $permitMatchingRedeclaration 581 | * 582 | * @return void 583 | */ 584 | protected function markVariableDeclaration( 585 | $varName, 586 | $scopeType, 587 | $typeHint, 588 | $stackPtr, 589 | $currScope, 590 | $permitMatchingRedeclaration = false 591 | ) { 592 | Helpers::debug("marking variable '{$varName}' declared in scope starting at token", $currScope); 593 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 594 | 595 | if (! empty($varInfo->scopeType)) { 596 | if (($permitMatchingRedeclaration === false) || ($varInfo->scopeType !== $scopeType)) { 597 | // Issue redeclaration/reuse warning 598 | // Note: we check off scopeType not firstDeclared, this is so that 599 | // we catch declarations that come after implicit declarations like 600 | // use of a variable as a local. 601 | $this->addWarning( 602 | 'Redeclaration of %s %s as %s.', 603 | $stackPtr, 604 | 'VariableRedeclaration', 605 | [ 606 | VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType], 607 | "\${$varName}", 608 | VariableInfo::$scopeTypeDescriptions[$scopeType], 609 | ] 610 | ); 611 | } 612 | } 613 | 614 | $varInfo->scopeType = $scopeType; 615 | if (isset($typeHint)) { 616 | $varInfo->typeHint = $typeHint; 617 | } 618 | if (isset($varInfo->firstDeclared) && ($varInfo->firstDeclared <= $stackPtr)) { 619 | Helpers::debug("variable '{$varName}' was already marked declared", $varInfo); 620 | return; 621 | } 622 | $varInfo->firstDeclared = $stackPtr; 623 | $varInfo->allAssignments[] = $stackPtr; 624 | Helpers::debug("variable '{$varName}' marked declared", $varInfo); 625 | } 626 | 627 | /** 628 | * @param string $message 629 | * @param int $stackPtr 630 | * @param string $code 631 | * @param string[] $data 632 | * 633 | * @return void 634 | */ 635 | protected function addWarning($message, $stackPtr, $code, $data) 636 | { 637 | if (! $this->currentFile) { 638 | throw new \Exception('Cannot add warning; current file is not set.'); 639 | } 640 | $this->currentFile->addWarning( 641 | $message, 642 | $stackPtr, 643 | $code, 644 | $data 645 | ); 646 | } 647 | 648 | /** 649 | * Record that a variable has been used within a scope. 650 | * 651 | * If the variable has not been defined first, this will still mark it used. 652 | * To display a warning for undefined variables, use 653 | * `markVariableReadAndWarnIfUndefined()`. 654 | * 655 | * @param string $varName 656 | * @param int $stackPtr 657 | * @param int $currScope 658 | * 659 | * @return void 660 | */ 661 | protected function markVariableRead($varName, $stackPtr, $currScope) 662 | { 663 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 664 | if (isset($varInfo->firstRead) && ($varInfo->firstRead <= $stackPtr)) { 665 | return; 666 | } 667 | $varInfo->firstRead = $stackPtr; 668 | } 669 | 670 | /** 671 | * Return true if a variable is defined within a scope. 672 | * 673 | * @param string $varName 674 | * @param int $stackPtr 675 | * @param int $currScope 676 | * 677 | * @return bool 678 | */ 679 | protected function isVariableUndefined($varName, $stackPtr, $currScope) 680 | { 681 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 682 | Helpers::debug('isVariableUndefined', $varInfo, 'at', $stackPtr); 683 | if ($varInfo->ignoreUndefined) { 684 | return false; 685 | } 686 | if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) { 687 | return false; 688 | } 689 | if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) { 690 | return false; 691 | } 692 | // If we are inside a for loop increment expression, check to see if the 693 | // variable was defined inside the for loop. 694 | foreach ($this->forLoops as $forLoop) { 695 | if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) { 696 | Helpers::debug('isVariableUndefined looking at increment expression for loop', $forLoop); 697 | if ( 698 | isset($varInfo->firstInitialized) 699 | && $varInfo->firstInitialized > $forLoop->blockStart 700 | && $varInfo->firstInitialized < $forLoop->blockEnd 701 | ) { 702 | return false; 703 | } 704 | } 705 | } 706 | // If we are inside a for loop body, check to see if the variable was 707 | // defined in that loop's third expression. 708 | foreach ($this->forLoops as $forLoop) { 709 | if ($stackPtr > $forLoop->blockStart && $stackPtr < $forLoop->blockEnd) { 710 | foreach ($forLoop->incrementVariables as $forLoopVarInfo) { 711 | if ($varInfo === $forLoopVarInfo) { 712 | return false; 713 | } 714 | } 715 | } 716 | } 717 | return true; 718 | } 719 | 720 | /** 721 | * Record a variable use and report a warning if the variable is undefined. 722 | * 723 | * @param File $phpcsFile 724 | * @param string $varName 725 | * @param int $stackPtr 726 | * @param int $currScope 727 | * 728 | * @return void 729 | */ 730 | protected function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope) 731 | { 732 | $this->markVariableRead($varName, $stackPtr, $currScope); 733 | if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) { 734 | Helpers::debug("variable $varName looks undefined"); 735 | 736 | if (Helpers::isVariableArrayPushShortcut($phpcsFile, $stackPtr)) { 737 | $this->warnAboutUndefinedArrayPushShortcut($phpcsFile, $varName, $stackPtr); 738 | // Mark the variable as defined if it's of the form `$x[] = 1;` 739 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 740 | return; 741 | } 742 | 743 | if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) { 744 | $this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr); 745 | return; 746 | } 747 | 748 | $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr); 749 | } 750 | } 751 | 752 | /** 753 | * Mark all variables within a scope as being used. 754 | * 755 | * This will prevent any of the variables in that scope from being reported 756 | * as unused. 757 | * 758 | * @param File $phpcsFile 759 | * @param int $stackPtr 760 | * 761 | * @return void 762 | */ 763 | protected function markAllVariablesRead(File $phpcsFile, $stackPtr) 764 | { 765 | $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr); 766 | if ($currScope === null) { 767 | return; 768 | } 769 | $scopeInfo = $this->getOrCreateScopeInfo($currScope); 770 | $count = count($scopeInfo->variables); 771 | Helpers::debug("marking all $count variables in scope as read"); 772 | foreach ($scopeInfo->variables as $varInfo) { 773 | $this->markVariableRead($varInfo->name, $stackPtr, $scopeInfo->scopeStartIndex); 774 | } 775 | } 776 | 777 | /** 778 | * Process a parameter definition if it is inside a function definition. 779 | * 780 | * This does not include variables imported by a "use" statement. 781 | * 782 | * @param File $phpcsFile 783 | * @param int $stackPtr 784 | * @param string $varName 785 | * @param int $outerScope 786 | * 787 | * @return void 788 | */ 789 | protected function processVariableAsFunctionParameter(File $phpcsFile, $stackPtr, $varName, $outerScope) 790 | { 791 | Helpers::debug('processVariableAsFunctionParameter', $stackPtr, $varName); 792 | $tokens = $phpcsFile->getTokens(); 793 | 794 | $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr); 795 | if (! is_int($functionPtr)) { 796 | throw new \Exception("Function index not found for function argument index {$stackPtr}"); 797 | } 798 | 799 | Helpers::debug('processVariableAsFunctionParameter found function definition', $tokens[$functionPtr]); 800 | $this->markVariableDeclaration($varName, ScopeType::PARAM, null, $stackPtr, $functionPtr); 801 | 802 | // Are we pass-by-reference? 803 | $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true); 804 | if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) { 805 | Helpers::debug('processVariableAsFunctionParameter found pass-by-reference to scope', $outerScope); 806 | $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr); 807 | $varInfo->referencedVariableScope = $outerScope; 808 | } 809 | 810 | // Are we optional with a default? 811 | if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) { 812 | Helpers::debug('processVariableAsFunctionParameter optional with default'); 813 | $this->markVariableAssignment($varName, $stackPtr, $functionPtr); 814 | } 815 | 816 | // Are we using constructor promotion? If so, that counts as both definition and use. 817 | if (Helpers::isConstructorPromotion($phpcsFile, $stackPtr)) { 818 | Helpers::debug('processVariableAsFunctionParameter constructor promotion'); 819 | $this->markVariableRead($varName, $stackPtr, $outerScope); 820 | } 821 | } 822 | 823 | /** 824 | * Process a variable definition if it is inside a function's "use" import. 825 | * 826 | * @param File $phpcsFile 827 | * @param int $stackPtr 828 | * @param string $varName 829 | * @param int $outerScope The start of the scope outside the function definition 830 | * 831 | * @return void 832 | */ 833 | protected function processVariableAsUseImportDefinition(File $phpcsFile, $stackPtr, $varName, $outerScope) 834 | { 835 | $tokens = $phpcsFile->getTokens(); 836 | 837 | Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope); 838 | 839 | $endOfArgsPtr = $phpcsFile->findPrevious([T_CLOSE_PARENTHESIS], $stackPtr - 1, null); 840 | if (! is_int($endOfArgsPtr)) { 841 | throw new \Exception("Arguments index not found for function use index {$stackPtr} when processing variable {$varName}"); 842 | } 843 | $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $endOfArgsPtr); 844 | if (! is_int($functionPtr)) { 845 | throw new \Exception("Function index not found for function use index {$stackPtr} (using {$endOfArgsPtr}) when processing variable {$varName}"); 846 | } 847 | 848 | // Use is both a read (in the enclosing scope) and a define (in the function scope) 849 | $this->markVariableRead($varName, $stackPtr, $outerScope); 850 | 851 | // If it's undefined in the enclosing scope, the use is wrong 852 | if ($this->isVariableUndefined($varName, $stackPtr, $outerScope) === true) { 853 | Helpers::debug("variable '{$varName}' in function definition looks undefined in scope", $outerScope); 854 | $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr); 855 | return; 856 | } 857 | 858 | $this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr); 859 | $this->markVariableAssignment($varName, $stackPtr, $functionPtr); 860 | 861 | // Are we pass-by-reference? If so, then any assignment to the variable in 862 | // the function scope also should count for the enclosing scope. 863 | $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true); 864 | if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) { 865 | Helpers::debug("variable '{$varName}' in function definition looks passed by reference"); 866 | $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr); 867 | $varInfo->referencedVariableScope = $outerScope; 868 | } 869 | } 870 | 871 | /** 872 | * Process a class property that is being defined. 873 | * 874 | * Property definitions are ignored currently because all property access is 875 | * legal, even to undefined properties. 876 | * 877 | * Can be called for any token and will return false if the variable is not 878 | * of this type. 879 | * 880 | * @param File $phpcsFile 881 | * @param int $stackPtr 882 | * 883 | * @return bool 884 | */ 885 | protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr) 886 | { 887 | // Make sure we are not in a class method before assuming it's a property. 888 | $tokens = $phpcsFile->getTokens(); 889 | 890 | /** @var array{conditions?: (int|string)[], content?: string}|null */ 891 | $token = $tokens[$stackPtr]; 892 | if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) { 893 | return Helpers::areAnyConditionsAClass($token); 894 | } 895 | return false; 896 | } 897 | 898 | /** 899 | * Process a variable that is being accessed inside a catch block. 900 | * 901 | * Can be called for any token and will return false if the variable is not 902 | * of this type. 903 | * 904 | * @param File $phpcsFile 905 | * @param int $stackPtr 906 | * @param string $varName 907 | * @param int $currScope 908 | * 909 | * @return bool 910 | */ 911 | protected function processVariableAsCatchBlock(File $phpcsFile, $stackPtr, $varName, $currScope) 912 | { 913 | $tokens = $phpcsFile->getTokens(); 914 | 915 | // Are we a catch block parameter? 916 | $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr); 917 | if ($openPtr === null) { 918 | return false; 919 | } 920 | 921 | $catchPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true); 922 | if (($catchPtr !== false) && ($tokens[$catchPtr]['code'] === T_CATCH)) { 923 | // Scope of the exception var is actually the function, not just the catch block. 924 | $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true); 925 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 926 | if ($this->allowUnusedCaughtExceptions) { 927 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 928 | $varInfo->ignoreUnused = true; 929 | } 930 | return true; 931 | } 932 | return false; 933 | } 934 | 935 | /** 936 | * Process a variable that is being accessed as a member of `$this`. 937 | * 938 | * Looks for variables of the form `$this->myVariable`. 939 | * 940 | * Can be called for any token and will return false if the variable is not 941 | * of this type. 942 | * 943 | * @param File $phpcsFile 944 | * @param int $stackPtr 945 | * @param string $varName 946 | * 947 | * @return bool 948 | */ 949 | protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr, $varName) 950 | { 951 | $tokens = $phpcsFile->getTokens(); 952 | $token = $tokens[$stackPtr]; 953 | 954 | // Are we $this within a class? 955 | if (($varName !== 'this') || empty($token['conditions'])) { 956 | return false; 957 | } 958 | 959 | // Handle enums specially since their condition may not exist in old phpcs. 960 | $inEnum = false; 961 | foreach ($this->enums as $enum) { 962 | if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) { 963 | $inEnum = true; 964 | } 965 | } 966 | 967 | $inFunction = false; 968 | foreach (array_reverse($token['conditions'], true) as $scopeCode) { 969 | // $this within a closure is valid 970 | if ($scopeCode === T_CLOSURE && $inFunction === false) { 971 | return true; 972 | } 973 | 974 | $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT]; 975 | if (defined('T_ENUM')) { 976 | $classlikeCodes[] = T_ENUM; 977 | } 978 | if (in_array($scopeCode, $classlikeCodes, true)) { 979 | return true; 980 | } 981 | 982 | if ($scopeCode === T_FUNCTION && $inEnum) { 983 | return true; 984 | } 985 | 986 | // Handle nested function declarations. 987 | if ($scopeCode === T_FUNCTION) { 988 | if ($inFunction === true) { 989 | break; 990 | } 991 | 992 | $inFunction = true; 993 | } 994 | } 995 | 996 | return false; 997 | } 998 | 999 | /** 1000 | * Process a superglobal variable that is being accessed. 1001 | * 1002 | * Can be called for any token and will return false if the variable is not 1003 | * of this type. 1004 | * 1005 | * @param string $varName 1006 | * 1007 | * @return bool 1008 | */ 1009 | protected function processVariableAsSuperGlobal($varName) 1010 | { 1011 | $superglobals = [ 1012 | 'GLOBALS', 1013 | '_SERVER', 1014 | '_GET', 1015 | '_POST', 1016 | '_FILES', 1017 | '_COOKIE', 1018 | '_SESSION', 1019 | '_REQUEST', 1020 | '_ENV', 1021 | 'argv', 1022 | 'argc', 1023 | 'http_response_header', 1024 | 'HTTP_RAW_POST_DATA', 1025 | ]; 1026 | // Are we a superglobal variable? 1027 | return (in_array($varName, $superglobals, true)); 1028 | } 1029 | 1030 | /** 1031 | * Process a variable that is being accessed with static syntax. 1032 | * 1033 | * That is, this will record the use of a variable of the form 1034 | * `MyClass::$myVariable` or `self::$myVariable`. 1035 | * 1036 | * Can be called for any token and will return false if the variable is not 1037 | * of this type. 1038 | * 1039 | * @param File $phpcsFile 1040 | * @param int $stackPtr 1041 | * 1042 | * @return bool 1043 | */ 1044 | protected function processVariableAsStaticMember(File $phpcsFile, $stackPtr) 1045 | { 1046 | $tokens = $phpcsFile->getTokens(); 1047 | 1048 | $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); 1049 | if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) { 1050 | return false; 1051 | } 1052 | $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true); 1053 | $staticReferences = [ 1054 | T_STRING, 1055 | T_SELF, 1056 | T_PARENT, 1057 | T_STATIC, 1058 | T_VARIABLE, 1059 | ]; 1060 | if ($classNamePtr === false || ! in_array($tokens[$classNamePtr]['code'], $staticReferences, true)) { 1061 | return false; 1062 | } 1063 | // "When calling static methods, the function call is stronger than the 1064 | // static property operator" so look for a function call. 1065 | $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); 1066 | if ($parenPointer !== false && $tokens[$parenPointer]['code'] === T_OPEN_PARENTHESIS) { 1067 | return false; 1068 | } 1069 | return true; 1070 | } 1071 | 1072 | /** 1073 | * @param File $phpcsFile 1074 | * @param int $stackPtr 1075 | * @param string $varName 1076 | * 1077 | * @return bool 1078 | */ 1079 | protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPtr, $varName) 1080 | { 1081 | // Are we refering to self:: outside a class? 1082 | 1083 | $tokens = $phpcsFile->getTokens(); 1084 | 1085 | /** @var array{conditions?: (int|string)[], content?: string}|null */ 1086 | $token = $tokens[$stackPtr]; 1087 | 1088 | $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); 1089 | if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) { 1090 | return false; 1091 | } 1092 | $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true); 1093 | if ($classNamePtr === false) { 1094 | return false; 1095 | } 1096 | $code = $tokens[$classNamePtr]['code']; 1097 | $staticReferences = [ 1098 | T_SELF, 1099 | T_STATIC, 1100 | ]; 1101 | if (! in_array($code, $staticReferences, true)) { 1102 | return false; 1103 | } 1104 | $errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass'; 1105 | $staticRefType = $code === T_SELF ? 'self::' : 'static::'; 1106 | if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) { 1107 | return false; 1108 | } 1109 | $phpcsFile->addError( 1110 | "Use of {$staticRefType}%s outside class definition.", 1111 | $stackPtr, 1112 | $errorClass, 1113 | ["\${$varName}"] 1114 | ); 1115 | return true; 1116 | } 1117 | 1118 | /** 1119 | * Process a variable that is being assigned. 1120 | * 1121 | * This will record that the variable has been defined within a scope so that 1122 | * later we can determine if it it unused and we can guarantee that any 1123 | * future uses of the variable are not using an undefined variable. 1124 | * 1125 | * References (on either side of an assignment) behave differently and this 1126 | * function handles those cases as well. 1127 | * 1128 | * @param File $phpcsFile 1129 | * @param int $stackPtr 1130 | * @param string $varName 1131 | * @param int $currScope 1132 | * 1133 | * @return void 1134 | */ 1135 | protected function processVariableAsAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) 1136 | { 1137 | Helpers::debug("processVariableAsAssignment: starting for '{$varName}'"); 1138 | $assignPtr = Helpers::getNextAssignPointer($phpcsFile, $stackPtr); 1139 | if (! is_int($assignPtr)) { 1140 | return; 1141 | } 1142 | 1143 | // If the right-hand-side of the assignment to this variable is a reference 1144 | // variable, then this variable is a reference to that one, and as such any 1145 | // assignment to this variable (except another assignment by reference, 1146 | // which would change the binding) has a side effect of changing the 1147 | // referenced variable and therefore should count as both an assignment and 1148 | // a read. 1149 | $tokens = $phpcsFile->getTokens(); 1150 | $referencePtr = $phpcsFile->findNext(Tokens::$emptyTokens, $assignPtr + 1, null, true, null, true); 1151 | if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) { 1152 | Helpers::debug("processVariableAsAssignment: found reference variable for '{$varName}'"); 1153 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 1154 | // If the variable was already declared, but was not yet read, it is 1155 | // unused because we're about to change the binding; that is, unless we 1156 | // are inside a conditional block because in that case the condition may 1157 | // never activate. 1158 | $scopeInfo = $this->getOrCreateScopeInfo($currScope); 1159 | $conditionPointer = Helpers::getClosestConditionPositionIfBeforeOtherConditions($tokens[$referencePtr]['conditions']); 1160 | $lastAssignmentPtr = $varInfo->firstDeclared; 1161 | if (! $conditionPointer && $lastAssignmentPtr) { 1162 | Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment"); 1163 | $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo); 1164 | } 1165 | if ($conditionPointer && $lastAssignmentPtr && $conditionPointer < $lastAssignmentPtr) { 1166 | // We may be inside a condition but the last assignment was also inside this condition. 1167 | Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment ignoring recent condition"); 1168 | $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo); 1169 | } 1170 | if ($conditionPointer && $lastAssignmentPtr && $conditionPointer > $lastAssignmentPtr) { 1171 | Helpers::debug("processVariableAsAssignment: not considering close of scope for '{$varName}' due to reference reassignment because it is conditional"); 1172 | } 1173 | // The referenced variable may have a different name, but we don't 1174 | // actually need to mark it as used in this case because the act of this 1175 | // assignment will mark it used on the next token. 1176 | $varInfo->referencedVariableScope = $currScope; 1177 | $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true); 1178 | // An assignment to a reference is a binding and should not count as 1179 | // initialization since it doesn't change any values. 1180 | $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope); 1181 | return; 1182 | } 1183 | 1184 | Helpers::debug('processVariableAsAssignment: marking as assignment in scope', $currScope); 1185 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1186 | 1187 | // If the left-hand-side of the assignment (the variable we are examining) 1188 | // is itself a reference, then that counts as a read as well as a write. 1189 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 1190 | if ($varInfo->isDynamicReference) { 1191 | Helpers::debug('processVariableAsAssignment: also marking as a use because variable is a reference'); 1192 | $this->markVariableRead($varName, $stackPtr, $currScope); 1193 | } 1194 | } 1195 | 1196 | /** 1197 | * Processes variables destructured from an array using shorthand list assignment. 1198 | * 1199 | * This will record the definition and assignment of variables defined using 1200 | * the format: 1201 | * 1202 | * ``` 1203 | * [ $foo, $bar, $baz ] = $ary; 1204 | * ``` 1205 | * 1206 | * Can be called for any token and will return false if the variable is not 1207 | * of this type. 1208 | * 1209 | * @param File $phpcsFile 1210 | * @param int $stackPtr 1211 | * @param string $varName 1212 | * @param int $currScope 1213 | * 1214 | * @return bool 1215 | */ 1216 | protected function processVariableAsListShorthandAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) 1217 | { 1218 | // OK, are we within a [ ... ] construct? 1219 | $openPtr = Helpers::findContainingOpeningSquareBracket($phpcsFile, $stackPtr); 1220 | if (! is_int($openPtr)) { 1221 | return false; 1222 | } 1223 | 1224 | // OK, we're a [ ... ] construct... are we being assigned to? 1225 | $assignments = Helpers::getListAssignments($phpcsFile, $openPtr); 1226 | if (! $assignments) { 1227 | return false; 1228 | } 1229 | $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) { 1230 | if ($assignment === $stackPtr) { 1231 | return $assignment; 1232 | } 1233 | return $thisAssignment; 1234 | }); 1235 | if (! $matchingAssignment) { 1236 | return false; 1237 | } 1238 | 1239 | // Yes, we're being assigned. 1240 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1241 | return true; 1242 | } 1243 | 1244 | /** 1245 | * Processes variables destructured from an array using list assignment. 1246 | * 1247 | * This will record the definition and assignment of variables defined using 1248 | * the format: 1249 | * 1250 | * ``` 1251 | * list( $foo, $bar, $baz ) = $ary; 1252 | * ``` 1253 | * 1254 | * Can be called for any token and will return false if the variable is not 1255 | * of this type. 1256 | * 1257 | * @param File $phpcsFile 1258 | * @param int $stackPtr 1259 | * @param string $varName 1260 | * @param int $currScope 1261 | * 1262 | * @return bool 1263 | */ 1264 | protected function processVariableAsListAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) 1265 | { 1266 | $tokens = $phpcsFile->getTokens(); 1267 | 1268 | // OK, are we within a list (...) construct? 1269 | $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr); 1270 | if ($openPtr === null) { 1271 | return false; 1272 | } 1273 | 1274 | $prevPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true); 1275 | if ((is_bool($prevPtr)) || ($tokens[$prevPtr]['code'] !== T_LIST)) { 1276 | return false; 1277 | } 1278 | 1279 | // OK, we're a list (...) construct... are we being assigned to? 1280 | $assignments = Helpers::getListAssignments($phpcsFile, $prevPtr); 1281 | if (! $assignments) { 1282 | return false; 1283 | } 1284 | $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) { 1285 | if ($assignment === $stackPtr) { 1286 | return $assignment; 1287 | } 1288 | return $thisAssignment; 1289 | }); 1290 | if (! $matchingAssignment) { 1291 | return false; 1292 | } 1293 | 1294 | // Yes, we're being assigned. 1295 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1296 | return true; 1297 | } 1298 | 1299 | /** 1300 | * Process a variable being defined (imported, really) with the `global` keyword. 1301 | * 1302 | * Can be called for any token and will return false if the variable is not 1303 | * of this type. 1304 | * 1305 | * @param File $phpcsFile 1306 | * @param int $stackPtr 1307 | * @param string $varName 1308 | * @param int $currScope 1309 | * 1310 | * @return bool 1311 | */ 1312 | protected function processVariableAsGlobalDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope) 1313 | { 1314 | $tokens = $phpcsFile->getTokens(); 1315 | 1316 | // Are we a global declaration? 1317 | // Search backwards for first token that isn't whitespace/comment, comma or variable. 1318 | $ignore = Tokens::$emptyTokens; 1319 | $ignore[T_VARIABLE] = T_VARIABLE; 1320 | $ignore[T_COMMA] = T_COMMA; 1321 | 1322 | $globalPtr = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true, null, true); 1323 | if (($globalPtr === false) || ($tokens[$globalPtr]['code'] !== T_GLOBAL)) { 1324 | return false; 1325 | } 1326 | 1327 | // It's a global declaration. 1328 | $this->markVariableDeclaration($varName, ScopeType::GLOBALSCOPE, null, $stackPtr, $currScope); 1329 | return true; 1330 | } 1331 | 1332 | /** 1333 | * Process a variable as a static declaration within a function. 1334 | * 1335 | * Specifically, this looks for variable definitions of the form `static 1336 | * $foo = 'hello';` or `static int $foo;` inside a function definition. 1337 | * 1338 | * This will not operate on variables that are written in a class definition 1339 | * outside of a function like `static $foo;` or `public static ?int $foo = 1340 | * 'bar';` because class properties (static or instance) are currently not 1341 | * tracked by this sniff. This is because a class property might be unused 1342 | * inside the class, but used outside the class (we cannot easily know if it 1343 | * is unused); this is also because it's common and legal to define class 1344 | * properties when they are assigned and that assignment can happen outside a 1345 | * class (we cannot easily know if the use of a property is undefined). These 1346 | * sorts of checks are better performed by static analysis tools that can see 1347 | * a whole project rather than a linter which can only easily see a file or 1348 | * some lines. 1349 | * 1350 | * If found, such a variable will be marked as declared (and possibly 1351 | * assigned, if it includes an initial value) within the scope of the 1352 | * function body. 1353 | * 1354 | * This will not operate on variables that use late static binding 1355 | * (`static::$foobar`) or the parameters of static methods even though they 1356 | * include the word `static` in the same statement. 1357 | * 1358 | * This only finds the defintions of static variables. Their use is handled 1359 | * by `processVariableAsStaticMember()`. 1360 | * 1361 | * Can be called for any token and will return false if the variable is not 1362 | * of this type. 1363 | * 1364 | * @param File $phpcsFile 1365 | * @param int $stackPtr 1366 | * @param string $varName 1367 | * @param int $currScope 1368 | * 1369 | * @return bool 1370 | */ 1371 | protected function processVariableAsStaticDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope) 1372 | { 1373 | $tokens = $phpcsFile->getTokens(); 1374 | 1375 | // Search backwards for a `static` keyword that occurs before the start of the statement. 1376 | $startOfStatement = $phpcsFile->findPrevious([T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_FN_ARROW, T_OPEN_PARENTHESIS], $stackPtr - 1, null, false, null, true); 1377 | $staticPtr = $phpcsFile->findPrevious([T_STATIC], $stackPtr - 1, null, false, null, true); 1378 | if (! is_int($startOfStatement)) { 1379 | $startOfStatement = 1; 1380 | } 1381 | if (! is_int($staticPtr)) { 1382 | return false; 1383 | } 1384 | // PHPCS is bad at finding the start of statements so we have to do it ourselves. 1385 | if ($staticPtr < $startOfStatement) { 1386 | return false; 1387 | } 1388 | 1389 | // Is the 'static' keyword an anonymous static function declaration? If so, 1390 | // this is not a static variable declaration. 1391 | $tokenAfterStatic = $phpcsFile->findNext(Tokens::$emptyTokens, $staticPtr + 1, null, true, null, true); 1392 | $functionTokenTypes = [ 1393 | T_FUNCTION, 1394 | T_CLOSURE, 1395 | T_FN, 1396 | ]; 1397 | if (is_int($tokenAfterStatic) && in_array($tokens[$tokenAfterStatic]['code'], $functionTokenTypes, true)) { 1398 | return false; 1399 | } 1400 | 1401 | // Is the token inside function parameters? If so, this is not a static 1402 | // declaration because we must be inside a function body. 1403 | if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) { 1404 | return false; 1405 | } 1406 | 1407 | // Is the token inside a function call? If so, this is not a static 1408 | // declaration. 1409 | if (Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) { 1410 | return false; 1411 | } 1412 | 1413 | // Is the keyword a late static binding? If so, this isn't the static 1414 | // keyword we're looking for, but since static:: isn't allowed in a 1415 | // compile-time constant, we also know we can't be part of a static 1416 | // declaration anyway, so there's no need to look any further. 1417 | $lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true); 1418 | if (($lateStaticBindingPtr !== false) && ($tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON)) { 1419 | return false; 1420 | } 1421 | 1422 | $this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope); 1423 | if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) { 1424 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1425 | } 1426 | return true; 1427 | } 1428 | 1429 | /** 1430 | * @param File $phpcsFile 1431 | * @param int $stackPtr 1432 | * @param string $varName 1433 | * @param int $currScope 1434 | * 1435 | * @return bool 1436 | */ 1437 | protected function processVariableAsForeachLoopVar(File $phpcsFile, $stackPtr, $varName, $currScope) 1438 | { 1439 | $tokens = $phpcsFile->getTokens(); 1440 | 1441 | // Are we a foreach loopvar? 1442 | $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr); 1443 | if (! is_int($openParenPtr)) { 1444 | return false; 1445 | } 1446 | $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true)); 1447 | if (! is_int($foreachPtr)) { 1448 | return false; 1449 | } 1450 | if ($tokens[$foreachPtr]['code'] === T_LIST) { 1451 | $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $foreachPtr); 1452 | if (! is_int($openParenPtr)) { 1453 | return false; 1454 | } 1455 | $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true)); 1456 | if (! is_int($foreachPtr)) { 1457 | return false; 1458 | } 1459 | } 1460 | if ($tokens[$foreachPtr]['code'] !== T_FOREACH) { 1461 | return false; 1462 | } 1463 | 1464 | // Is there an 'as' token between us and the foreach? 1465 | if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openParenPtr) === false) { 1466 | return false; 1467 | } 1468 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1469 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 1470 | 1471 | // Is this the value of a key => value foreach? 1472 | if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, $stackPtr - 1, $openParenPtr) !== false) { 1473 | $varInfo->isForeachLoopAssociativeValue = true; 1474 | } 1475 | 1476 | // Are we pass-by-reference? 1477 | $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true); 1478 | if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) { 1479 | Helpers::debug('processVariableAsForeachLoopVar: found foreach loop variable assigned by reference'); 1480 | $varInfo->isDynamicReference = true; 1481 | } 1482 | 1483 | return true; 1484 | } 1485 | 1486 | /** 1487 | * @param File $phpcsFile 1488 | * @param int $stackPtr 1489 | * @param string $varName 1490 | * @param int $currScope 1491 | * 1492 | * @return bool 1493 | */ 1494 | protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, $stackPtr, $varName, $currScope) 1495 | { 1496 | $tokens = $phpcsFile->getTokens(); 1497 | 1498 | // Are we pass-by-reference to known pass-by-reference function? 1499 | $functionPtr = Helpers::findFunctionCall($phpcsFile, $stackPtr); 1500 | if ($functionPtr === null || ! isset($tokens[$functionPtr])) { 1501 | return false; 1502 | } 1503 | 1504 | // Is our function a known pass-by-reference function? 1505 | $functionName = $tokens[$functionPtr]['content']; 1506 | $refArgs = $this->getPassByReferenceFunction($functionName); 1507 | if (! $refArgs) { 1508 | // Check again with the fully namespaced function name. 1509 | $functionName = Helpers::getFunctionNameWithNamespace($phpcsFile, $functionPtr); 1510 | if (! $functionName) { 1511 | return false; 1512 | } 1513 | $refArgs = $this->getPassByReferenceFunction($functionName); 1514 | if (! $refArgs) { 1515 | return false; 1516 | } 1517 | } 1518 | 1519 | $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr); 1520 | 1521 | // We're within a function call arguments list, find which arg we are. 1522 | $argPos = false; 1523 | foreach ($argPtrs as $idx => $ptrs) { 1524 | if (in_array($stackPtr, $ptrs)) { 1525 | $argPos = $idx + 1; 1526 | break; 1527 | } 1528 | } 1529 | if ($argPos === false) { 1530 | return false; 1531 | } 1532 | if (!in_array($argPos, $refArgs)) { 1533 | // Our arg wasn't mentioned explicitly, are we after an elipsis catch-all? 1534 | $elipsis = array_search('...', $refArgs); 1535 | if ($elipsis === false) { 1536 | return false; 1537 | } 1538 | $elipsis = (int)$elipsis; 1539 | if ($argPos < $refArgs[$elipsis - 1]) { 1540 | return false; 1541 | } 1542 | } 1543 | 1544 | // Our argument position matches that of a pass-by-ref argument, 1545 | // check that we're the only part of the argument expression. 1546 | foreach ($argPtrs[$argPos - 1] as $ptr) { 1547 | if ($ptr === $stackPtr) { 1548 | continue; 1549 | } 1550 | if (isset(Tokens::$emptyTokens[$tokens[$ptr]['code']]) === false) { 1551 | return false; 1552 | } 1553 | } 1554 | 1555 | // Just us, we can mark it as a write. 1556 | $this->markVariableAssignment($varName, $stackPtr, $currScope); 1557 | // It's a read as well for purposes of used-variables. 1558 | $this->markVariableRead($varName, $stackPtr, $currScope); 1559 | return true; 1560 | } 1561 | 1562 | /** 1563 | * @param File $phpcsFile 1564 | * @param int $stackPtr 1565 | * @param string $varName 1566 | * @param int $currScope 1567 | * 1568 | * @return bool 1569 | */ 1570 | protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope) 1571 | { 1572 | $tokens = $phpcsFile->getTokens(); 1573 | 1574 | // Are we a symbolic object property/function derefeference? 1575 | // Search backwards for first token that isn't whitespace, is it a "->" operator? 1576 | $objectOperatorPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true); 1577 | if (($objectOperatorPtr === false) || ($tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR)) { 1578 | return false; 1579 | } 1580 | 1581 | $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); 1582 | return true; 1583 | } 1584 | 1585 | /** 1586 | * Process a normal variable in the code. 1587 | * 1588 | * Most importantly, this function determines if the variable use is a "read" 1589 | * (using the variable for something) or a "write" (an assignment) or, 1590 | * sometimes, both at once. 1591 | * 1592 | * It also determines the scope of the variable (where it begins and ends). 1593 | * 1594 | * Using these two pieces of information, we can determine if the variable is 1595 | * being used ("read") without having been defined ("write"). 1596 | * 1597 | * We can also determine, once the scan has hit the end of a scope, if any of 1598 | * the variables within that scope have been defined ("write") without being 1599 | * used ("read"). That behavior, however, happens in the `processScopeClose()` 1600 | * function using the data gathered by this function. 1601 | * 1602 | * Some variables are used in more complex ways, so there are other similar 1603 | * functions to this one, like `processVariableInString`, and 1604 | * `processCompact`. They have the same purpose as this function, though. 1605 | * 1606 | * If the 'ignore-for-loops' option is true, we will ignore the special 1607 | * processing for the increment variables of for loops. This will prevent 1608 | * infinite loops. 1609 | * 1610 | * @param File $phpcsFile The PHP_CodeSniffer file where this token was found. 1611 | * @param int $stackPtr The position where the token was found. 1612 | * @param array $options See above. 1613 | * 1614 | * @return void 1615 | */ 1616 | protected function processVariable(File $phpcsFile, $stackPtr, $options = []) 1617 | { 1618 | $tokens = $phpcsFile->getTokens(); 1619 | $token = $tokens[$stackPtr]; 1620 | 1621 | // Get the name of the variable. 1622 | $varName = Helpers::normalizeVarName($token['content']); 1623 | Helpers::debug("examining token for variable '{$varName}' on line {$token['line']}", $token); 1624 | 1625 | // Find the start of the current scope. 1626 | $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr); 1627 | if ($currScope === null) { 1628 | Helpers::debug('no scope found'); 1629 | return; 1630 | } 1631 | Helpers::debug("start of scope for variable '{$varName}' is", $currScope); 1632 | 1633 | // Determine if variable is being assigned ("write") or used ("read"). 1634 | 1635 | // Read methods that preempt assignment: 1636 | // Are we a $object->$property type symbolic reference? 1637 | 1638 | // Possible assignment methods: 1639 | // Is a mandatory function/closure parameter 1640 | // Is an optional function/closure parameter with non-null value 1641 | // Is closure use declaration of a variable defined within containing scope 1642 | // catch (...) block start 1643 | // $this within a class. 1644 | // $GLOBALS, $_REQUEST, etc superglobals. 1645 | // $var part of class::$var static member 1646 | // Assignment via = 1647 | // Assignment via list (...) = 1648 | // Declares as a global 1649 | // Declares as a static 1650 | // Assignment via foreach (... as ...) { } 1651 | // Pass-by-reference to known pass-by-reference function 1652 | 1653 | // Are we inside the third expression of a for loop? Store such variables 1654 | // for processing after the loop ends by `processClosingForLoopsAt()`. 1655 | if (empty($options['ignore-for-loops'])) { 1656 | $forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops); 1657 | if ($forLoop) { 1658 | Helpers::debug('found variable inside for loop third expression'); 1659 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 1660 | $forLoop->incrementVariables[$stackPtr] = $varInfo; 1661 | return; 1662 | } 1663 | } 1664 | 1665 | // Are we a $object->$property type symbolic reference? 1666 | if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) { 1667 | Helpers::debug('found symbolic object property'); 1668 | return; 1669 | } 1670 | 1671 | // Are we a function or closure parameter? 1672 | if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) { 1673 | Helpers::debug('found function definition parameter'); 1674 | $this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope); 1675 | return; 1676 | } 1677 | 1678 | // Are we a variable being imported into a function's scope with "use"? 1679 | if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) { 1680 | Helpers::debug('found use scope import definition'); 1681 | $this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope); 1682 | return; 1683 | } 1684 | 1685 | // Are we a catch parameter? 1686 | if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) { 1687 | Helpers::debug('found catch block'); 1688 | return; 1689 | } 1690 | 1691 | // Are we $this within a class? 1692 | if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) { 1693 | Helpers::debug('found this usage within a class'); 1694 | return; 1695 | } 1696 | 1697 | // Are we a $GLOBALS, $_REQUEST, etc superglobal? 1698 | if ($this->processVariableAsSuperGlobal($varName)) { 1699 | Helpers::debug('found superglobal'); 1700 | return; 1701 | } 1702 | 1703 | // Check for static members used outside a class 1704 | if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) { 1705 | Helpers::debug('found static usage outside of class'); 1706 | return; 1707 | } 1708 | 1709 | // $var part of class::$var static member 1710 | if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) { 1711 | Helpers::debug('found static member'); 1712 | return; 1713 | } 1714 | 1715 | if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) { 1716 | Helpers::debug('found class property definition'); 1717 | return; 1718 | } 1719 | 1720 | // Is the next non-whitespace an assignment? 1721 | if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) { 1722 | Helpers::debug('found assignment'); 1723 | $this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope); 1724 | if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) { 1725 | Helpers::debug("found assignment that's also inside an expression"); 1726 | $this->markVariableRead($varName, $stackPtr, $currScope); 1727 | return; 1728 | } 1729 | return; 1730 | } 1731 | 1732 | // OK, are we within a list (...) = construct? 1733 | if ($this->processVariableAsListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) { 1734 | Helpers::debug('found list assignment'); 1735 | return; 1736 | } 1737 | 1738 | // OK, are we within a [...] = construct? 1739 | if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) { 1740 | Helpers::debug('found list shorthand assignment'); 1741 | return; 1742 | } 1743 | 1744 | // Are we a global declaration? 1745 | if ($this->processVariableAsGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) { 1746 | Helpers::debug('found global declaration'); 1747 | return; 1748 | } 1749 | 1750 | // Are we a static declaration? 1751 | if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) { 1752 | Helpers::debug('found static declaration'); 1753 | return; 1754 | } 1755 | 1756 | // Are we a foreach loopvar? 1757 | if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) { 1758 | Helpers::debug('found foreach loop variable'); 1759 | return; 1760 | } 1761 | 1762 | // Are we pass-by-reference to known pass-by-reference function? 1763 | if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) { 1764 | Helpers::debug('found pass by reference'); 1765 | return; 1766 | } 1767 | 1768 | // Are we a numeric variable used for constructs like preg_replace? 1769 | if (Helpers::isVariableANumericVariable($varName)) { 1770 | Helpers::debug('found numeric variable'); 1771 | return; 1772 | } 1773 | 1774 | if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) { 1775 | Helpers::debug('found variable inside else condition or body'); 1776 | $this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope); 1777 | return; 1778 | } 1779 | 1780 | // Are we an isset or empty call? 1781 | if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) { 1782 | Helpers::debug('found isset or empty'); 1783 | $this->markVariableRead($varName, $stackPtr, $currScope); 1784 | return; 1785 | } 1786 | 1787 | // OK, we don't appear to be a write to the var, assume we're a read. 1788 | Helpers::debug('looks like a variable read'); 1789 | $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); 1790 | } 1791 | 1792 | /** 1793 | * @param File $phpcsFile 1794 | * @param int $stackPtr 1795 | * @param string $varName 1796 | * @param int $currScope 1797 | * 1798 | * @return void 1799 | */ 1800 | protected function processVaribleInsideElse(File $phpcsFile, $stackPtr, $varName, $currScope) 1801 | { 1802 | // Find all assignments to this variable inside the current scope. 1803 | $varInfo = $this->getOrCreateVariableInfo($varName, $currScope); 1804 | $allAssignmentIndices = array_unique($varInfo->allAssignments); 1805 | // Find the attached 'if' and 'elseif' block start and end indices. 1806 | $blockIndices = Helpers::getAttachedBlockIndicesForElse($phpcsFile, $stackPtr); 1807 | 1808 | // If all of the assignments are within the previous attached blocks, then warn about undefined. 1809 | $tokens = $phpcsFile->getTokens(); 1810 | $assignmentsInsideAttachedBlocks = []; 1811 | foreach ($allAssignmentIndices as $index) { 1812 | foreach ($blockIndices as $blockIndex) { 1813 | $blockToken = $tokens[$blockIndex]; 1814 | Helpers::debug('for variable inside else, looking at assignment', $index, 'at block index', $blockIndex, 'which is token', $blockToken); 1815 | if (isset($blockToken['scope_opener']) && isset($blockToken['scope_closer'])) { 1816 | $scopeOpener = $blockToken['scope_opener']; 1817 | $scopeCloser = $blockToken['scope_closer']; 1818 | } else { 1819 | // If the `if` statement has no scope, it is probably inline, which 1820 | // means its scope is from the end of the condition up until the next 1821 | // semicolon 1822 | $scopeOpener = isset($blockToken['parenthesis_closer']) ? $blockToken['parenthesis_closer'] : $blockIndex + 1; 1823 | $scopeCloser = $phpcsFile->findNext([T_SEMICOLON], $scopeOpener); 1824 | if (! $scopeCloser) { 1825 | throw new \Exception("Cannot find scope for if condition block at index {$stackPtr} while examining variable {$varName}"); 1826 | } 1827 | } 1828 | Helpers::debug('for variable inside else, looking at scope', $index, 'between', $scopeOpener, 'and', $scopeCloser); 1829 | if (Helpers::isIndexInsideScope($index, $scopeOpener, $scopeCloser)) { 1830 | $assignmentsInsideAttachedBlocks[] = $index; 1831 | } 1832 | } 1833 | } 1834 | 1835 | if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) { 1836 | if (! $varInfo->ignoreUndefined) { 1837 | Helpers::debug("variable $varName inside else looks undefined"); 1838 | $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr); 1839 | } 1840 | return; 1841 | } 1842 | 1843 | Helpers::debug('looks like a variable read inside else'); 1844 | $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); 1845 | } 1846 | 1847 | /** 1848 | * Called to process variables found in double quoted strings. 1849 | * 1850 | * Note that there may be more than one variable in the string, which will 1851 | * result only in one call for the string. 1852 | * 1853 | * @param File $phpcsFile The PHP_CodeSniffer file where this token was found. 1854 | * @param int $stackPtr The position where the double quoted string was found. 1855 | * 1856 | * @return void 1857 | */ 1858 | protected function processVariableInString(File $phpcsFile, $stackPtr) 1859 | { 1860 | $tokens = $phpcsFile->getTokens(); 1861 | $token = $tokens[$stackPtr]; 1862 | 1863 | $regexp = Constants::getDoubleQuotedVarRegexp(); 1864 | if (! empty($regexp) && !preg_match_all($regexp, $token['content'], $matches)) { 1865 | Helpers::debug('processVariableInString: no variables found', $token); 1866 | return; 1867 | } 1868 | Helpers::debug('examining token for variable in string', $token); 1869 | 1870 | if (empty($matches)) { 1871 | Helpers::debug('processVariableInString: no variables found after search', $token); 1872 | return; 1873 | } 1874 | foreach ($matches[1] as $varName) { 1875 | $varName = Helpers::normalizeVarName($varName); 1876 | 1877 | // Are we $this within a class? 1878 | if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) { 1879 | continue; 1880 | } 1881 | 1882 | if ($this->processVariableAsSuperGlobal($varName)) { 1883 | continue; 1884 | } 1885 | 1886 | // Are we a numeric variable used for constructs like preg_replace? 1887 | if (Helpers::isVariableANumericVariable($varName)) { 1888 | continue; 1889 | } 1890 | 1891 | $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $varName); 1892 | if ($currScope === null) { 1893 | continue; 1894 | } 1895 | 1896 | $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); 1897 | } 1898 | } 1899 | 1900 | /** 1901 | * Called to process variables named in a call to compact(). 1902 | * 1903 | * @param File $phpcsFile The PHP_CodeSniffer file where this token was found. 1904 | * @param int $stackPtr The position where the call to compact() was found. 1905 | * 1906 | * @return void 1907 | */ 1908 | protected function processCompact(File $phpcsFile, $stackPtr) 1909 | { 1910 | Helpers::debug("processCompact at {$stackPtr}"); 1911 | $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr); 1912 | $variables = Helpers::getVariablesInsideCompact($phpcsFile, $stackPtr, $arguments); 1913 | foreach ($variables as $variable) { 1914 | $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $variable->name); 1915 | if ($currScope === null) { 1916 | continue; 1917 | } 1918 | $variablePosition = $variable->firstRead ? $variable->firstRead : $stackPtr; 1919 | $this->markVariableReadAndWarnIfUndefined($phpcsFile, $variable->name, $variablePosition, $currScope); 1920 | } 1921 | } 1922 | 1923 | /** 1924 | * Called to process the end of a scope. 1925 | * 1926 | * Note that although triggered by the closing curly brace of the scope, 1927 | * $stackPtr is the scope conditional, not the closing curly brace. 1928 | * 1929 | * @param File $phpcsFile The PHP_CodeSniffer file where this token was found. 1930 | * @param int $stackPtr The position of the scope conditional. 1931 | * 1932 | * @return void 1933 | */ 1934 | protected function processScopeClose(File $phpcsFile, $stackPtr) 1935 | { 1936 | Helpers::debug("processScopeClose at {$stackPtr}"); 1937 | $scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr); 1938 | if (is_null($scopeInfo)) { 1939 | return; 1940 | } 1941 | foreach ($scopeInfo->variables as $varInfo) { 1942 | $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo); 1943 | } 1944 | } 1945 | 1946 | /** 1947 | * Warn about an unused variable if it has not been used within a scope. 1948 | * 1949 | * @param File $phpcsFile 1950 | * @param VariableInfo $varInfo 1951 | * @param ScopeInfo $scopeInfo 1952 | * 1953 | * @return void 1954 | */ 1955 | protected function processScopeCloseForVariable(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo) 1956 | { 1957 | Helpers::debug('processScopeCloseForVariable', $varInfo); 1958 | if ($varInfo->ignoreUnused || isset($varInfo->firstRead)) { 1959 | return; 1960 | } 1961 | if ($this->allowUnusedFunctionParameters && $varInfo->scopeType === ScopeType::PARAM) { 1962 | return; 1963 | } 1964 | if ($this->allowUnusedParametersBeforeUsed && $varInfo->scopeType === ScopeType::PARAM && Helpers::areFollowingArgumentsUsed($varInfo, $scopeInfo)) { 1965 | Helpers::debug("variable '{$varInfo->name}' at end of scope has unused following args"); 1966 | return; 1967 | } 1968 | if ($this->allowUnusedForeachVariables && $varInfo->isForeachLoopAssociativeValue) { 1969 | return; 1970 | } 1971 | if ($varInfo->referencedVariableScope !== null && isset($varInfo->firstInitialized)) { 1972 | Helpers::debug("variable '{$varInfo->name}' at end of scope is a reference and so counts as used"); 1973 | // If we're pass-by-reference then it's a common pattern to 1974 | // use the variable to return data to the caller, so any 1975 | // assignment also counts as "variable use" for the purposes 1976 | // of "unused variable" warnings. 1977 | return; 1978 | } 1979 | if ($varInfo->scopeType === ScopeType::GLOBALSCOPE && isset($varInfo->firstInitialized)) { 1980 | Helpers::debug("variable '{$varInfo->name}' at end of scope is a global and so counts as used"); 1981 | // If we imported this variable from the global scope, any further use of 1982 | // the variable, including assignment, should count as "variable use" for 1983 | // the purposes of "unused variable" warnings. 1984 | return; 1985 | } 1986 | if (empty($varInfo->firstDeclared) && empty($varInfo->firstInitialized)) { 1987 | return; 1988 | } 1989 | if ($this->allowUnusedVariablesBeforeRequire && Helpers::isRequireInScopeAfter($phpcsFile, $varInfo, $scopeInfo)) { 1990 | return; 1991 | } 1992 | if ($scopeInfo->scopeStartIndex === 0 && $this->allowUnusedVariablesInFileScope) { 1993 | return; 1994 | } 1995 | if ( 1996 | ! empty($varInfo->firstDeclared) 1997 | && $varInfo->scopeType === ScopeType::PARAM 1998 | && Helpers::isInAbstractClass( 1999 | $phpcsFile, 2000 | Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0 2001 | ) 2002 | && Helpers::isFunctionBodyEmpty( 2003 | $phpcsFile, 2004 | Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0 2005 | ) 2006 | ) { 2007 | // Allow non-abstract methods inside an abstract class to have unused 2008 | // parameters if the method body does nothing. Such methods are 2009 | // effectively optional abstract methods so their unused parameters 2010 | // should be ignored as we do with abstract method parameters. 2011 | return; 2012 | } 2013 | 2014 | $this->warnAboutUnusedVariable($phpcsFile, $varInfo); 2015 | } 2016 | 2017 | /** 2018 | * Register warnings for a variable that is defined but not used. 2019 | * 2020 | * @param File $phpcsFile 2021 | * @param VariableInfo $varInfo 2022 | * 2023 | * @return void 2024 | */ 2025 | protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo) 2026 | { 2027 | foreach (array_unique($varInfo->allAssignments) as $indexForWarning) { 2028 | Helpers::debug("variable '{$varInfo->name}' at end of scope looks unused"); 2029 | $phpcsFile->addWarning( 2030 | 'Unused %s %s.', 2031 | $indexForWarning, 2032 | 'UnusedVariable', 2033 | [ 2034 | VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL], 2035 | "\${$varInfo->name}", 2036 | ] 2037 | ); 2038 | } 2039 | } 2040 | 2041 | /** 2042 | * @param File $phpcsFile 2043 | * @param string $varName 2044 | * @param int $stackPtr 2045 | * 2046 | * @return void 2047 | */ 2048 | protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr) 2049 | { 2050 | $phpcsFile->addWarning( 2051 | 'Variable %s is undefined.', 2052 | $stackPtr, 2053 | 'UndefinedVariable', 2054 | ["\${$varName}"] 2055 | ); 2056 | } 2057 | 2058 | /** 2059 | * @param File $phpcsFile 2060 | * @param string $varName 2061 | * @param int $stackPtr 2062 | * 2063 | * @return void 2064 | */ 2065 | protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr) 2066 | { 2067 | $phpcsFile->addWarning( 2068 | 'Array variable %s is undefined.', 2069 | $stackPtr, 2070 | 'UndefinedVariable', 2071 | ["\${$varName}"] 2072 | ); 2073 | } 2074 | 2075 | /** 2076 | * @param File $phpcsFile 2077 | * @param string $varName 2078 | * @param int $stackPtr 2079 | * 2080 | * @return void 2081 | */ 2082 | protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr) 2083 | { 2084 | $phpcsFile->addWarning( 2085 | 'Variable %s inside unset call is undefined.', 2086 | $stackPtr, 2087 | 'UndefinedUnsetVariable', 2088 | ["\${$varName}"] 2089 | ); 2090 | } 2091 | } 2092 | -------------------------------------------------------------------------------- /VariableAnalysis/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. 4 | 5 | -------------------------------------------------------------------------------- /VariableAnalysis/ruleset.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. 4 | 5 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sirbrillig/phpcs-variable-analysis", 3 | "description": "A PHPCS sniff to detect problems with variables.", 4 | "type": "phpcodesniffer-standard", 5 | "keywords": ["phpcs", "static analysis"], 6 | "license": "BSD-2-Clause", 7 | "authors": [ 8 | { 9 | "name": "Sam Graham", 10 | "email": "php-codesniffer-variableanalysis@illusori.co.uk" 11 | }, 12 | { 13 | "name": "Payton Swick", 14 | "email": "payton@foolord.com" 15 | } 16 | ], 17 | "support": { 18 | "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", 19 | "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki", 20 | "source": "https://github.com/sirbrillig/phpcs-variable-analysis" 21 | }, 22 | "config": { 23 | "sort-order": true, 24 | "allow-plugins": { 25 | "dealerdirect/phpcodesniffer-composer-installer": true 26 | }, 27 | "lock": false 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "VariableAnalysis\\": "VariableAnalysis/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "VariableAnalysis\\Tests\\": "Tests/" 37 | } 38 | }, 39 | "minimum-stability": "dev", 40 | "prefer-stable": true, 41 | "scripts": { 42 | "test": "./vendor/bin/phpunit --no-coverage", 43 | "coverage": "./vendor/bin/phpunit", 44 | "test-lte9": "./vendor/bin/phpunit -c phpunitlte9.xml.dist --no-coverage", 45 | "coverage-lte9": "./vendor/bin/phpunit -c phpunitlte9.xml.dist", 46 | "lint": "./vendor/bin/phpcs", 47 | "fix": "./vendor/bin/phpcbf", 48 | "phpstan": "./vendor/bin/phpstan analyse", 49 | "psalm": "./vendor/bin/psalm --no-cache", 50 | "static-analysis": "composer phpstan && composer psalm" 51 | }, 52 | "require": { 53 | "php": ">=5.4.0", 54 | "squizlabs/php_codesniffer": "^3.5.6" 55 | }, 56 | "require-dev": { 57 | "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", 58 | "phpcsstandards/phpcsdevcs": "^1.1", 59 | "phpstan/phpstan": "^1.7", 60 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", 61 | "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /psalm-autoloader.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------