TimeTrex/unit_tests/TTCodeStandard/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

1501 lines
54 KiB
PHP
Raw Permalink Normal View History

2022-12-13 07:10:06 +01:00
<?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
?>