diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58ae8be8..88543fb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.3.x" + - "2.0.x" jobs: lint: @@ -16,12 +16,12 @@ jobs: strategy: matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" @@ -56,6 +56,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -87,12 +88,12 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -126,12 +127,12 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 4c7990df..047fe906 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,13 +2,13 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '5 0 * * *' jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '31' diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 6a1c8156..1ba4fd77 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -10,7 +10,7 @@ jobs: toot: runs-on: ubuntu-latest steps: - - uses: cbrgm/mastodon-github-action@v1 + - uses: cbrgm/mastodon-github-action@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a8ac62..b8c96d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.1.0 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/LICENSE b/LICENSE index 0b9f74d9..cb2e557c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2017 Lukáš Unger +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index ecd8cfb2..1ee557df 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/2.x composer install --working-dir build-cs .PHONY: cs diff --git a/README.md b/README.md index 78f02320..25a155c1 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ parameters: # If you're using PHP config files for Symfony 5.3+, you also need this for auto-loading of `Symfony\Config`: scanDirectories: - var/cache/dev/Symfony/Config + # If you're using PHP config files (including the ones under packages/*.php) for Symfony 5.3+, + # you need this to load the helper functions (i.e. service(), env()): + scanFiles: + - vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php ``` ## Constant hassers diff --git a/composer.json b/composer.json index 7da1f489..c03d2c99 100644 --- a/composer.json +++ b/composer.json @@ -13,20 +13,19 @@ } ], "require": { - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^1.10.62" + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.11", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^8.5.29 || ^9.5", - "psr/container": "1.0 || 1.1.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.1.2", "symfony/config": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1", "symfony/dependency-injection": "^5.4 || ^6.1", diff --git a/extension.neon b/extension.neon index 1eb888ab..0803248f 100644 --- a/extension.neon +++ b/extension.neon @@ -5,28 +5,14 @@ parameters: uncheckedExceptionClasses: - 'Symfony\Component\Console\Exception\InvalidArgumentException' symfony: - container_xml_path: null containerXmlPath: null - constant_hassers: true constantHassers: true - console_application_loader: null consoleApplicationLoader: null - featureToggles: - skipCheckGenericClasses: - - Symfony\Component\Form\AbstractType - - Symfony\Component\Form\FormBuilderInterface - - Symfony\Component\Form\FormConfigBuilderInterface - - Symfony\Component\Form\FormConfigInterface - - Symfony\Component\Form\FormInterface - - Symfony\Component\Form\FormTypeExtensionInterface - - 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 - 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 @@ -65,6 +51,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 @@ -103,30 +90,29 @@ 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: symfony: structure([ - container_xml_path: schema(string(), nullable()) containerXmlPath: schema(string(), nullable()) - constant_hassers: bool() constantHassers: bool() - console_application_loader: schema(string(), nullable()) consoleApplicationLoader: schema(string(), nullable()) ]) services: - - - factory: PHPStan\Symfony\Configuration(%symfony%) - # console resolver - factory: PHPStan\Symfony\ConsoleApplicationResolver + arguments: + consoleApplicationLoader: %symfony.consoleApplicationLoader% # service map symfony.serviceMapFactory: class: PHPStan\Symfony\ServiceMapFactory factory: PHPStan\Symfony\XmlServiceMapFactory + arguments: + containerXmlPath: %symfony.containerXmlPath% - factory: @symfony.serviceMapFactory::create() @@ -134,21 +120,30 @@ services: symfony.parameterMapFactory: class: PHPStan\Symfony\ParameterMapFactory factory: PHPStan\Symfony\XmlParameterMapFactory + arguments: + containerXmlPath: %symfony.containerXmlPath% - 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) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # ControllerTrait::has() type specification @@ -200,6 +195,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 @@ -299,29 +299,29 @@ services: # ParameterBagInterface::get()/has() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has') + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # ContainerInterface::getParameter()/hasParameter() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter') + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter', %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # (Abstract)Controller::getParameter() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - class: PHPStan\Symfony\InputBagStubFilesExtension tags: - phpstan.stubFilesExtension - - class: PHPStan\Symfony\PasswordAuthenticatedUserStubFilesExtension + class: PHPStan\Symfony\SymfonyDiagnoseExtension tags: - - phpstan.stubFilesExtension + - phpstan.diagnoseExtension # FormInterface::getErrors() return type - @@ -359,3 +359,8 @@ services: - factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + - + class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0f6edd5c..79e87db9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,26 +1,79 @@ parameters: ignoreErrors: - - message: "#^Although PHPStan\\\\Reflection\\\\Php\\\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it's not guaranteed to always stay the same\\.$#" + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Rules/Symfony/UndefinedArgumentRule.php + + - + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Rules/Symfony/UndefinedOptionRule.php + + - + message: '#^Although PHPStan\\Reflection\\Php\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it''s not guaranteed to always stay the same\.$#' + identifier: phpstanApi.instanceofAssumption count: 1 path: src/Symfony/RequiredAutowiringExtension.php - - message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Console\\\\Input\\\\InputOption and 'isNegatable' will always evaluate to true\\.$#" + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php + + - + message: '#^Call to function method_exists\(\) with Symfony\\Component\\Console\\Input\\InputOption and ''isNegatable'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 path: src/Type/Symfony/GetOptionTypeHelper.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Methods\\\\CallMethodsRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php + + - + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php + + - + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php + + - + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php + + - + message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#' + identifier: method.internal + count: 1 + path: src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php + + - + message: '#^Accessing PHPStan\\Rules\\Methods\\CallMethodsRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/NonexistentInputBagClassTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\UninitializedPropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Properties\\UninitializedPropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Symfony/RequiredAutowiringExtensionTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeMethodCallRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index 5e10c2b8..96f1efea 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -19,8 +19,7 @@ final class ContainerInterfacePrivateServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; public function __construct(ServiceMap $symfonyServiceMap) { @@ -71,6 +70,7 @@ public function processNode(Node $node, Scope $scope): array if ($service !== null && !$service->isPublic()) { return [ RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId)) + ->identifier('symfonyContainer.privateService') ->build(), ]; } @@ -82,13 +82,13 @@ public function processNode(Node $node, Scope $scope): array private function isServiceSubscriber(Type $containerType, Scope $scope): TrinaryLogic { $serviceSubscriberInterfaceType = new ObjectType('Symfony\Contracts\Service\ServiceSubscriberInterface'); - $isContainerServiceSubscriber = $serviceSubscriberInterfaceType->isSuperTypeOf($containerType); + $isContainerServiceSubscriber = $serviceSubscriberInterfaceType->isSuperTypeOf($containerType)->result; $classReflection = $scope->getClassReflection(); if ($classReflection === null) { return $isContainerServiceSubscriber; } $containedClassType = new ObjectType($classReflection->getName()); - return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType)); + return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType)->result); } } diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php index ccc1999e..23444b6b 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -4,8 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ServiceMap; @@ -19,13 +19,11 @@ final class ContainerInterfaceUnknownServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; - /** @var Standard */ - private $printer; + private Printer $printer; - public function __construct(ServiceMap $symfonyServiceMap, Standard $printer) + public function __construct(ServiceMap $symfonyServiceMap, Printer $printer) { $this->serviceMap = $symfonyServiceMap; $this->printer = $printer; @@ -71,7 +69,9 @@ public function processNode(Node $node, Scope $scope): array $serviceIdType = $scope->getType($node->getArgs()[0]->value); if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { return [ - RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId))->build(), + RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId)) + ->identifier('symfonyContainer.serviceNotFound') + ->build(), ]; } } diff --git a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php index a55881ce..5435d4c9 100644 --- a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php +++ b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -46,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(2); // InputArgument::OPTIONAL } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -62,8 +61,8 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, %s given.', - $defaultType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), ]; } @@ -72,8 +71,8 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, %s given.', - $defaultType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php index 6964d25e..2e3dc0e9 100644 --- a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php +++ b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php @@ -15,7 +15,6 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -48,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(1); // InputOption::VALUE_NONE } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -67,8 +66,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects %s, %s given.', $checkType->describe(VerbosityLevel::typeOnly()), - $defaultType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), ]; } } @@ -78,8 +77,8 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, %s given.', - $defaultType->describe(VerbosityLevel::typeOnly()) - ))->build(), + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Symfony/UndefinedArgumentRule.php b/src/Rules/Symfony/UndefinedArgumentRule.php index 07f35e2a..ee36a23c 100644 --- a/src/Rules/Symfony/UndefinedArgumentRule.php +++ b/src/Rules/Symfony/UndefinedArgumentRule.php @@ -5,14 +5,13 @@ use InvalidArgumentException; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Type\ObjectType; use PHPStan\Type\Symfony\Helper; -use PHPStan\Type\TypeUtils; use function count; use function sprintf; @@ -22,13 +21,11 @@ final class UndefinedArgumentRule implements Rule { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var Standard */ - private $printer; + private Printer $printer; - public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Standard $printer) + public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer) { $this->consoleApplicationResolver = $consoleApplicationResolver; $this->printer = $printer; @@ -60,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array } $argType = $scope->getType($node->getArgs()[0]->value); - $argStrings = TypeUtils::getConstantStrings($argType); + $argStrings = $argType->getConstantStrings(); if (count($argStrings) !== 1) { return []; } @@ -75,7 +72,9 @@ public function processNode(Node $node, Scope $scope): array if ($scope->getType(Helper::createMarkerNode($node->var, $argType, $this->printer))->equals($argType)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define argument "%s".', $name, $argName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define argument "%s".', $name, $argName)) + ->identifier('symfonyConsole.argumentNotFound') + ->build(); } } diff --git a/src/Rules/Symfony/UndefinedOptionRule.php b/src/Rules/Symfony/UndefinedOptionRule.php index 5a8dc8e3..39a6a4ac 100644 --- a/src/Rules/Symfony/UndefinedOptionRule.php +++ b/src/Rules/Symfony/UndefinedOptionRule.php @@ -5,14 +5,13 @@ use InvalidArgumentException; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Type\ObjectType; use PHPStan\Type\Symfony\Helper; -use PHPStan\Type\TypeUtils; use function count; use function sprintf; @@ -22,13 +21,11 @@ final class UndefinedOptionRule implements Rule { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var Standard */ - private $printer; + private Printer $printer; - public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Standard $printer) + public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer) { $this->consoleApplicationResolver = $consoleApplicationResolver; $this->printer = $printer; @@ -60,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array } $optType = $scope->getType($node->getArgs()[0]->value); - $optStrings = TypeUtils::getConstantStrings($optType); + $optStrings = $optType->getConstantStrings(); if (count($optStrings) !== 1) { return []; } @@ -75,7 +72,9 @@ public function processNode(Node $node, Scope $scope): array if ($scope->getType(Helper::createMarkerNode($node->var, $optType, $this->printer))->equals($optType)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define option "%s".', $name, $optName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define option "%s".', $name, $optName)) + ->identifier('symfonyConsole.optionNotFound') + ->build(); } } diff --git a/src/Symfony/Configuration.php b/src/Symfony/Configuration.php deleted file mode 100644 index 4c1f1a31..00000000 --- a/src/Symfony/Configuration.php +++ /dev/null @@ -1,34 +0,0 @@ - */ - private $parameters; - - /** - * @param array $parameters - */ - public function __construct(array $parameters) - { - $this->parameters = $parameters; - } - - public function getContainerXmlPath(): ?string - { - return $this->parameters['containerXmlPath'] ?? $this->parameters['container_xml_path'] ?? null; - } - - public function hasConstantHassers(): bool - { - return $this->parameters['constantHassers'] ?? $this->parameters['constant_hassers'] ?? true; - } - - public function getConsoleApplicationLoader(): ?string - { - return $this->parameters['consoleApplicationLoader'] ?? $this->parameters['console_application_loader'] ?? null; - } - -} diff --git a/src/Symfony/ConsoleApplicationResolver.php b/src/Symfony/ConsoleApplicationResolver.php index 51bd7960..13b24d26 100644 --- a/src/Symfony/ConsoleApplicationResolver.php +++ b/src/Symfony/ConsoleApplicationResolver.php @@ -16,15 +16,18 @@ final class ConsoleApplicationResolver { - /** @var string|null */ - private $consoleApplicationLoader; + private ?string $consoleApplicationLoader = null; - /** @var Application|null */ - private $consoleApplication; + private ?Application $consoleApplication = null; - public function __construct(Configuration $configuration) + public function __construct(?string $consoleApplicationLoader) { - $this->consoleApplicationLoader = $configuration->getConsoleApplicationLoader(); + $this->consoleApplicationLoader = $consoleApplicationLoader; + } + + public function hasConsoleApplicationLoader(): bool + { + return $this->consoleApplicationLoader !== null; } private function getConsoleApplication(): ?Application diff --git a/src/Symfony/DefaultParameterMap.php b/src/Symfony/DefaultParameterMap.php index 26317468..3149fd7d 100644 --- a/src/Symfony/DefaultParameterMap.php +++ b/src/Symfony/DefaultParameterMap.php @@ -5,14 +5,13 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function array_map; final class DefaultParameterMap implements ParameterMap { /** @var ParameterDefinition[] */ - private $parameters; + private array $parameters; /** * @param ParameterDefinition[] $parameters @@ -37,11 +36,9 @@ public function getParameter(string $key): ?ParameterDefinition public static function getParameterKeysFromNode(Expr $node, Scope $scope): array { - $strings = TypeUtils::getConstantStrings($scope->getType($node)); + $strings = $scope->getType($node)->getConstantStrings(); - return array_map(static function (Type $type) { - return $type->getValue(); - }, $strings); + return array_map(static fn (Type $type) => $type->getValue(), $strings); } } diff --git a/src/Symfony/DefaultServiceMap.php b/src/Symfony/DefaultServiceMap.php index cb8259d2..5d3bccd0 100644 --- a/src/Symfony/DefaultServiceMap.php +++ b/src/Symfony/DefaultServiceMap.php @@ -4,14 +4,13 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\TypeUtils; use function count; final class DefaultServiceMap implements ServiceMap { /** @var ServiceDefinition[] */ - private $services; + private array $services; /** * @param ServiceDefinition[] $services @@ -36,7 +35,7 @@ public function getService(string $id): ?ServiceDefinition public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string { - $strings = TypeUtils::getConstantStrings($scope->getType($node)); + $strings = $scope->getType($node)->getConstantStrings(); return count($strings) === 1 ? $strings[0]->getValue() : null; } diff --git a/src/Symfony/InputBagStubFilesExtension.php b/src/Symfony/InputBagStubFilesExtension.php index 140dae99..6ce36f4b 100644 --- a/src/Symfony/InputBagStubFilesExtension.php +++ b/src/Symfony/InputBagStubFilesExtension.php @@ -9,8 +9,7 @@ class InputBagStubFilesExtension implements StubFilesExtension { - /** @var Reflector */ - private $reflector; + private Reflector $reflector; public function __construct( Reflector $reflector diff --git a/src/Symfony/MessageMap.php b/src/Symfony/MessageMap.php new file mode 100644 index 00000000..97bb8734 --- /dev/null +++ b/src/Symfony/MessageMap.php @@ -0,0 +1,24 @@ + */ + private array $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..3d7663ca --- /dev/null +++ b/src/Symfony/MessageMapFactory.php @@ -0,0 +1,152 @@ +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/Parameter.php b/src/Symfony/Parameter.php index 8ff3f7a8..53b53265 100644 --- a/src/Symfony/Parameter.php +++ b/src/Symfony/Parameter.php @@ -5,8 +5,7 @@ final class Parameter implements ParameterDefinition { - /** @var string */ - private $key; + private string $key; /** @var array|bool|float|int|string */ private $value; 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/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/src/Symfony/RequiredAutowiringExtension.php b/src/Symfony/RequiredAutowiringExtension.php index a3cc6a31..7d8d195d 100644 --- a/src/Symfony/RequiredAutowiringExtension.php +++ b/src/Symfony/RequiredAutowiringExtension.php @@ -13,8 +13,7 @@ class RequiredAutowiringExtension implements ReadWritePropertiesExtension, AdditionalConstructorsExtension { - /** @var FileTypeMapper */ - private $fileTypeMapper; + private FileTypeMapper $fileTypeMapper; public function __construct(FileTypeMapper $fileTypeMapper) { diff --git a/src/Symfony/Service.php b/src/Symfony/Service.php index c31324f5..44c0d1d7 100644 --- a/src/Symfony/Service.php +++ b/src/Symfony/Service.php @@ -5,27 +5,27 @@ final class Service implements ServiceDefinition { - /** @var string */ - private $id; + private string $id; - /** @var string|null */ - private $class; + private ?string $class = null; - /** @var bool */ - private $public; + private bool $public; - /** @var bool */ - private $synthetic; + private bool $synthetic; - /** @var string|null */ - private $alias; + private ?string $alias = null; + /** @var ServiceTag[] */ + private array $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 +33,7 @@ public function __construct( $this->public = $public; $this->synthetic = $synthetic; $this->alias = $alias; + $this->tags = $tags; } public function getId(): string @@ -60,4 +61,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 c7cdcd18..3862fa8d 100644 --- a/src/Symfony/ServiceDefinition.php +++ b/src/Symfony/ServiceDefinition.php @@ -2,6 +2,9 @@ namespace PHPStan\Symfony; +/** + * @api + */ interface ServiceDefinition { @@ -15,4 +18,7 @@ public function isSynthetic(): bool; public function getAlias(): ?string; + /** @return ServiceTag[] */ + public function getTags(): array; + } 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 { diff --git a/src/Symfony/ServiceTag.php b/src/Symfony/ServiceTag.php new file mode 100644 index 00000000..3b22ee34 --- /dev/null +++ b/src/Symfony/ServiceTag.php @@ -0,0 +1,30 @@ + */ + private array $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/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php new file mode 100644 index 00000000..8e2f8028 --- /dev/null +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -0,0 +1,62 @@ +parameterMap = $parameterMap; + $this->serviceMap = $serviceMap; + } + + public function getKey(): string + { + return 'symfonyDiContainer'; + } + + public function getHash(): string + { + $services = $parameters = []; + + foreach ($this->parameterMap->getParameters() as $parameter) { + $parameters[$parameter->getKey()] = $parameter->getValue(); + } + ksort($parameters); + + foreach ($this->serviceMap->getServices() as $service) { + $serviceTags = array_map( + static fn (ServiceTag $tag) => [ + 'name' => $tag->getName(), + 'attributes' => $tag->getAttributes(), + ], + $service->getTags(), + ); + sort($serviceTags); + + $services[$service->getId()] = [ + 'class' => $service->getClass(), + 'public' => $service->isPublic() ? 'yes' : 'no', + 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', + 'alias' => $service->getAlias(), + 'tags' => $serviceTags, + ]; + } + ksort($services); + + return hash('sha256', var_export(['parameters' => $parameters, 'services' => $services], true)); + } + +} diff --git a/src/Symfony/SymfonyDiagnoseExtension.php b/src/Symfony/SymfonyDiagnoseExtension.php new file mode 100644 index 00000000..38b19754 --- /dev/null +++ b/src/Symfony/SymfonyDiagnoseExtension.php @@ -0,0 +1,28 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function print(Output $output): void + { + $output->writeLineFormatted(sprintf( + 'Symfony\'s consoleApplicationLoader: %s', + $this->consoleApplicationResolver->hasConsoleApplicationLoader() ? 'In use' : 'No', + )); + $output->writeLineFormatted(''); + } + +} diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index 1467d050..4d3d3578 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -6,8 +6,10 @@ use PHPStan\ShouldNotHappenException; use SimpleXMLElement; use function base64_decode; +use function count; use function file_get_contents; use function is_numeric; +use function ksort; use function simplexml_load_string; use function sprintf; use function strpos; @@ -15,12 +17,11 @@ final class XmlParameterMapFactory implements ParameterMapFactory { - /** @var string|null */ - private $containerXml; + private ?string $containerXml = null; - public function __construct(Configuration $configuration) + public function __construct(?string $containerXmlPath) { - $this->containerXml = $configuration->getContainerXmlPath(); + $this->containerXml = $containerXmlPath; } public function create(): ParameterMap @@ -41,18 +42,23 @@ public function create(): ParameterMap /** @var Parameter[] $parameters */ $parameters = []; - foreach ($xml->parameters->parameter as $def) { - /** @var SimpleXMLElement $attrs */ - $attrs = $def->attributes(); - $parameter = new Parameter( - (string) $attrs->key, - $this->getNodeValue($def) - ); + if (count($xml->parameters) > 0) { + foreach ($xml->parameters->parameter as $def) { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); - $parameters[$parameter->getKey()] = $parameter; + $parameter = new Parameter( + (string) $attrs->key, + $this->getNodeValue($def), + ); + + $parameters[$parameter->getKey()] = $parameter; + } } + ksort($parameters); + return new DefaultParameterMap($parameters); } diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index ad52c373..ac79cb30 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -3,7 +3,9 @@ namespace PHPStan\Symfony; use SimpleXMLElement; +use function count; use function file_get_contents; +use function ksort; use function simplexml_load_string; use function sprintf; use function strpos; @@ -12,12 +14,11 @@ final class XmlServiceMapFactory implements ServiceMapFactory { - /** @var string|null */ - private $containerXml; + private ?string $containerXml = null; - public function __construct(Configuration $configuration) + public function __construct(?string $containerXmlPath) { - $this->containerXml = $configuration->getContainerXmlPath(); + $this->containerXml = $containerXmlPath; } public function create(): ServiceMap @@ -40,25 +41,38 @@ public function create(): ServiceMap $services = []; /** @var Service[] $aliases */ $aliases = []; - foreach ($xml->services->service as $def) { - /** @var SimpleXMLElement $attrs */ - $attrs = $def->attributes(); - if (!isset($attrs->id)) { - continue; - } - $service = new Service( - strpos((string) $attrs->id, '.') === 0 ? substr((string) $attrs->id, 1) : (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 - ); + if (count($xml->services) > 0) { + foreach ($xml->services->service as $def) { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); + if (!isset($attrs->id)) { + continue; + } + + $serviceTags = []; + foreach ($def->tag as $tag) { + $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? []; + $tagName = $tagAttrs['name']; + unset($tagAttrs['name']); + + $serviceTags[] = new ServiceTag($tagName, $tagAttrs); + } - if ($service->getAlias() !== null) { - $aliases[] = $service; - } else { - $services[$service->getId()] = $service; + $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, + $serviceTags, + ); + + if ($service->getAlias() !== null) { + $aliases[] = $service; + } else { + $services[$service->getId()] = $service; + } } } foreach ($aliases as $service) { @@ -72,11 +86,18 @@ public function create(): ServiceMap $services[$alias]->getClass(), $service->isPublic(), $service->isSynthetic(), - $alias + $alias, ); } + ksort($services); + return new DefaultServiceMap($services); } + private function cleanServiceId(string $id): string + { + return strpos($id, '.') === 0 ? substr($id, 1) : $id; + } + } diff --git a/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php b/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php index b57f3c35..5c21f021 100644 --- a/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php +++ b/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php @@ -3,25 +3,23 @@ namespace PHPStan\Type\Symfony; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\MethodTypeSpecifyingExtension; final class ArgumentTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var Standard */ - private $printer; + private Printer $printer; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; - public function __construct(Standard $printer) + public function __construct(Printer $printer) { $this->printer = $printer; } @@ -45,7 +43,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod return $this->typeSpecifier->create( Helper::createMarkerNode($node->var, $argType, $this->printer), $argType, - $context + $context, + $scope, ); } diff --git a/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php index ce736bfe..0862ce61 100644 --- a/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php @@ -34,7 +34,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), - $callbackReturnType->getCallableParametersAcceptors($scope) + $callbackReturnType->getCallableParametersAcceptors($scope), ); $returnType = $parametersAcceptor->getReturnType(); diff --git a/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php index 3b38258c..fba70cfd 100644 --- a/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use Throwable; use function count; use function get_class; @@ -18,8 +17,7 @@ final class CommandGetHelperDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -47,7 +45,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) !== 1) { return null; } diff --git a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php index 828b000e..1dae22e2 100644 --- a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php @@ -9,7 +9,6 @@ use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function count; use function in_array; @@ -55,14 +54,18 @@ public function getTypeFromMethodCall( { $calledOnType = $scope->getType($methodCall->var); - $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $defaultType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); if ($methodReflection->getName() === 'prototype') { if (!isset($methodCall->getArgs()[0])) { return $defaultType; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) { $type = $argStrings[0]->getValue(); @@ -72,7 +75,7 @@ public function getTypeFromMethodCall( return new ParentObjectType( $defaultType->describe(VerbosityLevel::typeOnly()), - $calledOnType + $calledOnType, ); } diff --git a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php index 6ef28bbd..800d9dbc 100644 --- a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php @@ -15,13 +15,14 @@ final class PassParentObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; /** @var string[] */ - private $methods; + private array $methods; /** + * @param class-string $className * @param string[] $methods */ public function __construct(string $className, array $methods) @@ -48,7 +49,11 @@ public function getTypeFromMethodCall( { $calledOnType = $scope->getType($methodCall->var); - $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $defaultType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); return new ParentObjectType($defaultType->describe(VerbosityLevel::typeOnly()), $calledOnType); } diff --git a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php index 4babdcd7..034d5d80 100644 --- a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType; use PHPStan\Type\Type; @@ -14,13 +13,14 @@ final class ReturnParentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; /** @var string[] */ - private $methods; + private array $methods; /** + * @param class-string $className * @param string[] $methods */ public function __construct(string $className, array $methods) @@ -43,14 +43,14 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $calledOnType = $scope->getType($methodCall->var); if ($calledOnType instanceof ParentObjectType) { return $calledOnType->getParent(); } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php index 8cf3170a..4f266c50 100644 --- a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Symfony\Config\ValueObject\TreeBuilderType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function count; final class TreeBuilderDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension @@ -47,7 +46,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $type = 'array'; if (isset($methodCall->getArgs()[1])) { - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[1]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[1]->value)->getConstantStrings(); if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) { $type = $argStrings[0]->getValue(); } diff --git a/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php index 4b7c1b3a..2be2b574 100644 --- a/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType; use PHPStan\Type\Symfony\Config\ValueObject\TreeBuilderType; @@ -28,20 +27,17 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $calledOnType = $scope->getType($methodCall->var); - - $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - if ($calledOnType instanceof TreeBuilderType) { return new ParentObjectType( $calledOnType->getRootNodeClassName(), - $calledOnType + $calledOnType, ); } - return $defaultType; + return null; } } diff --git a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php index 56aec24f..19baf926 100644 --- a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php +++ b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php @@ -9,8 +9,7 @@ class ParentObjectType extends ObjectType { - /** @var Type */ - private $parent; + private Type $parent; public function __construct(string $className, Type $parent) { diff --git a/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php index 71bc4fc2..93c713f1 100644 --- a/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php +++ b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php @@ -7,8 +7,7 @@ class TreeBuilderType extends ObjectType { - /** @var string */ - private $rootNodeClassName; + private string $rootNodeClassName; public function __construct(string $className, string $rootNodeClassName) { diff --git a/src/Type/Symfony/EnvelopeReturnTypeExtension.php b/src/Type/Symfony/EnvelopeReturnTypeExtension.php index 2e01dec7..06e08772 100644 --- a/src/Type/Symfony/EnvelopeReturnTypeExtension.php +++ b/src/Type/Symfony/EnvelopeReturnTypeExtension.php @@ -37,13 +37,13 @@ public function getTypeFromMethodCall( if (count($methodCall->getArgs()) === 0) { return new ArrayType( new GenericClassStringType(new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), - AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface'))) + TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType()), ); } $argType = $scope->getType($methodCall->getArgs()[0]->value); if (count($argType->getConstantStrings()) === 0) { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface'))); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType()); } $objectTypes = []; @@ -51,7 +51,7 @@ public function getTypeFromMethodCall( $objectTypes[] = new ObjectType($constantString->getValue()); } - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), TypeCombinator::union(...$objectTypes))); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), TypeCombinator::union(...$objectTypes)), new AccessoryArrayListType()); } } diff --git a/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php index 7af50773..d975d9d1 100644 --- a/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php +++ b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php @@ -19,8 +19,7 @@ class ExtensionGetConfigurationReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { diff --git a/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php index bdbc95ee..f80ddeb9 100644 --- a/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php @@ -41,10 +41,10 @@ public function getTypeFromMethodCall( $firstArgType = $scope->getType($methodCall->getArgs()[0]->value); $secondArgType = $scope->getType($methodCall->getArgs()[1]->value); - $firstIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType); - $firstIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType); - $secondIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($secondArgType); - $secondIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($secondArgType); + $firstIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result; + $firstIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result; + $secondIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($secondArgType)->result; + $secondIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($secondArgType)->result; $firstCompareType = $firstIsTrueType->compareTo($firstIsFalseType); $secondCompareType = $secondIsTrueType->compareTo($secondIsFalseType); diff --git a/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php b/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php index 5b05a711..f50dc4e8 100644 --- a/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -32,11 +31,11 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType); + $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result; + $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result; $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { @@ -48,7 +47,7 @@ public function getTypeFromMethodCall( return new ArrayType(new IntegerType(), new StringType()); } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Symfony/Helper.php b/src/Type/Symfony/Helper.php index 099b64c2..4aad820c 100644 --- a/src/Type/Symfony/Helper.php +++ b/src/Type/Symfony/Helper.php @@ -17,7 +17,7 @@ public static function createMarkerNode(Expr $expr, Type $type, PrettyPrinterAbs return new Expr\Variable(md5(sprintf( '%s::%s', $printer->prettyPrintExpr($expr), - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ))); } diff --git a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php index af35a548..75e6d0bc 100644 --- a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php @@ -37,7 +37,7 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { if ($methodReflection->getName() === 'get') { return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope); @@ -54,17 +54,21 @@ private function getGetTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { if (isset($methodCall->getArgs()[1])) { $argType = $scope->getType($methodCall->getArgs()[1]->value); $isNull = (new NullType())->isSuperTypeOf($argType); if ($isNull->no()) { - return TypeCombinator::removeNull(ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType()); + return TypeCombinator::removeNull(ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType()); } } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } private function getAllTypeFromMethodCall( diff --git a/src/Type/Symfony/InputBagTypeSpecifyingExtension.php b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php index d3ad578c..11cd39ef 100644 --- a/src/Type/Symfony/InputBagTypeSpecifyingExtension.php +++ b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php @@ -20,8 +20,7 @@ final class InputBagTypeSpecifyingExtension implements MethodTypeSpecifyingExten private const HAS_METHOD_NAME = 'has'; private const GET_METHOD_NAME = 'get'; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function getClass(): string { @@ -30,7 +29,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 @@ -38,7 +37,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod return $this->typeSpecifier->create( new MethodCall($node->var, self::GET_METHOD_NAME, $node->getArgs()), new NullType(), - $context->negate() + $context->negate(), + $scope, ); } diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php index 0af7c88d..88bd7b0e 100644 --- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -10,17 +10,17 @@ 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 { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -48,13 +48,14 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) !== 1) { return null; } $argName = $argStrings[0]->getValue(); $argTypes = []; + $canBeNullInInteract = false; foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { try { $command->mergeApplicationDefinition(); @@ -68,6 +69,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; @@ -76,7 +79,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } - return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : null; + if (count($argTypes) === 0) { + return null; + } + + $method = $scope->getFunction(); + if ( + $canBeNullInInteract + && $method instanceof MethodReflection + && ($method->getName() === 'interact' || $method->getName() === 'initialize') + && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true) + ) { + $argTypes[] = new NullType(); + } + + return TypeCombinator::union(...$argTypes); } } diff --git a/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php index d765f65b..6d0346cf 100644 --- a/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php @@ -10,17 +10,14 @@ use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; final class InputInterfaceGetOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var GetOptionTypeHelper */ - private $getOptionTypeHelper; + private GetOptionTypeHelper $getOptionTypeHelper; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper) { @@ -49,7 +46,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($optStrings) !== 1) { return null; } diff --git a/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php index 515c44d0..3105621d 100644 --- a/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php @@ -17,11 +17,9 @@ final class InputInterfaceGetOptionsDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var GetOptionTypeHelper */ - private $getOptionTypeHelper; + private GetOptionTypeHelper $getOptionTypeHelper; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper) { diff --git a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php index 4ce49c4a..34bffcea 100644 --- a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php @@ -10,15 +10,14 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function array_unique; use function count; +use function in_array; final class InputInterfaceHasArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -46,12 +45,23 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) !== 1) { return null; } $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/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php index 3be621f8..e4f8b5b1 100644 --- a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php @@ -10,15 +10,13 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function array_unique; use function count; final class InputInterfaceHasOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -46,7 +44,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($optStrings) !== 1) { return null; } diff --git a/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php index 5a2adae5..810de2f6 100644 --- a/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -30,11 +29,11 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType); + $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result; + $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result; $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { @@ -44,7 +43,7 @@ public function getTypeFromMethodCall( return new ArrayType(new IntegerType(), new StringType()); } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php new file mode 100644 index 00000000..2c7b1fbe --- /dev/null +++ b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php @@ -0,0 +1,89 @@ +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/src/Type/Symfony/OptionTypeSpecifyingExtension.php b/src/Type/Symfony/OptionTypeSpecifyingExtension.php index 98e0e3fd..8cd9dbd5 100644 --- a/src/Type/Symfony/OptionTypeSpecifyingExtension.php +++ b/src/Type/Symfony/OptionTypeSpecifyingExtension.php @@ -3,25 +3,23 @@ namespace PHPStan\Type\Symfony; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\MethodTypeSpecifyingExtension; final class OptionTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var Standard */ - private $printer; + private Printer $printer; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; - public function __construct(Standard $printer) + public function __construct(Printer $printer) { $this->printer = $printer; } @@ -45,7 +43,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod return $this->typeSpecifier->create( Helper::createMarkerNode($node->var, $argType, $this->printer), $argType, - $context + $context, + $scope, ); } diff --git a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php index 82d3599e..687b0c33 100644 --- a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php @@ -6,15 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\ParameterMap; 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; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; @@ -43,29 +41,27 @@ final class ParameterDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; - /** @var string|null */ - private $methodGet; + private ?string $methodGet = null; - /** @var string|null */ - private $methodHas; + private ?string $methodHas = null; - /** @var bool */ - private $constantHassers; + private bool $constantHassers; - /** @var ParameterMap */ - private $parameterMap; + private ParameterMap $parameterMap; - /** @var TypeStringResolver */ - private $typeStringResolver; + private TypeStringResolver $typeStringResolver; + /** + * @param class-string $className + */ public function __construct( string $className, ?string $methodGet, ?string $methodHas, - Configuration $configuration, + bool $constantHassers, ParameterMap $symfonyParameterMap, TypeStringResolver $typeStringResolver ) @@ -73,7 +69,7 @@ public function __construct( $this->className = $className; $this->methodGet = $methodGet; $this->methodHas = $methodHas; - $this->constantHassers = $configuration->hasConstantHassers(); + $this->constantHassers = $constantHassers; $this->parameterMap = $symfonyParameterMap; $this->typeStringResolver = $typeStringResolver; } @@ -85,14 +81,12 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - $methods = array_filter([$this->methodGet, $this->methodHas], static function (?string $method): bool { - return $method !== null; - }); + $methods = array_filter([$this->methodGet, $this->methodHas], static fn (?string $method): bool => $method !== null); return in_array($methodReflection->getName(), $methods, true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { switch ($methodReflection->getName()) { case $this->methodGet: @@ -168,16 +162,14 @@ 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( - TypeCombinator::union(...array_map(function ($item) use ($scope): Type { - return $this->generalizeTypeFromValue($scope, $item); - }, array_keys($value))), - TypeCombinator::union(...array_map(function ($item) use ($scope): Type { - return $this->generalizeTypeFromValue($scope, $item); - }, array_values($value))) + TypeCombinator::union(...array_map(fn ($item): Type => $this->generalizeTypeFromValue($scope, $item), array_keys($value))), + TypeCombinator::union(...array_map(fn ($item): Type => $this->generalizeTypeFromValue($scope, $item), array_values($value))), ); } @@ -204,7 +196,7 @@ private function generalizeType(Type $type): Type } return new ArrayType($this->generalizeType($type->getKeyType()), $this->generalizeType($type->getItemType())); } - if ($type instanceof ConstantType) { + if ($type->isConstantValue()->yes()) { return $type->generalize(GeneralizePrecision::lessSpecific()); } return $traverse($type); @@ -215,16 +207,15 @@ private function getHasTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) { - return $defaultReturnType; + return null; } $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope); if ($parameterKeys === []) { - return $defaultReturnType; + return null; } $has = null; @@ -237,7 +228,7 @@ private function getHasTypeFromMethodCall( ($has === true && $parameter === null) || ($has === false && $parameter !== null) ) { - return $defaultReturnType; + return null; } } diff --git a/src/Type/Symfony/RequestDynamicReturnTypeExtension.php b/src/Type/Symfony/RequestDynamicReturnTypeExtension.php index 284ddf01..fd0a3a00 100644 --- a/src/Type/Symfony/RequestDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/RequestDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ResourceType; @@ -29,15 +28,15 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { if (!isset($methodCall->getArgs()[0])) { return new StringType(); } $argType = $scope->getType($methodCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType)->result; + $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType)->result; $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new ResourceType(); @@ -46,7 +45,7 @@ public function getTypeFromMethodCall( return new StringType(); } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Symfony/RequestTypeSpecifyingExtension.php b/src/Type/Symfony/RequestTypeSpecifyingExtension.php index ffb2e8f3..40d38493 100644 --- a/src/Type/Symfony/RequestTypeSpecifyingExtension.php +++ b/src/Type/Symfony/RequestTypeSpecifyingExtension.php @@ -20,8 +20,7 @@ final class RequestTypeSpecifyingExtension implements MethodTypeSpecifyingExtens private const HAS_METHOD_NAME = 'hasSession'; private const GET_METHOD_NAME = 'getSession'; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function getClass(): string { @@ -45,7 +44,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod return $this->typeSpecifier->create( new MethodCall($node->var, self::GET_METHOD_NAME), TypeCombinator::removeNull($returnType), - $context + $context, + $scope, ); } diff --git a/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php b/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php index a659c684..5a95bf98 100644 --- a/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php @@ -52,9 +52,9 @@ public function getTypeFromMethodCall( new StringType(), new ArrayType( new StringType(), - new ObjectType(Cookie::class) - ) - ) + new ObjectType(Cookie::class), + ), + ), ); } } diff --git a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php index d3ea959d..84f256e4 100755 --- a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php @@ -17,12 +17,14 @@ class SerializerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $class; + /** @var class-string */ + private string $class; - /** @var string */ - private $method; + private string $method; + /** + * @param class-string $class + */ public function __construct(string $class, string $method) { $this->class = $class; diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php index b0101cf4..0667d30c 100644 --- a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php @@ -5,9 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\ParameterMap; use PHPStan\Symfony\ServiceDefinition; use PHPStan\Symfony\ServiceMap; @@ -23,30 +21,29 @@ final class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; - /** @var bool */ - private $constantHassers; + private bool $constantHassers; - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; - /** @var ParameterMap */ - private $parameterMap; + private ParameterMap $parameterMap; - /** @var ParameterBag|null */ - private $parameterBag; + private ?ParameterBag $parameterBag = null; + /** + * @param class-string $className + */ public function __construct( string $className, - Configuration $configuration, + bool $constantHassers, ServiceMap $symfonyServiceMap, ParameterMap $symfonyParameterMap ) { $this->className = $className; - $this->constantHassers = $configuration->hasConstantHassers(); + $this->constantHassers = $constantHassers; $this->serviceMap = $symfonyServiceMap; $this->parameterMap = $symfonyParameterMap; } @@ -61,7 +58,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return in_array($methodReflection->getName(), ['get', 'has'], true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { switch ($methodReflection->getName()) { case 'get': @@ -76,16 +73,15 @@ private function getGetTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); if (!isset($methodCall->getArgs()[0])) { - return $returnType; + return null; } $parameterBag = $this->tryGetParameterBag(); if ($parameterBag === null) { - return $returnType; + return null; } $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); @@ -96,7 +92,7 @@ private function getGetTypeFromMethodCall( } } - return $returnType; + return null; } private function tryGetParameterBag(): ?ParameterBag @@ -127,11 +123,10 @@ private function getHasTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) { - return $returnType; + return null; } $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); @@ -140,7 +135,7 @@ private function getHasTypeFromMethodCall( return new ConstantBooleanType($service !== null && $service->isPublic()); } - return $returnType; + return null; } private function determineServiceClass(ParameterBag $parameterBag, ServiceDefinition $service): ?string diff --git a/src/Type/Symfony/ServiceTypeSpecifyingExtension.php b/src/Type/Symfony/ServiceTypeSpecifyingExtension.php index b52c1531..dd767ccb 100644 --- a/src/Type/Symfony/ServiceTypeSpecifyingExtension.php +++ b/src/Type/Symfony/ServiceTypeSpecifyingExtension.php @@ -3,28 +3,29 @@ namespace PHPStan\Type\Symfony; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\MethodTypeSpecifyingExtension; final class ServiceTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; - /** @var Standard */ - private $printer; + private Printer $printer; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; - public function __construct(string $className, Standard $printer) + /** + * @param class-string $className + */ + public function __construct(string $className, Printer $printer) { $this->className = $className; $this->printer = $printer; @@ -49,7 +50,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod return $this->typeSpecifier->create( Helper::createMarkerNode($node->var, $argType, $this->printer), $argType, - $context + $context, + $scope, ); } 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/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/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(); } 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/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 + { + + } + } 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 @@ -create()); + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(null))->create()); } public function testGetPrivateService(): void @@ -29,7 +28,7 @@ public function testGetPrivateService(): void [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } @@ -42,7 +41,7 @@ public function testGetPrivateServiceInAbstractController(): void [ __DIR__ . '/ExampleAbstractController.php', ], - [] + [], ); } @@ -62,7 +61,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -82,7 +81,7 @@ public function testGetPrivateServiceInServiceSubscriber(): void __DIR__ . '/ExampleServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleServiceSubscriberFromLegacyController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php index 51513b09..dfa3d2b7 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules\Symfony; use PHPStan\Rules\Rule; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; use function class_exists; @@ -17,7 +16,7 @@ final class ContainerInterfacePrivateServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/container.xml'])))->create()); + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create()); } public function testGetPrivateService(): void @@ -34,7 +33,7 @@ public function testGetPrivateService(): void 'Service "private" is private.', 13, ], - ] + ], ); } @@ -54,7 +53,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -74,7 +73,7 @@ public function testGetPrivateServiceInServiceSubscriber(): void __DIR__ . '/ExampleServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleServiceSubscriberFromLegacyController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php index f975a179..8d70f1c3 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php @@ -2,13 +2,13 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use function class_exists; /** @@ -19,7 +19,7 @@ final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new Configuration([])))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), self::getContainer()->getByType(Printer::class)); } /** @@ -28,7 +28,7 @@ protected function getRule(): Rule protected function getMethodTypeSpecifyingExtensions(): array { return [ - new ServiceTypeSpecifyingExtension('Symfony\Bundle\FrameworkBundle\Controller\Controller', new Standard()), + new ServiceTypeSpecifyingExtension(AbstractController::class, self::getContainer()->getByType(Printer::class)), ]; } @@ -41,7 +41,7 @@ public function testGetPrivateService(): void [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } @@ -55,7 +55,7 @@ public function testGetPrivateServiceInAbstractController(): void [ __DIR__ . '/ExampleAbstractController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index 4bd233e9..c975750f 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -2,9 +2,8 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; use function class_exists; @@ -18,7 +17,7 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/container.xml'])))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), self::getContainer()->getByType(Printer::class)); } public function testGetPrivateService(): void @@ -35,7 +34,7 @@ public function testGetPrivateService(): void 'Service "unknown" is not registered in the container.', 25, ], - ] + ], ); } @@ -54,7 +53,7 @@ public function testGetPrivateServiceInAbstractController(): void 'Service "unknown" is not registered in the container.', 25, ], - ] + ], ); } @@ -68,7 +67,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void [ __DIR__ . '/ExampleServiceSubscriber.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ExampleServiceSubscriber.php b/tests/Rules/Symfony/ExampleServiceSubscriber.php index 9005cf60..ec9c966d 100644 --- a/tests/Rules/Symfony/ExampleServiceSubscriber.php +++ b/tests/Rules/Symfony/ExampleServiceSubscriber.php @@ -9,8 +9,7 @@ final class ExampleServiceSubscriber implements ServiceSubscriberInterface { - /** @var ContainerInterface */ - private $locator; + private ContainerInterface $locator; public function __construct(ContainerInterface $locator) { diff --git a/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php index 5e4f2ee6..bc6a6563 100644 --- a/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php +++ b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php @@ -35,7 +35,7 @@ public function testGetArgument(): void 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, array given.', 25, ], - ] + ], ); } diff --git a/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php b/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php index 3e5e03d4..2dcbbcd1 100644 --- a/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php +++ b/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php @@ -27,7 +27,7 @@ public function testGetArgument(): void 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, array given.', 29, ], - ] + ], ); } diff --git a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php index 95f4b733..d9970ef6 100644 --- a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php +++ b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php @@ -2,9 +2,8 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Testing\RuleTestCase; @@ -16,7 +15,7 @@ final class UndefinedArgumentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedArgumentRule(new ConsoleApplicationResolver(new Configuration(['consoleApplicationLoader' => __DIR__ . '/console_application_loader.php'])), new Standard()); + return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); } public function testGetArgument(): void @@ -30,7 +29,7 @@ public function testGetArgument(): void 'Command "example-rule" does not define argument "undefined".', 42, ], - ] + ], ); } diff --git a/tests/Rules/Symfony/UndefinedOptionRuleTest.php b/tests/Rules/Symfony/UndefinedOptionRuleTest.php index b3cbe0ef..7f759213 100644 --- a/tests/Rules/Symfony/UndefinedOptionRuleTest.php +++ b/tests/Rules/Symfony/UndefinedOptionRuleTest.php @@ -2,9 +2,8 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; -use PHPStan\Symfony\Configuration; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Testing\RuleTestCase; @@ -16,7 +15,7 @@ final class UndefinedOptionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedOptionRule(new ConsoleApplicationResolver(new Configuration(['consoleApplicationLoader' => __DIR__ . '/console_application_loader.php'])), new Standard()); + return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); } public function testGetArgument(): void @@ -30,7 +29,7 @@ public function testGetArgument(): void 'Command "example-rule" does not define option "bbb".', 49, ], - ] + ], ); } diff --git a/tests/Symfony/DefaultParameterMapTest.php b/tests/Symfony/DefaultParameterMapTest.php index 8e2e5b58..018a68a9 100644 --- a/tests/Symfony/DefaultParameterMapTest.php +++ b/tests/Symfony/DefaultParameterMapTest.php @@ -13,13 +13,13 @@ final class DefaultParameterMapTest extends TestCase */ public function testGetParameter(string $key, callable $validator): void { - $factory = new XmlParameterMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/container.xml'])); + $factory = new XmlParameterMapFactory(__DIR__ . '/container.xml'); $validator($factory->create()->getParameter($key)); } public function testGetParameterEscapedPath(): void { - $factory = new XmlParameterMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/containers/bugfix%2Fcontainer.xml'])); + $factory = new XmlParameterMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); $serviceMap = $factory->create(); self::assertNotNull($serviceMap->getParameter('app.string')); diff --git a/tests/Symfony/DefaultServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php index dc8487e5..b43bee49 100644 --- a/tests/Symfony/DefaultServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -13,13 +13,13 @@ final class DefaultServiceMapTest extends TestCase */ public function testGetService(string $id, callable $validator): void { - $factory = new XmlServiceMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/container.xml'])); + $factory = new XmlServiceMapFactory(__DIR__ . '/container.xml'); $validator($factory->create()->getService($id)); } public function testGetContainerEscapedPath(): void { - $factory = new XmlServiceMapFactory(new Configuration(['containerXmlPath' => __DIR__ . '/containers/bugfix%2Fcontainer.xml'])); + $factory = new XmlServiceMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); $serviceMap = $factory->create(); self::assertNotNull($serviceMap->getService('withClass')); @@ -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/SymfonyContainerResultCacheMetaExtensionTest.php b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php new file mode 100644 index 00000000..f5c8503f --- /dev/null +++ b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php @@ -0,0 +1,294 @@ + $sameHashContents + * @param ContainerContents $invalidatingContent + * + * @dataProvider provideContainerHashIsCalculatedCorrectlyCases + */ + public function testContainerHashIsCalculatedCorrectly( + array $sameHashContents, + array $invalidatingContent + ): void + { + $hash = null; + + self::assertGreaterThan(0, count($sameHashContents)); + + foreach ($sameHashContents as $content) { + $currentHash = (new SymfonyContainerResultCacheMetaExtension( + $content['parameters'] ?? new DefaultParameterMap([]), + $content['services'] ?? new DefaultServiceMap([]), + ))->getHash(); + + if ($hash === null) { + $hash = $currentHash; + } else { + self::assertSame($hash, $currentHash); + } + } + + self::assertNotSame( + $hash, + (new SymfonyContainerResultCacheMetaExtension( + $invalidatingContent['parameters'] ?? new DefaultParameterMap([]), + $invalidatingContent['services'] ?? new DefaultServiceMap([]), + ))->getHash(), + ); + } + + /** + * @return iterable, ContainerContents}> + */ + public static function provideContainerHashIsCalculatedCorrectlyCases(): iterable + { + yield 'service "class" changes' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), + ], + // Swapping services order in XML file does not affect the calculated hash + [ + 'services' => new DefaultServiceMap([ + new Service('Bar', 'Bar', true, false, null), + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'BarAdapter', true, false, null), + ]), + ], + ]; + + yield 'service visibility changes' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', false, false, null), + ]), + ], + ]; + + yield 'service syntheticity changes' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, true, null), + ]), + ], + ]; + + yield 'service alias changes' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + new Service('Baz', null, true, false, 'Foo'), + ]), + ], + // Swapping services order in XML file does not affect the calculated hash + [ + 'services' => new DefaultServiceMap([ + new Service('Baz', null, true, false, 'Foo'), + new Service('Bar', 'Bar', true, false, null), + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + new Service('Baz', null, true, false, 'Bar'), + ]), + ], + ]; + + yield 'service tag attributes changes' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.baz', ['baz' => 'baz']), + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'buzz']), + ]), + ]), + ], + ]; + + yield 'service tag added' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), + ], + ]; + + yield 'service tag removed' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), + ], + ]; + + yield 'new service added' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), + ], + ]; + + yield 'service removed' => [ + [ + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ]; + + yield 'parameter value changes' => [ + [ + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('bar', 'bar'), + new Parameter('foo', 'foo'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'buzz'), + ]), + ], + ]; + + yield 'new parameter added' => [ + [ + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), + ], + ]; + + yield 'parameter removed' => [ + [ + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + ]), + ], + ]; + } + +} diff --git a/tests/Symfony/config.neon b/tests/Symfony/config.neon deleted file mode 100644 index e4f42c64..00000000 --- a/tests/Symfony/config.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - symfony: - container_xml_path: container.xml 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 @@ + + 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/console_application_loader.php b/tests/Type/Symfony/console_application_loader.php index b6755aea..fb5459b5 100644 --- a/tests/Type/Symfony/console_application_loader.php +++ b/tests/Type/Symfony/console_application_loader.php @@ -15,9 +15,7 @@ $application->add(new ExampleOptionCommand()); if (class_exists(LazyCommand::class)) { - $application->add(new LazyCommand('lazy-example-option', [], '', false, static function () { - return new ExampleOptionLazyCommand(); - })); + $application->add(new LazyCommand('lazy-example-option', [], '', false, static fn () => new ExampleOptionLazyCommand())); } else { $application->add(new ExampleOptionLazyCommand()); } diff --git a/tests/Type/Symfony/container.xml b/tests/Type/Symfony/container.xml index e1ef6d89..16d4b7fe 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 @@ -95,5 +354,23 @@ + + + + + + + + + + + + + + + + + + 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')); diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php index 0ab3a322..0376429f 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,14 +15,49 @@ protected function configure(): void { parent::configure(); + $this->addArgument('required', InputArgument::REQUIRED); $this->addArgument('base'); } + 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')); + 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 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')); + assertType('string', $input->getArgument('required')); assertType('array|string', $input->getArgument('diff')); assertType('array', $input->getArgument('arr')); assertType('string|null', $input->getArgument('both')); diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php index e0cefb88..34a673a4 100644 --- a/tests/Type/Symfony/data/form_data_type.php +++ b/tests/Type/Symfony/data/form_data_type.php @@ -2,6 +2,7 @@ namespace GenericFormDataType; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -73,3 +74,20 @@ public function doSomethingNullable(): void } } + +class FormController extends AbstractController +{ + + public function doSomething(): void + { + $form = $this->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()); + } + +} diff --git a/tests/Type/Symfony/data/input_bag.php b/tests/Type/Symfony/data/input_bag.php index efd08461..77b58821 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')); @@ -17,5 +18,5 @@ assertType('bool|float|int|string|null', $bag->get('foo', null)); assertType('bool|float|int|string', $bag->get('foo', '')); assertType('bool|float|int|string', $bag->get('foo', 'baz')); -assertType('array', $bag->all()); -assertType('array', $bag->all('bar')); +assertType('array|bool|float|int|string>', $bag->all()); +assertType('array', $bag->all('bar')); 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())); + } +} diff --git a/tests/Type/Symfony/data/property_accessor.php b/tests/Type/Symfony/data/property_accessor.php index 0e445684..8d5e95f0 100644 --- a/tests/Type/Symfony/data/property_accessor.php +++ b/tests/Type/Symfony/data/property_accessor.php @@ -6,7 +6,7 @@ $array = [1 => 'ea']; $propertyAccessor->setValue($array, 'foo', 'bar'); -assertType('array', $array); +assertType('array', $array); $object = new \stdClass(); $propertyAccessor->setValue($object, 'foo', 'bar'); diff --git a/tests/Type/Symfony/extension-test.neon b/tests/Type/Symfony/extension-test.neon index f7dc1353..0f1d9522 100644 --- a/tests/Type/Symfony/extension-test.neon +++ b/tests/Type/Symfony/extension-test.neon @@ -1,4 +1,4 @@ parameters: symfony: - console_application_loader: console_application_loader.php - container_xml_path: container.xml + consoleApplicationLoader: console_application_loader.php + containerXmlPath: container.xml