Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
modprobe committed Jan 29, 2021
0 parents commit 44c1788
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor
/composer.lock
30 changes: 30 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "alxt/phpstan-bitbucket",
"description": "PHPStan error formatter for Bitbucket Pipeline Reports",
"type": "phpstan-extension",
"require": {
"php": "~7.1",
"ext-json": "*",
"phpstan/phpstan": "^0.12",
"guzzlehttp/guzzle": "^6.5",
"ramsey/uuid": "^3.9"
},
"authors": [
{
"name": "Alexander Timmermann",
"email": "[email protected]"
}
],
"autoload": {
"psr-4": {
"Alxt\\PHPStan\\ErrorFormatter\\": "src"
}
},
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
}
}
3 changes: 3 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
services:
errorFormatter.bitbucket:
class: Alxt\PHPStan\ErrorFormatter\BitbucketErrorFormatter
117 changes: 117 additions & 0 deletions src/BitbucketApiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Alxt\PHPStan\ErrorFormatter;

use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

class BitbucketApiClient
{
private const BASE_URL = 'http://api.bitbucket.org/2.0/';
private const PROXY_URL = 'http://localhost:29418';

private const REPORT_TITLE = 'PHPStan Report';

private $httpClient;

public function __construct(string $baseUrl = self::BASE_URL, string $proxyUrl = self::PROXY_URL)
{
$this->httpClient = new Client([
'base_uri' => $baseUrl,
RequestOptions::PROXY => $proxyUrl,
]);
}

public function createReport(int $numberOfIssues = 0): UuidInterface
{
$payload = $numberOfIssues > 0
? [
'title' => self::REPORT_TITLE,
'details' => sprintf('This PR introduces %d new issue(s).', $numberOfIssues),
'report_type' => 'BUG',
'result' => 'FAILED',
]
: [
'title' => self::REPORT_TITLE,
'details' => 'This PR introduces no new issues.',
'report_type' => 'BUG',
'result' => 'PASSED',
];

$result = $this->httpClient->put($this->buildReportUrl(), [
RequestOptions::JSON => $payload,
]);

$resultBody = json_decode((string) $result->getBody(), true);

return Uuid::fromString($resultBody['uuid']);
}

public function addAnnotation(
UuidInterface $reportUuid,
string $summary,
?string $filePath,
?int $line
): UuidInterface {
$payload = [
'annotation_type' => 'BUG',
'summary' => $summary,
];

if ($filePath !== null) {
$payload['path'] = $filePath;
}

if ($line !== null) {
$payload['line'] = $line;
}

$response = $this->httpClient->put($this->buildAnnotationUrl($reportUuid), [
RequestOptions::JSON => $payload,
]);

$responseBody = json_decode((string) $response->getBody(), true);

return Uuid::fromString($responseBody['uuid']);
}

private function buildReportUrl(?UuidInterface $uuid = null): string
{
$namespace = getenv('BITBUCKET_REPO_OWNER');
$slug = getenv('BITBUCKET_REPO_SLUG');
$commitHash = getenv('BITBUCKET_COMMIT');

return sprintf(
'repositories/%s/%s/commit/%s/reports/%s',
$namespace,
$slug,
$commitHash,
$uuid !== null ? '{' . $uuid->toString() . '}' : $this->buildReportName()
);
}

private function buildAnnotationUrl(UuidInterface $reportUuid): string
{
return sprintf(
'%s/annotations/%s',
$this->buildReportUrl($reportUuid),
$this->buildAnnotationName()
);
}

private function buildReportName(): string
{
$repoSlug = getenv('BITBUCKET_REPO_SLUG');

return $repoSlug . '-' . Uuid::uuid4()->toString();
}

private function buildAnnotationName(): string
{
$repoSlug = getenv('BITBUCKET_REPO_SLUG');

return $repoSlug . '-annotation-' . Uuid::uuid4()->toString();
}
}
47 changes: 47 additions & 0 deletions src/BitbucketErrorFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Alxt\PHPStan\ErrorFormatter;

use PHPStan\Command\AnalysisResult;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use PHPStan\Command\Output;
use PHPStan\File\ParentDirectoryRelativePathHelper;

class BitbucketErrorFormatter implements ErrorFormatter
{
private $relativePathHelper;
private $apiClient;

public function __construct()
{
RequirementsValidator::validate();

$this->relativePathHelper = new ParentDirectoryRelativePathHelper(getenv('BITBUCKET_CLONE_DIR'));
$this->apiClient = new BitbucketApiClient();
}

public function formatErrors(AnalysisResult $analysisResult, Output $output): int
{
$reportUuid = $this->apiClient->createReport($analysisResult->getTotalErrorsCount());

foreach ($analysisResult->getFileSpecificErrors() as $error) {
$this->apiClient->addAnnotation(
$reportUuid,
$error->getMessage(),
$this->relativePathHelper->getRelativePath($error->getFile()),
$error->getLine()
);
}

foreach ($analysisResult->getNotFileSpecificErrors() as $error) {
$this->apiClient->addAnnotation(
$reportUuid,
$error,
null,
null
);
}

return (int) $analysisResult->hasErrors();
}
}
26 changes: 26 additions & 0 deletions src/RequirementsValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Alxt\PHPStan\ErrorFormatter;

class RequirementsValidator
{
public static function validate(): void
{
self::assertEnvVariable('BITBUCKET_REPO_OWNER');
self::assertEnvVariable('BITBUCKET_REPO_SLUG');
self::assertEnvVariable('BITBUCKET_COMMIT');
self::assertEnvVariable('BITBUCKET_CLONE_DIR');
}

private static function raiseError(): void
{
throw new \RuntimeException('This extension can only be used within a Bitbucket Pipeline.');
}

private static function assertEnvVariable(string $variable): void
{
if (getenv($variable) === false) {
self::raiseError();
}
}
}

0 comments on commit 44c1788

Please sign in to comment.