Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion src/Command/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use OndraM\CiDetector\CiDetector;
use Override;
use PHPStan\Analyser\InternalError;
use PHPStan\Command\ErrorFormatter\AgentDetectedErrorFormatter;
use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter;
use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
Expand Down Expand Up @@ -235,11 +236,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat');
}

$container = $inceptionResult->getContainer();

if ($errorFormat === null) {
/** @var AgentDetectedErrorFormatter $agentFormatter */
$agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class);
if ($agentFormatter->isAgentDetected()) {
$errorFormat = 'json';
}
}

if ($errorFormat === null) {
$errorFormat = 'table';
}

$container = $inceptionResult->getContainer();
$errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat);
if (!$container->hasService($errorFormatterServiceName)) {
$errorOutput->writeLineFormatted(sprintf(
Expand Down
53 changes: 53 additions & 0 deletions src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command\ErrorFormatter;

use PHPStan\Command\AnalysisResult;
use PHPStan\Command\Output;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use function file_exists;
use function getenv;
use function is_string;
use function trim;

/**
* @api
*/
#[AutowiredService(as: AgentDetectedErrorFormatter::class)]
final class AgentDetectedErrorFormatter implements ErrorFormatter
{

public function __construct(
#[AutowiredParameter(ref: '@errorFormatter.json')]
private JsonErrorFormatter $jsonErrorFormatter,
)
{
}

public function isAgentDetected(): bool
{
$aiAgent = getenv('AI_AGENT');
if (is_string($aiAgent) && trim($aiAgent) !== '') {
return true;
}

return getenv('CURSOR_TRACE_ID') !== false
|| getenv('CURSOR_AGENT') !== false
|| getenv('GEMINI_CLI') !== false
|| getenv('CODEX_SANDBOX') !== false
|| getenv('AUGMENT_AGENT') !== false
|| getenv('OPENCODE_CLIENT') !== false
|| getenv('OPENCODE') !== false
|| getenv('CLAUDECODE') !== false
|| getenv('CLAUDE_CODE') !== false
|| getenv('REPL_ID') !== false
|| file_exists('/opt/.devin');
}

public function formatErrors(AnalysisResult $analysisResult, Output $output): int
{
return $this->jsonErrorFormatter->formatErrors($analysisResult, $output);
}

}
32 changes: 30 additions & 2 deletions src/Command/ErrorsConsoleStyle.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
use function explode;
use function implode;
use function sprintf;
use function file_exists;
use function getenv;
use function is_string;
use function strlen;
use function trim;
use const DIRECTORY_SEPARATOR;

final class ErrorsConsoleStyle extends SymfonyStyle
Expand All @@ -29,6 +33,8 @@ final class ErrorsConsoleStyle extends SymfonyStyle

private ?bool $isCiDetected = null;

private ?bool $isAgentDetected = null;

public function __construct(InputInterface $input, OutputInterface $output)
{
parent::__construct($input, $output);
Expand All @@ -45,6 +51,27 @@ private function isCiDetected(): bool
return $this->isCiDetected;
}

private function isAgentDetected(): bool
{
if ($this->isAgentDetected === null) {
$aiAgent = getenv('AI_AGENT');
$this->isAgentDetected = (is_string($aiAgent) && trim($aiAgent) !== '')
|| getenv('CURSOR_TRACE_ID') !== false
|| getenv('CURSOR_AGENT') !== false
|| getenv('GEMINI_CLI') !== false
|| getenv('CODEX_SANDBOX') !== false
|| getenv('AUGMENT_AGENT') !== false
|| getenv('OPENCODE_CLIENT') !== false
|| getenv('OPENCODE') !== false
|| getenv('CLAUDECODE') !== false
|| getenv('CLAUDE_CODE') !== false
|| getenv('REPL_ID') !== false
|| file_exists('/opt/.devin');
}

return $this->isAgentDetected;
}

/**
* @param string[] $headers
* @param string[][] $rows
Expand Down Expand Up @@ -95,9 +122,10 @@ public function createProgressBar(int $max = 0): ProgressBar
}

$ci = $this->isCiDetected();
$this->progressBar->setOverwrite(!$ci);
$agent = $this->isAgentDetected();
$this->progressBar->setOverwrite(!$ci && !$agent);

if ($ci) {
if ($ci || $agent) {
$this->progressBar->minSecondsBetweenRedraws(15);
$this->progressBar->maxSecondsBetweenRedraws(30);
} elseif (DIRECTORY_SEPARATOR === '\\') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command\ErrorFormatter;

use Override;
use PHPStan\Testing\ErrorFormatterTestCase;
use function putenv;

class AgentDetectedErrorFormatterTest extends ErrorFormatterTestCase
{

#[Override]
protected function setUp(): void
{
putenv('AI_AGENT');
putenv('CURSOR_TRACE_ID');
putenv('CURSOR_AGENT');
putenv('GEMINI_CLI');
putenv('CODEX_SANDBOX');
putenv('AUGMENT_AGENT');
putenv('OPENCODE_CLIENT');
putenv('OPENCODE');
putenv('CLAUDECODE');
putenv('CLAUDE_CODE');
putenv('REPL_ID');
}

#[Override]
protected function tearDown(): void
{
putenv('AI_AGENT');
putenv('CLAUDE_CODE');
}

public function testIsAgentDetectedReturnsFalse(): void
{
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
$this->assertFalse($formatter->isAgentDetected());
}

public function testIsAgentDetectedReturnsTrueWithAiAgent(): void
{
putenv('AI_AGENT=test');
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
$this->assertTrue($formatter->isAgentDetected());
}

public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void
{
putenv('CLAUDE_CODE=1');
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
$this->assertTrue($formatter->isAgentDetected());
}

public function testFormatErrorsProducesValidJson(): void
{
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));

$exitCode = $formatter->formatErrors(
$this->getAnalysisResult(1, 0),
$this->getOutput(),
);

$this->assertSame(1, $exitCode);
$this->assertJsonStringEqualsJsonString(
'{"totals":{"errors":0,"file_errors":1},"files":{"/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with \\"spaces\\" and unicode 😃.php":{"errors":1,"messages":[{"message":"Foo","line":4,"ignorable":true}]}},"errors":[]}',
$this->getOutputContent(),
);
}

public function testFormatErrorsNoErrors(): void
{
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));

$exitCode = $formatter->formatErrors(
$this->getAnalysisResult(0, 0),
$this->getOutput(),
);

$this->assertSame(0, $exitCode);
$this->assertJsonStringEqualsJsonString(
'{"totals":{"errors":0,"file_errors":0},"files":{},"errors":[]}',
$this->getOutputContent(),
);
}

}
Loading