Seditio Source
Root |
./othercms/phpBB3/vendor/s9e/text-formatter/src/Configurator/RendererGenerators/PHP/ControlStructuresOptimizer.php
<?php

/**
* @package   s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;

/**
* Optimize the control structures of a script
*
* Removes brackets in control structures wherever possible. Prevents the generation of EXT_STMT
* opcodes where they're not strictly required.
*/
class ControlStructuresOptimizer extends AbstractOptimizer
{
   
/**
    * @var integer Number of braces encountered in current source
    */
   
protected $braces;

   
/**
    * @var array Current context
    */
   
protected $context;

   
/**
    * Test whether current block ends with an if or elseif control structure
    *
    * @return bool
    */
   
protected function blockEndsWithIf()
    {
        return
in_array($this->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;
            }
        }
    }
}