From 8be74a175e2fa05c154fed5ec2de23d8da160f75 Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Tue, 27 Oct 2026 12:56:02 +0000 Subject: [PATCH] feat: update to use PHPStan Extension Installer --- README.md | 28 +++++++++++ composer.json | 27 +++++++++-- extension.php | 16 +++++++ larastan-strict-rules.neon | 46 ++++++++++++++++++ larastan.neon | 47 ------------------- phpstan.neon | 5 -- rector.php | 2 +- src/PHPStan/DisallowPHPUnitRule.php | 16 +++---- .../Generic/DisallowedAttributesRector.php | 11 ++++- src/WorksomeRectorConfig.php | 39 +++++++++++---- stubs/phpstan.neon.stub | 3 -- 11 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 extension.php create mode 100644 larastan-strict-rules.neon diff --git a/README.md b/README.md index 6e2bb1a..a705abe 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,34 @@ Add the following section to your `composer.json` file: }, ``` +### PHPStan + +PHPStan extensions are registered via [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), which is installed +alongside this package. Allow the plugin when prompted by Composer, or add it to your `composer.json` file: + +```json +"config": { + "allow-plugins": { + "phpstan/extension-installer": true + } +} +``` + +The Worksome rules, along with the PHPStan extensions this package depends on, are then loaded automatically. + +Laravel projects should also install Larastan and the Larastan strict rules: + +``` +composer require --dev larastan/larastan canvural/larastan-strict-rules +``` + +The Laravel rules (and Larastan within the Rector config) are only loaded when Larastan is installed, and the strict +rules are only loaded when both packages are installed. Packages listed in the extension installer's `ignore` option +are treated as not installed. + +> **Note:** Do not include the configuration files of any PHPStan extension that is registered with the extension installer (e.g. `vendor/larastan/larastan/extension.neon` +> or `vendor/worksome/coding-style/larastan.neon`) in your `phpstan.neon` file. PHPStan will fail if a file is included more than once. + ## Usage To use it, simply run one of the scripts added to composer. diff --git a/composer.json b/composer.json index 33fa01e..08de563 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,8 @@ "require": { "php": "^8.4", "composer-plugin-api": "^2.0", - "canvural/larastan-strict-rules": "^3.0", - "larastan/larastan": "^3.10", "laravel/pao": "^1.1", + "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-mockery": "^2.0", @@ -17,13 +16,21 @@ "symplify/easy-coding-standard": "^13.2.1" }, "require-dev": { + "canvural/larastan-strict-rules": "^3.0", "composer/composer": "^2.10", "friendsofphp/php-cs-fixer": "^3.95.5", + "larastan/larastan": "^3.10", "orchestra/testbench": "^11.1", "pestphp/pest": "^5.0", "squizlabs/php_codesniffer": "^4.0.1" }, + "suggest": { + "canvural/larastan-strict-rules": "Required for the Larastan strict rules in Laravel projects.", + "larastan/larastan": "Required for the Laravel PHPStan rules in Laravel projects." + }, "conflict": { + "canvural/larastan-strict-rules": "<3.0", + "larastan/larastan": "<3.10", "squizlabs/php_codesniffer": ">= 5" }, "autoload": { @@ -52,12 +59,24 @@ } ], "extra": { - "class": "Worksome\\CodingStyle\\Plugin" + "class": "Worksome\\CodingStyle\\Plugin", + "phpstan": { + "includes": [ + "extension.php" + ] + }, + "phpstan/extension-installer": { + "ignore": [ + "canvural/larastan-strict-rules", + "larastan/larastan" + ] + } }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true }, "sort-packages": true }, diff --git a/extension.php b/extension.php new file mode 100644 index 0000000..8956169 --- /dev/null +++ b/extension.php @@ -0,0 +1,16 @@ + [ + __DIR__ . '/phpstan.neon', + ...($larastan ? [__DIR__ . '/larastan.neon'] : []), + ...($larastanStrictRules ? [__DIR__ . '/larastan-strict-rules.neon'] : []), + ], +]; diff --git a/larastan-strict-rules.neon b/larastan-strict-rules.neon new file mode 100644 index 0000000..cd35b12 --- /dev/null +++ b/larastan-strict-rules.neon @@ -0,0 +1,46 @@ +includes: + - ../../canvural/larastan-strict-rules/rules.neon + +parameters: + larastanStrictRules: + allRules: false + noFacade: true + noValidationInController: true + scopeShouldReturnQueryBuilder: true + noPropertyAccessor: true + noGlobalLaravelFunction: true + allowedGlobalFunctions: + - class_basename + - class_uses_recursive + - e + - env + - object_get + - preg_replace_array + - retry + - str + - tap + - throw_if + - throw_unless + - trait_uses_recursive + - with + - collect + - data_fill + - data_get + - data_set + - value + - config + - fake + - method_field + - now + - old + - redirect + - response + - route + - trans + - trans_choice + - __ + - view + - url + - secure_url + - asset + - secure_asset diff --git a/larastan.neon b/larastan.neon index 739605f..c7e4fba 100644 --- a/larastan.neon +++ b/larastan.neon @@ -1,51 +1,4 @@ -includes: - - ./phpstan.neon - - ../../larastan/larastan/extension.neon - - ../../canvural/larastan-strict-rules/rules.neon - parameters: - larastanStrictRules: - allRules: false - noFacade: true - noValidationInController: true - scopeShouldReturnQueryBuilder: true - noPropertyAccessor: true - noGlobalLaravelFunction: true - allowedGlobalFunctions: - - class_basename - - class_uses_recursive - - e - - env - - object_get - - preg_replace_array - - retry - - str - - tap - - throw_if - - throw_unless - - trait_uses_recursive - - with - - collect - - data_fill - - data_get - - data_set - - value - - config - - fake - - method_field - - now - - old - - redirect - - response - - route - - trans - - trans_choice - - __ - - view - - url - - secure_url - - asset - - secure_asset worksomeLaravel: allRules: true disallowEnvironmentChecks: %worksomeLaravel.allRules% diff --git a/phpstan.neon b/phpstan.neon index 0a0dac1..e523064 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,3 @@ -includes: - - ../../phpstan/phpstan-deprecation-rules/rules.neon - - ../../phpstan/phpstan-mockery/extension.neon - - ../../spaze/phpstan-disallowed-calls/extension.neon - parameters: disallowedNamespaces: - namespace: 'JetBrains\PhpStorm\*' diff --git a/rector.php b/rector.php index b6594b7..84949ab 100644 --- a/rector.php +++ b/rector.php @@ -4,7 +4,7 @@ use Worksome\CodingStyle\WorksomeRectorConfig; -return WorksomeRectorConfig::configure(larastan: false) +return WorksomeRectorConfig::configure() ->withPaths([ __DIR__ . '/src', __DIR__ . '/tests', diff --git a/src/PHPStan/DisallowPHPUnitRule.php b/src/PHPStan/DisallowPHPUnitRule.php index 08c5384..6ae8a58 100644 --- a/src/PHPStan/DisallowPHPUnitRule.php +++ b/src/PHPStan/DisallowPHPUnitRule.php @@ -5,36 +5,34 @@ namespace Worksome\CodingStyle\PHPStan; use PhpParser\Node; -use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\TestCase; /** - * @implements Rule + * @implements Rule */ final class DisallowPHPUnitRule implements Rule { public function getNodeType(): string { - return Class_::class; + return InClassNode::class; } /** - * @param Class_ $node + * @param InClassNode $node */ public function processNode(Node $node, Scope $scope): array { - if ($node->isAbstract()) { - return []; - } + $classReflection = $node->getClassReflection(); - if ($node->namespacedName === null) { + if ($classReflection->isAbstract() || $classReflection->isAnonymous()) { return []; } - if (! is_subclass_of($node->namespacedName->toString(), TestCase::class)) { + if (! $classReflection->isSubclassOf(TestCase::class)) { return []; } diff --git a/src/Rector/Generic/DisallowedAttributesRector.php b/src/Rector/Generic/DisallowedAttributesRector.php index 61a4e54..1b24a9f 100644 --- a/src/Rector/Generic/DisallowedAttributesRector.php +++ b/src/Rector/Generic/DisallowedAttributesRector.php @@ -45,18 +45,27 @@ public function configure(array $configuration): void /** @param Node\AttributeGroup $node */ public function refactor(Node $node) { + $hasChanged = false; + foreach ($node->attrs as $key => $attribute) { if (! $this->isNames($attribute, $this->disallowedAttributes)) { continue; } unset($node->attrs[$key]); + $hasChanged = true; + } + + if (! $hasChanged) { + return null; } if ($node->attrs === []) { return NodeTraverser::REMOVE_NODE; } - return null; + $node->attrs = array_values($node->attrs); + + return $node; } } diff --git a/src/WorksomeRectorConfig.php b/src/WorksomeRectorConfig.php index f270d39..21a1092 100644 --- a/src/WorksomeRectorConfig.php +++ b/src/WorksomeRectorConfig.php @@ -2,6 +2,7 @@ namespace Worksome\CodingStyle; +use PHPStan\ExtensionInstaller\GeneratedConfig; use Rector\Config\RectorConfig; use Rector\Configuration\RectorConfigBuilder; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector; @@ -19,18 +20,18 @@ class WorksomeRectorConfig { - public static function configure(bool $larastan = true): RectorConfigBuilder + public static function configure(): RectorConfigBuilder { - $phpstanConfig = getcwd() . '/phpstan.neon'; + $larastan = class_exists( + GeneratedConfig::class + ) + ? GeneratedConfig::EXTENSIONS['larastan/larastan'] ?? null + : null; return RectorConfig::configure() - ->withPHPStanConfigs(match (true) { - file_exists($phpstanConfig) => [getcwd() . '/phpstan.neon'], - $larastan => [getcwd() . '/vendor/larastan/larastan/extension.neon'], - default => [], - }) + ->withPHPStanConfigs(self::phpstanExtensionConfigs()) ->withBootstrapFiles( - $larastan ? [getcwd() . '/vendor/larastan/larastan/bootstrap.php'] : [] + $larastan !== null ? [$larastan['install_path'] . '/bootstrap.php'] : [] ) ->withSets([ WorksomeSetList::GENERIC_CODE_QUALITY, @@ -50,4 +51,26 @@ public static function configure(bool $larastan = true): RectorConfigBuilder StrStartsWithRector::class, ]); } + + /** + * Rector does not load extensions registered by phpstan/extension-installer, so they are passed explicitly. + * + * @return list + */ + private static function phpstanExtensionConfigs(): array + { + if (! class_exists(GeneratedConfig::class)) { + return []; + } + + $configs = []; + + foreach (GeneratedConfig::EXTENSIONS as $extension) { + foreach ($extension['extra']['includes'] ?? [] as $include) { + $configs[] = $extension['install_path'] . '/' . $include; + } + } + + return $configs; + } } diff --git a/stubs/phpstan.neon.stub b/stubs/phpstan.neon.stub index 6c408df..8169c87 100644 --- a/stubs/phpstan.neon.stub +++ b/stubs/phpstan.neon.stub @@ -1,6 +1,3 @@ -includes: - - ./vendor/worksome/coding-style/larastan.neon - parameters: paths: - app