context['lastBlock'], [T_IF, T_ELSEIF], true); } /** * Test whether the token at current index is a control structure * * @return bool */ protected function isControlStructure() { return in_array( $this->tokens[$this->i][0], [T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_IF, T_WHILE], true ); } /** * Test whether current block is followed by an elseif/else structure * * @return bool */ protected function isFollowedByElse() { if ($this->i > $this->cnt - 4) { // It doesn't have room for another block return false; } // Compute the index of the next non-whitespace token $k = $this->i + 1; if ($this->tokens[$k][0] === T_WHITESPACE) { ++$k; } return in_array($this->tokens[$k][0], [T_ELSEIF, T_ELSE], true); } /** * Test whether braces must be preserved in current context * * @return bool */ protected function mustPreserveBraces() { // If current block ends with if/elseif and is followed by elseif/else, we must preserve // its braces to prevent it from merging with the outer elseif/else. IOW, we must preserve // the braces if "if{if{}}else" would become "if{if else}" return ($this->blockEndsWithIf() && $this->isFollowedByElse()); } /** * Optimize control structures in stored tokens * * @return void */ protected function optimizeTokens() { while (++$this->i < $this->cnt) { if ($this->tokens[$this->i] === ';') { ++$this->context['statements']; } elseif ($this->tokens[$this->i] === '{') { ++$this->braces; } elseif ($this->tokens[$this->i] === '}') { if ($this->context['braces'] === $this->braces) { $this->processEndOfBlock(); } --$this->braces; } elseif ($this->isControlStructure()) { $this->processControlStructure(); } } } /** * Process the control structure starting at current index * * @return void */ protected function processControlStructure() { // Save the index so we can rewind back to it in case of failure $savedIndex = $this->i; // Count this control structure in this context's statements unless it's an elseif/else // in which case it's already been counted as part of the if if (!in_array($this->tokens[$this->i][0], [T_ELSE, T_ELSEIF], true)) { ++$this->context['statements']; } // If the control structure is anything but an "else", skip its condition to reach the first // brace or statement if ($this->tokens[$this->i][0] !== T_ELSE) { $this->skipCondition(); } $this->skipWhitespace(); // Abort if this control structure does not use braces if ($this->tokens[$this->i] !== '{') { // Rewind all the way to the original token $this->i = $savedIndex; return; } ++$this->braces; // Replacement for the first brace $replacement = [T_WHITESPACE, '']; // Add a space after "else" if the brace is removed and it's not followed by whitespace or a // variable if ($this->tokens[$savedIndex][0] === T_ELSE && $this->tokens[$this->i + 1][0] !== T_VARIABLE && $this->tokens[$this->i + 1][0] !== T_WHITESPACE) { $replacement = [T_WHITESPACE, ' ']; } // Record the token of the control structure (T_IF, T_WHILE, etc...) in the current context $this->context['lastBlock'] = $this->tokens[$savedIndex][0]; // Create a new context $this->context = [ 'braces' => $this->braces, 'index' => $this->i, 'lastBlock' => null, 'parent' => $this->context, 'replacement' => $replacement, 'savedIndex' => $savedIndex, 'statements' => 0 ]; } /** * Process the block ending at current index * * @return void */ protected function processEndOfBlock() { if ($this->context['statements'] < 2 && !$this->mustPreserveBraces()) { $this->removeBracesInCurrentContext(); } $this->context = $this->context['parent']; // Propagate the "lastBlock" property upwards to handle multiple nested if statements $this->context['parent']['lastBlock'] = $this->context['lastBlock']; } /** * Remove the braces surrounding current context * * @return void */ protected function removeBracesInCurrentContext() { // Replace the first brace with the saved replacement $this->tokens[$this->context['index']] = $this->context['replacement']; // Remove the second brace or replace it with a semicolon if there are no statements in this // block $this->tokens[$this->i] = ($this->context['statements']) ? [T_WHITESPACE, ''] : ';'; // Remove the whitespace before braces. This is mainly cosmetic foreach ([$this->context['index'] - 1, $this->i - 1] as $tokenIndex) { if ($this->tokens[$tokenIndex][0] === T_WHITESPACE) { $this->tokens[$tokenIndex][1] = ''; } } // Test whether the current block followed an else statement then test whether this // else was followed by an if if ($this->tokens[$this->context['savedIndex']][0] === T_ELSE) { $j = 1 + $this->context['savedIndex']; while ($this->tokens[$j][0] === T_WHITESPACE || $this->tokens[$j][0] === T_COMMENT || $this->tokens[$j][0] === T_DOC_COMMENT) { ++$j; } if ($this->tokens[$j][0] === T_IF) { // Replace if with elseif $this->tokens[$j] = [T_ELSEIF, 'elseif']; // Remove the original else $j = $this->context['savedIndex']; $this->tokens[$j] = [T_WHITESPACE, '']; // Remove any whitespace before the original else if ($this->tokens[$j - 1][0] === T_WHITESPACE) { $this->tokens[$j - 1][1] = ''; } // Unindent what was the else's content $this->unindentBlock($j, $this->i - 1); // Ensure that the brace after the now-removed "else" was not replaced with a space $this->tokens[$this->context['index']] = [T_WHITESPACE, '']; } } $this->changed = true; } /** * {@inheritdoc} */ protected function reset($php) { parent::reset($php); $this->braces = 0; $this->context = [ 'braces' => 0, 'index' => -1, 'parent' => [], 'preventElse' => false, 'savedIndex' => 0, 'statements' => 0 ]; } /** * Skip the condition of a control structure * * @return void */ protected function skipCondition() { // Reach the opening parenthesis $this->skipToString('('); // Iterate through tokens until we have a match for every left parenthesis $parens = 0; while (++$this->i < $this->cnt) { if ($this->tokens[$this->i] === ')') { if ($parens) { --$parens; } else { break; } } elseif ($this->tokens[$this->i] === '(') { ++$parens; } } } }