diff --git a/doc/language.dot b/doc/language.dot index 3ee6601..3de5652 100644 --- a/doc/language.dot +++ b/doc/language.dot @@ -79,8 +79,8 @@ rankdir=TB; { rank=same; owning3; TraitImpl [fixedsize="1", width="2", height="2", shape="pentagon", style="filled", color="hotpink"]; - traitDoSomething [fixedsize="1", width="2", height="2", shape="rectangle", style="filled", label="doSomething"]; - "The trait TraitImpl is owning doSomething() method" [shape=plaintext]; + traitDoSomething [fixedsize="1", width="2", height="2", shape="rectangle", style="filled", label="TraitImpl\ndoSomething"]; + "The trait TraitImpl is owning TraitImpl::doSomething() method" [shape=plaintext]; TraitImpl -> traitDoSomething [label="owns"]; } @@ -110,9 +110,9 @@ rankdir=TB; { rank=same; depend4; - getSize [fixedsize="1", width="2", height="2", shape="rectangle", style="filled", label="getSize"]; + getSize [fixedsize="1", width="2", height="2", shape="rectangle", style="filled", label="Image\ngetSize"]; Image [fixedsize="1", width="2", height="2", shape="pentagon", style="filled", color="hotpink"]; - "The implementation getSize() \ndepends on Image trait" [shape=plaintext]; + "The implementation Image::getSize() \ndepends on Image trait" [shape=plaintext]; getSize -> Image [label="depends"]; } diff --git a/doc/language.gif b/doc/language.gif index 05af07c..166697d 100644 Binary files a/doc/language.gif and b/doc/language.gif differ diff --git a/phpunit.xml b/phpunit.xml index 56956a2..8c539b4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ + colors="false"> diff --git a/src/Analysis/HiddenCoupling.php b/src/Analysis/HiddenCoupling.php index 8d2fee6..487d43d 100644 --- a/src/Analysis/HiddenCoupling.php +++ b/src/Analysis/HiddenCoupling.php @@ -11,7 +11,7 @@ use Trismegiste\Mondrian\Transform\Vertex\MethodVertex; use Trismegiste\Mondrian\Transform\Vertex\ClassVertex; use Trismegiste\Mondrian\Transform\Vertex\InterfaceVertex; -use Trismegiste\Mondrian\Graph\Edge; +use Trismegiste\Mondrian\Transform\Vertex\TraitVertex; /** * HiddenCoupling is an analyser which checks and finds hidden coupling @@ -62,8 +62,8 @@ public function createReducedGraph() $reducedGraph = new \Trismegiste\Mondrian\Graph\Digraph(); $dependency = $this->getEdgeSet(); foreach ($dependency as $edge) { - if (($edge->getSource() instanceof ImplVertex) - && ($edge->getTarget() instanceof MethodVertex)) { + if (($edge->getSource() instanceof ImplVertex) && + ($edge->getTarget() instanceof MethodVertex)) { $this->resetVisited(); $edge->visited = true; @@ -88,8 +88,8 @@ protected function findOwningClassVertex(ImplVertex $impl) { list($className, $methodName) = explode('::', $impl->getName()); foreach ($this->graph->getSuccessor($impl) as $succ) { - if (($succ instanceof ClassVertex) - && ($succ->getName() == $className)) { + if ((($succ instanceof ClassVertex) || ($succ instanceof TraitVertex)) && + ($succ->getName() == $className)) { return $succ; } diff --git a/src/Builder/Compiler/AbstractTraverser.php b/src/Builder/Compiler/AbstractTraverser.php index a0d1781..ba874ef 100644 --- a/src/Builder/Compiler/AbstractTraverser.php +++ b/src/Builder/Compiler/AbstractTraverser.php @@ -6,7 +6,8 @@ namespace Trismegiste\Mondrian\Builder\Compiler; -use Trismegiste\Mondrian\Visitor; +use PhpParser\NodeVisitor; +use PhpParser\NodeTraverser; /** * AbstractTraverser partly builds the compiler with a traverser @@ -14,9 +15,9 @@ abstract class AbstractTraverser implements BuilderInterface { - public function buildTraverser(Visitor\FqcnHelper $collector) + public function buildTraverser(NodeVisitor $collector) { - $traverser = new \PHPParser_NodeTraverser(); + $traverser = new NodeTraverser(); $traverser->addVisitor($collector); return $traverser; diff --git a/src/Builder/Compiler/BuilderInterface.php b/src/Builder/Compiler/BuilderInterface.php index fe50f28..abd518a 100644 --- a/src/Builder/Compiler/BuilderInterface.php +++ b/src/Builder/Compiler/BuilderInterface.php @@ -2,7 +2,7 @@ namespace Trismegiste\Mondrian\Builder\Compiler; -use Trismegiste\Mondrian\Visitor\FqcnHelper; +use PhpParser\NodeVisitor; /** * BuilderInterface is a contract to build a compiler @@ -14,5 +14,5 @@ public function buildContext(); public function buildCollectors(); - public function buildTraverser(FqcnHelper $collector); + public function buildTraverser(NodeVisitor $collector); } \ No newline at end of file diff --git a/src/Transform/GraphBuilder.php b/src/Transform/GraphBuilder.php index 3915dac..6e621ac 100644 --- a/src/Transform/GraphBuilder.php +++ b/src/Transform/GraphBuilder.php @@ -57,9 +57,9 @@ public function buildContext() public function buildCollectors() { return array( - new Visitor\SymbolMap($this->reflection), - new Visitor\VertexCollector($this->reflection, $this->vertexContext, $this->graphResult), - new Visitor\EdgeCollector($this->reflection, $this->vertexContext, $this->graphResult) + new Visitor\SymbolMap\Collector($this->reflection, $this->vertexContext, $this->graphResult), + new Visitor\Vertex\Collector($this->reflection, $this->vertexContext, $this->graphResult), + new Visitor\Edge\Collector($this->reflection, $this->vertexContext, $this->graphResult) ); } diff --git a/src/Transform/ReflectionContext.php b/src/Transform/ReflectionContext.php index 396614c..3d297a0 100644 --- a/src/Transform/ReflectionContext.php +++ b/src/Transform/ReflectionContext.php @@ -13,6 +13,8 @@ * * Responsible for maintaining a list of methods, traits, classes and interfaces used * for building inheritance links in a digraph + * + * @todo this class lacks an interface */ class ReflectionContext { @@ -164,6 +166,21 @@ public function initSymbol($name, $symbolType) } } + public function initClass($name) + { + $this->initSymbol($name, self::SYMBOL_CLASS); + } + + public function initInterface($name) + { + $this->initSymbol($name, self::SYMBOL_INTERFACE); + } + + public function initTrait($name) + { + $this->initSymbol($name, self::SYMBOL_TRAIT); + } + /** * Stacks a parent type for a type * diff --git a/src/Visitor/Edge/ClassLevel.php b/src/Visitor/Edge/ClassLevel.php new file mode 100644 index 0000000..196a0cf --- /dev/null +++ b/src/Visitor/Edge/ClassLevel.php @@ -0,0 +1,86 @@ +getType()) { + + case 'Stmt_ClassMethod': + if ($node->isPublic()) { + $this->context->pushState('class-method', $node); + $this->enterPublicMethod($node); + } + break; + + case 'Stmt_TraitUse': + $fqcn = $this->getCurrentFqcn(); + $currentVertex = $this->findVertex('class', $fqcn); + $this->enterTraitUse($node, $currentVertex); + break; + } + } + + public function getName() + { + return 'class'; + } + + protected function enterPublicMethod(Stmt\ClassMethod $node) + { + // NS + $methodName = $node->name; + $currentFqcn = $this->getCurrentFqcn(); + $declaringFqcn = $this->getReflectionContext()->getDeclaringClass($currentFqcn, $methodName); + // Vertices + $signatureIndex = $declaringFqcn . '::' . $methodName; + $classVertex = $this->findVertex('class', $currentFqcn); + $signatureVertex = $this->findVertex('method', $signatureIndex); + $implVertex = $this->findVertex('impl', $currentFqcn . '::' . $methodName); + + // if current class == declaring class, we add the edge + if ($declaringFqcn == $currentFqcn) { + $this->getGraph()->addEdge($classVertex, $signatureVertex); // C -> M + if (!$node->isAbstract()) { + $this->getGraph()->addEdge($signatureVertex, $implVertex); // M -> S + $this->getGraph()->addEdge($implVertex, $classVertex); // S -> C + } + } else { + if (!$node->isAbstract()) { + $this->getGraph()->addEdge($classVertex, $implVertex); // C -> S + $this->getGraph()->addEdge($implVertex, $classVertex); // S -> C + } + } + + // in any case, we link the implementation to the params + foreach ($node->params as $idx => $param) { + // adding edge from signature to param : + $paramVertex = $this->findVertex('param', $signatureIndex . '/' . $idx); + // it is possible to not find the param because the signature + // is external to the source code : + if (!is_null($paramVertex)) { + if (!$node->isAbstract()) { + $this->getGraph()->addEdge($implVertex, $paramVertex); // S -> P + } + if ($currentFqcn === $declaringFqcn) { + $this->getGraph()->addEdge($signatureVertex, $paramVertex); // M -> P + // now the type of the param : + $this->typeHintParam($param, $paramVertex); + } + } + } + } + +} \ No newline at end of file diff --git a/src/Visitor/Edge/ClassMethodLevel.php b/src/Visitor/Edge/ClassMethodLevel.php new file mode 100644 index 0000000..fb8747d --- /dev/null +++ b/src/Visitor/Edge/ClassMethodLevel.php @@ -0,0 +1,25 @@ +getNamespacedName($node); + $src = $this->findVertex('class', $fqcn); + + // extends + if (!is_null($node->extends)) { + if (null !== $dst = $this->findVertex('class', (string) $this->resolveClassName($node->extends))) { + $this->getGraph()->addEdge($src, $dst); + } + } + // implements + foreach ($node->implements as $interf) { + if (null !== $dst = $this->findVertex('interface', (string) $this->resolveClassName($interf))) { + $this->getGraph()->addEdge($src, $dst); + } + } + } + + protected function enterInterfaceNode(Stmt\Interface_ $node) + { + $fqcn = $this->getNamespacedName($node); + $src = $this->findVertex('interface', $fqcn); + + // implements + foreach ($node->extends as $interf) { + if (null !== $dst = $this->findVertex('interface', (string) $this->resolveClassName($interf))) { + $this->getGraph()->addEdge($src, $dst); + } + } + } + + protected function enterTraitNode(Stmt\Trait_ $node) + { + // no edge to create + } + +} \ No newline at end of file diff --git a/src/Visitor/Edge/InterfaceLevel.php b/src/Visitor/Edge/InterfaceLevel.php new file mode 100644 index 0000000..639a35a --- /dev/null +++ b/src/Visitor/Edge/InterfaceLevel.php @@ -0,0 +1,53 @@ +getType() == 'Stmt_ClassMethod') { + // NS + $methodName = $node->name; + $currentFqcn = $this->getCurrentFqcn(); + $declaringFqcn = $this->getReflectionContext()->getDeclaringClass($currentFqcn, $methodName); + // Vertices + $signatureIndex = $declaringFqcn . '::' . $methodName; + $classVertex = $this->findVertex('interface', $currentFqcn); + $signatureVertex = $this->findVertex('method', $signatureIndex); + + // if current class == declaring class, we add the edge + if ($declaringFqcn == $currentFqcn) { + $this->getGraph()->addEdge($classVertex, $signatureVertex); // I -> M + // and we link the signature to the params + foreach ($node->params as $idx => $param) { + // adding edge from signature to param : + $paramVertex = $this->findVertex('param', $signatureIndex . '/' . $idx); + // it is possible to not find the param because the signature + // is external to the source code : + if (!is_null($paramVertex)) { + $this->getGraph()->addEdge($signatureVertex, $paramVertex); // M -> P + // now the type of the param : + $this->typeHintParam($param, $paramVertex); + } + } + } + } + } + + public function getName() + { + return 'interface'; + } + +} \ No newline at end of file diff --git a/src/Visitor/Edge/MethodLevelHelper.php b/src/Visitor/Edge/MethodLevelHelper.php new file mode 100644 index 0000000..75244d8 --- /dev/null +++ b/src/Visitor/Edge/MethodLevelHelper.php @@ -0,0 +1,184 @@ +currentFqcn = $this->context + ->getState('file') + ->getNamespacedName($this->context->getNodeFor($this->getParentName())); + $this->currentMethodNode = $this->context->getNodeFor($this->getName()); + $this->fileState = $this->context->getState('file'); + + switch ($node->getType()) { + + case 'Expr_MethodCall' : + $this->enterMethodCall($node); + break; + + case 'Expr_New': + $this->enterNewInstance($node); + break; + + case 'Expr_StaticCall': + $this->enterStaticCall($node); + break; + } + } + + /** + * Links the current implementation vertex to all methods with the same + * name. Filters on some obvious cases. + * + * @param Node\Expr\MethodCall $node + */ + protected function enterMethodCall(Node\Expr\MethodCall $node) + { + if (is_string($node->name)) { + $this->enterNonDynamicMethodCall($node); + } + } + + /** + * Process of simple call of a method + * Sample: $obj->getThing($arg); + * Do not process : call_user_func(array($obj, 'getThing'), $arg); + * Do not process : $reflectionMethod->invoke($obj, 'getThing', $arg); + * + * @param Node\Expr\MethodCall $node + */ + protected function enterNonDynamicMethodCall(Node\Expr\MethodCall $node) + { + $method = $node->name; + $candidate = null; + // skipping some obvious calls : + if (($node->var->getType() == 'Expr_Variable') && (is_string($node->var->name))) { + // searching a candidate for $called::$method + // I think there is a chain of responsibility beneath that : + $candidate = $this->getCalledMethodVertexOn($node->var->name, $method); + } + // fallback : link to every methods with the same name : + if (is_null($candidate)) { + $candidate = $this->getGraphContext() + ->findAllMethodSameName($method); + if (count($candidate)) { + // store the fallback for futher report + foreach ($candidate as $called) { + $this->getGraphContext() + ->logFallbackCall($this->currentFqcn, $this->currentMethodNode->name, $called->getName()); + } + } + } + $impl = $this->findVertex('impl', $this->currentFqcn . '::' . $this->currentMethodNode->name); + // fallback or not, we exclude calls from annotations + $exclude = $this->getGraphContext() + ->getExcludedCall($this->currentFqcn, $this->currentMethodNode->name); + foreach ($candidate as $methodVertex) { + if (!in_array($methodVertex->getName(), $exclude)) { + $this->getGraph()->addEdge($impl, $methodVertex); + } + } + } + + /** + * Try to find a signature to link with the method to call and the object against to + * + * @param string $called + * @param string $method + * @return null|array null if cannot determine vertex or an array of vertices (can be empty if no call must be made) + */ + protected function getCalledMethodVertexOn($called, $method) + { + // skipping $this : + if ($called == 'this') { + return array(); // nothing to call + } + + // checking if the called is a method param + $idx = false; + foreach ($this->currentMethodNode->params as $k => $paramSign) { + if ($paramSign->name == $called) { + $idx = $k; + break; + } + } + if (false !== $idx) { + $param = $this->currentMethodNode->params[$idx]; + // is it a typed param ? + if ($param->type instanceof \PHPParser_Node_Name) { + $paramType = (string) $this->fileState->resolveClassName($param->type); + // we check if it is an outer class or not : is it known ? + if (!is_null($cls = $this->findMethodInInheritanceTree($paramType, $method))) { + if (!is_null($signature = $this->findVertex('method', "$cls::$method"))) { + return array($signature); + } + } + } + } + + return null; // can't see shit captain + } + + /** + * Check if the class exists before searching for the + * declaring class of the method, because class could be unknown, outside + * or code could be bugged + */ + protected function findMethodInInheritanceTree($cls, $method) + { + if ($this->context->getReflectionContext()->hasDeclaringClass($cls)) { + return $this->context->getReflectionContext()->findMethodInInheritanceTree($cls, $method); + } + + return null; + } + + /** + * Visits a "new" statement node + * + * Add an edge from current implementation to the class which a new instance + * is created + * + * @param \PHPParser_Node_Expr_New $node + */ + protected function enterNewInstance(Node\Expr\New_ $node) + { + if ($node->class instanceof Node\Name) { + $classVertex = $this->findVertex('class', (string) $this->fileState->resolveClassName($node->class)); + if (!is_null($classVertex)) { + $impl = $this->findVertex('impl', $this->currentFqcn . '::' . $this->currentMethodNode->name); + $this->getGraph()->addEdge($impl, $classVertex); + } + } + } + + protected function enterStaticCall(Node\Expr\StaticCall $node) + { + if (($node->class instanceof Node\Name) && is_string($node->name)) { + $impl = $this->findVertex('impl', $this->currentFqcn . '::' . $this->currentMethodNode->name); + $target = $this->findVertex('method', (string) $this->fileState->resolveClassName($node->class) . '::' . $node->name); + if (!is_null($target)) { + $this->getGraph()->addEdge($impl, $target); + } + } + } + + abstract protected function getParentName(); +} \ No newline at end of file diff --git a/src/Visitor/Edge/ObjectLevelHelper.php b/src/Visitor/Edge/ObjectLevelHelper.php new file mode 100644 index 0000000..f8b704e --- /dev/null +++ b/src/Visitor/Edge/ObjectLevelHelper.php @@ -0,0 +1,50 @@ +findVertex($pool, $type); + if (!is_null($typeVertex)) { + return $typeVertex; + } + } + + return null; + } + + protected function typeHintParam(Param $param, ParamVertex $source) + { + if ($param->type instanceof \PhpParser\Node\Name) { + $paramType = (string) $this->context->getState('file')->resolveClassName($param->type); + // there is a type, we add a link to the type, if it is found + $typeVertex = $this->findTypeVertex($paramType); + if (!is_null($typeVertex)) { + // we add the edge + $this->getGraph()->addEdge($source, $typeVertex); + } + } + } + +} \ No newline at end of file diff --git a/src/Visitor/Edge/TraitLevel.php b/src/Visitor/Edge/TraitLevel.php new file mode 100644 index 0000000..807fcb9 --- /dev/null +++ b/src/Visitor/Edge/TraitLevel.php @@ -0,0 +1,77 @@ +getType()) { + + case 'Stmt_ClassMethod': + if ($node->isPublic()) { + $this->context->pushState('trait-method', $node); + $this->enterPublicMethod($node); + } + break; + + case 'Stmt_TraitUse': + $fqcn = $this->getCurrentFqcn(); + $currentVertex = $this->findVertex('trait', $fqcn); + $this->enterTraitUse($node, $currentVertex); + break; + } + } + + public function getName() + { + return 'trait'; + } + + protected function enterPublicMethod(Node\Stmt\ClassMethod $node) + { + // NS + $methodName = $node->name; + $currentFqcn = $this->getCurrentFqcn(); + // Vertices + $traitVertex = $this->findVertex('trait', $currentFqcn); + $implVertex = $this->findVertex('impl', $currentFqcn . '::' . $methodName); + // edge between impl and trait : + $this->getGraph()->addEdge($implVertex, $traitVertex); + $this->getGraph()->addEdge($traitVertex, $implVertex); + + // edges between impl towards param (with typed param) + foreach ($node->params as $idx => $param) { + // adding edge from implementation to param : + $paramVertex = $this->findVertex('param', "$currentFqcn::$methodName/$idx"); + $this->getGraph()->addEdge($implVertex, $paramVertex); + // now the type of the param : + $this->typeHintParam($param, $paramVertex); + } + + // edge between class vertex which using the trait and copy-pasted methods : + $traitUser = $this->getReflectionContext()->getClassesUsingTraitForDeclaringMethod($currentFqcn, $methodName); + foreach ($traitUser as $classname) { + // we link the class and the signature + $source = $this->findVertex('class', $classname); + $target = $this->findVertex('method', $classname . '::' . $methodName); + $this->getGraph()->addEdge($source, $target); + // and we link the copypasted signature to unique parameter + foreach ($node->params as $idx => $param) { + $paramVertex = $this->findVertex('param', "$currentFqcn::$methodName/$idx"); + $this->getGraph()->addEdge($target, $paramVertex); + } + } + } + +} \ No newline at end of file diff --git a/src/Visitor/Edge/TraitMethodLevel.php b/src/Visitor/Edge/TraitMethodLevel.php new file mode 100644 index 0000000..8e0340f --- /dev/null +++ b/src/Visitor/Edge/TraitMethodLevel.php @@ -0,0 +1,25 @@ +context->getState('file'); + foreach ($node->traits as $import) { + $name = (string) $fileState->resolveClassName($import); + $target = $this->findVertex('trait', $name); + // it's possible to not find a trait if it coming from an external library for example + // or could be dead code too + if (!is_null($target)) { + $this->getGraph()->addEdge($source, $target); + } + } + } + +} \ No newline at end of file diff --git a/src/Visitor/EdgeCollector.php b/src/Visitor/EdgeCollector.php deleted file mode 100644 index 8d05c86..0000000 --- a/src/Visitor/EdgeCollector.php +++ /dev/null @@ -1,429 +0,0 @@ -currentMethod) { - - switch ($node->getType()) { - - case 'Expr_MethodCall' : - $this->enterMethodCall($node); - break; - - case 'Expr_New': - $this->enterNewInstance($node); - break; - - case 'Expr_StaticCall': - $this->enterStaticCall($node); - break; - } - } - - // edge for use trait - if ($node->getType() === 'Stmt_TraitUse') { - $this->enterTraitUse($node); - } - } - - /** - * {@inheritDoc} - */ - public function leaveNode(\PHPParser_Node $node) - { - parent::leaveNode($node); - - switch ($node->getType()) { - - case 'Stmt_Class': - case 'Stmt_Interface': - case 'Stmt_Trait': - $this->currentClassVertex = null; - break; - - case 'Stmt_ClassMethod' : - $this->currentMethodNode = null; - break; - } - } - - /** - * Find a ParamVertex by its [classname x mehodName x position] - * @param string $className - * @param string $methodName - * @param int $idx - * @return ParamVertex - */ - protected function findParamVertexIdx($className, $methodName, $idx) - { - return $this->findVertex('param', $className . '::' . $methodName . '/' . $idx); - } - - /** - * Find a class or interface - * - * @param string $type fqcn to be found - * @return Vertex - */ - protected function findTypeVertex($type) - { - foreach (array('class', 'interface') as $pool) { - $typeVertex = $this->findVertex($pool, $type); - if (!is_null($typeVertex)) { - return $typeVertex; - } - } - - return null; - } - - /** - * Process the method node and adding the vertex of the first declared method - * - * @param \PHPParser_Node_Stmt_ClassMethod $node - * @param \Trismegiste\Mondrian\Transform\Vertex\MethodVertex $signature - */ - protected function enterDeclaredMethodNode(\PHPParser_Node_Stmt_ClassMethod $node, MethodVertex $signature) - { - $this->graph->addEdge($this->currentClassVertex, $signature); - // managing params of the signature : - foreach ($node->params as $idx => $param) { - // adding edge from signature to param : - $paramVertex = $this->findParamVertexIdx($this->currentClass, $this->currentMethod, $idx); - $this->graph->addEdge($signature, $paramVertex); - // now the type of the param : - if ($param->type instanceof \PHPParser_Node_Name) { - $paramType = (string) $this->resolveClassName($param->type); - // there is a type, we add a link to the type, if it is found - $typeVertex = $this->findTypeVertex($paramType); - if (!is_null($typeVertex)) { - // we add the edge - $this->graph->addEdge($paramVertex, $typeVertex); - } - } - } - } - - /** - * Process the implementation vertex with the method node - * - * @param \PHPParser_Node_Stmt_ClassMethod $node - * @param MethodVertex|null $signature the first declaring method vertex - * @param string $declaringClass the first declaring class of this method - */ - protected function enterImplementationNode(\PHPParser_Node_Stmt_ClassMethod $node, $signature, $declaringClass) - { - $impl = $this->findVertex('impl', $this->currentClass . '::' . $node->name); - $this->graph->addEdge($impl, $this->currentClassVertex); - // who is embedding the impl ? - if ($declaringClass == $this->currentClass) { - $this->graph->addEdge($signature, $impl); - } else { - $this->graph->addEdge($this->currentClassVertex, $impl); - } - // in any case, we link the implementation to the params - foreach ($node->params as $idx => $param) { - // adding edge from signature to param : - $paramVertex = $this->findParamVertexIdx($declaringClass, $this->currentMethod, $idx); - // it is possible to not find the param because the signature - // is external to the source code : - if (!is_null($paramVertex)) { - $this->graph->addEdge($impl, $paramVertex); - } - } - } - - /** - * {@inheritDoc} - */ - protected function enterPublicMethodNode(\PHPParser_Node_Stmt_ClassMethod $node) - { - $this->currentMethodNode = $node; - - if ($this->isTrait($this->currentClass)) { - $this->enterTraitMethod($node); - } elseif ($this->isInterface($this->currentClass)) { - $this->enterInterfaceMethod($node); - } else { - $this->enterClassMethod($node); - } - } - - private function enterInterfaceMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // search for the declaring class of this method - $declaringClass = $this->getDeclaringClass($this->currentClass, $this->currentMethod); - $signature = $this->findVertex('method', $declaringClass . '::' . $node->name); - // if current class == declaring class, we add the edge - if ($declaringClass == $this->currentClass) { - $this->enterDeclaredMethodNode($node, $signature); - } - } - - private function enterClassMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // search for the declaring class of this method - $declaringClass = $this->getDeclaringClass($this->currentClass, $this->currentMethod); - $signature = $this->findVertex('method', $declaringClass . '::' . $node->name); - // if current class == declaring class, we add the edge - if ($declaringClass == $this->currentClass) { - $this->enterDeclaredMethodNode($node, $signature); - } - // if not abstract, the implementation depends on the class. - // For odd reason, a method in an interface is not abstract - // that's why, there is a double check - if (!$node->isAbstract()) { - $this->enterImplementationNode($node, $signature, $declaringClass); - } - } - - private function enterTraitMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // edge between impl and trait : - $implVetex = $this->findVertex('impl', $this->getCurrentMethodIndex()); - $traitVertex = $this->currentClassVertex; - $this->graph->addEdge($implVetex, $traitVertex); - $this->graph->addEdge($traitVertex, $implVetex); - - // edges between impl towards param (with typed param) - foreach ($node->params as $idx => $param) { - // adding edge from implementation to param : - $paramVertex = $this->findParamVertexIdx($this->currentClass, $this->currentMethod, $idx); - $this->graph->addEdge($implVetex, $paramVertex); - // now the type of the param : - if ($param->type instanceof \PHPParser_Node_Name) { - $paramType = (string) $this->resolveClassName($param->type); - // there is a type, we add a link to the type, if it is found - $typeVertex = $this->findTypeVertex($paramType); - if (!is_null($typeVertex)) { - // we add the edge - $this->graph->addEdge($paramVertex, $typeVertex); - } - } - } - - // edge between class vertex which using the trait and copy-pasted methods : - $traitUser = $this->getClassesUsingTraitForDeclaringMethod($this->currentClass, $this->currentMethod); - foreach ($traitUser as $classname) { - // we link the class and the signature - $source = $this->findVertex('class', $classname); - $target = $this->findVertex('method', $classname . '::' . $this->currentMethod); - $this->graph->addEdge($source, $target); - // and copypasted signature to unique parameter - foreach ($node->params as $idx => $param) { - $paramVertex = $this->findParamVertexIdx($this->currentClass, $this->currentMethod, $idx); - $this->graph->addEdge($target, $paramVertex); - } - } - } - - /** - * {@inheritDoc} - */ - protected function enterInterfaceNode(\PHPParser_Node_Stmt_Interface $node) - { - $src = $this->findVertex('interface', $this->currentClass); - $this->currentClassVertex = $src; - - // implements - foreach ($node->extends as $interf) { - if (null !== $dst = $this->findVertex('interface', (string) $this->resolveClassName($interf))) { - $this->graph->addEdge($src, $dst); - } - } - } - - /** - * {@inheritDoc} - */ - protected function enterClassNode(\PHPParser_Node_Stmt_Class $node) - { - $src = $this->findVertex('class', $this->currentClass); - $this->currentClassVertex = $src; - - // extends - if (!is_null($node->extends)) { - if (null !== $dst = $this->findVertex('class', (string) $this->resolveClassName($node->extends))) { - $this->graph->addEdge($src, $dst); - } - } - // implements - foreach ($node->implements as $interf) { - if (null !== $dst = $this->findVertex('interface', (string) $this->resolveClassName($interf))) { - $this->graph->addEdge($src, $dst); - } - } - } - - /** - * Links the current implementation vertex to all methods with the same - * name. Filters on some obvious cases. - * - * @param \PHPParser_Node_Expr_MethodCall $node - * @return void - * - */ - protected function enterMethodCall(\PHPParser_Node_Expr_MethodCall $node) - { - if (is_string($node->name)) { - $this->enterNonDynamicMethodCall($node); - } - } - - protected function enterStaticCall(\PHPParser_Node_Expr_StaticCall $node) - { - if (($node->class instanceof \PHPParser_Node_Name) && is_string($node->name)) { - $impl = $this->findVertex('impl', $this->getCurrentMethodIndex()); - $target = $this->findVertex('method', (string) $this->resolveClassName($node->class) . '::' . $node->name); - if (!is_null($target)) { - $this->graph->addEdge($impl, $target); - } - } - } - - /** - * Try to find a signature to link with the method to call and the object against to - * - * @param string $called - * @param string $method - * @return null|array null if cannot determine vertex or an array of vertices (can be empty if no call must be made) - */ - protected function getCalledMethodVertexOn($called, $method) - { - // skipping $this : - if ($called == 'this') { - return array(); // nothing to call - } - - // checking if the called is a method param - $idx = false; - foreach ($this->currentMethodNode->params as $k => $paramSign) { - if ($paramSign->name == $called) { - $idx = $k; - break; - } - } - if (false !== $idx) { - $param = $this->currentMethodNode->params[$idx]; - // is it a typed param ? - if ($param->type instanceof \PHPParser_Node_Name) { - $paramType = (string) $this->resolveClassName($param->type); - // we check if it is an outer class or not : is it known ? - if (!is_null($cls = $this->findMethodInInheritanceTree($paramType, $method))) { - if (!is_null($signature = $this->findVertex('method', "$cls::$method"))) { - return array($signature); - } - } - } - } - - return null; // can't see shit captain - } - - /** - * Process of simple call of a method - * Sample: $obj->getThing($arg); - * Do not process : call_user_func(array($obj, 'getThing'), $arg); - * Do not process : $reflectionMethod->invoke($obj, 'getThing', $arg); - * - * @param \PHPParser_Node_Expr_MethodCall $node - * @return void - */ - protected function enterNonDynamicMethodCall(\PHPParser_Node_Expr_MethodCall $node) - { - $method = $node->name; - $candidate = null; - // skipping some obvious calls : - if (($node->var->getType() == 'Expr_Variable') && (is_string($node->var->name))) { - // searching a candidate for $called::$method - // I think there is a chain of responsibility beneath that : - $candidate = $this->getCalledMethodVertexOn($node->var->name, $method); - } - // fallback : link to every methods with the same name : - if (is_null($candidate)) { - $candidate = $this->findAllMethodSameName($method); - if (count($candidate)) { - // store the fallback for futher report - foreach ($candidate as $called) { - $this->logFallbackCall($this->currentClass, $this->currentMethod, $called->getName()); - } - } - } - $impl = $this->findVertex('impl', $this->currentClass . '::' . $this->currentMethod); - // fallback or not, we exclude calls from annotations - $exclude = $this->getExcludedCall($this->currentClass, $this->currentMethod); - foreach ($candidate as $methodVertex) { - if (!in_array($methodVertex->getName(), $exclude)) { - $this->graph->addEdge($impl, $methodVertex); - } - } - } - - /** - * Visits a "new" statement node - * - * Add an edge from current implementation to the class which a new instance - * is created - * - * @param \PHPParser_Node_Expr_New $node - */ - protected function enterNewInstance(\PHPParser_Node_Expr_New $node) - { - if ($node->class instanceof \PHPParser_Node_Name) { - $classVertex = $this->findVertex('class', (string) $this->resolveClassName($node->class)); - if (!is_null($classVertex)) { - $impl = $this->findVertex('impl', $this->getCurrentMethodIndex()); - $this->graph->addEdge($impl, $classVertex); - } - } - } - - protected function enterTraitNode(\PHPParser_Node_Stmt_Trait $node) - { - $src = $this->findVertex('trait', $this->currentClass); - $this->currentClassVertex = $src; - } - - protected function enterTraitUse(\PHPParser_Node_Stmt_TraitUse $node) - { - if (!$this->currentClassVertex) { - throw new \LogicException('using a trait when not in a class'); - } - - foreach ($node->traits as $import) { - $name = (string) $this->resolveClassName($import); - $target = $this->findVertex('trait', $name); - // it's possible to not find a trait if it is from an external library for example - // or could be dead code too - if (!is_null($target)) { - $this->graph->addEdge($this->currentClassVertex, $target); - } - } - } - -} diff --git a/src/Visitor/PassCollector.php b/src/Visitor/PassCollector.php deleted file mode 100644 index 7bc606c..0000000 --- a/src/Visitor/PassCollector.php +++ /dev/null @@ -1,144 +0,0 @@ -reflection = $ref; - $this->vertexDict = $grf; - $this->graph = $g; - } - - /** - * Finds the FQCN of the first declaring class/interface of a method - * - * @param string $cls subclass name - * @param string $meth method name - * @return string - */ - protected function getDeclaringClass($cls, $meth) - { - return $this->reflection->getDeclaringClass($cls, $meth); - } - - /** - * Is FQCN an interface ? - * - * @param string $cls FQCN - * - * @return bool - */ - protected function isInterface($cls) - { - return $this->reflection->isInterface($cls); - } - - /** - * Is FQCN a trait ? - * - * @param string $cls FQCN - * - * @return bool - */ - protected function isTrait($cls) - { - return $this->reflection->isTrait($cls); - } - - /** - * Find a vertex by its type and name - * - * @param string $type - * @param string $key - * @return Vertex or null - */ - protected function findVertex($type, $key) - { - return $this->vertexDict->findVertex($type, $key); - } - - /** - * See Context - */ - protected function findAllMethodSameName($method) - { - return $this->vertexDict->findAllMethodSameName($method); - } - - /** - * See Context - */ - protected function existsVertex($type, $key) - { - return $this->vertexDict->existsVertex($type, $key); - } - - /** - * Check if the class exists before searching for the - * declaring class of the method, because class could be unknown, outside - * or code could be bugged - */ - protected function findMethodInInheritanceTree($cls, $method) - { - if ($this->reflection->hasDeclaringClass($cls)) { - return $this->reflection->findMethodInInheritanceTree($cls, $method); - } - - return null; - } - - /** - * Returns a list of all classes using a trait for declaring a given method - * - * @param string $cls FQCN of trait - * - * @return array - */ - protected function getClassesUsingTraitForDeclaringMethod($cls, $method) - { - return $this->reflection->getClassesUsingTraitForDeclaringMethod($cls, $method); - } - - /** - * See Context - */ - protected function indicesVertex($typ, $index, Vertex $v) - { - $this->vertexDict->indicesVertex($typ, $index, $v); - } - - protected function logFallbackCall($class, $method, $called) - { - $this->vertexDict->logFallbackCall($class, $method, $called); - } - - protected function getExcludedCall($class, $method) - { - return $this->vertexDict->getExcludedCall($class, $method); - } - -} diff --git a/src/Visitor/State/AbstractObjectLevel.php b/src/Visitor/State/AbstractObjectLevel.php new file mode 100644 index 0000000..3b1fdbc --- /dev/null +++ b/src/Visitor/State/AbstractObjectLevel.php @@ -0,0 +1,29 @@ +context->getNodeFor($this->getName()); + $fileState = $this->context->getState('file'); + $fqcn = $fileState->getNamespacedName($objectNode); + + return $fqcn; + } + +} \ No newline at end of file diff --git a/src/Visitor/State/AbstractState.php b/src/Visitor/State/AbstractState.php new file mode 100644 index 0000000..9d8942f --- /dev/null +++ b/src/Visitor/State/AbstractState.php @@ -0,0 +1,73 @@ +context = $ctx; + } + + /** + * @inheritdoc + */ + public function leave(Node $node) + { + + } + + /** + * @return \Trismegiste\Mondrian\Transform\ReflectionContext + */ + protected function getReflectionContext() + { + return $this->context->getReflectionContext(); + } + + /** + * @return \Trismegiste\Mondrian\Transform\GraphContext + */ + protected function getGraphContext() + { + return $this->context->getGraphContext(); + } + + /** + * @return \Trismegiste\Mondrian\Graph\Graph + */ + protected function getGraph() + { + return $this->context->getGraph(); + } + + /** + * Search for a vertex of a given type + * + * @param string $type trait|class|interface|param|method|impl + * @param string $key the key for this vertex + * + * @return \Trismegiste\Mondrian\Graph\Vertex + */ + protected function findVertex($type, $key) + { + return $this->context->getGraphContext()->findVertex($type, $key); + } + +} \ No newline at end of file diff --git a/src/Visitor/State/FileLevelTemplate.php b/src/Visitor/State/FileLevelTemplate.php new file mode 100644 index 0000000..edbcc45 --- /dev/null +++ b/src/Visitor/State/FileLevelTemplate.php @@ -0,0 +1,136 @@ +getType()) { + + case 'Stmt_Namespace' : + $this->namespace = $node->name; + $this->aliases = array(); + // @todo : with multiple namespaces in one file : does this bug ? + // leave() shouldn't reset these values ? + break; + + case 'Stmt_UseUse' : + if (isset($this->aliases[$node->alias])) { + throw new \PhpParser\Error( + sprintf( + 'Cannot use "%s" as "%s" because the name is already in use', $node->name, $node->alias + ), $node->getLine() + ); + } + $this->aliases[$node->alias] = $node->name; + break; + + case 'Stmt_Class': + $this->context->pushState('class', $node); + $this->enterClassNode($node); + break; + + case 'Stmt_Trait': + $this->context->pushState('trait', $node); + $this->enterTraitNode($node); + break; + + case 'Stmt_Interface': + $this->context->pushState('interface', $node); + $this->enterInterfaceNode($node); + break; + } + } + + /** + * Enters in a class node + */ + abstract protected function enterClassNode(Node\Stmt\Class_ $node); + + /** + * Enters in an interface node + */ + abstract protected function enterInterfaceNode(Node\Stmt\Interface_ $node); + + /** + * Enters in a trait node + */ + abstract protected function enterTraitNode(Node\Stmt\Trait_ $node); + + public function getName() + { + return 'file'; + } + + /** + * resolve the Name with current namespace and alias + * + * @param Node\Name $src + * + * @return Node\Name|Node\Name\FullyQualified + */ + public function resolveClassName(Node\Name $src) + { + $name = clone $src; + // don't resolve special class names + if (in_array((string) $name, array('self', 'parent', 'static'))) { + return $name; + } + + // fully qualified names are already resolved + if ($name->isFullyQualified()) { + return $name; + } + + // resolve aliases (for non-relative names) + if (!$name->isRelative() && isset($this->aliases[$name->getFirst()])) { + $name->setFirst($this->aliases[$name->getFirst()]); + // if no alias exists prepend current namespace + } elseif (null !== $this->namespace) { + $name->prepend($this->namespace); + } + + return new Node\Name\FullyQualified($name->parts, $name->getAttributes()); + } + + /** + * Helper : get the FQCN of the given $node->name + * + * @param Node $node + * + * @return string + */ + public function getNamespacedName(Node $node) + { + if (null !== $this->namespace) { + $namespacedName = clone $this->namespace; + $namespacedName->append($node->name); + } else { + $namespacedName = $node->name; + } + + return (string) $namespacedName; + } + +} \ No newline at end of file diff --git a/src/Visitor/State/PackageLevel.php b/src/Visitor/State/PackageLevel.php new file mode 100644 index 0000000..17b441f --- /dev/null +++ b/src/Visitor/State/PackageLevel.php @@ -0,0 +1,37 @@ +getType()) { + case 'PhpFile': + $this->context->pushState('file', $node); + break; + } + } + + /** + * @inheritdoc + */ + public function getName() + { + return 'package'; + } + +} \ No newline at end of file diff --git a/src/Visitor/State/State.php b/src/Visitor/State/State.php new file mode 100644 index 0000000..0eed0ea --- /dev/null +++ b/src/Visitor/State/State.php @@ -0,0 +1,46 @@ +context = $ctx; - } - - /** - * {@inheritDoc} - */ - public function enterNode(\PHPParser_Node $node) - { - parent::enterNode($node); - - switch ($node->getType()) { - - case 'Stmt_TraitUse' : - $this->importSignatureTrait($node); - break; - } - } - - /** - * {@inheritDoc} - */ - protected function enterClassNode(\PHPParser_Node_Stmt_Class $node) - { - $this->context->initSymbol($this->currentClass, ReflectionContext::SYMBOL_CLASS); - // extends - if (!is_null($node->extends)) { - $name = (string) $this->resolveClassName($node->extends); - $this->context->initSymbol($name, ReflectionContext::SYMBOL_CLASS); - $this->context->pushParentClass($this->currentClass, $name); - } - // implements - foreach ($node->implements as $parent) { - $name = (string) $this->resolveClassName($parent); - $this->context->initSymbol($name, ReflectionContext::SYMBOL_INTERFACE); - $this->context->pushParentClass($this->currentClass, $name); - } - } - - /** - * {@inheritDoc} - */ - protected function enterInterfaceNode(\PHPParser_Node_Stmt_Interface $node) - { - $this->context->initSymbol($this->currentClass, ReflectionContext::SYMBOL_INTERFACE); - // extends - foreach ($node->extends as $interf) { - $name = (string) $this->resolveClassName($interf); - $this->context->initSymbol($name, ReflectionContext::SYMBOL_INTERFACE); - $this->context->pushParentClass($this->currentClass, $name); - } - } - - /** - * {@inheritDoc} - */ - protected function enterPublicMethodNode(\PHPParser_Node_Stmt_ClassMethod $node) - { - $this->context->addMethodToClass($this->currentClass, $node->name); - } - - /** - * Compiling the pass : resolving symbols in the context - */ - public function afterTraverse(array $dummy) - { - $this->context->resolveSymbol(); - } - - protected function enterTraitNode(\PHPParser_Node_Stmt_Trait $node) - { - $this->context->initSymbol($this->currentClass, ReflectionContext::SYMBOL_TRAIT); - } - - protected function importSignatureTrait(\PHPParser_Node_Stmt_TraitUse $node) - { - // @todo do not forget aliases - foreach ($node->traits as $import) { - $name = (string) $this->resolveClassName($import); - $this->context->initSymbol($name, ReflectionContext::SYMBOL_TRAIT); - $this->context->pushUseTrait($this->currentClass, $name); - } - } - -} diff --git a/src/Visitor/SymbolMap/ClassLevel.php b/src/Visitor/SymbolMap/ClassLevel.php new file mode 100644 index 0000000..b6a62b8 --- /dev/null +++ b/src/Visitor/SymbolMap/ClassLevel.php @@ -0,0 +1,39 @@ +getType()) { + + case 'Stmt_TraitUse' : + $this->importSignatureTrait($node); + break; + + case 'Stmt_ClassMethod': + if ($node->isPublic()) { + $fqcn = $this->getCurrentFqcn(); + $this->getReflectionContext()->addMethodToClass($fqcn, $node->name); + } + break; + } + } + + public function getName() + { + return 'class'; + } + +} \ No newline at end of file diff --git a/src/Visitor/SymbolMap/Collector.php b/src/Visitor/SymbolMap/Collector.php new file mode 100644 index 0000000..5b78e02 --- /dev/null +++ b/src/Visitor/SymbolMap/Collector.php @@ -0,0 +1,39 @@ +reflectionCtx->resolveSymbol(); + } + +} \ No newline at end of file diff --git a/src/Visitor/SymbolMap/FileLevel.php b/src/Visitor/SymbolMap/FileLevel.php new file mode 100644 index 0000000..21e9dc9 --- /dev/null +++ b/src/Visitor/SymbolMap/FileLevel.php @@ -0,0 +1,54 @@ +getNamespacedName($node); + $this->getReflectionContext()->initClass($fqcn); + // extends + if (!is_null($node->extends)) { + $name = (string) $this->resolveClassName($node->extends); + $this->getReflectionContext()->initClass($name); + $this->getReflectionContext()->pushParentClass($fqcn, $name); + } + // implements + foreach ($node->implements as $parent) { + $name = (string) $this->resolveClassName($parent); + $this->getReflectionContext()->initInterface($name); + $this->getReflectionContext()->pushParentClass($fqcn, $name); + } + } + + protected function enterInterfaceNode(Node\Stmt\Interface_ $node) + { + $fqcn = $this->getNamespacedName($node); + $this->getReflectionContext()->initInterface($fqcn); + // extends + foreach ($node->extends as $interf) { + $name = (string) $this->resolveClassName($interf); + $this->getReflectionContext()->initInterface($name); + $this->getReflectionContext()->pushParentClass($fqcn, $name); + } + } + + protected function enterTraitNode(Node\Stmt\Trait_ $node) + { + $fqcn = $this->getNamespacedName($node); + $this->getReflectionContext()->initTrait($fqcn); + } + +} \ No newline at end of file diff --git a/src/Visitor/SymbolMap/InterfaceLevel.php b/src/Visitor/SymbolMap/InterfaceLevel.php new file mode 100644 index 0000000..7c26862 --- /dev/null +++ b/src/Visitor/SymbolMap/InterfaceLevel.php @@ -0,0 +1,36 @@ +getType()) { + + case 'Stmt_ClassMethod': + if ($node->isPublic()) { + $fqcn = $this->getCurrentFqcn(); + $this->getReflectionContext()->addMethodToClass($fqcn, $node->name); + } + break; + } + } + + public function getName() + { + return 'interface'; + } + +} \ No newline at end of file diff --git a/src/Visitor/SymbolMap/TraitLevel.php b/src/Visitor/SymbolMap/TraitLevel.php new file mode 100644 index 0000000..1085f19 --- /dev/null +++ b/src/Visitor/SymbolMap/TraitLevel.php @@ -0,0 +1,39 @@ +getType()) { + + case 'Stmt_TraitUse' : + $this->importSignatureTrait($node); + break; + + case 'Stmt_ClassMethod': + if ($node->isPublic()) { + $fqcn = $this->getCurrentFqcn(); + $this->getReflectionContext()->addMethodToClass($fqcn, $node->name); + } + break; + } + } + + public function getName() + { + return 'trait'; + } + +} \ No newline at end of file diff --git a/src/Visitor/SymbolMap/TraitUserLevel.php b/src/Visitor/SymbolMap/TraitUserLevel.php new file mode 100644 index 0000000..acf46be --- /dev/null +++ b/src/Visitor/SymbolMap/TraitUserLevel.php @@ -0,0 +1,30 @@ +context->getState('file'); + $fqcn = $this->getCurrentFqcn(); + // @todo do not forget aliases + foreach ($node->traits as $import) { + $name = (string) $fileState->resolveClassName($import); + $this->getReflectionContext()->initTrait($name); + $this->getReflectionContext()->pushUseTrait($fqcn, $name); + } + } + +} \ No newline at end of file diff --git a/src/Visitor/Vertex/ClassLevel.php b/src/Visitor/Vertex/ClassLevel.php new file mode 100644 index 0000000..90f4a98 --- /dev/null +++ b/src/Visitor/Vertex/ClassLevel.php @@ -0,0 +1,37 @@ +name; + // if this class is declaring the method, we create a vertex for this signature + $declaringClass = $this->getReflectionContext()->getDeclaringClass($fqcn, $methodName); + if ($fqcn == $declaringClass) { + $this->pushMethod($node, "$fqcn::$methodName"); + } + + // if not abstract we add the vertex for the implementation + if (!$node->isAbstract()) { + $this->pushImplementation($node, "$fqcn::$methodName"); + } + } + + public function getName() + { + return 'class'; + } + +} \ No newline at end of file diff --git a/src/Visitor/Vertex/Collector.php b/src/Visitor/Vertex/Collector.php new file mode 100644 index 0000000..4992455 --- /dev/null +++ b/src/Visitor/Vertex/Collector.php @@ -0,0 +1,33 @@ +factoryPrototype($node, 'class', 'Trismegiste\Mondrian\Transform\Vertex\ClassVertex'); + } + + protected function enterInterfaceNode(Stmt\Interface_ $node) + { + $this->factoryPrototype($node, 'interface', 'Trismegiste\Mondrian\Transform\Vertex\InterfaceVertex'); + } + + protected function enterTraitNode(Stmt\Trait_ $node) + { + $this->factoryPrototype($node, 'trait', 'Trismegiste\Mondrian\Transform\Vertex\TraitVertex'); + } + + private function factoryPrototype(Stmt $node, $type, $vertexClass) + { + $index = $this->getNamespacedName($node); + + if (!$this->getGraphContext()->existsVertex($type, $index)) { + $factory = new \ReflectionClass($vertexClass); + $v = $factory->newInstance($index); + $this->getGraph()->addVertex($v); + $this->getGraphContext()->indicesVertex($type, $index, $v); + } + } + +} \ No newline at end of file diff --git a/src/Visitor/Vertex/InterfaceLevel.php b/src/Visitor/Vertex/InterfaceLevel.php new file mode 100644 index 0000000..3b64031 --- /dev/null +++ b/src/Visitor/Vertex/InterfaceLevel.php @@ -0,0 +1,32 @@ +name; + // if this class is declaring the method, we create a vertex for this signature + $declaringClass = $this->getReflectionContext()->getDeclaringClass($fqcn, $methodName); + if ($fqcn == $declaringClass) { + $this->pushMethod($node, "$fqcn::$methodName"); + } + } + + public function getName() + { + return 'interface'; + } + +} \ No newline at end of file diff --git a/src/Visitor/Vertex/ObjectLevelHelper.php b/src/Visitor/Vertex/ObjectLevelHelper.php new file mode 100644 index 0000000..ee5977d --- /dev/null +++ b/src/Visitor/Vertex/ObjectLevelHelper.php @@ -0,0 +1,87 @@ +getGraphContext(); + if (!$dict->existsVertex('method', $index)) { + $v = new MethodVertex($index); + $this->getGraph()->addVertex($v); + $dict->indicesVertex('method', $index, $v); + // now param + foreach ($node->params as $order => $aParam) { + $this->pushParameter($index, $order); + } + } + } + + /** + * Adding a new vertex if the implementation is not already indexed + * + * @param Stmt\ClassMethod $node + */ + protected function pushImplementation(Stmt\ClassMethod $node, $index) + { + $dict = $this->getGraphContext(); + if (!$dict->existsVertex('impl', $index)) { + $v = new ImplVertex($index); + $this->getGraph()->addVertex($v); + $dict->indicesVertex('impl', $index, $v); + } + } + + /** + * Add a parameter vertex. I must point out that I store the order + * of the parameter, not its name. Why ? Because, name can change accross + * inheritance tree. Therefore, it could fail the refactoring of the source + * from the digraph. + * + * @param string $methodName like 'FQCN::method' + * @param int $order + */ + protected function pushParameter($methodName, $order) + { + $dict = $this->getGraphContext(); + $index = $methodName . '/' . $order; + if (!$dict->existsVertex('param', $index)) { + $v = new ParamVertex($index); + $this->getGraph()->addVertex($v); + $dict->indicesVertex('param', $index, $v); + } + } + + final public function enter(Node $node) + { + if (($node->getType() == 'Stmt_ClassMethod') && + $node->isPublic()) { + $fqcn = $this->getCurrentFqcn(); + $this->enterPublicMethod($fqcn, $node); + } + } + + abstract protected function enterPublicMethod($fqcn, Stmt\ClassMethod $node); +} \ No newline at end of file diff --git a/src/Visitor/Vertex/TraitLevel.php b/src/Visitor/Vertex/TraitLevel.php new file mode 100644 index 0000000..69db27b --- /dev/null +++ b/src/Visitor/Vertex/TraitLevel.php @@ -0,0 +1,54 @@ +name; + $index = "$fqcn::$methodName"; + // create implemenation node + // if not abstract we add the vertex for the implementation + if (!$node->isAbstract()) { + $this->pushImplementation($node, $index); + } + + // push param for implementation, these parameters will be connected + // to copy-pasted signature (see below) + foreach ($node->params as $order => $aParam) { + $this->pushParameter($index, $order); + } + + // copy paste this signature in every class which use this current trait + // Anyway we check if there is no other parent which declaring first this method + $traitUser = $this->getReflectionContext()->getClassesUsingTraitForDeclaringMethod($fqcn, $methodName); + foreach ($traitUser as $classname) { + // we copy-paste the signature declaration in the class which using the current trait + $index = $classname . '::' . $methodName; + if (!$this->getGraphContext()->existsVertex('method', $index)) { + $v = new MethodVertex($index); + $this->getGraph()->addVertex($v); + $this->getGraphContext()->indicesVertex('method', $index, $v); + } + // we do not copy-paste the parameters, there will be connected to original parameters from trait (see above) + } + } + +} \ No newline at end of file diff --git a/src/Visitor/VertexCollector.php b/src/Visitor/VertexCollector.php deleted file mode 100644 index 2513ed1..0000000 --- a/src/Visitor/VertexCollector.php +++ /dev/null @@ -1,174 +0,0 @@ -currentClass; - if (!$this->existsVertex('class', $index)) { - $v = new Vertex\ClassVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('class', $index, $v); - } - } - - /** - * {@inheritDoc} - */ - protected function enterInterfaceNode(\PHPParser_Node_Stmt_Interface $node) - { - $index = $this->currentClass; - if (!$this->existsVertex('interface', $index)) { - $v = new Vertex\InterfaceVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('interface', $index, $v); - } - } - - /** - * {@inheritDoc} - */ - protected function enterPublicMethodNode(\PHPParser_Node_Stmt_ClassMethod $node) - { - if ($this->isTrait($this->currentClass)) { - $this->enterTraitMethod($node); - } elseif ($this->isInterface($this->currentClass)) { - $this->enterInterfaceMethod($node); - } else { - $this->enterClassMethod($node); - } - } - - private function enterTraitMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // create implemenation node - // if not abstract we add the vertex for the implementation - if (!$node->isAbstract()) { - $this->pushImplementation($node); - } - - // push param for implementation - $index = $this->currentClass . '::' . $this->currentMethod; - foreach ($node->params as $order => $aParam) { - $this->pushParameter($index, $order); - } - - // copy paste this signature in every class which use this current trait - // Anyway we check if there is no other parent which declaring first this method - $traitUser = $this->getClassesUsingTraitForDeclaringMethod($this->currentClass, $this->currentMethod); - foreach ($traitUser as $classname) { - // we copy-paste the signature declaration in the class which using the current trait - $index = $classname . '::' . $this->currentMethod; - if (!$this->existsVertex('method', $index)) { - $v = new Vertex\MethodVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('method', $index, $v); - } - } - } - - private function enterClassMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // if this class is declaring the method, we create a vertex for this signature - $declaringClass = $this->getDeclaringClass($this->currentClass, $this->currentMethod); - if ($this->currentClass == $declaringClass) { - $this->pushMethod($node); - } - - // if not abstract we add the vertex for the implementation - if (!$node->isAbstract()) { - $this->pushImplementation($node); - } - } - - private function enterInterfaceMethod(\PHPParser_Node_Stmt_ClassMethod $node) - { - // if this interface is declaring the method, we create a vertex for this signature - $declaringClass = $this->getDeclaringClass($this->currentClass, $this->currentMethod); - if ($this->currentClass == $declaringClass) { - $this->pushMethod($node); - } - } - - /** - * Adding a new vertex if the method is not already indexed - * Since it is a method, I'm also adding the parameters - * - * @param \PHPParser_Node_Stmt_ClassMethod $node - */ - protected function pushMethod(\PHPParser_Node_Stmt_ClassMethod $node, $index = null) - { - if (is_null($index)) { - $index = $this->getCurrentMethodIndex(); - } - if (!$this->existsVertex('method', $index)) { - $v = new Vertex\MethodVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('method', $index, $v); - // now param - foreach ($node->params as $order => $aParam) { - $this->pushParameter($index, $order); - } - } - } - - /** - * Adding a new vertex if the implementation is not already indexed - * - * @param \PHPParser_Node_Stmt_ClassMethod $node - */ - protected function pushImplementation(\PHPParser_Node_Stmt_ClassMethod $node) - { - $index = $this->getCurrentMethodIndex(); - if (!$this->existsVertex('impl', $index)) { - $v = new Vertex\ImplVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('impl', $index, $v); - } - } - - /** - * Add a parameter vertex. I must point out that I store the order - * of the parameter, not its name. Why ? Because, name can change accross - * inheritance tree. Therefore, it could fail the refactoring of the source - * from the digraph. - * - * @param string $methodName like 'FQCN::method' - * @param int $order - */ - protected function pushParameter($methodName, $order) - { - $index = $methodName . '/' . $order; - if (!$this->existsVertex('param', $index)) { - $v = new Vertex\ParamVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('param', $index, $v); - } - } - - protected function enterTraitNode(\PHPParser_Node_Stmt_Trait $node) - { - $index = $this->currentClass; - if (!$this->existsVertex('trait', $index)) { - $v = new Vertex\TraitVertex($index); - $this->graph->addVertex($v); - $this->indicesVertex('trait', $index, $v); - } - } - -} diff --git a/src/Visitor/VisitorGateway.php b/src/Visitor/VisitorGateway.php new file mode 100644 index 0000000..563ded8 --- /dev/null +++ b/src/Visitor/VisitorGateway.php @@ -0,0 +1,141 @@ +graphCtx = $grf; + $this->graph = $g; + $this->reflectionCtx = $ref; + + foreach ($visitor as $k => $v) { + if (!($v instanceof State\State)) { + throw new \InvalidArgumentException("Invalid visitor for index $k"); + } + $v->setContext($this); + $this->stateList[$v->getName()] = $v; + } + + $this->stateStack[0] = [ + 'node' => null, + 'state' => $visitor[0], + 'key' => $visitor[0]->getName() + ]; + } + + /** + * @inheritdoc + */ + public function enterNode(Node $node) + { + if ($this->debug) + printf("Entering %s %s %s %d\n", $this->stateStack[0]['key'], $node->getType(), $node->name, count($this->stateStack)); + return $this->stateStack[0]['state']->enter($node); + } + + /** + * @inheritdoc + */ + public function leaveNode(Node $node) + { + if ($this->debug) + printf("Leaving %s %s %s %d\n", $this->stateStack[0]['key'], $node->getType(), $node->name, count($this->stateStack)); + $ret = $this->stateStack[0]['state']->leave($node); + + if ($this->stateStack[0]['nodeType'] === $node->getType()) { + array_shift($this->stateStack); + } + + return $ret; + } + + public function pushState($stateKey, Node $node) + { + if ($this->debug) + printf("Stacking %s %s %s %d\n", $stateKey, $node->getType(), $node->name, count($this->stateStack)); + $state = $this->getState($stateKey); + + array_unshift($this->stateStack, [ + 'node' => $node, + 'state' => $state, + 'key' => $state->getName(), + 'nodeType' => $node->getType() + ]); + } + + public function getNodeFor($stateKey) + { + foreach ($this->stateStack as $assoc) { + if ($assoc['key'] === $stateKey) { + return $assoc['node']; + } + } + + throw new \InvalidArgumentException("$stateKey is not a currently stacked state"); + } + + public function getState($stateKey) + { + if (!array_key_exists($stateKey, $this->stateList)) { + throw new \InvalidArgumentException("$stateKey is not a registered state"); + } + + return $this->stateList[$stateKey]; + } + + public function getGraph() + { + return $this->graph; + } + + public function getGraphContext() + { + return $this->graphCtx; + } + + public function getReflectionContext() + { + return $this->reflectionCtx; + } + +} \ No newline at end of file diff --git a/tests/Fixtures/Project/TraitInternals.php b/tests/Fixtures/Project/TraitInternals.php new file mode 100644 index 0000000..8ae248d --- /dev/null +++ b/tests/Fixtures/Project/TraitInternals.php @@ -0,0 +1,33 @@ +calling(); + } + + public function staticCall() + { + TraitHelper::simple(); + } + + public function newInstance() + { + new TraitDocument(); + } + +} + +class TraitConfig { + public function calling() {} +} + +class TraitHelper { + static public function simple() {} +} + +class TraitDocument {} \ No newline at end of file diff --git a/tests/Transform/BuildGraph/ConcreteTest.php b/tests/Transform/BuildGraph/ConcreteTest.php index 7102133..c500014 100644 --- a/tests/Transform/BuildGraph/ConcreteTest.php +++ b/tests/Transform/BuildGraph/ConcreteTest.php @@ -56,8 +56,8 @@ public function testConcreteInheritance() $this->graph->expects($this->exactly(6))->method('addEdge'); $this->expectsAddEdge(5, 'class', 'Kitty\Soft', 'class', 'Kitty\Warm'); - $this->expectsAddEdge(6, 'impl', 'Kitty\Soft::purr', 'class', 'Kitty\Soft'); - $this->expectsAddEdge(7, 'class', 'Kitty\Soft', 'impl', 'Kitty\Soft::purr'); + $this->expectsAddEdge(6, 'class', 'Kitty\Soft', 'impl', 'Kitty\Soft::purr'); + $this->expectsAddEdge(7, 'impl', 'Kitty\Soft::purr', 'class', 'Kitty\Soft'); $this->compile($package); } diff --git a/tests/Transform/BuildGraph/MinimalGraphTest.php b/tests/Transform/BuildGraph/MinimalGraphTest.php index 4ef5344..e65fa67 100644 --- a/tests/Transform/BuildGraph/MinimalGraphTest.php +++ b/tests/Transform/BuildGraph/MinimalGraphTest.php @@ -55,8 +55,8 @@ public function testHintType() ->setTypeHint('Cfg')))) ->getNode(); - $this->expectsAddEdge(7, 'param', 'Project\Service::run/0', 'class', 'Project\Cfg'); - $this->expectsAddEdge(10, 'impl', 'Project\Service::run', 'param', 'Project\Service::run/0'); + $this->expectsAddEdge(10, 'param', 'Project\Service::run/0', 'class', 'Project\Cfg'); + $this->expectsAddEdge(8, 'impl', 'Project\Service::run', 'param', 'Project\Service::run/0'); $this->compile($pack); } diff --git a/tests/Transform/ParseAndGraphTest.php b/tests/Transform/ParseAndGraphTest.php index e4ab925..6283029 100644 --- a/tests/Transform/ParseAndGraphTest.php +++ b/tests/Transform/ParseAndGraphTest.php @@ -385,4 +385,41 @@ public function testTraitUsingTrait() , $result); } + public function testInternalForTrait() + { + $result = $this->callParse('TraitInternals.php'); + $this->assertCount(5 + 2 * 3 + 1, $result->getVertexSet()); + + $fqcn = 'Project\TraitInternals'; + $call = 'Project\TraitConfig'; + $helper = 'Project\TraitHelper'; + $instance = 'Project\TraitDocument'; + $this->assertEdges([ + // the trait + [['Trait', $fqcn], ['Impl', $fqcn . '::nonDynCall']], + [['Impl', $fqcn . '::nonDynCall'], ['Trait', $fqcn]], + [['Trait', $fqcn], ['Impl', $fqcn . '::staticCall']], + [['Impl', $fqcn . '::staticCall'], ['Trait', $fqcn]], + [['Trait', $fqcn], ['Impl', $fqcn . '::newInstance']], + [['Impl', $fqcn . '::newInstance'], ['Trait', $fqcn]], + // the param in the trait + [['Impl', $fqcn . '::nonDynCall'], ['Param', $fqcn . '::nonDynCall/0']], + [['Param', $fqcn . '::nonDynCall/0'], ['Class', $call]], + // the called class + [['Class', $call], ['Method', "$call::calling"]], + [['Method', "$call::calling"], ['Impl', "$call::calling"]], + [['Impl', "$call::calling"], ['Class', $call]], + // the helper class + [['Class', $helper], ['Method', "$helper::simple"]], + [['Method', "$helper::simple"], ['Impl', "$helper::simple"]], + [['Impl', "$helper::simple"], ['Class', $helper]], + // the edge between for the non-static method call + [['Impl', $fqcn . '::nonDynCall'], ['Method', "$call::calling"]], + // the edge for static call + [['Impl', $fqcn . '::staticCall'], ['Method', "$helper::simple"]], + // the edge for instantiation + [['Impl', $fqcn . '::newInstance'], ['Class', $instance]] + ], $result); + } + } diff --git a/tests/Visitor/EdgeCollectorTest.php b/tests/Visitor/Edge/CollectorTest.php similarity index 96% rename from tests/Visitor/EdgeCollectorTest.php rename to tests/Visitor/Edge/CollectorTest.php index 931fb69..7b00d87 100644 --- a/tests/Visitor/EdgeCollectorTest.php +++ b/tests/Visitor/Edge/CollectorTest.php @@ -4,12 +4,12 @@ * Mondrian */ -namespace Trismegiste\Mondrian\Tests\Visitor; +namespace Trismegiste\Mondrian\Tests\Visitor\Edge; -use Trismegiste\Mondrian\Visitor\EdgeCollector; +use Trismegiste\Mondrian\Visitor\Edge\Collector; /** - * EdgeCollectorTest is simple tests for EdgeCollector visitor. Tests the + * CollectorTest is simple tests for Edge\Collector visitor. Tests the * grammar implementation of digraph. * * Vocabulary : @@ -21,7 +21,7 @@ * * T : Trait * */ -class EdgeCollectorTest extends \PHPUnit_Framework_TestCase +class CollectorTest extends \PHPUnit_Framework_TestCase { protected $visitor; @@ -40,7 +40,7 @@ protected function setUp() ->getMock(); $this->graph = $this->getMockBuilder('Trismegiste\Mondrian\Graph\Graph') ->getMock(); - $this->visitor = new EdgeCollector($this->reflection, $this->dictionary, $this->graph); + $this->visitor = new Collector($this->reflection, $this->dictionary, $this->graph); $vertexNS = 'Trismegiste\Mondrian\Transform\Vertex'; $this->vertex = array( @@ -93,7 +93,7 @@ protected function setUp() array('Atavachron\Berwell', true) ))); - + $this->nodeList[-1] = new \Trismegiste\Mondrian\Parser\PhpFile('dummy', []); $this->nodeList[0] = new \PHPParser_Node_Stmt_Namespace(new \PHPParser_Node_Name('Atavachron')); } @@ -168,12 +168,12 @@ public function testConcreteMethod() ->with($this->vertex['C'], $this->vertex['M']); $this->graph - ->expects($this->at(2)) + ->expects($this->at(1)) ->method('addEdge') ->with($this->vertex['M'], $this->vertex['S']); $this->graph - ->expects($this->at(1)) + ->expects($this->at(2)) ->method('addEdge') ->with($this->vertex['S'], $this->vertex['C']); @@ -191,12 +191,12 @@ public function testOverridenMethod() $this->nodeList[2] = new \PHPParser_Node_Stmt_ClassMethod('sand'); $this->graph - ->expects($this->at(1)) + ->expects($this->at(0)) ->method('addEdge') ->with($this->vertex['C'], $this->vertex['S']); $this->graph - ->expects($this->at(0)) + ->expects($this->at(1)) ->method('addEdge') ->with($this->vertex['S'], $this->vertex['C']); @@ -285,7 +285,7 @@ public function testNonTypedParameterInClass() ->with($this->vertex['C'], $this->vertex['M']); $this->graph - ->expects($this->at(1)) + ->expects($this->at(4)) ->method('addEdge') ->with($this->vertex['M'], $this->vertex['P']); @@ -295,12 +295,12 @@ public function testNonTypedParameterInClass() ->with($this->vertex['S'], $this->vertex['C']); $this->graph - ->expects($this->at(3)) + ->expects($this->at(1)) ->method('addEdge') ->with($this->vertex['M'], $this->vertex['S']); $this->graph - ->expects($this->at(4)) + ->expects($this->at(3)) ->method('addEdge') ->with($this->vertex['S'], $this->vertex['P']); @@ -447,12 +447,12 @@ public function testExcludingCall() $this->graph ->expects($this->at(0)) ->method('addEdge') - ->with($this->vertex['S'], $this->vertex['C']); + ->with($this->vertex['C'], $this->vertex['S']); $this->graph ->expects($this->at(1)) ->method('addEdge') - ->with($this->vertex['C'], $this->vertex['S']); + ->with($this->vertex['S'], $this->vertex['C']); $this->visitNodeList(); } @@ -467,6 +467,10 @@ public function testSimpleTrait() $this->nodeList[1] = new \PHPParser_Node_Stmt_Trait('Dominant'); $this->nodeList[2] = new \PHPParser_Node_Stmt_ClassMethod('plague'); + $this->reflection->expects($this->once()) + ->method('getClassesUsingTraitForDeclaringMethod') + ->will($this->returnValue([])); + // edges : $this->graph ->expects($this->exactly(2)) diff --git a/tests/Visitor/PassCollectorTest.php b/tests/Visitor/PassCollectorTest.php deleted file mode 100644 index b826b5f..0000000 --- a/tests/Visitor/PassCollectorTest.php +++ /dev/null @@ -1,27 +0,0 @@ -getMockForAbstractClass( - 'Trismegiste\Mondrian\Visitor\PassCollector', array( - $this->getMock('Trismegiste\Mondrian\Transform\ReflectionContext'), - $this->getMockBuilder('Trismegiste\Mondrian\Transform\GraphContext') - ->disableOriginalConstructor() - ->getMock(), - $this->getMock('Trismegiste\Mondrian\Graph\Graph') - )); - } - -} diff --git a/tests/Visitor/SymbolMapTest.php b/tests/Visitor/SymbolMap/CollectorTest.php similarity index 56% rename from tests/Visitor/SymbolMapTest.php rename to tests/Visitor/SymbolMap/CollectorTest.php index ad03034..505885a 100644 --- a/tests/Visitor/SymbolMapTest.php +++ b/tests/Visitor/SymbolMap/CollectorTest.php @@ -4,15 +4,17 @@ * Mondrian */ -namespace Trismegiste\Mondrian\Tests\Visitor; +namespace Trismegiste\Mondrian\Tests\Visitor\SymbolMap; -use Trismegiste\Mondrian\Visitor\SymbolMap; +use Trismegiste\Mondrian\Visitor\SymbolMap\Collector; use Trismegiste\Mondrian\Transform\ReflectionContext; +use Trismegiste\Mondrian\Parser\PackageParser; +use Trismegiste\Mondrian\Tests\Fixtures\MockSplFileInfo; /** - * SymbolMapTest is a test for the visitor SymbolMap + * CollectorTest is a test for the visitor SymbolMap\Collector */ -class SymbolMapTest extends \PHPUnit_Framework_TestCase +class CollectorTest extends \PHPUnit_Framework_TestCase { protected $symbol = array(); @@ -24,53 +26,60 @@ class SymbolMapTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->context = new ReflectionContext(); - $this->visitor = new SymbolMap($this->context); - $this->parser = new \PHPParser_Parser(new \PHPParser_Lexer()); + $mockGraphCtx = $this->getMockBuilder('Trismegiste\Mondrian\Transform\GraphContext') + ->disableOriginalConstructor() + ->getMock(); + $mockGraph = $this->getMock('Trismegiste\Mondrian\Graph\Graph'); + $this->visitor = new Collector($this->context, $mockGraphCtx, $mockGraph); + $this->parser = new PackageParser(new \PHPParser_Parser(new \PHPParser_Lexer())); $this->traverser = new \PHPParser_NodeTraverser(); $this->traverser->addVisitor($this->visitor); } - public function testExternalInterfaceInheritance() + protected function scanFile($fixtures) { - $iter = array(__DIR__ . '/../Fixtures/Project/InheritExtra.php'); - foreach ($iter as $fch) { - $code = file_get_contents($fch); - $stmts = $this->parser->parse($code); - $this->traverser->traverse($stmts); + $iter = []; + + foreach ($fixtures as $fch) { + $path = __DIR__ . '/../../Fixtures/Project/' . $fch; + $code = file_get_contents($path); + $iter[] = new MockSplFileInfo($path, $code); } + + $stmts = $this->parser->parse(new \ArrayIterator($iter)); + $this->traverser->traverse($stmts); $this->visitor->afterTraverse(array()); + } + + public function testSimpleCase() + { + $this->scanFile(['Concrete.php']); $this->assertAttributeEquals(array( - 'Project\\InheritExtra' => array( + 'Project\\Concrete' => array( 'type' => 'c', - 'parent' => array(0 => 'IteratorAggregate'), - 'method' => array('getIterator' => 'IteratorAggregate'), - 'use' => [] - ), - 'IteratorAggregate' => array( - 'type' => 'i', 'parent' => array(), - 'method' => array(), + 'method' => array('simple' => 'Project\\Concrete'), 'use' => [] ), ), 'inheritanceMap', $this->context); } - public function testSimpleCase() + public function testExternalInterfaceInheritance() { - $iter = array(__DIR__ . '/../Fixtures/Project/Concrete.php'); - foreach ($iter as $fch) { - $code = file_get_contents($fch); - $stmts = $this->parser->parse($code); - $this->traverser->traverse($stmts); - } - $this->visitor->afterTraverse(array()); + $this->scanFile(['InheritExtra.php']); $this->assertAttributeEquals(array( - 'Project\\Concrete' => array( + 'Project\\InheritExtra' => array( 'type' => 'c', + 'parent' => array(0 => 'IteratorAggregate'), + 'method' => array('getIterator' => 'IteratorAggregate'), + 'use' => [] + ), + 'IteratorAggregate' => array( + 'type' => 'i', 'parent' => array(), - 'method' => array('simple' => 'Project\\Concrete'), + 'method' => array(), 'use' => [] ), ), 'inheritanceMap', $this->context); @@ -78,14 +87,7 @@ public function testSimpleCase() public function testAliasing() { - $iter = array(__DIR__ . '/../Fixtures/Project/Alias1.php', - __DIR__ . '/../Fixtures/Project/Alias2.php'); - foreach ($iter as $fch) { - $code = file_get_contents($fch); - $stmts = $this->parser->parse($code); - $this->traverser->traverse($stmts); - } - $this->visitor->afterTraverse(array()); + $this->scanFile(['Alias1.php', 'Alias2.php']); $this->assertAttributeEquals(array( 'Project\\Aliasing' => array( @@ -111,13 +113,7 @@ public function testAliasing() public function testSimpleTrait() { - $iter = array(__DIR__ . '/../Fixtures/Project/SimpleTrait.php'); - foreach ($iter as $fch) { - $code = file_get_contents($fch); - $stmts = $this->parser->parse($code); - $this->traverser->traverse($stmts); - } - $this->visitor->afterTraverse(array()); + $this->scanFile(['SimpleTrait.php']); $this->assertAttributeEquals(array( 'Project\\SimpleTrait' => array( @@ -129,16 +125,6 @@ public function testSimpleTrait() ), 'inheritanceMap', $this->context); } - protected function scanFile($iter) - { - foreach ($iter as $fch) { - $code = file_get_contents(__DIR__ . '/../Fixtures/Project/' . $fch); - $stmts = $this->parser->parse($code); - $this->traverser->traverse($stmts); - } - $this->visitor->afterTraverse(array()); - } - public function testImportingMethodFromTrait() { $this->scanFile([ @@ -190,4 +176,64 @@ public function testImportingMethodFromTraitWithInterfaceCollision() )), 'inheritanceMap', $this->context); } + public function testInterfaceExtends() + { + $this->scanFile(['Interface.php']); + + $this->assertAttributeEquals(array( + 'Project\\IOne' => array( + 'type' => 'i', + 'parent' => [], + 'method' => [], + 'use' => [] + ), + 'Project\\ITwo' => array( + 'type' => 'i', + 'parent' => [], + 'method' => [], + 'use' => [] + ), + 'Project\\IThree' => array( + 'type' => 'i', + 'parent' => ['Project\ITwo'], + 'method' => [], + 'use' => [] + ), + 'Project\\Multiple' => array( + 'type' => 'i', + 'parent' => ['Project\IOne', 'Project\ITwo'], + 'method' => [], + 'use' => [] + ), + ), 'inheritanceMap', $this->context); + } + + public function testTraitUsingTrait() + { + $this->scanFile([ + 'ServiceUsingTrait.php', + 'ServiceTrait.php' + ]); + + $this->assertAttributeEquals(array( + 'Project\\ServiceUsingTrait' => array( + 'type' => 't', + 'parent' => [], + // 'method' => array('someService' => 'Project\\ServiceTrait'), + 'method' => [], + 'use' => ['Project\\ServiceTrait'] + ), + 'Project\\ServiceTrait' => array( + 'type' => 't', + 'parent' => [], + 'method' => array('someService' => 'Project\\ServiceTrait'), + 'use' => [] + )), 'inheritanceMap', $this->context); + + $this->markTestIncomplete(); // @todo the commented line above must be incommented + // I will not create vertex for imported implementation from trait in a trait + // but a class using ServiceUsingTrait must copy-paste all methods signatures + // coming from all aggregated traits + } + } diff --git a/tests/Visitor/Vertex/CollectorTest.php b/tests/Visitor/Vertex/CollectorTest.php new file mode 100644 index 0000000..4de4464 --- /dev/null +++ b/tests/Visitor/Vertex/CollectorTest.php @@ -0,0 +1,262 @@ +reflection = $this->getMockBuilder('Trismegiste\Mondrian\Transform\ReflectionContext') + ->getMock(); + $this->vertex = $this->getMockBuilder('Trismegiste\Mondrian\Transform\GraphContext') + ->disableOriginalConstructor() + ->getMock(); + $this->graph = $this->getMockBuilder('Trismegiste\Mondrian\Graph\Graph') + ->getMock(); + $this->visitor = new Collector($this->reflection, $this->vertex, $this->graph); + } + + public function getTypeNodeSetting() + { + $vertexNS = 'Trismegiste\Mondrian\Transform\Vertex\\'; + $fileNode = new \Trismegiste\Mondrian\Parser\PhpFile('dummy', []); + $nsNode = new \PHPParser_Node_Stmt_Namespace(new \PHPParser_Node_Name('Tubular')); + $classNode = new \PHPParser_Node_Stmt_Class('Bells'); + $interfNode = new \PHPParser_Node_Stmt_Interface('Bells'); + $traitNode = new \PHPParser_Node_Stmt_Trait('Bells'); + return array( + array('class', 'Tubular\Bells', $vertexNS . 'ClassVertex', array($fileNode, $nsNode, $classNode)), + array('interface', 'Tubular\Bells', $vertexNS . 'InterfaceVertex', array($fileNode, $nsNode, $interfNode)), + array('trait', 'Tubular\Bells', $vertexNS . 'TraitVertex', array($fileNode, $nsNode, $traitNode)) + ); + } + + /** + * @dataProvider getTypeNodeSetting + */ + public function testNoNewClassVertex($type, $fqcn, $graphVertex, array $nodeList) + { + $this->vertex + ->expects($this->once()) + ->method('existsVertex') + ->with($type, $fqcn) + ->will($this->returnValue(true)); + + $this->vertex + ->expects($this->never()) + ->method('addVertex'); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + + /** + * @dataProvider getTypeNodeSetting + */ + public function testNewClassVertex($type, $fqcn, $graphVertex, array $nodeList) + { + $this->vertex + ->expects($this->once()) + ->method('existsVertex') + ->with($type, $fqcn) + ->will($this->returnValue(false)); + + $this->vertex + ->expects($this->once()) + ->method('indicesVertex') + ->with($type, $fqcn); + + $this->graph + ->expects($this->once()) + ->method('addVertex') + ->with($this->isInstanceOf($graphVertex)); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + + public function testNewMethodVertexForClass() + { + list($type, $fqcn, $graphVertex, $nodeList) = $this->getTypeNodeSetting()[0]; + $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); + $method->params[] = new \PHPParser_Node_Param('incantations'); + $nodeList[] = $method; + + $this->reflection + ->expects($this->once()) + ->method('getDeclaringClass') + ->with($fqcn, 'crisis') + ->will($this->returnValue($fqcn)); + + $this->graph + ->expects($this->exactly(4)) + ->method('addVertex'); + + $this->graph + ->expects($this->at(0)) + ->method('addVertex') + ->with($this->isInstanceOf($graphVertex)); + + $this->graph + ->expects($this->at(1)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); + + $this->graph + ->expects($this->at(2)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); + + $this->graph + ->expects($this->at(3)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ImplVertex')); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + + public function testNewMethodVertexForInterface() + { + list($type, $fqcn, $graphVertex, $nodeList) = $this->getTypeNodeSetting()[1]; + $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); + $method->params[] = new \PHPParser_Node_Param('incantations'); + $nodeList[] = $method; + + $this->reflection + ->expects($this->once()) + ->method('getDeclaringClass') + ->with($fqcn, 'crisis') + ->will($this->returnValue($fqcn)); + + $this->graph + ->expects($this->exactly(3)) + ->method('addVertex'); + + $this->graph + ->expects($this->at(0)) + ->method('addVertex') + ->with($this->isInstanceOf($graphVertex)); + + $this->graph + ->expects($this->at(1)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); + + $this->graph + ->expects($this->at(2)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + + public function testNewImplementationVertexForTrait() + { + list($type, $fqcn, $graphVertex, $nodeList) = $this->getTypeNodeSetting()[2]; + $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); + $method->params[] = new \PHPParser_Node_Param('incantations'); + $nodeList[] = $method; + + $this->reflection + ->expects($this->once()) + ->method('getClassesUsingTraitForDeclaringMethod') + ->with($fqcn, 'crisis') + ->will($this->returnValue([])); + + $this->graph + ->expects($this->exactly(3)) + ->method('addVertex'); + + $this->graph + ->expects($this->at(0)) + ->method('addVertex') + ->with($this->isInstanceOf($graphVertex)); + + $this->graph + ->expects($this->at(1)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ImplVertex')); + + $this->graph + ->expects($this->at(2)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + + public function testCopyPasteImportedMethodFromTrait() + { + list($type, $fqcn, $graphVertex, $nodeList) = $this->getTypeNodeSetting()[2]; + + $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); + $method->params[] = new \PHPParser_Node_Param('incantations'); + $nodeList[] = $method; + + $this->reflection + ->expects($this->once()) + ->method('getClassesUsingTraitForDeclaringMethod') + ->with($fqcn, 'crisis') + ->will($this->returnValue(['TraitUser1', 'TraitUser2'])); + + $this->graph + ->expects($this->exactly(5)) + ->method('addVertex'); + + // the trait vertex + $this->graph + ->expects($this->at(0)) + ->method('addVertex') + ->with($this->isInstanceOf($graphVertex)); + + // implementation + $this->graph + ->expects($this->at(1)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ImplVertex')); + $this->graph + ->expects($this->at(2)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); + + // first copy-pasted method + $this->graph + ->expects($this->at(3)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); + + // second copy-pasted method + $this->graph + ->expects($this->at(4)) + ->method('addVertex') + ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); + + foreach ($nodeList as $node) { + $this->visitor->enterNode($node); + } + } + +} diff --git a/tests/Visitor/VertexCollectorTest.php b/tests/Visitor/VertexCollectorTest.php deleted file mode 100644 index bcc9b9a..0000000 --- a/tests/Visitor/VertexCollectorTest.php +++ /dev/null @@ -1,206 +0,0 @@ -reflection = $this->getMockBuilder('Trismegiste\Mondrian\Transform\ReflectionContext') - ->getMock(); - $this->vertex = $this->getMockBuilder('Trismegiste\Mondrian\Transform\GraphContext') - ->disableOriginalConstructor() - ->getMock(); - $this->graph = $this->getMockBuilder('Trismegiste\Mondrian\Graph\Graph') - ->getMock(); - $this->visitor = new VertexCollector($this->reflection, $this->vertex, $this->graph); - } - - public function getTypeNodeSetting() - { - $vertexNS = 'Trismegiste\Mondrian\Transform\Vertex\\'; - $nsNode = new \PHPParser_Node_Stmt_Namespace(new \PHPParser_Node_Name('Tubular')); - $classNode = new \PHPParser_Node_Stmt_Class('Bells'); - $interfNode = new \PHPParser_Node_Stmt_Interface('Bells'); - $traitNode = new \PHPParser_Node_Stmt_Trait('Bells'); - return array( - array('class', 'Tubular\Bells', $vertexNS . 'ClassVertex', array($nsNode, $classNode)), - array('interface', 'Tubular\Bells', $vertexNS . 'InterfaceVertex', array($nsNode, $interfNode)), - array('trait', 'Tubular\Bells', $vertexNS . 'TraitVertex', array($nsNode, $traitNode)) - ); - } - - /** - * @dataProvider getTypeNodeSetting - */ - public function testNoNewClassVertex($type, $fqcn, $graphVertex, array $nodeList) - { - $this->vertex - ->expects($this->once()) - ->method('existsVertex') - ->with($type, $fqcn) - ->will($this->returnValue(true)); - - $this->vertex - ->expects($this->never()) - ->method('addVertex'); - - foreach ($nodeList as $node) { - $this->visitor->enterNode($node); - } - } - - /** - * @dataProvider getTypeNodeSetting - */ - public function testNewClassVertex($type, $fqcn, $graphVertex, array $nodeList) - { - $this->vertex - ->expects($this->once()) - ->method('existsVertex') - ->with($type, $fqcn) - ->will($this->returnValue(false)); - - $this->vertex - ->expects($this->once()) - ->method('indicesVertex') - ->with($type, $fqcn); - - $this->graph - ->expects($this->once()) - ->method('addVertex') - ->with($this->isInstanceOf($graphVertex)); - - foreach ($nodeList as $node) { - $this->visitor->enterNode($node); - } - } - - /** - * @dataProvider getTypeNodeSetting - */ - public function testNewMethodVertex($type, $fqcn, $graphVertex, array $nodeList) - { - $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); - $method->params[] = new \PHPParser_Node_Param('incantations'); - $nodeList[] = $method; - - $this->reflection - ->expects($this->once()) - ->method('getDeclaringClass') - ->with($fqcn, 'crisis') - ->will($this->returnValue($fqcn)); - - $this->reflection - ->expects($this->once()) - ->method('isInterface') - ->with($fqcn) - ->will($this->returnValue($type == 'interface')); - - $this->graph - ->expects($this->exactly($type == 'interface' ? 3 : 4)) - ->method('addVertex'); - - $this->graph - ->expects($this->at(0)) - ->method('addVertex') - ->with($this->isInstanceOf($graphVertex)); - - $this->graph - ->expects($this->at(1)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); - - $this->graph - ->expects($this->at(2)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); - - if ($type != 'interface') { - $this->graph - ->expects($this->at(3)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ImplVertex')); - } - - foreach ($nodeList as $node) { - $this->visitor->enterNode($node); - } - } - - /** - * @dataProvider getTypeNodeSetting - */ - public function testCopyPasteImportedMethodFromTrait($type, $fqcn, $graphVertex, array $nodeList) - { - if ($type === 'trait') { - $method = new \PHPParser_Node_Stmt_ClassMethod('crisis'); - $method->params[] = new \PHPParser_Node_Param('incantations'); - $nodeList[] = $method; - - $this->reflection - ->expects($this->once()) - ->method('isTrait') - ->with($fqcn) - ->will($this->returnValue(true)); - - $this->reflection - ->expects($this->once()) - ->method('getClassesUsingTraitForDeclaringMethod') - ->with($fqcn, 'crisis') - ->will($this->returnValue(['TraitUser1', 'TraitUser2'])); - - $this->graph - ->expects($this->exactly(5)) - ->method('addVertex'); - - // the trait vertex - $this->graph - ->expects($this->at(0)) - ->method('addVertex') - ->with($this->isInstanceOf($graphVertex)); - - // implementation - $this->graph - ->expects($this->at(1)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ImplVertex')); - $this->graph - ->expects($this->at(2)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\ParamVertex')); - - // first copy-pasted method - $this->graph - ->expects($this->at(3)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); - - // second copy-pasted method - $this->graph - ->expects($this->at(4)) - ->method('addVertex') - ->with($this->isInstanceOf('Trismegiste\Mondrian\Transform\Vertex\MethodVertex')); - - foreach ($nodeList as $node) { - $this->visitor->enterNode($node); - } - } - } - -} diff --git a/tests/Visitor/VisitorGatewayTest.php b/tests/Visitor/VisitorGatewayTest.php new file mode 100644 index 0000000..86bd12b --- /dev/null +++ b/tests/Visitor/VisitorGatewayTest.php @@ -0,0 +1,165 @@ +sut = new VisitorGateway($visitor, $this->reflectionCtx, $this->graphCtx, $this->graph); + } + + private function buildVisitorUnique() + { + $state = $this->getMockState('key'); + $this->buildVisitor([$state]); + } + + public function getMockState($key) + { + $state = $this->getMock('Trismegiste\Mondrian\Visitor\State\State'); + $state->expects($this->any()) + ->method('getName') + ->will($this->returnValue($key)); + + return $state; + } + + protected function setUp() + { + $this->reflectionCtx = $this->getMock('Trismegiste\Mondrian\Transform\ReflectionContext'); + $this->graphCtx = $this->getMockBuilder('Trismegiste\Mondrian\Transform\GraphContext') + ->disableOriginalConstructor() + ->getMock(); + $this->graph = $this->getMock('Trismegiste\Mondrian\Graph\Graph'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testEmpty() + { + $this->buildVisitor(); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testBadState() + { + $this->buildVisitor(['sfd']); + } + + public function testStateKey() + { + $state0 = $this->getMock('Trismegiste\Mondrian\Visitor\State\State'); + $state0->expects($this->exactly(2)) + ->method('getName') + ->will($this->returnValue('key0')); + $state0->expects($this->once()) + ->method('setContext'); + + $state1 = $this->getMock('Trismegiste\Mondrian\Visitor\State\State'); + $state1->expects($this->once()) + ->method('getName') + ->will($this->returnValue('key1')); + $state1->expects($this->once()) + ->method('setContext'); + + $this->buildVisitor([$state0, $state1]); + } + + public function testEnteringNode() + { + $state = $this->getMockState('key'); + $state->expects($this->once()) + ->method('enter'); + $this->buildVisitor([$state]); + $node = $this->getMock('PhpParser\Node'); + $this->sut->enterNode($node); + } + + public function testGetState() + { + $this->buildVisitorUnique(); + $this->assertInstanceOf('Trismegiste\Mondrian\Visitor\State\State', $this->sut->getState('key')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetUnknownState() + { + $this->buildVisitorUnique(); + $this->sut->getState('svfdgbtgf'); + } + + public function testShortcut() + { + $this->buildVisitorUnique(); + $this->sut->getGraph(); + $this->sut->getGraphContext(); + $this->sut->getReflectionContext(); + } + + public function testPushState() + { + $listing = [ + $this->getMockState('one'), + $this->getMockState('two'), + $this->getMockState('three') + ]; + $this->buildVisitor($listing); + $node = [ + $this->getMock('PhpParser\Node'), + $this->getMock('PhpParser\Node'), + $this->getMock('PhpParser\Node'), + ]; + + $this->assertNull($this->sut->getNodeFor('one')); + + $this->sut->pushState('two', $node[0]); + $this->assertNull($this->sut->getNodeFor('one')); + $this->assertEquals($node[0], $this->sut->getNodeFor('two')); + + $this->sut->pushState('three', $node[1]); + $this->assertNull($this->sut->getNodeFor('one')); + $this->assertEquals($node[0], $this->sut->getNodeFor('two')); + $this->assertEquals($node[1], $this->sut->getNodeFor('three')); + + $this->sut->leaveNode($node[1]); + $this->assertNull($this->sut->getNodeFor('one')); + $this->assertEquals($node[0], $this->sut->getNodeFor('two')); + $this->assertAttributeCount(2, 'stateStack', $this->sut); + + $this->sut->leaveNode($node[0]); + $this->assertAttributeCount(1, 'stateStack', $this->sut); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNoNodeFound() + { + $this->buildVisitorUnique(); + $this->sut->getNodeFor('fddbfgb'); + } + +} \ No newline at end of file