diff --git a/composer.json b/composer.json index cd2d480..5f79fca 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "shipmonk/coding-standard": "^0.1.3", "shipmonk/composer-dependency-analyser": "^1.8.1", "shipmonk/dead-code-detector": "^0.9.0", - "shipmonk/name-collision-detector": "^2.1.1" + "shipmonk/name-collision-detector": "^2.1.1", + "shipmonk/phpstan-dev": "^0.1.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 6380b1b..8b7ca12 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c7e2df2cf10e92a7163b989c71ac0396", + "content-hash": "6fb0c988abe9eb2fd7f966302480f059", "packages": [ { "name": "phpstan/phpstan", @@ -3130,6 +3130,61 @@ }, "time": "2024-03-01T13:26:32+00:00" }, + { + "name": "shipmonk/phpstan-dev", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/phpstan-dev.git", + "reference": "ec564f68d893fa966f70afa8274d03fa8f0103df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/phpstan-dev/zipball/ec564f68d893fa966f70afa8274d03fa8f0103df", + "reference": "ec564f68d893fa966f70afa8274d03fa8f0103df", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.8" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.6.0", + "ergebnis/composer-normalize": "^2.45.0", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "phpunit/phpunit": "^9.6.22", + "shipmonk/composer-dependency-analyser": "^1.8.1", + "shipmonk/dead-code-detector": "^0.9.0", + "shipmonk/name-collision-detector": "^2.1.1", + "slevomat/coding-standard": "^8.16.0" + }, + "suggest": { + "phpunit/phpunit": "^9.6.22 for running tests that use RuleTestCase" + }, + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\PHPStanDev\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Development utilities for PHPStan rules testing, extracted from shipmonk/phpstan-rules", + "keywords": [ + "PHPStan", + "static analysis", + "testing" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/phpstan-dev/issues", + "source": "https://github.com/shipmonk-rnd/phpstan-dev/tree/0.1.1" + }, + "time": "2025-08-25T12:16:48+00:00" + }, { "name": "slevomat/coding-standard", "version": "8.19.1", diff --git a/tests/RuleTestCase.php b/tests/RuleTestCase.php index 54da771..c1b28ea 100644 --- a/tests/RuleTestCase.php +++ b/tests/RuleTestCase.php @@ -2,28 +2,14 @@ namespace ShipMonk\PHPStan; -use LogicException; -use PHPStan\Analyser\Error; use PHPStan\Rules\Rule; -use PHPStan\Testing\RuleTestCase as OriginalRuleTestCase; -use function array_values; -use function explode; -use function file_get_contents; -use function file_put_contents; -use function implode; -use function ksort; -use function preg_match; -use function preg_match_all; -use function preg_replace; -use function sprintf; -use function trim; -use function uniqid; +use ShipMonk\PHPStanDev\RuleTestCase as ShipMonkDevRuleTestCase; /** * @template TRule of Rule - * @extends OriginalRuleTestCase + * @extends ShipMonkDevRuleTestCase */ -abstract class RuleTestCase extends OriginalRuleTestCase +abstract class RuleTestCase extends ShipMonkDevRuleTestCase { protected function analyseFile( @@ -31,137 +17,7 @@ protected function analyseFile( bool $autofix = false ): void { - $analyserErrors = $this->gatherAnalyserErrors([$file]); - - if ($autofix === true) { - $this->autofix($file, $analyserErrors); - self::fail("File $file was autofixed. This setup should never remain in the codebase."); - } - - $actualErrors = $this->processActualErrors($analyserErrors); - $expectedErrors = $this->parseExpectedErrors($file); - - self::assertSame( - implode("\n", $expectedErrors) . "\n", - implode("\n", $actualErrors) . "\n", - ); - } - - /** - * @param list $actualErrors - * @return list - */ - protected function processActualErrors(array $actualErrors): array - { - $resultToAssert = []; - - foreach ($actualErrors as $error) { - $usedLine = $error->getLine() ?? -1; - $key = sprintf('%04d', $usedLine) . '-' . uniqid(); - $resultToAssert[$key] = $this->formatErrorForAssert($error->getMessage(), $usedLine); - - self::assertNotNull($error->getIdentifier(), "Missing error identifier for error: {$error->getMessage()}"); - self::assertStringStartsWith('shipmonk.', $error->getIdentifier(), "Unexpected error identifier for: {$error->getMessage()}"); - } - - ksort($resultToAssert); - - return array_values($resultToAssert); - } - - /** - * @return list - */ - private function parseExpectedErrors(string $file): array - { - $fileLines = $this->getFileLines($file); - $expectedErrors = []; - - foreach ($fileLines as $line => $row) { - /** @var array{0: list, 1: list} $matches */ - $matched = preg_match_all('#// error:(.+)#', $row, $matches); - - if ($matched === false) { - throw new LogicException('Error while matching errors'); - } - - if ($matched === 0) { - continue; - } - - foreach ($matches[1] as $error) { - $actualLine = $line + 1; - $key = sprintf('%04d', $actualLine) . '-' . uniqid(); - $expectedErrors[$key] = $this->formatErrorForAssert(trim($error), $actualLine); - } - } - - ksort($expectedErrors); - - return array_values($expectedErrors); - } - - private function formatErrorForAssert( - string $message, - int $line - ): string - { - return sprintf('%02d: %s', $line, $message); - } - - /** - * @param list $analyserErrors - */ - private function autofix( - string $file, - array $analyserErrors - ): void - { - $errorsByLines = []; - - foreach ($analyserErrors as $analyserError) { - $line = $analyserError->getLine(); - - if ($line === null) { - throw new LogicException('Error without line number: ' . $analyserError->getMessage()); - } - - $errorsByLines[$line] = $analyserError; - } - - $fileLines = $this->getFileLines($file); - - foreach ($fileLines as $line => &$row) { - if (!isset($errorsByLines[$line + 1])) { - continue; - } - - $errorCommentPattern = '~ ?//.*$~'; - $errorMessage = $errorsByLines[$line + 1]->getMessage(); - $errorComment = ' // error: ' . $errorMessage; - - if (preg_match($errorCommentPattern, $row) === 1) { - $row = preg_replace($errorCommentPattern, $errorComment, $row); - } else { - $row .= $errorComment; - } - } - - file_put_contents($file, implode("\n", $fileLines)); - } - - /** - * @return list - */ - private function getFileLines(string $file): array - { - $fileData = file_get_contents($file); - - if ($fileData === false) { - throw new LogicException('Error while reading data from ' . $file); - } - - return explode("\n", $fileData); + $this->analyzeFiles([$file], $autofix); } }