1501 lines
54 KiB
PHP
1501 lines
54 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of the VariableAnalysis addon for PHP_CodeSniffer.
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* @category PHP
|
|
* @package PHP_CodeSniffer
|
|
* @author Sam Graham <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @copyright 2011-2012 Sam Graham <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @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 <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @copyright 2011-2012 Sam Graham <php-codesniffer-plugins BLAHBLAH illusori.co.uk>
|
|
* @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 <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @copyright 2011 Sam Graham <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @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 <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @copyright 2011 Sam Graham <php-codesniffer-variableanalysis BLAHBLAH illusori.co.uk>
|
|
* @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 = '|(?<!\\\\)(?:\\\\{2})*\${?([a-zA-Z0-9_]+)}?|';
|
|
|
|
/**
|
|
* Array of known pass-by-reference functions and the argument(s) which are passed
|
|
* by reference, the arguments are numbered starting from 1 and an elipsis '...'
|
|
* means all argument numbers after the previous should be considered pass-by-reference.
|
|
*/
|
|
private $_passByRefFunctions = array(
|
|
'__soapCall' => 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
|
|
|
|
?>
|