diff --git a/.gitattributes b/.gitattributes index 7da7680..cd79451 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,5 @@ .php-cs-fixer.php export-ignore Makefile export-ignore phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml export-ignore diff --git a/README.md b/README.md index ef64a27..ef4b4dd 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,17 @@ This extension provides following features: -1. Provide correct return for `\Laminas\ServiceManager\ServiceLocatorInterface::get()` -1. Handle controller plugins that are called using magic `__call()` in subclasses of +1. Provide correct return type for `$container->get()` calls on containers of type +`\Laminas\ServiceManager\ServiceLocatorInterface`, `\Interop\Container\ContainerInterface` or `\Psr\Container\ContainerInterface` +2. Handle controller plugins that are called using magic `__call()` in subclasses of `\Laminas\Mvc\Controller\AbstractController` -1. Provide correct return type for `plugin` method of `AbstractController`, `FilterChain`, `PhpRenderer` and `ValidatorChain` -1. `getApplication()`, `getRenderer()`, `getRequest()` and `getResponse()` methods on Controllers, MvcEvents, View, +3. Provide correct return type for `plugin` method of `AbstractController`, `FilterChain`, `PhpRenderer` and `ValidatorChain` +4. `getApplication()`, `getRenderer()`, `getRequest()` and `getResponse()` methods on Controllers, MvcEvents, View, ViewEvent and Application returns the real instance instead of type-hinted interfaces -1. `getView()` method on `\Laminas\View\Helper\AbstractHelper` returns the real Renderer instance instead of type-hinted +5. `getView()` method on `\Laminas\View\Helper\AbstractHelper` returns the real Renderer instance instead of type-hinted interface -1. `\Laminas\Stdlib\ArrayObject` is configured as a [Universal object crate](https://phpstan.org/config-reference#universal-object-crates) -1. Handle `\Laminas\Stdlib\AbstractOptions` magic properties +6. `\Laminas\Stdlib\ArrayObject` is configured as a [Universal object crate](https://phpstan.org/config-reference#universal-object-crates) +7. Handle `\Laminas\Stdlib\AbstractOptions` magic properties ## Installation diff --git a/composer.json b/composer.json index bbb1f4c..9a13bf0 100644 --- a/composer.json +++ b/composer.json @@ -19,37 +19,37 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^0.12.88" + "phpstan/phpstan": "^0.12.99" }, "conflict": { - "laminas/laminas-cache": "<2.11", + "laminas/laminas-cache": "<2.13", "laminas/laminas-filter": "<2.11", - "laminas/laminas-form": "<2.16", - "laminas/laminas-hydrator": "<4.0", + "laminas/laminas-form": "<2.17", + "laminas/laminas-hydrator": "<4.3", "laminas/laminas-i18n": "<2.11", "laminas/laminas-inputfilter": "<2.12", "laminas/laminas-log": "<2.13", - "laminas/laminas-mail": "<2.14", + "laminas/laminas-mail": "<2.15", "laminas/laminas-mvc": "<3.2", "laminas/laminas-paginator": "<2.10", - "laminas/laminas-validator": "<2.14" + "laminas/laminas-validator": "<2.15" }, "require-dev": { - "laminas/laminas-cache": "^2.11.1", - "laminas/laminas-filter": "^2.11.0", - "laminas/laminas-form": "^2.16.3", - "laminas/laminas-hydrator": "^4.1.0", - "laminas/laminas-i18n": "^2.11.1", + "laminas/laminas-cache": "^2.13.0", + "laminas/laminas-filter": "^2.11.1", + "laminas/laminas-form": "^2.17.0", + "laminas/laminas-hydrator": "^4.3.1", + "laminas/laminas-i18n": "^2.11.2", "laminas/laminas-inputfilter": "^2.12.0", "laminas/laminas-log": "^2.13.1", - "laminas/laminas-mail": "^2.14.0", + "laminas/laminas-mail": "^2.15.0", "laminas/laminas-mvc": "^3.2.0", "laminas/laminas-paginator": "^2.10.0", - "laminas/laminas-validator": "^2.14.4", + "laminas/laminas-validator": "^2.15.0", "malukenho/mcbumpface": "^1.1.5", - "phpstan/phpstan-phpunit": "^0.12.19", - "phpunit/phpunit": "^9.5.4", - "slam/php-cs-fixer-extensions": "^v3.0.1", + "phpstan/phpstan-phpunit": "^0.12.22", + "phpunit/phpunit": "^9.5.9", + "slam/php-cs-fixer-extensions": "^v3.1.0", "slam/php-debug-r": "^v1.7.0" }, "extra": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..0b382c3 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#2 \\$args of static method PHPStan\\\\Reflection\\\\ParametersAcceptorSelector\\:\\:selectFromArgs\\(\\) expects array\\, array\\ given\\.$#" + count: 1 + path: src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php + diff --git a/phpstan.neon b/phpstan.neon index 13b0b4a..af6903b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon + - phpstan-baseline.neon parameters: level: max diff --git a/src/Rules/Laminas/ServiceManagerGetMethodCallRule.php b/src/Rules/Laminas/ServiceManagerGetMethodCallRule.php index cb5d5fd..aaef95d 100644 --- a/src/Rules/Laminas/ServiceManagerGetMethodCallRule.php +++ b/src/Rules/Laminas/ServiceManagerGetMethodCallRule.php @@ -4,16 +4,19 @@ namespace LaminasPhpStan\Rules\Laminas; +use Interop\Container\ContainerInterface as InteropContainerInterface; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\ServiceLocatorInterface; use LaminasPhpStan\ServiceManagerLoader; use LaminasPhpStan\Type\Laminas\ObjectServiceManagerType; use PhpParser\Node; +use PhpParser\Node\Arg; use PHPStan\Analyser\Scope; use PHPStan\Broker\Broker; use PHPStan\Rules\Rule; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; +use Psr\Container\ContainerInterface as PsrContainerInterface; use ReflectionClass; /** @@ -47,13 +50,17 @@ public function processNode(Node $node, Scope $scope): array return []; } - $argType = $scope->getType($node->args[0]->value); + $firstArg = $node->args[0]; + if (! $firstArg instanceof Arg) { + return []; + } + $argType = $scope->getType($firstArg->value); if (! $argType instanceof ConstantStringType) { return []; } $calledOnType = $scope->getType($node->var); - if (! $calledOnType instanceof ObjectType || ! $calledOnType->isInstanceOf(ServiceLocatorInterface::class)->yes()) { + if (! $calledOnType instanceof ObjectType || ! $this->isTypeInstanceOfContainer($calledOnType)) { return []; } @@ -97,4 +104,11 @@ public function processNode(Node $node, Scope $scope): array $classDoesNotExistNote )]; } + + private function isTypeInstanceOfContainer(ObjectType $type): bool + { + return $type->isInstanceOf(ServiceLocatorInterface::class)->yes() + || $type->isInstanceOf(InteropContainerInterface::class)->yes() + || $type->isInstanceOf(PsrContainerInterface::class)->yes(); + } } diff --git a/src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php b/src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php index 3ef831d..fbf5bfe 100644 --- a/src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php +++ b/src/Type/Laminas/PluginMethodDynamicReturnTypeExtension/AbstractPluginMethodDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ namespace LaminasPhpStan\Type\Laminas\PluginMethodDynamicReturnTypeExtension; use LaminasPhpStan\ServiceManagerLoader; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; @@ -35,7 +36,16 @@ final public function getTypeFromMethodCall( MethodCall $methodCall, Scope $scope ): Type { - $argType = $scope->getType($methodCall->args[0]->value); + $firstArg = $methodCall->args[0]; + if (! $firstArg instanceof Arg) { + throw new \PHPStan\ShouldNotHappenException(\sprintf( + 'Argument passed to %s::%s should be a string, %s given', + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName(), + $firstArg->getType() + )); + } + $argType = $scope->getType($firstArg->value); $strings = TypeUtils::getConstantStrings($argType); $plugin = 1 === \count($strings) ? $strings[0]->getValue() : null; diff --git a/src/Type/Laminas/ServiceManagerGetDynamicReturnTypeExtension.php b/src/Type/Laminas/ServiceManagerGetDynamicReturnTypeExtension.php index 6215260..e2283c2 100644 --- a/src/Type/Laminas/ServiceManagerGetDynamicReturnTypeExtension.php +++ b/src/Type/Laminas/ServiceManagerGetDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\ServiceLocatorInterface; use LaminasPhpStan\ServiceManagerLoader; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Broker\Broker; @@ -62,7 +63,16 @@ public function getTypeFromMethodCall( $serviceManager = $this->serviceManagerLoader->getServiceLocator($calledOnType->getClassName()); - $argType = $scope->getType($methodCall->args[0]->value); + $firstArg = $methodCall->args[0]; + if (! $firstArg instanceof Arg) { + throw new \PHPStan\ShouldNotHappenException(\sprintf( + 'Argument passed to %s::%s should be a string, %s given', + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName(), + $firstArg->getType() + )); + } + $argType = $scope->getType($firstArg->value); if (! $argType instanceof ConstantStringType) { if ($serviceManager instanceof AbstractPluginManager) { $refClass = new ReflectionClass($serviceManager); diff --git a/tests/Rules/Laminas/PluginManagerGetMethodCallRuleTest.php b/tests/Rules/Laminas/PluginManagerGetMethodCallRuleTest.php index 32c26cf..548cb2e 100644 --- a/tests/Rules/Laminas/PluginManagerGetMethodCallRuleTest.php +++ b/tests/Rules/Laminas/PluginManagerGetMethodCallRuleTest.php @@ -12,6 +12,7 @@ /** * @covers \LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule + * @extends RuleTestCase */ final class PluginManagerGetMethodCallRuleTest extends RuleTestCase { @@ -22,6 +23,9 @@ protected function setUp(): void $this->serviceManagerLoader = new ServiceManagerLoader(null); } + /** + * @return Rule<\PhpParser\Node\Expr\MethodCall> + */ protected function getRule(): Rule { return new ServiceManagerGetMethodCallRule($this->createBroker(), $this->serviceManagerLoader); diff --git a/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/InteropContainerFoo.php b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/InteropContainerFoo.php new file mode 100644 index 0000000..50eb3cc --- /dev/null +++ b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/InteropContainerFoo.php @@ -0,0 +1,43 @@ +container = $container; + } + + public function foo(): void + { + $this->container->get('non_existent_service'); + + $this->container->get('EventManager'); + $this->container->get('foo', 'bar'); + $this->container->get([]); + + $getterName = 'get'; + $this->container->{$getterName}('EventManager'); + $this->container->has('EventManager'); + + $stdClass = new stdClass(); + $stdClass->get('non_existent_service'); + + $this->container->get(ControllerManager::class); + $this->container->get(FormElementManager::class); + } + + public function get(string $foo): void + { + } +} diff --git a/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/PsrContainerFoo.php b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/PsrContainerFoo.php new file mode 100644 index 0000000..5c72844 --- /dev/null +++ b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/PsrContainerFoo.php @@ -0,0 +1,43 @@ +container = $container; + } + + public function foo(): void + { + $this->container->get('non_existent_service'); + + $this->container->get('EventManager'); + $this->container->get('foo', 'bar'); + $this->container->get([]); + + $getterName = 'get'; + $this->container->{$getterName}('EventManager'); + $this->container->has('EventManager'); + + $stdClass = new stdClass(); + $stdClass->get('non_existent_service'); + + $this->container->get(ControllerManager::class); + $this->container->get(FormElementManager::class); + } + + public function get(string $foo): void + { + } +} diff --git a/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/Foo.php b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/ServiceManagerFoo.php similarity index 97% rename from tests/Rules/Laminas/ServiceManagerGetMethodCallRule/Foo.php rename to tests/Rules/Laminas/ServiceManagerGetMethodCallRule/ServiceManagerFoo.php index 0649020..129e4e4 100644 --- a/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/Foo.php +++ b/tests/Rules/Laminas/ServiceManagerGetMethodCallRule/ServiceManagerFoo.php @@ -9,7 +9,7 @@ use Laminas\ServiceManager\ServiceManager; use stdClass; -final class Foo +final class ServiceManagerFoo { private ServiceManager $serviceManager; diff --git a/tests/Rules/Laminas/ServiceManagerGetMethodCallRuleTest.php b/tests/Rules/Laminas/ServiceManagerGetMethodCallRuleTest.php index 69a3a2c..df51955 100644 --- a/tests/Rules/Laminas/ServiceManagerGetMethodCallRuleTest.php +++ b/tests/Rules/Laminas/ServiceManagerGetMethodCallRuleTest.php @@ -4,14 +4,18 @@ namespace LaminasPhpStan\Tests\Rules\Laminas; +use Interop\Container\ContainerInterface as InteropContainerInterface; +use Laminas\ServiceManager\ServiceManager; use LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule; use LaminasPhpStan\ServiceManagerLoader; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use Psr\Container\ContainerInterface as PsrContainerInterface; /** * @covers \LaminasPhpStan\Rules\Laminas\ServiceManagerGetMethodCallRule * @covers \LaminasPhpStan\UnmappedAliasServiceLocatorProxy + * @extends RuleTestCase */ final class ServiceManagerGetMethodCallRuleTest extends RuleTestCase { @@ -22,16 +26,34 @@ protected function setUp(): void $this->serviceManagerLoader = new ServiceManagerLoader(null); } + /** + * @return string[][] + */ + public function provideContainerTypes(): array + { + return [ + 'ServiceManager' => ['ServiceManagerFoo.php', ServiceManager::class], + 'Interop container' => ['InteropContainerFoo.php', InteropContainerInterface::class], + 'PSR container' => ['PsrContainerFoo.php', PsrContainerInterface::class], + ]; + } + + /** + * @return Rule<\PhpParser\Node\Expr\MethodCall> + */ protected function getRule(): Rule { return new ServiceManagerGetMethodCallRule($this->createBroker(), $this->serviceManagerLoader); } - public function testRule(): void + /** + * @dataProvider provideContainerTypes + */ + public function testRule(string $filename, string $containerClassname): void { - $this->analyse([__DIR__ . '/ServiceManagerGetMethodCallRule/Foo.php'], [ + $this->analyse([__DIR__ . '/ServiceManagerGetMethodCallRule/' . $filename], [ [ - 'The service "non_existent_service" was not configured in Laminas\ServiceManager\ServiceManager.', + 'The service "non_existent_service" was not configured in ' . $containerClassname . '.', 23, ], ]);