* @copyright 2011-2012 Sam Graham * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @link http://pear.php.net/package/PHP_CodeSniffer */ /** * Holds details of a scope. * * @category PHP * @package PHP_CodeSniffer * @author Sam Graham * @copyright 2011-2012 Sam Graham * @link http://pear.php.net/package/PHP_CodeSniffer */ class ScopeInfo { public $owner; public $opener; public $closer; public $variables = array(); function __construct($currScope) { $this->owner = $currScope; // TODO: extract opener/closer } } /** * Holds details of a variable within a scope. * * @category PHP * @package PHP_CodeSniffer * @author Sam Graham * @copyright 2011 Sam Graham * @link http://pear.php.net/package/PHP_CodeSniffer */ class VariableInfo { public $name; /** * What scope the variable has: local, param, static, global, bound */ public $scopeType; public $typeHint; public $passByReference = false; public $firstDeclared; public $firstInitialized; public $firstRead; public $ignoreUnused = false; static $scopeTypeDescriptions = array( 'local' => 'variable', 'param' => 'function parameter', 'static' => 'static variable', 'global' => 'global variable', 'bound' => 'bound variable', ); function __construct($varName) { $this->name = $varName; } } /** * Checks the for undefined function variables. * * This sniff checks that all function variables * are defined in the function body. * * @category PHP * @package PHP_CodeSniffer * @author Sam Graham * @copyright 2011 Sam Graham * @link http://pear.php.net/package/PHP_CodeSniffer */ class TTCodeStandard_Sniffs_CodeAnalysis_VariableAnalysisSniff implements PHP_CodeSniffer_Sniff { /** * The current phpcsFile being checked. * * @var phpcsFile */ protected $currentFile = null; /** * A list of scopes encountered so far and the variables within them. */ private $_scopes = array(); /** * A regexp for matching variable names in double-quoted strings. */ private $_double_quoted_variable_regexp = '|(? array(5), 'addFunction' => array(3), 'addTask' => array(3), 'addTaskBackground' => array(3), 'addTaskHigh' => array(3), 'addTaskHighBackground' => array(3), 'addTaskLow' => array(3), 'addTaskLowBackground' => array(3), 'addTaskStatus' => array(2), 'apc_dec' => array(3), 'apc_fetch' => array(2), 'apc_inc' => array(3), 'areConfusable' => array(3), 'array_multisort' => array(1), 'array_pop' => array(1), 'array_push' => array(1), 'array_replace' => array(1), 'array_replace_recursive' => array(1, 2, 3, '...'), 'array_shift' => array(1), 'array_splice' => array(1), 'array_unshift' => array(1), 'array_walk' => array(1), 'array_walk_recursive' => array(1), 'arsort' => array(1), 'asort' => array(1), 'asort' => array(1), 'bindColumn' => array(2), 'bindParam' => array(2), 'bind_param' => array(2, 3, '...'), 'bind_result' => array(1, 2, '...'), 'call_user_method' => array(2), 'call_user_method_array' => array(2), 'curl_multi_exec' => array(2), 'curl_multi_info_read' => array(2), 'current' => array(1), 'dbplus_curr' => array(2), 'dbplus_first' => array(2), 'dbplus_info' => array(3), 'dbplus_last' => array(2), 'dbplus_next' => array(2), 'dbplus_prev' => array(2), 'dbplus_tremove' => array(3), 'dns_get_record' => array(3, 4), 'domxml_open_file' => array(3), 'domxml_open_mem' => array(3), 'each' => array(1), 'enchant_dict_quick_check' => array(3), 'end' => array(1), 'ereg' => array(3), 'eregi' => array(3), 'exec' => array(2, 3), 'exif_thumbnail' => array(1, 2, 3), 'expect_expectl' => array(3), 'extract' => array(1), 'filter' => array(3), 'flock' => array(2,3), 'fscanf' => array(2, 3, '...'), 'fsockopen' => array(3, 4), 'ftp_alloc' => array(3), 'get' => array(2, 3), 'getByKey' => array(4), 'getMulti' => array(2), 'getMultiByKey' => array(3), 'getimagesize' => array(2), 'getmxrr' => array(2, 3), 'gnupg_decryptverify' => array(3), 'gnupg_verify' => array(4), 'grapheme_extract' => array(5), 'headers_sent' => array(1, 2), 'http_build_url' => array(4), 'http_get' => array(3), 'http_head' => array(3), 'http_negotiate_charset' => array(2), 'http_negotiate_content_type' => array(2), 'http_negotiate_language' => array(2), 'http_post_data' => array(4), 'http_post_fields' => array(5), 'http_put_data' => array(4), 'http_put_file' => array(4), 'http_put_stream' => array(4), 'http_request' => array(5), 'isSuspicious' => array(2), 'is_callable' => array(3), 'key' => array(1), 'krsort' => array(1), 'ksort' => array(1), 'ldap_get_option' => array(3), 'ldap_parse_reference' => array(3), 'ldap_parse_result' => array(3, 4, 5, 6), 'localtime' => array(2), 'm_completeauthorizations' => array(2), 'maxdb_stmt_bind_param' => array(3, 4, '...'), 'maxdb_stmt_bind_result' => array(2, 3, '...'), 'mb_convert_variables' => array(3, 4, '...'), 'mb_parse_str' => array(2), 'mqseries_back' => array(2, 3), 'mqseries_begin' => array(3, 4), 'mqseries_close' => array(4, 5), 'mqseries_cmit' => array(2, 3), 'mqseries_conn' => array(2, 3, 4), 'mqseries_connx' => array(2, 3, 4, 5), 'mqseries_disc' => array(2, 3), 'mqseries_get' => array(3, 4, 5, 6, 7, 8, 9), 'mqseries_inq' => array(6, 8, 9, 10), 'mqseries_open' => array(2, 4, 5, 6), 'mqseries_put' => array(3, 4, 6, 7), 'mqseries_put1' => array(2, 3, 4, 6, 7), 'mqseries_set' => array(9, 10), 'msg_receive' => array(3, 5, 8), 'msg_send' => array(6), 'mssql_bind' => array(3), 'natcasesort' => array(1), 'natsort' => array(1), 'ncurses_color_content' => array(2, 3, 4), 'ncurses_getmaxyx' => array(2, 3), 'ncurses_getmouse' => array(1), 'ncurses_getyx' => array(2, 3), 'ncurses_instr' => array(1), 'ncurses_mouse_trafo' => array(1, 2), 'ncurses_mousemask' => array(2), 'ncurses_pair_content' => array(2, 3), 'ncurses_wmouse_trafo' => array(2, 3), 'newt_button_bar' => array(1), 'newt_form_run' => array(2), 'newt_get_screen_size' => array(1, 2), 'newt_grid_get_size' => array(2, 3), 'newt_reflow_text' => array(5, 6), 'newt_win_entries' => array(7), 'newt_win_menu' => array(8), 'next' => array(1), 'oci_bind_array_by_name' => array(3), 'oci_bind_by_name' => array(3), 'oci_define_by_name' => array(3), 'oci_fetch_all' => array(2), 'ocifetchinto' => array(2), 'odbc_fetch_into' => array(2), 'openssl_csr_export' => array(2), 'openssl_csr_new' => array(2), 'openssl_open' => array(2), 'openssl_pkcs12_export' => array(2), 'openssl_pkcs12_read' => array(2), 'openssl_pkey_export' => array(2), 'openssl_private_decrypt' => array(2), 'openssl_private_encrypt' => array(2), 'openssl_public_decrypt' => array(2), 'openssl_public_encrypt' => array(2), 'openssl_random_pseudo_bytes' => array(2), 'openssl_seal' => array(2, 3), 'openssl_sign' => array(2), 'openssl_x509_export' => array(2), 'ovrimos_fetch_into' => array(2), 'parse' => array(2,3), 'parseCurrency' => array(2, 3), 'parse_str' => array(2), 'parsekit_compile_file' => array(2), 'parsekit_compile_string' => array(2), 'passthru' => array(2), 'pcntl_sigprocmask' => array(3), 'pcntl_sigtimedwait' => array(2), 'pcntl_sigwaitinfo' => array(2), 'pcntl_wait' => array(1), 'pcntl_waitpid' => array(2), 'pfsockopen' => array(3, 4), 'php_check_syntax' => array(2), 'poll' => array(1, 2, 3), 'preg_filter' => array(5), 'preg_match' => array(3), 'preg_match_all' => array(3), 'preg_replace' => array(5), 'preg_replace_callback' => array(5), 'prev' => array(1), 'proc_open' => array(3), 'query' => array(3), 'queryExec' => array(2), 'reset' => array(1), 'rsort' => array(1), 'settype' => array(1), 'shuffle' => array(1), 'similar_text' => array(3), 'socket_create_pair' => array(4), 'socket_getpeername' => array(2, 3), 'socket_getsockname' => array(2, 3), 'socket_recv' => array(2), 'socket_recvfrom' => array(2, 5, 6), 'socket_select' => array(1, 2, 3), 'sort' => array(1), 'sortWithSortKeys' => array(1), 'sqlite_exec' => array(3), 'sqlite_factory' => array(3), 'sqlite_open' => array(3), 'sqlite_popen' => array(3), 'sqlite_query' => array(4), 'sqlite_query' => array(4), 'sqlite_unbuffered_query' => array(4), 'sscanf' => array(3, '...'), 'str_ireplace' => array(4), 'str_replace' => array(4), 'stream_open' => array(4), 'stream_select' => array(1, 2, 3), 'stream_socket_accept' => array(3), 'stream_socket_client' => array(2, 3), 'stream_socket_recvfrom' => array(4), 'stream_socket_server' => array(2, 3), 'system' => array(2), 'uasort' => array(1), 'uksort' => array(1), 'unbufferedQuery' => array(3), 'usort' => array(1), 'wincache_ucache_dec' => array(3), 'wincache_ucache_get' => array(2), 'wincache_ucache_inc' => array(3), 'xdiff_string_merge3' => array(4), 'xdiff_string_patch' => array(4), 'xml_parse_into_struct' => array(3, 4), 'xml_set_object' => array(2), 'xmlrpc_decode_request' => array(2), 'xmlrpc_set_type' => array(1), 'xslt_set_object' => array(2), 'yaml_parse' => array(3), 'yaml_parse_file' => array(3), 'yaml_parse_url' => array(3), 'yaz_ccl_parse' => array(3), 'yaz_hits' => array(2), 'yaz_scan_result' => array(2), 'yaz_wait' => array(1), ); /** * Allows an install to extend the list of known pass-by-reference functions * by defining generic.codeanalysis.variableanalysis.sitePassByRefFunctions. */ public $sitePassByRefFunctions = null; /** * Allows exceptions in a catch block to be unused without provoking unused-var warning. * Set generic.codeanalysis.variableanalysis.allowUnusedCaughtExceptions to a true value. */ public $allowUnusedCaughtExceptions = false; /** * Allow function parameters to be unused without provoking unused-var warning. * Set generic.codeanalysis.variableanalysis.allowUnusedFunctionParameters to a true value. */ public $allowUnusedFunctionParameters = false; /** * A list of names of placeholder variables that you want to ignore from * unused variable warnings, ie things like $junk. */ public $validUnusedVariableNames = null; /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { // Magic to modfy $_passByRefFunctions with any site-specific settings. if (!empty($this->sitePassByRefFunctions)) { foreach (preg_split('/\s+/', trim($this->sitePassByRefFunctions)) as $line) { list ($function, $args) = explode(':', $line); $this->_passByRefFunctions[$function] = explode(',', $args); } } if (!empty($this->validUnusedVariableNames)) { $this->validUnusedVariableNames = preg_split('/\s+/', trim($this->validUnusedVariableNames)); } return array( T_VARIABLE, T_DOUBLE_QUOTED_STRING, T_HEREDOC, T_CLOSE_CURLY_BRACKET, T_STRING, ); }//end register() /** * Processes this test, when one of its tokens is encountered. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * * @return void */ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; //if ($token['content'] == '$param') { //echo "Found token on line {$token['line']}.\n" . print_r($token, true); //} if ($this->currentFile !== $phpcsFile) { $this->currentFile = $phpcsFile; } if ($token['code'] === T_VARIABLE) { return $this->processVariable($phpcsFile, $stackPtr); } if (($token['code'] === T_DOUBLE_QUOTED_STRING) || ($token['code'] === T_HEREDOC)) { return $this->processVariableInString($phpcsFile, $stackPtr); } if (($token['code'] === T_STRING) && ($token['content'] === 'compact')) { return $this->processCompact($phpcsFile, $stackPtr); } if (($token['code'] === T_CLOSE_CURLY_BRACKET) && isset($token['scope_condition'])) { return $this->processScopeClose($phpcsFile, $token['scope_condition']); } } function normalizeVarName($varName) { $varName = preg_replace('/[{}$]/', '', $varName); return $varName; } function scopeKey($currScope) { if ($currScope === false) { $currScope = 'file'; } return ($this->currentFile ? $this->currentFile->getFilename() : 'unknown file') . ':' . $currScope; } // Warning: this is an autovivifying get function getScopeInfo($currScope, $autoCreate = true) { $scopeKey = $this->scopeKey($currScope); if (!isset($this->_scopes[$scopeKey])) { if (!$autoCreate) { return null; } $this->_scopes[$scopeKey] = new ScopeInfo($currScope); } return $this->_scopes[$scopeKey]; } function getVariableInfo($varName, $currScope, $autoCreate = true) { $scopeInfo = $this->getScopeInfo($currScope, $autoCreate); if (!isset($scopeInfo->variables[$varName])) { if (!$autoCreate) { return null; } $scopeInfo->variables[$varName] = new VariableInfo($varName); if ($this->validUnusedVariableNames && in_array($varName, $this->validUnusedVariableNames)) { $scopeInfo->variables[$varName]->ignoreUnused = true; } } return $scopeInfo->variables[$varName]; } function markVariableAssignment($varName, $stackPtr, $currScope) { $varInfo = $this->getVariableInfo($varName, $currScope); if (!isset($varInfo->scopeType)) { $varInfo->scopeType = 'local'; } if (isset($varInfo->firstInitialized) && ($varInfo->firstInitialized <= $stackPtr)) { return; } $varInfo->firstInitialized = $stackPtr; } function markVariableDeclaration($varName, $scopeType, $typeHint, $stackPtr, $currScope, $permitMatchingRedeclaration = false) { $varInfo = $this->getVariableInfo($varName, $currScope); if (isset($varInfo->scopeType)) { if (($permitMatchingRedeclaration === false) || ($varInfo->scopeType !== $scopeType)) { // Issue redeclaration/reuse warning // Note: we check off scopeType not firstDeclared, this is so that // we catch declarations that come after implicit declarations like // use of a variable as a local. $this->currentFile->addWarning( "Redeclaration of %s %s as %s.", $stackPtr, 'VariableRedeclaration', array( VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType], "\${$varName}", VariableInfo::$scopeTypeDescriptions[$scopeType], ) ); } } $varInfo->scopeType = $scopeType; if (isset($typeHint)) { $varInfo->typeHint = $typeHint; } if (isset($varInfo->firstDeclared) && ($varInfo->firstDeclared <= $stackPtr)) { return; } $varInfo->firstDeclared = $stackPtr; } function markVariableRead($varName, $stackPtr, $currScope) { $varInfo = $this->getVariableInfo($varName, $currScope); if (isset($varInfo->firstRead) && ($varInfo->firstRead <= $stackPtr)) { return; } $varInfo->firstRead = $stackPtr; } function isVariableInitialized($varName, $stackPtr, $currScope) { $varInfo = $this->getVariableInfo($varName, $currScope); if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) { return true; } return false; } function isVariableUndefined($varName, $stackPtr, $currScope) { $varInfo = $this->getVariableInfo($varName, $currScope, false); if ( $varInfo->ignoreUnused ) { return false; } if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) { // TODO: do we want to check scopeType here? return false; } if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) { return false; } return true; } function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope) { $this->markVariableRead($varName, $stackPtr, $currScope); if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) { // We haven't been defined by this point. $phpcsFile->addWarning("Variable %s is undefined.", $stackPtr, 'UndefinedVariable', array("\${$varName}")); } return true; } function findFunctionPrototype( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) === false) { return false; } // Function names are T_STRING, and return-by-reference is T_BITWISE_AND, // so we look backwards from the opening bracket for the first thing that // isn't a function name, reference sigil or whitespace and check if // it's a function keyword. $functionPtr = $phpcsFile->findPrevious(array(T_STRING, T_WHITESPACE, T_BITWISE_AND), $openPtr - 1, null, true, null, true); if (($functionPtr !== false) && ($tokens[$functionPtr]['code'] === T_FUNCTION)) { return $functionPtr; } return false; } function findVariableScope( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; $in_class = false; if (!empty($token['conditions'])) { foreach (array_reverse($token['conditions'], true) as $scopePtr => $scopeCode) { if (($scopeCode === T_FUNCTION) || ($scopeCode === T_CLOSURE)) { return $scopePtr; } if (($scopeCode === T_CLASS) || ($scopeCode === T_INTERFACE)) { $in_class = true; } } } if (($scopePtr = $this->findFunctionPrototype($phpcsFile, $stackPtr)) !== false) { return $scopePtr; } if ($in_class) { // Member var of a class, we don't care. return false; } // File scope, hmm, lets use first token of file? return 0; } function isNextThingAnAssign( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); // Is the next non-whitespace an assignment? $nextPtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, true); if ($nextPtr !== false) { if ($tokens[$nextPtr]['code'] === T_EQUAL) { return $nextPtr; } } return false; } function findWhereAssignExecuted( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); // Write should be recorded at the next statement to ensure we treat // the assign as happening after the RHS execution. // eg: $var = $var + 1; -> RHS could still be undef. // However, if we're within a bracketed expression, we take place at // the closing bracket, if that's first. // eg: echo (($var = 12) && ($var == 12)); $semicolonPtr = $phpcsFile->findNext(T_SEMICOLON, $stackPtr + 1, null, false, null, true); $closePtr = false; if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) !== false) { if (isset($tokens[$openPtr]['parenthesis_closer'])) { $closePtr = $tokens[$openPtr]['parenthesis_closer']; } } if ($semicolonPtr === false) { if ($closePtr === false) { // TODO: panic return $stackPtr; } return $closePtr; } if ($closePtr < $semicolonPtr) { return $closePtr; } return $semicolonPtr; } function findContainingBrackets( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['nested_parenthesis'])) { $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']); return end($openPtrs); } return false; } function findFunctionCall( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); if ($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) { // First non-whitespace thing and see if it's a T_STRING function name $functionPtr = $phpcsFile->findPrevious(T_WHITESPACE, $openPtr - 1, null, true, null, true); if ($tokens[$functionPtr]['code'] === T_STRING) { return $functionPtr; } } return false; } function findFunctionCallArguments( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); // Slight hack: also allow this to find args for array constructor. // TODO: probably should refactor into three functions: arg-finding and bracket-finding if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) { // Assume $stackPtr is something within the brackets, find our function call if (($stackPtr = $this->findFunctionCall($phpcsFile, $stackPtr)) === false) { return false; } } // $stackPtr is the function name, find our brackets after it $openPtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, true); if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) { return false; } if (!isset($tokens[$openPtr]['parenthesis_closer'])) { return false; } $closePtr = $tokens[$openPtr]['parenthesis_closer']; $argPtrs = array(); $lastPtr = $openPtr; $lastArgComma = $openPtr; while (($nextPtr = $phpcsFile->findNext(T_COMMA, $lastPtr + 1, $closePtr)) !== false) { if ($this->findContainingBrackets($phpcsFile, $nextPtr) == $openPtr) { // Comma is at our level of brackets, it's an argument delimiter. array_push($argPtrs, range($lastArgComma + 1, $nextPtr - 1)); $lastArgComma = $nextPtr; } $lastPtr = $nextPtr; } array_push($argPtrs, range($lastArgComma + 1, $closePtr - 1)); return $argPtrs; } protected function checkForFunctionPrototype( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a function or closure parameter? // It would be nice to get the list of function parameters from watching for // T_FUNCTION, but AbstractVariableSniff and AbstractScopeSniff define everything // we need to do that as private or final, so we have to do it this hackish way. if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) === false) { return false; } // Function names are T_STRING, and return-by-reference is T_BITWISE_AND, // so we look backwards from the opening bracket for the first thing that // isn't a function name, reference sigil or whitespace and check if // it's a function keyword. $functionPtr = $phpcsFile->findPrevious(array(T_STRING, T_WHITESPACE, T_BITWISE_AND), $openPtr - 1, null, true, null, true); if (($functionPtr !== false) && (($tokens[$functionPtr]['code'] === T_FUNCTION) || ($tokens[$functionPtr]['code'] === T_CLOSURE))) { // TODO: typeHint $this->markVariableDeclaration($varName, 'param', null, $stackPtr, $functionPtr); // Are we pass-by-reference? $referencePtr = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true, null, true); if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) { $varInfo = $this->getVariableInfo($varName, $functionPtr); $varInfo->passByReference = true; } // Are we optional with a default? if ($this->isNextThingAnAssign($phpcsFile, $stackPtr) !== false) { $this->markVariableAssignment($varName, $stackPtr, $functionPtr); } return true; } // Is it a use keyword? Use is both a read and a define, fun! if (($functionPtr !== false) && ($tokens[$functionPtr]['code'] === T_USE)) { $this->markVariableRead($varName, $stackPtr, $currScope); if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) { // We haven't been defined by this point. $phpcsFile->addWarning("Variable %s is undefined.", $stackPtr, 'UndefinedVariable', array("\${$varName}")); return true; } // $functionPtr is at the use, we need the function keyword for start of scope. $functionPtr = $phpcsFile->findPrevious(T_CLOSURE, $functionPtr - 1, $currScope + 1, false, null, true); if ($functionPtr !== false) { // TODO: typeHints in use? $this->markVariableDeclaration($varName, 'bound', null, $stackPtr, $functionPtr); $this->markVariableAssignment($varName, $stackPtr, $functionPtr); return true; } } return false; } protected function checkForCatchBlock( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a catch block parameter? if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) === false) { return false; } // Function names are T_STRING, and return-by-reference is T_BITWISE_AND, // so we look backwards from the opening bracket for the first thing that // isn't a function name, reference sigil or whitespace and check if // it's a function keyword. $catchPtr = $phpcsFile->findPrevious(T_WHITESPACE, $openPtr - 1, null, true, null, true); if (($catchPtr !== false) && ($tokens[$catchPtr]['code'] === T_CATCH)) { // Scope of the exception var is actually the function, not just the catch block. // TODO: typeHint $this->markVariableDeclaration($varName, 'local', null, $stackPtr, $currScope, true); $this->markVariableAssignment($varName, $stackPtr, $currScope); if ($this->allowUnusedCaughtExceptions) { $varInfo = $this->getVariableInfo($varName, $currScope); $varInfo->ignoreUnused = true; } return true; } return false; } protected function checkForThisWithinClass( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we $this within a class? if (($varName != 'this') || empty($token['conditions'])) { return false; } foreach (array_reverse($token['conditions'], true) as $scopePtr => $scopeCode) { // $this within a closure is invalid // Note: have to fetch code from $tokens, T_CLOSURE isn't set for conditions codes. if ($tokens[$scopePtr]['code'] === T_CLOSURE) { return false; } if ($scopeCode === T_CLASS) { return true; } } return false; } protected function checkForSuperGlobal( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a superglobal variable? if (in_array($varName, array( 'GLOBALS', '_SERVER', '_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_REQUEST', '_ENV', 'argv', 'argc', ))) { return true; } return false; } protected function checkForStaticMember( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a static member? $doubleColonPtr = $stackPtr - 1; if ($tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) { return false; } $classNamePtr = $stackPtr - 2; if (($tokens[$classNamePtr]['code'] !== T_STRING) && ($tokens[$classNamePtr]['code'] !== T_SELF) && ($tokens[$classNamePtr]['code'] !== T_STATIC)) { return false; } // Are we refering to self:: outside a class? // TODO: not sure this is our business or should be some other sniff. if (($tokens[$classNamePtr]['code'] === T_SELF) || ($tokens[$classNamePtr]['code'] === T_STATIC)) { if ($tokens[$classNamePtr]['code'] === T_SELF) { $err_class = 'SelfOutsideClass'; $err_desc = 'self::'; } else { $err_class = 'StaticOutsideClass'; $err_desc = 'static::'; } if (!empty($token['conditions'])) { foreach (array_reverse($token['conditions'], true) as $scopePtr => $scopeCode) { // self within a closure is invalid // Note: have to fetch code from $tokens, T_CLOSURE isn't set for conditions codes. if ($tokens[$scopePtr]['code'] === T_CLOSURE) { $phpcsFile->addError("Use of {$err_desc}%s inside closure.", $stackPtr, $err_class, array("\${$varName}")); return true; } if ($scopeCode === T_CLASS) { return true; } } } $phpcsFile->addError("Use of {$err_desc}%s outside class definition.", $stackPtr, $err_class, array("\${$varName}")); return true; } return true; } protected function checkForAssignment( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Is the next non-whitespace an assignment? if (($assignPtr = $this->isNextThingAnAssign($phpcsFile, $stackPtr)) === false) { return false; } // Plain ol' assignment. Simpl(ish). if (($writtenPtr = $this->findWhereAssignExecuted($phpcsFile, $assignPtr)) === false) { $writtenPtr = $stackPtr; // I dunno } $this->markVariableAssignment($varName, $writtenPtr, $currScope); return true; } protected function checkForListAssignment( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // OK, are we within a list (...) construct? if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) === false) { return false; } $prevPtr = $phpcsFile->findPrevious(T_WHITESPACE, $openPtr - 1, null, true, null, true); if (($prevPtr === false) || ($tokens[$prevPtr]['code'] !== T_LIST)) { return false; } // OK, we're a list (...) construct... are we being assigned to? $closePtr = $tokens[$openPtr]['parenthesis_closer']; if (($assignPtr = $this->isNextThingAnAssign($phpcsFile, $closePtr)) === false) { return false; } // Yes, we're being assigned. $writtenPtr = $this->findWhereAssignExecuted($phpcsFile, $assignPtr); $this->markVariableAssignment($varName, $writtenPtr, $currScope); return true; } protected function checkForGlobalDeclaration( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a global declaration? // Search backwards for first token that isn't whitespace, comma or variable. $globalPtr = $phpcsFile->findPrevious( array(T_WHITESPACE, T_VARIABLE, T_COMMA), $stackPtr - 1, null, true, null, true); if (($globalPtr === false) || ($tokens[$globalPtr]['code'] !== T_GLOBAL)) { return false; } // It's a global declaration. $this->markVariableDeclaration($varName, 'global', null, $stackPtr, $currScope); return true; } protected function checkForStaticDeclaration( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a static declaration? // Static declarations are a bit more complicated than globals, since they // can contain assignments. The assignment is compile-time however so can // only be constant values, which makes life manageable. // // Just to complicate matters further, late static binding constants // take the form static::CONSTANT and are invalid within static variable // assignments, but we don't want to accidentally match their use of the // static keyword. // // Valid values are: // number T_MINUS T_LNUMBER T_DNUMBER // string T_CONSTANT_ENCAPSED_STRING // heredoc T_START_HEREDOC T_HEREDOC T_END_HEREDOC // nowdoc T_START_NOWDOC T_NOWDOC T_END_NOWDOC // define T_STRING // class constant T_STRING T_DOUBLE_COLON T_STRING // Search backwards for first token that isn't whitespace, comma, variable, // equals, or on the list of assignable constant values above. $staticPtr = $phpcsFile->findPrevious( array( T_WHITESPACE, T_VARIABLE, T_COMMA, T_EQUAL, T_MINUS, T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING, T_STRING, T_DOUBLE_COLON, T_START_HEREDOC, T_HEREDOC, T_END_HEREDOC, T_START_NOWDOC, T_NOWDOC, T_END_NOWDOC, ), $stackPtr - 1, null, true, null, true); if (($staticPtr === false) || ($tokens[$staticPtr]['code'] !== T_STATIC)) { //if ($varName == 'static4') { // echo "Failing token:\n" . print_r($tokens[$staticPtr], true); //} return false; } // Is it a late static binding static::? // If so, this isn't the static keyword we're looking for, but since // static:: isn't allowed in a compile-time constant, we also know // we can't be part of a static declaration anyway, so there's no // need to look any further. $lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true); if (($lateStaticBindingPtr !== false) && ($tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON)) { return false; } // It's a static declaration. $this->markVariableDeclaration($varName, 'static', null, $stackPtr, $currScope); if ($this->isNextThingAnAssign($phpcsFile, $stackPtr) !== false) { $this->markVariableAssignment($varName, $stackPtr, $currScope); } return true; } protected function checkForForeachLoopVar( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a foreach loopvar? if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) === false) { return false; } // Is there an 'as' token between us and the opening bracket? if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openPtr) === false) { return false; } $this->markVariableAssignment($varName, $stackPtr, $currScope); return true; } protected function checkForPassByReferenceFunctionCall( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we pass-by-reference to known pass-by-reference function? if (($functionPtr = $this->findFunctionCall($phpcsFile, $stackPtr)) === false) { return false; } // Is our function a known pass-by-reference function? $functionName = $tokens[$functionPtr]['content']; if (!isset($this->_passByRefFunctions[$functionName])) { return false; } $refArgs = $this->_passByRefFunctions[$functionName]; if (($argPtrs = $this->findFunctionCallArguments($phpcsFile, $stackPtr)) === false) { return false; } // We're within a function call arguments list, find which arg we are. $argPos = false; foreach ($argPtrs as $idx => $ptrs) { if (in_array($stackPtr, $ptrs)) { $argPos = $idx + 1; break; } } if ($argPos === false) { return false; } if (!in_array($argPos, $refArgs)) { // Our arg wasn't mentioned explicitly, are we after an elipsis catch-all? if (($elipsis = array_search('...', $refArgs)) === false) { return false; } if ($argPos < $refArgs[$elipsis - 1]) { return false; } } // Our argument position matches that of a pass-by-ref argument, // check that we're the only part of the argument expression. foreach ($argPtrs[$argPos - 1] as $ptr) { if ($ptr === $stackPtr) { continue; } if ($tokens[$ptr]['code'] !== T_WHITESPACE) { return false; } } // Just us, we can mark it as a write. $this->markVariableAssignment($varName, $stackPtr, $currScope); // It's a read as well for purposes of used-variables. $this->markVariableRead($varName, $stackPtr, $currScope); return true; } protected function checkForSymbolicObjectProperty( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $varName, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // Are we a symbolic object property/function derefeference? // Search backwards for first token that isn't whitespace, is it a "->" operator? $objectOperatorPtr = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true, null, true); if (($objectOperatorPtr === false) || ($tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR)) { return false; } $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); return true; } /** * Called to process class member vars. * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this * token was found. * @param int $stackPtr The position where the token was found. * * @return void */ protected function processMemberVar( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; // TODO: don't care for now } /** * Called to process normal member vars. * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this * token was found. * @param int $stackPtr The position where the token was found. * * @return void */ protected function processVariable( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; $varName = $this->normalizeVarName($token['content']); if (($currScope = $this->findVariableScope($phpcsFile, $stackPtr)) === false) { return; } //static $dump_token = false; //if ($varName == 'property') { // $dump_token = true; //} //if ($dump_token) { // echo "Found variable {$varName} on line {$token['line']} in scope {$currScope}.\n" . print_r($token, true); // echo "Prev:\n" . print_r($tokens[$stackPtr - 1], true); //} // Determine if variable is being assigned or read. // Read methods that preempt assignment: // Are we a $object->$property type symbolic reference? // Possible assignment methods: // Is a mandatory function/closure parameter // Is an optional function/closure parameter with non-null value // Is closure use declaration of a variable defined within containing scope // catch (...) block start // $this within a class (but not within a closure). // $GLOBALS, $_REQUEST, etc superglobals. // $var part of class::$var static member // Assignment via = // Assignment via list (...) = // Declares as a global // Declares as a static // Assignment via foreach (... as ...) { } // Pass-by-reference to known pass-by-reference function // Are we a $object->$property type symbolic reference? if ($this->checkForSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a function or closure parameter? if ($this->checkForFunctionPrototype($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a catch parameter? if ($this->checkForCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we $this within a class? if ($this->checkForThisWithinClass($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a $GLOBALS, $_REQUEST, etc superglobal? if ($this->checkForSuperGlobal($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // $var part of class::$var static member if ($this->checkForStaticMember($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Is the next non-whitespace an assignment? if ($this->checkForAssignment($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // OK, are we within a list (...) = construct? if ($this->checkForListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a global declaration? if ($this->checkForGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a static declaration? if ($this->checkForStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we a foreach loopvar? if ($this->checkForForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // Are we pass-by-reference to known pass-by-reference function? if ($this->checkForPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) { return; } // OK, we don't appear to be a write to the var, assume we're a read. $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); } /** * Called to process variables found in double quoted strings. * * Note that there may be more than one variable in the string, which will * result only in one call for the string. * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this * token was found. * @param int $stackPtr The position where the double quoted * string was found. * * @return void */ protected function processVariableInString( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; if (!preg_match_all($this->_double_quoted_variable_regexp, $token['content'], $matches)) { return; } $currScope = $this->findVariableScope($phpcsFile, $stackPtr); foreach ($matches[1] as $varName) { $varName = $this->normalizeVarName($varName); // Are we $this within a class? if ($this->checkForThisWithinClass($phpcsFile, $stackPtr, $varName, $currScope)) { continue; } if ($this->checkForSuperGlobal($phpcsFile, $stackPtr, $varName, $currScope)) { continue; } $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope); } } protected function processCompactArguments( PHP_CodeSniffer_File $phpcsFile, $stackPtr, $arguments, $currScope ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; foreach ($arguments as $argumentPtrs) { $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) { return $tokens[$argumentPtr]['code'] !== T_WHITESPACE; })); if (empty($argumentPtrs)) { continue; } if (!isset($tokens[$argumentPtrs[0]])) { continue; } $argument_first_token = $tokens[$argumentPtrs[0]]; if ($argument_first_token['code'] === T_ARRAY) { // It's an array argument, recurse. if (($array_arguments = $this->findFunctionCallArguments($phpcsFile, $argumentPtrs[0])) !== false) { $this->processCompactArguments($phpcsFile, $stackPtr, $array_arguments, $currScope); } continue; } if (count($argumentPtrs) > 1) { // Complex argument, we can't handle it, ignore. continue; } if ($argument_first_token['code'] === T_CONSTANT_ENCAPSED_STRING) { // Single-quoted string literal, ie compact('whatever'). // Substr is to strip the enclosing single-quotes. $varName = substr($argument_first_token['content'], 1, -1); $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope); continue; } if ($argument_first_token['code'] === T_DOUBLE_QUOTED_STRING) { // Double-quoted string literal. if (preg_match($this->_double_quoted_variable_regexp, $argument_first_token['content'])) { // Bail if the string needs variable expansion, that's runtime stuff. continue; } // Substr is to strip the enclosing double-quotes. $varName = substr($argument_first_token['content'], 1, -1); $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope); continue; } } } /** * Called to process variables named in a call to compact(). * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this * token was found. * @param int $stackPtr The position where the call to compact() * was found. * * @return void */ protected function processCompact( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; $currScope = $this->findVariableScope($phpcsFile, $stackPtr); if (($arguments = $this->findFunctionCallArguments($phpcsFile, $stackPtr)) !== false) { $this->processCompactArguments($phpcsFile, $stackPtr, $arguments, $currScope); } } /** * Called to process the end of a scope. * * Note that although triggered by the closing curly brace of the scope, $stackPtr is * the scope conditional, not the closing curly brace. * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this * token was found. * @param int $stackPtr The position of the scope conditional. * * @return void */ protected function processScopeClose( PHP_CodeSniffer_File $phpcsFile, $stackPtr ) { $scopeInfo = $this->getScopeInfo($stackPtr, false); if (is_null($scopeInfo)) { return; } foreach ($scopeInfo->variables as $varInfo) { if ($varInfo->ignoreUnused || isset($varInfo->firstRead)) { continue; } if ($this->allowUnusedFunctionParameters && $varInfo->scopeType == 'param') { continue; } if ($varInfo->passByReference && isset($varInfo->firstInitialized)) { // If we're pass-by-reference then it's a common pattern to // use the variable to return data to the caller, so any // assignment also counts as "variable use" for the purposes // of "unused variable" warnings. continue; } if (isset($varInfo->firstDeclared)) { $phpcsFile->addWarning( "Unused %s %s.", $varInfo->firstDeclared, 'UnusedVariable', array( VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType], "\${$varInfo->name}", ) ); } if (isset($varInfo->firstInitialized)) { $phpcsFile->addWarning( "Unused %s %s.", $varInfo->firstInitialized, 'UnusedVariable', array( VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType], "\${$varInfo->name}", ) ); } } } }//end class ?>