Skip to content

Commit

Permalink
Merge pull request #808 from marekstodolny/codeclimate-formatter
Browse files Browse the repository at this point in the history
Added a Codeclimate output formatter
  • Loading branch information
Denis Brumann authored Apr 9, 2022
2 parents 5c40ce3 + 44e3003 commit 365d270
Show file tree
Hide file tree
Showing 13 changed files with 1,020 additions and 0 deletions.
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use Qossmic\Deptrac\FileResolver;
use Qossmic\Deptrac\LayerAnalyser;
use Qossmic\Deptrac\OutputFormatter\BaselineOutputFormatter;
use Qossmic\Deptrac\OutputFormatter\CodeclimateOutputFormatter;
use Qossmic\Deptrac\OutputFormatter\ConsoleOutputFormatter;
use Qossmic\Deptrac\OutputFormatter\GithubActionsOutputFormatter;
use Qossmic\Deptrac\OutputFormatter\GraphVizOutputDisplayFormatter;
Expand Down Expand Up @@ -205,6 +206,9 @@
$services
->set(JsonOutputFormatter::class)
->tag('output_formatter');
$services
->set(CodeclimateOutputFormatter::class)
->tag('output_formatter');

/* Collectors */
$services
Expand Down
18 changes: 18 additions & 0 deletions docs/depfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ parameters:
- Controller
```

#### `codeclimate`

You can configure the codeclimate output by changing how severity is chosen.

#### `severity`

You can change how a severity of `failure`, `skipped`, `uncovered` violations will be treated.

```yaml
parameters:
formatters:
codeclimate:
severity:
failure: major
skipped: minor
uncovered: info
```

### `ignore_uncovered_internal_classes`

By default, PHP internal classes will not be reported as uncovered, if they are
Expand Down
48 changes: 48 additions & 0 deletions docs/formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,51 @@ Supported options:

The default formatter is the table formatter, which groups results by layers to its own table. It can be also
activated with `--formatter=table`.

## Codeclimate Formatter

By default, Codeclimate formatter dumps information to *STDOUT*. It can be activated
with `--formatter=codeclimate`

This formatter is compatible with GitLab CI.

Supported options:

```
--output= path to a dumped file
```

#### Change severity of a violation

Under `formatters.codeclimate.severity` you can define which severity string you want to assign to a given violation type. By default, deptrac uses `major` for failures, `minor` for skipped violations and `info` for uncovered dependencies.

```yaml
parameters:
formatters:
codeclimate:
severity:
failure: blocker
skipped: minor
uncovered: info
```

#### Example issue raport

```json
[
{
"type": "issue",
"check_name": "Dependency violation",
"fingerprint": "3c6b66029bacb18446b7889430ec5aad7fae01cb",
"description": "ClassA must not depend on ClassB (LayerA on LayerB)",
"categories": ["Style", "Complexity"],
"severity": "major",
"location": {
"path": "ClassA.php",
"lines": {
"begin": 12
}
}
}
]
```
32 changes: 32 additions & 0 deletions src/Configuration/ConfigurationCodeclimate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\Configuration;

final class ConfigurationCodeclimate
{
/** @var array{failure?: string, skipped?: string, uncovered?: string} */
private array $severityMap;

/**
* @param array{severity?: array{failure?: string, skipped?: string, uncovered?: string}} $array
*/
public static function fromArray(array $array): self
{
return new self($array['severity'] ?? []);
}

/**
* @param array{failure?: string, skipped?: string, uncovered?: string} $severityMap
*/
private function __construct(array $severityMap)
{
$this->severityMap = $severityMap;
}

public function getSeverity(string $key): ?string
{
return $this->severityMap[$key] ?? null;
}
}
215 changes: 215 additions & 0 deletions src/OutputFormatter/CodeclimateOutputFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?php

declare(strict_types=1);

namespace Qossmic\Deptrac\OutputFormatter;

use Exception;
use Qossmic\Deptrac\Configuration\ConfigurationCodeclimate;
use Qossmic\Deptrac\Console\Output;
use Qossmic\Deptrac\RulesetEngine\Context;
use Qossmic\Deptrac\RulesetEngine\Rule;
use Qossmic\Deptrac\RulesetEngine\SkippedViolation;
use Qossmic\Deptrac\RulesetEngine\Uncovered;
use Qossmic\Deptrac\RulesetEngine\Violation;
use function json_encode;
use function json_last_error;
use function sprintf;
use const JSON_PRETTY_PRINT;

final class CodeclimateOutputFormatter implements OutputFormatterInterface
{
public static function getName(): string
{
return 'codeclimate';
}

public static function getConfigName(): string
{
return self::getName();
}

/**
* {@inheritdoc}
*
* @throws Exception
*/
public function finish(
Context $context,
Output $output,
OutputFormatterInput $outputFormatterInput
): void {
/** @var array{severity?: array{failure?: string, skipped?: string, uncovered?: string}} $config */
$config = $outputFormatterInput->getConfig();
$formatterConfig = ConfigurationCodeclimate::fromArray($config);

$violations = [];
foreach ($context->rules() as $rule) {
if (!$rule instanceof Violation && !$rule instanceof SkippedViolation && !$rule instanceof Uncovered) {
continue;
}

if (!($outputFormatterInput->getReportSkipped()) && $rule instanceof SkippedViolation) {
continue;
}

if (!($outputFormatterInput->getReportUncovered()) && $rule instanceof Uncovered) {
continue;
}

switch (true) {
case $rule instanceof Violation:
$this->addFailure($violations, $rule, $formatterConfig);
break;
case $rule instanceof SkippedViolation:
$this->addSkipped($violations, $rule, $formatterConfig);
break;
case $rule instanceof Uncovered:
$this->addUncovered($violations, $rule, $formatterConfig);
break;
}
}

$json = json_encode($violations, JSON_PRETTY_PRINT);

if (false === $json) {
throw new Exception(sprintf('Unable to render codeclimate output. %s', $this->jsonLastError()));
}

$dumpJsonPath = $outputFormatterInput->getOutputPath();
if (null !== $dumpJsonPath) {
file_put_contents($dumpJsonPath, $json);
$output->writeLineFormatted('<info>Codeclimate Report dumped to '.realpath($dumpJsonPath).'</info>');

return;
}

$output->writeRaw($json);
}

/**
* @param array<array{type: string, check_name: string, fingerprint: string, description: string, categories: array<string>, severity: string, location: array{path: string, lines: array{begin: int}}}> $violationsArray
*/
private function addFailure(array &$violationsArray, Violation $violation, ConfigurationCodeclimate $config): void
{
$violationsArray[] = $this->buildRuleArray(
$violation,
$this->getFailureMessage($violation),
$config->getSeverity('failure') ?? 'major'
);
}

private function getFailureMessage(Violation $violation): string
{
$dependency = $violation->getDependency();

return sprintf(
'%s must not depend on %s (%s on %s)',
$dependency->getDependant()->toString(),
$dependency->getDependee()->toString(),
$violation->getDependantLayerName(),
$violation->getDependeeLayerName()
);
}

/**
* @param array<array{type: string, check_name: string, fingerprint: string, description: string, categories: array<string>, severity: string, location: array{path: string, lines: array{begin: int}}}> $violationsArray
*/
private function addSkipped(array &$violationsArray, SkippedViolation $violation, ConfigurationCodeclimate $config): void
{
$violationsArray[] = $this->buildRuleArray(
$violation,
$this->getWarningMessage($violation),
$config->getSeverity('skipped') ?? 'minor'
);
}

private function getWarningMessage(SkippedViolation $violation): string
{
$dependency = $violation->getDependency();

return sprintf(
'%s should not depend on %s (%s on %s)',
$dependency->getDependant()->toString(),
$dependency->getDependee()->toString(),
$violation->getDependantLayerName(),
$violation->getDependeeLayerName()
);
}

/**
* @param array<array{type: string, check_name: string, fingerprint: string, description: string, categories: array<string>, severity: string, location: array{path: string, lines: array{begin: int}}}> $violationsArray
*/
private function addUncovered(array &$violationsArray, Uncovered $violation, ConfigurationCodeclimate $config): void
{
$violationsArray[] = $this->buildRuleArray(
$violation,
$this->getUncoveredMessage($violation),
$config->getSeverity('uncovered') ?? 'info'
);
}

private function getUncoveredMessage(Uncovered $violation): string
{
$dependency = $violation->getDependency();

return sprintf(
'%s has uncovered dependency on %s (%s)',
$dependency->getDependant()->toString(),
$dependency->getDependee()->toString(),
$violation->getLayer()
);
}

private function jsonLastError(): string
{
switch (json_last_error()) {
case JSON_ERROR_NONE:
return 'No errors';
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case JSON_ERROR_STATE_MISMATCH:
return 'Underflow or the modes mismatch';
case JSON_ERROR_CTRL_CHAR:
return 'Unexpected control character found';
case JSON_ERROR_SYNTAX:
return 'Syntax error, malformed JSON';
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
default:
return 'Unknown error';
}
}

/**
* @return array{type: string, check_name: string, fingerprint: string, description: string, categories: array<string>, severity: string, location: array{path: string, lines: array{begin: int}}}
*/
private function buildRuleArray(Rule $rule, string $message, string $severity): array
{
return [
'type' => 'issue',
'check_name' => 'Dependency violation',
'fingerprint' => $this->buildFingerprint($rule),
'description' => $message,
'categories' => ['Style', 'Complexity'],
'severity' => $severity,
'location' => [
'path' => $rule->getDependency()->getFileOccurrence()->getFilepath(),
'lines' => [
'begin' => $rule->getDependency()->getFileOccurrence()->getLine(),
],
],
];
}

private function buildFingerprint(Rule $rule): string
{
return sha1(implode(',', [
get_class($rule),
$rule->getDependency()->getDependant()->toString(),
$rule->getDependency()->getDependee()->toString(),
$rule->getDependency()->getFileOccurrence()->getFilepath(),
$rule->getDependency()->getFileOccurrence()->getLine(),
]));
}
}
30 changes: 30 additions & 0 deletions tests/Configuration/ConfigurationCodeclimateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Tests\Qossmic\Deptrac\Configuration;

use PHPUnit\Framework\TestCase;
use Qossmic\Deptrac\Configuration\ConfigurationCodeclimate;

/**
* @covers \Qossmic\Deptrac\Configuration\ConfigurationCodeclimate
*/
final class ConfigurationCodeclimateTest extends TestCase
{
public function testFromArray(): void
{
$arr = [
'severity' => [
'failure' => 'blocker',
'skipped' => 'critical',
'uncovered' => 'info',
],
];
$config = ConfigurationCodeclimate::fromArray($arr);

self::assertEquals('blocker', $config->getSeverity('failure'));
self::assertEquals('critical', $config->getSeverity('skipped'));
self::assertEquals('info', $config->getSeverity('uncovered'));
}
}
Loading

0 comments on commit 365d270

Please sign in to comment.