<?php
/**
 * This file is part of the VariableAnalysis addon for PHP_CodeSniffer.
 *
 * @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
 */

namespace DrupalPractice\Sniffs\CodeAnalysis;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

/**
 * 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();


    /**
     * Constructor.
     *
     * @param int $currScope
     */
    function __construct($currScope)
    {
        // TODO: extract opener/closer.
        $this->owner = $currScope;

    }//end __construct()


}//end class


/**
 * 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;
    public $lastAssignment;

    static $scopeTypeDescriptions = array(
                                     'local'  => 'variable',
                                     'param'  => 'function parameter',
                                     'static' => 'static variable',
                                     'global' => 'global variable',
                                     'bound'  => 'bound variable',
                                    );


    /**
     * Constructor.
     *
     * @param string $varName
     */
    function __construct($varName)
    {
        $this->name = $varName;

    }//end __construct()


}//end class


/**
 * 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 VariableAnalysisSniff implements 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 = true;

    /**
     *  Allow function parameters to be unused without provoking unused-var warning.
     *  Set generic.codeanalysis.variableanalysis.allowUnusedFunctionParameters to a true value.
     */
    public $allowUnusedFunctionParameters = true;

    /**
     *  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) === false) {
            foreach (preg_split('/\s+/', trim($this->sitePassByRefFunctions)) as $line) {
                list ($function, $args) = explode(':', $line);
                $this->_passByRefFunctions[$function] = explode(',', $args);
            }
        }

        if (empty($this->validUnusedVariableNames) === false) {
            $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\Files\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(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

        // Debug code.
        // if ($token['content'] == '$param') {
        // echo "Found token on line {$token['line']}.\n" . print_r($token, true);
        // }
        // End: Debug code.
        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']) === true
        ) {
            return $this->processScopeClose($phpcsFile, $token['scope_condition']);
        }

    }//end process()


    /**
     * Remove special characters from the variable name.
     *
     * @param string $varName
     *
     * @return string
     */
    function normalizeVarName($varName)
    {
        $varName = preg_replace('/[{}$]/', '', $varName);
        return $varName;

    }//end normalizeVarName()


    /**
     * Generate a scope key based on the current file.
     *
     * @param string $currScope
     *
     * @return string
     */
    function scopeKey($currScope)
    {
        if ($currScope === false) {
            $currScope = 'file';
        }

        if (isset($this->currentFile) === true) {
            return ($this->currentFile->getFilename()).':'.$currScope;
        } else {
            return ('unknown file').':'.$currScope;
        }

    }//end scopeKey()


    /**
     * Warning: this is an autovivifying get.
     *
     * @param string|false $currScope
     * @param bool         $autoCreate
     *
     * @return ScopeInfo
     */
    function getScopeInfo($currScope, $autoCreate = true)
    {
        $scopeKey = $this->scopeKey($currScope);
        if (isset($this->_scopes[$scopeKey]) === false) {
            if ($autoCreate === false) {
                return null;
            }

            $this->_scopes[$scopeKey] = new ScopeInfo($currScope);
        }

        return $this->_scopes[$scopeKey];

    }//end getScopeInfo()


    /**
     * Get variable information for a given variable name.
     *
     * @param string $varName
     *   Name of the variable.
     * @param int    $currScope
     *   Token stack pointer of the current scope.
     * @param bool   $autoCreate
     *   TRUE if the variable should be auto created.
     *
     * @return VariableInfo|null
     *   Information about the variable.
     */
    function getVariableInfo($varName, $currScope, $autoCreate = true)
    {
        $scopeInfo = $this->getScopeInfo($currScope, $autoCreate);
        if (isset($scopeInfo->variables[$varName]) === false) {
            if ($autoCreate === false) {
                return null;
            }

            $scopeInfo->variables[$varName] = new VariableInfo($varName);
            if (is_array($this->validUnusedVariableNames) === true
                && in_array($varName, $this->validUnusedVariableNames) === true
            ) {
                $scopeInfo->variables[$varName]->ignoreUnused = true;
            }
        }

        return $scopeInfo->variables[$varName];

    }//end getVariableInfo()


    /**
     * Mark the given variable as being assigned.
     *
     * @param string $varName
     * @param int    $stackPtr
     * @param string $currScope
     *
     * @return void
     */
    function markVariableAssignment($varName, $stackPtr, $currScope)
    {
        $varInfo = $this->getVariableInfo($varName, $currScope);
        if (isset($varInfo->scopeType) === false) {
            $varInfo->scopeType = 'local';
        }

        if (isset($varInfo->firstInitialized) === true && ($varInfo->firstInitialized <= $stackPtr)) {
            $varInfo->lastAssignment = $stackPtr;
            return;
        }

        $varInfo->firstInitialized = $stackPtr;

    }//end markVariableAssignment()


    /**
     * Mark the given variable as being declared.
     *
     * @param string $varName
     * @param string $scopeType
     * @param string $typeHint
     * @param int    $stackPtr
     * @param string $currScope
     * @param bool   $permitMatchingRedeclaration
     *
     * @return void
     */
    function markVariableDeclaration($varName, $scopeType, $typeHint, $stackPtr, $currScope, $permitMatchingRedeclaration = false)
    {
        $varInfo = $this->getVariableInfo($varName, $currScope);
        if (isset($varInfo->scopeType) === true) {
            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) === true) {
            $varInfo->typeHint = $typeHint;
        }

        if (isset($varInfo->firstDeclared) === true && ($varInfo->firstDeclared <= $stackPtr)) {
            return;
        }

        // When a global variable is declared it also means we can consider it as
        // being initialized.
        if ($scopeType === 'global') {
            $varInfo->firstInitialized = $stackPtr;
        }

        $varInfo->firstDeclared = $stackPtr;

    }//end markVariableDeclaration()


    /**
     * Mark the given variable as being read.
     *
     * @param string $varName
     * @param int    $stackPtr
     * @param string $currScope
     *
     * @return void
     */
    function markVariableRead($varName, $stackPtr, $currScope)
    {
        $varInfo = $this->getVariableInfo($varName, $currScope);
        if (isset($varInfo->firstRead) === true && ($varInfo->firstRead <= $stackPtr)) {
            return;
        }

        $varInfo->firstRead = $stackPtr;

    }//end markVariableRead()


    /**
     * Checks if a variable has been initialized.
     *
     * @param string $varName
     * @param int    $stackPtr
     * @param string $currScope
     *
     * @return bool
     */
    function isVariableInitialized($varName, $stackPtr, $currScope)
    {
        $varInfo = $this->getVariableInfo($varName, $currScope);
        if (isset($varInfo->firstInitialized) === true && $varInfo->firstInitialized <= $stackPtr) {
            return true;
        }

        return false;

    }//end isVariableInitialized()


    /**
     * Checks if the given variable is undefined.
     *
     * @param string $varName
     * @param int    $stackPtr
     * @param string $currScope
     *
     * @return bool
     */
    function isVariableUndefined($varName, $stackPtr, $currScope)
    {
        $varInfo = $this->getVariableInfo($varName, $currScope, false);
        if (isset($varInfo->firstDeclared) === true && $varInfo->firstDeclared <= $stackPtr) {
            // TODO: do we want to check scopeType here?
            return false;
        }

        if (isset($varInfo->firstInitialized) === true && $varInfo->firstInitialized <= $stackPtr) {
            return false;
        }

        return true;

    }//end isVariableUndefined()


    /**
     * Marks a variable as read and throws a PHPCS warning if it is undefined.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param string                      $varName
     * @param int                         $stackPtr
     * @param string                      $currScope
     *
     * @return bool
     */
    function markVariableReadAndWarnIfUndefined(File $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;

    }//end markVariableReadAndWarnIfUndefined()


    /**
     * Returns the function declaration pointer.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return int|false
     */
    function findFunctionPrototype(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        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;

    }//end findFunctionPrototype()


    /**
     * Find the scope the given pointer is in.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return int|false
     */
    function findVariableScope(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

        $in_class = false;
        if (empty($token['conditions']) === false) {
            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) || ($scopeCode === T_TRAIT)) {
                    $in_class = true;
                }
            }
        }

        if (($scopePtr = $this->findFunctionPrototype($phpcsFile, $stackPtr)) !== false) {
            return $scopePtr;
        }

        if ($in_class === true) {
            // Member var of a class, we don't care.
            return false;
        }

        // File scope, hmm, lets use first token of file?
        return 0;

    }//end findVariableScope()


    /**
     * Checks if the next token is an assignment.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return bool
     */
    function isNextThingAnAssign(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;
            }

            // Special handling for initializing arrays on the fly, which is
            // also an assignment.
            if ($tokens[$nextPtr]['code'] === T_OPEN_SQUARE_BRACKET) {
                return $this->isNextThingAnAssign($phpcsFile, $tokens[$nextPtr]['bracket_closer']);
            }
        }

        return false;

    }//end isNextThingAnAssign()


    /**
     * Find the end of the assignment.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return int
     */
    function findWhereAssignExecuted(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,
        // eg: echo (($var = 12) && ($var == 12));
        // we take place at the closing bracket, if that's first.
        $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']) === true) {
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
            }
        }

        if ($semicolonPtr === false) {
            if ($closePtr === false) {
                // TODO: panic.
                return $stackPtr;
            }

            return $closePtr;
        }

        if ($closePtr !== false && $closePtr < $semicolonPtr) {
            return $closePtr;
        }

        return $semicolonPtr;

    }//end findWhereAssignExecuted()


    /**
     * Find the parenthesis if the pointer is in some.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return int|false
     */
    function findContainingBrackets(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
            $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
            return end($openPtrs);
        }

        return false;

    }//end findContainingBrackets()


    /**
     * Checks if the given pointer is in a function call.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return int|false
     */
    function findFunctionCall(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        if (($openPtr = $this->findContainingBrackets($phpcsFile, $stackPtr)) !== false) {
            // 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;

    }//end findFunctionCall()


    /**
     * Get the arguments of a function call.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     *
     * @return array|false
     */
    function findFunctionCallArguments(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']) === false) {
            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;

    }//end findFunctionCallArguments()


    /**
     * Checks the function prototype.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForFunctionPrototype(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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;
        }//end if

        // 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);

                // 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;
                }

                return true;
            }//end if
        }//end if

        return false;

    }//end checkForFunctionPrototype()


    /**
     * Checks if we are in a catch() block.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForCatchBlock(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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 !== false) {
                $varInfo = $this->getVariableInfo($varName, $currScope);
                $varInfo->ignoreUnused = true;
            }

            return true;
        }

        return false;

    }//end checkForCatchBlock()


    /**
     * Checks if $this is used within a class.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForThisWithinClass(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

        // Are we $this within a class?
        if (($varName !== 'this') || empty($token['conditions']) === true) {
            return false;
        }

        foreach (array_reverse($token['conditions'], true) as $scopePtr => $scopeCode) {
            if ($scopeCode === T_CLASS || $scopeCode === T_TRAIT) {
                return true;
            }
        }

        return false;

    }//end checkForThisWithinClass()


    /**
     * Checks if the variable is a PHP super global.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForSuperGlobal(
        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',
            )
        ) === true
        ) {
            return true;
        }

        return false;

    }//end checkForSuperGlobal()


    /**
     * Checks if the variable is a static class member.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForStaticMember(
        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 referring 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']) === false) {
                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 || $scopeCode === T_TRAIT) {
                        return true;
                    }
                }
            }

            $phpcsFile->addError(
                "Use of {$err_desc}%s outside class definition.",
                $stackPtr,
                $err_class,
                array("\${$varName}")
            );
            return true;
        }//end if

        return true;

    }//end checkForStaticMember()


    /**
     * Checks if the variable is being assigned to.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForAssignment(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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.
        }

        // Check for the ampersand '&' after the assignment, which means this
        // variable is taken by reference.
        $refPtr = $phpcsFile->findNext(T_WHITESPACE, ($assignPtr + 1), null, true);
        if ($tokens[$refPtr]['code'] === T_BITWISE_AND) {
            $varInfo = $this->getVariableInfo($varName, $currScope);
            $varInfo->passByReference = true;
        }

        $this->markVariableAssignment($varName, $writtenPtr, $currScope);
        return true;

    }//end checkForAssignment()


    /**
     * Check if this is a list language construct assignment.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForListAssignment(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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;

    }//end checkForListAssignment()


    /**
     * Check if this variable is declared globally.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForGlobalDeclaration(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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);
        // Also mark this variable as being a reference, so that we don't get
        // unused variable warnings if it is never read.
        $varInfo = $this->getVariableInfo($varName, $currScope);
        $varInfo->passByReference = true;
        return true;

    }//end checkForGlobalDeclaration()


    /**
     * Check is this is a static variable declaration.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForStaticDeclaration(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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)) {
            // Debug code.
            // if ($varName == 'static4') {
            // echo "Failing token:\n" . print_r($tokens[$staticPtr], true);
            // }
            // End: Debug code.
            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;

    }//end checkForStaticDeclaration()


    /**
     * Check if this is a foreach loop variable.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForForeachLoopVar(
        File $phpcsFile,
        $stackPtr,
        $varName,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        // 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);

        // Workaround: We want to allow foreach ($array as $key => $value) where
        // $value is never read, so we just mark it read immediately here.
        if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, ($stackPtr - 1), $openPtr) !== false) {
            $this->markVariableRead($varName, $stackPtr, $currScope);
        }

        // Foreach variables that are read as references like
        // foreach ($array as &$value) should not throw unused variable errors.
        if (($refPtr = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true)) !== false
            && $tokens[$refPtr]['code'] === T_BITWISE_AND
        ) {
            $varInfo = $this->getVariableInfo($varName, $currScope);
            $varInfo->passByReference = true;
        }

        return true;

    }//end checkForForeachLoopVar()


    /**
     * Check if this is a "&" function call.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForPassByReferenceFunctionCall(
        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]) === false) {
            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) === true) {
                $argPos = ($idx + 1);
                break;
            }
        }

        if ($argPos === false) {
            return false;
        }

        if (in_array($argPos, $refArgs) === false) {
            // 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;

    }//end checkForPassByReferenceFunctionCall()


    /**
     * Check if the variable is an object property.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param string                      $varName
     * @param string                      $currScope
     *
     * @return bool
     */
    protected function checkForSymbolicObjectProperty(
        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;

    }//end checkForSymbolicObjectProperty()


    /**
     * Called to process class member vars.
     *
     * @param \PHP_CodeSniffer\Files\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(
        File $phpcsFile,
        $stackPtr
    ) {
        // TODO: don't care for now.
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

    }//end processMemberVar()


    /**
     * Called to process normal member vars.
     *
     * @param \PHP_CodeSniffer\Files\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(
        File $phpcsFile,
        $stackPtr
    ) {
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

        $varName = $this->normalizeVarName($token['content']);
        if (($currScope = $this->findVariableScope($phpcsFile, $stackPtr)) === false) {
            return;
        }

        // Debug code.
        // 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) === true) {
            return;
        }

        // Are we a function or closure parameter?
        if ($this->checkForFunctionPrototype($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we a catch parameter?
        if ($this->checkForCatchBlock($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we $this within a class?
        if ($this->checkForThisWithinClass($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we a $GLOBALS, $_REQUEST, etc superglobal?
        if ($this->checkForSuperGlobal($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // $var part of class::$var static member
        if ($this->checkForStaticMember($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Is the next non-whitespace an assignment?
        if ($this->checkForAssignment($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // OK, are we within a list (...) = construct?
        if ($this->checkForListAssignment($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we a global declaration?
        if ($this->checkForGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we a static declaration?
        if ($this->checkForStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we a foreach loopvar?
        if ($this->checkForForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // Are we pass-by-reference to known pass-by-reference function?
        if ($this->checkForPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope) === true) {
            return;
        }

        // OK, we don't appear to be a write to the var, assume we're a read.
        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);

    }//end processVariable()


    /**
     * 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\Files\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(
        File
        $phpcsFile,
        $stackPtr
    ) {
        $tokens = $phpcsFile->getTokens();
        $token  = $tokens[$stackPtr];

        $runMatch = preg_match_all($this->_double_quoted_variable_regexp, $token['content'], $matches);
        if ($runMatch === 0 || $runMatch === false) {
            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) === true) {
                continue;
            }

            if ($this->checkForSuperGlobal($phpcsFile, $stackPtr, $varName, $currScope) === true) {
                continue;
            }

            $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
        }

    }//end processVariableInString()


    /**
     * Check variables in a compact() call.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile
     * @param int                         $stackPtr
     * @param array                       $arguments
     * @param string                      $currScope
     *
     * @return void
     */
    protected function processCompactArguments(
        File
        $phpcsFile,
        $stackPtr,
        $arguments,
        $currScope
    ) {
        $tokens = $phpcsFile->getTokens();

        foreach ($arguments as $argumentPtrs) {
            $argumentPtrs = array_values(
                array_filter(
                    $argumentPtrs,
                    function ($argumentPtr) use ($tokens) {
                        return $tokens[$argumentPtr]['code'] !== T_WHITESPACE;
                    }
                )
            );
            if (empty($argumentPtrs) === true) {
                continue;
            }

            if (isset($tokens[$argumentPtrs[0]]) === false) {
                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']) === 1) {
                    // 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;
            }
        }//end foreach

    }//end processCompactArguments()


    /**
     * Called to process variables named in a call to compact().
     *
     * @param \PHP_CodeSniffer\Files\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(
        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);
        }

    }//end processCompact()


    /**
     * 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\Files\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(
        File
        $phpcsFile,
        $stackPtr
    ) {
        $scopeInfo = $this->getScopeInfo($stackPtr, false);
        if (is_null($scopeInfo) === true) {
            return;
        }

        foreach ($scopeInfo->variables as $varInfo) {
            if (($varInfo->ignoreUnused === true) || (isset($varInfo->firstRead) === true)) {
                continue;
            }

            if (($this->allowUnusedFunctionParameters === true) && ($varInfo->scopeType === 'param')) {
                continue;
            }

            if (($varInfo->passByReference === true) && isset($varInfo->lastAssignment) === true) {
                // 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) === true) {
                $phpcsFile->addWarning(
                    "Unused %s %s.",
                    $varInfo->firstDeclared,
                    'UnusedVariable',
                    array(
                     VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
                     "\${$varInfo->name}",
                    )
                );
            } else if (isset($varInfo->firstInitialized) === true) {
                $phpcsFile->addWarning(
                    "Unused %s %s.",
                    $varInfo->firstInitialized,
                    'UnusedVariable',
                    array(
                     VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
                     "\${$varInfo->name}",
                    )
                );
            }//end if
        }//end foreach

    }//end processScopeClose()


}//end class
