Mercurial > hg > isophonics-drupal-site
diff vendor/drupal/coder/coder_sniffer/Drupal/Sniffs/WhiteSpace/ScopeIndentSniff.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vendor/drupal/coder/coder_sniffer/Drupal/Sniffs/WhiteSpace/ScopeIndentSniff.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,1286 @@ +<?php +/** + * Drupal_Sniffs_Whitespace_ScopeIndentSniff. + * + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer + */ + +/** + * Largely copied from Generic_Sniffs_Whitespace_ScopeIndentSniff, modified to make + * the exact mode working with comments and multi line statements. + * + * Checks that control structures are structured correctly, and their content + * is indented correctly. This sniff will throw errors if tabs are used + * for indentation rather than spaces. + * + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +class Drupal_Sniffs_WhiteSpace_ScopeIndentSniff implements PHP_CodeSniffer_Sniff +{ + + /** + * A list of tokenizers this sniff supports. + * + * @var array + */ + public $supportedTokenizers = array('PHP'); + + /** + * The number of spaces code should be indented. + * + * @var int + */ + public $indent = 2; + + /** + * Does the indent need to be exactly right? + * + * If TRUE, indent needs to be exactly $indent spaces. If FALSE, + * indent needs to be at least $indent spaces (but can be more). + * + * @var bool + */ + public $exact = true; + + /** + * Should tabs be used for indenting? + * + * If TRUE, fixes will be made using tabs instead of spaces. + * The size of each tab is important, so it should be specified + * using the --tab-width CLI argument. + * + * @var bool + */ + public $tabIndent = false; + + /** + * The --tab-width CLI value that is being used. + * + * @var int + */ + private $_tabWidth = null; + + /** + * List of tokens not needing to be checked for indentation. + * + * Useful to allow Sniffs based on this to easily ignore/skip some + * tokens from verification. For example, inline HTML sections + * or PHP open/close tags can escape from here and have their own + * rules elsewhere. + * + * @var int[] + */ + public $ignoreIndentationTokens = array(); + + /** + * List of tokens not needing to be checked for indentation. + * + * This is a cached copy of the public version of this var, which + * can be set in a ruleset file, and some core ignored tokens. + * + * @var int[] + */ + private $_ignoreIndentationTokens = array(); + + /** + * Any scope openers that should not cause an indent. + * + * @var int[] + */ + protected $nonIndentingScopes = array(); + + /** + * Show debug output for this sniff. + * + * @var bool + */ + private $_debug = false; + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { + $this->_debug = false; + } + + return array(T_OPEN_TAG); + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + { + $debug = PHP_CodeSniffer::getConfigData('scope_indent_debug'); + if ($debug !== null) { + $this->_debug = (bool) $debug; + } + + if ($this->_tabWidth === null) { + $cliValues = $phpcsFile->phpcs->cli->getCommandLineValues(); + if (isset($cliValues['tabWidth']) === false || $cliValues['tabWidth'] === 0) { + // We have no idea how wide tabs are, so assume 4 spaces for fixing. + // It shouldn't really matter because indent checks elsewhere in the + // standard should fix things up. + $this->_tabWidth = 4; + } else { + $this->_tabWidth = $cliValues['tabWidth']; + } + } + + $currentIndent = 0; + $lastOpenTag = $stackPtr; + $lastCloseTag = null; + $openScopes = array(); + $adjustments = array(); + $setIndents = array(); + + $tokens = $phpcsFile->getTokens(); + $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); + $trimmed = ltrim($tokens[$first]['content']); + if ($trimmed === '') { + $currentIndent = ($tokens[$stackPtr]['column'] - 1); + } else { + $currentIndent = (strlen($tokens[$first]['content']) - strlen($trimmed)); + } + + if ($this->_debug === true) { + $line = $tokens[$stackPtr]['line']; + echo "Start with token $stackPtr on line $line with indent $currentIndent".PHP_EOL; + } + + if (empty($this->_ignoreIndentationTokens) === true) { + $this->_ignoreIndentationTokens = array(T_INLINE_HTML => true); + foreach ($this->ignoreIndentationTokens as $token) { + if (is_int($token) === false) { + if (defined($token) === false) { + continue; + } + + $token = constant($token); + } + + $this->_ignoreIndentationTokens[$token] = true; + } + }//end if + + $this->exact = (bool) $this->exact; + $this->tabIndent = (bool) $this->tabIndent; + + for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) { + if ($i === false) { + // Something has gone very wrong; maybe a parse error. + break; + } + + $checkToken = null; + $checkIndent = null; + + $exact = (bool) $this->exact; + if ($exact === true && isset($tokens[$i]['nested_parenthesis']) === true) { + // Don't check indents exactly between parenthesis as they + // tend to have custom rules, such as with multi-line function calls + // and control structure conditions. + $exact = false; + } + + // Detect line changes and figure out where the indent is. + if ($tokens[$i]['column'] === 1) { + $trimmed = ltrim($tokens[$i]['content']); + if ($trimmed === '') { + if (isset($tokens[($i + 1)]) === true + && $tokens[$i]['line'] === $tokens[($i + 1)]['line'] + ) { + $checkToken = ($i + 1); + $tokenIndent = ($tokens[($i + 1)]['column'] - 1); + } + } else { + $checkToken = $i; + $tokenIndent = (strlen($tokens[$i]['content']) - strlen($trimmed)); + } + } + + // Closing parenthesis should just be indented to at least + // the same level as where they were opened (but can be more). + if (($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$checkToken]['parenthesis_opener']) === true) + || ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$i]['parenthesis_opener']) === true) + ) { + if ($checkToken !== null) { + $parenCloser = $checkToken; + } else { + $parenCloser = $i; + } + + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Closing parenthesis found on line $line".PHP_EOL; + } + + $parenOpener = $tokens[$parenCloser]['parenthesis_opener']; + if ($tokens[$parenCloser]['line'] !== $tokens[$parenOpener]['line']) { + $parens = 0; + if (isset($tokens[$parenCloser]['nested_parenthesis']) === true + && empty($tokens[$parenCloser]['nested_parenthesis']) === false + ) { + end($tokens[$parenCloser]['nested_parenthesis']); + $parens = key($tokens[$parenCloser]['nested_parenthesis']); + if ($this->_debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } + } + + $condition = 0; + if (isset($tokens[$parenCloser]['conditions']) === true + && empty($tokens[$parenCloser]['conditions']) === false + ) { + end($tokens[$parenCloser]['conditions']); + $condition = key($tokens[$parenCloser]['conditions']); + if ($this->_debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } + + if ($parens > $condition) { + if ($this->_debug === true) { + echo "\t* using parenthesis *".PHP_EOL; + } + + $parenOpener = $parens; + $condition = 0; + } else if ($condition > 0) { + if ($this->_debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $parenOpener = $condition; + $parens = 0; + } + + $exact = false; + + $lastOpenTagConditions = array_keys($tokens[$lastOpenTag]['conditions']); + $lastOpenTagCondition = array_pop($lastOpenTagConditions); + + if ($condition > 0 && $lastOpenTagCondition === $condition) { + if ($this->_debug === true) { + echo "\t* open tag is inside condition; using open tag *".PHP_EOL; + } + + $checkIndent = ($tokens[$lastOpenTag]['column'] - 1); + if (isset($adjustments[$condition]) === true) { + $checkIndent += $adjustments[$condition]; + } + + $currentIndent = $checkIndent; + + if ($this->_debug === true) { + $type = $tokens[$lastOpenTag]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $lastOpenTag ($type)".PHP_EOL; + } + } else if ($condition > 0 + && isset($tokens[$condition]['scope_opener']) === true + && isset($setIndents[$tokens[$condition]['scope_opener']]) === true + ) { + $checkIndent = $setIndents[$tokens[$condition]['scope_opener']]; + if (isset($adjustments[$condition]) === true) { + $checkIndent += $adjustments[$condition]; + } + + $currentIndent = $checkIndent; + + if ($this->_debug === true) { + $type = $tokens[$condition]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $condition ($type)".PHP_EOL; + } + } else { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parenOpener, true); + + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + if ($first === $tokens[$parenCloser]['parenthesis_opener']) { + // This is unlikely to be the start of the statement, so look + // back further to find it. + $first--; + } + + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->_debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + if ($this->_debug === true) { + echo "\t* first token is a scope closer *".PHP_EOL; + } + + if (isset($tokens[$first]['scope_condition']) === true) { + $scopeCloser = $first; + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['scope_condition'], true); + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + if ($tokens[$tokens[$scopeCloser]['scope_condition']]['code'] !== T_CLOSURE) { + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + } + + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + } else { + // Don't force current indent to divisible because there could be custom + // rules in place between parenthesis, such as with arrays. + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + }//end if + } else if ($this->_debug === true) { + echo "\t * ignoring single-line definition *".PHP_EOL; + }//end if + }//end if + + // Closing short array bracket should just be indented to at least + // the same level as where it was opened (but can be more). + if ($tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY + || ($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_SHORT_ARRAY) + ) { + if ($checkToken !== null) { + $arrayCloser = $checkToken; + } else { + $arrayCloser = $i; + } + + if ($this->_debug === true) { + $line = $tokens[$arrayCloser]['line']; + echo "Closing short array bracket found on line $line".PHP_EOL; + } + + $arrayOpener = $tokens[$arrayCloser]['bracket_opener']; + if ($tokens[$arrayCloser]['line'] !== $tokens[$arrayOpener]['line']) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $arrayOpener, true); + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + $exact = false; + + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + if ($first === $tokens[$arrayCloser]['bracket_opener']) { + // This is unlikely to be the start of the statement, so look + // back further to find it. + $first--; + } + + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->_debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + // The first token is a scope closer and would have already + // been processed and set the indent level correctly, so + // don't adjust it again. + if ($this->_debug === true) { + echo "\t* first token is a scope closer; ignoring closing short array bracket *".PHP_EOL; + } + + if (isset($setIndents[$first]) === true) { + $currentIndent = $setIndents[$first]; + if ($this->_debug === true) { + echo "\t=> indent reset to $currentIndent".PHP_EOL; + } + } + } else { + // Don't force current indent to be divisible because there could be custom + // rules in place for arrays. + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + } else if ($this->_debug === true) { + echo "\t * ignoring single-line definition *".PHP_EOL; + }//end if + }//end if + + // Adjust lines within scopes while auto-fixing. + if ($checkToken !== null + && $exact === false + && (empty($tokens[$checkToken]['conditions']) === false + || (isset($tokens[$checkToken]['scope_opener']) === true + && $tokens[$checkToken]['scope_opener'] === $checkToken)) + ) { + if (empty($tokens[$checkToken]['conditions']) === false) { + end($tokens[$checkToken]['conditions']); + $condition = key($tokens[$checkToken]['conditions']); + } else { + $condition = $tokens[$checkToken]['scope_condition']; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); + + if (isset($adjustments[$first]) === true + && (($adjustments[$first] < 0 && $tokenIndent > $currentIndent) + || ($adjustments[$first] > 0 && $tokenIndent < $currentIndent)) + ) { + $padding = ($tokenIndent + $adjustments[$first]); + if ($padding > 0) { + if ($this->tabIndent === true) { + $numTabs = floor($padding / $this->_tabWidth); + $numSpaces = ($padding - ($numTabs * $this->_tabWidth)); + $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); + } else { + $padding = str_repeat(' ', $padding); + } + } else { + $padding = ''; + } + + if ($checkToken === $i) { + $phpcsFile->fixer->replaceToken($checkToken, $padding.$trimmed); + } else { + // Easier to just replace the entire indent. + $phpcsFile->fixer->replaceToken(($checkToken - 1), $padding); + } + + if ($this->_debug === true) { + $length = strlen($padding); + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "Indent adjusted to $length for $type on line $line".PHP_EOL; + } + + $adjustments[$checkToken] = $adjustments[$first]; + + if ($this->_debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t=> Add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; + } + }//end if + }//end if + + // Scope closers reset the required indent to the same level as the opening condition. + if (($checkToken !== null + && isset($openScopes[$checkToken]) === true + || (isset($tokens[$checkToken]['scope_condition']) === true + && isset($tokens[$checkToken]['scope_closer']) === true + && $tokens[$checkToken]['scope_closer'] === $checkToken + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])) + || ($checkToken === null + && isset($openScopes[$i]) === true + || (isset($tokens[$i]['scope_condition']) === true + && isset($tokens[$i]['scope_closer']) === true + && $tokens[$i]['scope_closer'] === $i + && $tokens[$i]['line'] !== $tokens[$tokens[$i]['scope_opener']]['line'])) + ) { + if ($this->_debug === true) { + if ($checkToken === null) { + $type = $tokens[$tokens[$i]['scope_condition']]['type']; + $line = $tokens[$i]['line']; + } else { + $type = $tokens[$tokens[$checkToken]['scope_condition']]['type']; + $line = $tokens[$checkToken]['line']; + } + + echo "Close scope ($type) on line $line".PHP_EOL; + } + + $scopeCloser = $checkToken; + if ($scopeCloser === null) { + $scopeCloser = $i; + } else { + array_pop($openScopes); + } + + if (isset($tokens[$scopeCloser]['scope_condition']) === true) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['scope_condition'], true); + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + if ($tokens[$tokens[$scopeCloser]['scope_condition']]['code'] !== T_CLOSURE) { + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + } + + $setIndents[$scopeCloser] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$scopeCloser]['type']; + echo "\t=> indent set to $currentIndent by token $scopeCloser ($type)".PHP_EOL; + } + + // We only check the indent of scope closers if they are + // curly braces because other constructs tend to have different rules. + if ($tokens[$scopeCloser]['code'] === T_CLOSE_CURLY_BRACKET) { + $exact = true; + } else { + $checkToken = null; + } + }//end if + }//end if + + // Handle scope for JS object notation. + if ($phpcsFile->tokenizerType === 'JS' + && (($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_OBJECT + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['bracket_opener']]['line']) + || ($checkToken === null + && $tokens[$i]['code'] === T_CLOSE_OBJECT + && $tokens[$i]['line'] !== $tokens[$tokens[$i]['bracket_opener']]['line'])) + ) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Close JS object on line $line".PHP_EOL; + } + + $scopeCloser = $checkToken; + if ($scopeCloser === null) { + $scopeCloser = $i; + } else { + array_pop($openScopes); + } + + $parens = 0; + if (isset($tokens[$scopeCloser]['nested_parenthesis']) === true + && empty($tokens[$scopeCloser]['nested_parenthesis']) === false + ) { + end($tokens[$scopeCloser]['nested_parenthesis']); + $parens = key($tokens[$scopeCloser]['nested_parenthesis']); + if ($this->_debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } + } + + $condition = 0; + if (isset($tokens[$scopeCloser]['conditions']) === true + && empty($tokens[$scopeCloser]['conditions']) === false + ) { + end($tokens[$scopeCloser]['conditions']); + $condition = key($tokens[$scopeCloser]['conditions']); + if ($this->_debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } + + if ($parens > $condition) { + if ($this->_debug === true) { + echo "\t* using parenthesis *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parens, true); + $condition = 0; + } else if ($condition > 0) { + if ($this->_debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); + $parens = 0; + } else { + if ($this->_debug === true) { + $line = $tokens[$tokens[$scopeCloser]['bracket_opener']]['line']; + echo "\t* token is not in parenthesis or condition; using opener on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['bracket_opener'], true); + }//end if + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + if ($parens > 0 || $condition > 0) { + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + if ($condition > 0) { + $checkIndent += $this->indent; + $currentIndent += $this->indent; + $exact = true; + } + } else { + $checkIndent = $currentIndent; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + + if ($checkToken !== null + && isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$tokens[$checkToken]['code']]) === true + && in_array($tokens[$checkToken]['code'], $this->nonIndentingScopes) === false + && isset($tokens[$checkToken]['scope_opener']) === true + ) { + $exact = true; + + $lastOpener = null; + if (empty($openScopes) === false) { + end($openScopes); + $lastOpener = current($openScopes); + } + + // A scope opener that shares a closer with another token (like multiple + // CASEs using the same BREAK) needs to reduce the indent level so its + // indent is checked correctly. It will then increase the indent again + // (as all openers do) after being checked. + if ($lastOpener !== null + && isset($tokens[$lastOpener]['scope_closer']) === true + && $tokens[$lastOpener]['level'] === $tokens[$checkToken]['level'] + && $tokens[$lastOpener]['scope_closer'] === $tokens[$checkToken]['scope_closer'] + ) { + $currentIndent -= $this->indent; + $setIndents[$lastOpener] = $currentIndent; + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$lastOpener]['type']; + echo "Shared closer found on line $line".PHP_EOL; + echo "\t=> indent set to $currentIndent by token $lastOpener ($type)".PHP_EOL; + } + } + + if ($tokens[$checkToken]['code'] === T_CLOSURE + && $tokenIndent > $currentIndent + ) { + // The opener is indented more than needed, which is fine. + // But just check that it is divisible by our expected indent. + $checkIndent = (int) (ceil($tokenIndent / $this->indent) * $this->indent); + $exact = false; + + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Closure found on line $line".PHP_EOL; + echo "\t=> checking indent of $checkIndent; main indent remains at $currentIndent".PHP_EOL; + } + } + }//end if + + // Method prefix indentation has to be exact or else if will break + // the rest of the function declaration, and potentially future ones. + if ($checkToken !== null + && isset(PHP_CodeSniffer_Tokens::$methodPrefixes[$tokens[$checkToken]['code']]) === true + && $tokens[($checkToken + 1)]['code'] !== T_DOUBLE_COLON + ) { + $exact = true; + } + + // JS property indentation has to be exact or else if will break + // things like function and object indentation. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_PROPERTY) { + $exact = true; + } + + // PHP tags needs to be indented to exact column positions + // so they don't cause problems with indent checks for the code + // within them, but they don't need to line up with the current indent. + if ($checkToken !== null + && ($tokens[$checkToken]['code'] === T_OPEN_TAG + || $tokens[$checkToken]['code'] === T_OPEN_TAG_WITH_ECHO + || $tokens[$checkToken]['code'] === T_CLOSE_TAG) + ) { + $exact = true; + $checkIndent = ($tokens[$checkToken]['column'] - 1); + $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); + } + + // Check the line indent. + if ($checkIndent === null) { + $checkIndent = $currentIndent; + } + + // If the line starts with "->" we assume this is an indented chained + // method invocation, so we add one level of indentation. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_OBJECT_OPERATOR) { + $checkIndent += $this->indent; + } + + // Comments starting with a star have an extra whitespace. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_COMMENT) { + $content = trim($tokens[$checkToken]['content']); + if ($content{0} === '*') { + $checkIndent += 1; + } + } + + $adjusted = false; + if ($checkToken !== null + && isset($this->_ignoreIndentationTokens[$tokens[$checkToken]['code']]) === false + && (($tokenIndent !== $checkIndent && $exact === true) + || ($tokenIndent < $checkIndent && $exact === false)) + ) { + if ($tokenIndent > $checkIndent) { + // Ignore multi line statements. + $before = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($checkToken - 1), null, true); + if ($before !== false && in_array( + $tokens[$before]['code'], + array( + T_SEMICOLON, + T_CLOSE_CURLY_BRACKET, + T_OPEN_CURLY_BRACKET, + T_COLON, + ) + ) === false + ) { + continue; + } + } + + // Skip array closing indentation errors, this is handled by the + // ArraySniff. + if (($tokens[$checkToken]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$checkToken]['parenthesis_owner']) === true + && $tokens[$tokens[$checkToken]['parenthesis_owner']]['code'] === T_ARRAY) + || $tokens[$checkToken]['code'] === T_CLOSE_SHORT_ARRAY + ) { + continue; + } + + $type = 'IncorrectExact'; + $error = 'Line indented incorrectly; expected '; + if ($exact === false) { + $error .= 'at least '; + $type = 'Incorrect'; + } + + if ($this->tabIndent === true) { + $error .= '%s tabs, found %s'; + $data = array( + floor($checkIndent / $this->_tabWidth), + floor($tokenIndent / $this->_tabWidth), + ); + } else { + $error .= '%s spaces, found %s'; + $data = array( + $checkIndent, + $tokenIndent, + ); + } + + if ($this->_debug === true) { + $line = $tokens[$checkToken]['line']; + $message = vsprintf($error, $data); + echo "[Line $line] $message".PHP_EOL; + } + + $fix = $phpcsFile->addFixableError($error, $checkToken, $type, $data); + if ($fix === true || $this->_debug === true) { + $padding = ''; + if ($this->tabIndent === true) { + $numTabs = floor($checkIndent / $this->_tabWidth); + if ($numTabs > 0) { + $numSpaces = ($checkIndent - ($numTabs * $this->_tabWidth)); + $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); + } + } else if ($checkIndent > 0) { + $padding = str_repeat(' ', $checkIndent); + } + + if ($checkToken === $i) { + $accepted = $phpcsFile->fixer->replaceToken($checkToken, $padding.$trimmed); + } else { + // Easier to just replace the entire indent. + $accepted = $phpcsFile->fixer->replaceToken(($checkToken - 1), $padding); + } + + if ($accepted === true) { + $adjustments[$checkToken] = ($checkIndent - $tokenIndent); + if ($this->_debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t=> Add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; + } + } + } else { + // Assume the change would be applied and continue + // checking indents under this assumption. This gives more + // technically accurate error messages. + $adjustments[$checkToken] = ($checkIndent - $tokenIndent); + }//end if + }//end if + + if ($checkToken !== null) { + $i = $checkToken; + } + + // Completely skip here/now docs as the indent is a part of the + // content itself. + if ($tokens[$i]['code'] === T_START_HEREDOC + || $tokens[$i]['code'] === T_START_NOWDOC + ) { + $i = $phpcsFile->findNext(array(T_END_HEREDOC, T_END_NOWDOC), ($i + 1)); + continue; + } + + // Completely skip multi-line strings as the indent is a part of the + // content itself. + if ($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING + || $tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING + ) { + $i = $phpcsFile->findNext($tokens[$i]['code'], ($i + 1), null, true); + $i--; + continue; + } + + // Completely skip doc comments as they tend to have complex + // indentation rules. + if ($tokens[$i]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $i = $tokens[$i]['comment_closer']; + continue; + } + + // Open tags reset the indent level. + if ($tokens[$i]['code'] === T_OPEN_TAG + || $tokens[$i]['code'] === T_OPEN_TAG_WITH_ECHO + ) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Open PHP tag found on line $line".PHP_EOL; + } + + if ($checkToken === null) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + $currentIndent = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); + } else { + $currentIndent = ($tokens[$i]['column'] - 1); + } + + $lastOpenTag = $i; + + if (isset($adjustments[$i]) === true) { + $currentIndent += $adjustments[$i]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$i] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Close tags reset the indent level, unless they are closing a tag + // opened on the same line. + if ($tokens[$i]['code'] === T_CLOSE_TAG) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Close PHP tag found on line $line".PHP_EOL; + } + + if ($tokens[$lastOpenTag]['line'] !== $tokens[$i]['line']) { + $currentIndent = ($tokens[$i]['column'] - 1); + $lastCloseTag = $i; + } else { + if ($lastCloseTag === null) { + $currentIndent = 0; + } else { + $currentIndent = ($tokens[$lastCloseTag]['column'] - 1); + } + } + + if (isset($adjustments[$i]) === true) { + $currentIndent += $adjustments[$i]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$i] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Anon classes and functions set the indent based on their own indent level. + if ($tokens[$i]['code'] === T_CLOSURE || $tokens[$i]['code'] === T_ANON_CLASS) { + $closer = $tokens[$i]['scope_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->_debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); + $line = $tokens[$i]['line']; + echo "* ignoring single-line $type on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + if ($this->_debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); + $line = $tokens[$i]['line']; + echo "Open $type on line $line".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); + + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (floor($currentIndent / $this->indent) * $this->indent); + $i = $tokens[$i]['scope_opener']; + $setIndents[$i] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Scope openers increase the indent level. + if (isset($tokens[$i]['scope_condition']) === true + && isset($tokens[$i]['scope_opener']) === true + && $tokens[$i]['scope_opener'] === $i + ) { + $closer = $tokens[$i]['scope_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$i]['type']; + echo "* ignoring single-line $type on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + $condition = $tokens[$tokens[$i]['scope_condition']]['code']; + if (isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$condition]) === true + && in_array($condition, $this->nonIndentingScopes) === false + ) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$tokens[$i]['scope_condition']]['type']; + echo "Open scope ($type) on line $line".PHP_EOL; + } + + $currentIndent += $this->indent; + $setIndents[$i] = $currentIndent; + $openScopes[$tokens[$i]['scope_closer']] = $tokens[$i]['scope_condition']; + + if ($this->_debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + } + }//end if + + // JS objects set the indent level. + if ($phpcsFile->tokenizerType === 'JS' + && $tokens[$i]['code'] === T_OBJECT + ) { + $closer = $tokens[$i]['bracket_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "* ignoring single-line JS object on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + if ($this->_debug === true) { + $line = $tokens[$i]['line']; + echo "Open JS object on line $line".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + + continue; + }//end if + + // Closing an anon class or function. + if (isset($tokens[$i]['scope_condition']) === true + && $tokens[$i]['scope_closer'] === $i + && ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS) + ) { + if ($this->_debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2))); + $line = $tokens[$i]['line']; + echo "Close $type on line $line".PHP_EOL; + } + + $prev = false; + + $object = 0; + if ($phpcsFile->tokenizerType === 'JS') { + $conditions = $tokens[$i]['conditions']; + krsort($conditions, SORT_NUMERIC); + foreach ($conditions as $token => $condition) { + if ($condition === T_OBJECT) { + $object = $token; + break; + } + } + + if ($this->_debug === true && $object !== 0) { + $line = $tokens[$object]['line']; + echo "\t* token is inside JS object $object on line $line *".PHP_EOL; + } + } + + $parens = 0; + if (isset($tokens[$i]['nested_parenthesis']) === true + && empty($tokens[$i]['nested_parenthesis']) === false + ) { + end($tokens[$i]['nested_parenthesis']); + $parens = key($tokens[$i]['nested_parenthesis']); + if ($this->_debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } + } + + $condition = 0; + if (isset($tokens[$i]['conditions']) === true + && empty($tokens[$i]['conditions']) === false + ) { + end($tokens[$i]['conditions']); + $condition = key($tokens[$i]['conditions']); + if ($this->_debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } + + if ($parens > $object && $parens > $condition) { + if ($this->_debug === true) { + echo "\t* using parenthesis *".PHP_EOL; + } + + $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($parens - 1), null, true); + $object = 0; + $condition = 0; + } else if ($object > 0 && $object >= $condition) { + if ($this->_debug === true) { + echo "\t* using object *".PHP_EOL; + } + + $prev = $object; + $parens = 0; + $condition = 0; + } else if ($condition > 0) { + if ($this->_debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $prev = $condition; + $object = 0; + $parens = 0; + }//end if + + if ($prev === false) { + $prev = $phpcsFile->findPrevious(array(T_EQUAL, T_RETURN), ($tokens[$i]['scope_condition'] - 1), null, false, null, true); + if ($prev === false) { + $prev = $i; + if ($this->_debug === true) { + echo "\t* could not find a previous T_EQUAL or T_RETURN token; will use current token *".PHP_EOL; + } + } + } + + if ($this->_debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous token is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + $prev = $phpcsFile->findStartOfStatement($first); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->_debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* amended previous is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->_debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + $currentIndent = ($tokens[$first]['column'] - 1); + if ($object > 0 || $condition > 0) { + $currentIndent += $this->indent; + } + + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + if ($this->_debug === true) { + echo "\t* first token is a scope closer *".PHP_EOL; + } + + if ($condition === 0 || $tokens[$condition]['scope_opener'] < $first) { + $currentIndent = $setIndents[$first]; + } else if ($this->_debug === true) { + echo "\t* ignoring scope closer *".PHP_EOL; + } + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->_debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + }//end for + + // Don't process the rest of the file. + return $phpcsFile->numTokens; + + }//end process() + + +}//end class