22<?php
33
44/**
5- * PHPStan Results Parser
5+ * PHPStan Results Parser.
66 *
77 * This script parses PHPStan JSON output and generates a formatted, actionable report.
88 * It groups errors by class, strips noise, and generates a markdown checklist suitable
99 * for GitHub PR comments or Copilot context.
1010 *
1111 * Usage: php parse-phpstan-results.php phpstan.json
1212 */
13-
1413if ($ argc < 2 ) {
1514 echo "Usage: php parse-phpstan-results.php <phpstan.json> \n" ;
1615 exit (1 );
1716}
1817
1918$ jsonFile = $ argv [1 ];
2019
21- if (! file_exists ($ jsonFile )) {
22- echo "Error: File ' $ jsonFile' not found. \n" ;
20+ if ( ! file_exists ($ jsonFile )) {
21+ echo "Error: File ' { $ jsonFile} ' not found. \n" ;
2322 exit (1 );
2423}
2524
2625$ content = file_get_contents ($ jsonFile );
27- $ data = json_decode ($ content , true );
26+ $ data = json_decode ($ content , true );
2827
2928if (json_last_error () !== JSON_ERROR_NONE ) {
30- echo "Error: Invalid JSON in ' $ jsonFile': " . json_last_error_msg () . "\n" ;
29+ echo "Error: Invalid JSON in ' { $ jsonFile} ': " . json_last_error_msg () . "\n" ;
3130 exit (1 );
3231}
3332
3433// Extract errors from PHPStan JSON format
35- $ files = $ data ['files ' ] ?? [];
34+ $ files = $ data ['files ' ] ?? [];
3635$ totalErrors = $ data ['totals ' ]['file_errors ' ] ?? 0 ;
3736
3837if ($ totalErrors === 0 ) {
4241}
4342
4443// Group errors by class/file
45- $ errorsByFile = [];
44+ $ errorsByFile = [];
4645$ errorsByCategory = [
47- 'type_errors ' => [],
48- 'method_errors ' => [],
49- 'property_errors ' => [],
46+ 'type_errors ' => [],
47+ 'method_errors ' => [],
48+ 'property_errors ' => [],
5049 'return_type_errors ' => [],
51- 'other_errors ' => [],
50+ 'other_errors ' => [],
5251];
5352
5453foreach ($ files as $ filePath => $ fileData ) {
5554 $ messages = $ fileData ['messages ' ] ?? [];
56-
55+
5756 foreach ($ messages as $ message ) {
5857 $ errorText = $ message ['message ' ] ?? '' ;
59- $ line = $ message ['line ' ] ?? 0 ;
60-
58+ $ line = $ message ['line ' ] ?? 0 ;
59+
6160 // Categorize errors
6261 $ category = categorizeError ($ errorText );
63-
62+
6463 $ errorsByFile [$ filePath ][] = [
65- 'line ' => $ line ,
66- 'message ' => $ errorText ,
64+ 'line ' => $ line ,
65+ 'message ' => $ errorText ,
6766 'category ' => $ category ,
6867 ];
69-
68+
7069 $ errorsByCategory [$ category ][] = [
71- 'file ' => $ filePath ,
72- 'line ' => $ line ,
70+ 'file ' => $ filePath ,
71+ 'line ' => $ line ,
7372 'message ' => $ errorText ,
7473 ];
7574 }
7675}
7776
7877// Generate markdown report
7978echo "## 🔍 PHPStan Analysis Report \n\n" ;
80- echo "**Total Errors:** $ totalErrors \n\n" ;
79+ echo "**Total Errors:** { $ totalErrors} \n\n" ;
8180
8281// Summary by category
8382echo "### 📊 Error Summary by Category \n\n" ;
8685 if ($ count > 0 ) {
8786 $ emoji = getCategoryEmoji ($ category );
8887 $ label = getCategoryLabel ($ category );
89- echo "- $ emoji ** $ label**: $ count error(s) \n" ;
88+ echo "- { $ emoji} ** { $ label} **: { $ count} error(s) \n" ;
9089 }
9190}
9291echo "\n--- \n\n" ;
9796$ fileCount = 0 ;
9897foreach ($ errorsByFile as $ filePath => $ errors ) {
9998 $ fileCount ++;
100- $ shortPath = getShortPath ($ filePath );
99+ $ shortPath = getShortPath ($ filePath );
101100 $ errorCount = count ($ errors );
102-
103- echo "#### $ fileCount. ` $ shortPath` ( $ errorCount error(s)) \n\n" ;
104-
101+
102+ echo "#### { $ fileCount} . ` { $ shortPath} ` ( { $ errorCount} error(s)) \n\n" ;
103+
105104 foreach ($ errors as $ error ) {
106- $ line = $ error ['line ' ];
107- $ message = trimMessage ($ error ['message ' ]);
105+ $ line = $ error ['line ' ];
106+ $ message = trimMessage ($ error ['message ' ]);
108107 $ category = getCategoryLabel ($ error ['category ' ]);
109-
110- echo "- **Line $ line** [ $ category]: $ message \n" ;
108+
109+ echo "- **Line { $ line} ** [ { $ category} ]: { $ message} \n" ;
111110 }
112-
111+
113112 echo "\n" ;
114113}
115114
121120
122121foreach ($ errorsByFile as $ filePath => $ errors ) {
123122 $ shortPath = getShortPath ($ filePath );
124-
123+
125124 foreach ($ errors as $ error ) {
126- $ line = $ error ['line ' ];
125+ $ line = $ error ['line ' ];
127126 $ message = trimMessage ($ error ['message ' ], 80 );
128-
129- echo "- [ ] Fix error in ` $ shortPath: $ line` - $ message \n" ;
127+
128+ echo "- [ ] Fix error in ` { $ shortPath} : { $ line} ` - { $ message} \n" ;
130129 }
131130}
132131
133132echo "\n--- \n" ;
134133
135134/**
136- * Categorize error based on message content
135+ * Categorize error based on message content.
137136 */
138137function categorizeError (string $ message ): string
139138{
140- $ normalizedMessage = strtolower ($ message );
139+ $ normalizedMessage = mb_strtolower ($ message );
141140
142141 $ hasShouldReturn = str_contains ($ normalizedMessage , 'should return ' );
143142 $ hasMethod = str_contains ($ normalizedMessage , 'method ' );
@@ -165,44 +164,44 @@ function categorizeError(string $message): string
165164 if (($ hasType || $ hasExpects ) && ! $ hasMethod && ! $ hasCallTo && ! $ hasProperty ) {
166165 return 'type_errors ' ;
167166 }
168-
167+
169168 return 'other_errors ' ;
170169}
171170
172171/**
173- * Get emoji for error category
172+ * Get emoji for error category.
174173 */
175174function getCategoryEmoji (string $ category ): string
176175{
177176 $ emojis = [
178- 'type_errors ' => '🔢 ' ,
179- 'method_errors ' => '🔧 ' ,
180- 'property_errors ' => '📦 ' ,
177+ 'type_errors ' => '🔢 ' ,
178+ 'method_errors ' => '🔧 ' ,
179+ 'property_errors ' => '📦 ' ,
181180 'return_type_errors ' => '↩️ ' ,
182- 'other_errors ' => '⚠️ ' ,
181+ 'other_errors ' => '⚠️ ' ,
183182 ];
184-
183+
185184 return $ emojis [$ category ] ?? '❓ ' ;
186185}
187186
188187/**
189- * Get human-readable label for category
188+ * Get human-readable label for category.
190189 */
191190function getCategoryLabel (string $ category ): string
192191{
193192 $ labels = [
194- 'type_errors ' => 'Type Errors ' ,
195- 'method_errors ' => 'Method Errors ' ,
196- 'property_errors ' => 'Property Errors ' ,
193+ 'type_errors ' => 'Type Errors ' ,
194+ 'method_errors ' => 'Method Errors ' ,
195+ 'property_errors ' => 'Property Errors ' ,
197196 'return_type_errors ' => 'Return Type Errors ' ,
198- 'other_errors ' => 'Other Errors ' ,
197+ 'other_errors ' => 'Other Errors ' ,
199198 ];
200-
199+
201200 return $ labels [$ category ] ?? 'Unknown ' ;
202201}
203202
204203/**
205- * Shorten file path for readability
204+ * Shorten file path for readability.
206205 */
207206function getShortPath (string $ path ): string
208207{
@@ -212,39 +211,39 @@ function getShortPath(string $path): string
212211 // Derive project root based on this script's location: .github/scripts => project root is two levels up
213212 $ projectRoot = dirname (__DIR__ , 2 );
214213 if (is_string ($ projectRoot ) && $ projectRoot !== '' ) {
215- $ normalizedRoot = rtrim (str_replace ('\\' , '/ ' , $ projectRoot ), '/ ' ) . '/ ' ;
214+ $ normalizedRoot = mb_rtrim (str_replace ('\\' , '/ ' , $ projectRoot ), '/ ' ) . '/ ' ;
216215
217216 if (str_starts_with ($ normalizedPath , $ normalizedRoot )) {
218- $ normalizedPath = substr ($ normalizedPath , strlen ($ normalizedRoot ));
217+ $ normalizedPath = mb_substr ($ normalizedPath , mb_strlen ($ normalizedRoot ));
219218 }
220219 }
221220
222221 // Fallback: also try stripping the current working directory if it is a prefix
223222 $ cwd = getcwd ();
224223 if (is_string ($ cwd ) && $ cwd !== '' ) {
225- $ normalizedCwd = rtrim (str_replace ('\\' , '/ ' , $ cwd ), '/ ' ) . '/ ' ;
224+ $ normalizedCwd = mb_rtrim (str_replace ('\\' , '/ ' , $ cwd ), '/ ' ) . '/ ' ;
226225
227226 if (str_starts_with ($ normalizedPath , $ normalizedCwd )) {
228- $ normalizedPath = substr ($ normalizedPath , strlen ($ normalizedCwd ));
227+ $ normalizedPath = mb_substr ($ normalizedPath , mb_strlen ($ normalizedCwd ));
229228 }
230229 }
231230
232231 return $ normalizedPath ;
233232}
234233
235234/**
236- * Trim message to reasonable length
235+ * Trim message to reasonable length.
237236 */
238237function trimMessage (string $ message , int $ maxLength = 150 ): string
239238{
240239 // Remove excessive whitespace
241240 $ message = preg_replace ('/\s+/ ' , ' ' , $ message );
242- $ message = trim ($ message );
243-
241+ $ message = mb_trim ($ message );
242+
244243 // Truncate if too long (multibyte-safe)
245244 if (mb_strlen ($ message , 'UTF-8 ' ) > $ maxLength ) {
246245 $ message = mb_substr ($ message , 0 , $ maxLength - 3 , 'UTF-8 ' ) . '... ' ;
247246 }
248-
247+
249248 return $ message ;
250249}
0 commit comments