<?php
/**
 * CodeIgniter_Sniffs_NamingConventions_ValidVariableNameSniff.
 *
 * PHP version 5
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Thomas Ernest <thomas.ernest@baoabz.com>
 * @copyright 2010 Thomas Ernest
 * @license   http://thomas.ernest.fr/developement/php_cs/licence GNU General Public License
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
/**
 * CodeIgniter_Sniffs_NamingConventions_ValidVariableNameSniff.
 *
 * Ensures that variable names contain only lowercase letters,
 * use underscore separators.
 * Ensures that class attribute names are prefixed with an underscore,
 * only when they are private.
 * Ensure that variable names are longer than 3 chars except those declared
 * in for loops.
 *
 * @todo Try to avoid overly long and verbose names in using property rule and
 * configuration variable to set limits. Have a look at
 * CodeIgniter_Sniffs_NamingConventions_ValidMethodNameSniff.
 * @todo Use a property rule or a configuration variable to allow users to set
 * minimum variable name length. Have a look at
 * CodeIgniter_Sniffs_Files_ClosingLocationCommentSniff and application root.
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Thomas Ernest <thomas.ernest@baoabz.com>
 * @copyright 2010 Thomas Ernest
 * @license   http://thomas.ernest.fr/developement/php_cs/licence GNU General Public License
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
namespace CodeIgniter\Sniffs\NamingConventions;

use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
use PHP_CodeSniffer\Files\File;

class ValidVariableNameSniff extends AbstractVariableSniff
{


    /**
     * Processes class member variables.
     *
     * @param File $phpcsFile The file being scanned.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     *
     * @return void
     */
    protected function processMemberVar(File $phpcsFile, $stackPtr)
    {
        // get variable name and properties
        $tokens = $phpcsFile->getTokens();
        $varTk = $tokens[$stackPtr];
        $varName = substr($varTk['content'], 1);
        $varProps = $phpcsFile->getMemberProperties($stackPtr);
        // check(s)
        if ( ! $this->checkLowerCase($phpcsFile, $stackPtr, $varName) ) {
            return;
        }
        if ( ! $this->checkVisibilityPrefix($phpcsFile, $stackPtr, $varName, $varProps)) {
            return;
        }
        if ( ! $this->checkLength($phpcsFile, $stackPtr, $varName)) {
            return;
        }

    }//end processMemberVar()


    /**
     * Processes normal variables.
     *
     * @param File $phpcsFile The file where this token was found.
     * @param int                  $stackPtr  The position where the token was found.
     *
     * @return void
     */
    protected function processVariable(File $phpcsFile, $stackPtr)
    {
        // get variable name
        $tokens = $phpcsFile->getTokens();
        $varTk = $tokens[$stackPtr];
        $varName = substr($varTk['content'], 1);
        // skip the current object variable, i.e. $this
        if (0 === strcmp($varName, 'this')) {
            return;
        }
        // check(s)
        if ( ! $this->checkLowerCase($phpcsFile, $stackPtr, $varName)) {
            return;
        }
        if ( ! $this->checkLength($phpcsFile, $stackPtr, $varName)) {
            return;
        }

    }//end processVariable()


    /**
     * Processes variables in double quoted strings.
     *
     * @param File $phpcsFile The file where this token was found.
     * @param int                  $stackPtr  The position where the token was found.
     *
     * @return void
     */
    protected function processVariableInString(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $stringTk = $tokens[$stackPtr];
        $stringString = $stringTk['content'];
        $varAt = self::_getVariablePosition($stringString, 0);
        while (false !== $varAt) {
            // get variable name
            $matches = array();
            preg_match('/^\$\{?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}?/', substr($stringString, $varAt), $matches);
            $varName = $matches[1];
            // check(s)
            if ( ! $this->checkLowerCase($phpcsFile, $stackPtr, $varName)) {
                return;
            }
            if ( ! $this->checkLength($phpcsFile, $stackPtr, $varName)) {
                return;
            }
            // prepare checking next variable
            $varAt = self::_getVariablePosition($stringString, $varAt + 1);
        }

    }//end processVariableInString()


    /**
     * Checks that the variable name is all in lower case, else it add an error
     * to $phpcsFile. Returns true if variable name is all in lower case, false
     * otherwise.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     * @param string               $varName   The name of the variable to
     *                                        procced without $, { nor }.
     *
     * @return bool true if variable name is all in lower case, false otherwise.
     */
    protected function checkLowerCase(File $phpcsFile, $stackPtr, $varName)
    {
        $isInLowerCase = true;
        if (0 !== strcmp($varName, strtolower($varName))) {
            // get the expected variable name
            $varNameWithUnderscores = preg_replace('/([A-Z])/', '_${1}', $varName);
            $expectedVarName = strtolower(ltrim($varNameWithUnderscores, '_'));
            // adapts the error message to the error case
            if (strlen($varNameWithUnderscores) > strlen($varName)) {
                $error = 'Variables should not use CamelCasing or start with a Capital.';
            } else {
                $error = 'Variables should be entirely lowercased.';
            }
            $error = $error . 'Please consider "' . $expectedVarName
                . '" instead of "' . $varName . '".';
            // adds the error and changes return value
            $phpcsFile->addError($error, $stackPtr);
            $isInLowerCase = false;
        }
        return $isInLowerCase;
    }//end checkLowerCase()

    /**
     * Checks that an underscore is used at the beginning of a variable only if
     * it is about a private variable. If it isn't a private variable, then it
     * must not be prefixed with an underscore. Returns true if $varName is
     * properly prefixed according to the variable visibility provided in
     * $varProps, false otherwise.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     * @param string               $varName   The name of the variable to
     *                                        procced without $, { nor }.
     * @param array                $varProps  Member variable properties like
     *                                        its visibility.
     *
     * @return bool true if variable name is prefixed with an underscore only
     * when it is about a private variable, false otherwise.
     */
    protected function checkVisibilityPrefix(File $phpcsFile, $stackPtr, $varName, $varProps)
    {
        $isVisibilityPrefixRight = true;
        $scope = $varProps['scope'];
        // If it's a private variable, it must have an underscore on the front.
        if ($scope === 'private' && $varName{0} !== '_') {
            $error = "Private variable name \"$varName\" must be prefixed with an underscore";
            $phpcsFile->addError($error, $stackPtr);
            $isVisibilityPrefixRight = false;
        } else if ($scope !== 'private' && $varName{0} === '_') {
            // If it's not a private variable,
            // then it must not start with an underscore.
            if (isset ($scopeSpecified) && true === $scopeSpecified) {
                $error = "Public variable name \"$varName\" must not be prefixed with an underscore";
            } else {
                $error = ucfirst($scope) . " variable name \"$varName\" must not be prefixed with an underscore";
            }
            $phpcsFile->addError($error, $stackPtr);
            $isVisibilityPrefixRight = false;
        }
        return $isVisibilityPrefixRight;
    }//end checkVisibilityPrefix()

    /**
     * Checks that variable name length is not too short. Returns true, if it
     * meets minimum length requirement, false otherwise.
     *
     * A variable name is too short if it is shorter than the minimal
     * length and it isn't in the list of allowed short names nor declared in a
     * for loop (in which it would be nested).
     * The minimal length is defined in the function. It is 3 chars now.
     * The list of allowed short names is defined in the function.
     * It is case-sensitive. It contains only 'ci' now.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     * @param string               $varName   The name of the variable to
     *                                        procced without $, { nor }.
     *
     * @return bool false if variable name $varName is shorter than the minimal
     * length and it isn't in the list of allowed short names nor declared in a
     * for loop (in which it would be nested), otherwise true.
     */
    protected function checkLength(File $phpcsFile, $stackPtr, $varName)
    {
        $minLength = 3;
        $allowedShortName = array('ci');

        $isLengthRight = true;
        // cleans variable name
        $varName = ltrim($varName, '_');
        if (strlen($varName) <= $minLength) {
            // skips adding an error, if it is a specific variable name
            if (in_array($varName, $allowedShortName)) {
                return $isLengthRight;
            }
            // skips adding an error, if the variable is in a for loop
            if (false !== self::_isInForLoop($phpcsFile, $stackPtr, $varName)) {
                return $isLengthRight;
            }
            // adds the error message finally
            $error = 'Very short'
                . (
                    $minLength > 0 ?
                    ' (i.e. less than ' . ($minLength + 1) . ' chars)'
                    : ''
                )
                . ', non-word variables like "' . $varName
                . '" should only be used as iterators in for() loops.';
            $phpcsFile->addError($error, $stackPtr);
            $isLengthRight = false;
        }
        return $isLengthRight;
    }//end checkLength()

    /**
     * Returns the position of closest previous T_FOR, if token associated with
     * $stackPtr in $phpcsFile is in a for loop, otherwise false.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     * @param string               $varName   The name of the variable to
     *                                        procced without $, { nor }.
     *
     * @return int|bool Position of T_FOR if token associated with $stackPtr in
     *                  $phpcsFile is in the head of a for loop, otherwise false.
     */
    private static function _isInForLoop(File $phpcsFile, $stackPtr, $varName)
    {
        $keepLookingFromPtr = $stackPtr;
        while (false !== $keepLookingFromPtr) {
            // looks if it is in (head or body) of a for loop
            $forPtr = self::_isInForLoopHead($phpcsFile, $keepLookingFromPtr);
            if (false === $forPtr) {
                $forPtr = self::_isInForLoopBody($phpcsFile, $keepLookingFromPtr);
            }
            // checks if it is declared in here and prepares next step
            if (false !== $forPtr) {
                if (false !== self::_isDeclaredInForLoop($phpcsFile, $forPtr, $varName)) {
                    return $forPtr;
                }
                $keepLookingFromPtr = $forPtr;
            } else {
                $keepLookingFromPtr = false;
            }
        }
        return false;
    }//end _isInForLoop()

    /**
     * Returns the position of closest previous T_FOR, if token associated with
     * $stackPtr in $phpcsFile is in the head of a for loop, otherwise false.
     * The head is the code placed between parenthesis next to the key word
     * 'for' : for (<loop_head>) {<loop_body>}.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     *
     * @return int|bool Position of T_FOR if token associated with $stackPtr in
     *                  $phpcsFile is in the head of a for loop, otherwise false.
     */
    private static function _isInForLoopHead(File $phpcsFile, $stackPtr)
    {
        $isInForLoop = false;
        $tokens = $phpcsFile->getTokens();
        $currentTk = $tokens[$stackPtr];
        if (array_key_exists('nested_parenthesis', $currentTk)) {
            $nestedParenthesis = $currentTk['nested_parenthesis'];
            foreach ( $nestedParenthesis as $openParPtr => $closeParPtr) {
                $nonWhitspacePtr = $phpcsFile->findPrevious(
                    array(T_WHITESPACE),
                    $openParPtr - 1,
                    null,
                    true,
                    null,
                    true
                );
                if (false !== $nonWhitspacePtr) {
                    $isFor = T_FOR === $tokens[$nonWhitspacePtr]['code'];
                    if ($isFor) {
                        $isInForLoop = $nonWhitspacePtr;
                        break;
                    }
                }
            }
        }
        return $isInForLoop;
    }//end _isInForLoopHead()

    /**
     * Returns the position of closest previous T_FOR, if token associated with
     * $stackPtr in $phpcsFile is in the body of a for loop, otherwise false.
     * The body are the instructions placed after parenthesis of a 'for'
     * declaration, enclosed with curly brackets usually.
     * 'for' : for (<loop_head>) {<loop_body>}.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     *
     * @return int|bool Position of T_FOR if token associated with $stackPtr in
     *                  $phpcsFile is in the body of a for loop, otherwise false.
     */
    private static function _isInForLoopBody(File $phpcsFile, $stackPtr)
    {
        $isInForLoop = false;
        $tokens = $phpcsFile->getTokens();
        // get englobing hierarchy
        $parentPtrAndCode = $tokens[$stackPtr]['conditions'];
        krsort($parentPtrAndCode);

        // looks for a for loop having a body not enclosed with curly brackets,
        // which involves that its body contains only one instruction.
        if (is_array($parentPtrAndCode) && ! empty($parentPtrAndCode)) {
            $parentCode = reset($parentPtrAndCode);
            $parentPtr = key($parentPtrAndCode);
            $openBracketPtr = $tokens[$parentPtr]['scope_opener'];
        } else {
            $parentCode = 0;
            $parentPtr = 0;
            $openBracketPtr = 0;
        }
        $openResearchScopePtr = $stackPtr;
        // recursive search, since a for statement may englobe other inline
        // control statement or may be near to function calls, etc...
        while (false !== $openResearchScopePtr) {
            $closeParPtr = $phpcsFile->findPrevious(
                array(T_CLOSE_PARENTHESIS),
                $openResearchScopePtr,
                null,
                false,
                null,
                true
            );
            // is there a closing parenthesis with a control statement before
            // the previous instruction ?
            if (false !== $closeParPtr) {
                // is there no opening curly bracket specific to
                // set of instructions, between the closing parenthesis
                // and the current token ?
                if ($openBracketPtr < $closeParPtr) {
                    // starts the search from the token before the closing
                    // parenthesis, if it isn't a for statement
                    $openResearchScopePtr = $closeParPtr - 1;
                    // is this parenthesis associated with a for statement ?
                    $closeParenthesisTk = $tokens[$closeParPtr];
                    if (array_key_exists('parenthesis_owner', $closeParenthesisTk)) {
                        $mayBeForPtr = $closeParenthesisTk['parenthesis_owner'];
                        $mayBeForTk = $tokens[$mayBeForPtr];
                        if (T_FOR === $mayBeForTk['code']) {
                            return $mayBeForPtr;
                        }
                    }
                } else {
                    // if it is about a for loop, don't go further
                    // and detect it after one more loop execution, do it now
                    if (T_FOR === $parentCode) {
                        return $parentPtr;
                    }
                    // starts the search from the token before the one
                    // englobing the current statement
                    $openResearchScopePtr = $parentPtr - 1;
                    // re-initialize variables about the englobing structure
                    if (is_array($parentPtrAndCode)) {
                        $parentCode = next($parentPtrAndCode);
                        $parentPtr = key($parentPtrAndCode);
                        $openBracketPtr = $tokens[$parentPtr]['scope_opener'];
                    }
                }
            } else {
                $openResearchScopePtr = false;
            }
        }
        // looks for a for loop having a body enclosed with curly brackets
        foreach ($parentPtrAndCode as $parentPtr => $parentCode) {
            if (T_FOR === $parentCode) {
                return $parentPtr;
            }
        }
        return false;
    }//end _isInForLoopBody()

    /**
     * Returns true if a variable declared in the head of the for loop pointed
     * by $forPtr in file $phpcsFile has the name $varName.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $forPtr    The position of the 'for' token
     *                                        in the stack passed in $tokens.
     * @param string               $varName   The name of the variable to
     *                                        procced without $, { nor }.
     *
     * @return int|bool true if a variable declared in the head of the for loop
     * pointed by $forPtr in file $phpcsFile has the name $varName.
     */
    private static function _isDeclaredInForLoop(File $phpcsFile, $forPtr, $varName)
    {
        $isDeclaredInFor = false;
        $tokens = $phpcsFile->getTokens();
        $forVarPtrs = self::_getVarDeclaredInFor($phpcsFile, $forPtr);
        foreach ($forVarPtrs as $forVarPtr) {
            $forVarTk = $tokens[$forVarPtr];
            // get variable name
            $matches = array();
            preg_match('/^\$\{?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}?/', $forVarTk['content'], $matches);
            $forVarName = $matches[1];
            if (0 === strcmp($forVarName, $varName)) {
                $isDeclaredInFor = $forVarPtr;
                break;
            }
        }
        return $isDeclaredInFor;
    }//end _isDeclaredInForLoop()

    /**
     * Returns list of pointers to variables declared in for loop associated to
     * $forPtr in file $phpcsFile.
     *
     * All pointers in the result list are pointing to token with code
     * T_VARIABLE. An exception is raised, if $forPtr doesn't point a token with
     * code T_FOR.
     *
     * @param File $phpcsFile The current file being processed.
     * @param int                  $forPtr    The position of the current token
     *                                        in the stack passed in $tokens.
     *
     * @return array List of pointers to variables declared in for loop $forPtr.
     */
    private static function _getVarDeclaredInFor(File $phpcsFile, $forPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $forTk = $tokens[$forPtr];
        if (T_FOR !== $forTk['code']) {
            throw new PHP_CodeSniffer_Exception('$forPtr must be of type T_FOR');
        }
        $openParPtr = $forTk['parenthesis_opener'];
        $openParenthesisTk = $tokens[$openParPtr];
        $endOfDeclPtr = $phpcsFile->findNext(array(T_SEMICOLON), $openParPtr);
        $forVarPtrs = array();
        $varPtr = $phpcsFile->findNext(
            array(T_VARIABLE),
            $openParPtr + 1,
            $endOfDeclPtr
        );
        while (false !== $varPtr) {
            $forVarPtrs [] = $varPtr;
            $varPtr = $phpcsFile->findNext(
                array(T_VARIABLE),
                $varPtr + 1,
                $endOfDeclPtr
            );
        }
        return $forVarPtrs;
    }//end _getVarDeclaredInFor()

    /**
     * Returns the position of first occurrence of a PHP variable starting with
     * $ in $haystack from $offset.
     *
     * @param string $haystack The string to search in.
     * @param int    $offset   The optional offset parameter allows you to
     *                         specify which character in haystack to start
     *                         searching. The returned position is still
     *                         relative to the beginning of haystack.
     *
     * @return mixed The position as an integer
     *               or the boolean false, if no variable is found.
     */
    private static function _getVariablePosition($haystack, $offset = 0)
    {
        $var_starts_at = strpos($haystack, '$', $offset);
        $is_a_var = false;
        while (false !== $var_starts_at && ! $is_a_var) {
            // makes sure that $ is used for a variable and not as a symbol,
            // if $ is protected with the escape char, then it is a symbol.
            if (0 !== strcmp($haystack[$var_starts_at - 1], '\\')) {
                if (0 === strcmp($haystack[$var_starts_at + 1], '{')) {
                    // there is an opening brace in the right place
                    // so it looks for the closing brace in the right place
                    $hsChunk2 = substr($haystack, $var_starts_at + 2);
                    if (1 === preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\}/', $hsChunk2)) {
                        $is_a_var = true;
                    }
                } else {
                    $hsChunk1 = substr($haystack, $var_starts_at + 1);
                    if (1 === preg_match('/^[a-zA-Z_\x7f-\xff]/', $hsChunk1)) {
                        // $ is used for a variable and not as a symbol,
                        // since what follows $ matchs the definition of
                        // a variable label for PHP.
                        $is_a_var = true;
                    }
                }
            }
            // update $var_starts_at for the next variable
            // only if no variable was found, since it is returned otherwise.
            if ( ! $is_a_var) {
                $var_starts_at = strpos($haystack, '$', $var_starts_at + 1);
            }
        }
        if ($is_a_var) {
            return $var_starts_at;
        } else {
            return false;
        }
    }//end _getVariablePosition()
}//end class

?>