mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENHANCEMENT: move code introspection functions to the silverstripe-codeviz module
This commit is contained in:
parent
69c066cb2c
commit
e262a29193
@ -1,3 +0,0 @@
|
||||
pre { border: 1px #777 solid; background-color: #CCC; color: #333; margin: 0.5em 2em; padding: 1em; line-height: 120%; }
|
||||
|
||||
pre strong { background-color: #FFC; color: #000; padding: 2px; }
|
@ -1,3 +0,0 @@
|
||||
pre { border: 1px #777 solid; background-color: #CCC; color: #333; margin: 0.5em 2em; padding: 1em; line-height: 120%; }
|
||||
|
||||
pre strong { background-color: #FFC; color: #000; padding: 2px; }
|
@ -1,351 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Allows human reading of a test in a format suitable for agile documentation
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class CodeViewer extends Controller {
|
||||
|
||||
public static $url_handlers = array(
|
||||
'' => 'browse',
|
||||
'$Class' => 'viewClass'
|
||||
);
|
||||
|
||||
static $allowed_actions = array(
|
||||
'index',
|
||||
'browse',
|
||||
'viewClass'
|
||||
);
|
||||
|
||||
/**
|
||||
* Define a simple finite state machine.
|
||||
* Top keys are the state names. 'start' is the first state, and 'die' is the error state.
|
||||
* Inner keys are token names/codes. The values are either a string, new state, or an array(new state, handler method).
|
||||
* The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
|
||||
*/
|
||||
static $fsm = array(
|
||||
'start' => array(
|
||||
T_CLASS => array('className','createClass'),
|
||||
T_DOC_COMMENT => array('', 'saveClassComment'),
|
||||
),
|
||||
'className' => array(
|
||||
T_STRING => array('classSpec', 'setClassName'),
|
||||
),
|
||||
'classSpec' => array(
|
||||
'{' => 'classBody',
|
||||
),
|
||||
'classBody' => array(
|
||||
T_FUNCTION => array('methodName','createBodyMethod'),
|
||||
'}' => array('start', 'completeClass'),
|
||||
T_DOC_COMMENT => array('', 'saveMethodComment'),
|
||||
),
|
||||
'methodName' => array(
|
||||
T_STRING => array('methodSpec', 'setMethodName'),
|
||||
),
|
||||
'methodSpec' => array(
|
||||
'{' => 'methodBody',
|
||||
),
|
||||
'methodBody' => array(
|
||||
'{' => array('!push','appendMethodContent'),
|
||||
'}' => array(
|
||||
'hasstack' => array('!pop', 'appendMethodContent'),
|
||||
'nostack' => array('classBody', 'completeMethod'),
|
||||
),
|
||||
T_VARIABLE => array('variable', 'potentialMethodCall'),
|
||||
T_COMMENT => array('', 'appendMethodComment'),
|
||||
T_DOC_COMMENT => array('', 'appendMethodComment'),
|
||||
'*' => array('', 'appendMethodContent'),
|
||||
),
|
||||
'variable' => array(
|
||||
T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'variableArrow' => array(
|
||||
T_STRING => array('methodOrProperty', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodOrProperty' => array(
|
||||
'(' => array('methodCall', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodCall' => array(
|
||||
'(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
|
||||
')' => array('methodBody', 'completeMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
'nestedInMethodCall' => array(
|
||||
'(' => array('!push', 'potentialMethodCall'),
|
||||
')' => array('!pop', 'potentialMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
);
|
||||
|
||||
function init() {
|
||||
parent::init();
|
||||
|
||||
if(!Permission::check('ADMIN')) return Security::permissionFailure();
|
||||
TestRunner::use_test_manifest();
|
||||
}
|
||||
|
||||
public function browse() {
|
||||
$classes = ClassInfo::subclassesFor('SapphireTest');
|
||||
|
||||
array_shift($classes);
|
||||
ksort($classes);
|
||||
|
||||
$result ='<h1>View any of the following test classes</h1>';
|
||||
$result .='<ul>';
|
||||
foreach($classes as $class) {
|
||||
$result .="<li><a href=\"{$this->Link($class)}\">$class</a></li>";
|
||||
}
|
||||
$result .='</ul>';
|
||||
|
||||
$result .='<h1>View any of the following other classes</h1>';
|
||||
|
||||
$classes = array_keys(ClassInfo::allClasses());
|
||||
sort($classes);
|
||||
|
||||
$result .='<ul>';
|
||||
foreach($classes as $class) {
|
||||
$result .="<li><a href=\"{$this->Link($class)}\">$class</a></li>";
|
||||
}
|
||||
$result .='</ul>';
|
||||
|
||||
return $this->customise(array (
|
||||
'Content' => $result
|
||||
))->renderWith('CodeViewer');
|
||||
}
|
||||
|
||||
public function viewClass(SS_HTTPRequest $request) {
|
||||
$class = $request->param('Class');
|
||||
|
||||
if(!class_exists($class)) {
|
||||
throw new Exception('CodeViewer->viewClass(): not passed a valid class to view (does the class exist?)');
|
||||
}
|
||||
|
||||
return $this->customise(array (
|
||||
'Content' => $this->testAnalysis(getClassFile($class))
|
||||
))->renderWith('CodeViewer');
|
||||
}
|
||||
|
||||
public function Link($action = null) {
|
||||
return Controller::join_links(Director::absoluteBaseURL(), 'dev/viewcode/', $action);
|
||||
}
|
||||
|
||||
protected $classComment, $methodComment;
|
||||
|
||||
function saveClassComment($token) {
|
||||
$this->classComment = $this->parseComment($token);
|
||||
}
|
||||
function saveMethodComment($token) {
|
||||
$this->methodComment = $this->parseComment($token);
|
||||
}
|
||||
|
||||
function createClass($token) {
|
||||
$this->currentClass = array(
|
||||
"description" => $this->classComment['pretty'],
|
||||
"heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null,
|
||||
);
|
||||
$ths->classComment = null;
|
||||
}
|
||||
function setClassName($token) {
|
||||
$this->currentClass['name'] = $token[1];
|
||||
if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1];
|
||||
}
|
||||
function completeClass($token) {
|
||||
$this->classes[] = $this->currentClass;
|
||||
}
|
||||
|
||||
function createBodyMethod($token) {
|
||||
$this->currentMethod = array();
|
||||
$this->currentMethod['content'] = "<pre>";
|
||||
$this->currentMethod['description'] = $this->methodComment['pretty'];
|
||||
$this->currentMethod['heading'] = isset($this->methodComment['heading']) ? $this->methodComment['heading'] : null;
|
||||
$this->methodComment = null;
|
||||
|
||||
}
|
||||
function setMethodName($token) {
|
||||
$this->currentMethod['name'] = $token[1];
|
||||
if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
|
||||
}
|
||||
function appendMethodComment($token) {
|
||||
if(substr($token[1],0,2) == '/*') {
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] .= $this->prettyComment($token) . "<pre>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
}
|
||||
|
||||
function prettyComment($token) {
|
||||
$comment = preg_replace('/^\/\*/','',$token[1]);
|
||||
$comment = preg_replace('/\*\/$/','',$comment);
|
||||
$comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
|
||||
$comment = htmlentities($comment, ENT_COMPAT, 'UTF-8');
|
||||
$comment = str_replace("\n\n", "</p><p>", $comment);
|
||||
return "<p>$comment</p>";
|
||||
}
|
||||
|
||||
function parseComment($token) {
|
||||
$parsed = array();
|
||||
|
||||
$comment = preg_replace('/^\/\*/','',$token[1]);
|
||||
$comment = preg_replace('/\*\/$/','',$comment);
|
||||
$comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
|
||||
|
||||
foreach(array('heading','nav') as $var) {
|
||||
if(preg_match('/@' . $var . '\s+([^\n]+)\n/', $comment, $matches)) {
|
||||
$parsed[$var] = $matches[1];
|
||||
$comment = preg_replace('/@' . $var . '\s+([^\n]+)\n/','', $comment);
|
||||
}
|
||||
}
|
||||
|
||||
$parsed['pretty'] = "<p>" . str_replace("\n\n", "</p><p>", htmlentities($comment, ENT_COMPAT, 'UTF-8')). "</p>";
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
protected $isNewLine = true;
|
||||
|
||||
function appendMethodContent($token) {
|
||||
if($this->potentialMethodCall) {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
//if($this->isNewLine && isset($token[2])) $this->currentMethod['content'] .= $token[2] . ": ";
|
||||
$this->isNewLine = false;
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethod($token) {
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
|
||||
$this->currentClass['methods'][] = $this->currentMethod;
|
||||
}
|
||||
|
||||
protected $potentialMethodCall = "";
|
||||
function potentialMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
if(strpos($this->potentialMethodCall, '-></span><span class="T_STRING">assert') !== false) {
|
||||
$this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
}
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the "pre" block in method content.
|
||||
* Will remove whitespace and empty "pre" blocks
|
||||
*/
|
||||
function closeOffMethodContentPre() {
|
||||
$this->currentMethod['content'] = trim($this->currentMethod['content']);
|
||||
if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
|
||||
else $this->currentMethod['content'] .= '</pre>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given token as HTML
|
||||
*/
|
||||
function renderToken($token) {
|
||||
$tokenContent = htmlentities(
|
||||
is_array($token) ? $token[1] : $token,
|
||||
ENT_COMPAT,
|
||||
'UTF-8'
|
||||
);
|
||||
$tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
|
||||
|
||||
switch($tokenName) {
|
||||
case "T_WHITESPACE":
|
||||
if(strpos($tokenContent, "\n") !== false) $this->isNewLine = true;
|
||||
return $tokenContent;
|
||||
default:
|
||||
return "<span class=\"$tokenName\">$tokenContent</span>";
|
||||
}
|
||||
}
|
||||
|
||||
protected $classes = array();
|
||||
protected $currentMethod, $currentClass;
|
||||
|
||||
function testAnalysis($file) {
|
||||
$content = file_get_contents($file);
|
||||
$tokens = token_get_all($content);
|
||||
|
||||
// Execute a finite-state-machine with a built-in state stack
|
||||
// This FSM+stack gives us enough expressive power for simple PHP parsing
|
||||
$state = "start";
|
||||
$stateStack = array();
|
||||
|
||||
//echo "<li>state $state";
|
||||
foreach($tokens as $token) {
|
||||
// Get token name - some tokens are arrays, some arent'
|
||||
if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
|
||||
//echo "<li>token '$tokenName'";
|
||||
|
||||
// Find the rule for that token in the current state
|
||||
if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
|
||||
else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
|
||||
else $rule = null;
|
||||
|
||||
// Check to see if we have specified multiple rules depending on whether the stack is populated
|
||||
if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
|
||||
if($stateStack) $rule = $rule['hasstack'];
|
||||
else $rule = $rule = $rule['nostack'];
|
||||
}
|
||||
|
||||
if(is_array($rule)) {
|
||||
list($destState, $methodName) = $rule;
|
||||
$this->$methodName($token);
|
||||
} else if($rule) {
|
||||
$destState = $rule;
|
||||
} else {
|
||||
$destState = null;
|
||||
}
|
||||
//echo "<li>->state $destState";
|
||||
|
||||
if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
|
||||
$action = $parts[1];
|
||||
$argument = isset($parts[2]) ? substr($parts[2],1) : null;
|
||||
$destState = null;
|
||||
|
||||
switch($action) {
|
||||
case "push":
|
||||
$stateStack[] = $state;
|
||||
if($argument) $destState = $argument;
|
||||
break;
|
||||
|
||||
case "pop":
|
||||
if($stateStack) $destState = array_pop($stateStack);
|
||||
else if($argument) $destState = $argument;
|
||||
else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if($destState) $state = $destState;
|
||||
if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$subclasses = ClassInfo::subclassesFor('SapphireTest');
|
||||
foreach($this->classes as $classDef) {
|
||||
if(true ||in_array($classDef['name'], $subclasses)) {
|
||||
echo "<h1>$classDef[heading]</h1>";
|
||||
echo "<div style=\"font-weight: bold\">$classDef[description]</div>";
|
||||
if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
|
||||
if(true || substr($method['name'],0,4) == 'test') {
|
||||
//$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
|
||||
$title = $method['heading'];
|
||||
|
||||
echo "<h2>$title</h2>";
|
||||
echo "<div style=\"font-weight: bold\">$method[description]</div>";
|
||||
echo $method['content'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -83,8 +83,7 @@ class DevelopmentAdmin extends Controller {
|
||||
"tests/endsession" => "Ends a test session",
|
||||
"jstests" => "See a list of JavaScript tests to run",
|
||||
"jstests/all" => "Run all JavaScript tests",
|
||||
"tasks" => "See a list of build tasks to run",
|
||||
"viewcode" => "Read source code in a literate programming style",
|
||||
"tasks" => "See a list of build tasks to run"
|
||||
);
|
||||
|
||||
// Web mode
|
||||
@ -127,10 +126,6 @@ class DevelopmentAdmin extends Controller {
|
||||
return TaskRunner::create();
|
||||
}
|
||||
|
||||
function viewmodel() {
|
||||
return ModelViewer::create();
|
||||
}
|
||||
|
||||
function build($request) {
|
||||
if(Director::is_cli()) {
|
||||
$da = DatabaseAdmin::create();
|
||||
@ -187,8 +182,4 @@ class DevelopmentAdmin extends Controller {
|
||||
function errors() {
|
||||
Director::redirect("Debug_");
|
||||
}
|
||||
|
||||
function viewcode($request) {
|
||||
return CodeViewer::create();
|
||||
}
|
||||
}
|
||||
|
@ -1,213 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Gives you a nice way of viewing your data model.
|
||||
* Access at dev/viewmodel.
|
||||
*
|
||||
* Requirements: http://graphviz.org/
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer extends Controller {
|
||||
static $url_handlers = array(
|
||||
'$Module!' => 'handleModule',
|
||||
);
|
||||
|
||||
protected $module = null;
|
||||
|
||||
function handleModule($request) {
|
||||
return new ModelViewer_Module($request->param('Module'));
|
||||
}
|
||||
|
||||
function init() {
|
||||
parent::init();
|
||||
|
||||
$canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
|
||||
if(!$canAccess) return Security::permissionFailure($this);
|
||||
|
||||
// check for graphviz dependencies
|
||||
$returnCode = 0;
|
||||
$output = array();
|
||||
exec("which neato", $output, $returnCode);
|
||||
if($returnCode != 0) {
|
||||
user_error(
|
||||
'You don\'t seem to have the GraphViz library (http://graphviz.org/) and the "neato" command-line utility available',
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model classes
|
||||
*/
|
||||
function Models() {
|
||||
$classes = ClassInfo::subclassesFor('DataObject');
|
||||
array_shift($classes);
|
||||
$output = new ArrayList();
|
||||
foreach($classes as $class) {
|
||||
$output->push(new ModelViewer_Model($class));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model classes, grouped by Module
|
||||
*/
|
||||
function Modules() {
|
||||
$classes = ClassInfo::subclassesFor('DataObject');
|
||||
array_shift($classes);
|
||||
|
||||
$modules = array();
|
||||
foreach($classes as $class) {
|
||||
$model = new ModelViewer_Model($class);
|
||||
if(!isset($modules[$model->Module])) $modules[$model->Module] = new ArrayList();
|
||||
$modules[$model->Module]->push($model);
|
||||
}
|
||||
ksort($modules);
|
||||
unset($modules['userforms']);
|
||||
|
||||
if($this->module) {
|
||||
$modules = array($this->module => $modules[$this->module]);
|
||||
}
|
||||
|
||||
$output = new ArrayList();
|
||||
foreach($modules as $moduleName => $models) {
|
||||
$output->push(new ArrayData(array(
|
||||
'Link' => 'dev/viewmodel/' . $moduleName,
|
||||
'Name' => $moduleName,
|
||||
'Models' => $models,
|
||||
)));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer_Module extends ModelViewer {
|
||||
static $url_handlers = array(
|
||||
'graph' => 'graph',
|
||||
);
|
||||
|
||||
/**
|
||||
* ModelViewer can be optionally constructed to restrict its output to a specific module
|
||||
*/
|
||||
function __construct($module = null) {
|
||||
$this->module = $module;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
function graph() {
|
||||
SSViewer::set_source_file_comments(false);
|
||||
$dotContent = $this->renderWith("ModelViewer_dotsrc");
|
||||
$CLI_dotContent = escapeshellarg($dotContent);
|
||||
|
||||
$output= `echo $CLI_dotContent | neato -Tpng:gd &> /dev/stdout`;
|
||||
if(substr($output,1,3) == 'PNG') header("Content-type: image/png");
|
||||
else header("Content-type: text/plain");
|
||||
echo $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single model in the model viewer
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer_Model extends ViewableData {
|
||||
protected $className;
|
||||
|
||||
function __construct($className) {
|
||||
$this->className = $className;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
function getModule() {
|
||||
$classes = SS_ClassLoader::instance()->getManifest()->getClasses();
|
||||
$className = strtolower($this->className);
|
||||
|
||||
if(($pos = strpos($className,'_')) !== false) $className = substr($className,0,$pos);
|
||||
if(isset($classes[$className])) {
|
||||
if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $classes[$className], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return $this->className;
|
||||
}
|
||||
|
||||
function getParentModel() {
|
||||
$parentClass = get_parent_class($this->className);
|
||||
if($parentClass != "DataObject") return $parentClass;
|
||||
}
|
||||
|
||||
function Fields() {
|
||||
$output = new ArrayList();
|
||||
|
||||
$output->push(new ModelViewer_Field($this,'ID', 'PrimaryKey'));
|
||||
if(!$this->ParentModel) {
|
||||
$output->push(new ModelViewer_Field($this,'Created', 'Datetime'));
|
||||
$output->push(new ModelViewer_Field($this,'LastEdited', 'Datetime'));
|
||||
}
|
||||
|
||||
$db = singleton($this->className)->uninherited('db',true);
|
||||
if($db) foreach($db as $k => $v) {
|
||||
$output->push(new ModelViewer_Field($this, $k, $v));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
function Relations() {
|
||||
$output = new ArrayList();
|
||||
|
||||
foreach(array('has_one','has_many','many_many') as $relType) {
|
||||
$items = singleton($this->className)->uninherited($relType,true);
|
||||
if($items) foreach($items as $k => $v) {
|
||||
$output->push(new ModelViewer_Relation($this, $k, $v, $relType));
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer_Field extends ViewableData {
|
||||
public $Model, $Name, $Type;
|
||||
|
||||
function __construct($model, $name, $type) {
|
||||
$this->Model = $model;
|
||||
$this->Name = $name;
|
||||
$this->Type = $type;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tools
|
||||
*/
|
||||
class ModelViewer_Relation extends ViewableData {
|
||||
public $Model, $Name, $RelationType, $RelatedClass;
|
||||
|
||||
function __construct($model, $name, $relatedClass, $relationType) {
|
||||
$this->Model = $model;
|
||||
$this->Name = $name;
|
||||
$this->RelatedClass = $relatedClass;
|
||||
$this->RelationType = $relationType;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,257 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Allows human reading of a test in a format suitable for agile documentation
|
||||
* @package framework
|
||||
* @subpackage testing
|
||||
*/
|
||||
class TestViewer extends Controller {
|
||||
/**
|
||||
* Define a simple finite state machine.
|
||||
* Top keys are the state names. 'start' is the first state, and 'die' is the error state.
|
||||
* Inner keys are token names/codes. The values are either a string, new state, or an array(new state, handler method).
|
||||
* The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
|
||||
*/
|
||||
static $fsm = array(
|
||||
'start' => array(
|
||||
T_CLASS => array('className','createClass'),
|
||||
),
|
||||
'className' => array(
|
||||
T_STRING => array('classSpec', 'setClassName'),
|
||||
),
|
||||
'classSpec' => array(
|
||||
'{' => 'classBody',
|
||||
),
|
||||
'classBody' => array(
|
||||
T_FUNCTION => array('methodName','createBodyMethod'),
|
||||
'}' => array('start', 'completeClass'),
|
||||
),
|
||||
'methodName' => array(
|
||||
T_STRING => array('methodSpec', 'setMethodName'),
|
||||
),
|
||||
'methodSpec' => array(
|
||||
'{' => 'methodBody',
|
||||
),
|
||||
'methodBody' => array(
|
||||
'{' => array('!push','appendMethodContent'),
|
||||
'}' => array(
|
||||
'hasstack' => array('!pop', 'appendMethodContent'),
|
||||
'nostack' => array('classBody', 'completeMethod'),
|
||||
),
|
||||
T_VARIABLE => array('variable', 'potentialMethodCall'),
|
||||
T_COMMENT => array('', 'appendMethodComment'),
|
||||
'*' => array('', 'appendMethodContent'),
|
||||
),
|
||||
'variable' => array(
|
||||
T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'variableArrow' => array(
|
||||
T_STRING => array('methodOrProperty', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodOrProperty' => array(
|
||||
'(' => array('methodCall', 'potentialMethodCall'),
|
||||
T_WHITESPACE => array('', 'potentialMethodCall'),
|
||||
'*' => array('methodBody', 'appendMethodContent'),
|
||||
),
|
||||
'methodCall' => array(
|
||||
'(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
|
||||
')' => array('methodBody', 'completeMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
'nestedInMethodCall' => array(
|
||||
'(' => array('!push', 'potentialMethodCall'),
|
||||
')' => array('!pop', 'potentialMethodCall'),
|
||||
'*' => array('', 'potentialMethodCall'),
|
||||
),
|
||||
);
|
||||
|
||||
function init() {
|
||||
parent::init();
|
||||
|
||||
$canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
|
||||
if(!$canAccess) return Security::permissionFailure($this);
|
||||
}
|
||||
|
||||
function createClass($token) {
|
||||
$this->currentClass = array();
|
||||
}
|
||||
function setClassName($token) {
|
||||
$this->currentClass['name'] = $token[1];
|
||||
}
|
||||
function completeClass($token) {
|
||||
$this->classes[] = $this->currentClass;
|
||||
}
|
||||
|
||||
function createBodyMethod($token) {
|
||||
$this->currentMethod = array();
|
||||
$this->currentMethod['content'] = "<pre>";
|
||||
}
|
||||
function setMethodName($token) {
|
||||
$this->currentMethod['name'] = $token[1];
|
||||
}
|
||||
function appendMethodComment($token) {
|
||||
if(substr($token[1],0,2) == '/*') {
|
||||
$comment = preg_replace('/^\/\*/','',$token[1]);
|
||||
$comment = preg_replace('/\*\/$/','',$comment);
|
||||
$comment = preg_replace('/\n[\t ]*\* */m',"\n",$comment);
|
||||
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] .= "<p>$comment</p><pre>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
|
||||
}
|
||||
function appendMethodContent($token) {
|
||||
if($this->potentialMethodCall) {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
$this->currentMethod['content'] .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethod($token) {
|
||||
$this->closeOffMethodContentPre();
|
||||
$this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
|
||||
$this->currentClass['methods'][] = $this->currentMethod;
|
||||
}
|
||||
|
||||
protected $potentialMethodCall = "";
|
||||
function potentialMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
}
|
||||
function completeMethodCall($token) {
|
||||
$this->potentialMethodCall .= $this->renderToken($token);
|
||||
if(strpos($this->potentialMethodCall, '-></span><span class="T_STRING">assert') !== false) {
|
||||
$this->currentMethod['content'] .= "<strong>" . $this->potentialMethodCall . "</strong>";
|
||||
} else {
|
||||
$this->currentMethod['content'] .= $this->potentialMethodCall;
|
||||
}
|
||||
$this->potentialMethodCall = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the "pre" block in method content.
|
||||
* Will remove whitespace and empty "pre" blocks
|
||||
*/
|
||||
function closeOffMethodContentPre() {
|
||||
$this->currentMethod['content'] = trim($this->currentMethod['content']);
|
||||
if(substr($this->currentMethod['content'],-5) == '<pre>') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
|
||||
else $this->currentMethod['content'] .= '</pre>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given token as HTML
|
||||
*/
|
||||
function renderToken($token) {
|
||||
$tokenContent = htmlentities(
|
||||
is_array($token) ? $token[1] : $token,
|
||||
ENT_COMPAT,
|
||||
'UTF-8'
|
||||
);
|
||||
$tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
|
||||
|
||||
switch($tokenName) {
|
||||
case "T_WHITESPACE":
|
||||
return $tokenContent;
|
||||
default:
|
||||
return "<span class=\"$tokenName\">$tokenContent</span>";
|
||||
}
|
||||
}
|
||||
|
||||
protected $classes = array();
|
||||
protected $currentMethod, $currentClass;
|
||||
|
||||
function Content() {
|
||||
$className = $this->urlParams['ID'];
|
||||
if($className && ClassInfo::exists($className)) {
|
||||
return $this->testAnalysis(getClassFile($className));
|
||||
} else {
|
||||
$result = "<h1>View any of the following test classes</h1>";
|
||||
$classes = ClassInfo::subclassesFor('SapphireTest');
|
||||
ksort($classes);
|
||||
foreach($classes as $className) {
|
||||
if($className == 'SapphireTest') continue;
|
||||
$result .= "<li><a href=\"TestViewer/show/$className\">$className</a></li>";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
function testAnalysis($file) {
|
||||
$content = file_get_contents($file);
|
||||
$tokens = token_get_all($content);
|
||||
|
||||
// Execute a finite-state-machine with a built-in state stack
|
||||
// This FSM+stack gives us enough expressive power for simple PHP parsing
|
||||
$state = "start";
|
||||
$stateStack = array();
|
||||
|
||||
//echo "<li>state $state";
|
||||
foreach($tokens as $token) {
|
||||
// Get token name - some tokens are arrays, some arent'
|
||||
if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
|
||||
//echo "<li>token '$tokenName'";
|
||||
|
||||
// Find the rule for that token in the current state
|
||||
if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
|
||||
else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
|
||||
else $rule = null;
|
||||
|
||||
// Check to see if we have specified multiple rules depending on whether the stack is populated
|
||||
if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
|
||||
if($stateStack) $rule = $rule['hasstack'];
|
||||
else $rule = $rule = $rule['nostack'];
|
||||
}
|
||||
|
||||
if(is_array($rule)) {
|
||||
list($destState, $methodName) = $rule;
|
||||
$this->$methodName($token);
|
||||
} else if($rule) {
|
||||
$destState = $rule;
|
||||
} else {
|
||||
$destState = null;
|
||||
}
|
||||
//echo "<li>->state $destState";
|
||||
|
||||
if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
|
||||
$action = $parts[1];
|
||||
$argument = isset($parts[2]) ? substr($parts[2],1) : null;
|
||||
$destState = null;
|
||||
|
||||
switch($action) {
|
||||
case "push":
|
||||
$stateStack[] = $state;
|
||||
if($argument) $destState = $argument;
|
||||
break;
|
||||
|
||||
case "pop":
|
||||
if($stateStack) $destState = array_pop($stateStack);
|
||||
else if($argument) $destState = $argument;
|
||||
else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if($destState) $state = $destState;
|
||||
if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
|
||||
}
|
||||
|
||||
$subclasses = ClassInfo::subclassesFor('SapphireTest');
|
||||
foreach($this->classes as $classDef) {
|
||||
if(in_array($classDef['name'], $subclasses)) {
|
||||
echo "<h1>$classDef[name]</h1>";
|
||||
if($classDef['methods']) foreach($classDef['methods'] as $method) {
|
||||
if(substr($method['name'],0,4) == 'test') {
|
||||
//$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
|
||||
$title = $method['name'];
|
||||
|
||||
echo "<h2>$title</h2>";
|
||||
echo $method['content'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
pre {
|
||||
border: 1px #777 solid;
|
||||
background-color: #CCC;
|
||||
color: #333;
|
||||
margin: 0.5em 2em;
|
||||
padding: 1em;
|
||||
line-height: 120%;
|
||||
}
|
||||
|
||||
pre strong {
|
||||
background-color: #FFC;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
pre {
|
||||
border: 1px #777 solid;
|
||||
background-color: #CCC;
|
||||
color: #333;
|
||||
margin: 0.5em 2em;
|
||||
padding: 1em;
|
||||
line-height: 120%;
|
||||
}
|
||||
|
||||
pre strong {
|
||||
background-color: #FFC;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<link rel="stylesheet" href="$ModulePath(framework)/css/CodeViewer.css" />
|
||||
</head>
|
||||
<body>
|
||||
$Content
|
||||
</body>
|
||||
</html>
|
@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<title>Data Model</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Data Model for your project</h1>
|
||||
|
||||
<% control Modules %>
|
||||
<h1>Module $Name</h1>
|
||||
|
||||
<img src="$Link/graph" />
|
||||
|
||||
<% control Models %>
|
||||
<h2>$Name <% if ParentModel %> (subclass of $ParentModel)<% end_if %></h2>
|
||||
<h4>Fields</h4>
|
||||
<ul>
|
||||
<% control Fields %>
|
||||
<li>$Name - $Type</li>
|
||||
<% end_control %>
|
||||
</ul>
|
||||
|
||||
<h4>Relations</h4>
|
||||
<ul>
|
||||
<% control Relations %>
|
||||
<li>$Name $RelationType $RelatedClass</li>
|
||||
<% end_control %>
|
||||
</ul>
|
||||
<% end_control %>
|
||||
<% end_control %>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,20 +0,0 @@
|
||||
digraph g {
|
||||
orientation=portrait;
|
||||
overlap=false;
|
||||
splines=true;
|
||||
|
||||
edge[fontsize=8,len=1.5];
|
||||
node[fontsize=10,shape=box];
|
||||
|
||||
<% control Modules %>
|
||||
<% control Models %>
|
||||
$Name [shape=record,label="{$Name|<% control Fields %>$Name\\n<% end_control %>}"];
|
||||
<% if ParentModel %>
|
||||
$Name -> $ParentModel [style=dotted];
|
||||
<% end_if %>
|
||||
<% control Relations %>
|
||||
$Model.Name -> $RelatedClass [label="$Name\\n$RelationType"];
|
||||
<% end_control %>
|
||||
<% end_control %>
|
||||
<% end_control %>
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<% base_tag %>
|
||||
<link rel="stylesheet" href="$ModulePath(framework)/css/TestViewer.css" />
|
||||
</head>
|
||||
<body>
|
||||
$Content
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user