1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Command\ErrorFormatter;
4:
5: use PHPStan\Analyser\Error;
6: use PHPStan\Command\AnalyseCommand;
7: use PHPStan\Command\AnalysisResult;
8: use PHPStan\Command\Output;
9: use PHPStan\File\RelativePathHelper;
10: use PHPStan\File\SimpleRelativePathHelper;
11: use Symfony\Component\Console\Formatter\OutputFormatter;
12: use function array_map;
13: use function count;
14: use function is_string;
15: use function sprintf;
16: use function str_replace;
17:
18: class TableErrorFormatter implements ErrorFormatter
19: {
20:
21: public function __construct(
22: private RelativePathHelper $relativePathHelper,
23: private SimpleRelativePathHelper $simpleRelativePathHelper,
24: private CiDetectedErrorFormatter $ciDetectedErrorFormatter,
25: private bool $showTipsOfTheDay,
26: private ?string $editorUrl,
27: )
28: {
29: }
30:
31: /** @api */
32: public function formatErrors(
33: AnalysisResult $analysisResult,
34: Output $output,
35: ): int
36: {
37: $this->ciDetectedErrorFormatter->formatErrors($analysisResult, $output);
38: $projectConfigFile = 'phpstan.neon';
39: if ($analysisResult->getProjectConfigFile() !== null) {
40: $projectConfigFile = $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile());
41: }
42:
43: $style = $output->getStyle();
44:
45: if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) {
46: $style->success('No errors');
47: if ($this->showTipsOfTheDay) {
48: if ($analysisResult->isDefaultLevelUsed()) {
49: $output->writeLineFormatted('💡 Tip of the Day:');
50: $output->writeLineFormatted(sprintf(
51: "PHPStan is performing only the most basic checks.\nYou can pass a higher rule level through the <fg=cyan>--%s</> option\n(the default and current level is %d) to analyse code more thoroughly.",
52: AnalyseCommand::OPTION_LEVEL,
53: AnalyseCommand::DEFAULT_LEVEL,
54: ));
55: $output->writeLineFormatted('');
56: }
57: }
58:
59: return 0;
60: }
61:
62: /** @var array<string, Error[]> $fileErrors */
63: $fileErrors = [];
64: foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
65: if (!isset($fileErrors[$fileSpecificError->getFile()])) {
66: $fileErrors[$fileSpecificError->getFile()] = [];
67: }
68:
69: $fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError;
70: }
71:
72: foreach ($fileErrors as $file => $errors) {
73: $rows = [];
74: foreach ($errors as $error) {
75: $message = $error->getMessage();
76: if ($error->getTip() !== null) {
77: $tip = $error->getTip();
78: $tip = str_replace('%configurationFile%', $projectConfigFile, $tip);
79: $message .= "\n💡 " . $tip;
80: }
81: if (is_string($this->editorUrl)) {
82: $editorFile = $error->getTraitFilePath() ?? $error->getFilePath();
83: $url = str_replace(
84: ['%file%', '%relFile%', '%line%'],
85: [$editorFile, $this->simpleRelativePathHelper->getRelativePath($editorFile), (string) $error->getLine()],
86: $this->editorUrl,
87: );
88: $message .= "\n✏️ <href=" . OutputFormatter::escape($url) . '>' . $this->relativePathHelper->getRelativePath($editorFile) . '</>';
89: }
90: $rows[] = [
91: (string) $error->getLine(),
92: $message,
93: ];
94: }
95:
96: $style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
97: }
98:
99: if (count($analysisResult->getNotFileSpecificErrors()) > 0) {
100: $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', $error], $analysisResult->getNotFileSpecificErrors()));
101: }
102:
103: $warningsCount = count($analysisResult->getWarnings());
104: if ($warningsCount > 0) {
105: $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', $warning], $analysisResult->getWarnings()));
106: }
107:
108: $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
109: if ($warningsCount > 0) {
110: $finalMessage .= sprintf($warningsCount === 1 ? ' and %d warning' : ' and %d warnings', $warningsCount);
111: }
112:
113: if ($analysisResult->getTotalErrorsCount() > 0) {
114: $style->error($finalMessage);
115: } else {
116: $style->warning($finalMessage);
117: }
118:
119: return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0;
120: }
121:
122: }
123: