Skip to content

Commit 4a228a2

Browse files
committed
[console] Ignore warning preambles before structured JSON
1 parent ba1afb2 commit 4a228a2

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

src/Console/Logger/Processor/CommandOutputProcessor.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,55 @@ private function decodeStructuredOutput(string $content): mixed
108108
return $decodedDocuments;
109109
}
110110

111+
$decodedStructuredOutput = $this->decodeStructuredOutputAfterTextPreamble($content);
112+
113+
if (null !== $decodedStructuredOutput) {
114+
return $decodedStructuredOutput;
115+
}
116+
111117
return $content;
112118
}
113119

120+
/**
121+
* Decodes structured output that is preceded by plain-text warnings or banners.
122+
*
123+
* Some tooling emits advisory text before a valid JSON payload even when a
124+
* machine-readable format is requested. When the suffix starting at the
125+
* first valid JSON token is fully decodable, the textual preamble SHALL be
126+
* ignored so parent command envelopes remain parseable.
127+
*
128+
* @param string $content the buffered output contents
129+
*
130+
* @return mixed the decoded JSON payload when a valid structured suffix exists
131+
*/
132+
private function decodeStructuredOutputAfterTextPreamble(string $content): mixed
133+
{
134+
$offset = 0;
135+
136+
while (null !== ($offset = $this->findNextJsonDocumentOffset($content, $offset))) {
137+
$structuredSuffix = trim(substr($content, $offset));
138+
139+
if ('' === $structuredSuffix) {
140+
return null;
141+
}
142+
143+
try {
144+
return $this->normalizeStructuredPayload(json_decode($structuredSuffix, true));
145+
} catch (JsonException) {
146+
}
147+
148+
$decodedDocuments = $this->decodeJsonDocumentStream($structuredSuffix);
149+
150+
if (null !== $decodedDocuments) {
151+
return $decodedDocuments;
152+
}
153+
154+
++$offset;
155+
}
156+
157+
return null;
158+
}
159+
114160
/**
115161
* Decodes a stream that contains multiple JSON documents separated by whitespace.
116162
*
@@ -220,6 +266,27 @@ private function consumeJsonDocument(string $content, int &$offset): ?string
220266
return null;
221267
}
222268

269+
/**
270+
* Finds the offset of the next possible JSON document opening token.
271+
*
272+
* @param string $content the buffered output contents
273+
* @param int $offset the offset from which scanning SHALL start
274+
*
275+
* @return ?int the offset of the next "{" or "[" token
276+
*/
277+
private function findNextJsonDocumentOffset(string $content, int $offset): ?int
278+
{
279+
$length = \strlen($content);
280+
281+
for (; $offset < $length; ++$offset) {
282+
if ('{' === $content[$offset] || '[' === $content[$offset]) {
283+
return $offset;
284+
}
285+
}
286+
287+
return null;
288+
}
289+
223290
/**
224291
* Normalizes decoded structured payloads produced by wrapped tooling.
225292
*

tests/Console/Logger/Processor/CommandOutputProcessorTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,38 @@ public function processWillDecodeMultipleJsonBufferedOutputsIntoAList(): void
158158
], $context['output']);
159159
}
160160

161+
/**
162+
* @return void
163+
*/
164+
#[Test]
165+
public function processWillDiscardPlainTextPreambleBeforeStructuredJsonOutput(): void
166+
{
167+
$processor = new CommandOutputProcessor();
168+
$output = new BufferedOutput();
169+
$output->write(
170+
"Warning: advisory text before JSON.\n"
171+
. "{\"about\":\"PHP CS Fixer\"}\n"
172+
. "{\"totals\":{\"changed_files\":0,\"errors\":0},\"changed_files\":[\"src/Foo.php\"]}\n"
173+
);
174+
175+
$context = $processor->process([
176+
'output' => $output,
177+
]);
178+
179+
self::assertSame([
180+
[
181+
'about' => 'PHP CS Fixer',
182+
],
183+
[
184+
'totals' => [
185+
'changed_files' => 0,
186+
'errors' => 0,
187+
],
188+
'changed_files' => [],
189+
],
190+
], $context['output']);
191+
}
192+
161193
/**
162194
* @return void
163195
*/

0 commit comments

Comments
 (0)