Version: current

      1 <?php
      2 /**
      3  * Allows human reading of a test in a format suitable for agile documentation
      4  *
      5  * @package sapphire
      6  * @subpackage tools
      7  */
      8 class CodeViewer extends Controller {
      9
     10     public static $url_handlers = array (
     11         ''       => 'browse',
     12         '$Class' => 'viewClass'
     13     );
     14
     15     /**
     16      * Define a simple finite state machine.
     17      * Top keys are the state names.  'start' is the first state, and 'die' is the error state.
     18      * Inner keys are token names/codes.  The values are either a string, new state, or an array(new state, handler method).
     19      * The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
     20      */
     21     static $fsm = array(
     22         'start' => array(
     23             T_CLASS => array('className','createClass'),
     24             T_DOC_COMMENT => array('', 'saveClassComment'),
     25         ),
     26         'className' => array(
     27             T_STRING => array('classSpec', 'setClassName'),
     28         ),
     29         'classSpec' => array(
     30             '{' => 'classBody',
     31         ),
     32         'classBody' => array(
     33             T_FUNCTION => array('methodName','createMethod'),
     34             '}' => array('start', 'completeClass'),
     35             T_DOC_COMMENT => array('', 'saveMethodComment'),
     36         ),
     37         'methodName' => array(
     38             T_STRING => array('methodSpec', 'setMethodName'),
     39         ),
     40         'methodSpec' => array(
     41             '{' => 'methodBody',
     42         ),
     43         'methodBody' => array(
     44             '{' => array('!push','appendMethodContent'),
     45             '}' => array(
     46                 'hasstack' => array('!pop', 'appendMethodContent'),
     47                 'nostack' => array('classBody', 'completeMethod'),
     48             ),
     49             T_VARIABLE => array('variable', 'potentialMethodCall'),
     50             T_COMMENT => array('', 'appendMethodComment'),
     51             T_DOC_COMMENT => array('', 'appendMethodComment'),
     52             '*' => array('', 'appendMethodContent'),
     53         ),
     54         'variable' => array(
     55             T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
     56             '*' => array('methodBody', 'appendMethodContent'),
     57         ),
     58         'variableArrow' => array(
     59             T_STRING => array('methodOrProperty', 'potentialMethodCall'),
     60             T_WHITESPACE => array('', 'potentialMethodCall'),
     61             '*' => array('methodBody', 'appendMethodContent'),
     62         ),
     63         'methodOrProperty' => array(
     64             '(' => array('methodCall', 'potentialMethodCall'),
     65             T_WHITESPACE => array('', 'potentialMethodCall'),
     66             '*' => array('methodBody', 'appendMethodContent'),
     67         ),
     68         'methodCall' => array(
     69             '(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
     70             ')' => array('methodBody', 'completeMethodCall'),
     71             '*' => array('', 'potentialMethodCall'),
     72         ),
     73         'nestedInMethodCall' => array(
     74             '(' => array('!push', 'potentialMethodCall'),
     75             ')' => array('!pop', 'potentialMethodCall'),
     76             '*' => array('', 'potentialMethodCall'),
     77         ),
     78     );
     79
     80     function init() {
     81         parent::init();
     82
     83         if(!Permission::check('ADMIN')) return Security::permissionFailure();
     84         ManifestBuilder::load_test_manifest();
     85     }
     86
     87     public function browse() {
     88         $classes = ClassInfo::subclassesFor('SapphireTest');
     89
     90         array_shift($classes);
     91         ksort($classes);
     92
     93         $result  ='<h1>View any of the following test classes</h1>';
     94         $result .='<ul>';
     95         foreach($classes as $class) {
     96             $result .="<li><a href=\"{$this->Link($class)}\">$class</a></li>";
     97         }
     98         $result .='</ul>';
     99
    100         $result .='<h1>View any of the following other classes</h1>';
    101
    102         $classes = array_keys(ClassInfo::allClasses());
    103         sort($classes);
    104
    105         $result .='<ul>';
    106         foreach($classes as $class) {
    107             $result .="<li><a href=\"{$this->Link($class)}\">$class</a></li>";
    108         }
    109         $result .='</ul>';
    110
    111         return $this->customise(array (
    112             'Content' => $result
    113         ))->renderWith('CodeViewer');
    114     }
    115
    116     public function viewClass(SS_HTTPRequest $request) {
    117         $class = $request->param('Class');
    118
    119         if(!class_exists($class)) {
    120             throw new Exception('CodeViewer->viewClass(): not passed a valid class to view (does the class exist?)');
    121         }
    122
    123         return $this->customise(array (
    124             'Content' => $this->testAnalysis(getClassFile($class))
    125         ))->renderWith('CodeViewer');
    126     }
    127
    128     public function Link($action = null) {
    129         return Controller::join_links(Director::absoluteBaseURL(), 'dev/viewcode/', $action);
    130     }
    131
    132     protected $classComment, $methodComment;
    133
    134     function saveClassComment($token) {
    135         $this->classComment = $this->parseComment($token);
    136     }
    137     function saveMethodComment($token) {
    138         $this->methodComment = $this->parseComment($token);
    139     }
    140
    141     function createClass($token) {
    142         $this->currentClass = array(
    143             "description" => $this->classComment['pretty'],
    144             "heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null,
    145         );
    146         $ths->classComment = null;
    147     }
    148     function setClassName($token) {
    149         $this->currentClass['name'] = $token[1];
    150         if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1];
    151     }
    152     function completeClass($token) {
    153         $this->classes[] = $this->currentClass;
    154     }
    155
    156     function createMethod($token) {
    157         $this->currentMethod = array();
    158         $this->currentMethod['content'] = "<pre>";
    159         $this->currentMethod['description'] = $this->methodComment['pretty'];
    160         $this->currentMethod['heading'] = isset($this->methodComment['heading']) ? $this->methodComment['heading'] : null;
    161         $this->methodComment = null;
    162
    163     }
    164     function setMethodName($token) {
    165         $this->currentMethod['name'] = $token[1];
    166         if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
    167     }
    168     function appendMethodComment($token) {
    169         if(substr($token[1],0,2) == '/*') {
    170             $this->closeOffMethodContentPre();
    171             $this->currentMethod['content'] .= $this->prettyComment($token) . "<pre>";
    172         } else {
    173             $this->currentMethod['content'] .= $this->renderToken($token);
    174         }
    175     }
    176
    177     function prettyComment($token) {
    178         $comment = preg_replace('/^\/\*/','',$token[1]);
    179         $comment = preg_replace('/\*\/$/','',$comment);
    180         $comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
    181         $comment = htmlentities($comment);
    182         $comment = str_replace("\n\n", "</p><p>", $comment);
    183         return "<p>$comment</p>";
    184     }
    185
    186     function parseComment($token) {
    187         $parsed = array();
    188
    189         $comment = preg_replace('/^\/\*/','',$token[1]);
    190         $comment = preg_replace('/\*\/$/','',$comment);
    191         $comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
    192
    193         foreach(array('heading','nav') as $var) {
    194             if(preg_match('/@' . $var . '\s+([^\n]+)\n/', $comment, $matches)) {
    195                 $parsed[$var] = $matches[1];
    196                 $comment = preg_replace('/@' . $var . '\s+([^\n]+)\n/','', $comment);
    197             }
    198         }
    199
    200         $parsed['pretty'] = "<p>" . str_replace("\n\n", "</p><p>", htmlentities($comment)). "</p>";
    201         return $parsed;
    202     }
    203
    204     protected $isNewLine = true;
    205
    206     function appendMethodContent($token) {
    207         if($this->potentialMethodCall) {
    208             $this->currentMethod['content'] .= $this->potentialMethodCall;
    209             $this->potentialMethodCall = "";
    210         }
    211         //if($this->isNewLine && isset($token[2])) $this->currentMethod['content'] .= $token[2] . ": ";
    212         $this->isNewLine = false;
    213         $this->currentMethod['content'] .= $this->renderToken($token);
    214     }
    215     function completeMethod($token) {
    216         $this->closeOffMethodContentPre();
    217         $this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
    218         $this->currentClass['methods'][] = $this->currentMethod;
    219     }
    220
    221     protected $potentialMethodCall = "";
    222     function potentialMethodCall($token) {
    223         $this->potentialMethodCall .= $this->renderToken($token);
    224     }
    225     function completeMethodCall($token) {
    226         $this->potentialMethodCall .= $this->renderToken($token);
    227         if(strpos($this->potentialMethodCall, '-&gt;</span><span class="T_STRING">assert') !== false) {
    228             $this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
    229         } else {
    230             $this->currentMethod['content'] .= $this->potentialMethodCall;
    231         }
    232         $this->potentialMethodCall = "";
    233     }
    234
    235     /**
    236      * Finish the "pre" block in method content.
    237      * Will remove whitespace and empty "pre" blocks
    238      */
    239     function closeOffMethodContentPre() {
    240         $this->currentMethod['content'] = trim($this->currentMethod['content']);
    241         if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
    242         else $this->currentMethod['content'] .= '</pre>';
    243     }
    244
    245     /**
    246      * Render the given token as HTML
    247      */
    248     function renderToken($token) {
    249         $tokenContent = htmlentities(is_array($token) ? $token[1] : $token);
    250         $tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
    251
    252         switch($tokenName) {
    253             case "T_WHITESPACE":
    254                 if(strpos($tokenContent, "\n") !== false) $this->isNewLine = true;
    255                 return $tokenContent;
    256             default:
    257                 return "<span class=\"$tokenName\">$tokenContent</span>";
    258         }
    259     }
    260
    261     protected $classes = array();
    262     protected $currentMethod, $currentClass;
    263
    264     function testAnalysis($file) {
    265         $content = file_get_contents($file);
    266         $tokens = token_get_all($content);
    267
    268         // Execute a finite-state-machine with a built-in state stack
    269         // This FSM+stack gives us enough expressive power for simple PHP parsing
    270         $state = "start";
    271         $stateStack = array();
    272
    273         //echo "<li>state $state";
    274         foreach($tokens as $token) {
    275             // Get token name - some tokens are arrays, some arent'
    276             if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
    277             //echo "<li>token '$tokenName'";
    278
    279             // Find the rule for that token in the current state
    280             if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
    281             else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
    282             else $rule = null;
    283
    284             // Check to see if we have specified multiple rules depending on whether the stack is populated
    285             if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
    286                 if($stateStack) $rule = $rule['hasstack'];
    287                 else $rule = $rule = $rule['nostack'];
    288             }
    289
    290             if(is_array($rule)) {
    291                 list($destState, $methodName) = $rule;
    292                 $this->$methodName($token);
    293             } else if($rule) {
    294                 $destState = $rule;
    295             } else {
    296                 $destState = null;
    297             }
    298             //echo "<li>->state $destState";
    299
    300             if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
    301                 $action = $parts[1];
    302                 $argument = isset($parts[2]) ? substr($parts[2],1) : null;
    303                 $destState = null;
    304
    305                 switch($action) {
    306                     case "push":
    307                         $stateStack[] = $state;
    308                         if($argument) $destState = $argument;
    309                         break;
    310
    311                     case "pop":
    312                         if($stateStack) $destState = array_pop($stateStack);
    313                         else if($argument) $destState = $argument;
    314                         else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
    315                 }
    316             }
    317
    318             if($destState) $state = $destState;
    319             if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
    320         }
    321
    322         $subclasses = ClassInfo::subclassesFor('SapphireTest');
    323         foreach($this->classes as $classDef) {
    324             if(true ||in_array($classDef['name'], $subclasses)) {
    325                 echo "<h1>$classDef[heading]</h1>";
    326                 echo "<div style=\"font-weight: bold\">$classDef[description]</div>";
    327                 if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
    328                     if(true || substr($method['name'],0,4) == 'test') {
    329                         //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
    330                         $title = $method['heading'];
    331
    332                         echo "<h2>$title</h2>";
    333                         echo "<div style=\"font-weight: bold\">$method[description]</div>";
    334                         echo $method['content'];
    335                     }
    336                 }
    337             }
    338
    339         }
    340     }
    341 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -