jenkins-bot has submitted this change and it was merged.

Change subject: Add while loops (version 0.5.0)
......................................................................


Add while loops (version 0.5.0)

* Add Parser Function Foxway ( {{#foxway: echo | 'hello' }} )
* Add variable $wgFoxway_max_execution_time
* Add variable $wgFoxway_max_execution_time_for_scope
* Add Hook 'ParserLimitReport'
* Add Array function key_exists (Alias of array_key_exists)
* Fix not enabled by default function strncasecmp()
* Fix boolean AND & OR operators

Change-Id: I426f3b2048205a920fd5e11596b09b4cc640f240
---
M Foxway.body.php
M Foxway.i18n.php
M Foxway.php
M Settings.php
M includes/Debug.php
M includes/Interpreter.php
M includes/Runtime.php
M includes/RuntimeDebug.php
M includes/functions/FArray.php
M resources/ext.foxway.debug.css
A resources/ext.foxway.debugloops.js
M tests/phpunit/includes/InterpreterTest.php
12 files changed, 559 insertions(+), 66 deletions(-)

Approvals:
  Pastakhov: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Foxway.body.php b/Foxway.body.php
index f707185..5bb13a9 100644
--- a/Foxway.body.php
+++ b/Foxway.body.php
@@ -9,16 +9,62 @@
  */
 class Foxway {
 
+       static $DebugLoops = false;
+       static $time = false;
+       static $startTime = false;
+
        static $frames=array();
 
+       /**
+        *
+        * @param Parser $parser
+        * @param PPFrame $frame
+        * @param array $args
+        */
+       public static function renderFunction( $parser, $frame, $args ) {
+               self::$startTime = microtime(true);
+
+               $is_banned = self::isBanned($frame);
+               if( $is_banned ) {
+                       return $is_banned;
+               }
+
+               $command = array_shift($args);
+               if( count($args) > 0 ) {
+                       foreach ($args as &$value) {
+                               $value = $frame->expand( $value );
+                       }
+                       $command = "echo $command (" . implode(',', $args) . 
');';
+               }
+               //MWDebug::log($command);
+
+               $result = Foxway\Interpreter::run(
+                               $command,
+                               array($frame->getTitle()->getPrefixedText()),
+                               self::getScope($frame)
+                               );
+
+               foreach ($result as &$value) {
+                       if( $value instanceof Foxway\iRawOutput ) {
+                               $value = '(object)';
+                       }
+               }
+               $return = implode($result);
+
+               self::$time += microtime(true) - self::$startTime;
+               return \UtfNormal::cleanUp($return);
+       }
+
        public static function render($input, array $args, Parser $parser, 
PPFrame $frame) {
-               global $wgNamespacesWithFoxway;
-               if( $wgNamespacesWithFoxway !== true && 
empty($wgNamespacesWithFoxway[$frame->getTitle()->getNamespace()]) ) {
-                       return Html::element( 'span', array('class'=>'error'), 
wfMessage('foxway-disabled-for-namespace', 
$frame->getTitle()->getNsText())->escaped() );
+               self::$startTime = microtime(true);
+
+               $is_banned = self::isBanned($frame);
+               if( $is_banned ) {
+                       return $is_banned;
                }
 
                $is_debug = isset($args['debug']);
-               $return = '';
+               $return = false;
 
                $result = Foxway\Interpreter::run(
                                $input,
@@ -27,18 +73,32 @@
                                $is_debug
                        );
 
-               foreach ($result as &$value) {
-                       if( $value instanceof Foxway\iRawOutput ) {
-                               $value = (string)$value;
-                       }
-               }
-
                if( $is_debug ) {
                        $parser->getOutput()->addModules('ext.Foxway.Debug');
+                       if( self::$DebugLoops ) {
+                               
$parser->getOutput()->addModules('ext.Foxway.DebugLoops');
+                       }
                        $return .= self::insertNoWiki( $parser, 
array_shift($result) ) . "\n";
                }
 
-               return $return . self::insertGeneral( $parser, 
$parser->recursiveTagParse(implode($result),$frame) );
+               if( count($result) > 0 ) {
+                       //$return .= 
Sanitizer::removeHTMLtags(implode($result));
+                       $return .= self::insertGeneral( $parser, 
$parser->recursiveTagParse(implode($result),$frame) );
+               }
+
+               self::$time += microtime(true) - self::$startTime;
+               return \UtfNormal::cleanUp($return);
+       }
+
+       public static function isBanned(PPFrame $frame) {
+               global $wgNamespacesWithFoxway, $wgFoxway_max_execution_time;
+               if( $wgNamespacesWithFoxway !== true && 
empty($wgNamespacesWithFoxway[$frame->getTitle()->getNamespace()]) ) {
+                       return Html::element( 'span', array('class'=>'error'), 
wfMessage('foxway-disabled-for-namespace', 
$frame->getTitle()->getNsText())->escaped() );
+               }
+               if( $wgFoxway_max_execution_time !== false && self::$time >= 
$wgFoxway_max_execution_time) {
+                       return Html::element( 'span', array('class'=>'error'), 
wfMessage('foxway-php-fatal-error-max-execution-time', 
$wgFoxway_max_execution_time, $frame->getTitle()->getPrefixedText())->escaped() 
);
+               }
+               return false;
        }
 
        /**
@@ -75,4 +135,5 @@
                self::$frames[] = array($frame, $scope);
                return $scope;
        }
-}
\ No newline at end of file
+
+}
diff --git a/Foxway.i18n.php b/Foxway.i18n.php
index 670e1b8..d21358a 100644
--- a/Foxway.i18n.php
+++ b/Foxway.i18n.php
@@ -18,6 +18,9 @@
        'foxway-error-bad-delimiter' => 'Delimiter must not be alphanumeric or 
backslash',
        'foxway-error-no-ending-matching-delimiter' => 'No ending matching 
delimiter "$1" found',
        'foxway-error-unknown-modifier' => 'Unknown modifier "$1"',
+       'foxway-php-fatal-error-cannot-break-continue' => 'PHP fatal error: 
Cannot break/continue $1 levels on page $2 line $3.',
+       'foxway-php-fatal-error-max-execution-time' => 'PHP fatal error: 
Maximum execution time of $1 second exceeded on page $2.',
+       'foxway-php-fatal-error-max-execution-time-scope' => 'PHP fatal error: 
Maximum execution time of $1 second exceeded on page $2 line $3.',
        'foxway-php-fatal-error-undefined-function' => 'PHP fatal error: Call 
to undefined function $1() on page $2 line $3.',
        'foxway-php-not-variable-passed-by-reference' => 'PHP fatal error: Only 
variables can be passed by reference, function $1() on page $2 line $3.',
        'foxway-php-syntax-error-unexpected' => 'PHP parse error: Syntax error, 
unexpected $1 in command line code on line $2.',
@@ -38,6 +41,17 @@
        'foxway-error-no-ending-matching-delimiter' => 'Error message. 
Parameters:
 * $1 - delimiter',
        'foxway-error-unknown-modifier' => 'Error message, parameter $1 is 
modifier',
+       'foxway-php-fatal-error-max-execution-time' => 'Error message, 
parameters:
+* $1 - the number of seconds
+* $2 - the name of the page on which the error occurred',
+       'foxway-php-fatal-error-cannot-break-continue' => 'Error message, 
parameters:
+* $1 - the number of user defined level
+* $2 - the name of the page on which the error occurred
+* $3 - the line number where the error occurred',
+       'foxway-php-fatal-error-max-execution-time-scope' => 'Error message, 
parameters:
+* $1 - the number of seconds
+* $2 - the name of the page on which the error occurred
+* $3 - the line number where the error occurred',
        'foxway-php-fatal-error-undefined-function' => 'Error message, 
parameters:
 * $1 - user-specified function name
 * $2 - the name of the page on which the error occurred
diff --git a/Foxway.php b/Foxway.php
index 4295eb8..bf013fb 100644
--- a/Foxway.php
+++ b/Foxway.php
@@ -15,7 +15,7 @@
        die( 'This file is an extension to MediaWiki and thus not a valid entry 
point.' );
 }
 
-define( 'Foxway_VERSION' , '0.4.4' );
+define( 'Foxway_VERSION' , '0.5.0' );
 
 // Register this extension on Special:Version
 $wgExtensionCredits['parserhook'][] = array(
@@ -23,7 +23,7 @@
        'name'                          => 'Foxway',
        'version'                       => Foxway_VERSION,
        'url'                           => 
'https://www.mediawiki.org/wiki/Extension:Foxway',
-       'author'                        => array( '[[mw:User:Pastakhov|Pavel 
Astakhov]]' ),
+       'author'                        => 
'[https://www.mediawiki.org/wiki/User:Pastakhov Pavel Astakhov]',
        'descriptionmsg'        => 'foxway-desc'
 );
 
@@ -42,9 +42,20 @@
  * @codeCoverageIgnore
  */
 $wgHooks['ParserFirstCallInit'][] = function( Parser &$parser ) {
-   //$parser->setFunctionHook( 'foxway', 'Foxway::renderParserFunction' );
+   $parser->setFunctionHook( 'foxway', 'Foxway::renderFunction', 
SFH_OBJECT_ARGS );
    $parser->setHook( 'foxway', 'Foxway::render' );
    return true;
+};
+
+/**
+ * @codeCoverageIgnore
+ */
+$wgHooks['ParserLimitReport'][] = function( $parser, &$limitReport ) {
+       if( Foxway::$time !== false ) {
+               $limitReport .= sprintf( "Foxway time usage: %.3f secs\n", 
Foxway::$time );
+               # print_r(Foxway\Runtime::getTime(),true);
+       }
+       return true;
 };
 
 // Preparing classes for autoloading
@@ -78,6 +89,13 @@
        'remoteExtPath' => 'Foxway'
 );
 
+$wgResourceModules['ext.Foxway.DebugLoops'] = array(
+       'scripts' => 'resources/ext.foxway.debugloops.js',
+       'dependencies' => 'jquery',
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'Foxway'
+);
+
 /**
  * Add files to phpunit test
  * @codeCoverageIgnore
diff --git a/Settings.php b/Settings.php
index 69ba5ca..57345a6 100644
--- a/Settings.php
+++ b/Settings.php
@@ -18,7 +18,8 @@
 }
 
 // Default settings
-
+$wgFoxway_max_execution_time = 2;
+$wgFoxway_max_execution_time_for_scope = 0.5;
 
 /**
  * You can specify the namespaces in which is allowed to use the extension 
Foxway.
@@ -117,6 +118,7 @@
                'each',
                'end',
                'in_array',
+               'key_exists',
                'key',
                'krsort',
                'ksort',
@@ -189,6 +191,7 @@
                'strlen',
                'strnatcasecmp',
                'strnatcmp',
+               'strncasecmp',
                'strncmp',
                'strpbrk',
                'strpos',
@@ -218,10 +221,9 @@
                'preg_grep',
                'preg_last_error',
                'preg_match_all',
-               'preg_match_all',
                'preg_match',
                'preg_quote',
-               //'preg_replace', not tested
+               //'preg_replace', not fully tested
                'preg_split',
        ),
        'FMath' => array( // Math Functions @see 
http://www.php.net/manual/en/ref.math.php
diff --git a/includes/Debug.php b/includes/Debug.php
index 437bfca..03634f6 100644
--- a/includes/Debug.php
+++ b/includes/Debug.php
@@ -11,6 +11,8 @@
 class Debug implements \ArrayAccess, iRawOutput {
 
        protected $_container = array();
+       private $loops = array();
+       private $closeTags = array();
 
        public function offsetExists($offset)
     {
@@ -78,6 +80,9 @@
                        case T_UNSET_CAST: // (unset)
                        case T_ECHO:
                        case T_PRINT:
+                       case T_CONTINUE:
+                       case T_BREAK:
+                       case T_WHILE:
                        case T_IF:
                        case T_ELSE:
                        case T_ELSEIF:
@@ -131,8 +136,35 @@
        public function __toString() {
                return \Html::rawElement( 'table', 
array('class'=>'foxway_debug'),
                                \Html::rawElement( 'tr', array(), 
\Html::element( 'th', array(), 'Debug view' ) ) .
-                               \Html::rawElement( 'tr', array(), 
\Html::rawElement( 'td', array(), implode('', $this->_container) ) )
+                               \Html::rawElement( 'tr', array(), 
\Html::rawElement( 'td', array(), implode('', $this->_container).implode('', 
$this->closeTags) ) )
                                );
        }
-}
 
+       public function setLoopState($state, $index = 0) {
+               switch ($state) {
+                       case FOXWAY_STATE_HEAD:
+                               if( !isset($this->loops[$index]) ) {
+                                       \Foxway::$DebugLoops = true;
+                                       $this->_container[] = 
\Html::openElement( 'table', array('class'=>'foxway_debug_loop') );
+                                       $this->closeTags[] = 
\Html::closeElement('table');
+                                       $this->loops[$index] = 0;
+                               }
+                               $this->loops[$index]++;
+                               $this->_container[] = \Html::openElement('tr', 
array('class'=>'foxway_debug_loophead')) . \Html::element('td', array(), 
$this->loops[$index]) . \Html::openElement('td');
+                               $this->closeTags[] = \Html::closeElement('td') 
. \Html::closeElement('tr');
+                               break;
+                       case FOXWAY_STATE_BODY:
+                               $this->_container[] = array_pop( 
$this->closeTags ) . \Html::openElement('tr', 
array('class'=>'foxway_debug_loopbody')) . \Html::element('td') . 
\Html::openElement('td');
+                               $this->closeTags[] = \Html::closeElement('td') 
. \Html::closeElement('tr');
+                               break;
+                       case FOXWAY_STATE_ENDBODY:
+                               $this->_container[] = array_pop( 
$this->closeTags );
+                               break;
+                       case FOXWAY_STATE_ENDLOOP:
+                               $this->_container[] = array_pop( 
$this->closeTags );
+                               array_pop($this->loops);
+                               break;
+               }
+       }
+
+}
diff --git a/includes/Interpreter.php b/includes/Interpreter.php
index 25e71aa..113c89e 100644
--- a/includes/Interpreter.php
+++ b/includes/Interpreter.php
@@ -5,6 +5,8 @@
 define( 'FOXWAY_ENDIF', 1 );
 define( 'FOXWAY_ELSE' , 2 );
 define( 'FOXWAY_VALUE', 3 );
+define( 'FOXWAY_START_LOOP', 4 );
+define( 'FOXWAY_PARENT_LOOP', 5);
 
 define( 'FOXWAY_ALLOW_PARENTHES_WITH_VOID_PARAMS', 1 << 0 );
 define( 'FOXWAY_EXPECT_CURLY_CLOSE', 1 << 1 );
@@ -25,6 +27,11 @@
 define( 'FOXWAY_EXPECT_GLOBAL_VARIABLE', 1 << 16 );
 define( 'FOXWAY_EXPECT_PARENTHES_WITH_VARIABLE_ONLY', 1 << 17 );
 define( 'FOXWAY_ALLOW_PARENTHES_WITH_VARIABLE_ONLY', 1 << 18 );
+
+define( 'FOXWAY_STATE_HEAD', 1 );
+define( 'FOXWAY_STATE_BODY', 2 );
+define( 'FOXWAY_STATE_ENDBODY', 3 );
+define( 'FOXWAY_STATE_ENDLOOP', 4 );
 
 /**
  * Interpreter class of Foxway extension.
@@ -52,6 +59,8 @@
                '%',
                '&',
                '|',
+               T_BOOLEAN_AND, // &&
+               T_BOOLEAN_OR, // ||
                '^',
                T_SL,                                           // <<
                T_SR,                                           // >>
@@ -118,25 +127,30 @@
        public static function run($source, array $args=array(), $scope='', 
$is_debug=false) {
                global $wgFoxwayAllowedPHPConstants;
 
-               $tokens = self::getTokens($source);
-
-               $return = array();
                $debug = $is_debug ? new Debug() : false;
-               $blocks = array();
-               $expected = false;
-               $parentheses = array();
-               $parenthesFlags = FOXWAY_EXPECT_SEMICOLON;
-               $curlyLever = 0;
-               $IfIndex = false;
-               $incrementVariable = false;
-               $commandResult = null;
-               $tokenLine = 1;
-
                if( $debug ) {
                        $runtime = new RuntimeDebug( $args, $scope );
                } else {
                        $runtime = new Runtime( $args, $scope );
                }
+               $r = $runtime->startTime($scope);
+               if( $r !== null ) {
+                       return array($r);
+               }
+
+               $return = array();
+               $tokens = self::getTokens($source); // @todo cache
+               $blocks = array(); // @todo cache
+               $expected = false;
+               $parentheses = array();
+               $parenthesFlags = FOXWAY_EXPECT_SEMICOLON;
+               $curlyLever = 0;
+               $ifIndex = false;
+               $incrementVariable = false;
+               $commandResult = null;
+               $tokenLine = 1;
+               $breakNumber = 0;
+               $loopContinue = false;
 
                $operators = $runtime->getOperators();
 
@@ -157,7 +171,6 @@
 
                        switch ($id) {
                                case ';':
-                                       $parenthesFlags = 
FOXWAY_EXPECT_SEMICOLON;
                                        if ( !($parenthesFlags & 
FOXWAY_EXPECT_SEMICOLON) ) {
                                                $return[] = new 
ErrorMessage(__LINE__, $tokenLine, E_PARSE, $id);
                                                break 2;
@@ -283,10 +296,13 @@
                                                        
FOXWAY_EXPECT_FUNCTION_PARENTHESES |
                                                        
FOXWAY_EXPECT_PARENTHES_WITH_DOUBLE_ARROW;
                                        break;
+                               case T_WHILE:
+                                       if( $debug ) { 
$debug->setLoopState(FOXWAY_STATE_HEAD, $index); }
+                                       // break is not necessary here
                                case T_IF:
                                        $parentheses[] = $parenthesFlags; 
//TODO check &= ~FOXWAY_EXPECT_SEMICOLON;
                                        $parenthesFlags = 
FOXWAY_EXPECT_FUNCTION_PARENTHESES;
-                                       $IfIndex = $index;
+                                       $ifIndex = $index;
                                        $expected = array('(');
                                        $runtime->addCommand($id);
                                        break;
@@ -294,6 +310,10 @@
                                        $runtime->addCommand($id);
                                        $parenthesFlags = 
FOXWAY_ALLOW_LIST_PARAMS | FOXWAY_EXPECT_SEMICOLON;
                                        break;
+                               case T_CONTINUE:
+                               case T_BREAK:
+                                       $ifIndex = $index;
+                                       // break is not necessary here
                                case T_PRINT:
                                        $runtime->addCommand($id);
                                        $parenthesFlags = 
FOXWAY_EXPECT_SEMICOLON;
@@ -454,14 +474,53 @@
                                                case T_PRINT:
                                                        $return = 
array_merge($return, $result);
                                                        break;
-                                               case T_IF:
-                                                       if( 
!isset($blocks[$IfIndex]) ) {
-                                                               if( 
self::findIfElseIndexes($tokens, $blocks, $IfIndex, $index) !== true ) {
+                                               case T_CONTINUE:
+                                               case T_BREAK:
+                                                       $result = 
isset($result[0]) ? intval($result[0]) : 0 ;
+                                                       if( $result > 1 ) {
+                                                               $result--;
+                                                       }
+                                                       if( $command == T_BREAK 
) { // For 'break N' command
+                                                               $breakNumber = 
$result+1; // $breakNumber = N;
+                                                               $loopContinue = 
false;
+                                                       } elseif( $result > 0 ) 
{ // For 'continue N > 1' command
+                                                               $breakNumber = 
$result; // $breakNumber = N-1;
+                                                               $loopContinue = 
true;  // and mark loop continue
+                                                       } // nothing for 
command 'continue 1'
+                                                       if( 
!isset($blocks[$ifIndex][FOXWAY_ENDBLOCK]) ) {
+                                                               $return[] = new 
ErrorMessage( __LINE__, $tokenLine, E_ERROR, 
array('foxway-php-fatal-error-cannot-break-continue', $text, 
isset($args[0])?$args[0]:'n\a') );
+                                                               break 2;
+                                                       }
+                                                       $index = 
$blocks[$ifIndex][FOXWAY_ENDBLOCK]-1;
+                                                       break;
+                                               case T_WHILE:
+                                                       if( 
!isset($blocks[$ifIndex]) ) {
+                                                               if( 
self::findLoopIndexes($tokens, $blocks, $ifIndex, $index) !== true ) {
                                                                        
$return[] = new ErrorMessage(__LINE__, $tokenLine, E_PARSE, '$end');
                                                                        break 2;
                                                                }
                                                        }
-                                                       $curBlock = 
$blocks[$IfIndex];
+                                                       $expected = false;
+                                                       if( $result == false ) 
{ // if true - just go next
+                                                               $curBlock = 
$blocks[$ifIndex];
+                                                               $index = 
$curBlock[FOXWAY_ENDBLOCK];
+                                                               $commandResult 
= null;
+                                                               if( $debug ) {
+                                                                       
$debug[] = 'skip';
+                                                                       
$debug->setLoopState(FOXWAY_STATE_ENDLOOP, $ifIndex);
+                                                               }
+                                                               continue 2;
+                                                       }
+                                                       if( $debug ) { 
$debug->setLoopState(FOXWAY_STATE_BODY, $ifIndex); }
+                                                       break;
+                                               case T_IF:
+                                                       if( 
!isset($blocks[$ifIndex]) ) {
+                                                               if( 
self::findIfElseIndexes($tokens, $blocks, $ifIndex, $index) !== true ) {
+                                                                       
$return[] = new ErrorMessage(__LINE__, $tokenLine, E_PARSE, '$end');
+                                                                       break 2;
+                                                               }
+                                                       }
+                                                       $curBlock = 
$blocks[$ifIndex];
                                                        $elseIndex = 
$curBlock[FOXWAY_ELSE];
                                                        if( $result ) {
                                                                if( $elseIndex 
) {
@@ -490,7 +549,7 @@
                                                case T_ELSEIF:
                                                        if( $result ) { // chek 
for IF
                                                                // this code 
from previus swith( $id ) case T_IF: & case T_ECHO:
-                                                               $IfIndex = 
$index;
+                                                               $ifIndex = 
$index;
                                                                $expected = 
array('(');
                                                                
$runtime->addCommand(T_IF);
                                                                break;
@@ -514,6 +573,9 @@
                                                                }
                                                                continue 2;
                                                        }
+                                                       break;
+                                               default :
+                                                       // @todo error message ?
                                                        break;
                                        }
                                }elseif( $commandResult instanceof ErrorMessage 
) {
@@ -564,6 +626,8 @@
                                        // break is not necessary here
                                case T_ECHO:
                                case T_PRINT:
+//                             case T_CONTINUE:
+//                             case T_BREAK:
                                case ',':
                                case T_CONCAT_EQUAL:    // .=
                                case T_PLUS_EQUAL:              // +=
@@ -585,6 +649,8 @@
                                case '%':
                                case '&':
                                case '|':
+                               case T_BOOLEAN_AND:
+                               case T_BOOLEAN_OR:
                                case '^':
                                case T_SL: // <<
                                case T_SR: // >>
@@ -623,7 +689,7 @@
                                        }
                                        break;
                                case '}':
-                                       if( $parenthesFlags & 
FOXWAY_EXPECT_CURLY_CLOSE ) {
+                                       if( $parenthesFlags & 
FOXWAY_EXPECT_CURLY_CLOSE ) { // This is '}' (CURLY CLOSE) for T_CURLY_OPEN
                                                $parenthesFlags = 
array_pop($parentheses);
                                                $expected = array(
                                                        
T_CONSTANT_ENCAPSED_STRING,
@@ -634,9 +700,29 @@
                                                        T_CURLY_OPEN,
                                                        '"',
                                                        );
-                                       } elseif( $curlyLever ) {
+                                       } elseif( $curlyLever ) { // This is 
mark of end block
                                                $curlyLever--;
-                                       }else {
+                                               if( isset($blocks[$index]) && 
isset($blocks[$index][FOXWAY_START_LOOP]) ) { // This is end of loop
+                                                       if( $debug ) { 
$debug->setLoopState(FOXWAY_STATE_ENDBODY); }
+                                                       if( $breakNumber > 1 || 
($breakNumber==1 && $loopContinue) ) { // This is 'break > 1' or 'continue > 1' 
- go to end of parent loop
+                                                               $breakNumber--;
+                                                               if( 
!isset($blocks[$index][FOXWAY_PARENT_LOOP][FOXWAY_ENDBLOCK]) ) {
+                                                                       
$return[] = new ErrorMessage( __LINE__, $tokenLine, E_ERROR, 
array('foxway-php-fatal-error-cannot-break-continue', $text, 
isset($args[0])?$args[0]:'n\a') );
+                                                                       break 2;
+                                                               }
+                                                               $index = 
$blocks[$index][FOXWAY_PARENT_LOOP][FOXWAY_ENDBLOCK]-1;
+                                                               if( $debug ) { 
$debug->setLoopState(FOXWAY_STATE_ENDLOOP); }
+                                                               continue 2;
+                                                       }
+                                                       if( $breakNumber != 1 ) 
{ // This is not 'break 1' (possibly 'continue 1') - go to start loop
+                                                               $index = 
$blocks[$index][FOXWAY_START_LOOP]-1;
+                                                               continue 2;
+                                                       }
+                                                       //This is 'break 1' - 
just go next, out of this loop
+                                                       $breakNumber--;
+                                                       if( $debug ) { 
$debug->setLoopState(FOXWAY_STATE_ENDLOOP); }
+                                               }
+                                       } else { // This is mark of end block 
without mark of start block
                                                $return[] = new 
ErrorMessage(__LINE__, $tokenLine, E_PARSE, $id);
                                                break 2;
                                        }
@@ -663,6 +749,11 @@
 
                        /*****************   EXPECT  PHASE  TWO  
**************************/
                        switch ($id) {
+                               case ';':
+                                       if( 
isset($blocks[$index][FOXWAY_START_LOOP]) ) {
+                                               $index = 
$blocks[$index][FOXWAY_START_LOOP]-1;
+                                       }
+                                       break;
                                case T_ARRAY:
                                        $parenthesFlags |= 
FOXWAY_ALLOW_PARENTHES_WITH_VOID_PARAMS;
                                        break;
@@ -705,6 +796,7 @@
                if( $debug ) {
                        array_unshift($return, $debug);
                }
+               $runtime->stopTime($scope);
                return $return;
        }
 
@@ -730,7 +822,6 @@
                static $replacement = array('$1"', "$1\n", "$1\r", "$1\t", 
"$1\v", '$1$', '\\');
                return preg_replace($pattern, $replacement, $string);
        }
-
 
        private static function findTernaryIndexes( &$tokens, &$blocks, $index 
) {
                $embedded = 0;
@@ -786,7 +877,61 @@
                }
        }
 
-       private static function findIfElseIndexes(&$tokens, &$blocks, $ifIndex, 
$index) {
+       public static function findLoopIndexes( &$tokens, &$blocks, $ifIndex, 
$index, &$parentLoop = false ) {
+               $count = count($tokens);
+               $nestedBlocks = 0;
+               for( $i = $index; $i < $count; $i++ ) { // find end of block
+                       $token = $tokens[$i];
+                       if ( is_string($token) ) {
+                               $id = $token;
+                       } else {
+                               list($id, , $tokenLine) = $token;
+                       }
+                       switch ($id) {
+                               case '{':
+                               case T_CURLY_OPEN:
+                                       $nestedBlocks++;
+                                       break;
+                               case '}':
+                                       $nestedBlocks--;
+                                       // break is not necessary here
+                               case ';':
+                                       if($nestedBlocks == 0 ) {
+                                               break 2;
+                                       }
+                                       break;
+                               case T_WHILE:
+                                       if( 
/*!isset($blocks[$i][FOXWAY_ENDBLOCK]) &&*/ self::findLoopIndexes($tokens, 
$blocks, $i, self::findLastParenthesis($tokens, $i), $blocks[$ifIndex]) !== 
true ) {
+                                               return false;
+                                       }
+                                       $i = $blocks[$i][FOXWAY_ENDBLOCK];
+                                       if( $nestedBlocks == 0 ) { break 2;     
}
+                                       break;
+                               case T_IF:
+                                       if( /*!isset($blocks[$i][FOXWAY_ENDIF]) 
&&*/ self::findIfElseIndexes($tokens, $blocks, $i, 
self::findLastParenthesis($tokens, $i), $blocks[$ifIndex]) !== true ) {
+                                               return false;
+                                       }
+                                       $i = $blocks[$i][FOXWAY_ENDIF];
+                                       if( $nestedBlocks == 0 ) { break 2;     
}
+                                       break;
+                               case T_CONTINUE:
+                               case T_BREAK:
+                                       $blocks[$i] = &$blocks[$ifIndex];
+                                       break;
+                       }
+               }
+               if( $i >= $count ) {
+                       return false;
+               }
+               $blocks[$ifIndex][FOXWAY_ENDBLOCK] = $i;
+               $blocks[$i][FOXWAY_START_LOOP] = $ifIndex;
+               if( $parentLoop !== false ) {
+                       $blocks[$i][FOXWAY_PARENT_LOOP] = &$parentLoop;
+               }
+               return true;
+       }
+
+       private static function findIfElseIndexes( &$tokens, &$blocks, 
$ifIndex, $index, &$parentLoop = false) {
                $count = count($tokens);
 
                $nestedBlocks = 0;
@@ -805,22 +950,29 @@
                                        $nestedBlocks--;
                                        // break is not necessary here
                                case ';':
-                                       if($nestedBlocks == 0 ) {
-                                               break 2;
-                                       }
+                                       if($nestedBlocks == 0 ) { break 2; }
                                        break;
                                case T_IF:
-                                       if( $nestedBlocks == 0 ) {
-                                               if( 
!isset($blocks[$i][FOXWAY_ENDIF]) && self::findIfElseIndexes($tokens, $blocks, 
$i, self::findLastParenthesis($tokens, $i)) !== true ) {
-                                                       return false;
-                                               }
-                                               $i = $blocks[$i][FOXWAY_ENDIF];
-                                               break 2;
+                                       if( /*!isset($blocks[$i][FOXWAY_ENDIF]) 
&&*/ self::findIfElseIndexes($tokens, $blocks, $i, 
self::findLastParenthesis($tokens, $i), $parentLoop) !== true ) {
+                                               return false;
                                        }
+                                       $i = $blocks[$i][FOXWAY_ENDIF];
+                                       if( $nestedBlocks == 0 ) { break 2;     
}
+                                       break;
+                               case T_WHILE:
+                                       if( 
/*!isset($blocks[$i][FOXWAY_ENDBLOCK]) &&*/ self::findLoopIndexes($tokens, 
$blocks, $i, self::findLastParenthesis($tokens, $i), $parentLoop) !== true ) {
+                                               return false;
+                                       }
+                                       $i = $blocks[$i][FOXWAY_ENDBLOCK];
+                                       if( $nestedBlocks == 0 ) { break 2;     
}
+                                       break;
+                               case T_CONTINUE:
+                               case T_BREAK:
+                                       $blocks[$i] = &$parentLoop;
                                        break;
                        }
                }
-               if( $i == $count ) {
+               if( $i >= $count ) {
                        return false; // end of block not find
                }
                $blocks[$ifIndex][FOXWAY_ENDBLOCK] = $i;
@@ -839,7 +991,7 @@
                                case T_WHITESPACE:
                                        break; // ignore it
                                case T_ELSEIF:
-                                       if( !isset($blocks[$i]) && 
self::findIfElseIndexes($tokens, $blocks, $i, 
self::findLastParenthesis($tokens, $i)) !== true ) {
+                                       if( !isset($blocks[$i]) && 
self::findIfElseIndexes($tokens, $blocks, $i, 
self::findLastParenthesis($tokens, $i), $parentLoop) !== true ) {
                                                return false;
                                        }
                                        $blocks[$ifIndex][FOXWAY_ELSE] = $i; // 
We fount T_ELSE or T_ELSEIF
@@ -850,7 +1002,7 @@
                                        $i = $blocks[$i][FOXWAY_ENDBLOCK];
                                        break;
                                case T_ELSE:
-                                       if( !isset($blocks[$i]) && 
self::findIfElseIndexes($tokens, $blocks, $i, $i+1) !== true ) {
+                                       if( !isset($blocks[$i]) && 
self::findIfElseIndexes($tokens, $blocks, $i, $i+1, $parentLoop) !== true ) {
                                                return false;
                                        }
                                        $blocks[$ifIndex][FOXWAY_ELSE] = $i; // 
We fount T_ELSE or T_ELSEIF
diff --git a/includes/Runtime.php b/includes/Runtime.php
index 5860ed3..4c4fac6 100644
--- a/includes/Runtime.php
+++ b/includes/Runtime.php
@@ -32,6 +32,8 @@
        protected static $variables = array();
        protected static $staticVariables = array();
        protected static $globalVariables = array();
+       protected static $time = array();
+       protected static $startTime = array();
        protected $thisVariables;
        protected $args;
        protected $scope;
@@ -53,8 +55,8 @@
                array('&'),
                array('^'),
                array('|'),
-               array('&&'),
-               array('||'),
+               array(T_BOOLEAN_AND), // &&
+               array(T_BOOLEAN_OR), // ||
                array('?', ':'),
                //                              +=                      -=      
                        *=                      /=                      .=      
                        %=                              &=                      
|=                      ^=                      <<=                     >>=     
                        =>
                array('=', T_PLUS_EQUAL, T_MINUS_EQUAL, T_MUL_EQUAL, 
T_DIV_EQUAL, T_CONCAT_EQUAL, T_MOD_EQUAL, T_AND_EQUAL, T_OR_EQUAL, T_XOR_EQUAL, 
T_SL_EQUAL, T_SR_EQUAL, T_DOUBLE_ARROW),
@@ -70,6 +72,7 @@
                if( !isset(self::$variables[$scope]) ) {
                        self::$variables[$scope] = array();
                }
+               $this->scope = $scope;
                $this->thisVariables = &self::$variables[$scope];
                $this->thisVariables['argv'] = $args;
                $this->thisVariables['argc'] = count($args);
@@ -251,16 +254,23 @@
                                }
                                // break is not necessary here
                        case ')':
+                               $return = $this->checkExceedsTime();
+                               if( $return !== null ) {
+                                       $this->lastCommand = false;
+                               }
                                $this->parenthesesClose();
-                               $return = null;
                                switch ($this->lastCommand) {
                                        case false:
                                        case T_ECHO:
                                        case T_PRINT:
+                                       case T_CONTINUE:
+                                       case T_BREAK:
                                                break 2;
+                                       case T_WHILE:
                                        case T_IF:
-                                               $this->lastCommand = false;
-                                               return array( T_IF, 
$this->lastParam->getValue() );
+                                               $return = array( 
$this->lastCommand, $this->lastParam->getValue() );
+                                               //$this->lastCommand = false;
+                                               //return $return;
                                                break;
                                        case T_ARRAY:
                                                $this->lastParam = new RValue( 
(array)$this->lastParam );
@@ -415,6 +425,12 @@
                        case '^':
                                $this->lastParam = new RValue( 
$param->getValue() ^ $lastParam );
                                break;
+                       case T_BOOLEAN_AND: // &&
+                               $this->lastParam = new RValue( 
$param->getValue() && $lastParam );
+                               break;
+                       case T_BOOLEAN_OR: // ||
+                               $this->lastParam = new RValue( 
$param->getValue() || $lastParam );
+                               break;
                        case T_SL: // <<
                                $this->lastParam = new RValue( 
$param->getValue() << $lastParam );
                                break;
@@ -474,16 +490,21 @@
 
        // Remember the child class RuntimeDebug
        public function getCommandResult( ) {
+               $return = $this->checkExceedsTime();
+               if( $return !== null ) {
+                       return $return;
+               }
                if( $this->lastParam !== null ) {
                        $this->addOperator(',');
                }
-               $return = null;
 
                // Remember the child class RuntimeDebug
                switch ($this->lastCommand) {
                        case T_ECHO:
                        case T_PRINT:
-                               $return = array( T_ECHO, $this->listParams );
+                       case T_CONTINUE:
+                       case T_BREAK:
+                               $return = array( $this->lastCommand, 
$this->listParams );
                                break;
                        case false:
                                break; // exsample: $foo = 'foobar';
@@ -542,4 +563,29 @@
                return $return;
        }
 
+       public function startTime($scope) {
+               self::$startTime[$scope] = microtime(true);
+               if( isset(self::$time[$scope]) ) {
+                       return $this->checkExceedsTime();
+               }else{
+                       self::$time[$scope] = 0;
+               }
+       }
+
+       public function stopTime($scope) {
+               self::$time[$scope] += microtime(true) - 
self::$startTime[$scope];
+       }
+
+       public static function getTime() {
+               return self::$time;
+       }
+
+       public function checkExceedsTime() {
+               global $wgFoxway_max_execution_time_for_scope;
+               if( microtime(true) - self::$startTime[$this->scope] + 
self::$time[$this->scope] > $wgFoxway_max_execution_time_for_scope ) {
+                       return new ErrorMessage( __LINE__, null, E_ERROR, 
array( 'foxway-php-fatal-error-max-execution-time-scope', 
$wgFoxway_max_execution_time_for_scope, 
isset($this->args[0])?$this->args[0]:'n\a' ) );
+               }
+               return null;
+       }
+
 }
diff --git a/includes/RuntimeDebug.php b/includes/RuntimeDebug.php
index 75cb095..670f90f 100644
--- a/includes/RuntimeDebug.php
+++ b/includes/RuntimeDebug.php
@@ -103,9 +103,12 @@
                                case T_ARRAY:
                                case T_ECHO:
                                case T_PRINT:
+                               case T_CONTINUE:
+                               case T_BREAK:
                                        break;
+                               case T_WHILE:
                                case T_IF:
-                                       $this->debug[] = 
self::getHTMLForCommand(T_IF) .
+                                       $this->debug[] = 
self::getHTMLForCommand( $this->lastCommandDebug ) .
                                                        "(&nbsp;" . 
self::getHTMLForValue( $this->lastParam ) .
                                                        
"&nbsp;)&nbsp;<b>=></b>&nbsp;" .
                                                        self::getHTMLForValue( 
new RValue($this->lastParam->getValue() ? true : false) );
@@ -119,12 +122,19 @@
                $lastCommand = $this->lastCommand;
 
                $return = parent::getCommandResult();
+               if( $return instanceof ErrorMessage ){
+                       $lastCommand = false;
+               }
 
                switch ($lastCommand) {
                        case T_ECHO:
                        case T_PRINT:
                                $this->debug[] = 
self::getHTMLForCommand($lastCommand) . '&nbsp;' . implode(', ', 
$this->savedListParams) . ';';
                                $this->debug[] = implode('', $return[1]);
+                               break;
+                       case T_CONTINUE:
+                       case T_BREAK:
+                               $this->debug[] = 
self::getHTMLForCommand($lastCommand) . '&nbsp;' . implode(', ', 
$this->savedListParams) . ';';
                                break;
                        default :
                                $this->debug[] = is_array($return) ? $return[1] 
: $return ;
@@ -203,6 +213,15 @@
                                break;
                        case T_PRINT:
                                $return = \Html::element('span', 
array('class'=>'foxway_construct'), 'print');
+                               break;
+                       case T_CONTINUE:
+                               $return = \Html::element('span', 
array('class'=>'foxway_construct'), 'continue');
+                               break;
+                       case T_BREAK:
+                               $return = \Html::element('span', 
array('class'=>'foxway_construct'), 'break');
+                               break;
+                       case T_WHILE:
+                               $return = \Html::element('span', 
array('class'=>'foxway_construct'), 'while');
                                break;
                        case T_IF:
                                $return = \Html::element('span', 
array('class'=>'foxway_construct'), 'if');
@@ -284,6 +303,27 @@
                        case T_IS_NOT_IDENTICAL: // !==
                                $operator = '!==';
                                break;
+                       case T_SL: // <<
+                               $operator = '<<';
+                               break;
+                       case T_SR: // >>
+                               $operator= '>>';
+                               break;
+                       case T_BOOLEAN_AND: // &&
+                               $operator = '&&';
+                               break;
+                       case T_BOOLEAN_OR: // ||
+                               $operator = '||';
+                               break;
+                       case T_LOGICAL_AND:
+                               $operator = 'and';
+                               break;
+                       case T_LOGICAL_XOR:
+                               $operator = 'xor';
+                               break;
+                       case T_LOGICAL_OR:
+                               $operator = 'or';
+                               break;
                        case T_INT_CAST:
                                $operator = \Html::element('span', 
array('class'=>'foxway_construct'), '(integer)');
                                break;
@@ -312,7 +352,7 @@
                                "(" . implode( ', ', $listParams ) .    
")&nbsp;<b>=></b>&nbsp;" .
                                self::getHTMLForValue( $this->lastParam );
 
-               return  $return;
+               return $return;
        }
 
 }
diff --git a/includes/functions/FArray.php b/includes/functions/FArray.php
index 289dfb5..09057c9 100644
--- a/includes/functions/FArray.php
+++ b/includes/functions/FArray.php
@@ -69,6 +69,7 @@
                'f_end' => array('end', 1, 1),
                // @todo extract (if there is a need in this)
                'f_in_array' => array('in_array', 2, 3),
+               'f_key_exists' => array('array_key_exists', 2, 2), // Alias of 
array_key_exists()
                'f_key' => array('key', 1, 1),
                'f_krsort' => array('krsort', 1, 2),
                'f_ksort' => array('ksort', 1, 2),
diff --git a/resources/ext.foxway.debug.css b/resources/ext.foxway.debug.css
index 15abc91..ab59200 100644
--- a/resources/ext.foxway.debug.css
+++ b/resources/ext.foxway.debug.css
@@ -10,6 +10,23 @@
        background-color: #f9f9f9;
 }
 
+.foxway_debug_loop {
+       width: auto;
+       border: 1px solid gray;
+       border-spacing: 0;
+}
+
+.foxway_debug_loop td {
+       padding: 0px;
+       border: 0px;
+       background-color: transparent;
+}
+
+.foxway_debug_loophead {
+       background-color: antiquewhite;
+       cursor: pointer;
+}
+
 .foxway_comment,
 .foxway_skipped {
        color:#969696;
diff --git a/resources/ext.foxway.debugloops.js 
b/resources/ext.foxway.debugloops.js
new file mode 100644
index 0000000..cd1ea97
--- /dev/null
+++ b/resources/ext.foxway.debugloops.js
@@ -0,0 +1,10 @@
+(function ($) {
+
+       $().ready(function () {
+                $('.foxway_debug_loopbody').hide();
+                $('.foxway_debug_loophead').click(function () {
+                                       $(this).next().toggle();
+                });
+            });
+
+})(window.jQuery);
diff --git a/tests/phpunit/includes/InterpreterTest.php 
b/tests/phpunit/includes/InterpreterTest.php
index d443795..2d5c4b1 100644
--- a/tests/phpunit/includes/InterpreterTest.php
+++ b/tests/phpunit/includes/InterpreterTest.php
@@ -2,6 +2,9 @@
 
 namespace Foxway;
 
+$wgFoxway_max_execution_time = 20;
+$wgFoxway_max_execution_time_for_scope = 20;
+
 /**
  * Generated by PHPUnit_SkeletonGenerator 1.2.0 on 2013-03-28 at 05:28:38.
  */
@@ -2014,6 +2017,103 @@
                                );
        }
 
+       public function testRun_while_1() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 3 ) { echo 
$i++; }'),
+                               array('1', '2', '3')
+                               );
+       }
+       public function testRun_while_2() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 3 ) echo 
$i++;'),
+                               array('1', '2', '3')
+                               );
+       }
+       public function testRun_while_continue_1() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 3 ) { echo 
$i++; continue; $i++ }'),
+                               array('1', '2', '3')
+                               );
+       }
+       public function testRun_while_break_1() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 33 ) { 
echo $i++; break; $i++ }'),
+                               array('1')
+                               );
+       }
+       public function testRun_while_if_continue_1() {
+               $this->assertEquals(
+                               Interpreter::run('$i=0; while( $i <= 2 ) { 
$i++; if($i == 2) continue; echo $i; }'),
+                               array('1', '3')
+                               );
+       }
+       public function testRun_while_if_continue_2() {
+               $this->assertEquals(
+                               Interpreter::run('$i=0; while( $i <= 2 ) { 
$i++; if($i == 2) { echo "Two"; continue; } echo $i; }'),
+                               array('1', 'Two', '3')
+                               );
+       }
+       public function testRun_while_if_break_1() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 33 ) { 
echo $i++; if($i == 3) break; }'),
+                               array('1', '2')
+                               );
+       }
+       public function testRun_while_if_break_2() {
+               $this->assertEquals(
+                               Interpreter::run('$i=1; while( $i <= 33 ) { 
echo $i++; if($i == 3){echo "The end"; break; echo "anything";} }'),
+                               array('1', '2', 'The end')
+                               );
+       }
+       public function testRun_while_while_1() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=3 
){ echo "|$i|"; $y=1; while( $y<=$i ){ echo "($y)"; $y++; } $i++; }') ),
+                               '|1|(1)|2|(1)(2)|3|(1)(2)(3)'
+                               );
+       }
+       public function testRun_while_while_continue_1() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=3 
){ echo "|$i|"; $y=0; while( $y<3 ){ $y++; if( $y==2) continue; echo "($y)";  } 
$i++; }') ),
+                               '|1|(1)(3)|2|(1)(3)|3|(1)(3)'
+                               );
+       }
+       public function testRun_while_while_continue_2() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=0; while( $i<5 ){ 
$i++; echo "|$i|"; $y=0; while( $y<3 ){ $y++; if( $y==$i ){ continue 2; } echo 
"($y)";  } }') ),
+                               '|1||2|(1)|3|(1)(2)|4|(1)(2)(3)|5|(1)(2)(3)'
+                               );
+       }
+       public function testRun_while_while_break_1() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=5 
){ echo "|$i|"; $y=0; while( $y<3 ){ $y++; if( $y==2) break; echo "($y)";  } 
$i++; }') ),
+                               '|1|(1)|2|(1)|3|(1)|4|(1)|5|(1)'
+                               );
+       }
+       public function testRun_while_while_break_2() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=5 
){ echo "|$i|"; $y=0; while( $y<4 && $y<$i ){ $y++; if( $y==3) { break 2; echo 
"hohoho"; } echo "($y)";  } $i++; }') ),
+                               '|1|(1)|2|(1)(2)|3|(1)(2)'
+                               );
+       }
+       public function testRun_while_if_while_1() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=5 
){ echo "|$i|"; $y=0; if( $i==2 || $i == 4 ) while( $y<$i ){ $y++; echo "($y)"; 
} $i++; } ') ),
+                               '|1||2|(1)(2)|3||4|(1)(2)(3)(4)|5|'
+                               );
+       }
+       public function testRun_while_if_while_2() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=5 
){ echo "|$i|"; $y=0; if( $i==2 || $i == 4 ) { echo "."; while( $y<$i ){ $y++; 
echo "($y)"; } } $i++; } ') ),
+                               '|1||2|.(1)(2)|3||4|.(1)(2)(3)(4)|5|'
+                               );
+       }
+       public function testRun_while_if_while_if_1() {
+               $this->assertEquals(
+                               implode( Interpreter::run('$i=1; while( $i<=5 
){ echo "|$i|"; $y=0; if( $i==2 || $i == 4 ) { echo "."; while( $y<$i ){ $y++; 
if($y < 3) echo "($y)"; else break 2; } } $i++; } ') ),
+                               '|1||2|.(1)(2)|3||4|.(1)(2)'
+                               );
+       }
+
 }
 
 // @todo echo is_scalar(array("foo","bar") ? "true" : "false"; // most return 
error

-- 
To view, visit https://gerrit.wikimedia.org/r/69836
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I426f3b2048205a920fd5e11596b09b4cc640f240
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/Foxway
Gerrit-Branch: master
Gerrit-Owner: Pastakhov <[email protected]>
Gerrit-Reviewer: Pastakhov <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to