diff --git a/bin/php-exception-flow.php b/bin/php-exception-flow.php index 2529915..4eea726 100644 --- a/bin/php-exception-flow.php +++ b/bin/php-exception-flow.php @@ -1,313 +1,12 @@ create(PhpParser\ParserFactory::PREFER_PHP7); -$wrapped_parser = new WrappedParser($php_parser); -$caching_parser = new FileCachingParser(__DIR__ . "/../cache/" . $parsed_project . "/ast", $wrapped_parser); - - -$ast_system = new AstSystem(); - -$dir = $argv[1]; -$iter = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY); - -$skipped_files = 0; -/** @var SplFileInfo $file */ -foreach ($iter as $file) { - if ($file->isFile() === false) { - continue; - } - //skip tests - if (preg_match('/[\\\\\/]test(s)?[\\\\\/]/i', $file->getRealPath(), $matches) === 1) { - $skipped_files += 1; - continue; - } - - $extension = $file->getExtension(); - if ($extension === "php" || $extension === "inc") { - $ast_system->addAst($file->getPathname(), $caching_parser->parse($file->getPathname())); - } -} - -print sprintf("skipped %d files because they were located in a folder called 'test'\n", $skipped_files); -print "parsing done\n"; -$parse_finished_time = time(); - -$cfg_system_factory = CfgSystemFactory::createDefault(); -$cfg_system = createCfgSystem($cfg_system_factory, $ast_system); -print "Cfg creation done\n"; -$cfg_finished_time = time(); - - -$state = calculateState($cfg_system); -print "type-inference done\n"; -$type_inference_time = time(); - -$ast_nodes_collector = linkingCfgPass($cfg_system); -print "linking types back to ast done\n"; -$type_linking_time = time(); - -$scope_collector = calculateScopes($state, $ast_nodes_collector, $ast_system); -print "scope collection done\n"; -$scope_collection_time = time(); - -$class_method_to_method_map = calculateClassMethodToMethodMap($ast_system, $state); -print "call site calculation done\n"; -$call_site_calculation_time = time(); - -$builtin_collector = new \PhpExceptionFlow\Scope\Collector\BuiltInCollector($state->internalTypeInfo); - -$combining_scope_collector = new \PhpExceptionFlow\Scope\Collector\CombiningScopeCollector([ - $scope_collector, - $builtin_collector, -]); - -$scopes = $combining_scope_collector->getTopLevelScopes(); - -$call_resolver = new AstCallNodeToScopeResolver($combining_scope_collector->getMethodScopes(), $combining_scope_collector->getFunctionScopes(), $class_method_to_method_map, $state); - -$call_to_scope_linker = new ScopeVisitor\CallToScopeLinkingVisitor(new PhpParser\NodeTraverser(), new AstVisitor\CallCollector(), $call_resolver); - -$scope_traverser = new \PhpExceptionFlow\Scope\ScopeTraverser(); -$catch_clause_type_resolver = new ScopeVisitor\CaughtExceptionTypesCalculator($state); -$scope_traverser->addVisitor($catch_clause_type_resolver); -$scope_traverser->addVisitor($call_to_scope_linker); -$scope_traverser->traverse($scopes); -$scope_traverser->removeVisitor($catch_clause_type_resolver); -$scope_traverser->removeVisitor($call_to_scope_linker); - -print "resolved calls and catch clauses\n"; -$calls_catches_resolved_time = time(); - -$combining_mutable = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); -$combining_immutable = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); - -$encounters_calc = new \PhpExceptionFlow\EncountersCalculator($combining_mutable, $combining_immutable, $call_to_scope_linker->getCalleeCalledByCallerScopes()); - -$raises_calculator = new \PhpExceptionFlow\FlowCalculator\RaisesCalculator(new PhpParser\NodeTraverser(), new AstVisitor\ThrowsCollector(true)); -$raises_scope_traverser = new \PhpExceptionFlow\Scope\ScopeTraverser(); -$raises_wrapping_visitor = new ScopeVisitor\CalculatorWrappingVisitor($raises_calculator, ScopeVisitor\CalculatorWrappingVisitor::CALCULATE_ON_ENTER); -$raises_scope_traverser->addVisitor($raises_wrapping_visitor); -$traversing_raises_calculator = new \PhpExceptionFlow\FlowCalculator\TraversingCalculator($raises_scope_traverser, $raises_wrapping_visitor, $raises_calculator); - -$combining = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); - -$uncaught_calculator = new \PhpExceptionFlow\FlowCalculator\UncaughtCalculator($catch_clause_type_resolver, $combining); -$propagates_calculator = new \PhpExceptionFlow\FlowCalculator\PropagatesCalculator($call_to_scope_linker->getCallerCallsCalleeScopes(), $combining); - -$combining_mutable->addCalculator($uncaught_calculator); -$combining_mutable->addCalculator($propagates_calculator); -$combining_immutable->addCalculator($traversing_raises_calculator); - -$combining->addCalculator($combining_immutable); -$combining->addCalculator($combining_mutable); - -print "calculating encounters\n"; -$encounters_calc->calculateEncounters($scope_collector->getTopLevelScopes()); -print "calculation done\n"; -$calculation_done_time = time(); - - -$printing_visitor = new ScopeVisitor\DetailedPrintingVisitor($raises_calculator, $uncaught_calculator, $propagates_calculator); -$csv_printing_visitor = new ScopeVisitor\CsvPrintingVisitor($combining); -$scope_traverser->addVisitor($printing_visitor); -$scope_traverser->addVisitor($csv_printing_visitor); -$scope_traverser->traverse($scope_collector->getTopLevelScopes()); -$scope_traverser->removeVisitor($printing_visitor); -$scope_traverser->removeVisitor($csv_printing_visitor); - - - -$result_file = fopen(sprintf("%s/../results/%s_encounters.txt", __DIR__, $parsed_project), 'w'); -fwrite($result_file, $printing_visitor->getResult()); -$class_method_to_method_file = fopen(sprintf("%s/../results/%s_class_method_to_method.txt", __DIR__, $parsed_project), 'w'); -fwrite($class_method_to_method_file, stringifyClassMethodToMethodMap($class_method_to_method_map)); -$scope_calls_scope_file = fopen(sprintf("%s/../results/%s_scope_calls_scope_file.txt", __DIR__, $parsed_project), 'w'); -fwrite($scope_calls_scope_file, stringifyScopeCallsScopeMap($call_to_scope_linker)); -$unresolved_calls_file = fopen(sprintf("%s/../results/%s_unresolved_calls_file.txt", __DIR__, $parsed_project), 'w'); -fwrite($unresolved_calls_file, stringifyUnresolvedCallsPerScope($call_to_scope_linker->getUnresolvedCalls())); - -$csv_printing_visitor->writeToFile(sprintf("%s/../results/%s_encounters_eval.csv", __DIR__, $parsed_project)); - - -print sprintf("Started at %d\n", $start_time); -print sprintf("Parsing done in:\t%d\n", $parse_finished_time - $start_time); -print sprintf("Cfg creation done in:\t%d\n", $cfg_finished_time - $parse_finished_time); -print sprintf("Type inference done in:\t%d\n", $type_inference_time - $cfg_finished_time); -print sprintf("Type linking done in:\t%d\n", $type_linking_time - $type_inference_time); -print sprintf("Scope collection done in:\t%d\n", $scope_collection_time - $type_linking_time); -print sprintf("Classmethod/method map done in:\t%d\n", $call_site_calculation_time - $scope_collection_time); -print sprintf("Call/catch resolution done in:\t%d\n", $calls_catches_resolved_time - $call_site_calculation_time); -print sprintf("Encounters calculation done in:\t%d\n", $calculation_done_time - $calls_catches_resolved_time); -print sprintf("Complete process:\t%d\n", $calculation_done_time - $start_time); - - - -/** - * @param CfgSystemFactoryInterface $cfg_system_factory - * @param AstSystem $ast_system - * @return CfgSystem - */ -function createCfgSystem(CfgSystemFactoryInterface $cfg_system_factory, AstSystem $ast_system) { - $cfg_system = $cfg_system_factory->create($ast_system); - $cfg_traverser = new PHPCfg\Traverser; - $cfg_system_traverser = new CfgBridge\SystemTraverser($cfg_traverser); - $simplifier = new PHPCfg\Visitor\Simplifier; - $cfg_system_traverser->addVisitor($simplifier); - $cfg_system_traverser->traverse($cfg_system); - return $cfg_system; -} - -/** - * @param CfgSystem $cfg_system - * @return PHPTypes\State - * @throws \InvalidArgumentException - */ -function calculateState(CfgSystem $cfg_system) { - $type_reconstructor = new PHPTypes\TypeReconstructor(); - - $scripts = []; - foreach ($cfg_system->getFilenames() as $filename) { - $scripts[] = $cfg_system->getScript($filename); - } - - $state = new PHPTypes\State($scripts); - $type_reconstructor->resolve($state); - return $state; +if ($argc !== 3) { + throw new \UnexpectedValueException(sprintf("Expected exactly 2 input arguments, got %d", $argc - 1)); } -/** - * @param CfgSystem $cfg_system - * @return PHPCfg\Visitor\AstNodeToCfgNodesCollector - */ -function linkingCfgPass(CfgSystem $cfg_system) { - $cfg_traverser = new PHPCfg\Traverser; - $cfg_system_traverser = new CfgBridge\SystemTraverser($cfg_traverser); - $operand_ast_node_linker = new PHPCfg\Visitor\OperandAstNodeLinker(); - $ast_nodes_collector = new PHPCfg\Visitor\AstNodeToCfgNodesCollector(); - $cfg_system_traverser->addVisitor($operand_ast_node_linker); - $cfg_system_traverser->addVisitor($ast_nodes_collector); - $cfg_system_traverser->traverse($cfg_system); - return $ast_nodes_collector; -} - -/** - * @param PHPTypes\State $state - * @param PHPCfg\Visitor\AstNodeToCfgNodesCollector $ast_nodes_collector - * @param AstSystem $ast_system - * @return AstVisitor\ScopeCollector - */ -function calculateScopes(PHPTypes\State $state, PHPCfg\Visitor\AstNodeToCfgNodesCollector $ast_nodes_collector, AstSystem $ast_system) { - $ast_traverser = new PhpParser\NodeTraverser; - $ast_system_traverser = new AstSystemTraverser($ast_traverser); - - $scope_collector = new AstVisitor\ScopeCollector($state); - $ast_system_traverser->addVisitor(new AstVisitor\TypesToAstVisitor($ast_nodes_collector->getLinkedOps(), $ast_nodes_collector->getLinkedOperands())); - $ast_system_traverser->addVisitor($scope_collector); - - // now do a walk over the AST to collect the scopes - $ast_system_traverser->traverse($ast_system); - - return $scope_collector; -} - -/** - * @param AstSystem $ast_system - * @param PHPTypes\State $state - * @return array - */ -function calculateClassMethodToMethodMap(AstSystem $ast_system, PHPTypes\State $state) { - $partial_order = new PartialOrder(new MethodComparator($state)); - $method_collecting_visitor = new AstVisitor\MethodCollectingVisitor($partial_order); - - $ast_traverser = new PhpParser\NodeTraverser(); - $ast_system_traverser = new AstSystemTraverser($ast_traverser); - $ast_system_traverser->addVisitor($method_collecting_visitor); - $ast_system_traverser->traverse($ast_system); - - print "Created partial order with methods\n"; - - $method_resolver = new MethodResolver($state); - return $method_resolver->fromPartialOrder($partial_order); -} - -function stringifyClassMethodToMethodMap(array $map) { - $res = ""; - foreach ($map as $class => $call_sites) { - foreach ($call_sites as $call_site => $methods) { - $class_name = explode("\\", $class); - $res .= sprintf("%s->%s() resolves to: \n", array_pop($class_name), $call_site); - foreach ($methods as $method) { - $methods_class = explode("\\", $method->getClass()); - $res .= sprintf("\t%s->%s\n", array_pop($methods_class), $method->getName()); - } - } - } - - return $res; -} - -function stringifyScopeCallsScopeMap(ScopeVisitor\CallToScopeLinkingVisitor $call_linker) { - $res = ""; - $call_map = $call_linker->getCallerCallsCalleeScopes(); - foreach ($call_map as $caller) { - foreach ($call_map[$caller] as $callee) { - $res .= sprintf("%s calls %s\n", $caller->getName(), $callee->getName()); - } - } - return $res; -} - -function stringifyUnresolvedCallsPerScope($unresolved_calls) { - $prettyPrinter = new PhpParser\PrettyPrinter\Standard; - - $res = ""; - /** @var \PhpExceptionFlow\Scope\Scope $caller */ - foreach ($unresolved_calls as $caller) { - if ($unresolved_calls[$caller]->count() === 0) continue; +$project_path = $argv[1]; +$path_to_output_folder = $argv[2]; - $res .= sprintf("Scope %s has unresolved: \n", $caller->getName()); - foreach ($unresolved_calls[$caller] as $call_node) { - $message = $unresolved_calls[$caller][$call_node]; - $call_string = $prettyPrinter->prettyPrint([$call_node]); - $res .= sprintf("\t%s was unresolved with message '%s'\n", $call_string, $message); - } - } - return $res; -} \ No newline at end of file +$runner = new \PhpExceptionFlow\Runner($project_path, $path_to_output_folder); +$runner->run(); \ No newline at end of file diff --git a/composer.lock b/composer.lock index c7b9031..77be2e9 100644 --- a/composer.lock +++ b/composer.lock @@ -13,7 +13,7 @@ "source": { "type": "git", "url": "https://github.com/tomdenbraber/php-cfg", - "reference": "7ecdddbb3e6e07f759e6cbf4ffdb5e272c765c35" + "reference": "d46d3509ff85b2a6d3a13fed9af3a467c183f46c" }, "require": { "nikic/php-parser": "^2.0", @@ -40,7 +40,7 @@ } ], "description": "A Control Flow Graph implementation for PHP", - "time": "2017-04-20 09:33:05" + "time": "2017-05-15 14:22:56" }, { "name": "ircmaxell/php-types", @@ -48,7 +48,7 @@ "source": { "type": "git", "url": "https://github.com/tomdenbraber/php-types", - "reference": "2bdf8b305d3c08f48e4316b69d5e4cce53bd4c6f" + "reference": "2ee45cceaaa26808f25b334bd837b2d68e1e34ac" }, "require": { "ircmaxell/php-cfg": "dev-master", @@ -74,7 +74,7 @@ } ], "description": "A PHP CFG Type Inference / Reconstruction Engine", - "time": "2017-03-13 13:44:51" + "time": "2017-05-15 14:24:09" }, { "name": "nikic/php-parser", diff --git a/src/PhpExceptionFlow/AstVisitor/MethodCollectingVisitor.php b/src/PhpExceptionFlow/AstVisitor/MethodCollectingVisitor.php index cb75318..ae34c86 100644 --- a/src/PhpExceptionFlow/AstVisitor/MethodCollectingVisitor.php +++ b/src/PhpExceptionFlow/AstVisitor/MethodCollectingVisitor.php @@ -22,7 +22,7 @@ public function __construct(PartialOrderInterface $partial_order) { } public function enterNode(Node $node) { - if ($node instanceof Node\Stmt\Namespace_) { + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { $this->current_namespace = strtolower(implode("\\", $node->name->parts)); } else if ($node instanceof Node\Stmt\ClassLike) { $cls_name = strlen($this->current_namespace) > 0 ? $this->current_namespace . "\\" . strtolower($node->name) : strtolower($node->name); diff --git a/src/PhpExceptionFlow/AstVisitor/ScopeCollector.php b/src/PhpExceptionFlow/AstVisitor/ScopeCollector.php index 3bb7c43..dbdbe7e 100644 --- a/src/PhpExceptionFlow/AstVisitor/ScopeCollector.php +++ b/src/PhpExceptionFlow/AstVisitor/ScopeCollector.php @@ -49,24 +49,28 @@ public function __construct() { } public function enterNode(Node $node) { - if ($node instanceof Node\Stmt\Namespace_) { + if ($node instanceof Node\Stmt\Namespace_ && $node->name !== null) { $this->current_namespace = strtolower(implode("\\", $node->name->parts)); } else if ($node instanceof Node\Stmt\ClassLike) { $this->current_class = $node; } else if ($node instanceof Node\FunctionLike) { switch (get_class($node)) { case Node\Expr\Closure::class: + /** @var Node\Expr\Closure stmts */ //todo: implement correctly, thinking of how closures behave. - //currently do nothing to not interfere with 'normal' function scopes + //currently, remove all its statements so that statements in closures do not interfere with the normal scopes + $node->stmts = []; + return $node; break; case Node\Stmt\Function_::class: /** @var Node\Stmt\Function_ $node */ - $name = $node->name; + $name = strlen($this->current_namespace) > 0 ? $this->current_namespace . "\\" . strtolower($node->name) : strtolower($node->name); $this->current_scope = new Scope($name); break; case Node\Stmt\ClassMethod::class: /** @var Node\Stmt\ClassMethod $node */ - $name = $this->current_class->name . "::" . $node->name; + $cls_name = strlen($this->current_namespace) > 0 ? $this->current_namespace . "\\" . strtolower($this->current_class->name) : strtolower($this->current_class->name); + $name = $cls_name . "::" . strtolower($node->name); $this->current_scope = new Scope($name); break; default: diff --git a/src/PhpExceptionFlow/CallGraphConstruction/AstCallNodeToScopeResolver.php b/src/PhpExceptionFlow/CallGraphConstruction/AstCallNodeToScopeResolver.php index 67dd871..411f334 100644 --- a/src/PhpExceptionFlow/CallGraphConstruction/AstCallNodeToScopeResolver.php +++ b/src/PhpExceptionFlow/CallGraphConstruction/AstCallNodeToScopeResolver.php @@ -133,7 +133,7 @@ private function resolveStaticCall(Node\Expr\StaticCall $call) { throw new \UnexpectedValueException(sprintf("Method %s::%s() could not be found in applies to set (%d) ", $class, is_string($call->name) === true ? $call->name : $call->name->getType(), $call->getLine())); } } else { - throw new \UnexpectedValueException(sprintf("Cannot resolve static call; class expression has type %s, method-name is %s", $call->class->getAttribute("type", Type::unknown()), $call->name)); + throw new \UnexpectedValueException(sprintf("Cannot resolve static call; class expression has type %s, call %s is located at line %d", $call->class->getAttribute("type", Type::unknown()), is_string($call->name) === true ? $call->name : $call->name->getType(), $call->getLine())); } } @@ -171,28 +171,33 @@ private function resolveConstructorCall(Node\Expr\New_ $call) { private function resolveCallToParent(Node\Expr\StaticCall $call) { if ($call->class instanceof Node\Name) { $class = strtolower(implode("\\", $call->class->parts)); - $class_resolves = $this->state->classResolves[$class]; - if (is_string($call->name) === true && isset($this->class_method_to_implementations[$class][$call->name]) === true) { - /** @var Method[] $called_methods */ - $called_methods = $this->class_method_to_implementations[$class][$call->name]; - $called_scopes = []; - foreach ($called_methods as $called_method) { - $called_method_name = strtolower($called_method->getName()); - $called_method_class = strtolower($called_method->getClass()); - if (isset($this->method_scopes[$called_method_class][$called_method_name]) === true) { - if (isset($class_resolves[$called_method_class]) === true) { - $called_scopes[] = $this->method_scopes[$called_method_class][$called_method_name]; + + if (isset($this->state->classResolves[$class]) === true) { + $class_resolves = $this->state->classResolves[$class]; + if (is_string($call->name) === true && isset($this->class_method_to_implementations[$class][$call->name]) === true) { + /** @var Method[] $called_methods */ + $called_methods = $this->class_method_to_implementations[$class][$call->name]; + $called_scopes = []; + foreach ($called_methods as $called_method) { + $called_method_name = strtolower($called_method->getName()); + $called_method_class = strtolower($called_method->getClass()); + if (isset($this->method_scopes[$called_method_class][$called_method_name]) === true) { + if (isset($class_resolves[$called_method_class]) === true) { + $called_scopes[] = $this->method_scopes[$called_method_class][$called_method_name]; + } + } else { + throw new \UnexpectedValueException(sprintf("Method %s::%s() could not be found in method scopes", $class, $call->name)); } - } else { - throw new \UnexpectedValueException(sprintf("Method %s::%s() could not be found in method scopes", $class, $call->name)); } + return $called_scopes; + } else { + throw new \UnexpectedValueException(sprintf("Method %s::%s() could not be found in applies to set (%d) ", $class, is_string($call->name) === true ? $call->name : $call->name->getType(), $call->getLine())); } - return $called_scopes; } else { - throw new \UnexpectedValueException(sprintf("Method %s::%s() could not be found in applies to set (%d) ", $class, is_string($call->name) === true ? $call->name : $call->name->getType(), $call->getLine())); + throw new \UnexpectedValueException(sprintf("Class %s cannot be found in the state", $class)); } } else { - throw new \UnexpectedValueException(sprintf("Cannot resolve static call; class expression has type %s, method-name is %s", $call->class->getAttribute("type", Type::unknown()), $call->name)); + throw new \UnexpectedValueException(sprintf("Cannot resolve static call; class expression has type %s, call %s is located at line %d", $call->class->getAttribute("type", Type::unknown()), is_string($call->name) === true ? $call->name : $call->name->getType(), $call->getLine())); } } } \ No newline at end of file diff --git a/src/PhpExceptionFlow/CallGraphConstruction/Method.php b/src/PhpExceptionFlow/CallGraphConstruction/Method.php index 6b30617..456b189 100644 --- a/src/PhpExceptionFlow/CallGraphConstruction/Method.php +++ b/src/PhpExceptionFlow/CallGraphConstruction/Method.php @@ -1,9 +1,10 @@ class, $this->method_node->name, count($this->method_node->params), $this->method_node->type); + return sprintf("%s::%s", strtolower($this->class), strtolower($this->method_node->name)); + } + + public function jsonSerialize() { + return [ + $this->__toString() => [ + "abstract" => !$this->isImplemented(), + ] + ]; } } \ No newline at end of file diff --git a/src/PhpExceptionFlow/CallGraphConstruction/MethodResolver.php b/src/PhpExceptionFlow/CallGraphConstruction/MethodResolver.php index 84f352d..cff1322 100644 --- a/src/PhpExceptionFlow/CallGraphConstruction/MethodResolver.php +++ b/src/PhpExceptionFlow/CallGraphConstruction/MethodResolver.php @@ -97,6 +97,11 @@ private function resolveInheritedMethod(Method $method, PartialOrderInterface $p $child_methods = $partial_order->getChildren($method); $current_classlike = strtolower($method->getClass()); + if (isset($this->state->classResolvedBy[$current_classlike]) === false) { + print sprintf("%s is not registered in State, so it will be skipped(R3).\n", $current_classlike); + return; + } + $subclasses_not_implementing = $this->state->classResolvedBy[$current_classlike]; unset($subclasses_not_implementing[$current_classlike]); foreach ($child_methods as $child) { @@ -119,6 +124,11 @@ private function resolveMethodsSubsitutionPrinciple(Method $method, PartialOrder $ancestors = $partial_order->getAncestors($method); $current_classlike = strtolower($method->getClass()); + if (isset($this->state->classResolvedBy[$current_classlike]) === false) { + print sprintf("%s is not registered in State, so it will be skipped(R2, R4).\n", $current_classlike); + return; + } + $current_classlike_resolves = $this->state->classResolves[$current_classlike]; unset($current_classlike_resolves[$current_classlike]); $current_classlike_resolves = array_keys($current_classlike_resolves); diff --git a/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrder.php b/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrder.php index c8cb8fa..5283cdd 100644 --- a/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrder.php +++ b/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrder.php @@ -3,7 +3,7 @@ use PhpExceptionFlow\Collection\PartialOrderInterface; -class PartialOrder implements PartialOrderInterface { +class PartialOrder implements PartialOrderInterface, \JsonSerializable { /** @var ComparatorInterface */ private $comparator; @@ -16,61 +16,96 @@ class PartialOrder implements PartialOrderInterface { /** @var \SplObjectStorage */ private $sub_links; + private $maximal_elements; + private $minimal_elements; + public function __construct(ComparatorInterface $comparator) { $this->comparator = $comparator; $this->elements = new \SplObjectStorage(); $this->super_links = new \SplObjectStorage(); $this->sub_links = new \SplObjectStorage(); + $this->maximal_elements = new \SplObjectStorage(); + $this->minimal_elements = new \SplObjectStorage(); } - public function addElement($element_to_add) { - if ($this->elements->contains($element_to_add) === true) { + public function addElement(PartialOrderElementInterface $element) { + if ($this->elements->contains($element) === true) { return; } - $parents = array(); - foreach ($this->getMaximalElements() as $maximal_element) { - $possible_parents = $this->getSmallestPossibleParents($element_to_add, $maximal_element); - if ($possible_parents !== false) { - $parents = array_merge($possible_parents, $parents); + $this->elements->attach($element); + $this->super_links->attach($element, array()); + $this->sub_links->attach($element, array()); + + $maximal_elements = $this->getMaximalElements(); + + $this->maximal_elements->attach($element); + + foreach ($maximal_elements as $maximal_element) { + $comparison = $this->comparator->compare($element, $maximal_element); + if ($comparison === self::SMALLER) { + $this->insertElementBeneath($element, $maximal_element); + } else if ($comparison === self::GREATER) { + $this->insertElementAbove($element, $maximal_element); } } - $added_parent = false; - foreach ($parents as $parent) { - $this->addRelationBetween($parent, $element_to_add); - $added_parent = true; - } - $children = array(); - foreach ($this->getMinimalElements() as $minimal_element) { - $possible_children = $this->getGreatestPossibleChildren($element_to_add, $minimal_element); - if ($possible_children !== false) { - $children = array_merge($possible_children, $children); + + $minimal_elements = $this->getMinimalElements(); + + $this->minimal_elements->attach($element); + foreach ($minimal_elements as $minimal_element) { + $comparison = $this->comparator->compare($element, $minimal_element); + if ($comparison === self::SMALLER) { + $this->insertElementBeneath($element, $minimal_element); + } else if ($comparison === self::GREATER) { + $this->insertElementAbove($element, $minimal_element); } } + } - $added_child = false; + private function insertElementBeneath(PartialOrderElementInterface $element, PartialOrderElementInterface $ancestor) { + $children = $this->getChildren($ancestor); + $child_selected = false; foreach ($children as $child) { - $this->addRelationBetween($element_to_add, $child); - $added_child = true; - foreach ($this->getParents($child) as $childs_parent) { - if ($this->comparator->compare($childs_parent, $element_to_add) === self::GREATER) { - //childs_parent > element_to_add >= child - $this->removeRelationBetween($childs_parent, $child); - } + $comparison = $this->comparator->compare($element, $child); + if ($comparison === self::GREATER) { + $child_selected = true; + $this->insertElementBetween($element, $ancestor, $child); + } elseif ($comparison === self::SMALLER) { + $child_selected = true; + $this->insertElementBeneath($element, $child); } } - - if ($added_parent === false) { - $this->super_links->attach($element_to_add, array()); + if ($child_selected === false) { + $this->addRelationBetween($ancestor, $element); } - if ($added_child === false) { - $this->sub_links->attach($element_to_add, array()); + } + + private function insertElementAbove(PartialOrderElementInterface $element, PartialOrderElementInterface $descendant) { + $parents = $this->getParents($descendant); + $child_selected = false; + foreach ($parents as $parent) { + $comparison = $this->comparator->compare($element, $parent); + if ($comparison === self::GREATER) { + $this->insertElementAbove($element, $parent); + } elseif ($comparison === self::SMALLER) { + $this->insertElementBetween($element, $parent, $descendant); + } + } + if ($child_selected === false) { + $this->addRelationBetween($element, $descendant); } - $this->elements->attach($element_to_add); + + } + + private function insertElementBetween($element, $parent, $child) { + $this->addRelationBetween($element, $child); + $this->addRelationBetween($parent, $element); + $this->removeRelationBetween($parent, $child); } - public function removeElement($element) { + public function removeElement(PartialOrderElementInterface $element) { if ($this->elements->contains($element) === true) { foreach ($this->super_links[$element] as $parent) { $parents_children = $this->sub_links[$parent]; @@ -116,95 +151,30 @@ public function getGreatestElement() { * @return array of the 'greatest' elements of this partial order */ public function getMaximalElements() { - $maximal_elements = array(); - foreach ($this->super_links as $element) { - if (empty($this->super_links[$element]) === true) { - //this element has no parents and thus is maximal - $maximal_elements[] = $element; - } + $max_els = []; + foreach ($this->maximal_elements as $obj) { + $max_els[] = $obj; } - return $maximal_elements; + return $max_els; } /** * @return array of the 'smallest' elements of this partial order */ public function getMinimalElements() { - $minimal_elements = array(); - foreach ($this->sub_links as $element) { - if (empty($this->sub_links [$element]) === true) { - //this element has no children and thus is minimal - $minimal_elements[] = $element; - } + $min_els = []; + foreach ($this->minimal_elements as $obj) { + $min_els[] = $obj; } - return $minimal_elements; + return $min_els; } /** - * Retrieves the 'smallest' possible element from the partial order whioh is still greater than - * the given new_element, when we start looking at the given member_element - * @param $new_element - * @param $member_element - * @return mixed - */ - private function getSmallestPossibleParents($new_element, $member_element) { - $resulting_elems = []; - $compare_res = $this->comparator->compare($new_element, $member_element); - switch ($compare_res) { - case self::NOT_COMPARABLE: - case self::EQUAL: - case self::GREATER; - return false; - case self::SMALLER: - foreach ($this->sub_links[$member_element] as $smaller_element) { - $smaller_parents = $this->getSmallestPossibleParents($new_element, $smaller_element); - if ($smaller_parents !== false) { - $resulting_elems[] = $smaller_element; - } - } - if (empty($resulting_elems) === true) { - $resulting_elems[] = $member_element; - } - break; - } - return $resulting_elems; - } - - /** - * Retrieves the 'greatest' possible element from the partial order whioh is still smaller than - * the given new_element, when we start looking at the given member_element - * @param $new_element - * @param $member_element - * @return mixed - */ - private function getGreatestPossibleChildren($new_element, $member_element) { - $resulting_elems = []; - $compare_res = $this->comparator->compare($new_element, $member_element); - switch ($compare_res) { - case self::NOT_COMPARABLE: - case self::EQUAL: - case self::SMALLER: - return false; - case self::GREATER; - foreach ($this->super_links[$member_element] as $greater_element) { - $greater_children = $this->getGreatestPossibleChildren($new_element, $greater_element); - if ($greater_children !== false) { - $resulting_elems[] = $greater_element; - } - } - if (empty($resulting_elems) === true) { - $resulting_elems[] = $member_element; - } - break; - } - return $resulting_elems; - } - - - /** + * @param PartialOrderElementInterface $element * @throws \UnexpectedValueException when the given element is not in the partial order + * @return PartialOrderElementInterface[] */ - public function getAncestors($element) { + public function getAncestors(PartialOrderElementInterface $element) { if ($this->elements->contains($element) === false) { throw new \UnexpectedValueException("No such element in this partial order."); } @@ -221,9 +191,11 @@ public function getAncestors($element) { } /** + * @param PartialOrderElementInterface $element * @throws \UnexpectedValueException when the given element is not in the partial order + * @return PartialOrderElementInterface[] */ - public function getParents($element) { + public function getParents(PartialOrderElementInterface $element) { if ($this->elements->contains($element) === false) { throw new \UnexpectedValueException("No such element in this partial order."); } @@ -231,9 +203,11 @@ public function getParents($element) { } /** + * @param PartialOrderElementInterface $element * @throws \UnexpectedValueException when the given element is not in the partial order + * @return PartialOrderElementInterface[] */ - public function getChildren($element) { + public function getChildren(PartialOrderElementInterface $element) { if ($this->elements->contains($element) === false) { throw new \UnexpectedValueException("No such element in this partial order."); } @@ -241,9 +215,11 @@ public function getChildren($element) { } /** + * @param PartialOrderElementInterface $element * @throws \UnexpectedValueException when the given element is not in the partial order + * @return PartialOrderElementInterface[] */ - public function getDescendants($element) { + public function getDescendants(PartialOrderElementInterface $element) { if ($this->elements->contains($element) === false) { throw new \UnexpectedValueException("No such element in this partial order."); } @@ -259,12 +235,33 @@ public function getDescendants($element) { return $descendants; } + /** + * @return array with for each method/function its direct children and its direct parents + */ + public function jsonSerialize() { + $result = []; + foreach ($this->elements as $element) { + $result = array_merge($element->jsonSerialize(), $result); + + $result[(string)$element]['ancestors'] = []; + foreach ($this->getAncestors($element) as $ancestor) { + $result[(string)$element]['ancestors'][(string)$ancestor] = (string)$ancestor; + } + + $result[(string)$element]['descendants'] = []; + foreach ($this->getDescendants($element) as $descendant) { + $result[(string)$element]['descendants'][(string)$descendant] = (string)$descendant; + } + } + return $result; + } + /** * Removes the relationship between the two given elements - * @param $greater - * @param $smaller + * @param PartialOrderElementInterface $greater + * @param PartialOrderElementInterface $smaller */ - private function removeRelationBetween($greater, $smaller) { + private function removeRelationBetween(PartialOrderElementInterface $greater, PartialOrderElementInterface $smaller) { $children = $this->sub_links[$greater]; array_splice($children, array_search($smaller, $children, true), 1); $this->sub_links[$greater] = $children; @@ -276,23 +273,30 @@ private function removeRelationBetween($greater, $smaller) { /** * Adds the relationship between the two given elements - * @param $greater - * @param $smaller + * @param PartialOrderElementInterface $greater + * @param PartialOrderElementInterface $smaller */ - private function addRelationBetween($greater, $smaller) { - if (isset($this->sub_links[$greater]) === false) { - $this->sub_links[$greater] = array(); + private function addRelationBetween(PartialOrderElementInterface $greater, PartialOrderElementInterface $smaller) { + /** @var PartialOrderElementInterface[] $children */ + $children = $this->sub_links[$greater]; + if (in_array($smaller, $children, true) === false) { + $children[] = $smaller; + $this->sub_links[$greater] = $children; } - if (isset($this->super_links[$smaller]) === false) { - $this->super_links[$smaller] = array(); + + /** @var PartialOrderElementInterface[] $parents */ + $parents = $this->super_links[$smaller]; + if (in_array($greater, $parents, true) === false) { + $parents[] = $greater; + $this->super_links[$smaller] = $parents; } - $children = $this->sub_links[$greater]; - $children[] = $smaller; - $this->sub_links[$greater] = $children; + if ($this->minimal_elements->contains($greater) === true) { + $this->minimal_elements->detach($greater); + } - $parents = $this->super_links[$smaller]; - $parents[] = $greater; - $this->super_links[$smaller] = $parents; + if ($this->maximal_elements->contains($smaller) === true) { + $this->maximal_elements->detach($smaller); + } } } \ No newline at end of file diff --git a/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrderElementInterface.php b/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrderElementInterface.php new file mode 100644 index 0000000..9d792ef --- /dev/null +++ b/src/PhpExceptionFlow/Collection/PartialOrder/PartialOrderElementInterface.php @@ -0,0 +1,6 @@ +path_collection->getEntries(); + foreach ($links as $link) { + if ($link instanceof Catches && $link->getCaughtBy() === $catch_) { + yield $this->path_collection->getShortestPathEndingInLink($link); + } + } + } + public function __toString() { return (string) $this->getType(); } diff --git a/src/PhpExceptionFlow/Path/PathCollection.php b/src/PhpExceptionFlow/Path/PathCollection.php index ca880c3..3f8a76b 100644 --- a/src/PhpExceptionFlow/Path/PathCollection.php +++ b/src/PhpExceptionFlow/Path/PathCollection.php @@ -9,6 +9,7 @@ class PathCollection { /** @var PathEntryInterface $initial_link */ private $initial_link; + /** @var PathEntryInterface[] */ private $entries = []; /** @var \SplObjectStorage|PathEntryInterface[][] $scope_from_links */ @@ -21,10 +22,10 @@ public function __construct(PathEntryInterface $initial_link) { $this->scope_from_links = new \SplObjectStorage; $this->scope_to_links = new \SplObjectStorage; - $this->addEntry($initial_link); + $this->entries[] = $initial_link; + $this->scope_to_links[$initial_link->getToScope()] = [$initial_link]; } - /** * @return PathEntryInterface[][] */ @@ -60,7 +61,7 @@ public function getPaths() { foreach ($next_links as $next_link) { if (isset($covered_links[(string)$next_link]) === false) { foreach ($paths_ending_in_current_elem as $i => $relevant_path) { - $relevant_path[] = $next_link; + $relevant_path[] = $next_link; $paths[] = $relevant_path; if (isset($link_to_ind[(string)$next_link]) === true) { $link_to_ind[(string)$next_link][] = $no_paths; @@ -121,4 +122,129 @@ public function getEntriesForFromScope(Scope $scope) { return []; } } + + public function getEntries() { + return $this->entries; + } + + /** + * Does a DFS creation of all possible paths (leaves out cycles) + * @param PathEntryInterface $final_link + * @return \Generator + */ + public function getPathsEndingInLink(PathEntryInterface $final_link) { + $stack = [$final_link]; + $covered_entries = []; + $currently_stacked_entries = [ + (string) $final_link => true + ]; + $covered_for_current_root = []; + + while (empty($stack) === false) { + /** @var PathEntryInterface $current_entry */ + $current_entry = $stack[0]; + if (isset($covered_for_current_root[(string)$current_entry]) === false) { + $covered_for_current_root[(string)$current_entry] = []; + } + + $links = $this->scope_to_links[$current_entry->getFromScope()]; + $added_link_to_stack = false; + foreach ($links as $link) { + if (isset($covered_entries[(string)$link]) === false && isset($currently_stacked_entries[(string)$link]) === false && isset($covered_for_current_root[(string)$current_entry][(string)$link]) === false) { + array_unshift($stack, $link); + $added_link_to_stack = true; + $currently_stacked_entries[(string)$link] = true; + $covered_for_current_root[(string)$current_entry][(string)$link] = true; + break; + } + } + + if ($added_link_to_stack === false) { + $top_item = $stack[0]; + //this path is finished. if it ends in the initial link, it is a path from $initial_link to $final_link. + if ($top_item === $this->initial_link) { + yield $stack; + } + $finished_entry = array_shift($stack); + unset($currently_stacked_entries[(string)$top_item]); + $covered_entries[(string)$top_item] = true; + $this->cleanCoveredEntries((string)$finished_entry, $covered_entries, $covered_for_current_root); + } + } + } + + /** + * Uses Dijkstra to find the shortest path from the initial link to the given final link + * @param PathEntryInterface $final_link + * @return PathEntryInterface[] + */ + public function getShortestPathEndingInLink(PathEntryInterface $final_link) : array { + $queue = []; + + $distances = new \SplObjectStorage(); + $previous = new \SplObjectStorage(); + + $distances[$this->initial_link] = 0; + + foreach ($this->entries as $node) { + if ($node !== $this->initial_link) { + $distances[$node] = INF; + $previous[$node] = null; + } + $queue[] = $node; + } + + while (empty($queue) === false) { + /** @var PathEntryInterface $current_entry */ + $min_dist = INF; + $min_index = -1; + //fetch entry with lowest distance from the queue + foreach ($queue as $i => $entry) { + if ($distances[$entry] < $min_dist) { + $min_dist = $distances[$entry]; + $current_entry = $entry; + $min_index = $i; + } + } + array_splice($queue, $min_index, 1); + + + if ($current_entry === $final_link) { + break; + } + + + if ($this->scope_from_links->contains($current_entry->getToScope()) === true) { + foreach ($this->scope_from_links[$current_entry->getToScope()] as $neighbour) { + $dist = $distances[$current_entry] + 1; //the distance between each scope is the same, so +1 + if ($dist < $distances[$neighbour]) { + $distances[$neighbour] = $dist; + $previous[$neighbour] = $current_entry; + } + } + } + } + //now rebuild path + $path = []; + $last_link = $final_link; + while ($previous->contains($last_link) === true) { + array_unshift($path, $last_link); + $last_link = $previous[$last_link]; + } + array_unshift($path, $last_link); + + return $path; + } + + + private function cleanCoveredEntries(string $entry_key, array &$covered_entries, array &$covered_for_current_root) { + if (isset($covered_for_current_root[$entry_key]) === false) { + return; + } + foreach ($covered_for_current_root[$entry_key] as $related_entry => $_) { + unset($covered_entries[$related_entry]); + $this->cleanCoveredEntries($related_entry, $covered_entries, $covered_for_current_root); + unset($covered_for_current_root[$related_entry]); + } + } } \ No newline at end of file diff --git a/src/PhpExceptionFlow/Path/PathEntryInterface.php b/src/PhpExceptionFlow/Path/PathEntryInterface.php index 0349fd8..6d27f0d 100644 --- a/src/PhpExceptionFlow/Path/PathEntryInterface.php +++ b/src/PhpExceptionFlow/Path/PathEntryInterface.php @@ -15,7 +15,7 @@ public function getType(); public function getFromScope(); /** - * @return Scope|null + * @return Scope */ public function getToScope(); diff --git a/src/PhpExceptionFlow/Runner.php b/src/PhpExceptionFlow/Runner.php new file mode 100644 index 0000000..0a13fc4 --- /dev/null +++ b/src/PhpExceptionFlow/Runner.php @@ -0,0 +1,410 @@ +path_to_project = $path_to_project; + $this->path_to_output_folder = $path_to_output_folder; + $this->path_to_project_specific_output = $path_to_output_folder . "/" . basename(realpath($path_to_project)); + } + + + public function run() { + $this->createNeededDirectories(realpath($this->path_to_project), realpath($this->path_to_output_folder)); + + $this->ast_system = $this->parseProject(); + print "AST created.\n"; + $cfg_system_factory = CfgSystemFactory::createDefault(); + $this->cfg_system = $this->createCfgSystem($cfg_system_factory, $this->ast_system); + print "CFGs created.\n"; + $this->state = $this->calculateState($this->cfg_system); + print "State calculated.\n"; + $ast_nodes_collector = $this->linkingCfgPass($this->cfg_system); + print "Connected CFG to AST.\n"; + $this->scope_collector = $this->calculateScopes($this->state, $ast_nodes_collector, $this->ast_system); + print "Calculated scopes.\n"; + $this->method_partial_order = $this->calculatePartialOrder($this->ast_system, $this->state); + print "Calculated partial order.\n"; + $this->class_method_to_method_map = $this->calculateClassMethodToMethodMap($this->method_partial_order, $this->state); + print "Created class method to method map.\n"; + + $builtin_collector = new \PhpExceptionFlow\Scope\Collector\BuiltInCollector($this->state->internalTypeInfo); + + $combining_scope_collector = new \PhpExceptionFlow\Scope\Collector\CombiningScopeCollector([ + $this->scope_collector, + $builtin_collector, + ]); + + $scopes = $combining_scope_collector->getTopLevelScopes(); + + $call_resolver = new AstCallNodeToScopeResolver($combining_scope_collector->getMethodScopes(), $combining_scope_collector->getFunctionScopes(), $this->class_method_to_method_map, $this->state); + $call_to_scope_linker = new CallToScopeLinkingVisitor(new AstNodeTraverser, new AstVisitor\CallCollector(), $call_resolver); + + $scope_traverser = new \PhpExceptionFlow\Scope\ScopeTraverser(); + $catch_clause_type_resolver = new CaughtExceptionTypesCalculator($this->state); + $scope_traverser->addVisitor($catch_clause_type_resolver); + $scope_traverser->addVisitor($call_to_scope_linker); + $scope_traverser->traverse($scopes); + $scope_traverser->removeVisitor($catch_clause_type_resolver); + $scope_traverser->removeVisitor($call_to_scope_linker); + + print "Linked calls to scopes and resolved catch clause types.\n"; + + $combining_calculator = $this->calculateEncounters($this->scope_collector, $call_to_scope_linker, $catch_clause_type_resolver); + + print "Calculated the exception flow.\n"; + + unset($this->ast_system); + unset($this->cfg_system); + + file_put_contents($this->path_to_project_specific_output . "/method_order.json", json_encode($this->method_partial_order, JSON_PRETTY_PRINT)); + unset($this->method_partial_order); + file_put_contents($this->path_to_project_specific_output . "/class_hierarchy.json", json_encode([ + "class resolves" => $this->state->classResolves, + "class resolved by" => $this->state->classResolvedBy, + ], JSON_PRETTY_PRINT)); + unset($this->state); + file_put_contents($this->path_to_project_specific_output . "/class_method_to_method.json", json_encode($this->serializeClassMethodToMethodMap($this->class_method_to_method_map), JSON_PRETTY_PRINT)); + unset($this->class_method_to_method_map); + file_put_contents($this->path_to_project_specific_output . "/unresolved_calls.json", json_encode($this->serializeUnresolvedCalls($call_to_scope_linker->getUnresolvedCalls()), JSON_PRETTY_PRINT)); + file_put_contents($this->path_to_project_specific_output . "/scope_calls_scope.json", json_encode($this->serializeScopeCallsScopeMap($call_to_scope_linker), JSON_PRETTY_PRINT)); + unset($this->call_to_scope_linker); + + $json_printing_visitor = new JsonPrintingVisitor($combining_calculator); + $scope_traverser->addVisitor($json_printing_visitor); + $scope_traverser->traverse($this->scope_collector->getTopLevelScopes()); + $scope_traverser->removeVisitor($json_printing_visitor); + file_put_contents($this->path_to_project_specific_output . "/exception_flow.json", $json_printing_visitor->getResult()); + unset($json_printing_visitor); + + + /** @var UncaughtCalculator $uncaught_calculator */ + $uncaught_calculator = $combining_calculator->getCalculator("uncaught"); + $paths_file = fopen($this->path_to_project_specific_output . "/path_to_catch_clauses.json", "w"); + $caught_path_collecting_visitor = new CatchesPathVisitor($uncaught_calculator,$paths_file); + $scope_traverser->addVisitor($caught_path_collecting_visitor); + $scope_traverser->traverse($this->scope_collector->getTopLevelScopes()); + $scope_traverser->removeVisitor($caught_path_collecting_visitor); + + fclose($paths_file); + + //file_put_contents($this->path_to_project_specific_output . "/path_to_catch_clauses.json", json_encode($caught_path_collecting_visitor->getPaths(), JSON_PRETTY_PRINT)); + unset($caught_path_collecting_visitor); + + $this->output_files = [ + "exception flow" => $this->path_to_project_specific_output . "/exception_flow.json", + "method order" => $this->path_to_project_specific_output . "/method_order.json", + "class hierarchy" => $this->path_to_project_specific_output . "/class_hierarchy.json", + "unresolved calls" => $this->path_to_project_specific_output . "/unresolved_calls.json", + "class method to method" => $this->path_to_project_specific_output . "/class_method_to_method.json", + "scope calls scope" => $this->path_to_project_specific_output . "/scope_calls_scope.json", + "path to catch clauses" => $this->path_to_project_specific_output . "/path_to_catch_clauses.json", + "ast system cache" => __DIR__ . "/../../cache/" . basename(realpath($this->path_to_project)) . "/ast", + ]; + } + + private function parseProject() { + $parsed_project = basename(realpath($this->path_to_project)); + $php_parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $wrapped_parser = new AstWrappedParser($php_parser); + $caching_parser = new AstFileCachingParser(__DIR__ . "/../../cache/" . $parsed_project . "/ast", $wrapped_parser); + + $ast_system = new AstSystem(); + + $dir = $this->path_to_project; + $iter = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY); + + $skipped_files = 0; + /** @var \SplFileInfo $file */ + foreach ($iter as $file) { + if ($file->isFile() === false) { + continue; + } + //skip tests + if (preg_match('/[\\\\\/]test(s)?[\\\\\/]/i', $file->getRealPath(), $matches) === 1) { + $skipped_files += 1; + continue; + } + + $extension = $file->getExtension(); + if ($extension === "php" || $extension === "inc") { + $ast_system->addAst($file->getPathname(), $caching_parser->parse($file->getPathname())); + } + } + return $ast_system; + } + + /** + * @param CfgSystemFactoryInterface $cfg_system_factory + * @param AstSystem $ast_system + * @return CfgSystem + */ + private function createCfgSystem(CfgSystemFactoryInterface $cfg_system_factory, AstSystem $ast_system) { + $cfg_system = $cfg_system_factory->create($ast_system); + $cfg_traverser = new CfgTraverser(); + $cfg_system_traverser = new CfgBridge\SystemTraverser($cfg_traverser); + $simplifier = new CfgSimplifier; + $cfg_system_traverser->addVisitor($simplifier); + $cfg_system_traverser->traverse($cfg_system); + return $cfg_system; + } + + + /** + * @param CfgSystem $cfg_system + * @return State + * @throws \InvalidArgumentException + */ + private function calculateState(CfgSystem $cfg_system) { + $type_reconstructor = new TypeReconstructor; + + $scripts = []; + foreach ($cfg_system->getFilenames() as $filename) { + $scripts[] = $cfg_system->getScript($filename); + } + + $state = new State($scripts); + $type_reconstructor->resolve($state); + return $state; + } + + /** + * @param CfgSystem $cfg_system + * @return AstNodeToCfgNodesCollector + */ + private function linkingCfgPass(CfgSystem $cfg_system) { + $cfg_traverser = new CfgTraverser; + $cfg_system_traverser = new CfgBridge\SystemTraverser($cfg_traverser); + $operand_ast_node_linker = new OperandAstNodeLinker(); + $ast_nodes_collector = new AstNodeToCfgNodesCollector; + $cfg_system_traverser->addVisitor($operand_ast_node_linker); + $cfg_system_traverser->addVisitor($ast_nodes_collector); + $cfg_system_traverser->traverse($cfg_system); + return $ast_nodes_collector; + } + + /** + * @param State $state + * @param AstNodeToCfgNodesCollector $ast_nodes_collector + * @param AstSystem $ast_system + * @return AstVisitor\ScopeCollector + */ + private function calculateScopes(State $state, AstNodeToCfgNodesCollector $ast_nodes_collector, AstSystem $ast_system) { + $ast_traverser = new AstNodeTraverser(); + $ast_system_traverser = new AstSystemTraverser($ast_traverser); + + $scope_collector = new AstVisitor\ScopeCollector($state); + $ast_system_traverser->addVisitor(new AstVisitor\TypesToAstVisitor($ast_nodes_collector->getLinkedOps(), $ast_nodes_collector->getLinkedOperands())); + $ast_system_traverser->addVisitor($scope_collector); + + // now do a walk over the AST to collect the scopes + $ast_system_traverser->traverse($ast_system); + + return $scope_collector; + } + + /** + * @param AstSystem $ast_system + * @param State $state + * @return PartialOrderInterface + */ + private function calculatePartialOrder(AstSystem $ast_system, State $state) { + $partial_order = new PartialOrder(new MethodComparator($state)); + $method_collecting_visitor = new AstVisitor\MethodCollectingVisitor($partial_order); + + $ast_traverser = new AstNodeTraverser(); + $ast_system_traverser = new AstSystemTraverser($ast_traverser); + $ast_system_traverser->addVisitor($method_collecting_visitor); + $ast_system_traverser->traverse($ast_system); + + return $partial_order; + } + + /** + * @param PartialOrderInterface $partial_order + * @param State $state + * @return CallGraphConstruction\Method[][][] + */ + private function calculateClassMethodToMethodMap(PartialOrderInterface $partial_order, State $state) { + $method_resolver = new MethodResolver($state); + return $method_resolver->fromPartialOrder($partial_order); + } + + /** + * @param $scope_collector + * @param $call_to_scope_linker + * @param $catch_clause_type_resolver + * @throws \LogicException + * @return CombiningCalculatorInterface + */ + private function calculateEncounters(ScopeCollector $scope_collector, CallToScopeLinkingVisitor $call_to_scope_linker, CaughtExceptionTypesCalculator $catch_clause_type_resolver) { + $combining_mutable = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); + $combining_immutable = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); + + $encounters_calc = new \PhpExceptionFlow\EncountersCalculator($combining_mutable, $combining_immutable, $call_to_scope_linker->getCalleeCalledByCallerScopes()); + + $raises_calculator = new \PhpExceptionFlow\FlowCalculator\RaisesCalculator(new AstNodeTraverser(), new AstVisitor\ThrowsCollector(true)); + $raises_scope_traverser = new \PhpExceptionFlow\Scope\ScopeTraverser(); + $raises_wrapping_visitor = new CalculatorWrappingVisitor($raises_calculator, CalculatorWrappingVisitor::CALCULATE_ON_ENTER); + $raises_scope_traverser->addVisitor($raises_wrapping_visitor); + $traversing_raises_calculator = new \PhpExceptionFlow\FlowCalculator\TraversingCalculator($raises_scope_traverser, $raises_wrapping_visitor, $raises_calculator); + + $combining = new \PhpExceptionFlow\FlowCalculator\CombiningCalculator(); + + $uncaught_calculator = new \PhpExceptionFlow\FlowCalculator\UncaughtCalculator($catch_clause_type_resolver, $combining); + $propagates_calculator = new \PhpExceptionFlow\FlowCalculator\PropagatesCalculator($call_to_scope_linker->getCallerCallsCalleeScopes(), $combining); + + $combining_mutable->addCalculator($uncaught_calculator); + $combining_mutable->addCalculator($propagates_calculator); + $combining_immutable->addCalculator($traversing_raises_calculator); + + $combining->addCalculator($combining_immutable); + $combining->addCalculator($combining_mutable); + + $encounters_calc->calculateEncounters($scope_collector->getTopLevelScopes()); + + return $combining; + } + + + /** + * @param array $map + * @return array + */ + private function serializeClassMethodToMethodMap(array $map) { + $res = []; + foreach ($map as $class => $call_sites) { + $res[$class] = []; + foreach ($call_sites as $call_site => $methods) { + $res[$class][$call_site] = []; + foreach ($methods as $method) { + $res[$class][$call_site][] = (string)$method; + } + + + /*$res .= sprintf("%s->%s() resolves to: \n", array_pop($class_name), $call_site); + foreach ($methods as $method) { + $methods_class = explode("\\", $method->getClass()); + $res .= sprintf("\t%s->%s\n", array_pop($methods_class), $method->getName()); + }*/ + } + } + + return $res; + } + + /** + * @param CallToScopeLinkingVisitor $call_linker + * @return array + */ + private function serializeScopeCallsScopeMap(CallToScopeLinkingVisitor $call_linker) { + $res = []; + $call_map = $call_linker->getCallerCallsCalleeScopes(); + foreach ($call_map as $caller) { + $res[$caller->getName()] = []; + foreach ($call_map[$caller] as $callee) { + $res[$caller->getName()][] = $callee->getName(); + } + } + return $res; + } + + /** + * @param $unresolved_calls + * @return array + */ + private function serializeUnresolvedCalls($unresolved_calls) { + $prettyPrinter = new Standard(); + $res = []; + /** @var \PhpExceptionFlow\Scope\Scope $caller */ + foreach ($unresolved_calls as $caller) { + $res[$caller->getName()] = []; + foreach ($unresolved_calls[$caller] as $call_node) { + $res[$caller->getName()][] = [ + "message" => $unresolved_calls[$caller][$call_node], + "code" => $prettyPrinter->prettyPrint([$call_node]), + ]; + } + } + return $res; + } + + private function createNeededDirectories($path_to_project, $results_folder) { + $project_name = basename(realpath($path_to_project)); + + if (is_dir(__DIR__ . "/../../cache/" . $project_name) === false) { + mkdir(__DIR__ . "/../../cache/" . $project_name, 0777, true); + mkdir(__DIR__ . "/../../cache/" . $project_name . "/ast", 0777, true); + } + + if (is_dir($results_folder . "/" . $project_name) === false) { + mkdir($results_folder . "/" . $project_name, 0777, true); + } else { + throw new \LogicException(sprintf("The output folder %s already exists.", $results_folder . "/" . $project_name)); + } + } +} \ No newline at end of file diff --git a/src/PhpExceptionFlow/Scope/ScopeVisitor/CatchesPathVisitor.php b/src/PhpExceptionFlow/Scope/ScopeVisitor/CatchesPathVisitor.php new file mode 100644 index 0000000..e813cd3 --- /dev/null +++ b/src/PhpExceptionFlow/Scope/ScopeVisitor/CatchesPathVisitor.php @@ -0,0 +1,79 @@ +uncaught_calculator = $uncaught_calculator; + $this->file_resource = $file_resource; + } + + public function beforeTraverse(array $scopes) { + $this->paths = []; + } + + public function enterGuardedScope(GuardedScope $guarded_scope) { + $scope_name = $guarded_scope->getInclosedScope()->getName(); + + foreach ($guarded_scope->getCatchClauses() as $catch_) { + try { + /** @var Exception_[] $caught_exceptions */ + $caught_exceptions = $this->uncaught_calculator->getCaughtExceptions($catch_); + } catch (\UnexpectedValueException $e) { //catch clause does not catch anything, so just ignore + $caught_exceptions = []; + } + + $exception_type_occurrences = []; + if (empty($caught_exceptions) === false) { + if (isset($this->paths[$scope_name]) === false) { + $this->paths[$scope_name] = []; + } + + foreach ($caught_exceptions as $caught_exception) { + if(isset($exception_type_occurrences[(string)$caught_exception]) === false) { + $exception_type_occurrences[(string)$caught_exception] = 0; + } else { + $exception_type_occurrences[(string)$caught_exception] += 1; + } + + $exception_name = (string)$caught_exception . "#" . $exception_type_occurrences[(string)$caught_exception]; + foreach ($caught_exception->getPathsToCatchClause($catch_) as $path) { + $this->paths[$scope_name][$exception_name] = $this->pathToJsonSerialiazable($path); + } + + } + } + } + } + + public function afterTraverse(array $scopes) { + fwrite($this->file_resource, json_encode($this->paths, JSON_PRETTY_PRINT)); + } + + /** + * @param PathEntryInterface[] $path + * @return array + */ + private function pathToJsonSerialiazable(array $path) { + $result = []; + foreach ($path as $entry) { + $result[] = [ + "scope" => $entry->getToScope()->getName(), + "link" => $entry->getType() + ]; + } + return $result; + } +} \ No newline at end of file diff --git a/src/PhpExceptionFlow/Scope/ScopeVisitor/CaughtExceptionTypesCalculator.php b/src/PhpExceptionFlow/Scope/ScopeVisitor/CaughtExceptionTypesCalculator.php index b0296f1..69e5a43 100644 --- a/src/PhpExceptionFlow/Scope/ScopeVisitor/CaughtExceptionTypesCalculator.php +++ b/src/PhpExceptionFlow/Scope/ScopeVisitor/CaughtExceptionTypesCalculator.php @@ -22,6 +22,16 @@ public function enterGuardedScope(GuardedScope $guarded_scope) { foreach ($guarded_scope->getCatchClauses() as $catch_clause) { $caught_type = strtolower(implode('\\', $catch_clause->type->parts)); $caught_types = []; + + if (isset($this->state->classResolvedBy[$caught_type]) === false) { + //todo: this is a really bad idea + //Apparently, this type is unknown (probably because it is an installed extension and not a native PHP/included package type) + //a type always resolves to itself, so just add it like that. + //this might result in uncaught exceptions that are actually caught... + $this->state->classResolvedBy[$caught_type] = [$caught_type => $caught_type]; + $this->state->classResolves[$caught_type] = [$caught_type => $caught_type]; + print sprintf("Added type to type matrix, as it was unknown: %s\n", $caught_type); + } foreach ($this->state->classResolvedBy[$caught_type] as $resolved_by) { if (in_array($resolved_by, $already_caught, true) === false) { $caught_types[] = new Type(Type::TYPE_OBJECT, [], $resolved_by); diff --git a/src/PhpExceptionFlow/Scope/ScopeVisitor/JsonPrintingVisitor.php b/src/PhpExceptionFlow/Scope/ScopeVisitor/JsonPrintingVisitor.php index 6b7ed37..06ed233 100644 --- a/src/PhpExceptionFlow/Scope/ScopeVisitor/JsonPrintingVisitor.php +++ b/src/PhpExceptionFlow/Scope/ScopeVisitor/JsonPrintingVisitor.php @@ -14,8 +14,13 @@ class JsonPrintingVisitor extends AbstractScopeVisitor { private $top_level_entries = []; private $key_stack = []; + /** @var \SplObjectStorage $unique_exceptions */ + private $unique_exceptions; + + public function __construct(CombiningCalculatorInterface $encounters_calculator) { $this->encounters_calculator = $encounters_calculator; + $this->unique_exceptions = new \SplObjectStorage; } public function enterScope(Scope $scope) { @@ -28,24 +33,28 @@ public function enterScope(Scope $scope) { ]; foreach ($encounters as $exception) { + if ($this->unique_exceptions->contains($exception) === false) { + $this->unique_exceptions->attach($exception, uniqid("exception_", true)); + } + $causes = $exception->getCauses($scope); foreach ($causes["raises"] as $original_scope) { - $scope_entry["raises"][] = (string)$exception; + $scope_entry["raises"][$this->unique_exceptions[$exception]] = (string)$exception; } /** @var Scope $original_scope */ foreach ($causes["propagates"] as $original_scope) { $original_scope_name = $original_scope->getName(); $propagated_exceptions = $scope_entry["propagates"][$original_scope_name] ?? []; - $propagated_exceptions[] = (string)$exception; + $propagated_exceptions[$this->unique_exceptions[$exception]] = (string)$exception; $scope_entry["propagates"][$original_scope_name] = $propagated_exceptions; } /** @var Scope $escaped_from_scope */ foreach ($causes["uncaught"] as $escaped_from_scope) { $escaped_from_scope_name = $escaped_from_scope->getName(); $escaped_exceptions = $scope_entry["uncaught"][$escaped_from_scope_name] ?? []; - $escaped_exceptions[] = (string)$exception; + $escaped_exceptions[$this->unique_exceptions[$exception]] = (string)$exception; $scope_entry["uncaught"][$escaped_from_scope_name] = $escaped_exceptions; } } @@ -73,12 +82,16 @@ public function enterGuardedScope(GuardedScope $guarded_scope) { } foreach ($inclosed_encounters as $exception) { + if ($this->unique_exceptions->contains($exception) === false) { + $this->unique_exceptions->attach($exception, uniqid("exception_", true)); + } + if (($catches_path_entry = $exception->pathEndsIn($inclosed)) !== false) { if ($catches_path_entry instanceof Catches === false) { throw new \LogicException(sprintf("Unknown type %s apparently terminates the Exception Flow, but it is unknown how to handle it.", get_class($catches_path_entry))); } /** @var Catches $catches_path_entry */ - $guarded_scope_entry["catch clauses"][(string)$catches_path_entry->getCaughtBy()->type][] = (string)$exception; + $guarded_scope_entry["catch clauses"][(string)$catches_path_entry->getCaughtBy()->type][$this->unique_exceptions[$exception]] = (string)$exception; } } diff --git a/test/PhpExceptionFlow/Collection/Test/Number.php b/test/PhpExceptionFlow/Collection/Test/Number.php index 14e2a1c..7134d8b 100644 --- a/test/PhpExceptionFlow/Collection/Test/Number.php +++ b/test/PhpExceptionFlow/Collection/Test/Number.php @@ -2,10 +2,20 @@ namespace PhpExceptionFlow\Collection\Test; //this class is only used for testing the PartialOrder class -class Number { +use PhpExceptionFlow\Collection\PartialOrder\PartialOrderElementInterface; + +class Number implements PartialOrderElementInterface { public $value; public function __construct($value) { $this->value = $value; } + + public function __toString() { + return (string)$this->value; + } + + public function jsonSerialize() { + return $this->__toString(); + } } \ No newline at end of file diff --git a/test/PhpExceptionFlow/Path/PathCollectionTest.php b/test/PhpExceptionFlow/Path/PathCollectionTest.php index 56c3871..1bdb6bf 100644 --- a/test/PhpExceptionFlow/Path/PathCollectionTest.php +++ b/test/PhpExceptionFlow/Path/PathCollectionTest.php @@ -117,4 +117,41 @@ public function testDoublePathOnlyShowsUpOnce() { $this->assertEquals([$initial_link, $propagates_2], $paths[2]); $this->assertEquals([$initial_link, $propagates_2, $propagates_3], $paths[3]); } + + + + public function testPathsUntilCatchClause() { + $scope_a = new Scope("a"); + $scope_b = new Scope("b"); + $scope_c = new Scope("c"); + $scope_d = new Scope("d"); + $guarding_d = new GuardedScope($this->createMock(Scope::class), $scope_d); + + $raises_a = new Raises($scope_a); + $propagates_a_b = new Propagates($scope_a, $scope_b); + $propagates_a_d = new Propagates($scope_a, $scope_d); + $propagates_b_a = new Propagates($scope_b, $scope_a); + $propagates_b_c = new Propagates($scope_b, $scope_c); + $propagates_b_d = new Propagates($scope_b, $scope_d); + $propagates_c_d = new Propagates($scope_c, $scope_d); + $catches_d = new Catches($guarding_d, $this->createMock(Catch_::class)); + + $path_collection = new PathCollection($raises_a); + $path_collection->addEntry($propagates_a_b); + $path_collection->addEntry($propagates_a_d); + $path_collection->addEntry($propagates_b_a); //introduces cycle, but is ignored. + $path_collection->addEntry($propagates_b_c); + $path_collection->addEntry($propagates_b_d); + $path_collection->addEntry($propagates_c_d); + $path_collection->addEntry($catches_d); + + $paths = []; + foreach ($path_collection->getPathsEndingInLink($catches_d) as $path) { + $paths[] = $path; + } + $this->assertCount(3, $paths); + $this->assertEquals([$raises_a, $propagates_a_d, $catches_d], $paths[0]); + $this->assertEquals([$raises_a, $propagates_a_b, $propagates_b_d, $catches_d], $paths[1]); + $this->assertEquals([$raises_a, $propagates_a_b, $propagates_b_c, $propagates_c_d, $catches_d], $paths[2]); + } } \ No newline at end of file diff --git a/test/assets/code/throw_in_closure_is_ignored.test b/test/assets/code/throw_in_closure_is_ignored.test new file mode 100644 index 0000000..ba07118 --- /dev/null +++ b/test/assets/code/throw_in_closure_is_ignored.test @@ -0,0 +1,7 @@ +