Skip to content

Instantly share code, notes, and snippets.

@NickSdot
Last active October 22, 2025 06:24
Show Gist options
  • Select an option

  • Save NickSdot/58d27003b5c76c662bb1bfac550f3517 to your computer and use it in GitHub Desktop.

Select an option

Save NickSdot/58d27003b5c76c662bb1bfac550f3517 to your computer and use it in GitHub Desktop.
Show the PHPStan error identifier in PHPStorm inspections. Available as Composer package: https://github.com/NickSdot/phpstan-phpstorm-error-identifiers
image

Steps

  1. Copy the class somewhere in your project
  2. Change the namespace
  3. Add the following to your phpstan.neon
services:
	errorFormatter.checkstyle:
		class: Local\PhpStan\CheckstyleErrorFormatter # change to where you pasted it!

That's it. No other changes compared to the original formatter where made.

This is a PHPStorm bug, a PR to PHPStan doesn't make sense.

<?php
declare(strict_types=1);
namespace Local\PhpStan; // <----------------------------------- change to where you paste it!
use PHPStan\Analyser\Error;
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use PHPStan\Command\Output;
use PHPStan\File\RelativePathHelper;
use function count;
use function htmlspecialchars;
use function sprintf;
final readonly class CheckstyleErrorFormatter implements ErrorFormatter
{
public function __construct(private RelativePathHelper $relativePathHelper) {}
public function formatErrors(
AnalysisResult $analysisResult,
Output $output,
): int {
$output->writeRaw('<?xml version="1.0" encoding="UTF-8"?>');
$output->writeLineFormatted('');
$output->writeRaw('<checkstyle>');
$output->writeLineFormatted('');
foreach ($this->groupByFile($analysisResult) as $relativeFilePath => $errors) {
$output->writeRaw(sprintf(
'<file name="%s">',
$this->escape($relativeFilePath),
));
$output->writeLineFormatted('');
foreach ($errors as $error) {
// Note:
// PHPStorm ignores the "source" attribute; also we need better formatting options.
// Go and smash the upvote button here:
// https://youtrack.jetbrains.com/issue/WI-81974/PHPStan-Show-source-of-the-message-too
// https://youtrack.jetbrains.com/issue/WI-78524/PHPStan-Make-it-possible-to-use-more-detailed-error-reporting-formats
$identifier = null !== $error->getIdentifier()
? " // @phpstan-ignore {$error->getIdentifier()}" :
'';
$output->writeRaw(sprintf(
'<error line="%d" column="1" severity="error" message="%s" />',
$this->escape((string) $error->getLine()),
$this->escape($error->getMessage() . $identifier),
));
$output->writeLineFormatted('');
}
$output->writeRaw('</file>');
$output->writeLineFormatted('');
}
$notFileSpecificErrors = $analysisResult->getNotFileSpecificErrors();
if (count($notFileSpecificErrors) > 0) {
$output->writeRaw('<file>');
$output->writeLineFormatted('');
foreach ($notFileSpecificErrors as $error) {
$output->writeRaw(sprintf(' <error severity="error" message="%s" />', $this->escape($error)));
$output->writeLineFormatted('');
}
$output->writeRaw('</file>');
$output->writeLineFormatted('');
}
if ($analysisResult->hasWarnings()) {
$output->writeRaw('<file>');
$output->writeLineFormatted('');
foreach ($analysisResult->getWarnings() as $warning) {
$output->writeRaw(sprintf(' <error severity="warning" message="%s" />', $this->escape($warning)));
$output->writeLineFormatted('');
}
$output->writeRaw('</file>');
$output->writeLineFormatted('');
}
$output->writeRaw('</checkstyle>');
$output->writeLineFormatted('');
return $analysisResult->hasErrors() ? 1 : 0;
}
/** Escapes values for using in XML */
private function escape(string $string): string
{
return htmlspecialchars($string, \ENT_XML1 | \ENT_COMPAT, 'UTF-8');
}
/**
* Group errors by file
*
* @return Error Array that have as key the relative path of file
* and as value an array with occurred errors.
*/
private function groupByFile(AnalysisResult $analysisResult): array
{
$files = [];
/** @var Error $fileSpecificError */
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
$absolutePath = $fileSpecificError->getFilePath();
if (null !== $fileSpecificError->getTraitFilePath()) {
$absolutePath = $fileSpecificError->getTraitFilePath();
}
$relativeFilePath = $this->relativePathHelper->getRelativePath(
$absolutePath,
);
$files[$relativeFilePath][] = $fileSpecificError;
}
return $files;
}
}
@staabm
Copy link

staabm commented Oct 21, 2025

@NickSdot
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment