Skip to content

Commit 4790241

Browse files
committed
Switch agent output from JSON to TOON format for ~19% token reduction
Adds helgesverre/toon and creates ToonErrorFormatter that outputs in TOON (Token-Oriented Object Notation) format. When an AI agent is detected, PHPStan now outputs in TOON instead of JSON, reducing token consumption while remaining machine-parseable. Follows up on #4938.
1 parent 730ebb3 commit 4790241

7 files changed

Lines changed: 179 additions & 19 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"nette/utils": "^3.2.5",
2525
"nikic/php-parser": "^5.7.0",
2626
"ondram/ci-detector": "^4.0",
27+
"helgesverre/toon": "^1.0",
2728
"shipfastlabs/agent-detector": "^1.0",
2829
"ondrejmirtes/better-reflection": "6.65.0.9",
2930
"ondrejmirtes/composer-attribute-collector": "^1.1.1",

composer.lock

Lines changed: 66 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conf/services.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ services:
197197
arguments:
198198
pretty: true
199199

200+
errorFormatter.toon:
201+
class: PHPStan\Command\ErrorFormatter\ToonErrorFormatter
202+
200203
stubFileTypeMapper:
201204
class: PHPStan\Type\FileTypeMapper
202205
arguments:

src/Command/AnalyseCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
242242
/** @var AgentDetectedErrorFormatter $agentFormatter */
243243
$agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class);
244244
if ($agentFormatter->isAgentDetected()) {
245-
$errorFormat = 'json';
245+
$errorFormat = 'toon';
246246
}
247247
}
248248

src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ final class AgentDetectedErrorFormatter implements ErrorFormatter
1616
{
1717

1818
public function __construct(
19-
#[AutowiredParameter(ref: '@errorFormatter.json')]
20-
private JsonErrorFormatter $jsonErrorFormatter,
19+
#[AutowiredParameter(ref: '@errorFormatter.toon')]
20+
private ToonErrorFormatter $toonErrorFormatter,
2121
)
2222
{
2323
}
@@ -29,7 +29,7 @@ public function isAgentDetected(): bool
2929

3030
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
3131
{
32-
return $this->jsonErrorFormatter->formatErrors($analysisResult, $output);
32+
return $this->toonErrorFormatter->formatErrors($analysisResult, $output);
3333
}
3434

3535
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Command\ErrorFormatter;
4+
5+
use HelgeSverre\Toon\Toon;
6+
use PHPStan\Command\AnalysisResult;
7+
use PHPStan\Command\Output;
8+
use Symfony\Component\Console\Formatter\OutputFormatter;
9+
use function count;
10+
11+
final class ToonErrorFormatter implements ErrorFormatter
12+
{
13+
14+
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
15+
{
16+
$errorsArray = [
17+
'totals' => [
18+
'errors' => count($analysisResult->getNotFileSpecificErrors()),
19+
'file_errors' => count($analysisResult->getFileSpecificErrors()),
20+
],
21+
'files' => [],
22+
'errors' => [],
23+
];
24+
25+
$tipFormatter = new OutputFormatter(false);
26+
27+
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
28+
$file = $fileSpecificError->getFile();
29+
if (!isset($errorsArray['files'][$file])) {
30+
$errorsArray['files'][$file] = [
31+
'errors' => 0,
32+
'messages' => [],
33+
];
34+
}
35+
$errorsArray['files'][$file]['errors']++;
36+
37+
$message = [
38+
'message' => $fileSpecificError->getMessage(),
39+
'line' => $fileSpecificError->getLine(),
40+
'ignorable' => $fileSpecificError->canBeIgnored(),
41+
];
42+
43+
if ($fileSpecificError->getTip() !== null) {
44+
$message['tip'] = $tipFormatter->format($fileSpecificError->getTip());
45+
}
46+
47+
if ($fileSpecificError->getIdentifier() !== null) {
48+
$message['identifier'] = $fileSpecificError->getIdentifier();
49+
}
50+
51+
$errorsArray['files'][$file]['messages'][] = $message;
52+
}
53+
54+
foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) {
55+
$errorsArray['errors'][] = $notFileSpecificError;
56+
}
57+
58+
$toon = Toon::encode($errorsArray);
59+
60+
$output->writeRaw($toon);
61+
62+
return $analysisResult->hasErrors() ? 1 : 0;
63+
}
64+
65+
}

tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Command\ErrorFormatter;
44

5+
use HelgeSverre\Toon\Toon;
56
use Override;
67
use PHPStan\Testing\ErrorFormatterTestCase;
78
use function putenv;
@@ -33,54 +34,79 @@ protected function tearDown(): void
3334

3435
public function testIsAgentDetectedReturnsFalse(): void
3536
{
36-
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
37+
$formatter = new AgentDetectedErrorFormatter(new ToonErrorFormatter());
3738
$this->assertFalse($formatter->isAgentDetected());
3839
}
3940

4041
public function testIsAgentDetectedReturnsTrueWithAiAgent(): void
4142
{
4243
putenv('AI_AGENT=test');
43-
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
44+
$formatter = new AgentDetectedErrorFormatter(new ToonErrorFormatter());
4445
$this->assertTrue($formatter->isAgentDetected());
4546
}
4647

4748
public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void
4849
{
4950
putenv('CLAUDE_CODE=1');
50-
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
51+
$formatter = new AgentDetectedErrorFormatter(new ToonErrorFormatter());
5152
$this->assertTrue($formatter->isAgentDetected());
5253
}
5354

54-
public function testFormatErrorsProducesValidJson(): void
55+
public function testFormatErrorsProducesToonOutput(): void
5556
{
56-
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
57+
$formatter = new AgentDetectedErrorFormatter(new ToonErrorFormatter());
5758

5859
$exitCode = $formatter->formatErrors(
5960
$this->getAnalysisResult(1, 0),
6061
$this->getOutput(),
6162
);
6263

6364
$this->assertSame(1, $exitCode);
64-
$this->assertJsonStringEqualsJsonString(
65-
'{"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":[]}',
66-
$this->getOutputContent(),
67-
);
65+
66+
$expectedData = [
67+
'totals' => [
68+
'errors' => 0,
69+
'file_errors' => 1,
70+
],
71+
'files' => [
72+
'/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php' => [
73+
'errors' => 1,
74+
'messages' => [
75+
[
76+
'message' => 'Foo',
77+
'line' => 4,
78+
'ignorable' => true,
79+
],
80+
],
81+
],
82+
],
83+
'errors' => [],
84+
];
85+
86+
$this->assertSame(Toon::encode($expectedData), $this->getOutputContent());
6887
}
6988

7089
public function testFormatErrorsNoErrors(): void
7190
{
72-
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
91+
$formatter = new AgentDetectedErrorFormatter(new ToonErrorFormatter());
7392

7493
$exitCode = $formatter->formatErrors(
7594
$this->getAnalysisResult(0, 0),
7695
$this->getOutput(),
7796
);
7897

7998
$this->assertSame(0, $exitCode);
80-
$this->assertJsonStringEqualsJsonString(
81-
'{"totals":{"errors":0,"file_errors":0},"files":{},"errors":[]}',
82-
$this->getOutputContent(),
83-
);
99+
100+
$expectedData = [
101+
'totals' => [
102+
'errors' => 0,
103+
'file_errors' => 0,
104+
],
105+
'files' => [],
106+
'errors' => [],
107+
];
108+
109+
$this->assertSame(Toon::encode($expectedData), $this->getOutputContent());
84110
}
85111

86112
}

0 commit comments

Comments
 (0)