diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..33ec9e9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5d6519e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +/cache export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..0170af2 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,50 @@ +name: Checks +on: + pull_request: + push: + branches: + - master +jobs: + checks: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - + name: Checkout code + uses: actions/checkout@v4 + - + name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + - + name: Install dependencies + run: composer install --no-progress --prefer-dist --no-interaction + + - + name: Run checks + run: composer check + + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: [ '8.1', '8.2', '8.3', '8.4' ] + dependency-version: [ prefer-lowest, prefer-stable ] + steps: + - + name: Checkout code + uses: actions/checkout@v4 + - + name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + - + name: Update dependencies + run: composer update --no-progress --${{ matrix.dependency-version }} --no-interaction + - + name: Run tests + run: composer check:tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b2b006 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +/cache/* +!/cache/.gitkeep +/.idea +/phpstan.neon +/composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..72b3054 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# PHPStan error ignore inliner + +Allows you to easily **inline ignore your PHPStan errors** via `@phpstan-ignore` comment. + +So instead of: + +```neon +parameters: + ignoreErrors: + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + path: ../src/App/User.php + count: 1 +``` + +You will have the ignored error directly in the source code `src/App/User.php`: + +```php +class User { + + public function updateSurname(string $surname): void + { + if (empty($surname)) { // @phpstan-ignore empty.notAllowed + throw new EmptyNameException(); + } + } + +} +``` + +## Installation: + +```sh +composer require --dev shipmonk/phpstan-ignore-inliner +``` + +## Usage + +```sh +vendor/bin/phpstan --error-format=json | vendor/bin/inline-phpstan-ignores +``` + +## Contributing +- Check your code by `composer check` +- Autofix coding-style by `composer fix:cs` +- All functionality must be tested diff --git a/bin/inline-phpstan-ignores b/bin/inline-phpstan-ignores new file mode 100755 index 0000000..2bd7005 --- /dev/null +++ b/bin/inline-phpstan-ignores @@ -0,0 +1,36 @@ +#!/usr/bin/env php +readInput(); + $errorsData = json_decode($input, associative: true, flags: JSON_THROW_ON_ERROR); + $errors = $errorsData['files'] ?? throw new FailureException('No \'files\' key found on input JSON.'); + + $inliner = new InlineIgnoreInliner($io); + $inliner->inlineErrors($errors); + + $errorsCount = count($errors); + echo "Done, $errorsCount errors processed.\n"; + +} catch (JsonException | FailureException $e) { + echo $e->getMessage() . $usage; +} diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f5adc0f --- /dev/null +++ b/composer.json @@ -0,0 +1,68 @@ +{ + "name": "shipmonk/phpstan-ignore-inliner", + "description": "Inline your PHPStan error ignores into the source files via @phpstan-ignore comments.", + "license": [ + "MIT" + ], + "type": "phpstan-extension", + "keywords": [ + "dev", + "phpstan", + "phpstan errors", + "phpstan extension", + "error identifier" + ], + "require": { + "php": "^8.1", + "phpstan/phpstan": "^2" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.7.0", + "ergebnis/composer-normalize": "2.47.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-phpunit": "^2.0.6", + "phpstan/phpstan-strict-rules": "^2.0.4", + "phpunit/phpunit": "^10.5.46", + "shipmonk/phpstan-rules": "^4.1.2", + "slevomat/coding-standard": "^8.18.0" + }, + "autoload": { + "psr-4": { + "ShipMonk\\PHPStan\\Errors\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ShipMonk\\PHPStan\\Errors\\": "tests/" + } + }, + "bin": [ + "bin/inline-phpstan-ignores" + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": false, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + }, + "sort-packages": true + }, + "scripts": { + "check": [ + "@check:composer", + "@check:ec", + "@check:cs", + "@check:types", + "@check:tests" + ], + "check:composer": [ + "composer normalize --dry-run --no-check-lock --no-update-lock", + "composer validate --strict" + ], + "check:cs": "phpcs", + "check:ec": "ec src tests", + "check:tests": "phpunit tests", + "check:types": "phpstan analyse -vv --ansi", + "fix:cs": "phpcbf" + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..e7c73b8 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + src/ + tests/ + + tests/data/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exception type missing for @throws annotation + + + Only 1 @return annotation is allowed in a function comment + + + Extra @param annotation + + + @param annotation for parameter "%s" missing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..c486be5 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,31 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - bin/inline-phpstan-ignores + - src + - tests + excludePaths: + analyseAndScan: + - tests/data/* + tmpDir: cache/phpstan/ + checkMissingCallableSignature: true + checkUninitializedProperties: true + checkBenevolentUnionTypes: true + checkImplicitMixed: true + checkTooWideReturnTypesInProtectedAndPublicMethods: true + reportAnyTypeWideningInVarTag: true + exceptions: + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + implicitThrows: false + uncheckedExceptionClasses: + - LogicException + + ignoreErrors: + - # allow uncatched exceptions in tests + identifier: missingType.checkedException + path: tests/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7e2b669 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/FailureException.php b/src/FailureException.php new file mode 100644 index 0000000..497fa1c --- /dev/null +++ b/src/FailureException.php @@ -0,0 +1,10 @@ +}> $errors + * @throws FailureException + */ + public function inlineErrors( + array $errors + ): void + { + foreach ($errors as $filePath => $fileErrors) { + foreach ($fileErrors['messages'] as $error) { + $line = $error['line'] ?? null; + $identifier = $error['identifier'] ?? null; + $ignorable = $error['ignorable'] ?? null; + + if ($line === null || $identifier === null || $ignorable === false) { + continue; + } + + [$trueFilePath] = explode(' (in context of class', $filePath, 2); // solve trait "filepath" in format "src/App/MyTrait.php (in context of class App\Clazz)" + + $fileContent = $this->io->readFile($trueFilePath); + $lines = explode("\n", $fileContent); + + $lineContent = $lines[$line - 1]; + + $append = str_contains($lineContent, '// @phpstan-ignore ') + ? ', ' . $identifier + : ' // @phpstan-ignore ' . $identifier; + + $lines[$line - 1] .= $append; + $this->io->writeFile($trueFilePath, implode("\n", $lines)); + } + } + } + +} diff --git a/src/Io.php b/src/Io.php new file mode 100644 index 0000000..a2d8e0f --- /dev/null +++ b/src/Io.php @@ -0,0 +1,55 @@ +createMock(Io::class); + $ioMock->expects(self::exactly(2)) + ->method('writeFile') + ->willReturnCallback(static function (string $filePath, string $contents) use ($tmpFilePath): void { + self::assertNotFalse(file_put_contents($tmpFilePath, $contents)); + }); + $ioMock->expects(self::exactly(2)) + ->method('readFile') + ->willReturnCallback(static function (string $filePath) use ($tmpFilePath): string|false { + return file_get_contents($tmpFilePath); + }); + + $testJson = file_get_contents(__DIR__ . '/data/errors.json'); + $testData = json_decode($testJson, associative: true)['files']; // @phpstan-ignore argument.type + + $inliner = new InlineIgnoreInliner($ioMock); + $inliner->inlineErrors($testData); + + self::assertFileEquals(__DIR__ . '/data/test.fixed.php', $tmpFilePath); + } + +} diff --git a/tests/data/errors.json b/tests/data/errors.json new file mode 100644 index 0000000..c8dc886 --- /dev/null +++ b/tests/data/errors.json @@ -0,0 +1,26 @@ +{ + "totals": { + "errors": 0, + "file_errors": 2 + }, + "files": { + "tests/data/test.php": { + "errors": 2, + "messages": [ + { + "message": "Method Dummy::test() has parameter $b with no type specified.", + "line": 6, + "ignorable": true, + "identifier": "missingType.parameter" + }, + { + "message": "Method Dummy::test() with return type void returns null but should not return anything.", + "line": 8, + "ignorable": true, + "identifier": "return.void" + } + ] + } + }, + "errors": [] +} diff --git a/tests/data/test.fixed.php b/tests/data/test.fixed.php new file mode 100644 index 0000000..b44c76d --- /dev/null +++ b/tests/data/test.fixed.php @@ -0,0 +1,11 @@ +