Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Baseline implementation of MermaidJS Output Formatter - qossmic/deptrac#1372 #20

Merged
merged 3 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
use Qossmic\Deptrac\Supportive\OutputFormatter\GraphVizOutputImageFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\JsonOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\JUnitOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\MermaidJSOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\TableOutputFormatter;
use Qossmic\Deptrac\Supportive\OutputFormatter\XMLOutputFormatter;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand Down Expand Up @@ -417,6 +418,9 @@
$services
->set(CodeclimateOutputFormatter::class)
->tag('output_formatter');
$services
->set(MermaidJSOutputFormatter::class)
->tag('output_formatter');

/*
* Console
Expand Down
18 changes: 18 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ deptrac:
- Dependency
- InputCollector
- Layer
mermaidjs:
direction: TD
groups:
Contract:
- Contract
Supportive:
- Supportive
- File
Symfony:
- Console
- DependencyInjection
- OutputFormatter
Core:
- Analyser
- Ast
- Dependency
- InputCollector
- Layer

layers:
# Domains
Expand Down
6 changes: 6 additions & 0 deletions docs/formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ Will produce the following graph:
#### Pointing to groups instead of nodes
With `formatters.graphviz.pointToGroups` set to `true`, when you have a node inside a groups with the same name as the group itself, edges pointing to that node will point to the group instead. This might be useful for example if you want to provide a "public API" for a module defined by a group.

## MermaidJS Formatter

The MermaidJS formatter is a console formatter, which generates a mermaid.js compatible graph definition. It can be activated with `--formatter=mermaidjs`.
With the -o option you can specify the output file.

gennadigennadigennadi marked this conversation as resolved.
Show resolved Hide resolved

## JSON Formatter

By default, Json formatter dumps information to *STDOUT*. It can be activated
Expand Down
47 changes: 47 additions & 0 deletions src/Contract/Config/Formatter/MermaidJsConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Contract\Config\Formatter;

use Qossmic\Deptrac\Contract\Config\Layer;

final class MermaidJsConfig implements FormatterConfigInterface
{
private string $name = 'mermaidjs';

private string $direction = 'TD';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the yaml config, you allow to modify this parameter, but I do not see an equivalent way to do this in the php config. Can you make sure the feature set is equivalent between the two?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be now equivalent.


/** @var array<string, Layer[]> */
private array $groups = [];

public function getName(): string
{
return $this->name;
}

public function groups(string $name, Layer ...$layerConfigs): self
{
foreach ($layerConfigs as $layerConfig) {
$this->groups[$name][] = $layerConfig;
}

return $this;
}

public function toArray(): array
{
$output = [];

if ([] !== $this->groups) {
$output['groups'] = array_map(
static fn (array $configs) => array_map(static fn (Layer $layer) => $layer->name, $configs),
$this->groups
);
}

$output['direction'] = $this->direction;

return $output;
}
}
15 changes: 15 additions & 0 deletions src/Supportive/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ private function appendFormatters(ArrayNodeDefinition $node): void
})
->end()
->end()
->arrayNode('mermaidjs')
->info('Configure MermaidJS output formatter')
->addDefaultsIfNotSet()
->children()
->scalarNode('direction')->defaultValue('TD')
->end()
->arrayNode('groups')
->info('Combine multiple layers to a group')
->useAttributeAsKey('name')
->arrayPrototype()
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->arrayNode('codeclimate')
->addDefaultsIfNotSet()
->info('Configure Codeclimate output formatters')
Expand Down
117 changes: 117 additions & 0 deletions src/Supportive/OutputFormatter/MermaidJSOutputFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Supportive\OutputFormatter;

use Qossmic\Deptrac\Contract\OutputFormatter\OutputFormatterInput;
use Qossmic\Deptrac\Contract\OutputFormatter\OutputFormatterInterface;
use Qossmic\Deptrac\Contract\OutputFormatter\OutputInterface;
use Qossmic\Deptrac\Contract\Result\OutputResult;
use Qossmic\Deptrac\Supportive\OutputFormatter\Configuration\FormatterConfiguration;

/**
* @internal
*/
final class MermaidJSOutputFormatter implements OutputFormatterInterface
{
/** @var array{direction: string, groups: array<string, string[]>} */
private array $config;
private const GRAPH_TYPE = 'flowchart %s;';

private const GRAPH_END = ' end;';
private const SUBGRAPH = ' subgraph %sGroup;';
private const LAYER = ' %s;';
private const GRAPH_NODE_FORMAT = ' %s -->|%d| %s;';
private const VIOLATION_STYLE_FORMAT = ' linkStyle %d stroke:red,stroke-width:4px;';

public function __construct(FormatterConfiguration $config)
{
/** @var array{direction: string, groups: array<string, string[]>} $extractedConfig */
$extractedConfig = $config->getConfigFor('mermaidjs');
$this->config = $extractedConfig;
}

public static function getName(): string
{
return 'mermaidjs';
}

public function finish(
OutputResult $result,
OutputInterface $output,
OutputFormatterInput $outputFormatterInput
): void {
$graph = $this->parseResults($result);
$violations = $result->violations();
$buffer = '';

$buffer .= sprintf(self::GRAPH_TYPE.PHP_EOL, $this->config['direction']);

foreach ($this->config['groups'] as $subGraphName => $layers) {
$buffer .= sprintf(self::SUBGRAPH.PHP_EOL, $subGraphName);

foreach ($layers as $layer) {
$buffer .= sprintf(self::LAYER.PHP_EOL, $layer);
}

$buffer .= self::GRAPH_END.PHP_EOL;
}

$linkCount = 0;
$violationsLinks = [];
$violationGraphLinks = [];

foreach ($violations as $violation) {
if (!isset($violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()])) {
$violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()] = 1;
} else {
++$violationsLinks[$violation->getDependerLayer()][$violation->getDependentLayer()];
}
}

foreach ($violationsLinks as $dependerLayer => $layers) {
foreach ($layers as $dependentLayer => $count) {
$buffer .= sprintf(self::GRAPH_NODE_FORMAT.PHP_EOL, $dependerLayer, $count, $dependentLayer);
$violationGraphLinks[] = $linkCount;
++$linkCount;
}
}

foreach ($graph as $dependerLayer => $layers) {
foreach ($layers as $dependentLayer => $count) {
if (!isset($violationsLinks[$dependerLayer][$dependentLayer])) {
$buffer .= sprintf(self::GRAPH_NODE_FORMAT.PHP_EOL, $dependerLayer, $count, $dependentLayer);
}
}
}

foreach ($violationGraphLinks as $linkNumber) {
$buffer .= sprintf(self::VIOLATION_STYLE_FORMAT.PHP_EOL, $linkNumber);
}

if (null !== $outputFormatterInput->outputPath) {
file_put_contents($outputFormatterInput->outputPath, $buffer);
} else {
$output->writeRaw($buffer);
}
}

/**
* @return array<string, array<string, int<1, max>>>
*/
protected function parseResults(OutputResult $result): array
{
$graph = [];

foreach ($result->allowed() as $rule) {
if (!isset($graph[$rule->getDependerLayer()][$rule->getDependentLayer()])) {
$graph[$rule->getDependerLayer()][$rule->getDependentLayer()] = 1;
} else {
++$graph[$rule->getDependerLayer()][$rule->getDependentLayer()];
}
}

return $graph;
}
}
4 changes: 4 additions & 0 deletions tests/Supportive/DependencyInjection/DeptracExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class DeptracExtensionTest extends TestCase
'groups' => [],
'point_to_groups' => false,
],
'mermaidjs' => [
'direction' => 'TD',
'groups' => [],
],
'codeclimate' => [
'severity' => [
'failure' => 'major',
Expand Down
72 changes: 72 additions & 0 deletions tests/Supportive/OutputFormatter/MermaidJSOutputFormatterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Tests\Qossmic\Deptrac\Supportive\OutputFormatter;

use PHPUnit\Framework\TestCase;
use Qossmic\Deptrac\Contract\Analyser\AnalysisResult;
use Qossmic\Deptrac\Contract\Ast\DependencyType;
use Qossmic\Deptrac\Contract\Ast\FileOccurrence;
use Qossmic\Deptrac\Contract\OutputFormatter\OutputFormatterInput;
use Qossmic\Deptrac\Contract\Result\Allowed;
use Qossmic\Deptrac\Contract\Result\OutputResult;
use Qossmic\Deptrac\Contract\Result\Violation;
use Qossmic\Deptrac\Core\Ast\AstMap\ClassLike\ClassLikeToken;
use Qossmic\Deptrac\Core\Dependency\Dependency;
use Qossmic\Deptrac\Supportive\Console\Symfony\Style;
use Qossmic\Deptrac\Supportive\Console\Symfony\SymfonyOutput;
use Qossmic\Deptrac\Supportive\OutputFormatter\Configuration\FormatterConfiguration;
use Qossmic\Deptrac\Supportive\OutputFormatter\MermaidJSOutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Style\SymfonyStyle;
use Tests\Qossmic\Deptrac\Supportive\OutputFormatter\data\DummyViolationCreatingRule;

class MermaidJSOutputFormatterTest extends TestCase
{
public function testFinish(): void
{
$dependency = new Dependency(
ClassLikeToken::fromFQCN('ClassA'),
ClassLikeToken::fromFQCN('ClassC'), new FileOccurrence('classA.php', 0), DependencyType::PARAMETER
);

$analysisResult = new AnalysisResult();
$analysisResult->addRule(new Allowed($dependency, 'LayerA', 'LayerB'));
$analysisResult->addRule(new Allowed($dependency, 'LayerC', 'LayerD'));
$analysisResult->addRule(new Allowed($dependency, 'LayerA', 'LayerC'));

$analysisResult->addRule(new Violation($dependency, 'LayerA', 'LayerC', new DummyViolationCreatingRule()));
$analysisResult->addRule(new Violation($dependency, 'LayerB', 'LayerC', new DummyViolationCreatingRule()));

$bufferedOutput = new BufferedOutput();

$output = $this->createSymfonyOutput($bufferedOutput);
$outputFormatterInput = new OutputFormatterInput(null, true, true, false);

$mermaidJSOutputFormatter = new MermaidJSOutputFormatter(new FormatterConfiguration([
'mermaidjs' => [
'direction' => 'TD',
'groups' => [
'User' => [
'LayerA',
'LayerB',
],
'Admin' => [
'LayerC',
'LayerD',
],
],
],
]));
$mermaidJSOutputFormatter->finish(OutputResult::fromAnalysisResult($analysisResult), $output, $outputFormatterInput);
$this->assertSame(file_get_contents(__DIR__.'/data/mermaidjs-expected.txt'), $bufferedOutput->fetch());
}

private function createSymfonyOutput(BufferedOutput $bufferedOutput): SymfonyOutput
{
return new SymfonyOutput(
$bufferedOutput,
new Style(new SymfonyStyle($this->createMock(InputInterface::class), $bufferedOutput))
);
}
}
15 changes: 15 additions & 0 deletions tests/Supportive/OutputFormatter/data/mermaidjs-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
flowchart TD;
subgraph UserGroup;
LayerA;
LayerB;
end;
subgraph AdminGroup;
LayerC;
LayerD;
end;
LayerA -->|1| LayerC;
LayerB -->|1| LayerC;
LayerA -->|1| LayerB;
LayerC -->|1| LayerD;
linkStyle 0 stroke:red,stroke-width:4px;
linkStyle 1 stroke:red,stroke-width:4px;
Loading