diff --git a/app/Application/Analyze/AnalyzeMetric.php b/app/Application/Analyze/AnalyzeMetric.php new file mode 100644 index 0000000..50423aa --- /dev/null +++ b/app/Application/Analyze/AnalyzeMetric.php @@ -0,0 +1,50 @@ +metric['name']; + } + + public function dependencies(): array + { + return $this->metric['dependencies']; + } + + public function abstract(): bool + { + return $this->metric['abstract']; + } + + public function efferentCoupling(): float + { + return $this->metric['coupling']['efferent']; + } + + public function afferentCoupling(): float + { + return $this->metric['coupling']['afferent']; + } + + public function instability(): float + { + return $this->metric['coupling']['instability']; + } + + public function numberOfAbstractDependencies(): int + { + return $this->metric['abstractness']['numberOfAbstractDependencies']; + } + + public function abstractnessRatio(): float + { + return $this->metric['abstractness']['ratio']; + } +} diff --git a/app/Application/Analyze/AnalyzeResponseMapper.php b/app/Application/Analyze/AnalyzeResponseMapper.php index 73695bc..32a5512 100644 --- a/app/Application/Analyze/AnalyzeResponseMapper.php +++ b/app/Application/Analyze/AnalyzeResponseMapper.php @@ -11,7 +11,14 @@ public function from(DependencyAggregator $dependencyAggregator): AnalyzeRespons { return new AnalyzeResponse( count: $dependencyAggregator->count(), - metrics: $dependencyAggregator->toArray(), + metrics: $this->map($dependencyAggregator), ); } -} + + private function map(DependencyAggregator $dependencyAggregator): array + { + return array_map(function (array $metric) { + return new AnalyzeMetric($metric); + }, $dependencyAggregator->toArray()); + } +} diff --git a/app/Commands/AbstractCommand.php b/app/Commands/AbstractCommand.php index 576058c..22e3aeb 100644 --- a/app/Commands/AbstractCommand.php +++ b/app/Commands/AbstractCommand.php @@ -6,10 +6,22 @@ abstract class AbstractCommand extends Command { - public function stringToList(string $key): array + public function optionToList(string $key): array { $value = $this->option($key); + return $this->stringToList($value); + } + + public function argumentToList(string $key): array + { + $value = $this->argument($key); + + return $this->stringToList($value); + } + + private function stringToList(?string $value): array + { return $value === null ? [] : explode(',', $value); } } diff --git a/app/Commands/Analyze/AnalyzeCommand.php b/app/Commands/Analyze/Class/AnalyzeCommand.php similarity index 82% rename from app/Commands/Analyze/AnalyzeCommand.php rename to app/Commands/Analyze/Class/AnalyzeCommand.php index 45d06a3..f57d076 100644 --- a/app/Commands/Analyze/AnalyzeCommand.php +++ b/app/Commands/Analyze/Class/AnalyzeCommand.php @@ -1,6 +1,6 @@ argument('path'), - only: $this->stringToList('only'), - exclude: $this->stringToList('exclude'), + only: $this->optionToList('only'), + exclude: $this->optionToList('exclude'), ); } diff --git a/app/Commands/Analyze/Graph/GraphPresenterFactory.php b/app/Commands/Analyze/Class/Graph/GraphPresenterFactory.php similarity index 68% rename from app/Commands/Analyze/Graph/GraphPresenterFactory.php rename to app/Commands/Analyze/Class/Graph/GraphPresenterFactory.php index 0006d5c..ce6392e 100644 --- a/app/Commands/Analyze/Graph/GraphPresenterFactory.php +++ b/app/Commands/Analyze/Class/Graph/GraphPresenterFactory.php @@ -1,13 +1,13 @@ option('graph') + ? $this->makeGraphPresenter($command) + : $this->makeSummaryPresenter($command); + } + + private function makeGraphPresenter(Command $command): AnalyzePresenter + { + return $this->graphPresenterFactory->make($command); + } + + private function makeSummaryPresenter(Command $command): AnalyzePresenter + { + return $this->summaryPresenterFactory->make($command); + } +} diff --git a/app/Commands/Analyze/Summary/SummaryPresenterFactory.php b/app/Commands/Analyze/Class/Summary/SummaryPresenterFactory.php similarity index 67% rename from app/Commands/Analyze/Summary/SummaryPresenterFactory.php rename to app/Commands/Analyze/Class/Summary/SummaryPresenterFactory.php index 4068500..c32537d 100644 --- a/app/Commands/Analyze/Summary/SummaryPresenterFactory.php +++ b/app/Commands/Analyze/Class/Summary/SummaryPresenterFactory.php @@ -1,13 +1,13 @@ execute( + request: $this->makeRequest(), + presenter: $this->makePresenter(), + ); + } + + private function makeRequest(): AnalyzeRequest + { + return new AnalyzeRequest( + path: $this->argument('path'), + ); + } + + private function makePresenter(): AnalyzePresenter + { + return app(PresenterFactory::class)->make($this); + } +} diff --git a/app/Commands/Analyze/Component/Factories/PresenterFactory.php b/app/Commands/Analyze/Component/Factories/PresenterFactory.php new file mode 100644 index 0000000..a42ae99 --- /dev/null +++ b/app/Commands/Analyze/Component/Factories/PresenterFactory.php @@ -0,0 +1,33 @@ +option('graph') + ? $this->makeGraphPresenter($command) + : $this->makeSummaryPresenter($command); + } + + private function makeGraphPresenter(Command $command): AnalyzePresenter + { + return $this->graphPresenterFactory->make($command); + } + + private function makeSummaryPresenter(Command $command): AnalyzePresenter + { + return $this->summaryPresenterFactory->make($command); + } +} diff --git a/app/Commands/Analyze/Component/Factories/TransformerFactory.php b/app/Commands/Analyze/Component/Factories/TransformerFactory.php new file mode 100644 index 0000000..d36c33a --- /dev/null +++ b/app/Commands/Analyze/Component/Factories/TransformerFactory.php @@ -0,0 +1,17 @@ + $command->argumentToList('components'), + ]); + } +} diff --git a/app/Commands/Analyze/Component/Graph/GraphPresenterFactory.php b/app/Commands/Analyze/Component/Graph/GraphPresenterFactory.php new file mode 100644 index 0000000..4ff7b45 --- /dev/null +++ b/app/Commands/Analyze/Component/Graph/GraphPresenterFactory.php @@ -0,0 +1,31 @@ +view, + mapper: $this->mapper, + transformer: $this->transformerFactory->make($command), + settings: $this->settingsFactory->make($command), + ); + } +} diff --git a/app/Commands/Analyze/Component/Graph/GraphSettingsFactory.php b/app/Commands/Analyze/Component/Graph/GraphSettingsFactory.php new file mode 100644 index 0000000..923adb7 --- /dev/null +++ b/app/Commands/Analyze/Component/Graph/GraphSettingsFactory.php @@ -0,0 +1,18 @@ +argumentToList('components'), + info: $command->option('info'), + debug: $command->option('debug'), + ); + } +} diff --git a/app/Commands/Analyze/Component/Summary/SummaryPresenterFactory.php b/app/Commands/Analyze/Component/Summary/SummaryPresenterFactory.php new file mode 100644 index 0000000..cdc9931 --- /dev/null +++ b/app/Commands/Analyze/Component/Summary/SummaryPresenterFactory.php @@ -0,0 +1,31 @@ +view, + mapper: $this->mapper, + transformer: $this->transformerFactory->make($command), + settings: $this->settingsFactory->make($command), + ); + } +} diff --git a/app/Commands/Analyze/Component/Summary/SummarySettingsFactory.php b/app/Commands/Analyze/Component/Summary/SummarySettingsFactory.php new file mode 100644 index 0000000..569a4ce --- /dev/null +++ b/app/Commands/Analyze/Component/Summary/SummarySettingsFactory.php @@ -0,0 +1,18 @@ +argumentToList('components'), + info: $command->option('info'), + debug: $command->option('debug'), + ); + } +} diff --git a/app/Commands/Analyze/PresenterFactory.php b/app/Commands/Analyze/PresenterFactory.php deleted file mode 100644 index d3c01f5..0000000 --- a/app/Commands/Analyze/PresenterFactory.php +++ /dev/null @@ -1,33 +0,0 @@ -option('graph') - ? $this->makeGraphPresenter($command) - : $this->makeSummaryPresenter($command); - } - - private function makeGraphPresenter(Command $command): GraphPresenter - { - return app(GraphPresenterFactory::class)->make($command); - } - - private function makeSummaryPresenter(Command $command): SummaryPresenter - { - return app(SummaryPresenterFactory::class)->make($command); - } -} diff --git a/app/Commands/Cyclic/CyclicCommand.php b/app/Commands/Cyclic/CyclicCommand.php index e3ad2f0..b912a0c 100644 --- a/app/Commands/Cyclic/CyclicCommand.php +++ b/app/Commands/Cyclic/CyclicCommand.php @@ -30,8 +30,8 @@ private function makeRequest(): CyclicRequest { return new CyclicRequest( path: $this->argument('path'), - only: $this->stringToList('only'), - exclude: $this->stringToList('exclude'), + only: $this->optionToList('only'), + exclude: $this->optionToList('exclude'), ); } diff --git a/app/Commands/Weakness/WeaknessCommand.php b/app/Commands/Weakness/WeaknessCommand.php index 6fcd5d5..0bbc346 100644 --- a/app/Commands/Weakness/WeaknessCommand.php +++ b/app/Commands/Weakness/WeaknessCommand.php @@ -32,8 +32,8 @@ private function makeRequest(): WeaknessRequest { return new WeaknessRequest( path: $this->argument('path'), - only: $this->stringToList('only'), - exclude: $this->stringToList('exclude'), + only: $this->optionToList('only'), + exclude: $this->optionToList('exclude'), ); } diff --git a/app/Infrastructure/Analyze/Adapters/Jerowork/NativeDecliner.php b/app/Infrastructure/Analyze/Adapters/Jerowork/NativeDecliner.php new file mode 100644 index 0000000..9f96d27 --- /dev/null +++ b/app/Infrastructure/Analyze/Adapters/Jerowork/NativeDecliner.php @@ -0,0 +1,51 @@ +toString(); + + return $this->isNativePrimitiveType($fqn) || $this->isNativePhpClass($fqn); + } + + private function isNativePrimitiveType(string $fqn): bool + { + return in_array(strtolower($fqn), $this->primitiveTypes, true); + } + + private function isNativePhpClass(string $fqn): bool + { + return function_exists($fqn) || + class_exists($fqn, false) || + interface_exists($fqn, false); + } +} diff --git a/app/Infrastructure/Analyze/Adapters/Jerowork/NodeTraverserFactory.php b/app/Infrastructure/Analyze/Adapters/Jerowork/NodeTraverserFactory.php index e99db9b..035324c 100644 --- a/app/Infrastructure/Analyze/Adapters/Jerowork/NodeTraverserFactory.php +++ b/app/Infrastructure/Analyze/Adapters/Jerowork/NodeTraverserFactory.php @@ -5,9 +5,19 @@ use PhpParser\NodeTraverser; use PhpParser\NodeTraverserInterface; use PhpParser\NodeVisitor\ParentConnectingVisitor; +use App\Infrastructure\Analyze\Adapters\Jerowork\NativeDecliner; use App\Infrastructure\Analyze\Adapters\Jerowork\Visitors\DetectClassTypeVisitor; use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\ParseClassFqnNodeVisitor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\ParseInlineFqnNodeVisitor; use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\ParseImportedFqnNodeVisitor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Decliner\NamespaceDecliner; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Decliner\ImportedFqnDecliner; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Decliner\PhpNativeAccessorDecliner; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Processor\RootLevelFunctionProcessor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Processor\FullyQualifiedNameProcessor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Processor\InlineFqnIsImportedProcessor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Processor\InlineFqnIsImportedAsAliasProcessor; +use Jerowork\ClassDependenciesParser\PhpParser\NodeVisitor\InlineFqnParser\Processor\InlineFqnWithinSameNamespaceProcessor; class NodeTraverserFactory { @@ -19,6 +29,23 @@ public function createTraverser(array &$collectors): NodeTraverserInterface $traverser->addVisitor(new ParseClassFqnNodeVisitor($collectors['dependencies'])); $traverser->addVisitor(new ParseImportedFqnNodeVisitor($collectors['dependencies'])); $traverser->addVisitor(new DetectClassTypeVisitor($collectors['type'])); + $traverser->addVisitor(new ParseInlineFqnNodeVisitor( + $collectors['dependencies'], + [ + new NamespaceDecliner(), + new ImportedFqnDecliner(), + new PhpNativeAccessorDecliner(), + new NativeDecliner(), + ], + [ + new FullyQualifiedNameProcessor(), + new RootLevelFunctionProcessor(), + new InlineFqnIsImportedProcessor(), + new InlineFqnIsImportedAsAliasProcessor(), + new InlineFqnWithinSameNamespaceProcessor(), + ], + )); + return $traverser; } diff --git a/app/Presenter/Analyze/Graph/Adapters/Cytoscape/CytoscapeGraph.php b/app/Infrastructure/Graph/Adapters/Cytoscape/CytoscapeNetwork.php similarity index 74% rename from app/Presenter/Analyze/Graph/Adapters/Cytoscape/CytoscapeGraph.php rename to app/Infrastructure/Graph/Adapters/Cytoscape/CytoscapeNetwork.php index 6fa9d0f..6bd1158 100644 --- a/app/Presenter/Analyze/Graph/Adapters/Cytoscape/CytoscapeGraph.php +++ b/app/Infrastructure/Graph/Adapters/Cytoscape/CytoscapeNetwork.php @@ -1,12 +1,12 @@ mapNodes($metrics); + $this->mapEdges($metrics); + + return $this->network; + } + + private function mapNodes(array $metrics): void + { + foreach ($metrics as $item) { + $this->network->addNode($item->name(), $item->instability()); + } + } + + private function mapEdges(array $metrics): void + { + foreach ($metrics as $item) { + + foreach ($item->dependencies() as $dependency) { + + if ($this->network->missingNode($dependency)) { + $this->network->addNode($dependency); + } + + $this->network->addEdge(source: $item->name(), target: $dependency); + } + } + } +} diff --git a/app/Presenter/Analyze/Graph/Adapters/Cytoscape/Edges.php b/app/Infrastructure/Graph/Adapters/Cytoscape/Edges.php similarity index 87% rename from app/Presenter/Analyze/Graph/Adapters/Cytoscape/Edges.php rename to app/Infrastructure/Graph/Adapters/Cytoscape/Edges.php index 1ad0292..454905c 100644 --- a/app/Presenter/Analyze/Graph/Adapters/Cytoscape/Edges.php +++ b/app/Infrastructure/Graph/Adapters/Cytoscape/Edges.php @@ -1,6 +1,6 @@ getExecCommand(); + + exec("$command $this->file"); + } + + public function save(string $html): void + { + file_put_contents('graph.html', $html); + } + + private function getExecCommand(): string + { + return match (PHP_OS_FAMILY) { + 'Windows' => 'start', + 'Darwin' => 'open', + default => 'xdg-open', + }; + } +} diff --git a/app/Presenter/Analyze/Graph/GraphEnums.php b/app/Presenter/Analyze/Class/Graph/GraphEnums.php similarity index 60% rename from app/Presenter/Analyze/Graph/GraphEnums.php rename to app/Presenter/Analyze/Class/Graph/GraphEnums.php index 9a6d073..b9fd786 100644 --- a/app/Presenter/Analyze/Graph/GraphEnums.php +++ b/app/Presenter/Analyze/Class/Graph/GraphEnums.php @@ -1,6 +1,6 @@ $metrics + */ + public function from(array $metrics): Network + { + $metrics = $this->metricMapper->from($metrics); + + $networkAttributes = $this->networkAttributesMapper->map($metrics); + + $network = $this->networkBuilder->build($networkAttributes); + + return $network; + } +} diff --git a/app/Presenter/Analyze/Graph/GraphPresenter.php b/app/Presenter/Analyze/Class/Graph/GraphPresenter.php similarity index 72% rename from app/Presenter/Analyze/Graph/GraphPresenter.php rename to app/Presenter/Analyze/Class/Graph/GraphPresenter.php index 009856e..c1e48e4 100644 --- a/app/Presenter/Analyze/Graph/GraphPresenter.php +++ b/app/Presenter/Analyze/Class/Graph/GraphPresenter.php @@ -1,22 +1,24 @@ transformer->apply($metrics); - $graph = $this->mapper->make($metrics); + $network = $this->mapper->from($metrics); - $this->view->show(new GraphViewModel($graph)); + $this->view->show(new GraphViewModel($network)); } } diff --git a/app/Presenter/Analyze/Graph/GraphSettings.php b/app/Presenter/Analyze/Class/Graph/GraphSettings.php similarity index 78% rename from app/Presenter/Analyze/Graph/GraphSettings.php rename to app/Presenter/Analyze/Class/Graph/GraphSettings.php index af580a0..61911e5 100644 --- a/app/Presenter/Analyze/Graph/GraphSettings.php +++ b/app/Presenter/Analyze/Class/Graph/GraphSettings.php @@ -1,6 +1,6 @@ view->make('graph', [ + $view = $this->view->make('class-graph', [ 'nodes' => $viewModel->nodes(), 'edges' => $viewModel->edges(), ]); @@ -36,12 +38,12 @@ private function render(GraphViewModel $viewModel): string private function open(): void { - exec('open graph.html'); + $this->systemFileLauncher->open(); } private function save(string $html): void { - file_put_contents('graph.html', $html); + $this->systemFileLauncher->save($html); } private function showInfo(GraphViewModel $viewModel): void diff --git a/app/Presenter/Analyze/Class/Graph/GraphViewModel.php b/app/Presenter/Analyze/Class/Graph/GraphViewModel.php new file mode 100644 index 0000000..d6cda45 --- /dev/null +++ b/app/Presenter/Analyze/Class/Graph/GraphViewModel.php @@ -0,0 +1,28 @@ +network->countNodes() > GraphEnums::READABILITY_THRESHOLD->value; + } + + public function nodes(): array + { + return $this->network->nodes(); + } + + public function edges(): array + { + return $this->network->edges(); + } +} diff --git a/app/Presenter/Analyze/Class/Shared/Metric.php b/app/Presenter/Analyze/Class/Shared/Metric.php new file mode 100644 index 0000000..ddc1aae --- /dev/null +++ b/app/Presenter/Analyze/Class/Shared/Metric.php @@ -0,0 +1,29 @@ +name; + } + + public function instability(): float + { + return $this->instability; + } + + public function dependencies(): array + { + return $this->dependencies; + } +} \ No newline at end of file diff --git a/app/Presenter/Analyze/Class/Shared/MetricMapper.php b/app/Presenter/Analyze/Class/Shared/MetricMapper.php new file mode 100644 index 0000000..fb521cf --- /dev/null +++ b/app/Presenter/Analyze/Class/Shared/MetricMapper.php @@ -0,0 +1,24 @@ +makeClass($metric); + }, $metrics); + } + + private function makeClass(AnalyzeMetric $metric): Metric + { + return new Metric( + name: $metric->name(), + instability: $metric->instability(), + dependencies: $metric->dependencies(), + ); + } +} diff --git a/app/Presenter/Analyze/Summary/SummaryMapper.php b/app/Presenter/Analyze/Class/Summary/SummaryMapper.php similarity index 53% rename from app/Presenter/Analyze/Summary/SummaryMapper.php rename to app/Presenter/Analyze/Class/Summary/SummaryMapper.php index a1a99cf..ee9663f 100644 --- a/app/Presenter/Analyze/Summary/SummaryMapper.php +++ b/app/Presenter/Analyze/Class/Summary/SummaryMapper.php @@ -1,10 +1,11 @@ $metric['name'], - 'Ec' => $metric['coupling']['efferent'], - 'Ac' => $metric['coupling']['afferent'], - 'I' => $metric['coupling']['instability'], - 'Na' => $metric['abstractness']['numberOfAbstractDependencies'], - 'A' => $metric['abstractness']['ratio'], + 'name' => $metric->name(), + 'Ec' => $metric->efferentCoupling(), + 'Ac' => $metric->afferentCoupling(), + 'I' => $metric->instability(), + 'Na' => $metric->numberOfAbstractDependencies(), + 'A' => $metric->abstractnessRatio(), ]; }, $metrics); } @@ -33,7 +34,7 @@ private static function formatHumanReadableMetrics(array $metrics): array { return array_map(function ($metric) { return [ - 'name' => $metric['name'], + 'name' => $metric->name(), 'stability' => StabilityCalculator::calculate($metric), 'abstractness' => AbstractnessCalculator::calculate($metric), 'maintainability' => MaintainabilityCalculator::calculate($metric), diff --git a/app/Presenter/Analyze/Summary/SummaryPresenter.php b/app/Presenter/Analyze/Class/Summary/SummaryPresenter.php similarity index 77% rename from app/Presenter/Analyze/Summary/SummaryPresenter.php rename to app/Presenter/Analyze/Class/Summary/SummaryPresenter.php index f8ef0cb..b28a7ca 100644 --- a/app/Presenter/Analyze/Summary/SummaryPresenter.php +++ b/app/Presenter/Analyze/Class/Summary/SummaryPresenter.php @@ -1,17 +1,17 @@ componentMapper->from($metrics); + + $networkAttributes = $this->networkAttributesMapper->map($components); + + return $this->networkBuilder->build($networkAttributes); + } +} diff --git a/app/Presenter/Analyze/Component/Graph/GraphPresenter.php b/app/Presenter/Analyze/Component/Graph/GraphPresenter.php new file mode 100644 index 0000000..4a37c33 --- /dev/null +++ b/app/Presenter/Analyze/Component/Graph/GraphPresenter.php @@ -0,0 +1,53 @@ +settings->debug) { + alert($e); + } + + alert($e->getMessage()); + } + + public function present(AnalyzeResponse $response): void + { + $metrics = $response->metrics; + + $metrics = $this->transformer->apply($metrics); + + $network = $this->mapper->from($metrics); + + $this->view->show(new GraphViewModel($network)); + } +} diff --git a/app/Presenter/Analyze/Component/Graph/GraphSettings.php b/app/Presenter/Analyze/Component/Graph/GraphSettings.php new file mode 100644 index 0000000..5f866e8 --- /dev/null +++ b/app/Presenter/Analyze/Component/Graph/GraphSettings.php @@ -0,0 +1,12 @@ +render($viewModel); + + $this->save($html); + + $this->open(); + + $this->showInfo($viewModel); + } + + private function render(GraphViewModel $viewModel): string + { + $view = $this->view->make('components-graph', [ + 'nodes' => $viewModel->nodes(), + 'edges' => $viewModel->edges(), + ]); + + return $view->render(); + } + + private function open(): void + { + $this->systemFileLauncher->open(); + } + + private function save(string $html): void + { + $this->systemFileLauncher->save($html); + } + + private function showInfo(GraphViewModel $viewModel): void + { + info('Graph successfully generated in graph.html'); + } +} diff --git a/app/Presenter/Analyze/Component/Graph/GraphViewModel.php b/app/Presenter/Analyze/Component/Graph/GraphViewModel.php new file mode 100644 index 0000000..e007da5 --- /dev/null +++ b/app/Presenter/Analyze/Component/Graph/GraphViewModel.php @@ -0,0 +1,22 @@ +network->nodes(); + } + + public function edges(): array + { + return $this->network->edges(); + } +} diff --git a/app/Presenter/Analyze/Component/Shared/Collector.php b/app/Presenter/Analyze/Component/Shared/Collector.php new file mode 100644 index 0000000..91cbe69 --- /dev/null +++ b/app/Presenter/Analyze/Component/Shared/Collector.php @@ -0,0 +1,41 @@ +name = $name; + } + + public function collect(AnalyzeMetric $metric): void + { + $this->totalInstability += $metric->instability(); + + if ($metric->abstract()) { + $this->countAbstractions++; + } + + $this->countClasses++; + } + + public function addDependency(string $dependency): void + { + $this->dependencies[] = $dependency; + } + + public function hasDependency(string $dependency): bool + { + return in_array($dependency, $this->dependencies, true); + } +} + \ No newline at end of file diff --git a/app/Presenter/Analyze/Component/Shared/Component.php b/app/Presenter/Analyze/Component/Shared/Component.php new file mode 100644 index 0000000..6f828a9 --- /dev/null +++ b/app/Presenter/Analyze/Component/Shared/Component.php @@ -0,0 +1,46 @@ +name; + } + + public function countClasses(): int + { + return $this->countClasses; + } + + public function countAbstractions(): int + { + return $this->countAbstractions; + } + + public function abstractness(): float + { + return number_format($this->countAbstractions / $this->countClasses, 2); + } + + public function instability(): float + { + return number_format($this->totalInstability / $this->countClasses, 2); + } + + public function dependencies(): array + { + return $this->dependencies; + } +} diff --git a/app/Presenter/Analyze/Component/Shared/ComponentFactory.php b/app/Presenter/Analyze/Component/Shared/ComponentFactory.php new file mode 100644 index 0000000..9df7d8b --- /dev/null +++ b/app/Presenter/Analyze/Component/Shared/ComponentFactory.php @@ -0,0 +1,17 @@ +name, + countClasses: $collector->countClasses, + countAbstractions: $collector->countAbstractions, + totalInstability: $collector->totalInstability, + dependencies: $collector->dependencies, + ); + } +} diff --git a/app/Presenter/Analyze/Component/Shared/ComponentMapper.php b/app/Presenter/Analyze/Component/Shared/ComponentMapper.php new file mode 100644 index 0000000..23308cd --- /dev/null +++ b/app/Presenter/Analyze/Component/Shared/ComponentMapper.php @@ -0,0 +1,55 @@ +> $metrics + */ + public function from(array $metrics): array + { + $components = array_keys($metrics); + + $items = []; + + foreach ($metrics as $component => $componentMetrics) { + + $collector = new Collector(); + + $collector->setName($component); + + foreach ($componentMetrics as $metric) { + + $collector->collect($metric); + + foreach ($metric->dependencies() as $dependency) { + + foreach ($components as $otherComponent) { + + if (Str::startsWith($dependency, $otherComponent)) { + + if ($otherComponent === $component) { + continue; + } + + if (! $collector->hasDependency($otherComponent)) { + $collector->addDependency($otherComponent); + } + } + } + } + } + + $items[] = $this->componentFactory->make($collector); + } + + return $items; + } +} \ No newline at end of file diff --git a/app/Presenter/Analyze/Component/Summary/SummaryMapper.php b/app/Presenter/Analyze/Component/Summary/SummaryMapper.php new file mode 100644 index 0000000..d0bb51b --- /dev/null +++ b/app/Presenter/Analyze/Component/Summary/SummaryMapper.php @@ -0,0 +1,32 @@ +componentMapper->from($metrics); + + return $this->format($components); + } + + private function format(array $components): array + { + return array_map(function ($component) { + return [ + 'name' => $component->name(), + 'Nc' => $component->countClasses(), + 'Na' => $component->countAbstractions(), + 'A' => $component->abstractness(), + 'I' => $component->instability(), + ]; + }, $components); + } +} diff --git a/app/Presenter/Analyze/Component/Summary/SummaryPresenter.php b/app/Presenter/Analyze/Component/Summary/SummaryPresenter.php new file mode 100644 index 0000000..69c8ccf --- /dev/null +++ b/app/Presenter/Analyze/Component/Summary/SummaryPresenter.php @@ -0,0 +1,52 @@ +settings->debug) { + alert($e); + } + + alert($e->getMessage()); + } + + public function present(AnalyzeResponse $response): void + { + $metrics = $response->metrics; + + $metrics = $this->transformer->apply($metrics); + + $metrics = $this->mapper->from($metrics); + + $this->view->show(new SummaryViewModel($metrics)); + } +} diff --git a/app/Presenter/Analyze/Component/Summary/SummarySettings.php b/app/Presenter/Analyze/Component/Summary/SummarySettings.php new file mode 100644 index 0000000..e962c9b --- /dev/null +++ b/app/Presenter/Analyze/Component/Summary/SummarySettings.php @@ -0,0 +1,12 @@ +displayMetrics($viewModel); + $this->displayInfo($viewModel); + } + + private function displayMetrics(SummaryViewModel $viewModel): void + { + $viewModel->hasComponents() + ? $this->showComponents($viewModel) + : warning('No classes found'); + } + + private function showComponents(SummaryViewModel $viewModel): void + { + table( + headers: $viewModel->headers(), + rows: $viewModel->components(), + ); + } + + private function displayInfo(SummaryViewModel $viewModel): void + { + $viewModel->needInfo() + ? $this->showInfo($viewModel) + : $this->showOutro($viewModel); + } + + private function showInfo(SummaryViewModel $viewModel): void + { + $viewModel->isHumanReadable() + ? $this->showHumanReadableInfo() + : $this->showMetricsInfo(); + } + + private function showHumanReadableInfo(): void + { + outro('For more information, see the documentation: https://php-quality-tools.com/class-dependencies-analyzer'); + } + + private function showMetricsInfo(): void + { + outro('Try --human-readable to get a more human readable output.'); + outro('See the documentation for more information : https://php-quality-tools.com/class-dependencies-analyzer'); + } + + private function showOutro(SummaryViewModel $viewModel): void + { + outro('Add --info to get more information on metrics.'); + outro(sprintf('Found %d classes in the given path', $viewModel->count())); + } +} diff --git a/app/Presenter/Analyze/Component/Summary/SummaryViewModel.php b/app/Presenter/Analyze/Component/Summary/SummaryViewModel.php new file mode 100644 index 0000000..b9a2bab --- /dev/null +++ b/app/Presenter/Analyze/Component/Summary/SummaryViewModel.php @@ -0,0 +1,40 @@ +components; + } + + public function headers(): array + { + return array_keys(array_values($this->components)[0]); + } + + public function count(): int + { + return count($this->components); + } + + public function hasComponents(): bool + { + return $this->count() > 0; + } + + public function needInfo(): bool + { + return false; + } + + public function isHumanReadable(): bool + { + return false; + } +} diff --git a/app/Presenter/Analyze/Filters/Contracts/Transformer.php b/app/Presenter/Analyze/Filters/Contracts/Transformer.php deleted file mode 100644 index 630197a..0000000 --- a/app/Presenter/Analyze/Filters/Contracts/Transformer.php +++ /dev/null @@ -1,8 +0,0 @@ -mapNodes($metrics); - $this->mapEdges($metrics); - - return $this->graph; - } - - private function mapNodes(array $metrics): void - { - foreach ($metrics as $item) { - $this->graph->addNode($item['name'], $item['coupling']['instability']); - } - } - - private function mapEdges(array $metrics): void - { - foreach ($metrics as $item) { - - foreach ($item['dependencies'] as $dependency) { - - if ($this->graph->missingNode($dependency)) { - $this->graph->addNode($dependency); - } - - $this->graph->addEdge(source: $item['name'], target: $dependency); - } - } - } -} diff --git a/app/Presenter/Analyze/Graph/GraphViewModel.php b/app/Presenter/Analyze/Graph/GraphViewModel.php deleted file mode 100644 index e3f4f93..0000000 --- a/app/Presenter/Analyze/Graph/GraphViewModel.php +++ /dev/null @@ -1,28 +0,0 @@ -graph->countNodes() > GraphEnums::READABILITY_THRESHOLD->value; - } - - public function nodes(): array - { - return $this->graph->nodes(); - } - - public function edges(): array - { - return $this->graph->edges(); - } -} diff --git a/app/Presenter/Analyze/Graph/Ports/Graph.php b/app/Presenter/Analyze/Graph/Ports/Graph.php deleted file mode 100644 index 165641d..0000000 --- a/app/Presenter/Analyze/Graph/Ports/Graph.php +++ /dev/null @@ -1,13 +0,0 @@ -abstractnessRatio(); + + if ($ratio > 0.7) { + return 'abstract'; + } + + if ($ratio < 0.3) { + return 'concrete'; + } + + return 'balanced'; + } +} diff --git a/app/Presenter/Analyze/Shared/Calculators/Calculator.php b/app/Presenter/Analyze/Shared/Calculators/Calculator.php new file mode 100644 index 0000000..f8032fd --- /dev/null +++ b/app/Presenter/Analyze/Shared/Calculators/Calculator.php @@ -0,0 +1,10 @@ +abstractnessRatio() < 0.3 && $metric->instability() > 0.7; + } + + private static function isLowlyAbstractAndHighlyStable(AnalyzeMetric $metric): bool + { + return $metric->abstractnessRatio() < 0.3 && $metric->instability() < 0.3; + } +} diff --git a/app/Presenter/Analyze/Shared/Calculators/StabilityCalculator.php b/app/Presenter/Analyze/Shared/Calculators/StabilityCalculator.php new file mode 100644 index 0000000..71fd55c --- /dev/null +++ b/app/Presenter/Analyze/Shared/Calculators/StabilityCalculator.php @@ -0,0 +1,24 @@ +instability(); + + if ($instability > 0.7) { + return 'unstable'; + } + + if ($instability < 0.3) { + return 'stable'; + } + + return 'flexible'; + } +} diff --git a/app/Presenter/Analyze/Shared/Filters/Collectors/Components.php b/app/Presenter/Analyze/Shared/Filters/Collectors/Components.php new file mode 100644 index 0000000..e744b44 --- /dev/null +++ b/app/Presenter/Analyze/Shared/Filters/Collectors/Components.php @@ -0,0 +1,25 @@ +components = $components; + } + + public function get(): array + { + return $this->components; + } + + public function add(string $component, array $metric): void + { + $this->components[$component][] = $metric; + } +} diff --git a/app/Presenter/Analyze/Filters/Collectors/Depth.php b/app/Presenter/Analyze/Shared/Filters/Collectors/Depth.php similarity index 55% rename from app/Presenter/Analyze/Filters/Collectors/Depth.php rename to app/Presenter/Analyze/Shared/Filters/Collectors/Depth.php index 1996362..f04b4fb 100644 --- a/app/Presenter/Analyze/Filters/Collectors/Depth.php +++ b/app/Presenter/Analyze/Shared/Filters/Collectors/Depth.php @@ -1,14 +1,16 @@ depth[$class['name']] = $class; + $this->depth[$class->name()] = $class; } public function has(string $name): bool diff --git a/app/Presenter/Analyze/Filters/Collectors/Metrics.php b/app/Presenter/Analyze/Shared/Filters/Collectors/Metrics.php similarity index 66% rename from app/Presenter/Analyze/Filters/Collectors/Metrics.php rename to app/Presenter/Analyze/Shared/Filters/Collectors/Metrics.php index f4e1951..2746cfe 100644 --- a/app/Presenter/Analyze/Filters/Collectors/Metrics.php +++ b/app/Presenter/Analyze/Shared/Filters/Collectors/Metrics.php @@ -1,6 +1,8 @@ metrics[$name]); } - public function get(string $name): array + public function get(string $name): AnalyzeMetric { return $this->metrics[$name]; } diff --git a/app/Presenter/Analyze/Shared/Filters/Contracts/Transformer.php b/app/Presenter/Analyze/Shared/Filters/Contracts/Transformer.php new file mode 100644 index 0000000..121768e --- /dev/null +++ b/app/Presenter/Analyze/Shared/Filters/Contracts/Transformer.php @@ -0,0 +1,17 @@ + $metrics + * @return array + */ + public function apply(array $metrics): array; +} diff --git a/app/Presenter/Analyze/Shared/Filters/Transformers/ComponentTransformer.php b/app/Presenter/Analyze/Shared/Filters/Transformers/ComponentTransformer.php new file mode 100644 index 0000000..21b879f --- /dev/null +++ b/app/Presenter/Analyze/Shared/Filters/Transformers/ComponentTransformer.php @@ -0,0 +1,40 @@ +targetedComponents as $component) { + + $name = $metric->name(); + + if ($this->isTargetedComponent($name, $component)) { + + $components[$component][] = $metric; + + break; + } + } + } + + return $components; + } + + private function isTargetedComponent(string $name, string $component): bool + { + return Str::startsWith($name, $component); + } +} diff --git a/app/Presenter/Analyze/Filters/Transformers/NullTransformer.php b/app/Presenter/Analyze/Shared/Filters/Transformers/NullTransformer.php similarity index 53% rename from app/Presenter/Analyze/Filters/Transformers/NullTransformer.php rename to app/Presenter/Analyze/Shared/Filters/Transformers/NullTransformer.php index 3d735da..cc11a16 100644 --- a/app/Presenter/Analyze/Filters/Transformers/NullTransformer.php +++ b/app/Presenter/Analyze/Shared/Filters/Transformers/NullTransformer.php @@ -1,8 +1,8 @@ metrics->set($metrics); @@ -28,7 +31,7 @@ public function apply(array $metrics): array $this->depth->add($targetClass); - foreach ($targetClass['dependencies'] as $dependency) { + foreach ($targetClass->dependencies() as $dependency) { $this->deepDive($dependency); } @@ -54,7 +57,7 @@ private function deepDive(string $dependency): void $this->incrementDeep(); - foreach ($targetClass['dependencies'] as $innerDependency) { + foreach ($targetClass->dependencies() as $innerDependency) { $this->deepDive($innerDependency); } diff --git a/app/Presenter/Analyze/Shared/Network/Network.php b/app/Presenter/Analyze/Shared/Network/Network.php new file mode 100644 index 0000000..13fe967 --- /dev/null +++ b/app/Presenter/Analyze/Shared/Network/Network.php @@ -0,0 +1,10 @@ +name; + } + + public function instability(): float + { + return $this->instability; + } + + public function dependencies(): array + { + return $this->dependencies; + } +} diff --git a/app/Presenter/Analyze/Shared/Network/NetworkAttributesMapper.php b/app/Presenter/Analyze/Shared/Network/NetworkAttributesMapper.php new file mode 100644 index 0000000..0407c32 --- /dev/null +++ b/app/Presenter/Analyze/Shared/Network/NetworkAttributesMapper.php @@ -0,0 +1,29 @@ +makeNetworkAttribute($item); + } + + return $attributes; + } + + public function makeNetworkAttribute(Networkable $item): NetworkAttribute + { + return new NetworkAttribute( + $item->name(), + $item->instability(), + $item->dependencies(), + ); + } +} diff --git a/app/Presenter/Analyze/Shared/Network/NetworkBuilder.php b/app/Presenter/Analyze/Shared/Network/NetworkBuilder.php new file mode 100644 index 0000000..f0d5f6f --- /dev/null +++ b/app/Presenter/Analyze/Shared/Network/NetworkBuilder.php @@ -0,0 +1,14 @@ + $attributes + */ + public function build(array $attributes): Network; +} diff --git a/app/Presenter/Analyze/Shared/Network/Networkable.php b/app/Presenter/Analyze/Shared/Network/Networkable.php new file mode 100644 index 0000000..34d8cf0 --- /dev/null +++ b/app/Presenter/Analyze/Shared/Network/Networkable.php @@ -0,0 +1,10 @@ + 0.7) { - return 'abstract'; - } - - if ($ratio < 0.3) { - return 'concrete'; - } - - return 'balanced'; - } -} diff --git a/app/Presenter/Analyze/Summary/Calculators/Calculator.php b/app/Presenter/Analyze/Summary/Calculators/Calculator.php deleted file mode 100644 index 2f549f3..0000000 --- a/app/Presenter/Analyze/Summary/Calculators/Calculator.php +++ /dev/null @@ -1,8 +0,0 @@ - 0.7; - } - - private static function isLowlyAbstractAndHighlyStable(float $abstractness, float $instability): bool - { - return $abstractness < 0.3 && $instability < 0.3; - } - - private static function getValues(array $metric): array - { - return [ - $metric['abstractness']['ratio'], - $metric['coupling']['instability'], - ]; - } -} diff --git a/app/Presenter/Analyze/Summary/Calculators/StabilityCalculator.php b/app/Presenter/Analyze/Summary/Calculators/StabilityCalculator.php deleted file mode 100644 index e338134..0000000 --- a/app/Presenter/Analyze/Summary/Calculators/StabilityCalculator.php +++ /dev/null @@ -1,23 +0,0 @@ - 0.7) { - return 'unstable'; - } - - if ($instability < 0.3) { - return 'stable'; - } - - return 'flexible'; - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6ab9c37..daab933 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,14 +6,18 @@ use Illuminate\Support\ServiceProvider; use App\Domain\Ports\Aggregators\FileAggregator; use App\Domain\Ports\Repositories\FileRepository; -use App\Presenter\Analyze\Graph\Ports\GraphMapper; +use App\Presenter\Analyze\Shared\Views\SystemFileLauncher; +use App\Presenter\Analyze\Shared\Ports\GraphMapper; use App\Infrastructure\Analyze\Ports\AnalyzerService; +use App\Presenter\Analyze\Shared\Network\NetworkBuilder; use App\Infrastructure\Analyze\Ports\ClassDependenciesParser; -use App\Infrastructure\Analyze\Adapters\Services\AnalyzerServiceAdapter; +use App\Infrastructure\Views\Adapters\SystemFileLauncherAdapter; +use App\Infrastructure\Graph\Adapters\Cytoscape\CytoscapeGraphMapper; use App\Infrastructure\Analyze\Adapters\Jerowork\NodeTraverserFactory; use App\Infrastructure\File\Adapters\Aggregators\FileAggregatorAdapter; +use App\Infrastructure\Analyze\Adapters\Services\AnalyzerServiceAdapter; use App\Infrastructure\File\Adapters\Repositories\FileRepositoryAdapter; -use App\Presenter\Analyze\Graph\Adapters\Cytoscape\CytoscapeGraphMapper; +use App\Infrastructure\Graph\Adapters\Cytoscape\CytoscapeNetworkBuilder; use App\Infrastructure\Analyze\Adapters\Jerowork\ClassDependenciesParserAdapter; class AppServiceProvider extends ServiceProvider @@ -37,7 +41,9 @@ public function register(): void $this->app->bind(AnalyzerService::class, AnalyzerServiceAdapter::class); - $this->app->bind(GraphMapper::class, CytoscapeGraphMapper::class); + $this->app->bind(NetworkBuilder::class, CytoscapeNetworkBuilder::class); + + $this->app->bind(SystemFileLauncher::class, SystemFileLauncherAdapter::class); $this->app->bind(ClassDependenciesParser::class, function () { return new ClassDependenciesParserAdapter( diff --git a/composer.json b/composer.json index 6dfa853..85c036d 100644 --- a/composer.json +++ b/composer.json @@ -60,12 +60,14 @@ "php class-dependencies-analyzer cyclic app", "php class-dependencies-analyzer cyclic app --only='App\\Application'", "php class-dependencies-analyzer cyclic app --exclude='App\\Infrastructure'", - "php class-dependencies-analyzer analyze app", - "php class-dependencies-analyzer analyze app --only='App\\Application'", - "php class-dependencies-analyzer analyze app --exclude='App\\Infrastructure'", - "php class-dependencies-analyzer analyze app --target='App\\Application\\Analyze\\AnalyzeAction'", - "php class-dependencies-analyzer analyze app --target='App\\Application\\Analyze\\AnalyzeAction' --depth-limit=2", - "php class-dependencies-analyzer analyze app --graph", + "php class-dependencies-analyzer analyze:class app", + "php class-dependencies-analyzer analyze:class app --only='App\\Application'", + "php class-dependencies-analyzer analyze:class app --exclude='App\\Infrastructure'", + "php class-dependencies-analyzer analyze:class app --target='App\\Application\\Analyze\\AnalyzeAction'", + "php class-dependencies-analyzer analyze:class app --target='App\\Application\\Analyze\\AnalyzeAction' --depth-limit=2", + "php class-dependencies-analyzer analyze:class app --graph", + "php class-dependencies-analyzer analyze:component app 'App\\Application'", + "php class-dependencies-analyzer analyze:component app 'App\\Application' --graph", "vendor/bin/pest -p" ], "coverage": "php -d xdebug.mode=coverage vendor/bin/pest --coverage -p" diff --git a/resources/views/graph.blade.php b/resources/views/class-graph.blade.php similarity index 100% rename from resources/views/graph.blade.php rename to resources/views/class-graph.blade.php diff --git a/resources/views/components-graph.blade.php b/resources/views/components-graph.blade.php new file mode 100644 index 0000000..db6a887 --- /dev/null +++ b/resources/views/components-graph.blade.php @@ -0,0 +1,169 @@ + + + + + + Module Dependency Graph + + + + + + + +
+
+
+
High Instability (> 0.8) +
+
+
Medium Instability (> 0.4) +
+
+
Low Instability (≤ 0.4) +
+
+
Unstable Dependency (red arrow) +
+
+ + + + \ No newline at end of file diff --git a/tests/Builders/AnalyzeMetricBuilder.php b/tests/Builders/AnalyzeMetricBuilder.php new file mode 100644 index 0000000..a704fb0 --- /dev/null +++ b/tests/Builders/AnalyzeMetricBuilder.php @@ -0,0 +1,70 @@ +name = $value; + + return $this; + } + + public function isAbstract(): self + { + $this->abstract = true; + + return $this; + } + + public function withDependencies(array $value): self + { + $this->dependencies = $value; + + return $this; + } + + public function withInstability(float $value): self + { + $this->instability = $value; + + return $this; + } + + public function withAbstractnessRatio(float $value): self + { + $this->ratio = $value; + + return $this; + } + + public function build(): AnalyzeMetric + { + return new AnalyzeMetric([ + 'name' => $this->name, + 'dependencies' => $this->dependencies, + 'abstract' => $this->abstract, + 'coupling' => [ + 'efferent' => $this->efferent, + 'afferent' => $this->afferent, + 'instability' => $this->instability, + ], + 'abstractness' => [ + 'numberOfAbstractDependencies' => $this->numberOfAbstractDependencies, + 'ratio' => $this->ratio, + ], + ]); + } +} diff --git a/tests/Feature/AnalyzeClassTest.php b/tests/Feature/AnalyzeClassTest.php new file mode 100755 index 0000000..18e38b8 --- /dev/null +++ b/tests/Feature/AnalyzeClassTest.php @@ -0,0 +1,21 @@ +artisan('analyze:class app')->assertSuccessful(); +}); + +it('can run the analyze:class command with a custom target', function () { + $this->artisan('analyze:class app --target=App\Application\Analyze\AnalyzeAction')->assertSuccessful(); +}); + +it('can run the analyze:class command with only a specific namespace', function () { + $this->artisan('analyze:class app --only=App\Application')->assertSuccessful(); +}); + +it('can run the analyze:class command with exclude a specific namespace', function () { + $this->artisan('analyze:class app --exclude=App\Application')->assertSuccessful(); +}); + +it('can run the analyze:class command with graph', function () { + $this->artisan('analyze:class app --graph')->assertSuccessful(); +})->skip('need to find a way to bypass graph generation'); diff --git a/tests/Feature/CyclicTest.php b/tests/Feature/CyclicTest.php new file mode 100644 index 0000000..9a0c2ee --- /dev/null +++ b/tests/Feature/CyclicTest.php @@ -0,0 +1,13 @@ +artisan('cyclic app')->assertSuccessful(); +}); + +it('can run the cyclic command with only option', function () { + $this->artisan("cyclic app --only='App\Application'")->assertSuccessful(); +}); + +it('can run the cyclic command with exclude option', function () { + $this->artisan("cyclic app --exclude='App\Application'")->assertSuccessful(); +}); diff --git a/tests/Feature/InspireCommandTest.php b/tests/Feature/InspireCommandTest.php deleted file mode 100755 index b3d9bbc..0000000 --- a/tests/Feature/InspireCommandTest.php +++ /dev/null @@ -1 +0,0 @@ -artisan('weakness app')->assertSuccessful(); +}); + +it('can run the weakness command with only option', function () { + $this->artisan("weakness app --only='App\Application'")->assertSuccessful(); +}); + +it('can run the weakness command with exclude option', function () { + $this->artisan("weakness app --exclude='App\Application'")->assertSuccessful(); +}); + +it('can run the weakness command with limit option', function () { + $this->artisan("weakness app --limit=10")->assertSuccessful(); +}); + +it('can run the weakness command with min-delta option', function () { + $this->artisan("weakness app --min-delta=10")->assertSuccessful(); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 6e6c3c6..452ae86 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Tests; +use Tests\Builders\AnalyzeMetricBuilder; use Tests\Builders\ClassDependenciesBuilder; use Tests\Builders\DependencyAggregatorBuilder; use LaravelZero\Framework\Testing\TestCase as BaseTestCase; @@ -19,4 +20,9 @@ public function oneDependencyAggregator(): DependencyAggregatorBuilder { return app(DependencyAggregatorBuilder::class); } + + public function oneAnalyzeMetric(): AnalyzeMetricBuilder + { + return app(AnalyzeMetricBuilder::class); + } } diff --git a/tests/Unit/Aggregators/DependencyAggregatorTest.php b/tests/Unit/Domain/Aggregators/DependencyAggregatorTest.php similarity index 100% rename from tests/Unit/Aggregators/DependencyAggregatorTest.php rename to tests/Unit/Domain/Aggregators/DependencyAggregatorTest.php diff --git a/tests/Unit/Entities/ClassDependenciesTest.php b/tests/Unit/Domain/Entities/ClassDependenciesTest.php similarity index 100% rename from tests/Unit/Entities/ClassDependenciesTest.php rename to tests/Unit/Domain/Entities/ClassDependenciesTest.php diff --git a/tests/Unit/Services/CyclicDependencyTest.php b/tests/Unit/Domain/Services/CyclicDependencyTest.php similarity index 100% rename from tests/Unit/Services/CyclicDependencyTest.php rename to tests/Unit/Domain/Services/CyclicDependencyTest.php diff --git a/tests/Unit/ValueObjects/CouplingTest.php b/tests/Unit/Domain/ValueObjects/CouplingTest.php similarity index 100% rename from tests/Unit/ValueObjects/CouplingTest.php rename to tests/Unit/Domain/ValueObjects/CouplingTest.php diff --git a/tests/Unit/ValueObjects/FqcnTest.php b/tests/Unit/Domain/ValueObjects/FqcnTest.php similarity index 100% rename from tests/Unit/ValueObjects/FqcnTest.php rename to tests/Unit/Domain/ValueObjects/FqcnTest.php diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 61cd84c..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Unit/Filters/TargetFilterTest.php b/tests/Unit/Filters/TargetFilterTest.php deleted file mode 100644 index 84d0eff..0000000 --- a/tests/Unit/Filters/TargetFilterTest.php +++ /dev/null @@ -1,84 +0,0 @@ -apply([ - 'A' => [ - 'name' => 'A', - 'dependencies' => ['B'], - ], - 'B' => [ - 'name' => 'B', - 'dependencies' => [], - ], - 'C' => [ - 'name' => 'C', - 'dependencies' => [], - ], - ]); - - expect($result)->toBe([ - 'A' => [ - 'name' => 'A', - 'dependencies' => ['B'], - ], - 'B' => [ - 'name' => 'B', - 'dependencies' => [], - ], - ]); -}); - -it('should throw an exception if the target is not found', function () { - - $filter = new TargetTransformer( - new Depth(), - new Metrics(), - 'D', - ); - - $filter->apply([]); - -})->throws(Exception::class, 'Target D not found on metrics, try verify the target name.'); - -it('should stop if the depth limit is reached', function () { - - $filter = new TargetTransformer( - new Depth(), - new Metrics(), - 'A', - 1, - ); - - $result = $filter->apply([ - 'A' => [ - 'name' => 'A', - 'dependencies' => ['B'], - ], - 'B' => [ - 'name' => 'B', - 'dependencies' => ['C'], - ], - 'C' => [ - 'name' => 'C', - 'dependencies' => [], - ], - ]); - - expect($result)->toBe([ - 'A' => [ - 'name' => 'A', - 'dependencies' => ['B'], - ], - ]); -}); diff --git a/tests/Unit/Infrastructure/Services/AnalyzerServiceAdapterTest.php b/tests/Unit/Infrastructure/Services/AnalyzerServiceAdapterTest.php new file mode 100644 index 0000000..6ddd657 --- /dev/null +++ b/tests/Unit/Infrastructure/Services/AnalyzerServiceAdapterTest.php @@ -0,0 +1,34 @@ +getDependencies(new FileStub('A.php')); + + expect($dependencies)->toBeInstanceOf(ClassDependencies::class); + + expect($dependencies->toArray()['dependencies'])->toBe( + [ + 'App\Infrastructure\Services\Stubs\B', + 'App\Infrastructure\Services\Stubs\C', + 'App\Infrastructure\Services\Stubs\D', + 'App\Infrastructure\Services\Stubs\E', + 'F', + 'G', + ] + ); +}); + +it('exclude native PHP dependencies', function () { + + $analyzerServiceAdapter = app(AnalyzerServiceAdapter::class); + + $dependencies = $analyzerServiceAdapter->getDependencies(new FileStub('Native.php')); + + expect($dependencies->hasNoDependencies())->toBeTrue(); +}); diff --git a/tests/Unit/Infrastructure/Services/FileStub.php b/tests/Unit/Infrastructure/Services/FileStub.php new file mode 100644 index 0000000..125e15b --- /dev/null +++ b/tests/Unit/Infrastructure/Services/FileStub.php @@ -0,0 +1,17 @@ +path; + } +} diff --git a/tests/Unit/Infrastructure/Services/Stubs/A.php b/tests/Unit/Infrastructure/Services/Stubs/A.php new file mode 100644 index 0000000..595da05 --- /dev/null +++ b/tests/Unit/Infrastructure/Services/Stubs/A.php @@ -0,0 +1,22 @@ +calculate([ - 'abstractness' => ['ratio' => $abstractness], - 'coupling' => ['instability' => $instability], - ]); + $analyzeMetric = $this->oneAnalyzeMetric() + ->withInstability($instability) + ->withAbstractnessRatio($abstractness) + ->build(); + + $maintainability = $calculator->calculate($analyzeMetric); expect($maintainability)->toBe($expected); diff --git a/tests/Unit/Presenter/ComponentMapperTest.php b/tests/Unit/Presenter/ComponentMapperTest.php new file mode 100644 index 0000000..27aff36 --- /dev/null +++ b/tests/Unit/Presenter/ComponentMapperTest.php @@ -0,0 +1,96 @@ +componentMapper = new ComponentMapper(new ComponentFactory()); +}); + +it('should map metrics to components', function () { + + $metrics = [ + 'A' => [ + $this->oneAnalyzeMetric()->withName('A\Class1')->build(), + $this->oneAnalyzeMetric()->withName('A\Class2')->build(), + $this->oneAnalyzeMetric()->withName('A\Class3')->build(), + ] + ]; + + $components = $this->componentMapper->from($metrics); + + expect($components)->toHaveCount(1); + expect($components[0])->toBeInstanceOf(Component::class); + expect($components[0]->name())->toBe('A'); +}); + +it('should map metrics to components with dependencies', function () { + + $metrics = [ + 'A' => [ + $this->oneAnalyzeMetric()->withName('A\Class1')->withDependencies(['B\Class2'])->build(), + ], + 'B' => [ + // + ] + ]; + + $components = $this->componentMapper->from($metrics); + + expect($components)->toHaveCount(2); + expect($components[0]->dependencies())->toBe(['B']); +}); + +it('should not keep dependencies from unwanted namespaces', function () { + + $metrics = [ + 'A' => [ + /** + * This dependency is in an unwanted namespace C + */ + $this->oneAnalyzeMetric()->withName('A\Class1')->withDependencies(['C\Class2'])->build(), + ], + 'B' => [ + // + ] + ]; + + $components = $this->componentMapper->from($metrics); + + expect($components)->toHaveCount(2); + expect($components[0]->dependencies())->toBe([]); +}); + +it('should calculate the average abstractness', function () { + + $metrics = [ + 'A' => [ + $this->oneAnalyzeMetric()->build(), + $this->oneAnalyzeMetric()->isAbstract()->build(), + $this->oneAnalyzeMetric()->isAbstract()->build(), + $this->oneAnalyzeMetric()->isAbstract()->build(), + ], + ]; + + $components = $this->componentMapper->from($metrics); + + expect($components[0]->countClasses())->toBe(4); + expect($components[0]->countAbstractions())->toBe(3); + expect($components[0]->abstractness())->toBe(0.75); +}); + +it('should calculate the average instability', function () { + + $metrics = [ + 'A' => [ + $this->oneAnalyzeMetric()->withInstability(0.3)->build(), + $this->oneAnalyzeMetric()->withInstability(0.7)->build(), + $this->oneAnalyzeMetric()->withInstability(1)->build(), + ], + ]; + + $components = $this->componentMapper->from($metrics); + + expect($components[0]->instability())->toBe(0.67); +}); diff --git a/tests/Unit/Presenter/Filters/TargetFilterTest.php b/tests/Unit/Presenter/Filters/TargetFilterTest.php new file mode 100644 index 0000000..296de46 --- /dev/null +++ b/tests/Unit/Presenter/Filters/TargetFilterTest.php @@ -0,0 +1,55 @@ +apply([ + 'A' => $this->oneAnalyzeMetric()->withName('A')->withDependencies(['B'])->build(), + 'B' => $this->oneAnalyzeMetric()->withName('B')->build(), + 'C' => $this->oneAnalyzeMetric()->withName('C')->build(), + ]); + + expect($result)->toHaveLength(2); + expect($result)->toHaveKeys(['A', 'B']); +}); + +it('should throw an exception if the target is not found', function () { + + $filter = new TargetTransformer( + new Depth(), + new Metrics(), + 'D', + ); + + $filter->apply([]); + +})->throws(Exception::class, 'Target D not found on metrics, try verify the target name.'); + +it('should stop if the depth limit is reached', function () { + + $filter = new TargetTransformer( + new Depth(), + new Metrics(), + 'A', + 3, + ); + + $result = $filter->apply([ + 'A' => $this->oneAnalyzeMetric()->withName('A')->withDependencies(['B'])->build(), + 'B' => $this->oneAnalyzeMetric()->withName('B')->withDependencies(['C'])->build(), + 'C' => $this->oneAnalyzeMetric()->withName('C')->withDependencies(['D'])->build(), + 'D' => $this->oneAnalyzeMetric()->withName('D')->build(), + ]); + + expect($result)->toHaveLength(3); + expect($result)->toHaveKeys(['A', 'B', 'C']); +}); diff --git a/tests/Unit/Formatters/ArrayFormatterTest.php b/tests/Unit/Presenter/Formatters/ArrayFormatterTest.php similarity index 100% rename from tests/Unit/Formatters/ArrayFormatterTest.php rename to tests/Unit/Presenter/Formatters/ArrayFormatterTest.php diff --git a/tests/Unit/Formatters/NameFormatterTest.php b/tests/Unit/Presenter/Formatters/NameFormatterTest.php similarity index 100% rename from tests/Unit/Formatters/NameFormatterTest.php rename to tests/Unit/Presenter/Formatters/NameFormatterTest.php diff --git a/tests/Unit/Helpers/CycleHelperTest.php b/tests/Unit/Presenter/Helpers/CycleHelperTest.php similarity index 100% rename from tests/Unit/Helpers/CycleHelperTest.php rename to tests/Unit/Presenter/Helpers/CycleHelperTest.php diff --git a/tests/Unit/Presenters/WeaknessSummaryPresenterTest.php b/tests/Unit/Presenter/WeaknessSummaryPresenterTest.php similarity index 94% rename from tests/Unit/Presenters/WeaknessSummaryPresenterTest.php rename to tests/Unit/Presenter/WeaknessSummaryPresenterTest.php index 20853db..5bce67d 100644 --- a/tests/Unit/Presenters/WeaknessSummaryPresenterTest.php +++ b/tests/Unit/Presenter/WeaknessSummaryPresenterTest.php @@ -1,7 +1,7 @@