├── .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 | --------------------------------------------------------------------------------