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 getenv;
15: use function is_string;
16: use function sprintf;
17: use function str_replace;
18:
19: class TableErrorFormatter implements ErrorFormatter
20: {
21:
22: public function __construct(
23: private RelativePathHelper $relativePathHelper,
24: private SimpleRelativePathHelper $simpleRelativePathHelper,
25: private CiDetectedErrorFormatter $ciDetectedErrorFormatter,
26: private bool $showTipsOfTheDay,
27: private ?string $editorUrl,
28: private ?string $editorUrlTitle,
29: )
30: {
31: }
32:
33: /** @api */
34: public function formatErrors(
35: AnalysisResult $analysisResult,
36: Output $output,
37: ): int
38: {
39: $this->ciDetectedErrorFormatter->formatErrors($analysisResult, $output);
40: $projectConfigFile = 'phpstan.neon';
41: if ($analysisResult->getProjectConfigFile() !== null) {
42: $projectConfigFile = $this->relativePathHelper->getRelativePath($analysisResult->getProjectConfigFile());
43: }
44:
45: $style = $output->getStyle();
46:
47: if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) {
48: $style->success('No errors');
49:
50: if ($this->showTipsOfTheDay) {
51: if ($analysisResult->isDefaultLevelUsed()) {
52: $output->writeLineFormatted('💡 Tip of the Day:');
53: $output->writeLineFormatted(sprintf(
54: "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.",
55: AnalyseCommand::OPTION_LEVEL,
56: AnalyseCommand::DEFAULT_LEVEL,
57: ));
58: $output->writeLineFormatted('');
59: }
60: }
61:
62: return 0;
63: }
64:
65: /** @var array<string, Error[]> $fileErrors */
66: $fileErrors = [];
67: foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
68: if (!isset($fileErrors[$fileSpecificError->getFile()])) {
69: $fileErrors[$fileSpecificError->getFile()] = [];
70: }
71:
72: $fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError;
73: }
74:
75: foreach ($fileErrors as $file => $errors) {
76: $rows = [];
77: foreach ($errors as $error) {
78: $message = $error->getMessage();
79: if ($error->getTip() !== null) {
80: $tip = $error->getTip();
81: $tip = str_replace('%configurationFile%', $projectConfigFile, $tip);
82: $message .= "\n💡 " . $tip;
83: }
84: if (is_string($this->editorUrl)) {
85: $editorFile = $error->getTraitFilePath() ?? $error->getFilePath();
86: $url = str_replace(
87: ['%file%', '%relFile%', '%line%'],
88: [$editorFile, $this->simpleRelativePathHelper->getRelativePath($editorFile), (string) $error->getLine()],
89: $this->editorUrl,
90: );
91:
92: if (is_string($this->editorUrlTitle)) {
93: $title = str_replace(
94: ['%file%', '%relFile%', '%line%'],
95: [$editorFile, $this->simpleRelativePathHelper->getRelativePath($editorFile), (string) $error->getLine()],
96: $this->editorUrlTitle,
97: );
98: } else {
99: $title = $this->relativePathHelper->getRelativePath($editorFile);
100: }
101:
102: $message .= "\n✏️ <href=" . OutputFormatter::escape($url) . '>' . $title . '</>';
103: }
104: $rows[] = [
105: $this->formatLineNumber($error->getLine()),
106: $message,
107: ];
108: }
109:
110: $style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
111: }
112:
113: if (count($analysisResult->getNotFileSpecificErrors()) > 0) {
114: $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', $error], $analysisResult->getNotFileSpecificErrors()));
115: }
116:
117: $warningsCount = count($analysisResult->getWarnings());
118: if ($warningsCount > 0) {
119: $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', $warning], $analysisResult->getWarnings()));
120: }
121:
122: $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
123: if ($warningsCount > 0) {
124: $finalMessage .= sprintf($warningsCount === 1 ? ' and %d warning' : ' and %d warnings', $warningsCount);
125: }
126:
127: if ($analysisResult->getTotalErrorsCount() > 0) {
128: $style->error($finalMessage);
129: } else {
130: $style->warning($finalMessage);
131: }
132:
133: return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0;
134: }
135:
136: private function formatLineNumber(?int $lineNumber): string
137: {
138: if ($lineNumber === null) {
139: return '';
140: }
141:
142: $isRunningInVSCodeTerminal = getenv('TERM_PROGRAM') === 'vscode';
143: if ($isRunningInVSCodeTerminal) {
144: return ':' . $lineNumber;
145: }
146:
147: return (string) $lineNumber;
148: }
149:
150: }
151: