PHPParser optimisations and update

This commit is contained in:
Daniel Hensby 2016-05-19 18:50:51 +01:00
parent f8e3443c89
commit cdb4a86e1c
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
12 changed files with 389 additions and 897 deletions

View File

@ -18,17 +18,17 @@
"require": {
"php": ">=5.5.0",
"composer/installers": "~1.0",
"monolog/monolog": "~1.11",
"league/flysystem": "~1.0.12",
"symfony/yaml": "~2.7",
"embed/embed": "^2.6",
"league/flysystem": "~1.0.12",
"monolog/monolog": "~1.11",
"nikic/php-parser": "^2 || ^3",
"silverstripe/config": "^1@dev",
"swiftmailer/swiftmailer": "~5.4",
"symfony/cache": "^3.3@dev",
"symfony/config": "^2.8",
"symfony/translation": "^2.8",
"vlucas/phpdotenv": "^2.4",
"nikic/php-parser": "^2.1",
"silverstripe/config": "^1@dev"
"symfony/yaml": "~2.7",
"vlucas/phpdotenv": "^2.4"
},
"require-dev": {
"phpunit/PHPUnit": "~4.8",

View File

@ -0,0 +1,86 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* Class ClassContentRemover
* @package SilverStripe\Core\Manifest
*
* A utility class to clean the contents of a PHP file containing classes.
*
* It removes any code with in `$cut_off_depth` number of curly braces.
*/
class ClassContentRemover
{
/**
* @param string $filePath
* @param int $cutOffDepth The number of levels of curly braces to go before ignoring the content
*
* @return string
*/
public static function remove_class_content($filePath, $cutOffDepth = 1)
{
// Use PHP's built in method to strip comments and whitespace
$contents = php_strip_whitespace($filePath);
if (!trim($contents)) {
return $contents;
}
if (!preg_match('/\b(?:class|interface|trait)/i', $contents)) {
return '';
}
// tokenize the file contents
$tokens = token_get_all($contents);
$cleanContents = '';
$depth = 0;
$startCounting = false;
// iterate over all the tokens and only store the tokens that are outside $cutOffDepth
foreach ($tokens as $token) {
// only store the string literal of the token, that's all we need
if (!is_array($token)) {
$token = [
T_STRING,
$token,
null
];
}
// only count if we see a class/interface/trait keyword
if (!$startCounting && in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT])) {
$startCounting = true;
}
// use curly braces as a sign of depth
if ($token[1] === '{') {
if ($depth < $cutOffDepth) {
$cleanContents .= $token[1];
}
if ($startCounting) {
++$depth;
}
} elseif ($token[1] === '}') {
if ($startCounting) {
--$depth;
// stop counting if we've just come out of the
// class/interface/trait declaration
if ($depth <= 0) {
$startCounting = false;
}
}
if ($depth < $cutOffDepth) {
$cleanContents .= $token[1];
}
} elseif ($depth < $cutOffDepth) {
$cleanContents .= $token[1];
}
}
// return cleaned class
return trim($cleanContents);
}
}

View File

@ -3,8 +3,9 @@
namespace SilverStripe\Core\Manifest;
use Exception;
use PhpParser\Error;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use SilverStripe\Control\Director;
@ -48,127 +49,17 @@ class ClassManifest
protected $traits = array();
/**
* @return TokenisedRegularExpression
* @var \PhpParser\Parser
*/
public static function get_class_parser()
{
return new TokenisedRegularExpression(array(
0 => T_CLASS,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'),
3 => array(T_WHITESPACE, 'optional' => true),
4 => T_EXTENDS,
5 => array(T_WHITESPACE, 'optional' => true),
6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14),
7 => array(T_WHITESPACE, 'optional' => true),
8 => T_IMPLEMENTS,
9 => array(T_WHITESPACE, 'optional' => true),
10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'),
11 => array(T_WHITESPACE, 'optional' => true),
12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'),
13 => array(T_WHITESPACE, 'can_jump_to' => 10),
14 => array(T_WHITESPACE, 'optional' => true),
15 => '{',
));
}
private $parser;
/**
* @return TokenisedRegularExpression
* @var NodeTraverser
*/
public static function get_namespaced_class_parser()
{
return new TokenisedRegularExpression(array(
0 => T_CLASS,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'),
3 => array(T_WHITESPACE, 'optional' => true),
4 => T_EXTENDS,
5 => array(T_WHITESPACE, 'optional' => true),
6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true),
7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)),
8 => array(T_WHITESPACE, 'optional' => true),
9 => T_IMPLEMENTS,
10 => array(T_WHITESPACE, 'optional' => true),
11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true),
12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'),
13 => array(T_WHITESPACE, 'optional' => true),
14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'),
15 => array(T_WHITESPACE, 'can_jump_to' => 11),
16 => array(T_WHITESPACE, 'optional' => true),
17 => '{',
));
}
private $traverser;
/**
* @return TokenisedRegularExpression
* @var ClassManifestVisitor
*/
public static function get_trait_parser()
{
return new TokenisedRegularExpression(array(
0 => T_TRAIT,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_STRING, 'save_to' => 'traitName')
));
}
/**
* @return TokenisedRegularExpression
*/
public static function get_namespace_parser()
{
return new TokenisedRegularExpression(array(
0 => T_NAMESPACE,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true),
3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2),
4 => array(T_WHITESPACE, 'optional' => true),
5 => ';',
));
}
/**
* @return TokenisedRegularExpression
*/
public static function get_interface_parser()
{
return new TokenisedRegularExpression(array(
0 => T_INTERFACE,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_STRING, 'save_to' => 'interfaceName')
));
}
/**
* Create a {@link TokenisedRegularExpression} that extracts the namespaces imported with the 'use' keyword
*
* This searches symbols for a `use` followed by 1 or more namespaces which are optionally aliased using the `as`
* keyword. The relevant matching tokens are added one-by-one into an array (using `save_to` param).
*
* eg: use Namespace\ClassName as Alias, OtherNamespace\ClassName;
*
* @return TokenisedRegularExpression
*/
public static function get_imported_namespace_parser()
{
return new TokenisedRegularExpression(array(
0 => T_USE,
1 => array(T_WHITESPACE, 'optional' => true),
2 => array(T_NS_SEPARATOR, 'save_to' => 'importString[]', 'optional' => true),
3 => array(T_STRING, 'save_to' => 'importString[]', 'can_jump_to' => array(2, 8)),
4 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
5 => array(T_AS, 'save_to' => 'importString[]'),
6 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
7 => array(T_STRING, 'save_to' => 'importString[]'),
8 => array(T_WHITESPACE, 'optional' => true),
9 => array(',', 'save_to' => 'importString[]', 'optional' => true, 'can_jump_to' => 2),
10 => array(T_WHITESPACE, 'optional' => true, 'can_jump_to' => 2),
11 => ';',
));
}
protected $parser;
protected $traverser;
protected $visitor;
private $visitor;
/**
* Constructs and initialises a new class manifest, either loading the data
@ -189,11 +80,6 @@ class ClassManifest
$this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : ''));
$this->cacheKey = 'manifest';
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP5, new PhpParser\Lexer);
$this->traverser = new NodeTraverser;
$this->traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
$this->traverser->addVisitor($this->visitor = new SilverStripeNodeVisitor);
if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) {
$this->classes = $data['classes'];
$this->descendants = $data['descendants'];
@ -207,6 +93,35 @@ class ClassManifest
}
}
public function getParser()
{
if (!$this->parser) {
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
}
return $this->parser;
}
public function getTraverser()
{
if (!$this->traverser) {
$this->traverser = new NodeTraverser;
$this->traverser->addVisitor(new NameResolver);
$this->traverser->addVisitor($this->getVisitor());
}
return $this->traverser;
}
public function getVisitor()
{
if (!$this->visitor) {
$this->visitor = new ClassManifestVisitor;
}
return $this->visitor;
}
/**
* Returns the file path to a class or interface if it exists in the
* manifest.
@ -447,125 +362,6 @@ class ClassManifest
}
}
/**
* Find a the full namespaced declaration of a class (or interface) from a list of candidate imports
*
* This is typically used to determine the full class name in classes that have imported namesapced symbols (having
* used the `use` keyword)
*
* NB: remember the '\\' is an escaped backslash and is interpreted as a single \
*
* @param string $class The class (or interface) name to find in the candidate imports
* @param string $namespace The namespace that was declared for the classes definition (if there was one)
* @param array $imports The list of imported symbols (Classes or Interfaces) to test against
*
* @return string The fully namespaced class name
*/
protected function findClassOrInterfaceFromCandidateImports($class, $namespace = '', $imports = array())
{
//normalise the namespace
$namespace = rtrim($namespace, '\\');
//by default we'll use the $class as our candidate
$candidateClass = $class;
if (!$class) {
return $candidateClass;
}
//if the class starts with a \ then it is explicitly in the global namespace and we don't need to do
// anything else
if (substr($class, 0, 1) == '\\') {
$candidateClass = substr($class, 1);
return $candidateClass;
}
//if there's a namespace, starting assumption is the class is defined in that namespace
if ($namespace) {
$candidateClass = $namespace . '\\' . $class;
}
if (empty($imports)) {
return $candidateClass;
}
//normalised class name (PHP is case insensitive for symbols/namespaces
$lClass = strtolower($class);
//go through all the imports and see if the class exists within one of them
foreach ($imports as $alias => $import) {
//normalise import
$import = trim($import, '\\');
//if there is no string key, then there was no declared alias - we'll use the main declaration
if (is_int($alias)) {
$alias = strtolower($import);
} else {
$alias = strtolower($alias);
}
//exact match? Then it's a class in the global namespace that was imported OR it's an alias of
// another namespace
// or if it ends with the \ClassName then it's the class we are looking for
if ($lClass == $alias
|| substr_compare(
$alias,
'\\' . $lClass,
strlen($alias) - strlen($lClass) - 1,
// -1 because the $lClass length is 1 longer due to \
strlen($alias)
) === 0
) {
$candidateClass = $import;
break;
}
}
return $candidateClass;
}
/**
* Return an array of array($alias => $import) from tokenizer's tokens of a PHP file
*
* NB: If there is no alias we don't set a key to the array
*
* @param array $tokens The parsed tokens from tokenizer's parsing of a PHP file
*
* @return array The array of imports as (optional) $alias => $import
*/
protected function getImportsFromTokens($tokens)
{
//parse out the imports
$imports = self::get_imported_namespace_parser()->findAll($tokens);
//if there are any imports, clean them up
// imports come to us as array('importString' => array([array of matching tokens]))
// we need to join this nested array into a string and split out the alias and the import
if (!empty($imports)) {
$cleanImports = array();
foreach ($imports as $import) {
if (!empty($import['importString'])) {
//join the array up into a string
$importString = implode('', $import['importString']);
//split at , to get each import declaration
$importSet = explode(',', $importString);
foreach ($importSet as $importDeclaration) {
//split at ' as ' (any case) to see if we are aliasing the namespace
$importDeclaration = preg_split('/\s+as\s+/i', $importDeclaration);
//shift off the fully namespaced import
$qualifiedImport = array_shift($importDeclaration);
//if there are still items in the array, it's the alias
if (!empty($importDeclaration)) {
$cleanImports[array_shift($importDeclaration)] = $qualifiedImport;
} else {
$cleanImports[] = $qualifiedImport;
}
}
}
}
$imports = $cleanImports;
}
return $imports;
}
public function handleFile($basename, $pathname, $depth)
{
if ($basename == self::CONF_FILE) {
@ -575,8 +371,6 @@ class ClassManifest
$classes = null;
$interfaces = null;
$namespace = null;
$imports = null;
$traits = null;
// The results of individual file parses are cached, since only a few
@ -589,78 +383,50 @@ class ClassManifest
if ($data = $this->cache->load($key)) {
$valid = (
isset($data['classes']) && is_array($data['classes'])
&& isset($data['interfaces']) && is_array($data['interfaces'])
&& isset($data['namespace']) && is_string($data['namespace'])
&& isset($data['imports']) && is_array($data['imports'])
&& isset($data['traits']) && is_array($data['traits'])
&& isset($data['interfaces'])
&& is_array($data['interfaces'])
&& isset($data['traits'])
&& is_array($data['traits'])
);
if ($valid) {
$classes = $data['classes'];
$interfaces = $data['interfaces'];
$namespace = $data['namespace'];
$imports = $data['imports'];
$traits = $data['traits'];
}
}
if (!$valid) {
$this->visitor->reset();
$stmts = $this->parser->parse(file_get_contents($pathname));
$this->traverser->traverse($stmts);
$fileContents = ClassContentRemover::remove_class_content($pathname);
try {
$stmts = $this->getParser()->parse($fileContents);
} catch (Error $e) {
// if our mangled contents breaks, try again with the proper file contents
$stmts = $this->getParser()->parse(file_get_contents($pathname));
}
$this->getTraverser()->traverse($stmts);
$classes = $this->visitor->getClasses();
$traits = $this->visitor->getTraits();
$namespace = $this->visitor->getNamespace();
$imports = [];//$this->getImportsFromTokens($tokens);
$interfaces = $this->visitor->getInterfaces();
$classes = $this->getVisitor()->getClasses();
$interfaces = $this->getVisitor()->getInterfaces();
$traits = $this->getVisitor()->getTraits();
$cache = array(
'classes' => $classes,
'interfaces' => $interfaces,
'namespace' => $namespace,
'imports' => $imports,
'traits' => $traits
'traits' => $traits,
);
$this->cache->save($cache, $key);
}
// Ensure namespace has no trailing slash, and namespaceBase does
$namespaceBase = '';
if ($namespace) {
$namespace = rtrim($namespace, '\\');
$namespaceBase = $namespace . '\\';
}
foreach ($classes as $className => $classInfo) {
$extends = isset($classInfo['extends']) ? $classInfo['extends'] : null;
$implements = isset($classInfo['interfaces']) ? $classInfo['interfaces'] : null;
foreach ($classes as $class) {
$name = $namespaceBase . $class['className'];
$extends = isset($class['extends']) ? implode('', $class['extends']) : null;
$implements = isset($class['interfaces']) ? $class['interfaces'] : null;
if ($extends) {
$extends = $this->findClassOrInterfaceFromCandidateImports($extends, $namespace, $imports);
}
if (!empty($implements)) {
//join all the tokens
$implements = implode('', $implements);
//split at comma
$implements = explode(',', $implements);
//normalise interfaces
foreach ($implements as &$interface) {
$interface = $this->findClassOrInterfaceFromCandidateImports($interface, $namespace, $imports);
}
//release the var name
unset($interface);
}
$lowercaseName = strtolower($name);
$lowercaseName = strtolower($className);
if (array_key_exists($lowercaseName, $this->classes)) {
throw new Exception(sprintf(
'There are two files containing the "%s" class: "%s" and "%s"',
$name,
$className,
$this->classes[$lowercaseName],
$pathname
));
@ -669,15 +435,17 @@ class ClassManifest
$this->classes[$lowercaseName] = $pathname;
if ($extends) {
$extends = strtolower($extends);
foreach ($extends as $ancestor) {
$ancestor = strtolower($ancestor);
if (!isset($this->children[$extends])) {
$this->children[$extends] = array($name);
} else {
$this->children[$extends][] = $name;
if (!isset($this->children[$ancestor])) {
$this->children[$ancestor] = array($className);
} else {
$this->children[$ancestor][] = $className;
}
}
} else {
$this->roots[] = $name;
$this->roots[] = $className;
}
if ($implements) {
@ -685,19 +453,19 @@ class ClassManifest
$interface = strtolower($interface);
if (!isset($this->implementors[$interface])) {
$this->implementors[$interface] = array($name);
$this->implementors[$interface] = array($className);
} else {
$this->implementors[$interface][] = $name;
$this->implementors[$interface][] = $className;
}
}
}
}
foreach ($interfaces as $interface) {
$this->interfaces[strtolower($namespaceBase . $interface['interfaceName'])] = $pathname;
foreach ($interfaces as $interfaceName => $interfaceInfo) {
$this->interfaces[strtolower($interfaceName)] = $pathname;
}
foreach ($traits as $trait) {
$this->traits[strtolower($namespaceBase . $trait['traitName'])] = $pathname;
foreach ($traits as $traitName => $traitInfo) {
$this->traits[strtolower($traitName)] = $pathname;
}
}
@ -729,78 +497,3 @@ class ClassManifest
}
}
}
class SilverStripeNodeVisitor extends NodeVisitorAbstract
{
private $classes = [];
private $traits = [];
private $namespace = '';
private $interfaces = [];
public function reset()
{
$this->classes = [];
$this->traits = [];
$this->namespace = '';
$this->interfaces = [];
}
public function enterNode(PhpParser\Node $node)
{
if ($node instanceof PhpParser\Node\Stmt\Class_) {
$extends = [];
$implements = [];
if ($node->extends) {
$extends[] = (string)$node->extends;
}
if ($node->implements) {
foreach ($node->implements as $implement) {
$implements[] = (string)$implement;
}
}
$this->classes[] = [
'className' => $node->name,
'extends' => $extends,
'implements' => $implements
];
} else if ($node instanceof PhpParser\Node\Stmt\Trait_) {
$this->traits[] = ['traitName' => (string)$node->name];
} else if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
$this->namespace = (string)$node->name;
} else if ($node instanceof PhpParser\Node\Stmt\Interface_) {
$this->interfaces[] = ['interfaceName' => (string)$node->name];
}
if (!$node instanceof PhpParser\Node\Stmt\Namespace_) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}
public function getClasses()
{
return $this->classes;
}
public function getTraits()
{
return $this->traits;
}
public function getNamespace()
{
return $this->namespace;
}
public function getInterfaces()
{
return $this->interfaces;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace SilverStripe\Core\Manifest;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
class ClassManifestVisitor extends NodeVisitorAbstract
{
private $classes = [];
private $traits = [];
private $interfaces = [];
public function resetState()
{
$this->classes = [];
$this->traits = [];
$this->interfaces = [];
}
public function beforeTraverse(array $nodes)
{
$this->resetState();
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Class_) {
$extends = '';
$interfaces = [];
if ($node->extends) {
$extends = array((string)$node->extends);
}
if ($node->implements) {
foreach ($node->implements as $interface) {
$interfaces[] = (string)$interface;
}
}
$this->classes[(string)$node->namespacedName] = [
'extends' => $extends,
'interfaces' => $interfaces,
];
} elseif ($node instanceof Node\Stmt\Trait_) {
$this->traits[(string)$node->namespacedName] = array();
} elseif ($node instanceof Node\Stmt\Interface_) {
$extends = array();
foreach ($node->extends as $ancestor) {
$extends[] = (string)$ancestor;
}
$this->interfaces[(string)$node->namespacedName] = [
'extends' => $extends,
];
}
if (!$node instanceof Node\Stmt\Namespace_) {
//break out of traversal as we only need highlevel information here!
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}
public function getClasses()
{
return $this->classes;
}
public function getTraits()
{
return $this->traits;
}
public function getInterfaces()
{
return $this->interfaces;
}
}

View File

@ -1,125 +0,0 @@
<?php
namespace SilverStripe\Core\Manifest;
/**
* A tokenised regular expression is a parser, similar to a regular expression, that acts on tokens rather than
* characters. This is a crucial component of the ManifestBuilder.
*/
class TokenisedRegularExpression
{
/**
* The regular expression definition
*/
protected $expression;
/**
* The first expression to match
*/
protected $firstMatch;
public function __construct($expression)
{
$this->expression = $expression;
$this->firstMatch = is_array($expression[0]) ? $expression[0][0] : $expression[0];
}
public function findAll($tokens)
{
$tokenTypes = array();
foreach ($tokens as $i => $token) {
if (is_array($token)) {
$tokenType = $token[0];
} else {
$tokenType = $token;
// Pre-process string tokens for matchFrom()
$tokens[$i] = array($token, $token);
}
if ($tokenType == $this->firstMatch) {
$tokenTypes[$i] = $tokenType;
}
}
$allMatches = array();
foreach ($tokenTypes as $startKey => $dud) {
$matches = array();
if ($this->matchFrom($startKey, 0, $tokens, $matches)) {
$allMatches[] = $matches;
}
}
return $allMatches;
}
public function matchFrom($tokenPos, $expressionPos, &$tokens, &$matches)
{
$expressionRule = $this->expression[$expressionPos];
$expectation = is_array($expressionRule) ? $expressionRule[0] : $expressionRule;
if (!is_array($expressionRule)) {
$expressionRule = array();
}
if ($expectation == $tokens[$tokenPos][0]) {
if (isset($expressionRule['save_to'])) {
// Append to an array
if (substr($expressionRule['save_to'], -2) == '[]') {
$matches[substr($expressionRule['save_to'], 0, -2)][] = $tokens[$tokenPos][1];
} // Regular variable setting
else {
$matches[$expressionRule['save_to']] = $tokens[$tokenPos][1];
}
}
// End of the expression
if (!isset($this->expression[$expressionPos+1])) {
return true;
// Process next step as normal
} elseif ($this->matchFrom($tokenPos+1, $expressionPos+1, $tokens, $matches)) {
return true;
// This step is optional
} elseif (isset($expressionRule['optional'])
&& $this->matchFrom($tokenPos, $expressionPos+1, $tokens, $matches)) {
return true;
// Process jumps
} elseif (isset($expressionRule['can_jump_to'])) {
if (is_array($expressionRule['can_jump_to'])) {
foreach ($expressionRule['can_jump_to'] as $canJumpTo) {
// can_jump_to & optional both set
if (isset($expressionRule['optional'])
&& $this->matchFrom($tokenPos, $canJumpTo, $tokens, $matches)) {
return true;
}
// can_jump_to set (optional may or may not be set)
if ($this->matchFrom($tokenPos+1, $canJumpTo, $tokens, $matches)) {
return true;
}
}
} else {
// can_jump_to & optional both set
if (isset($expressionRule['optional'])
&& $this->matchFrom($tokenPos, $expressionRule['can_jump_to'], $tokens, $matches)) {
return true;
}
// can_jump_to set (optional may or may not be set)
if ($this->matchFrom($tokenPos+1, $expressionRule['can_jump_to'], $tokens, $matches)) {
return true;
}
}
}
} elseif (isset($expressionRule['optional'])) {
if (isset($this->expression[$expressionPos+1])) {
return $this->matchFrom($tokenPos, $expressionPos+1, $tokens, $matches);
} else {
return true;
}
} elseif (in_array($tokens[$tokenPos][0], array(T_COMMENT, T_DOC_COMMENT, T_WHITESPACE))) {
return $this->matchFrom($tokenPos + 1, $expressionPos, $tokens, $matches);
}
return false;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Core\Manifest\ClassContentRemover;
use SilverStripe\Dev\SapphireTest;
class ClassContentRemoverTest extends SapphireTest
{
public function testRemoveClassContent()
{
$filePath = dirname(__FILE__) . '/fixtures/classcontentremover/ContentRemoverTestA.php';
$cleanContents = ClassContentRemover::remove_class_content($filePath);
$expected = '<?php
namespace TestNamespace\\Testing; use TestNamespace\\{Test1, Test2, Test3}; class MyTest extends Test1 implements Test2 {}';
$this->assertEquals($expected, $cleanContents);
}
public function testRemoveClassContentConditional()
{
$filePath = dirname(__FILE__) . '/fixtures/classcontentremover/ContentRemoverTestB.php';
$cleanContents = ClassContentRemover::remove_class_content($filePath);
$expected = '<?php
namespace TestNamespace\\Testing; use TestNamespace\\{Test1, Test2, Test3}; if (class_exists(\'Class\')) { class MyTest extends Test1 implements Test2 {} class MyTest2 {} }';
$this->assertEquals($expected, $cleanContents);
}
public function testRemoveClassContentNoClass()
{
$filePath = dirname(__FILE__) . '/fixtures/classcontentremover/ContentRemoverTestC.php';
$cleanContents = ClassContentRemover::remove_class_content($filePath);
$this->assertEmpty($cleanContents);
}
public function testRemoveClassContentSillyMethod()
{
$filePath = dirname(__FILE__) . '/fixtures/classcontentremover/ContentRemoverTestD.php';
$cleanContents = ClassContentRemover::remove_class_content($filePath);
$expected = '<?php
class SomeClass {} class AnotherClass {}';
$this->assertEquals($expected, $cleanContents);
}
}

View File

@ -36,57 +36,6 @@ class NamespacedClassManifestTest extends SapphireTest
ClassLoader::instance()->popManifest();
}
/**
* @skipUpgrade
*/
public function testGetImportedNamespaceParser()
{
$file = file_get_contents($this->base . DIRECTORY_SEPARATOR . 'module/classes/ClassI.php');
$tokens = token_get_all($file);
$parsedTokens = ClassManifest::get_imported_namespace_parser()->findAll($tokens);
$expectedItems = array(
array('SilverStripe', '\\', 'Admin', '\\', 'ModelAdmin'),
array('SilverStripe', '\\', 'Control', '\\', 'Controller', ' ', 'as', ' ', 'Cont'),
array(
'SilverStripe', '\\', 'Control', '\\', 'HTTPRequest', ' ', 'as', ' ', 'Request', ',',
'SilverStripe', '\\', 'Control', '\\', 'HTTPResponse', ' ', 'as', ' ', 'Response', ',',
'SilverStripe', '\\', 'Security', '\\', 'PermissionProvider', ' ', 'as', ' ', 'P',
),
array('silverstripe', '\\', 'test', '\\', 'ClassA'),
array('\\', 'SilverStripe', '\\', 'Core', '\\', 'Object'),
);
$this->assertEquals(count($expectedItems), count($parsedTokens));
foreach ($expectedItems as $i => $item) {
$this->assertEquals($item, $parsedTokens[$i]['importString']);
}
}
public function testGetImportsFromTokens()
{
$file = file_get_contents($this->base . DIRECTORY_SEPARATOR . 'module/classes/ClassI.php');
$tokens = token_get_all($file);
$method = new ReflectionMethod($this->manifest, 'getImportsFromTokens');
$method->setAccessible(true);
$expectedImports = array(
'SilverStripe\\Admin\\ModelAdmin',
'Cont' => 'SilverStripe\\Control\\Controller',
'Request' => 'SilverStripe\\Control\\HTTPRequest',
'Response' => 'SilverStripe\\Control\\HTTPResponse',
'P' => 'SilverStripe\\Security\\PermissionProvider',
'silverstripe\\test\\ClassA',
'\\SilverStripe\\Core\\Object',
);
$imports = $method->invoke($this->manifest, $tokens);
$this->assertEquals($expectedImports, $imports);
}
public function testClassInfoIsCorrect()
{
$this->assertContains('SilverStripe\Framework\Tests\ClassI', ClassInfo::implementorsOf('SilverStripe\\Security\\PermissionProvider'));
@ -101,141 +50,6 @@ class NamespacedClassManifestTest extends SapphireTest
$this->assertContains('SilverStripe\Framework\Tests\ClassI', ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin'));
}
/**
* @skipUpgrade
*/
public function testFindClassOrInterfaceFromCandidateImports()
{
$method = new ReflectionMethod($this->manifest, 'findClassOrInterfaceFromCandidateImports');
$method->setAccessible(true);
$this->assertTrue(ClassInfo::exists('silverstripe\test\ClassA'));
$this->assertEquals(
'PermissionProvider',
$method->invokeArgs(
$this->manifest,
[
'\PermissionProvider',
'Test\Namespace',
array(
'TestOnly',
'Controller',
),
]
)
);
$this->assertEquals(
'PermissionProvider',
$method->invokeArgs(
$this->manifest,
array(
'PermissionProvider',
'Test\NAmespace',
array(
'PermissionProvider',
)
)
)
);
$this->assertEmpty(
$method->invokeArgs(
$this->manifest,
array(
'',
'TextNamespace',
array(
'PermissionProvider',
),
)
)
);
$this->assertEmpty(
$method->invokeArgs(
$this->manifest,
array(
'',
'',
array()
)
)
);
$this->assertEquals(
'silverstripe\test\ClassA',
$method->invokeArgs(
$this->manifest,
array(
'ClassA',
'Test\Namespace',
array(
'silverstripe\test\ClassA',
'PermissionProvider',
),
)
)
);
$this->assertEquals(
'ClassA',
$method->invokeArgs(
$this->manifest,
array(
'\ClassA',
'Test\Namespace',
array(
'silverstripe\test',
),
)
)
);
$this->assertEquals(
'ClassA',
$method->invokeArgs(
$this->manifest,
array(
'ClassA',
'silverstripe\test',
array(
'\ClassA',
),
)
)
);
$this->assertEquals(
'ClassA',
$method->invokeArgs(
$this->manifest,
array(
'Alias',
'silverstripe\test',
array(
'Alias' => '\ClassA',
),
)
)
);
$this->assertEquals(
'silverstripe\test\ClassA',
$method->invokeArgs(
$this->manifest,
array(
'ClassA',
'silverstripe\test',
array(
'silverstripe\test\ClassB',
),
)
)
);
}
public function testGetItemPath()
{
$expect = array(

View File

@ -1,201 +0,0 @@
<?php
namespace SilverStripe\Core\Tests\Manifest;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Dev\SapphireTest;
class TokenisedRegularExpressionTest extends SapphireTest
{
public function getTokens()
{
return token_get_all(
<<<PHP
<?php
class ClassA {
}
class ClassB{
}
class ClassC extends ParentClassC {
}
class ClassD extends ParentClassD
implements InterfaceA {
}
interface InterfaceA {
}
interface InterfaceB extends Something{
}
class ClassE extends ParentClassE
implements InterfaceA,InterfaceB {
}
class ClassF extends ParentClassF
implements InterfaceA, InterfaceB {
}
interface InterfaceC extends InterfaceA, InterfaceB {
}
interface InterfaceD extends InterfaceA, InterfaceB, InterfaceC {
}
PHP
);
}
public function getNamespaceTokens()
{
return token_get_all(
<<<PHP
<?php
namespace silverstripe\\test;
class ClassA {
}
class ClassB extends ParentClassB {
}
class ClassC extends \\ParentClassC {
}
class ClassD extends subtest\\ParentClassD {
}
class ClassE implements InterfaceE {
}
class ClassF implements \\InterfaceF {
}
class ClassG implements subtest\\InterfaceG {
}
PHP
);
}
public function testClassDefParser()
{
$parser = ClassManifest::get_class_parser();
$tokens = $this->getTokens();
$matches = $parser->findAll($tokens);
$classes = array();
if ($matches) {
foreach ($matches as $match) {
$classes[$match['className']] = $match;
}
}
$this->assertArrayHasKey('ClassA', $classes);
$this->assertArrayHasKey('ClassB', $classes);
$this->assertArrayHasKey('ClassC', $classes);
$this->assertEquals(array('ParentClassC'), $classes['ClassC']['extends']);
$this->assertArrayHasKey('ClassD', $classes);
$this->assertEquals(array('ParentClassD'), $classes['ClassD']['extends']);
$this->assertContains('InterfaceA', $classes['ClassD']['interfaces']);
$this->assertArrayHasKey('ClassE', $classes);
$this->assertEquals(array('ParentClassE'), $classes['ClassE']['extends']);
$this->assertContains('InterfaceA', $classes['ClassE']['interfaces']);
$this->assertContains('InterfaceB', $classes['ClassE']['interfaces']);
$this->assertArrayHasKey('ClassF', $classes);
$this->assertEquals(array('ParentClassF'), $classes['ClassF']['extends']);
$this->assertContains('InterfaceA', $classes['ClassF']['interfaces']);
$this->assertContains('InterfaceB', $classes['ClassF']['interfaces']);
}
public function testNamesapcedClassDefParser()
{
$parser = ClassManifest::get_namespaced_class_parser();
$tokens = $this->getNamespaceTokens();
$matches = $parser->findAll($tokens);
$classes = array();
if ($matches) {
foreach ($matches as $match) {
$classes[$match['className']] = $match;
}
}
$this->assertArrayHasKey('ClassA', $classes);
$this->assertArrayHasKey('ClassB', $classes);
$this->assertEquals(array('ParentClassB'), $classes['ClassB']['extends']);
$this->assertArrayHasKey('ClassC', $classes);
$this->assertEquals(array('\\', 'ParentClassC'), $classes['ClassC']['extends']);
$this->assertArrayHasKey('ClassD', $classes);
$this->assertEquals(array('subtest', '\\', 'ParentClassD'), $classes['ClassD']['extends']);
$this->assertArrayHasKey('ClassE', $classes);
$this->assertContains('InterfaceE', $classes['ClassE']['interfaces']);
$this->assertArrayHasKey('ClassF', $classes);
$this->assertEquals(array('\\', 'InterfaceF'), $classes['ClassF']['interfaces']);
}
public function testInterfaceDefParser()
{
$parser = ClassManifest::get_interface_parser();
$tokens = $this->getTokens();
$matches = $parser->findAll($tokens);
$interfaces = array();
if ($matches) {
foreach ($matches as $match) {
$interfaces[$match['interfaceName']] = $match;
}
}
$this->assertArrayHasKey('InterfaceA', $interfaces);
$this->assertArrayHasKey('InterfaceB', $interfaces);
$this->assertArrayHasKey('InterfaceC', $interfaces);
$this->assertArrayHasKey('InterfaceD', $interfaces);
}
public function testNamespaceDefParser()
{
$parser = ClassManifest::get_namespace_parser();
$namespacedTokens = $this->getNamespaceTokens();
$tokens = $this->getTokens();
$namespacedMatches = $parser->findAll($namespacedTokens);
$matches = $parser->findAll($tokens);
$this->assertEquals(array(), $matches);
$this->assertEquals(array('silverstripe', '\\', 'test'), $namespacedMatches[0]['namespaceName']);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace TestNamespace\Testing;
use TestNamespace\{Test1, Test2, Test3};
class MyTest extends Test1 implements Test2
{
public function MyMethod()
{
//We shouldn't see anything in here
$var = 1;
$var += 1;
return $var;
}
public function MyNestedMethod()
{
$var = 1;
for ($i = 0; $i < 5; ++$i) {
if ($i % 2) {
$var += $i;
}
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace TestNamespace\Testing;
use TestNamespace\{Test1, Test2, Test3};
if (class_exists('Class')) {
class MyTest extends Test1 implements Test2
{
public function MyMethod()
{
//We shouldn't see anything in here
$var = 1;
$var += 1;
return $var;
}
public function MyNestedMethod()
{
$var = 1;
for ($i = 0; $i < 5; ++$i) {
if ($i % 2) {
$var += $i;
}
}
}
}
class MyTest2
{
public function SecondClassMethod() {
return 'witty remark';
}
}
}

View File

@ -0,0 +1,9 @@
<?php
$var = false;
if ($var) {
$var = !$var;
} else {
$var = $var + 1;
}

View File

@ -0,0 +1,13 @@
<?php
class SomeClass
{
function bob()
{
return '{' . '{' . '{';
}
}
class AnotherClass
{
}