From 99848d82a72417cc70d3d57cbae0887040ca7fbe Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 11 Apr 2024 16:37:03 +0200 Subject: [PATCH 01/20] Add stub for Compound::getConstraints() --- extension.neon | 2 ++ .../Component/Validator/Constraints/Composite.stub | 9 +++++++++ .../Component/Validator/Constraints/Compound.stub | 14 ++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 stubs/Symfony/Component/Validator/Constraints/Composite.stub create mode 100644 stubs/Symfony/Component/Validator/Constraints/Compound.stub diff --git a/extension.neon b/extension.neon index 09837d28..1eb888ab 100644 --- a/extension.neon +++ b/extension.neon @@ -96,6 +96,8 @@ parameters: - stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub - stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub - stubs/Symfony/Component/Validator/Constraint.stub + - stubs/Symfony/Component/Validator/Constraints/Composite.stub + - stubs/Symfony/Component/Validator/Constraints/Compound.stub - stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub - stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub - stubs/Symfony/Contracts/Cache/CacheInterface.stub diff --git a/stubs/Symfony/Component/Validator/Constraints/Composite.stub b/stubs/Symfony/Component/Validator/Constraints/Composite.stub new file mode 100644 index 00000000..8344ea94 --- /dev/null +++ b/stubs/Symfony/Component/Validator/Constraints/Composite.stub @@ -0,0 +1,9 @@ + $options + * @return array + */ + abstract protected function getConstraints(array $options): array; +} From 3fbf634b2495adaa1dc4b7300293f8abf74128d8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 6 Mar 2024 17:17:02 +0100 Subject: [PATCH 02/20] Synchronize stub with symfony --- .../Component/Validator/ConstraintViolationInterface.stub | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub index e7f2b8a2..fd1c7b9b 100644 --- a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub +++ b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub @@ -3,7 +3,9 @@ namespace Symfony\Component\Validator; /** - * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. + * @method Constraint|null getConstraint() Returns the constraint whose validation caused the violation. Not implementing it is deprecated since Symfony 6.3. + * @method mixed getCause() Returns the cause of the violation. Not implementing it is deprecated since Symfony 6.2. + * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. */ interface ConstraintViolationInterface { From f4b9407fa3203aebafd422ae8f0eb1ef94659a80 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 13 Apr 2024 10:28:03 +0200 Subject: [PATCH 03/20] Synchronize stub with symfony --- stubs/Symfony/Component/Validator/Constraints/Compound.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/Symfony/Component/Validator/Constraints/Compound.stub b/stubs/Symfony/Component/Validator/Constraints/Compound.stub index 3227009b..6a36c6bf 100644 --- a/stubs/Symfony/Component/Validator/Constraints/Compound.stub +++ b/stubs/Symfony/Component/Validator/Constraints/Compound.stub @@ -7,7 +7,7 @@ use Symfony\Component\Validator\Constraint; abstract class Compound extends Composite { /** - * @param array $options + * @param array $options * @return array */ abstract protected function getConstraints(array $options): array; From 2c3d666889173e902714359d8216ec5627527d92 Mon Sep 17 00:00:00 2001 From: Benoit Viguier Date: Wed, 29 May 2024 10:32:08 +0200 Subject: [PATCH 04/20] Support for alias to inlined service --- src/Symfony/XmlServiceMapFactory.php | 9 +++++++-- tests/Symfony/DefaultServiceMapTest.php | 11 +++++++++++ tests/Symfony/container.xml | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index ad52c373..1cae5d97 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -48,11 +48,11 @@ public function create(): ServiceMap } $service = new Service( - strpos((string) $attrs->id, '.') === 0 ? substr((string) $attrs->id, 1) : (string) $attrs->id, + $this->cleanServiceId((string) $attrs->id), isset($attrs->class) ? (string) $attrs->class : null, isset($attrs->public) && (string) $attrs->public === 'true', isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', - isset($attrs->alias) ? (string) $attrs->alias : null + isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null ); if ($service->getAlias() !== null) { @@ -79,4 +79,9 @@ public function create(): ServiceMap return new DefaultServiceMap($services); } + private function cleanServiceId(string $id): string + { + return strpos($id, '.') === 0 ? substr($id, 1) : $id; + } + } diff --git a/tests/Symfony/DefaultServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php index dc8487e5..a0a27d98 100644 --- a/tests/Symfony/DefaultServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -113,6 +113,17 @@ static function (?Service $service): void { self::assertSame('withClass', $service->getAlias()); }, ]; + yield [ + 'aliasForInlined', + static function (?Service $service): void { + self::assertNotNull($service); + self::assertSame('aliasForInlined', $service->getId()); + self::assertNull($service->getClass()); + self::assertFalse($service->isPublic()); + self::assertFalse($service->isSynthetic()); + self::assertSame('inlined', $service->getAlias()); + }, + ]; } } diff --git a/tests/Symfony/container.xml b/tests/Symfony/container.xml index 4be3bd98..f456ab51 100644 --- a/tests/Symfony/container.xml +++ b/tests/Symfony/container.xml @@ -41,5 +41,7 @@ + + From af6ae0f4b91bc080265e80776af26da3e5befb28 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 30 May 2024 17:01:27 +0200 Subject: [PATCH 05/20] Update getArgument return type in interact method --- ...InterfaceGetArgumentDynamicReturnTypeExtension.php | 11 +++++++++++ tests/Type/Symfony/data/ExampleBaseCommand.php | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php index 0af7c88d..a8d3eb37 100644 --- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -10,11 +10,13 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function count; +use function in_array; final class InputInterfaceGetArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -76,6 +78,15 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } + $method = $scope->getFunction(); + if ( + $method instanceof MethodReflection + && $method->getName() === 'interact' + && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true) + ) { + $argTypes[] = new NullType(); + } + return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : null; } diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php index 0ab3a322..6c1f9422 100644 --- a/tests/Type/Symfony/data/ExampleBaseCommand.php +++ b/tests/Type/Symfony/data/ExampleBaseCommand.php @@ -17,6 +17,17 @@ protected function configure(): void $this->addArgument('base'); } + protected function interact(InputInterface $input, OutputInterface $output): int + { + assertType('string|null', $input->getArgument('base')); + assertType('string|null', $input->getArgument('aaa')); + assertType('string|null', $input->getArgument('bbb')); + assertType('array|string|null', $input->getArgument('diff')); + assertType('array|null', $input->getArgument('arr')); + assertType('string|null', $input->getArgument('both')); + assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question')); + } + protected function execute(InputInterface $input, OutputInterface $output): int { assertType('string|null', $input->getArgument('base')); From bca27f1701fc1a297749e6c2a1e3da4462c1a6af Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Jun 2024 15:55:20 +0200 Subject: [PATCH 06/20] Fix interact method inference --- ...nterfaceGetArgumentDynamicReturnTypeExtension.php | 12 ++++++++++-- tests/Type/Symfony/data/ExampleBaseCommand.php | 12 ++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php index a8d3eb37..860317f6 100644 --- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -57,6 +57,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $argName = $argStrings[0]->getValue(); $argTypes = []; + $canBeNullInInteract = false; foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { try { $command->mergeApplicationDefinition(); @@ -70,6 +71,8 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $argType = new StringType(); if (!$argument->isRequired()) { $argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault())); + } else { + $canBeNullInInteract = true; } } $argTypes[] = $argType; @@ -78,16 +81,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } + if (count($argTypes) === 0) { + return null; + } + $method = $scope->getFunction(); if ( - $method instanceof MethodReflection + $canBeNullInInteract + && $method instanceof MethodReflection && $method->getName() === 'interact' && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true) ) { $argTypes[] = new NullType(); } - return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : null; + return TypeCombinator::union(...$argTypes); } } diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php index 6c1f9422..d2357089 100644 --- a/tests/Type/Symfony/data/ExampleBaseCommand.php +++ b/tests/Type/Symfony/data/ExampleBaseCommand.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Symfony; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use function PHPStan\Testing\assertType; @@ -14,16 +15,18 @@ protected function configure(): void { parent::configure(); + $this->addArgument('required', InputArgument::REQUIRED); $this->addArgument('base'); } protected function interact(InputInterface $input, OutputInterface $output): int { assertType('string|null', $input->getArgument('base')); - assertType('string|null', $input->getArgument('aaa')); - assertType('string|null', $input->getArgument('bbb')); - assertType('array|string|null', $input->getArgument('diff')); - assertType('array|null', $input->getArgument('arr')); + assertType('string', $input->getArgument('aaa')); + assertType('string', $input->getArgument('bbb')); + assertType('string|null', $input->getArgument('required')); + assertType('array|string', $input->getArgument('diff')); + assertType('array', $input->getArgument('arr')); assertType('string|null', $input->getArgument('both')); assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question')); } @@ -33,6 +36,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int assertType('string|null', $input->getArgument('base')); assertType('string', $input->getArgument('aaa')); assertType('string', $input->getArgument('bbb')); + assertType('string', $input->getArgument('required')); assertType('array|string', $input->getArgument('diff')); assertType('array', $input->getArgument('arr')); assertType('string|null', $input->getArgument('both')); From 1bd7c339f622dfb5a1a97dcaf1a862734eabfa1d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 19 Jun 2024 21:29:16 +0200 Subject: [PATCH 07/20] Also support command argument and initialize method --- ...eGetArgumentDynamicReturnTypeExtension.php | 2 +- ...eHasArgumentDynamicReturnTypeExtension.php | 12 ++++++++++ .../Type/Symfony/data/ExampleBaseCommand.php | 23 ++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php index 860317f6..5eb4cfe9 100644 --- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -89,7 +89,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ( $canBeNullInInteract && $method instanceof MethodReflection - && $method->getName() === 'interact' + && ($method->getName() === 'interact' || $method->getName() === 'initialize') && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true) ) { $argTypes[] = new NullType(); diff --git a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php index 4ce49c4a..2ca6b4a1 100644 --- a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\TypeUtils; use function array_unique; use function count; +use function in_array; final class InputInterfaceHasArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -52,6 +53,17 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $argName = $argStrings[0]->getValue(); + if ($argName === 'command') { + $method = $scope->getFunction(); + if ( + $method instanceof MethodReflection + && ($method->getName() === 'interact' || $method->getName() === 'initialize') + && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true) + ) { + return null; + } + } + $returnTypes = []; foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { try { diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php index d2357089..0376429f 100644 --- a/tests/Type/Symfony/data/ExampleBaseCommand.php +++ b/tests/Type/Symfony/data/ExampleBaseCommand.php @@ -19,8 +19,26 @@ protected function configure(): void $this->addArgument('base'); } - protected function interact(InputInterface $input, OutputInterface $output): int + protected function initialize(InputInterface $input, OutputInterface $output): void { + assertType('bool', $input->hasArgument('command')); + assertType('string|null', $input->getArgument('command')); + + assertType('string|null', $input->getArgument('base')); + assertType('string', $input->getArgument('aaa')); + assertType('string', $input->getArgument('bbb')); + assertType('string|null', $input->getArgument('required')); + assertType('array|string', $input->getArgument('diff')); + assertType('array', $input->getArgument('arr')); + assertType('string|null', $input->getArgument('both')); + assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question')); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + assertType('bool', $input->hasArgument('command')); + assertType('string|null', $input->getArgument('command')); + assertType('string|null', $input->getArgument('base')); assertType('string', $input->getArgument('aaa')); assertType('string', $input->getArgument('bbb')); @@ -33,6 +51,9 @@ protected function interact(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int { + assertType('true', $input->hasArgument('command')); + assertType('string', $input->getArgument('command')); + assertType('string|null', $input->getArgument('base')); assertType('string', $input->getArgument('aaa')); assertType('string', $input->getArgument('bbb')); From e909a075d69e0d4db262ac3407350ae2c6b6ab5f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jul 2024 13:33:56 +0200 Subject: [PATCH 08/20] DiagnoseExtension --- composer.json | 2 +- extension.neon | 4 +++ src/Symfony/ConsoleApplicationResolver.php | 5 ++++ src/Symfony/SymfonyDiagnoseExtension.php | 29 ++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/SymfonyDiagnoseExtension.php diff --git a/composer.json b/composer.json index 2269d9a4..f132544d 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.11.7" }, "conflict": { "symfony/framework-bundle": "<3.0" diff --git a/extension.neon b/extension.neon index 1eb888ab..e58d668f 100644 --- a/extension.neon +++ b/extension.neon @@ -322,6 +322,10 @@ services: class: PHPStan\Symfony\PasswordAuthenticatedUserStubFilesExtension tags: - phpstan.stubFilesExtension + - + class: PHPStan\Symfony\SymfonyDiagnoseExtension + tags: + - phpstan.diagnoseExtension # FormInterface::getErrors() return type - diff --git a/src/Symfony/ConsoleApplicationResolver.php b/src/Symfony/ConsoleApplicationResolver.php index 51bd7960..52f5f4f0 100644 --- a/src/Symfony/ConsoleApplicationResolver.php +++ b/src/Symfony/ConsoleApplicationResolver.php @@ -27,6 +27,11 @@ public function __construct(Configuration $configuration) $this->consoleApplicationLoader = $configuration->getConsoleApplicationLoader(); } + public function hasConsoleApplicationLoader(): bool + { + return $this->consoleApplicationLoader !== null; + } + private function getConsoleApplication(): ?Application { if ($this->consoleApplicationLoader === null) { diff --git a/src/Symfony/SymfonyDiagnoseExtension.php b/src/Symfony/SymfonyDiagnoseExtension.php new file mode 100644 index 00000000..afd566dc --- /dev/null +++ b/src/Symfony/SymfonyDiagnoseExtension.php @@ -0,0 +1,29 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function print(Output $output): void + { + $output->writeLineFormatted(sprintf( + 'Symfony\'s consoleApplicationLoader: %s', + $this->consoleApplicationResolver->hasConsoleApplicationLoader() ? 'In use' : 'No' + )); + $output->writeLineFormatted(''); + } + +} From ee88a01bc48f608143d3376802ec952270737cb8 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 18 Jul 2024 12:19:10 +0200 Subject: [PATCH 09/20] Fix internal error when DIC param map has > 256 items --- .../ParameterDynamicReturnTypeExtension.php | 5 +- tests/Type/Symfony/container.xml | 259 ++++++++++++++++++ .../data/ExampleAbstractController.php | 1 + 3 files changed, 264 insertions(+), 1 deletion(-) diff --git a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php index 82d3599e..82535133 100644 --- a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -168,7 +169,9 @@ private function generalizeTypeFromValue(Scope $scope, $value): Type $valueTypes[] = $this->generalizeTypeFromValue($scope, $element); } - return new ConstantArrayType($keyTypes, $valueTypes); + return ConstantArrayTypeBuilder::createFromConstantArray( + new ConstantArrayType($keyTypes, $valueTypes) + )->getArray(); } return new ArrayType( diff --git a/tests/Type/Symfony/container.xml b/tests/Type/Symfony/container.xml index e1ef6d89..224c72db 100644 --- a/tests/Type/Symfony/container.xml +++ b/tests/Type/Symfony/container.xml @@ -82,6 +82,265 @@ value of b value of c + + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 + v11 + v12 + v13 + v14 + v15 + v16 + v17 + v18 + v19 + v20 + v21 + v22 + v23 + v24 + v25 + v26 + v27 + v28 + v29 + v30 + v31 + v32 + v33 + v34 + v35 + v36 + v37 + v38 + v39 + v40 + v41 + v42 + v43 + v44 + v45 + v46 + v47 + v48 + v49 + v50 + v51 + v52 + v53 + v54 + v55 + v56 + v57 + v58 + v59 + v60 + v61 + v62 + v63 + v64 + v65 + v66 + v67 + v68 + v69 + v70 + v71 + v72 + v73 + v74 + v75 + v76 + v77 + v78 + v79 + v80 + v81 + v82 + v83 + v84 + v85 + v86 + v87 + v88 + v89 + v90 + v91 + v92 + v93 + v94 + v95 + v96 + v97 + v98 + v99 + v100 + v101 + v102 + v103 + v104 + v105 + v106 + v107 + v108 + v109 + v110 + v111 + v112 + v113 + v114 + v115 + v116 + v117 + v118 + v119 + v120 + v121 + v122 + v123 + v124 + v125 + v126 + v127 + v128 + v129 + v130 + v131 + v132 + v133 + v134 + v135 + v136 + v137 + v138 + v139 + v140 + v141 + v142 + v143 + v144 + v145 + v146 + v147 + v148 + v149 + v150 + v151 + v152 + v153 + v154 + v155 + v156 + v157 + v158 + v159 + v160 + v161 + v162 + v163 + v164 + v165 + v166 + v167 + v168 + v169 + v170 + v171 + v172 + v173 + v174 + v175 + v176 + v177 + v178 + v179 + v180 + v181 + v182 + v183 + v184 + v185 + v186 + v187 + v188 + v189 + v190 + v191 + v192 + v193 + v194 + v195 + v196 + v197 + v198 + v199 + v200 + v201 + v202 + v203 + v204 + v205 + v206 + v207 + v208 + v209 + v210 + v211 + v212 + v213 + v214 + v215 + v216 + v217 + v218 + v219 + v220 + v221 + v222 + v223 + v224 + v225 + v226 + v227 + v228 + v229 + v230 + v231 + v232 + v233 + v234 + v235 + v236 + v237 + v238 + v239 + v240 + v241 + v242 + v243 + v244 + v245 + v246 + v247 + v248 + v249 + v250 + v251 + v252 + v253 + v254 + v255 + v256 + v257 + VGhpcyBpcyBhIEJlbGwgY2hhciAH Y-m-d\TH:i:sP diff --git a/tests/Type/Symfony/data/ExampleAbstractController.php b/tests/Type/Symfony/data/ExampleAbstractController.php index d9857a53..53b38066 100644 --- a/tests/Type/Symfony/data/ExampleAbstractController.php +++ b/tests/Type/Symfony/data/ExampleAbstractController.php @@ -80,6 +80,7 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType("array{a: string, b: string, c: string}", $container->getParameter('app.map')); assertType("array{a: string, b: string, c: string}", $parameterBag->get('app.map')); assertType("array{a: string, b: string, c: string}", $this->getParameter('app.map')); + assertType("non-falsy-string", implode(',', $this->getParameter('app.hugemap'))); assertType("string", $container->getParameter('app.binary')); assertType("string", $parameterBag->get('app.binary')); assertType("string", $this->getParameter('app.binary')); From 14eec8c011b856eee4d744a2a3f709db1e1858bd Mon Sep 17 00:00:00 2001 From: Zachary Lund Date: Mon, 12 Aug 2024 11:03:46 -0500 Subject: [PATCH 10/20] Add stub for AbstractController::createForm() --- extension.neon | 2 ++ .../Controller/AbstractController.stub | 24 +++++++++++++++++++ .../Service/ServiceSubscriberInterface.stub | 7 ++++++ tests/Type/Symfony/data/form_data_type.php | 18 ++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub create mode 100644 stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub diff --git a/extension.neon b/extension.neon index e58d668f..512f9908 100644 --- a/extension.neon +++ b/extension.neon @@ -27,6 +27,7 @@ parameters: - stubs/Psr/Cache/CacheException.stub - stubs/Psr/Cache/CacheItemInterface.stub - stubs/Psr/Cache/InvalidArgumentException.stub + - stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub - stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub - stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.stub - stubs/Symfony/Bundle/FrameworkBundle/Test/TestContainer.stub @@ -103,6 +104,7 @@ parameters: - stubs/Symfony/Contracts/Cache/CacheInterface.stub - stubs/Symfony/Contracts/Cache/CallbackInterface.stub - stubs/Symfony/Contracts/Cache/ItemInterface.stub + - stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub - stubs/Twig/Node/Node.stub parametersSchema: diff --git a/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub b/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub new file mode 100644 index 00000000..075dce6d --- /dev/null +++ b/stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub @@ -0,0 +1,24 @@ + + * @template TData + * + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) + */ + protected function createForm(string $type, $data = null, array $options = []): FormInterface + { + } +} diff --git a/stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub b/stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub new file mode 100644 index 00000000..8860e239 --- /dev/null +++ b/stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub @@ -0,0 +1,7 @@ +createForm(DataClassType::class, new DataClass()); + assertType('GenericFormDataType\DataClass', $form->getData()); + } + + public function doSomethingNullable(): void + { + $form = $this->createForm(DataClassType::class); + assertType('GenericFormDataType\DataClass|null', $form->getData()); + } + +} From 3cf113fa3e190ab9ab47b0aed46f32a36dfc2c7a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 5 Sep 2024 13:39:28 +0200 Subject: [PATCH 11/20] Fix stubs --- .../Component/EventDispatcher/EventDispatcherInterface.stub | 2 +- stubs/Symfony/Component/HttpFoundation/Cookie.stub | 4 ++-- .../Serializer/Exception/ExtraAttributesException.stub | 2 +- stubs/Symfony/Contracts/Cache/CacheInterface.stub | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub b/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub index e4ad8fc4..a58e43ca 100644 --- a/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub +++ b/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub @@ -10,5 +10,5 @@ interface EventDispatcherInterface * * @return TEvent */ - public function dispatch(object $event, string $eventName = null): object; + public function dispatch(object $event, ?string $eventName = null): object; } diff --git a/stubs/Symfony/Component/HttpFoundation/Cookie.stub b/stubs/Symfony/Component/HttpFoundation/Cookie.stub index 40ee45ab..cfb45fa3 100644 --- a/stubs/Symfony/Component/HttpFoundation/Cookie.stub +++ b/stubs/Symfony/Component/HttpFoundation/Cookie.stub @@ -21,7 +21,7 @@ class Cookie * * @throws \InvalidArgumentException */ - public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null); + public function __construct(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null); /** * @param string $name The name of the cookie @@ -36,7 +36,7 @@ class Cookie * * @throws \InvalidArgumentException */ - public function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null): self; + public function create(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): self; /** * @return self::SAMESITE_*|null diff --git a/stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub b/stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub index 18621931..b23f0d19 100644 --- a/stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub +++ b/stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub @@ -7,7 +7,7 @@ class ExtraAttributesException extends RuntimeException /** * @param string[] $extraAttributes */ - public function __construct(array $extraAttributes, \Throwable $previous = null) + public function __construct(array $extraAttributes, ?\Throwable $previous = null) { } diff --git a/stubs/Symfony/Contracts/Cache/CacheInterface.stub b/stubs/Symfony/Contracts/Cache/CacheInterface.stub index ff3027b1..a361ead4 100644 --- a/stubs/Symfony/Contracts/Cache/CacheInterface.stub +++ b/stubs/Symfony/Contracts/Cache/CacheInterface.stub @@ -15,5 +15,5 @@ interface CacheInterface * * @throws InvalidArgumentException */ - public function get(string $key, callable $callback, float $beta = null, array &$metadata = null); + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null); } From 42797536c3de1531769a725bfbb404f220ae0516 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 5 Sep 2024 18:13:37 +0200 Subject: [PATCH 12/20] Test newer PHP versions --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4453157..c4f7c922 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" @@ -93,6 +95,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -132,6 +136,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" From 30d088616836b108fbfc634211c2feae130ae42e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 5 Sep 2024 18:13:54 +0200 Subject: [PATCH 13/20] Pin build-cs --- .github/workflows/build.yml | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4f7c922..ad0554fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,6 +58,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "1.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index ecd8cfb2..b01b1537 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x composer install --working-dir build-cs .PHONY: cs From 51ab2438fb2695467cf96b58d2f8f28d4dd1e3e9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 5 Sep 2024 18:15:09 +0200 Subject: [PATCH 14/20] Require PHPStan 1.12 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f132544d..0d5cd41c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require": { "php": "^7.2 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^1.11.7" + "phpstan/phpstan": "^1.12" }, "conflict": { "symfony/framework-bundle": "<3.0" From f7d5782044bedf93aeb3f38e09c91148ee90e5a1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 26 Sep 2024 11:24:58 +0200 Subject: [PATCH 15/20] Fix InputBagTypeSpecifyingExtension --- src/Type/Symfony/InputBagTypeSpecifyingExtension.php | 2 +- tests/Type/Symfony/data/input_bag.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Type/Symfony/InputBagTypeSpecifyingExtension.php b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php index d3ad578c..3a4e54fc 100644 --- a/src/Type/Symfony/InputBagTypeSpecifyingExtension.php +++ b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php @@ -30,7 +30,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool { - return $methodReflection->getName() === self::HAS_METHOD_NAME && !$context->null(); + return $methodReflection->getName() === self::HAS_METHOD_NAME && $context->false(); } public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes diff --git a/tests/Type/Symfony/data/input_bag.php b/tests/Type/Symfony/data/input_bag.php index efd08461..744a7765 100644 --- a/tests/Type/Symfony/data/input_bag.php +++ b/tests/Type/Symfony/data/input_bag.php @@ -7,7 +7,8 @@ assertType('bool|float|int|string|null', $bag->get('foo')); if ($bag->has('foo')) { - assertType('bool|float|int|string', $bag->get('foo')); + // Because `has` rely on `array_key_exists` we can still have set the NULL value. + assertType('bool|float|int|string|null', $bag->get('foo')); assertType('bool|float|int|string|null', $bag->get('bar')); } else { assertType('null', $bag->get('foo')); From 270c2ee1478d1f8dc5121f539e890017bd64b04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Wed, 30 Oct 2024 13:01:49 +0100 Subject: [PATCH 16/20] Update Process.stub --- extension.neon | 1 + .../Process/Exception/LogicException.stub | 8 ++++++++ stubs/Symfony/Component/Process/Process.stub | 15 +++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 stubs/Symfony/Component/Process/Exception/LogicException.stub diff --git a/extension.neon b/extension.neon index 512f9908..4868bc2d 100644 --- a/extension.neon +++ b/extension.neon @@ -66,6 +66,7 @@ parameters: - stubs/Symfony/Component/Messenger/Envelope.stub - stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub - stubs/Symfony/Component/OptionsResolver/Options.stub + - stubs/Symfony/Component/Process/Exception/LogicException.stub - stubs/Symfony/Component/Process/Process.stub - stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub - stubs/Symfony/Component/PropertyAccess/Exception/ExceptionInterface.stub diff --git a/stubs/Symfony/Component/Process/Exception/LogicException.stub b/stubs/Symfony/Component/Process/Exception/LogicException.stub new file mode 100644 index 00000000..cb781d6a --- /dev/null +++ b/stubs/Symfony/Component/Process/Exception/LogicException.stub @@ -0,0 +1,8 @@ + */ class Process implements \IteratorAggregate { + /** + * @param int $flags + * + * @return \Generator + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIterator(int $flags = 0): \Generator + { + + } + } From c7b7e7f520893621558bfbfdb2694d4364565c1d Mon Sep 17 00:00:00 2001 From: Michael Telgmann Date: Wed, 6 Nov 2024 11:08:10 +0100 Subject: [PATCH 17/20] feat: Add @api annotation to interfaces The following interfaces are now part of the public API and can be safely relied on. - \PHPStan\Symfony\ParameterDefinition - \PHPStan\Symfony\ParameterMap - \PHPStan\Symfony\ServiceDefinition - \PHPStan\Symfony\ServiceMap --- src/Symfony/ParameterDefinition.php | 3 +++ src/Symfony/ParameterMap.php | 3 +++ src/Symfony/ServiceDefinition.php | 3 +++ src/Symfony/ServiceMap.php | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/Symfony/ParameterDefinition.php b/src/Symfony/ParameterDefinition.php index e1aa2eaa..1da7723b 100644 --- a/src/Symfony/ParameterDefinition.php +++ b/src/Symfony/ParameterDefinition.php @@ -2,6 +2,9 @@ namespace PHPStan\Symfony; +/** + * @api + */ interface ParameterDefinition { diff --git a/src/Symfony/ParameterMap.php b/src/Symfony/ParameterMap.php index ff0f5224..0c551635 100644 --- a/src/Symfony/ParameterMap.php +++ b/src/Symfony/ParameterMap.php @@ -5,6 +5,9 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +/** + * @api + */ interface ParameterMap { diff --git a/src/Symfony/ServiceDefinition.php b/src/Symfony/ServiceDefinition.php index c7cdcd18..6df34cba 100644 --- a/src/Symfony/ServiceDefinition.php +++ b/src/Symfony/ServiceDefinition.php @@ -2,6 +2,9 @@ namespace PHPStan\Symfony; +/** + * @api + */ interface ServiceDefinition { diff --git a/src/Symfony/ServiceMap.php b/src/Symfony/ServiceMap.php index 6665ede0..bbd2d8a3 100644 --- a/src/Symfony/ServiceMap.php +++ b/src/Symfony/ServiceMap.php @@ -5,6 +5,9 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +/** + * @api + */ interface ServiceMap { From dd1aaa7f85f9916222a2ce7e4d21072fe03958f4 Mon Sep 17 00:00:00 2001 From: bnowak Date: Sat, 4 Jan 2025 14:55:31 +0100 Subject: [PATCH 18/20] Support for Messenger HandleTrait return types --- extension.neon | 12 ++ src/Symfony/MessageMap.php | 24 +++ src/Symfony/MessageMapFactory.php | 154 ++++++++++++++++++ src/Symfony/Service.php | 13 +- src/Symfony/ServiceDefinition.php | 3 + src/Symfony/ServiceTag.php | 31 ++++ src/Symfony/ServiceTagDefinition.php | 13 ++ src/Symfony/XmlServiceMapFactory.php | 12 +- ...essengerHandleTraitReturnTypeExtension.php | 91 +++++++++++ tests/Type/Symfony/ExtensionTest.php | 1 + tests/Type/Symfony/container.xml | 18 ++ .../Symfony/data/messenger_handle_trait.php | 113 +++++++++++++ 12 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/MessageMap.php create mode 100644 src/Symfony/MessageMapFactory.php create mode 100644 src/Symfony/ServiceTag.php create mode 100644 src/Symfony/ServiceTagDefinition.php create mode 100644 src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php create mode 100644 tests/Type/Symfony/data/messenger_handle_trait.php diff --git a/extension.neon b/extension.neon index 4868bc2d..cbdfd73d 100644 --- a/extension.neon +++ b/extension.neon @@ -140,6 +140,13 @@ services: - factory: @symfony.parameterMapFactory::create() + # message map + symfony.messageMapFactory: + class: PHPStan\Symfony\MessageMapFactory + factory: PHPStan\Symfony\MessageMapFactory + - + factory: @symfony.messageMapFactory::create() + # ControllerTrait::get()/has() return type - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface) @@ -203,6 +210,11 @@ services: factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + # Messenger HandleTrait::handle() return type + - + class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension + tags: [phpstan.broker.expressionTypeResolverExtension] + # InputInterface::getArgument() return type - factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension diff --git a/src/Symfony/MessageMap.php b/src/Symfony/MessageMap.php new file mode 100644 index 00000000..7523742c --- /dev/null +++ b/src/Symfony/MessageMap.php @@ -0,0 +1,24 @@ + */ + private $messageMap; + + /** @param array $messageMap */ + public function __construct(array $messageMap) + { + $this->messageMap = $messageMap; + } + + public function getTypeForClass(string $class): ?Type + { + return $this->messageMap[$class] ?? null; + } + +} diff --git a/src/Symfony/MessageMapFactory.php b/src/Symfony/MessageMapFactory.php new file mode 100644 index 00000000..5c9ef152 --- /dev/null +++ b/src/Symfony/MessageMapFactory.php @@ -0,0 +1,154 @@ +serviceMap = $symfonyServiceMap; + $this->reflectionProvider = $reflectionProvider; + } + + public function create(): MessageMap + { + $returnTypesMap = []; + + foreach ($this->serviceMap->getServices() as $service) { + $serviceClass = $service->getClass(); + + if ($serviceClass === null) { + continue; + } + + foreach ($service->getTags() as $tag) { + if ($tag->getName() !== self::MESSENGER_HANDLER_TAG) { + continue; + } + + if (!$this->reflectionProvider->hasClass($serviceClass)) { + continue; + } + + $reflectionClass = $this->reflectionProvider->getClass($serviceClass); + + /** @var array{handles?: class-string, method?: string} $tagAttributes */ + $tagAttributes = $tag->getAttributes(); + + if (isset($tagAttributes['handles'])) { + $handles = [$tagAttributes['handles'] => ['method' => $tagAttributes['method'] ?? self::DEFAULT_HANDLER_METHOD]]; + } else { + $handles = $this->guessHandledMessages($reflectionClass); + } + + foreach ($handles as $messageClassName => $options) { + $methodName = $options['method'] ?? self::DEFAULT_HANDLER_METHOD; + + if (!$reflectionClass->hasNativeMethod($methodName)) { + continue; + } + + $methodReflection = $reflectionClass->getNativeMethod($methodName); + + foreach ($methodReflection->getVariants() as $variant) { + $returnTypesMap[$messageClassName][] = $variant->getReturnType(); + } + } + } + } + + $messageMap = []; + foreach ($returnTypesMap as $messageClassName => $returnTypes) { + if (count($returnTypes) !== 1) { + continue; + } + + $messageMap[$messageClassName] = $returnTypes[0]; + } + + return new MessageMap($messageMap); + } + + /** @return iterable> */ + private function guessHandledMessages(ClassReflection $reflectionClass): iterable + { + if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) { + $className = $reflectionClass->getName(); + + foreach ($className::getHandledMessages() as $index => $value) { + $containOptions = self::containOptions($index, $value); + if ($containOptions === true) { + yield $index => $value; + } elseif ($containOptions === false) { + yield $value => ['method' => self::DEFAULT_HANDLER_METHOD]; + } + } + + return; + } + + if (!$reflectionClass->hasNativeMethod(self::DEFAULT_HANDLER_METHOD)) { + return; + } + + $methodReflection = $reflectionClass->getNativeMethod(self::DEFAULT_HANDLER_METHOD); + + $variants = $methodReflection->getVariants(); + if (count($variants) !== 1) { + return; + } + + $parameters = $variants[0]->getParameters(); + + if (count($parameters) !== 1) { + return; + } + + $classNames = $parameters[0]->getType()->getObjectClassNames(); + + if (count($classNames) !== 1) { + return; + } + + yield $classNames[0] => ['method' => self::DEFAULT_HANDLER_METHOD]; + } + + /** + * @param mixed $index + * @param mixed $value + * @phpstan-assert-if-true =class-string $index + * @phpstan-assert-if-true =array $value + * @phpstan-assert-if-false =int $index + * @phpstan-assert-if-false =class-string $value + */ + private static function containOptions($index, $value): ?bool + { + if (is_string($index) && class_exists($index) && is_array($value)) { + return true; + } elseif (is_int($index) && is_string($value) && class_exists($value)) { + return false; + } + + return null; + } + +} diff --git a/src/Symfony/Service.php b/src/Symfony/Service.php index c31324f5..1cc465ac 100644 --- a/src/Symfony/Service.php +++ b/src/Symfony/Service.php @@ -20,12 +20,17 @@ final class Service implements ServiceDefinition /** @var string|null */ private $alias; + /** @var ServiceTag[] */ + private $tags; + + /** @param ServiceTag[] $tags */ public function __construct( string $id, ?string $class, bool $public, bool $synthetic, - ?string $alias + ?string $alias, + array $tags = [] ) { $this->id = $id; @@ -33,6 +38,7 @@ public function __construct( $this->public = $public; $this->synthetic = $synthetic; $this->alias = $alias; + $this->tags = $tags; } public function getId(): string @@ -60,4 +66,9 @@ public function getAlias(): ?string return $this->alias; } + public function getTags(): array + { + return $this->tags; + } + } diff --git a/src/Symfony/ServiceDefinition.php b/src/Symfony/ServiceDefinition.php index 6df34cba..3862fa8d 100644 --- a/src/Symfony/ServiceDefinition.php +++ b/src/Symfony/ServiceDefinition.php @@ -18,4 +18,7 @@ public function isSynthetic(): bool; public function getAlias(): ?string; + /** @return ServiceTag[] */ + public function getTags(): array; + } diff --git a/src/Symfony/ServiceTag.php b/src/Symfony/ServiceTag.php new file mode 100644 index 00000000..a8437fd1 --- /dev/null +++ b/src/Symfony/ServiceTag.php @@ -0,0 +1,31 @@ + */ + private $attributes; + + /** @param array $attributes */ + public function __construct(string $name, array $attributes = []) + { + $this->name = $name; + $this->attributes = $attributes; + } + + public function getName(): string + { + return $this->name; + } + + public function getAttributes(): array + { + return $this->attributes; + } + +} diff --git a/src/Symfony/ServiceTagDefinition.php b/src/Symfony/ServiceTagDefinition.php new file mode 100644 index 00000000..b0f66d9c --- /dev/null +++ b/src/Symfony/ServiceTagDefinition.php @@ -0,0 +1,13 @@ + */ + public function getAttributes(): array; + +} diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 1cae5d97..0c44207e 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -47,12 +47,22 @@ public function create(): ServiceMap continue; } + $serviceTags = []; + foreach ($def->tag as $tag) { + $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? []; + $tagName = $tagAttrs['name']; + unset($tagAttrs['name']); + + $serviceTags[] = new ServiceTag($tagName, $tagAttrs); + } + $service = new Service( $this->cleanServiceId((string) $attrs->id), isset($attrs->class) ? (string) $attrs->class : null, isset($attrs->public) && (string) $attrs->public === 'true', isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', - isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null + isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null, + $serviceTags ); if ($service->getAlias() !== null) { diff --git a/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php new file mode 100644 index 00000000..a5dce362 --- /dev/null +++ b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php @@ -0,0 +1,91 @@ +messageMapFactory = $symfonyMessageMapFactory; + } + + public function getType(Expr $expr, Scope $scope): ?Type + { + if ($this->isSupported($expr, $scope)) { + $args = $expr->getArgs(); + if (count($args) !== 1) { + return null; + } + + $arg = $args[0]->value; + $argClassNames = $scope->getType($arg)->getObjectClassNames(); + + if (count($argClassNames) === 1) { + $messageMap = $this->getMessageMap(); + $returnType = $messageMap->getTypeForClass($argClassNames[0]); + + if (!is_null($returnType)) { + return $returnType; + } + } + } + + return null; + } + + private function getMessageMap(): MessageMap + { + if ($this->messageMap === null) { + $this->messageMap = $this->messageMapFactory->create(); + } + + return $this->messageMap; + } + + /** + * @phpstan-assert-if-true =MethodCall $expr + */ + private function isSupported(Expr $expr, Scope $scope): bool + { + if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->name !== self::TRAIT_METHOD_NAME) { + return false; + } + + if (!$scope->isInClass()) { + return false; + } + + $reflectionClass = $scope->getClassReflection()->getNativeReflection(); + + if (!$reflectionClass->hasMethod(self::TRAIT_METHOD_NAME)) { + return false; + } + + $methodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); + $declaringClassReflection = $methodReflection->getBetterReflection()->getDeclaringClass(); + + return $declaringClassReflection->getName() === self::TRAIT_NAME; + } + +} diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index a076caac..40420be0 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -14,6 +14,7 @@ class ExtensionTest extends TypeInferenceTestCase /** @return mixed[] */ public function dataFileAsserts(): iterable { + yield from $this->gatherAssertTypes(__DIR__ . '/data/messenger_handle_trait.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/envelope_all.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/header_bag_get.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/response_header_bag_get_cookies.php'); diff --git a/tests/Type/Symfony/container.xml b/tests/Type/Symfony/container.xml index 224c72db..16d4b7fe 100644 --- a/tests/Type/Symfony/container.xml +++ b/tests/Type/Symfony/container.xml @@ -354,5 +354,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/Type/Symfony/data/messenger_handle_trait.php b/tests/Type/Symfony/data/messenger_handle_trait.php new file mode 100644 index 00000000..7a86d482 --- /dev/null +++ b/tests/Type/Symfony/data/messenger_handle_trait.php @@ -0,0 +1,113 @@ + ['method' => 'handleInt']; + yield FloatQuery::class => ['method' => 'handleFloat']; + yield StringQuery::class => ['method' => 'handleString']; + } + + public function __invoke(BooleanQuery $query): bool + { + return true; + } + + public function handleInt(IntQuery $query): int + { + return 0; + } + + public function handleFloat(FloatQuery $query): float + { + return 0.0; + } + + public function handleString(StringQuery $query): string + { + return 'string result'; + } +} + +class TaggedQuery {} +class TaggedResult {} +class TaggedHandler +{ + public function handle(TaggedQuery $query): TaggedResult + { + return new TaggedResult(); + } +} + +class MultiHandlesForInTheSameHandlerQuery {} +class MultiHandlesForInTheSameHandler implements MessageSubscriberInterface +{ + public static function getHandledMessages(): iterable + { + yield MultiHandlesForInTheSameHandlerQuery::class; + yield MultiHandlesForInTheSameHandlerQuery::class => ['priority' => '0']; + } + + public function __invoke(MultiHandlesForInTheSameHandlerQuery $query): bool + { + return true; + } +} + +class MultiHandlersForTheSameMessageQuery {} +class MultiHandlersForTheSameMessageHandler1 +{ + public function __invoke(MultiHandlersForTheSameMessageQuery $query): bool + { + return true; + } +} +class MultiHandlersForTheSameMessageHandler2 +{ + public function __invoke(MultiHandlersForTheSameMessageQuery $query): bool + { + return false; + } +} + +class HandleTraitClass { + use HandleTrait; + + public function __invoke() + { + assertType(RegularQueryResult::class, $this->handle(new RegularQuery())); + + assertType('bool', $this->handle(new BooleanQuery())); + assertType('int', $this->handle(new IntQuery())); + assertType('float', $this->handle(new FloatQuery())); + assertType('string', $this->handle(new StringQuery())); + + assertType(TaggedResult::class, $this->handle(new TaggedQuery())); + + // HandleTrait will throw exception in fact due to multiple handle methods/handlers per single query + assertType('mixed', $this->handle(new MultiHandlesForInTheSameHandlerQuery())); + assertType('mixed', $this->handle(new MultiHandlersForTheSameMessageQuery())); + } +} From 08b97ab6621a57d6bbb8add1a358c5bf25cd98df Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 19 Mar 2025 12:54:42 +0100 Subject: [PATCH 19/20] Synchronize the EventSubscriberInterface with the upstream type As of Symfony 5.4, Symfony ships a precise type for this interface but the stub overrides it. --- .../Component/EventDispatcher/EventSubscriberInterface.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub b/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub index 35a77c93..62474d10 100644 --- a/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub +++ b/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub @@ -5,7 +5,7 @@ namespace Symfony\Component\EventDispatcher; interface EventSubscriberInterface { /** - * @return array|array|array>> + * @return array> */ public static function getSubscribedEvents(); } From 78b6b5a62f56731d938031c8f59817ed83b2328a Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 19 Mar 2025 18:31:11 +0100 Subject: [PATCH 20/20] Remove the generic type for PasswordUpgraderInterface This interface method can be called on a user provider even for user types not supported by that provider (for instance when using a chain provider). The implementation is expected to deal with that case gracefully, which will be enforced by phpstan when the generics are gone. --- extension.neon | 5 --- ...ordAuthenticatedUserStubFilesExtension.php | 36 ------------------- .../PasswordAuthenticatedUserInterface.stub | 7 ---- .../Core/User/PasswordUpgraderInterface.stub | 14 -------- 4 files changed, 62 deletions(-) delete mode 100644 src/Symfony/PasswordAuthenticatedUserStubFilesExtension.php delete mode 100644 stubs/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.stub delete mode 100644 stubs/Symfony/Component/Security/Core/User/PasswordUpgraderInterface.stub diff --git a/extension.neon b/extension.neon index cbdfd73d..a38fd4bf 100644 --- a/extension.neon +++ b/extension.neon @@ -22,7 +22,6 @@ parameters: - Symfony\Component\Form\FormTypeInterface - Symfony\Component\OptionsResolver\Options - Symfony\Component\Security\Core\Authorization\Voter\Voter - - Symfony\Component\Security\Core\User\PasswordUpgraderInterface stubFiles: - stubs/Psr/Cache/CacheException.stub - stubs/Psr/Cache/CacheItemInterface.stub @@ -333,10 +332,6 @@ services: class: PHPStan\Symfony\InputBagStubFilesExtension tags: - phpstan.stubFilesExtension - - - class: PHPStan\Symfony\PasswordAuthenticatedUserStubFilesExtension - tags: - - phpstan.stubFilesExtension - class: PHPStan\Symfony\SymfonyDiagnoseExtension tags: diff --git a/src/Symfony/PasswordAuthenticatedUserStubFilesExtension.php b/src/Symfony/PasswordAuthenticatedUserStubFilesExtension.php deleted file mode 100644 index 8f8c4782..00000000 --- a/src/Symfony/PasswordAuthenticatedUserStubFilesExtension.php +++ /dev/null @@ -1,36 +0,0 @@ -reflector = $reflector; - } - - public function getFiles(): array - { - try { - $this->reflector->reflectClass('Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface'); - } catch (IdentifierNotFound $e) { - return []; - } - - return [ - __DIR__ . '/../../stubs/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.stub', - __DIR__ . '/../../stubs/Symfony/Component/Security/Core/User/PasswordUpgraderInterface.stub', - ]; - } - -} diff --git a/stubs/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.stub b/stubs/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.stub deleted file mode 100644 index 19cc6040..00000000 --- a/stubs/Symfony/Component/Security/Core/User/PasswordAuthenticatedUserInterface.stub +++ /dev/null @@ -1,7 +0,0 @@ -