├── .tool-versions
├── .typos.toml
├── docs
├── example.png
└── github-actions.png
├── playground
├── phpstan.neon
├── missing_types.php
├── undefined_errors.php
├── type_errors.php
├── dead_code.php
└── logic_errors.php
├── extension.neon
├── phpunit.xml
├── src
├── Config
│ └── FriendlyFormatterConfig.php
├── CodeHighlight
│ ├── FallbackHighlighter.php
│ └── CodeHighlighter.php
├── ErrorFormat
│ ├── SummaryWriter.php
│ └── ErrorWriter.php
└── FriendlyErrorFormatter.php
├── LICENSE.md
├── .devcontainer
├── devcontainer.json
└── Dockerfile
├── composer.json
└── CLAUDE.md
/.tool-versions:
--------------------------------------------------------------------------------
1 | php 8.5.0
2 |
--------------------------------------------------------------------------------
/.typos.toml:
--------------------------------------------------------------------------------
1 | [files]
2 | extend-exclude = [
3 | ".git/",
4 | ]
5 | ignore-hidden = false
6 |
--------------------------------------------------------------------------------
/docs/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yamadashy/phpstan-friendly-formatter/HEAD/docs/example.png
--------------------------------------------------------------------------------
/docs/github-actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yamadashy/phpstan-friendly-formatter/HEAD/docs/github-actions.png
--------------------------------------------------------------------------------
/playground/phpstan.neon:
--------------------------------------------------------------------------------
1 | # PHPStan configuration for playground
2 | # Usage: cd playground && ../vendor/bin/phpstan analyze
3 |
4 | includes:
5 | - ../extension.neon
6 |
7 | parameters:
8 | level: 9
9 | paths:
10 | - .
11 |
12 | # Friendly formatter settings
13 | friendly:
14 | lineBefore: 3
15 | lineAfter: 3
16 | editorUrl: 'vscode://file/%%relFile%%:%%line%%'
17 |
--------------------------------------------------------------------------------
/extension.neon:
--------------------------------------------------------------------------------
1 | parametersSchema:
2 | friendly: structure([
3 | lineBefore: int()
4 | lineAfter: int()
5 | editorUrl: schema(string(), nullable())
6 | ])
7 |
8 | parameters:
9 | friendly:
10 | lineBefore: 2
11 | lineAfter: 2
12 | editorUrl: null
13 |
14 | services:
15 | errorFormatter.friendly:
16 | class: Yamadashy\PhpStanFriendlyFormatter\FriendlyErrorFormatter
17 | arguments:
18 | relativePathHelper: @relativePathHelper
19 | simpleRelativePathHelper: @simpleRelativePathHelper
20 | lineBefore: %friendly.lineBefore%
21 | lineAfter: %friendly.lineAfter%
22 | editorUrl: %friendly.editorUrl%
23 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | tests
13 |
14 |
15 |
16 |
17 | src
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Config/FriendlyFormatterConfig.php:
--------------------------------------------------------------------------------
1 | lineBefore = $lineBefore;
30 | $this->lineAfter = $lineAfter;
31 | $this->editorUrl = $editorUrl;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/playground/missing_types.php:
--------------------------------------------------------------------------------
1 | untyped = $input;
31 | return $input;
32 | }
33 | }
34 |
35 | // 4. Mixed type usage
36 | function usesMixed(mixed $value): mixed
37 | {
38 | return $value->someMethod(); // Error: calling method on mixed
39 | }
40 |
--------------------------------------------------------------------------------
/playground/undefined_errors.php:
--------------------------------------------------------------------------------
1 | nonExistentMethod(); // Error: undefined method
25 |
26 | // 3. Undefined class
27 | $instance = new NonExistentClass(); // Error: undefined class
28 |
29 | // 4. Undefined constant
30 | echo UNDEFINED_CONSTANT; // Error: undefined constant
31 |
32 | // 5. Undefined property
33 | class AnotherClass
34 | {
35 | public string $definedProperty = 'hello';
36 | }
37 |
38 | $another = new AnotherClass();
39 | echo $another->undefinedProperty; // Error: undefined property
40 |
41 | // 6. Undefined function
42 | undefinedFunction(); // Error: undefined function
43 |
--------------------------------------------------------------------------------
/src/CodeHighlight/FallbackHighlighter.php:
--------------------------------------------------------------------------------
1 | '.str_pad((string) $i, $lineNumberWidth, ' ', STR_PAD_LEFT).'| >';
24 | $snippet .= ($lineNumber === $i ? ' > >' : ' ').$linePrefix.$currentLine."\n";
25 | }
26 |
27 | return rtrim($snippet);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Kazuki Yamada
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/playground/type_errors.php:
--------------------------------------------------------------------------------
1 | $items */
38 | function processStringArray(array $items): void
39 | {
40 | foreach ($items as $item) {
41 | echo $item;
42 | }
43 | }
44 |
45 | processStringArray([1, 2, 3]); // Error: array of int, not string
46 |
47 | // 5. Property type mismatch
48 | class TypedClass
49 | {
50 | public string $name;
51 |
52 | public function __construct()
53 | {
54 | $this->name = 42; // Error: wrong type
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/ErrorFormat/SummaryWriter.php:
--------------------------------------------------------------------------------
1 | ';
11 |
12 | public function writeGroupedErrorsSummary(AnalysisResult $analysisResult, Output $output): void
13 | {
14 | /** @var array $errorCounter */
15 | $errorCounter = [];
16 |
17 | foreach ($analysisResult->getFileSpecificErrors() as $error) {
18 | $identifier = $error->getIdentifier() ?? self::IDENTIFIER_NO_IDENTIFIER;
19 | if (!\array_key_exists($identifier, $errorCounter)) {
20 | $errorCounter[$identifier] = 0;
21 | }
22 | ++$errorCounter[$identifier];
23 | }
24 |
25 | arsort($errorCounter);
26 |
27 | $output->writeLineFormatted('📊 Error Identifier Summary:');
28 | $output->writeLineFormatted('────────────────────────────');
29 |
30 | foreach ($errorCounter as $identifier => $count) {
31 | $output->writeLineFormatted(\sprintf(' %d %s', $count, $identifier));
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/playground/dead_code.php:
--------------------------------------------------------------------------------
1 | 'first',
56 | 'key' => 'second', // Error: duplicate key
57 | ];
58 | }
59 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PHPStan Friendly Formatter Dev",
3 | "build": {
4 | "dockerfile": "Dockerfile",
5 | "args": {
6 | "TZ": "${localEnv:TZ:America/Los_Angeles}",
7 | "CLAUDE_CODE_VERSION": "latest",
8 | "GIT_DELTA_VERSION": "0.18.2",
9 | "ZSH_IN_DOCKER_VERSION": "1.2.0"
10 | }
11 | },
12 | "customizations": {
13 | "vscode": {
14 | "extensions": [
15 | "anthropic.claude-code",
16 | "eamodio.gitlens"
17 | ],
18 | "settings": {
19 | "terminal.integrated.defaultProfile.linux": "zsh",
20 | "terminal.integrated.profiles.linux": {
21 | "bash": {
22 | "path": "bash",
23 | "icon": "terminal-bash"
24 | },
25 | "zsh": {
26 | "path": "zsh"
27 | }
28 | }
29 | }
30 | }
31 | },
32 | "remoteUser": "node",
33 | "mounts": [
34 | "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind",
35 | "source=${localEnv:HOME}/.claude.json,target=/home/node/.claude.json,type=bind",
36 | "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume"
37 | ],
38 | "containerEnv": {
39 | "NODE_OPTIONS": "--max-old-space-size=4096",
40 | "CLAUDE_CONFIG_DIR": "/home/node/.claude",
41 | "POWERLEVEL9K_DISABLE_GITSTATUS": "true"
42 | },
43 | "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
44 | "workspaceFolder": "/workspace",
45 | "postStartCommand": "composer install"
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yamadashy/phpstan-friendly-formatter",
3 | "type": "phpstan-extension",
4 | "description": "Simple error formatter for PHPStan that display code frame",
5 | "keywords": ["package", "php", "phpstan", "static analysis"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Kazuki Yamada",
10 | "email": "koukun0120@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.1",
15 | "php-parallel-lint/php-console-highlighter": "^0.3 || ^0.4 || ^0.5 || ^1.0",
16 | "phpstan/phpstan": "^1.0 || ^2.0"
17 | },
18 | "require-dev": {
19 | "friendsofphp/php-cs-fixer": "^3.92.0",
20 | "phpstan/phpstan-phpunit": "^2.0.10",
21 | "phpunit/phpunit": "^10.0 || ^11.0"
22 | },
23 | "minimum-stability": "dev",
24 | "prefer-stable": true,
25 | "autoload": {
26 | "psr-4": {
27 | "Yamadashy\\PhpStanFriendlyFormatter\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Tests\\": "tests/"
33 | }
34 | },
35 | "config": {
36 | "sort-packages": true,
37 | "preferred-install": "dist"
38 | },
39 | "extra": {
40 | "phpstan": {
41 | "includes": ["extension.neon"]
42 | }
43 | },
44 | "scripts": {
45 | "tests": [
46 | "@cs-fix-dry",
47 | "@analyze",
48 | "@test"
49 | ],
50 | "test": "phpunit",
51 | "analyze": "phpstan analyze -c phpstan.neon.dist --error-format friendly",
52 | "analyze-raw": "phpstan analyze -c phpstan.neon.dist --error-format raw",
53 | "analyze-table": "phpstan analyze -c phpstan.neon.dist --error-format table",
54 | "playground": "phpstan analyze playground -c playground/phpstan.neon --error-format friendly",
55 | "cs-fix": "php-cs-fixer fix",
56 | "cs-fix-dry": "php-cs-fixer fix --dry-run"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/CodeHighlight/CodeHighlighter.php:
--------------------------------------------------------------------------------
1 | highlighter = new Highlighter($colors);
28 | } elseif (
29 | class_exists('\JakubOnderka\PhpConsoleHighlighter\Highlighter')
30 | && class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor')
31 | ) {
32 | // Support Highlighter and ConsoleColor < 1.0.
33 | $colors = new LegacyConsoleColor();
34 | $this->highlighter = new LegacyHighlighter($colors);
35 | } else {
36 | // Fallback to non-highlighted output
37 | $this->highlighter = new FallbackHighlighter();
38 | }
39 | }
40 |
41 | public function highlight(string $fileContent, int $lineNumber, int $lineBefore, int $lineAfter): string
42 | {
43 | /** @phpstan-ignore class.notFound */
44 | $content = $this->highlighter->getCodeSnippet(
45 | $fileContent,
46 | $lineNumber,
47 | $lineBefore,
48 | $lineAfter
49 | );
50 |
51 | return rtrim($content, "\n");
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | PHPStan Friendly Formatter is a PHPStan extension that provides enhanced error output with syntax-highlighted code context around errors. It transforms standard PHPStan output into a visually clear format showing actual problematic code with line numbers.
8 |
9 | ## Commands
10 |
11 | ```bash
12 | composer test # Run PHPUnit tests
13 | composer cs-fix-dry # Check code style (dry run)
14 | composer cs-fix # Fix code style issues
15 | composer analyze # Run PHPStan with friendly formatter
16 | composer tests # Run all: cs-fix-dry → analyze → test
17 | ```
18 |
19 | ## Architecture
20 |
21 | ```
22 | src/
23 | ├── FriendlyErrorFormatter.php # Main entry point, implements PHPStan ErrorFormatter
24 | ├── CodeHighlight/
25 | │ ├── CodeHighlighter.php # Syntax highlighting with version fallback
26 | │ └── FallbackHighlighter.php # Non-highlighted fallback
27 | ├── Config/
28 | │ └── FriendlyFormatterConfig.php # Formatter configuration (lineBefore, lineAfter, editorUrl)
29 | └── ErrorFormat/
30 | ├── ErrorWriter.php # Formats individual errors with code context
31 | └── SummaryWriter.php # Error identifier summary statistics
32 | ```
33 |
34 | **Data Flow:** PHPStan AnalysisResult → FriendlyErrorFormatter → ErrorWriter (with CodeHighlighter) → SummaryWriter → Console output
35 |
36 | ## Key Configuration Files
37 |
38 | - `phpstan.neon.dist` - PHPStan config (level 10, paths, formatter settings)
39 | - `extension.neon` - PHPStan extension registration and parameter schema
40 | - `.php-cs-fixer.dist.php` - Code style rules
41 | - `phpunit.xml` - PHPUnit 10/11 compatible config
42 | - `.tool-versions` - PHP version for local dev and CI (used by `php-version-file` in GitHub Actions)
43 |
44 | ## Compatibility
45 |
46 | - PHP: ^8.1 (tested on 8.1, 8.2, 8.3, 8.4, 8.5)
47 | - PHPStan: ^1.0 || ^2.0
48 | - PHPUnit: ^10.0 || ^11.0 (tests use PHP 8 attributes)
49 | - php-console-highlighter: ^0.3 || ^0.4 || ^0.5 || ^1.0 (with graceful fallback)
50 |
51 | The codebase handles multiple dependency versions through class existence checks and fallback implementations.
52 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:24
2 |
3 | ARG TZ
4 | ENV TZ="$TZ"
5 |
6 | ARG CLAUDE_CODE_VERSION=latest
7 |
8 | # Install basic development tools
9 | RUN apt-get update && apt-get install -y --no-install-recommends \
10 | less \
11 | git \
12 | procps \
13 | sudo \
14 | fzf \
15 | zsh \
16 | man-db \
17 | unzip \
18 | gnupg2 \
19 | gh \
20 | jq \
21 | nano \
22 | vim \
23 | ca-certificates \
24 | curl \
25 | lsb-release \
26 | && apt-get clean && rm -rf /var/lib/apt/lists/*
27 |
28 | # Install PHP 8.5 from Sury repository
29 | RUN curl -sSL https://packages.sury.org/php/README.txt | bash -x && \
30 | apt-get update && apt-get install -y --no-install-recommends \
31 | php8.5-cli \
32 | php8.5-mbstring \
33 | php8.5-xml \
34 | php8.5-curl \
35 | php8.5-zip \
36 | && apt-get clean && rm -rf /var/lib/apt/lists/*
37 |
38 | # Install Composer
39 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
40 |
41 | # Ensure default node user has access to /usr/local/share
42 | RUN mkdir -p /usr/local/share/npm-global && \
43 | chown -R node:node /usr/local/share/npm-global
44 |
45 | ARG USERNAME=node
46 |
47 | # Persist shell history
48 | RUN mkdir /commandhistory \
49 | && touch /commandhistory/.bash_history \
50 | && chown -R $USERNAME /commandhistory
51 |
52 | # Set `DEVCONTAINER` environment variable to help with orientation
53 | ENV DEVCONTAINER=true
54 |
55 | # Create workspace and config directories and set permissions
56 | RUN mkdir -p /workspace /home/node/.claude && \
57 | chown -R node:node /workspace /home/node/.claude
58 |
59 | WORKDIR /workspace
60 |
61 | ARG GIT_DELTA_VERSION=0.18.2
62 | RUN ARCH=$(dpkg --print-architecture) && \
63 | wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
64 | dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
65 | rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
66 |
67 | # Set up non-root user
68 | USER node
69 |
70 | # Install global packages
71 | ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
72 | ENV PATH=$PATH:/usr/local/share/npm-global/bin
73 |
74 | # Set the default shell to zsh rather than sh
75 | ENV SHELL=/bin/zsh
76 |
77 | # Set the default editor and visual
78 | ENV EDITOR=nano
79 | ENV VISUAL=nano
80 |
81 | # Default powerline10k theme
82 | ARG ZSH_IN_DOCKER_VERSION=1.2.0
83 | RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
84 | -p git \
85 | -p fzf \
86 | -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
87 | -a "source /usr/share/doc/fzf/examples/completion.zsh" \
88 | -a "setopt APPEND_HISTORY INC_APPEND_HISTORY SHARE_HISTORY; export HISTFILE=/commandhistory/.bash_history" \
89 | -x
90 |
91 | # Install Claude
92 | RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
93 |
--------------------------------------------------------------------------------
/src/FriendlyErrorFormatter.php:
--------------------------------------------------------------------------------
1 | relativePathHelper = $relativePathHelper;
34 | $this->simpleRelativePathHelper = $simpleRelativePathHelper;
35 | $this->config = new FriendlyFormatterConfig(
36 | $lineBefore,
37 | $lineAfter,
38 | $editorUrl
39 | );
40 | }
41 |
42 | /**
43 | * @return int error code
44 | */
45 | public function formatErrors(AnalysisResult $analysisResult, Output $output): int
46 | {
47 | if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) {
48 | return $this->handleNoErrors($output);
49 | }
50 |
51 | $output->writeLineFormatted('');
52 |
53 | $errorWriter = new ErrorWriter($this->relativePathHelper, $this->simpleRelativePathHelper, $this->config);
54 | $errorWriter->writeFileSpecificErrors($analysisResult, $output);
55 | $errorWriter->writeNotFileSpecificErrors($analysisResult, $output);
56 | $errorWriter->writeWarnings($analysisResult, $output);
57 |
58 | $summaryWriter = new SummaryWriter();
59 | $summaryWriter->writeGroupedErrorsSummary($analysisResult, $output);
60 |
61 | $this->writeAnalysisResultMessage($analysisResult, $output);
62 |
63 | return 1;
64 | }
65 |
66 | private function handleNoErrors(Output $output): int
67 | {
68 | $output->getStyle()->success('No errors');
69 |
70 | return 0;
71 | }
72 |
73 | private function writeAnalysisResultMessage(AnalysisResult $analysisResult, Output $output): void
74 | {
75 | $warningsCount = \count($analysisResult->getWarnings());
76 | $finalMessage = \sprintf(1 === $analysisResult->getTotalErrorsCount() ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
77 |
78 | if ($warningsCount > 0) {
79 | $finalMessage .= \sprintf(1 === $warningsCount ? ' and %d warning' : ' and %d warnings', $warningsCount);
80 | }
81 |
82 | if ($analysisResult->getTotalErrorsCount() > 0) {
83 | $output->getStyle()->error($finalMessage);
84 | } else {
85 | $output->getStyle()->warning($finalMessage);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/ErrorFormat/ErrorWriter.php:
--------------------------------------------------------------------------------
1 | relativePathHelper = $relativePathHelper;
31 | $this->simpleRelativePathHelper = $simpleRelativePathHelper;
32 | $this->config = $config;
33 | }
34 |
35 | public function writeFileSpecificErrors(AnalysisResult $analysisResult, Output $output): void
36 | {
37 | $codeHighlighter = new CodeHighlighter();
38 | $errorsByFile = [];
39 |
40 | foreach ($analysisResult->getFileSpecificErrors() as $error) {
41 | $filePath = $error->getTraitFilePath() ?? $error->getFilePath();
42 | $relativeFilePath = $this->relativePathHelper->getRelativePath($filePath);
43 | $errorsByFile[$relativeFilePath][] = $error;
44 | }
45 |
46 | foreach ($errorsByFile as $relativeFilePath => $errors) {
47 | $output->writeLineFormatted("❯ {$relativeFilePath}");
48 | $output->writeLineFormatted('--'.str_repeat('-', mb_strlen($relativeFilePath)));
49 | $output->writeLineFormatted('');
50 |
51 | foreach ($errors as $error) {
52 | $message = $error->getMessage();
53 | $tip = $this->getFormattedTip($error);
54 | $errorIdentifier = $error->getIdentifier();
55 | $filePath = $error->getTraitFilePath() ?? $error->getFilePath();
56 | $line = $error->getLine();
57 | $fileContent = null;
58 |
59 | if (file_exists($filePath)) {
60 | $fileContent = (string) file_get_contents($filePath);
61 | }
62 |
63 | if (null === $fileContent) {
64 | $codeSnippet = ' >';
65 | } elseif (null === $line) {
66 | $codeSnippet = ' >';
67 | } else {
68 | $codeSnippet = $codeHighlighter->highlight($fileContent, $line, $this->config->lineBefore, $this->config->lineAfter);
69 | }
70 |
71 | $output->writeLineFormatted(" ✘> {$message}>");
72 |
73 | if (null !== $tip) {
74 | $output->writeLineFormatted(" 💡 {$tip}>");
75 | }
76 |
77 | if (null !== $errorIdentifier) {
78 | $output->writeLineFormatted(" 🪪 {$errorIdentifier}>");
79 | }
80 |
81 | if (\is_string($this->config->editorUrl)) {
82 | /**
83 | * SimpleRelativePathHelper is not covered by PHPStan's backward compatibility promise.
84 | *
85 | * @see https://phpstan.org/developing-extensions/backward-compatibility-promise
86 | *
87 | * @phpstan-ignore-next-line phpstanApi.method
88 | */
89 | $relFile = $this->simpleRelativePathHelper->getRelativePath($filePath);
90 | $output->writeLineFormatted(' ✏️ getLine()],
93 | $this->config->editorUrl,
94 | )).'>'.$relativeFilePath.'>');
95 | }
96 |
97 | $output->writeLineFormatted($codeSnippet);
98 | $output->writeLineFormatted('');
99 | }
100 | }
101 | }
102 |
103 | public function writeNotFileSpecificErrors(AnalysisResult $analysisResult, Output $output): void
104 | {
105 | foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) {
106 | $output->writeLineFormatted(" ✘> {$notFileSpecificError}>");
107 | $output->writeLineFormatted('');
108 | }
109 | }
110 |
111 | public function writeWarnings(AnalysisResult $analysisResult, Output $output): void
112 | {
113 | foreach ($analysisResult->getWarnings() as $warning) {
114 | $output->writeLineFormatted(" ⚠> {$warning}>");
115 | $output->writeLineFormatted('');
116 | }
117 | }
118 |
119 | private function getFormattedTip(Error $error): ?string
120 | {
121 | $tip = $error->getTip();
122 |
123 | if (null === $tip) {
124 | return null;
125 | }
126 |
127 | return implode("\n ", explode("\n", $tip));
128 | }
129 | }
130 |
--------------------------------------------------------------------------------