diff --git a/.gitattributes b/.gitattributes index 17405d6e..1545ee73 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,6 @@ tmp export-ignore .gitignore export-ignore .travis.yml export-ignore Makefile export-ignore -phpcs.xml export-ignore phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 15b77335..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: composer - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..d3f5961e --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,19 @@ +{ + "extends": [ + "config:base", + "schedule:weekly" + ], + "rangeStrategy": "update-lockfile", + "packageRules": [ + { + "matchPaths": ["+(composer.json)"], + "enabled": true, + "groupName": "root-composer" + }, + { + "matchPaths": [".github/**"], + "enabled": true, + "groupName": "github-actions" + } + ] +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bccd397b..88543fb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "master" + - "2.0.x" jobs: lint: @@ -16,16 +16,16 @@ jobs: strategy: matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -39,27 +39,30 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Lint" run: "make lint" - coding-standards: + coding-standard: name: "Coding Standard" runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 + + - name: "Checkout build-cs" + uses: actions/checkout@v4 + with: + repository: "phpstan/build-cs" + path: "build-cs" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" @@ -67,6 +70,10 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install build-cs dependencies" + working-directory: "build-cs" + run: "composer install --no-interaction --no-progress" + - name: "Lint" run: "make lint" @@ -81,22 +88,19 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" - exclude: - - php-version: "7.1" - dependencies: "highest" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -112,10 +116,6 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Tests" run: "make tests" @@ -127,19 +127,19 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -157,9 +157,5 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "PHPStan" run: "make phpstan" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 00000000..a8535014 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 960c1ba5..047fe906 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,20 +2,20 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '5 0 * * *' jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '31' - issue-exclude-created-before: '' - issue-exclude-labels: '' - issue-lock-labels: '' - issue-lock-comment: > + issue-inactive-days: '31' + exclude-issue-created-before: '' + exclude-any-issue-labels: '' + add-issue-labels: '' + issue-comment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml new file mode 100644 index 00000000..1ba4fd77 --- /dev/null +++ b/.github/workflows/release-toot.yml @@ -0,0 +1,21 @@ +name: Toot release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - uses: cbrgm/mastodon-github-action@v2 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + MASTODON_URL: https://phpc.social + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml new file mode 100644 index 00000000..09b39ded --- /dev/null +++ b/.github/workflows/release-tweet.yml @@ -0,0 +1,24 @@ +name: Tweet release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + tweet: + runs-on: ubuntu-latest + steps: + - uses: Eomm/why-don-t-you-tweet@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + tweet-message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225470a6..b8c96d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,19 +14,19 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v4 - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v1.0.0 + uses: metcalfc/changelog-generator@v4.6.2 with: - myToken: ${{ secrets.GITHUB_TOKEN }} + myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Create release" id: create-release uses: actions/create-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} diff --git a/.gitignore b/.gitignore index d6a83e59..7de9f3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /tests/tmp +/build-cs /vendor -composer.lock +/composer.lock .phpunit.result.cache 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 fe917d3b..1ee557df 100644 --- a/Makefile +++ b/Makefile @@ -10,14 +10,24 @@ lint: php vendor/bin/parallel-lint --colors \ src tests +.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/2.x + composer install --working-dir build-cs + .PHONY: cs cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + php build-cs/vendor/bin/phpcs --standard=build-cs/phpcs.xml src tests .PHONY: cs-fix cs-fix: - php build-cs/vendor/bin/phpcbf + php build-cs/vendor/bin/phpcbf --standard=build-cs/phpcs.xml src tests .PHONY: phpstan phpstan: php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests + +.PHONY: phpstan-generate-baseline +phpstan-generate-baseline: + php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon diff --git a/README.md b/README.md index ef670e6b..25a155c1 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,18 @@ You have to provide a path to `srcDevDebugProjectContainer.xml` or similar XML f ```yaml parameters: symfony: - container_xml_path: var/cache/dev/srcDevDebugProjectContainer.xml + containerXmlPath: var/cache/dev/srcDevDebugProjectContainer.xml # or with Symfony 4.2+ - container_xml_path: var/cache/dev/srcApp_KernelDevDebugContainer.xml + containerXmlPath: var/cache/dev/srcApp_KernelDevDebugContainer.xml # or with Symfony 5+ - container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml + containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml # 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 @@ -86,7 +90,7 @@ In that case, you can disable the `::has()` method return type resolving like th ``` parameters: symfony: - constant_hassers: false + constantHassers: false ``` Be aware that it may hide genuine errors in your application. @@ -99,7 +103,7 @@ by providing the console application from your own application. This will allow ```neon parameters: symfony: - console_application_loader: tests/console-application.php + consoleApplicationLoader: tests/console-application.php ``` Symfony 4: @@ -132,6 +136,22 @@ $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); return new Application($kernel); ``` +[Single Command Application](https://symfony.com/doc/current/components/console/single_command_tool.html): + +```php +// tests/console-application.php + +use App\Application; // where Application extends Symfony\Component\Console\SingleCommandApplication +use Symfony\Component\Console; + +require __DIR__ . '/../vendor/autoload.php'; + +$application = new Console\Application(); +$application->add(new Application()); + +return $application; +``` + You may then encounter an error with PhpParser: > Compile Error: Cannot Declare interface PhpParser\NodeVisitor, because the name is already in use diff --git a/build-cs/.gitignore b/build-cs/.gitignore deleted file mode 100644 index ff72e2d0..00000000 --- a/build-cs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/composer.lock -/vendor diff --git a/build-cs/composer.json b/build-cs/composer.json deleted file mode 100644 index ed7744e1..00000000 --- a/build-cs/composer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "require-dev": { - "consistence-community/coding-standard": "^3.10", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.4" - } -} diff --git a/composer.json b/composer.json index 06adbfbf..c03d2c99 100644 --- a/composer.json +++ b/composer.json @@ -13,36 +13,34 @@ } ], "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^1.0" + "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.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/config": "^4.2 || ^5.0", - "symfony/console": "^4.0 || ^5.0", - "symfony/framework-bundle": "^4.4 || ^5.0", - "symfony/http-foundation": "^4.0 || ^5.0", - "symfony/messenger": "^4.2 || ^5.0", - "symfony/serializer": "^4.0 || ^5.0" + "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", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" }, "config": { - "platform": { - "php": "7.4.6" - }, "sort-packages": true }, "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", diff --git a/extension.neon b/extension.neon index 4b36a06c..0803248f 100644 --- a/extension.neon +++ b/extension.neon @@ -5,21 +5,41 @@ parameters: uncheckedExceptionClasses: - 'Symfony\Component\Console\Exception\InvalidArgumentException' symfony: - container_xml_path: null - constant_hassers: true - console_application_loader: null + containerXmlPath: null + constantHassers: true + consoleApplicationLoader: null 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 - - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub + - stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.stub + - stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub + - stubs/Symfony/Component/Console/Command.stub + - stubs/Symfony/Component/Console/Exception/ExceptionInterface.stub + - stubs/Symfony/Component/Console/Exception/InvalidArgumentException.stub + - stubs/Symfony/Component/Console/Exception/LogicException.stub + - stubs/Symfony/Component/Console/Helper/HelperInterface.stub + - stubs/Symfony/Component/Console/Output/OutputInterface.stub - stubs/Symfony/Component/DependencyInjection/ContainerBuilder.stub - stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub - stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub - stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub - stubs/Symfony/Component/EventDispatcher/GenericEvent.stub + - stubs/Symfony/Component/Form/AbstractType.stub + - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub + - stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub + - stubs/Symfony/Component/Form/Exception/RuntimeException.stub + - stubs/Symfony/Component/Form/Exception/TransformationFailedException.stub - stubs/Symfony/Component/Form/DataTransformerInterface.stub - stubs/Symfony/Component/Form/FormBuilderInterface.stub + - stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub + - stubs/Symfony/Component/Form/FormConfigInterface.stub - stubs/Symfony/Component/Form/FormInterface.stub + - stubs/Symfony/Component/Form/FormFactoryInterface.stub - stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub - stubs/Symfony/Component/Form/FormTypeInterface.stub - stubs/Symfony/Component/Form/FormView.stub @@ -29,13 +49,33 @@ parameters: - stubs/Symfony/Component/HttpFoundation/Session.stub - stubs/Symfony/Component/Messenger/StampInterface.stub - 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 + - stubs/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.stub + - stubs/Symfony/Component/PropertyAccess/Exception/RuntimeException.stub + - stubs/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.stub + - stubs/Symfony/Component/PropertyAccess/PropertyAccessorInterface.stub - stubs/Symfony/Component/PropertyAccess/PropertyPathInterface.stub - stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub - stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub + - stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub + - stubs/Symfony/Component/Security/Core/Authorization/Voter/Voter.stub + - stubs/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.stub - stubs/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.stub - stubs/Symfony/Component/Serializer/Encoder/DecoderInterface.stub - stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub + - stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub + - stubs/Symfony/Component/Serializer/Exception/CircularReferenceException.stub + - stubs/Symfony/Component/Serializer/Exception/ExceptionInterface.stub + - stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub + - stubs/Symfony/Component/Serializer/Exception/InvalidArgumentException.stub + - stubs/Symfony/Component/Serializer/Exception/LogicException.stub + - stubs/Symfony/Component/Serializer/Exception/RuntimeException.stub + - stubs/Symfony/Component/Serializer/Exception/UnexpectedValueException.stub - stubs/Symfony/Component/Serializer/Normalizer/ContextAwareDenormalizerInterface.stub - stubs/Symfony/Component/Serializer/Normalizer/ContextAwareNormalizerInterface.stub - stubs/Symfony/Component/Serializer/Normalizer/DenormalizableInterface.stub @@ -43,15 +83,21 @@ parameters: - stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub - stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub - stubs/Symfony/Component/Validator/Constraint.stub + - stubs/Symfony/Component/Validator/Constraints/Composite.stub + - stubs/Symfony/Component/Validator/Constraints/Compound.stub - stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub - stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub + - stubs/Symfony/Contracts/Cache/CacheInterface.stub + - 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()) - constant_hassers: bool() - console_application_loader: schema(string(), nullable()) + containerXmlPath: schema(string(), nullable()) + constantHassers: bool() + consoleApplicationLoader: schema(string(), nullable()) ]) services: @@ -59,34 +105,45 @@ services: - factory: PHPStan\Symfony\ConsoleApplicationResolver arguments: - consoleApplicationLoader: %symfony.console_application_loader% + consoleApplicationLoader: %symfony.consoleApplicationLoader% # service map symfony.serviceMapFactory: class: PHPStan\Symfony\ServiceMapFactory - factory: PHPStan\Symfony\XmlServiceMapFactory(%symfony.container_xml_path%) + factory: PHPStan\Symfony\XmlServiceMapFactory + arguments: + containerXmlPath: %symfony.containerXmlPath% - factory: @symfony.serviceMapFactory::create() # parameter map symfony.parameterMapFactory: class: PHPStan\Symfony\ParameterMapFactory - factory: PHPStan\Symfony\XmlParameterMapFactory(%symfony.container_xml_path%) + 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, %symfony.constant_hassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constant_hassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constant_hassers%) + 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, %symfony.constant_hassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # ControllerTrait::has() type specification @@ -138,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 @@ -154,10 +216,17 @@ services: tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # InputInterface::getOption() return type + - + factory: PHPStan\Type\Symfony\GetOptionTypeHelper - factory: PHPStan\Type\Symfony\InputInterfaceGetOptionDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + # InputInterface::getOptions() return type + - + factory: PHPStan\Type\Symfony\InputInterfaceGetOptionsDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + # InputInterface::hasOption() type specification - factory: PHPStan\Type\Symfony\OptionTypeSpecifyingExtension @@ -230,22 +299,68 @@ services: # ParameterBagInterface::get()/has() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constant_hassers%) + 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', %symfony.constant_hassers%) + 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, %symfony.constant_hassers%) + 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, %symfony.constant_hassers%) + 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\SymfonyDiagnoseExtension + tags: + - phpstan.diagnoseExtension + + # FormInterface::getErrors() return type + - + factory: PHPStan\Type\Symfony\Form\FormInterfaceDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Command::getHelper() return type + - + factory: PHPStan\Type\Symfony\CommandGetHelperDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # ResponseHeaderBag::getCookies() return type + - + factory: PHPStan\Type\Symfony\ResponseHeaderBagDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # InputBag::get() type specification + - + factory: PHPStan\Type\Symfony\InputBagTypeSpecifyingExtension + tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] + + # Additional constructors and initialization checks for @required autowiring + - + class: PHPStan\Symfony\RequiredAutowiringExtension + tags: + - phpstan.properties.readWriteExtension + - phpstan.additionalConstructorsExtension + + # CacheInterface::get() return type + - + factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Extension::getConfiguration() return type + - + factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + - + class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 2807efec..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - src - tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - tests/tmp - tests/*/data - diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..79e87db9 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,79 @@ +parameters: + ignoreErrors: + - + 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 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: '#^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\.$#' + 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\.$#' + identifier: phpstanApi.classConstant + count: 1 + path: tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php diff --git a/phpstan.neon b/phpstan.neon index 39998f59..f13073e1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,7 @@ includes: - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon parameters: excludePaths: diff --git a/phpunit.xml b/phpunit.xml index f9f3afe2..2e2f6167 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ beStrictAboutTodoAnnotatedTests="true" failOnRisky="true" failOnWarning="true" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml" > diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index 4c9a6af1..96f1efea 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ServiceMap; use PHPStan\TrinaryLogic; use PHPStan\Type\ObjectType; @@ -19,8 +19,7 @@ final class ContainerInterfacePrivateServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; public function __construct(ServiceMap $symfonyServiceMap) { @@ -32,17 +31,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - } - if (!$node->name instanceof Node\Identifier) { return []; } @@ -78,7 +68,11 @@ public function processNode(Node $node, Scope $scope): array if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); if ($service !== null && !$service->isPublic()) { - return [sprintf('Service "%s" is private.', $serviceId)]; + return [ + RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId)) + ->identifier('symfonyContainer.privateService') + ->build(), + ]; } } @@ -88,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 cb0ae4e6..23444b6b 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -4,13 +4,14 @@ 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\ShouldNotHappenException; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ServiceMap; use PHPStan\Type\ObjectType; use PHPStan\Type\Symfony\Helper; +use function sprintf; /** * @implements Rule @@ -18,13 +19,11 @@ final class ContainerInterfaceUnknownServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; - /** @var \PhpParser\PrettyPrinter\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; @@ -35,17 +34,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - } - if (!$node->name instanceof Node\Identifier) { return []; } @@ -78,7 +68,11 @@ public function processNode(Node $node, Scope $scope): array $service = $this->serviceMap->getService($serviceId); $serviceIdType = $scope->getType($node->getArgs()[0]->value); if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { - return [sprintf('Service "%s" is not registered in the container.', $serviceId)]; + return [ + 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 c29f11ed..5435d4c9 100644 --- a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php +++ b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php @@ -6,16 +6,16 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerType; 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; use function sprintf; /** @@ -29,17 +29,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - }; - if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf($scope->getType($node->var))->yes()) { return []; } @@ -51,10 +42,10 @@ public function processNode(Node $node, Scope $scope): array } $modeType = isset($node->getArgs()[1]) ? $scope->getType($node->getArgs()[1]->value) : new NullType(); - if ($modeType instanceof NullType) { + if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(2); // InputArgument::OPTIONAL } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -67,12 +58,22 @@ public function processNode(Node $node, Scope $scope): array // not an array if (($mode & 4) !== 4 && !(new UnionType([new StringType(), new NullType()]))->isSuperTypeOf($defaultType)->yes()) { - return [sprintf('Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))]; + return [ + RuleErrorBuilder::message(sprintf( + 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, %s given.', + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), + ]; } // is array if (($mode & 4) === 4 && !(new UnionType([new ArrayType(new IntegerType(), new StringType()), new NullType()]))->isSuperTypeOf($defaultType)->yes()) { - return [sprintf('Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))]; + return [ + RuleErrorBuilder::message(sprintf( + 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, %s given.', + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), + ]; } return []; diff --git a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php index 3eb2df1b..2e3dc0e9 100644 --- a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php +++ b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -15,9 +15,9 @@ 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; use function sprintf; /** @@ -31,17 +31,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - }; - if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf($scope->getType($node->var))->yes()) { return []; } @@ -53,10 +44,10 @@ public function processNode(Node $node, Scope $scope): array } $modeType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new NullType(); - if ($modeType instanceof NullType) { + if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(1); // InputOption::VALUE_NONE } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -71,13 +62,24 @@ public function processNode(Node $node, Scope $scope): array if (($mode & 8) !== 8) { $checkType = new UnionType([new StringType(), new IntegerType(), new NullType(), new BooleanType()]); if (!$checkType->isSuperTypeOf($defaultType)->yes()) { - return [sprintf('Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects %s, %s given.', $checkType->describe(VerbosityLevel::typeOnly()), $defaultType->describe(VerbosityLevel::typeOnly()))]; + return [ + 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()), + ))->identifier('argument.type')->build(), + ]; } } // is array if (($mode & 8) === 8 && !(new UnionType([new ArrayType(new MixedType(), new StringType()), new NullType()]))->isSuperTypeOf($defaultType)->yes()) { - return [sprintf('Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))]; + return [ + RuleErrorBuilder::message(sprintf( + 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, %s given.', + $defaultType->describe(VerbosityLevel::typeOnly()), + ))->identifier('argument.type')->build(), + ]; } return []; diff --git a/src/Rules/Symfony/UndefinedArgumentRule.php b/src/Rules/Symfony/UndefinedArgumentRule.php index 59f71ed6..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\ShouldNotHappenException; +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 \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var \PhpParser\PrettyPrinter\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; @@ -39,17 +36,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - }; - $classReflection = $scope->getClassReflection(); if ($classReflection === null) { return []; @@ -69,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 []; } @@ -84,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[] = sprintf('Command "%s" does not define argument "%s".', $name, $argName); + $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 29e08819..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\ShouldNotHappenException; +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 \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - /** @var \PhpParser\PrettyPrinter\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; @@ -39,17 +36,8 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope - * @return (string|\PHPStan\Rules\RuleError)[] errors - */ public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof MethodCall) { - throw new ShouldNotHappenException(); - }; - $classReflection = $scope->getClassReflection(); if ($classReflection === null) { return []; @@ -69,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 []; } @@ -84,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[] = sprintf('Command "%s" does not define option "%s".', $name, $optName); + $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define option "%s".', $name, $optName)) + ->identifier('symfonyConsole.optionNotFound') + ->build(); } } diff --git a/src/Symfony/ConsoleApplicationResolver.php b/src/Symfony/ConsoleApplicationResolver.php index a493cbf0..13b24d26 100644 --- a/src/Symfony/ConsoleApplicationResolver.php +++ b/src/Symfony/ConsoleApplicationResolver.php @@ -5,46 +5,57 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use function file_exists; use function get_class; use function is_readable; +use function method_exists; +use function sprintf; final class ConsoleApplicationResolver { - /** @var \Symfony\Component\Console\Application|null */ - private $consoleApplication; + private ?string $consoleApplicationLoader = null; + + private ?Application $consoleApplication = null; public function __construct(?string $consoleApplicationLoader) { - if ($consoleApplicationLoader === null) { - return; - } - $this->consoleApplication = $this->loadConsoleApplication($consoleApplicationLoader); + $this->consoleApplicationLoader = $consoleApplicationLoader; } - /** - * @return \Symfony\Component\Console\Application|null - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint - */ - private function loadConsoleApplication(string $consoleApplicationLoader) + public function hasConsoleApplicationLoader(): bool { - if (!file_exists($consoleApplicationLoader) - || !is_readable($consoleApplicationLoader) + return $this->consoleApplicationLoader !== null; + } + + private function getConsoleApplication(): ?Application + { + if ($this->consoleApplicationLoader === null) { + return null; + } + + if ($this->consoleApplication !== null) { + return $this->consoleApplication; + } + + if (!file_exists($this->consoleApplicationLoader) + || !is_readable($this->consoleApplicationLoader) ) { - throw new ShouldNotHappenException(); + throw new ShouldNotHappenException(sprintf('Cannot load console application. Check the parameters.symfony.consoleApplicationLoader setting in PHPStan\'s config. The offending value is "%s".', $this->consoleApplicationLoader)); } - return require $consoleApplicationLoader; + return $this->consoleApplication = require $this->consoleApplicationLoader; } /** - * @return \Symfony\Component\Console\Command\Command[] + * @return Command[] */ public function findCommands(ClassReflection $classReflection): array { - if ($this->consoleApplication === null) { + $consoleApplication = $this->getConsoleApplication(); + if ($consoleApplication === null) { return []; } @@ -54,7 +65,7 @@ public function findCommands(ClassReflection $classReflection): array } $commands = []; - foreach ($this->consoleApplication->all() as $name => $command) { + foreach ($consoleApplication->all() as $name => $command) { $commandClass = new ObjectType(get_class($command)); $isLazyCommand = (new ObjectType('Symfony\Component\Console\Command\LazyCommand'))->isSuperTypeOf($commandClass)->yes(); diff --git a/src/Symfony/DefaultParameterMap.php b/src/Symfony/DefaultParameterMap.php index a58ed9fc..3149fd7d 100644 --- a/src/Symfony/DefaultParameterMap.php +++ b/src/Symfony/DefaultParameterMap.php @@ -4,17 +4,17 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\TypeUtils; -use function count; +use PHPStan\Type\Type; +use function array_map; final class DefaultParameterMap implements ParameterMap { - /** @var \PHPStan\Symfony\ParameterDefinition[] */ - private $parameters; + /** @var ParameterDefinition[] */ + private array $parameters; /** - * @param \PHPStan\Symfony\ParameterDefinition[] $parameters + * @param ParameterDefinition[] $parameters */ public function __construct(array $parameters) { @@ -22,7 +22,7 @@ public function __construct(array $parameters) } /** - * @return \PHPStan\Symfony\ParameterDefinition[] + * @return ParameterDefinition[] */ public function getParameters(): array { @@ -34,10 +34,11 @@ public function getParameter(string $key): ?ParameterDefinition return $this->parameters[$key] ?? null; } - public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string + public static function getParameterKeysFromNode(Expr $node, Scope $scope): array { - $strings = TypeUtils::getConstantStrings($scope->getType($node)); - return count($strings) === 1 ? $strings[0]->getValue() : null; + $strings = $scope->getType($node)->getConstantStrings(); + + return array_map(static fn (Type $type) => $type->getValue(), $strings); } } diff --git a/src/Symfony/DefaultServiceMap.php b/src/Symfony/DefaultServiceMap.php index 6339b450..5d3bccd0 100644 --- a/src/Symfony/DefaultServiceMap.php +++ b/src/Symfony/DefaultServiceMap.php @@ -4,17 +4,16 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\TypeUtils; use function count; final class DefaultServiceMap implements ServiceMap { - /** @var \PHPStan\Symfony\ServiceDefinition[] */ - private $services; + /** @var ServiceDefinition[] */ + private array $services; /** - * @param \PHPStan\Symfony\ServiceDefinition[] $services + * @param ServiceDefinition[] $services */ public function __construct(array $services) { @@ -22,7 +21,7 @@ public function __construct(array $services) } /** - * @return \PHPStan\Symfony\ServiceDefinition[] + * @return ServiceDefinition[] */ public function getServices(): array { @@ -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/FakeParameterMap.php b/src/Symfony/FakeParameterMap.php index 7e723411..53acdc3c 100644 --- a/src/Symfony/FakeParameterMap.php +++ b/src/Symfony/FakeParameterMap.php @@ -9,7 +9,7 @@ final class FakeParameterMap implements ParameterMap { /** - * @return \PHPStan\Symfony\ParameterDefinition[] + * @return ParameterDefinition[] */ public function getParameters(): array { @@ -21,9 +21,9 @@ public function getParameter(string $key): ?ParameterDefinition return null; } - public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string + public static function getParameterKeysFromNode(Expr $node, Scope $scope): array { - return null; + return []; } } diff --git a/src/Symfony/FakeServiceMap.php b/src/Symfony/FakeServiceMap.php index d05fe8ed..88f9edac 100644 --- a/src/Symfony/FakeServiceMap.php +++ b/src/Symfony/FakeServiceMap.php @@ -9,7 +9,7 @@ final class FakeServiceMap implements ServiceMap { /** - * @return \PHPStan\Symfony\ServiceDefinition[] + * @return ServiceDefinition[] */ public function getServices(): array { diff --git a/src/Symfony/InputBagStubFilesExtension.php b/src/Symfony/InputBagStubFilesExtension.php index 94fd2104..6ce36f4b 100644 --- a/src/Symfony/InputBagStubFilesExtension.php +++ b/src/Symfony/InputBagStubFilesExtension.php @@ -2,14 +2,27 @@ namespace PHPStan\Symfony; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\StubFilesExtension; class InputBagStubFilesExtension implements StubFilesExtension { + private Reflector $reflector; + + public function __construct( + Reflector $reflector + ) + { + $this->reflector = $reflector; + } + public function getFiles(): array { - if (!class_exists('Symfony\Component\HttpFoundation\InputBag')) { + try { + $this->reflector->reflectClass('Symfony\Component\HttpFoundation\InputBag'); + } catch (IdentifierNotFound $e) { return []; } 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 1df679bc..53b53265 100644 --- a/src/Symfony/Parameter.php +++ b/src/Symfony/Parameter.php @@ -5,14 +5,12 @@ final class Parameter implements ParameterDefinition { - /** @var string */ - private $key; + private string $key; /** @var array|bool|float|int|string */ private $value; /** - * @param string $key * @param array|bool|float|int|string $value */ public function __construct( 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 fc14fb2c..0c551635 100644 --- a/src/Symfony/ParameterMap.php +++ b/src/Symfony/ParameterMap.php @@ -5,16 +5,22 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +/** + * @api + */ interface ParameterMap { /** - * @return \PHPStan\Symfony\ParameterDefinition[] + * @return ParameterDefinition[] */ public function getParameters(): array; public function getParameter(string $key): ?ParameterDefinition; - public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string; + /** + * @return array + */ + public static function getParameterKeysFromNode(Expr $node, Scope $scope): array; } diff --git a/src/Symfony/RequiredAutowiringExtension.php b/src/Symfony/RequiredAutowiringExtension.php new file mode 100644 index 00000000..7d8d195d --- /dev/null +++ b/src/Symfony/RequiredAutowiringExtension.php @@ -0,0 +1,90 @@ +fileTypeMapper = $fileTypeMapper; + } + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + // If the property is public, check for @required on the property itself + if (!$property->isPublic()) { + return false; + } + + if ($property->getDocComment() !== null && $this->isRequiredFromDocComment($property->getDocComment())) { + return true; + } + + // Check for the attribute version + if ($property instanceof PhpPropertyReflection && count($property->getNativeReflection()->getAttributes('Symfony\Contracts\Service\Attribute\Required')) > 0) { + return true; + } + + return false; + } + + public function getAdditionalConstructors(ClassReflection $classReflection): array + { + $additionalConstructors = []; + $nativeReflection = $classReflection->getNativeReflection(); + + foreach ($nativeReflection->getMethods() as $method) { + if (!$method->isPublic()) { + continue; + } + + if ($method->getDocComment() !== false && $this->isRequiredFromDocComment($method->getDocComment())) { + $additionalConstructors[] = $method->getName(); + } + + if (count($method->getAttributes('Symfony\Contracts\Service\Attribute\Required')) === 0) { + continue; + } + + $additionalConstructors[] = $method->getName(); + } + + return $additionalConstructors; + } + + private function isRequiredFromDocComment(string $docComment): bool + { + $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc(null, null, null, null, $docComment); + + foreach ($phpDoc->getPhpDocNodes() as $node) { + // @required tag is available, meaning this property is always initialized + if (count($node->getTagsByName('@required')) > 0) { + return true; + } + } + + return false; + } + +} 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 b954dc72..bbd2d8a3 100644 --- a/src/Symfony/ServiceMap.php +++ b/src/Symfony/ServiceMap.php @@ -5,11 +5,14 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +/** + * @api + */ interface ServiceMap { /** - * @return \PHPStan\Symfony\ServiceDefinition[] + * @return ServiceDefinition[] */ public function getServices(): array; 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/XmlContainerNotExistsException.php b/src/Symfony/XmlContainerNotExistsException.php index 7238cc6e..59b600de 100644 --- a/src/Symfony/XmlContainerNotExistsException.php +++ b/src/Symfony/XmlContainerNotExistsException.php @@ -2,7 +2,9 @@ namespace PHPStan\Symfony; -final class XmlContainerNotExistsException extends \InvalidArgumentException +use InvalidArgumentException; + +final class XmlContainerNotExistsException extends InvalidArgumentException { } diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index 6a7575f4..4d3d3578 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -2,17 +2,26 @@ namespace PHPStan\Symfony; +use InvalidArgumentException; +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; final class XmlParameterMapFactory implements ParameterMapFactory { - /** @var string|null */ - private $containerXml; + private ?string $containerXml = null; - public function __construct(?string $containerXml) + public function __construct(?string $containerXmlPath) { - $this->containerXml = $containerXml; + $this->containerXml = $containerXmlPath; } public function create(): ParameterMap @@ -31,37 +40,46 @@ public function create(): ParameterMap throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml)); } - /** @var \PHPStan\Symfony\Parameter[] $parameters */ + /** @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); } /** * @return array|bool|float|int|string */ - private function getNodeValue(\SimpleXMLElement $def) + private function getNodeValue(SimpleXMLElement $def) { - /** @var \SimpleXMLElement $attrs */ + /** @var SimpleXMLElement $attrs */ $attrs = $def->attributes(); $value = null; switch ((string) $attrs->type) { case 'collection': $value = []; - foreach ($def->children() as $child) { - /** @var \SimpleXMLElement $childAttrs */ + $children = $def->children(); + if ($children === null) { + throw new ShouldNotHappenException(); + } + foreach ($children as $child) { + /** @var SimpleXMLElement $childAttrs */ $childAttrs = $child->attributes(); if (isset($childAttrs->key)) { @@ -79,7 +97,7 @@ private function getNodeValue(\SimpleXMLElement $def) case 'binary': $value = base64_decode((string) $def, true); if ($value === false) { - throw new \InvalidArgumentException(sprintf('Parameter "%s" of binary type is not valid base64 encoded string.', (string) $attrs->key)); + throw new InvalidArgumentException(sprintf('Parameter "%s" of binary type is not valid base64 encoded string.', (string) $attrs->key)); } break; diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 409ef847..ac79cb30 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -2,6 +2,10 @@ 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; @@ -10,12 +14,11 @@ final class XmlServiceMapFactory implements ServiceMapFactory { - /** @var string|null */ - private $containerXml; + private ?string $containerXml = null; - public function __construct(?string $containerXml) + public function __construct(?string $containerXmlPath) { - $this->containerXml = $containerXml; + $this->containerXml = $containerXmlPath; } public function create(): ServiceMap @@ -34,29 +37,42 @@ public function create(): ServiceMap throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml)); } - /** @var \PHPStan\Symfony\Service[] $services */ + /** @var Service[] $services */ $services = []; - /** @var \PHPStan\Symfony\Service[] $aliases */ + /** @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) { @@ -70,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 74b6981f..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 \PhpParser\PrettyPrinter\Standard */ - private $printer; + private Printer $printer; - /** @var \PHPStan\Analyser\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 new file mode 100644 index 00000000..0862ce61 --- /dev/null +++ b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php @@ -0,0 +1,48 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (!isset($methodCall->getArgs()[1])) { + return null; + } + + $callbackReturnType = $scope->getType($methodCall->getArgs()[1]->value); + if ($callbackReturnType->isCallable()->yes()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $callbackReturnType->getCallableParametersAcceptors($scope), + ); + $returnType = $parametersAcceptor->getReturnType(); + + // generalize template parameters + return $returnType->generalize(GeneralizePrecision::templateArgument()); + } + + return null; + } + +} diff --git a/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php new file mode 100644 index 00000000..fba70cfd --- /dev/null +++ b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php @@ -0,0 +1,67 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Command\Command'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getHelper'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (!isset($methodCall->getArgs()[0])) { + return null; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return null; + } + + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + if (count($argStrings) !== 1) { + return null; + } + $argName = $argStrings[0]->getValue(); + + $returnTypes = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $returnTypes[] = new ObjectType(get_class($command->getHelper($argName))); + } catch (Throwable $e) { + // no-op + } + } + + return count($returnTypes) > 0 ? TypeCombinator::union(...$returnTypes) : null; + } + +} diff --git a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php index cd1d4e51..1dae22e2 100644 --- a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php @@ -9,8 +9,9 @@ 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; final class ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -53,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(); @@ -70,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 5d19b60f..800d9dbc 100644 --- a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php @@ -10,18 +10,19 @@ use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function in_array; final class PassParentObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; /** @var string[] */ - private $methods; + private array $methods; /** - * @param string $className + * @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 8dd47a90..034d5d80 100644 --- a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php @@ -5,22 +5,22 @@ 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; +use function in_array; final class ReturnParentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; /** @var string[] */ - private $methods; + private array $methods; /** - * @param string $className + * @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 a5f4f724..4f266c50 100644 --- a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php @@ -6,10 +6,11 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\ShouldNotHappenException; 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 { @@ -37,7 +38,7 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type { if (!$methodCall->class instanceof Name) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $className = $scope->resolveName($methodCall->class); @@ -45,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 e4e6a34c..06e08772 100644 --- a/src/Type/Symfony/EnvelopeReturnTypeExtension.php +++ b/src/Type/Symfony/EnvelopeReturnTypeExtension.php @@ -5,12 +5,15 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\MixedType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; final class EnvelopeReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -32,15 +35,23 @@ public function getTypeFromMethodCall( ): Type { if (count($methodCall->getArgs()) === 0) { - return new ArrayType(new MixedType(), new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface'))); + return new ArrayType( + new GenericClassStringType(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 (!$argType instanceof ConstantStringType) { - return new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')); + if (count($argType->getConstantStrings()) === 0) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType()); } - return new ArrayType(new MixedType(), new ObjectType($argType->getValue())); + $objectTypes = []; + foreach ($argType->getConstantStrings() as $constantString) { + $objectTypes[] = new ObjectType($constantString->getValue()); + } + + 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 new file mode 100644 index 00000000..d975d9d1 --- /dev/null +++ b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php @@ -0,0 +1,104 @@ +reflectionProvider = $reflectionProvider; + } + + public function getClass(): string + { + return 'Symfony\Component\DependencyInjection\Extension\Extension'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getConfiguration' + && $methodReflection->getDeclaringClass()->getName() === 'Symfony\Component\DependencyInjection\Extension\Extension'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + $types = []; + $extensionType = $scope->getType($methodCall->var); + $classes = $extensionType->getObjectClassNames(); + + foreach ($classes as $extensionName) { + if (str_contains($extensionName, "\0")) { + $types[] = new NullType(); + continue; + } + + $lastBackslash = strrpos($extensionName, '\\'); + if ($lastBackslash === false) { + $types[] = new NullType(); + continue; + } + + $configurationName = substr_replace($extensionName, '\Configuration', $lastBackslash); + if (!$this->reflectionProvider->hasClass($configurationName)) { + $types[] = new NullType(); + continue; + } + + $reflection = $this->reflectionProvider->getClass($configurationName); + if ($this->hasRequiredConstructor($reflection)) { + $types[] = new NullType(); + continue; + } + + $types[] = new ObjectType($configurationName); + } + + return TypeCombinator::union(...$types); + } + + private function hasRequiredConstructor(ClassReflection $class): bool + { + if (!$class->hasConstructor()) { + return false; + } + + $constructor = $class->getConstructor(); + foreach ($constructor->getVariants() as $variant) { + $anyRequired = false; + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->isOptional()) { + $anyRequired = true; + break; + } + } + + if (!$anyRequired) { + return false; + } + } + + return true; + } + +} diff --git a/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php new file mode 100644 index 00000000..f80ddeb9 --- /dev/null +++ b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php @@ -0,0 +1,64 @@ +getName() === 'getErrors'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + if (!isset($methodCall->getArgs()[1])) { + return new GenericObjectType(FormErrorIterator::class, [new ObjectType(FormError::class)]); + } + + $firstArgType = $scope->getType($methodCall->getArgs()[0]->value); + $secondArgType = $scope->getType($methodCall->getArgs()[1]->value); + + $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); + + if ($firstCompareType === $firstIsTrueType && $secondCompareType === $secondIsFalseType) { + return new GenericObjectType(FormErrorIterator::class, [ + new UnionType([ + new ObjectType(FormError::class), + new ObjectType(FormErrorIterator::class), + ]), + ]); + } + + return new GenericObjectType(FormErrorIterator::class, [new ObjectType(FormError::class)]); + } + +} diff --git a/src/Type/Symfony/GetOptionTypeHelper.php b/src/Type/Symfony/GetOptionTypeHelper.php new file mode 100644 index 00000000..6ddf87d8 --- /dev/null +++ b/src/Type/Symfony/GetOptionTypeHelper.php @@ -0,0 +1,41 @@ +acceptValue()) { + if (method_exists($option, 'isNegatable') && $option->isNegatable()) { + return new UnionType([new BooleanType(), new NullType()]); + } + + return new BooleanType(); + } + + $optType = TypeCombinator::union(new StringType(), new NullType()); + if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) { + $optType = TypeCombinator::removeNull($optType); + } + if ($option->isArray()) { + $optType = new ArrayType(new IntegerType(), $optType); + } + + return TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault())); + } + +} 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 b4875829..75e6d0bc 100644 --- a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php @@ -18,6 +18,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function in_array; final class InputBagDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -36,7 +37,7 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { if ($methodReflection->getName() === 'get') { return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope); @@ -53,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 new file mode 100644 index 00000000..11cd39ef --- /dev/null +++ b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php @@ -0,0 +1,50 @@ +getName() === self::HAS_METHOD_NAME && $context->false(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + return $this->typeSpecifier->create( + new MethodCall($node->var, self::GET_METHOD_NAME, $node->getArgs()), + new NullType(), + $context->negate(), + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php index 15e75d0e..88bd7b0e 100644 --- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -6,22 +6,21 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Symfony\ConsoleApplicationResolver; 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 \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -38,26 +37,25 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'getArgument'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - if (!isset($methodCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) !== 1) { - return $defaultReturnType; + return null; } $argName = $argStrings[0]->getValue(); $argTypes = []; + $canBeNullInInteract = false; foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { try { $command->mergeApplicationDefinition(); @@ -71,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; @@ -79,7 +79,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } - return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : $defaultReturnType; + 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 fb397311..6d0346cf 100644 --- a/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php @@ -6,28 +6,23 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Symfony\ConsoleApplicationResolver; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; 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; final class InputInterfaceGetOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; - public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) + private GetOptionTypeHelper $getOptionTypeHelper; + + public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper) { $this->consoleApplicationResolver = $consoleApplicationResolver; + $this->getOptionTypeHelper = $getOptionTypeHelper; } public function getClass(): string @@ -40,22 +35,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'getOption'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - if (!isset($methodCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($optStrings) !== 1) { - return $defaultReturnType; + return null; } $optName = $optStrings[0]->getValue(); @@ -64,25 +57,13 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method try { $command->mergeApplicationDefinition(); $option = $command->getDefinition()->getOption($optName); - if (!$option->acceptValue()) { - $optType = new BooleanType(); - } else { - $optType = TypeCombinator::union(new StringType(), new NullType()); - if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) { - $optType = TypeCombinator::removeNull($optType); - } - if ($option->isArray()) { - $optType = new ArrayType(new IntegerType(), $optType); - } - $optType = TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault())); - } - $optTypes[] = $optType; + $optTypes[] = $this->getOptionTypeHelper->getOptionType($scope, $option); } catch (InvalidArgumentException $e) { // noop } } - return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : $defaultReturnType; + return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : null; } } diff --git a/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php new file mode 100644 index 00000000..3105621d --- /dev/null +++ b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php @@ -0,0 +1,67 @@ +consoleApplicationResolver = $consoleApplicationResolver; + $this->getOptionTypeHelper = $getOptionTypeHelper; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getOptions'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return null; + } + + $optTypes = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $options = $command->getDefinition()->getOptions(); + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($options as $name => $option) { + $optionType = $this->getOptionTypeHelper->getOptionType($scope, $option); + $builder->setOffsetValueType(new ConstantStringType($name), $optionType); + } + + $optTypes[] = $builder->getArray(); + } catch (InvalidArgumentException $e) { + // noop + } + } + + return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : null; + } + +} diff --git a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php index 112a2bcf..34bffcea 100644 --- a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php @@ -7,19 +7,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Symfony\ConsoleApplicationResolver; -use PHPStan\Type\BooleanType; 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 \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -36,25 +34,34 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'hasArgument'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = new BooleanType(); - if (!isset($methodCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($argStrings) !== 1) { - return $defaultReturnType; + 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 { @@ -67,11 +74,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (count($returnTypes) === 0) { - return $defaultReturnType; + return null; } $returnTypes = array_unique($returnTypes); - return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType; + return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null; } } diff --git a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php index eec7201c..e4f8b5b1 100644 --- a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php @@ -7,19 +7,16 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Symfony\ConsoleApplicationResolver; -use PHPStan\Type\BooleanType; 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 \PHPStan\Symfony\ConsoleApplicationResolver */ - private $consoleApplicationResolver; + private ConsoleApplicationResolver $consoleApplicationResolver; public function __construct(ConsoleApplicationResolver $consoleApplicationResolver) { @@ -36,22 +33,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'hasOption'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = new BooleanType(); - if (!isset($methodCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value)); + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); if (count($optStrings) !== 1) { - return $defaultReturnType; + return null; } $optName = $optStrings[0]->getValue(); @@ -67,11 +62,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (count($returnTypes) === 0) { - return $defaultReturnType; + return null; } $returnTypes = array_unique($returnTypes); - return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType; + return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : 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 347cf591..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 \PhpParser\PrettyPrinter\Standard */ - private $printer; + private Printer $printer; - /** @var \PHPStan\Analyser\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 8e79cf59..687b0c33 100644 --- a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php @@ -4,15 +4,15 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; 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; @@ -21,35 +21,57 @@ use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; +use Symfony\Component\DependencyInjection\EnvVarProcessor; +use function array_filter; +use function array_keys; +use function array_map; +use function array_values; +use function class_exists; +use function count; use function in_array; +use function is_array; +use function is_int; +use function is_string; +use function preg_match; +use function strlen; 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 \PHPStan\Symfony\ParameterMap */ - private $parameterMap; + private ParameterMap $parameterMap; - public function __construct(string $className, ?string $methodGet, ?string $methodHas, bool $constantHassers, ParameterMap $symfonyParameterMap) + private TypeStringResolver $typeStringResolver; + + /** + * @param class-string $className + */ + public function __construct( + string $className, + ?string $methodGet, + ?string $methodHas, + bool $constantHassers, + ParameterMap $symfonyParameterMap, + TypeStringResolver $typeStringResolver + ) { $this->className = $className; $this->methodGet = $methodGet; $this->methodHas = $methodHas; $this->constantHassers = $constantHassers; $this->parameterMap = $symfonyParameterMap; + $this->typeStringResolver = $typeStringResolver; } public function getClass(): string @@ -59,14 +81,12 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - $methods = array_filter([$this->methodGet, $this->methodHas], 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: @@ -85,7 +105,7 @@ private function getGetTypeFromMethodCall( { // We don't use the method's return type because this won't work properly with lowest and // highest versions of Symfony ("mixed" for lowest, "array|bool|float|integer|string|null" for highest). - $returnType = new UnionType([ + $defaultReturnType = new UnionType([ new ArrayType(new MixedType(), new MixedType()), new BooleanType(), new FloatType(), @@ -94,18 +114,77 @@ private function getGetTypeFromMethodCall( new NullType(), ]); if (!isset($methodCall->getArgs()[0])) { - return $returnType; + return $defaultReturnType; + } + + $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope); + if ($parameterKeys === []) { + return $defaultReturnType; } - $parameterKey = $this->parameterMap::getParameterKeyFromNode($methodCall->getArgs()[0]->value, $scope); - if ($parameterKey !== null) { + $returnTypes = []; + foreach ($parameterKeys as $parameterKey) { $parameter = $this->parameterMap->getParameter($parameterKey); - if ($parameter !== null) { - return $this->generalizeType($scope->getTypeFromValue($parameter->getValue())); + if ($parameter === null) { + return $defaultReturnType; } + + $returnTypes[] = $this->generalizeTypeFromValue($scope, $parameter->getValue()); } - return $returnType; + return TypeCombinator::union(...$returnTypes); + } + + /** + * @param array|bool|float|int|string $value + */ + private function generalizeTypeFromValue(Scope $scope, $value): Type + { + if (is_array($value) && $value !== []) { + $hasOnlyStringKey = true; + foreach (array_keys($value) as $key) { + if (is_int($key)) { + $hasOnlyStringKey = false; + break; + } + } + + if ($hasOnlyStringKey) { + $keyTypes = []; + $valueTypes = []; + foreach ($value as $key => $element) { + $keyType = $scope->getTypeFromValue($key); + $keyStringTypes = $keyType->getConstantStrings(); + if (count($keyStringTypes) !== 1) { + throw new ShouldNotHappenException(); + } + $keyTypes[] = $keyStringTypes[0]; + $valueTypes[] = $this->generalizeTypeFromValue($scope, $element); + } + + return ConstantArrayTypeBuilder::createFromConstantArray( + new ConstantArrayType($keyTypes, $valueTypes), + )->getArray(); + } + + return new ArrayType( + 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))), + ); + } + + if ( + class_exists(EnvVarProcessor::class) + && is_string($value) + && preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1 + && strlen($matches[0]) === strlen($value) + ) { + $providedTypes = EnvVarProcessor::getProvidedTypes(); + + return $this->typeStringResolver->resolve($providedTypes[$matches[1]] ?? 'bool|int|float|string|array'); + } + + return $this->generalizeType($scope->getTypeFromValue($value)); } private function generalizeType(Type $type): Type @@ -117,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); @@ -128,20 +207,32 @@ 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; + } + + $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope); + if ($parameterKeys === []) { + return null; } - $parameterKey = $this->parameterMap::getParameterKeyFromNode($methodCall->getArgs()[0]->value, $scope); - if ($parameterKey !== null) { + $has = null; + foreach ($parameterKeys as $parameterKey) { $parameter = $this->parameterMap->getParameter($parameterKey); - return new ConstantBooleanType($parameter !== null); + + if ($has === null) { + $has = $parameter !== null; + } elseif ( + ($has === true && $parameter === null) + || ($has === false && $parameter !== null) + ) { + return null; + } } - return $returnType; + return new ConstantBooleanType($has); } } 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 66372a7b..40d38493 100644 --- a/src/Type/Symfony/RequestTypeSpecifyingExtension.php +++ b/src/Type/Symfony/RequestTypeSpecifyingExtension.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; -use PHPStan\Broker\Broker; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\MethodTypeSpecifyingExtension; @@ -21,16 +20,7 @@ final class RequestTypeSpecifyingExtension implements MethodTypeSpecifyingExtens private const HAS_METHOD_NAME = 'hasSession'; private const GET_METHOD_NAME = 'getSession'; - /** @var Broker */ - private $broker; - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function __construct(Broker $broker) - { - $this->broker = $broker; - } + private TypeSpecifier $typeSpecifier; public function getClass(): string { @@ -44,9 +34,8 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $classReflection = $this->broker->getClass(self::REQUEST_CLASS); - $methodVariants = $classReflection->getNativeMethod(self::GET_METHOD_NAME)->getVariants(); - $returnType = ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType(); + $methodVariants = $methodReflection->getDeclaringClass()->getNativeMethod(self::GET_METHOD_NAME)->getVariants(); + $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodVariants)->getReturnType(); if (!TypeCombinator::containsNull($returnType)) { return new SpecifiedTypes(); @@ -55,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 new file mode 100644 index 00000000..5a95bf98 --- /dev/null +++ b/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php @@ -0,0 +1,65 @@ +getName() === 'getCookies'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + if (isset($methodCall->getArgs()[0])) { + $node = $methodCall->getArgs()[0]->value; + + if ( + $node instanceof ClassConstFetch && + $node->class instanceof Name && + $node->name instanceof Identifier && + $node->class->toString() === ResponseHeaderBag::class && + $node->name->name === 'COOKIES_ARRAY' + ) { + return new ArrayType( + new StringType(), + new ArrayType( + new StringType(), + new ArrayType( + new StringType(), + new ObjectType(Cookie::class), + ), + ), + ); + } + } + + return new ArrayType(new IntegerType(), new ObjectType(Cookie::class)); + } + +} diff --git a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php index fda9b048..84f256e4 100755 --- a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php @@ -6,21 +6,25 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; +use function substr; 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; @@ -44,13 +48,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $argType = $scope->getType($methodCall->getArgs()[1]->value); - if (!$argType instanceof ConstantStringType) { + if (count($argType->getConstantStrings()) === 0) { return new MixedType(); } - $objectName = $argType->getValue(); + $types = []; + foreach ($argType->getConstantStrings() as $constantString) { + $types[] = $this->getType($constantString->getValue()); + } - return $this->getType($objectName); + return TypeCombinator::union(...$types); } private function getType(string $objectName): Type diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php index 650aaa42..0667d30c 100644 --- a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php @@ -5,32 +5,47 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\Symfony\ParameterMap; +use PHPStan\Symfony\ServiceDefinition; use PHPStan\Symfony\ServiceMap; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use function class_exists; use function in_array; +use function is_string; final class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $className; + /** @var class-string */ + private string $className; - /** @var bool */ - private $constantHassers; + private bool $constantHassers; - /** @var \PHPStan\Symfony\ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; - public function __construct(string $className, bool $constantHassers, ServiceMap $symfonyServiceMap) + private ParameterMap $parameterMap; + + private ?ParameterBag $parameterBag = null; + + /** + * @param class-string $className + */ + public function __construct( + string $className, + bool $constantHassers, + ServiceMap $symfonyServiceMap, + ParameterMap $symfonyParameterMap + ) { $this->className = $className; $this->constantHassers = $constantHassers; $this->serviceMap = $symfonyServiceMap; + $this->parameterMap = $symfonyParameterMap; } public function getClass(): string @@ -43,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': @@ -58,33 +73,60 @@ 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 null; } $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); if ($service !== null && (!$service->isSynthetic() || $service->getClass() !== null)) { - return new ObjectType($service->getClass() ?? $serviceId); + return new ObjectType($this->determineServiceClass($parameterBag, $service) ?? $serviceId); } } - return $returnType; + return null; + } + + private function tryGetParameterBag(): ?ParameterBag + { + if ($this->parameterBag !== null) { + return $this->parameterBag; + } + + return $this->parameterBag = $this->tryCreateParameterBag(); + } + + private function tryCreateParameterBag(): ?ParameterBag + { + if (!class_exists(ParameterBag::class)) { + return null; + } + + $parameters = []; + + foreach ($this->parameterMap->getParameters() as $parameterDefinition) { + $parameters[$parameterDefinition->getKey()] = $parameterDefinition->getValue(); + } + + return new ParameterBag($parameters); } 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); @@ -93,7 +135,22 @@ private function getHasTypeFromMethodCall( return new ConstantBooleanType($service !== null && $service->isPublic()); } - return $returnType; + return null; + } + + private function determineServiceClass(ParameterBag $parameterBag, ServiceDefinition $service): ?string + { + $class = $service->getClass(); + if ($class === null) { + return null; + } + + $value = $parameterBag->resolveValue($class); + if (!is_string($value)) { + return null; + } + + return $value; } } diff --git a/src/Type/Symfony/ServiceTypeSpecifyingExtension.php b/src/Type/Symfony/ServiceTypeSpecifyingExtension.php index a5aad54e..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 \PhpParser\PrettyPrinter\Standard */ - private $printer; + private Printer $printer; - /** @var \PHPStan\Analyser\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/Psr/Cache/CacheException.stub b/stubs/Psr/Cache/CacheException.stub new file mode 100644 index 00000000..1be3e49b --- /dev/null +++ b/stubs/Psr/Cache/CacheException.stub @@ -0,0 +1,7 @@ + + * @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/Bundle/FrameworkBundle/KernelBrowser.stub b/stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub new file mode 100644 index 00000000..ec54f1eb --- /dev/null +++ b/stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub @@ -0,0 +1,13 @@ + $config + * + * @return string|string[] + */ + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId); +} diff --git a/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub b/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub new file mode 100644 index 00000000..5c6b54fd --- /dev/null +++ b/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub @@ -0,0 +1,15 @@ + $config + * + * @return string[] + */ + public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array; +} diff --git a/stubs/Symfony/Component/Console/Command.stub b/stubs/Symfony/Component/Console/Command.stub new file mode 100644 index 00000000..c656ef6a --- /dev/null +++ b/stubs/Symfony/Component/Console/Command.stub @@ -0,0 +1,17 @@ + $messages + * @param int-mask-of $options + */ + public function write($messages, bool $newline = false, int $options = 0): void; + + /** + * @param string|iterable $messages + * @param int-mask-of $options + */ + public function writeln($messages, int $options = 0): void; +} diff --git a/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub b/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub index d0b04792..b1229723 100644 --- a/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub +++ b/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub @@ -8,6 +8,8 @@ interface ExtensionInterface { /** * @param array $configs + * + * @throws \InvalidArgumentException */ public function load(array $configs, ContainerBuilder $container): void; } 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/Form/AbstractType.stub b/stubs/Symfony/Component/Form/AbstractType.stub new file mode 100644 index 00000000..e99b746c --- /dev/null +++ b/stubs/Symfony/Component/Form/AbstractType.stub @@ -0,0 +1,31 @@ + + */ +abstract class AbstractType implements FormTypeInterface +{ + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void; + + /** + * @param FormInterface $form + * @param array $options + */ + public function buildView(FormView $view, FormInterface $form, array $options): void; + + /** + * @param FormInterface $form + * @param array $options + */ + public function finishView(FormView $view, FormInterface $form, array $options): void; + +} diff --git a/stubs/Symfony/Component/Form/DataTransformerInterface.stub b/stubs/Symfony/Component/Form/DataTransformerInterface.stub index edd4411e..393fa803 100644 --- a/stubs/Symfony/Component/Form/DataTransformerInterface.stub +++ b/stubs/Symfony/Component/Form/DataTransformerInterface.stub @@ -14,6 +14,8 @@ interface DataTransformerInterface * @phpstan-param T|null $value The value in the original representation * * @phpstan-return R|null The value in the transformed representation + * + * @throws TransformationFailedException */ public function transform($value); @@ -21,6 +23,8 @@ interface DataTransformerInterface * @phpstan-param R|null $value The value in the transformed representation * * @phpstan-return T|null The value in the original representation + * + * @throws TransformationFailedException */ public function reverseTransform($value); } diff --git a/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub b/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub new file mode 100644 index 00000000..306015dc --- /dev/null +++ b/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub @@ -0,0 +1,7 @@ + + * @template TData + * + * @extends \Traversable> + * @extends FormConfigBuilderInterface */ -interface FormBuilderInterface extends \Traversable +interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface { + /** + * @return FormInterface + */ + public function getForm(): FormInterface; + } diff --git a/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub new file mode 100644 index 00000000..a167ce43 --- /dev/null +++ b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub @@ -0,0 +1,13 @@ + + */ +interface FormConfigBuilderInterface extends FormConfigInterface +{ + +} diff --git a/stubs/Symfony/Component/Form/FormConfigInterface.stub b/stubs/Symfony/Component/Form/FormConfigInterface.stub new file mode 100644 index 00000000..942d467b --- /dev/null +++ b/stubs/Symfony/Component/Form/FormConfigInterface.stub @@ -0,0 +1,16 @@ + + * @template TData + * + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) + * + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function create(string $type = FormType::class, $data = null, array $options = []): FormInterface; + + /** + * @template TFormType of FormTypeInterface + * @template TData + * + * @param class-string $type + * @param TData $data + * @param array $options + * + * @phpstan-return ($data is null ? FormInterface : FormInterface) + * + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function createNamed(string $name, string $type = FormType::class, $data = null, array $options = []): FormInterface; +} diff --git a/stubs/Symfony/Component/Form/FormInterface.stub b/stubs/Symfony/Component/Form/FormInterface.stub index 868c4ae2..4bd21229 100644 --- a/stubs/Symfony/Component/Form/FormInterface.stub +++ b/stubs/Symfony/Component/Form/FormInterface.stub @@ -3,9 +3,22 @@ namespace Symfony\Component\Form; /** - * @extends \Traversable + * @template TData + * + * @extends \ArrayAccess> + * @extends \Traversable> */ -interface FormInterface extends \Traversable +interface FormInterface extends \ArrayAccess, \Traversable, \Countable { + /** + * @param TData $modelData + * + * @return $this + */ + public function setData($modelData): FormInterface; + /** + * @return TData + */ + public function getData(); } diff --git a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub index 25b6f25d..a03d5e1c 100644 --- a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub +++ b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub @@ -2,19 +2,25 @@ namespace Symfony\Component\Form; +/** + * @template TData + */ interface FormTypeExtensionInterface { /** + * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function buildView(FormView $view, FormInterface $form, array $options): void; /** + * @phpstan-param FormInterface $form * @param array $options */ public function finishView(FormView $view, FormInterface $form, array $options): void; diff --git a/stubs/Symfony/Component/Form/FormTypeInterface.stub b/stubs/Symfony/Component/Form/FormTypeInterface.stub index cebbc1c2..8536656a 100644 --- a/stubs/Symfony/Component/Form/FormTypeInterface.stub +++ b/stubs/Symfony/Component/Form/FormTypeInterface.stub @@ -2,19 +2,25 @@ namespace Symfony\Component\Form; +/** + * @template TData + */ interface FormTypeInterface { /** + * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void; /** + * @param FormInterface $form * @param array $options */ public function buildView(FormView $view, FormInterface $form, array $options): void; /** + * @param FormInterface $form * @param array $options */ public function finishView(FormView $view, FormInterface $form, array $options): void; diff --git a/stubs/Symfony/Component/Form/FormView.stub b/stubs/Symfony/Component/Form/FormView.stub index 8f920ed6..08c64752 100644 --- a/stubs/Symfony/Component/Form/FormView.stub +++ b/stubs/Symfony/Component/Form/FormView.stub @@ -6,8 +6,8 @@ use ArrayAccess; use IteratorAggregate; /** - * @implements IteratorAggregate - * @implements ArrayAccess + * @implements IteratorAggregate + * @implements ArrayAccess */ class FormView implements ArrayAccess, IteratorAggregate { @@ -15,7 +15,7 @@ class FormView implements ArrayAccess, IteratorAggregate /** * Returns an iterator to iterate over children (implements \IteratorAggregate). * - * @return \ArrayIterator The iterator + * @return \ArrayIterator The iterator */ public function getIterator(); 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/HttpFoundation/ParameterBag.stub b/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub index 00a40741..0a6858e3 100644 --- a/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub +++ b/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub @@ -7,5 +7,10 @@ namespace Symfony\Component\HttpFoundation; */ class ParameterBag implements \IteratorAggregate { - + /** + * @return list + */ + public function keys(): array + { + } } diff --git a/stubs/Symfony/Component/HttpFoundation/Request.stub b/stubs/Symfony/Component/HttpFoundation/Request.stub index 00d03b09..0c2140cc 100644 --- a/stubs/Symfony/Component/HttpFoundation/Request.stub +++ b/stubs/Symfony/Component/HttpFoundation/Request.stub @@ -26,4 +26,47 @@ class Request */ public $cookies; + /** + * @return string[] + */ + public static function getTrustedProxies(): array; + + /** + * @return string[] + */ + public static function getTrustedHosts(): array; + + /** + * @param string $format + * + * @return string[] + */ + public static function getMimeTypes($format): array; + + /** + * @param string|null $format + * @param string|string[] $mimeTypes + */ + public function setFormat($format, $mimeTypes): void; + + /** + * @return string[] + */ + public function getLanguages(): array; + + /** + * @return string[] + */ + public function getCharsets(): array; + + /** + * @return string[] + */ + public function getEncodings(): array; + + /** + * @return string[] + */ + public function getAcceptableContentTypes(): array; + } diff --git a/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub new file mode 100644 index 00000000..2856e32b --- /dev/null +++ b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub @@ -0,0 +1,7 @@ +, value-of> + */ +interface Options extends \ArrayAccess, \Countable +{ + /** + * @param key-of $offset + * + * @return bool + */ + public function offsetExists($offset); + + /** + * @template TOffset of key-of + * @param TOffset $offset + * @return TArray[TOffset] + */ + public function offsetGet($offset); + + /** + * @template TOffset of key-of + * @param TOffset|null $offset + * @param TArray[TOffset] $value + * + * @return void + */ + public function offsetSet($offset, $value); + + /** + * @template TOffset of key-of + * @param TOffset $offset + * + * @return void + */ + public function offsetUnset($offset); +} 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/PropertyAccess/Exception/AccessException.stub b/stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub new file mode 100644 index 00000000..a763b784 --- /dev/null +++ b/stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub @@ -0,0 +1,7 @@ + + * @phpstan-param T &$objectOrArray + * @phpstan-param-out ($objectOrArray is object ? T : array) $objectOrArray + * @phpstan-param string|PropertyPathInterface $propertyPath + * @phpstan-param mixed $value + * + * @return void + * + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\AccessException If a property/index does not exist or is not public + * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array + */ + public function setValue(&$objectOrArray, $propertyPath, $value); + +} diff --git a/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub b/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub new file mode 100644 index 00000000..204a9c40 --- /dev/null +++ b/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub @@ -0,0 +1,7 @@ + $context * @return mixed + * + * @throws UnexpectedValueException */ public function decode($data, $format, array $context = []); /** * @param string $format Format name + * @param array $context * @return bool */ - public function supportsDecoding($format); + public function supportsDecoding($format, array $context = []); } diff --git a/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub index eb6fc97c..11e374eb 100644 --- a/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub +++ b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub @@ -2,6 +2,8 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + interface EncoderInterface { /** @@ -9,12 +11,15 @@ interface EncoderInterface * @param string $format * @param array $context * @return string + * + * @throws UnexpectedValueException */ public function encode($data, $format, array $context = []); /** * @param string $format Format name + * @param array $context * @return bool */ - public function supportsEncoding($format); + public function supportsEncoding($format, array $context = []); } diff --git a/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub b/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub new file mode 100644 index 00000000..3c569971 --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub @@ -0,0 +1,7 @@ + $context - * @return object|array + * @return mixed + * + * @throws BadMethodCallException + * @throws InvalidArgumentException + * @throws UnexpectedValueException + * @throws ExtraAttributesException + * @throws LogicException + * @throws RuntimeException + * @throws ExceptionInterface */ public function denormalize($data, $type, $format = null, array $context = []); @@ -17,7 +33,8 @@ interface DenormalizerInterface * @param mixed $data * @param string $type * @param string|null $format + * @param array $context * @return bool */ - public function supportsDenormalization($data, $type, $format = null); + public function supportsDenormalization($data, $type, $format = null, array $context = []); } diff --git a/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub index a65b5bed..ba86b6b6 100644 --- a/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub +++ b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub @@ -2,20 +2,34 @@ namespace Symfony\Component\Serializer\Normalizer; +use ArrayObject; +use Symfony\Component\Serializer\Exception\CircularReferenceException; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; + interface NormalizerInterface { /** * @param mixed $object * @param string|null $format * @param array $context - * @return array|string|int|float|bool|null + * + * @return array|ArrayObject|string|int|float|bool|null + * + * @throws InvalidArgumentException + * @throws CircularReferenceException + * @throws LogicException + * @throws ExceptionInterface */ public function normalize($object, $format = null, array $context = []); /** * @param mixed $data * @param string|null $format + * @param array $context + * * @return bool */ - public function supportsNormalization($data, $format = null); + public function supportsNormalization($data, $format = null, array $context = []); } diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub index ef3312bb..fd1c7b9b 100644 --- a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub +++ b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub @@ -2,6 +2,11 @@ namespace Symfony\Component\Validator; +/** + * @method Constraint|null getConstraint() Returns the constraint whose validation caused the violation. Not implementing it is deprecated since Symfony 6.3. + * @method mixed getCause() Returns the cause of the violation. Not implementing it is deprecated since Symfony 6.2. + * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. + */ interface ConstraintViolationInterface { } diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub index 423bdfb0..00a2a9d5 100644 --- a/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub +++ b/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub @@ -3,8 +3,11 @@ namespace Symfony\Component\Validator; /** + * @extends \ArrayAccess * @extends \Traversable + * + * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. */ -interface ConstraintViolationListInterface extends \Traversable +interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess { } diff --git a/stubs/Symfony/Component/Validator/Constraints/Composite.stub b/stubs/Symfony/Component/Validator/Constraints/Composite.stub new file mode 100644 index 00000000..8344ea94 --- /dev/null +++ b/stubs/Symfony/Component/Validator/Constraints/Composite.stub @@ -0,0 +1,9 @@ + $options + * @return array + */ + abstract protected function getConstraints(array $options): array; +} diff --git a/stubs/Symfony/Contracts/Cache/CacheInterface.stub b/stubs/Symfony/Contracts/Cache/CacheInterface.stub new file mode 100644 index 00000000..a361ead4 --- /dev/null +++ b/stubs/Symfony/Contracts/Cache/CacheInterface.stub @@ -0,0 +1,19 @@ +|callable(\Symfony\Contracts\Cache\ItemInterface, bool): T $callback + * @param array $metadata + * @return T + * + * @throws InvalidArgumentException + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null); +} diff --git a/stubs/Symfony/Contracts/Cache/CallbackInterface.stub b/stubs/Symfony/Contracts/Cache/CallbackInterface.stub new file mode 100644 index 00000000..9b5a6e1a --- /dev/null +++ b/stubs/Symfony/Contracts/Cache/CallbackInterface.stub @@ -0,0 +1,16 @@ +getByType(CallMethodsRule::class); } diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php index 81925291..bbecb2e8 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; +use function class_exists; +use function interface_exists; /** * @extends RuleTestCase @@ -26,7 +28,7 @@ public function testGetPrivateService(): void [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } @@ -39,7 +41,7 @@ public function testGetPrivateServiceInAbstractController(): void [ __DIR__ . '/ExampleAbstractController.php', ], - [] + [], ); } @@ -59,7 +61,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -79,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 b3a8fa31..dfa3d2b7 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; +use function class_exists; +use function interface_exists; /** * @extends RuleTestCase @@ -29,9 +31,9 @@ public function testGetPrivateService(): void [ [ 'Service "private" is private.', - 12, + 13, ], - ] + ], ); } @@ -51,7 +53,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -71,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 33e3015d..8d70f1c3 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php @@ -2,11 +2,14 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; 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; /** * @extends RuleTestCase @@ -16,16 +19,16 @@ final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), self::getContainer()->getByType(Printer::class)); } /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] + * @return MethodTypeSpecifyingExtension[] */ protected function getMethodTypeSpecifyingExtensions(): array { return [ - new ServiceTypeSpecifyingExtension('Symfony\Bundle\FrameworkBundle\Controller\Controller', new Standard()), + new ServiceTypeSpecifyingExtension(AbstractController::class, self::getContainer()->getByType(Printer::class)), ]; } @@ -38,7 +41,7 @@ public function testGetPrivateService(): void [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } @@ -52,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 297158a6..c975750f 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; +use function class_exists; +use function interface_exists; /** * @extends RuleTestCase @@ -15,7 +17,7 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), self::getContainer()->getByType(Printer::class)); } public function testGetPrivateService(): void @@ -30,9 +32,9 @@ public function testGetPrivateService(): void [ [ 'Service "unknown" is not registered in the container.', - 24, + 25, ], - ] + ], ); } @@ -49,9 +51,9 @@ public function testGetPrivateServiceInAbstractController(): void [ [ 'Service "unknown" is not registered in the container.', - 24, + 25, ], - ] + ], ); } @@ -65,7 +67,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void [ __DIR__ . '/ExampleServiceSubscriber.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ExampleAbstractController.php b/tests/Rules/Symfony/ExampleAbstractController.php index 4f0d2abd..22e5900e 100644 --- a/tests/Rules/Symfony/ExampleAbstractController.php +++ b/tests/Rules/Symfony/ExampleAbstractController.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Symfony; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Test\TestContainer; final class ExampleAbstractController extends AbstractController { @@ -14,7 +15,7 @@ public function privateService(): void public function privateServiceInTestContainer(): void { - /** @var \Symfony\Bundle\FrameworkBundle\Test\TestContainer $container */ + /** @var TestContainer $container */ $container = doFoo(); $container->get('private'); } diff --git a/tests/Rules/Symfony/ExampleController.php b/tests/Rules/Symfony/ExampleController.php index edbdaaf5..5b9e1ca4 100644 --- a/tests/Rules/Symfony/ExampleController.php +++ b/tests/Rules/Symfony/ExampleController.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Symfony; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Test\TestContainer; final class ExampleController extends Controller { @@ -14,7 +15,7 @@ public function privateService(): void public function privateServiceInTestContainer(): void { - /** @var \Symfony\Bundle\FrameworkBundle\Test\TestContainer $container */ + /** @var TestContainer $container */ $container = doFoo(); $container->get('private'); } diff --git a/tests/Rules/Symfony/ExampleServiceSubscriber.php b/tests/Rules/Symfony/ExampleServiceSubscriber.php index 3d7f294d..ec9c966d 100644 --- a/tests/Rules/Symfony/ExampleServiceSubscriber.php +++ b/tests/Rules/Symfony/ExampleServiceSubscriber.php @@ -3,13 +3,13 @@ namespace PHPStan\Rules\Symfony; use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; use Symfony\Contracts\Service\ServiceSubscriberInterface; final class ExampleServiceSubscriber implements ServiceSubscriberInterface { - /** @var ContainerInterface */ - private $locator; + private ContainerInterface $locator; public function __construct(ContainerInterface $locator) { @@ -24,7 +24,7 @@ public function privateService(): void public function containerParameter(): void { - /** @var \Symfony\Component\DependencyInjection\ParameterBag\ContainerBag $containerBag */ + /** @var ContainerBag $containerBag */ $containerBag = doFoo(); $containerBag->get('parameter_name'); } 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 b3f9e56e..d9970ef6 100644 --- a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php +++ b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Testing\RuleTestCase; @@ -15,7 +15,7 @@ final class UndefinedArgumentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedArgumentRule(new ConsoleApplicationResolver(__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 @@ -29,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 32ebe773..7f759213 100644 --- a/tests/Rules/Symfony/UndefinedOptionRuleTest.php +++ b/tests/Rules/Symfony/UndefinedOptionRuleTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Symfony; -use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Symfony\ConsoleApplicationResolver; use PHPStan\Testing\RuleTestCase; @@ -15,7 +15,7 @@ final class UndefinedOptionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedOptionRule(new ConsoleApplicationResolver(__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 @@ -29,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 0b97d078..018a68a9 100644 --- a/tests/Symfony/DefaultParameterMapTest.php +++ b/tests/Symfony/DefaultParameterMapTest.php @@ -26,19 +26,19 @@ public function testGetParameterEscapedPath(): void } /** - * @return \Iterator + * @return Iterator */ public function getParameterProvider(): Iterator { yield [ 'unknown', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNull($parameter); }, ]; yield [ 'app.string', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.string', $parameter->getKey()); self::assertSame('abcdef', $parameter->getValue()); @@ -46,7 +46,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.int', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.int', $parameter->getKey()); self::assertSame(123, $parameter->getValue()); @@ -54,7 +54,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.int_as_string', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.int_as_string', $parameter->getKey()); self::assertSame('123', $parameter->getValue()); @@ -62,7 +62,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.float', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.float', $parameter->getKey()); self::assertSame(123.45, $parameter->getValue()); @@ -70,7 +70,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.float_as_string', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.float_as_string', $parameter->getKey()); self::assertSame('123.45', $parameter->getValue()); @@ -78,7 +78,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.boolean', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.boolean', $parameter->getKey()); self::assertTrue($parameter->getValue()); @@ -86,7 +86,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.boolean_as_string', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.boolean_as_string', $parameter->getKey()); self::assertSame('true', $parameter->getValue()); @@ -94,7 +94,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.list', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.list', $parameter->getKey()); self::assertEquals(['en', 'es', 'fr'], $parameter->getValue()); @@ -102,7 +102,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.list_of_list', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.list_of_list', $parameter->getKey()); self::assertEquals([ @@ -113,7 +113,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.map', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.map', $parameter->getKey()); self::assertEquals([ @@ -125,7 +125,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.binary', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.binary', $parameter->getKey()); self::assertSame('This is a Bell char ', $parameter->getValue()); @@ -133,7 +133,7 @@ function (?Parameter $parameter): void { ]; yield [ 'app.constant', - function (?Parameter $parameter): void { + static function (?Parameter $parameter): void { self::assertNotNull($parameter); self::assertSame('app.constant', $parameter->getKey()); self::assertSame('Y-m-d\TH:i:sP', $parameter->getValue()); diff --git a/tests/Symfony/DefaultServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php index 8616ffaf..b43bee49 100644 --- a/tests/Symfony/DefaultServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -26,19 +26,19 @@ public function testGetContainerEscapedPath(): void } /** - * @return \Iterator + * @return Iterator */ public function getServiceProvider(): Iterator { yield [ 'unknown', - function (?Service $service): void { + static function (?Service $service): void { self::assertNull($service); }, ]; yield [ 'withoutClass', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('withoutClass', $service->getId()); self::assertNull($service->getClass()); @@ -49,7 +49,7 @@ function (?Service $service): void { ]; yield [ 'withClass', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('withClass', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -60,7 +60,7 @@ function (?Service $service): void { ]; yield [ 'withoutPublic', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('withoutPublic', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -71,7 +71,7 @@ function (?Service $service): void { ]; yield [ 'publicNotTrue', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('publicNotTrue', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -82,7 +82,7 @@ function (?Service $service): void { ]; yield [ 'public', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('public', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -93,7 +93,7 @@ function (?Service $service): void { ]; yield [ 'synthetic', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('synthetic', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -104,7 +104,7 @@ function (?Service $service): void { ]; yield [ 'alias', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('alias', $service->getId()); self::assertSame('Foo', $service->getClass()); @@ -113,6 +113,17 @@ 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/RequiredAutowiringExtensionTest.php b/tests/Symfony/RequiredAutowiringExtensionTest.php new file mode 100644 index 00000000..93fb3822 --- /dev/null +++ b/tests/Symfony/RequiredAutowiringExtensionTest.php @@ -0,0 +1,65 @@ + + */ +final class RequiredAutowiringExtensionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $container = self::getContainer(); + $container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG); + + return $container->getByType(UninitializedPropertyRule::class); + } + + public function testRequiredAnnotations(): void + { + $this->analyse([__DIR__ . '/data/required-annotations.php'], [ + [ + 'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $three. Give it default value or assign it in the constructor.', + 12, + ], + [ + 'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $four. Give it default value or assign it in the constructor.', + 14, + ], + ]); + } + + public function testRequiredAttributes(): void + { + if (!class_exists(Required::class)) { + self::markTestSkipped('Required symfony/service-contracts@3.2.1 or higher is not installed'); + } + + $this->analyse([__DIR__ . '/data/required-attributes.php'], [ + [ + 'Class RequiredAttributesTest\TestAttributes has an uninitialized property $three. Give it default value or assign it in the constructor.', + 14, + ], + [ + 'Class RequiredAttributesTest\TestAttributes has an uninitialized property $four. Give it default value or assign it in the constructor.', + 16, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/required-autowiring-config.neon', + ]; + } + +} 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/Symfony/data/required-annotations.php b/tests/Symfony/data/required-annotations.php new file mode 100644 index 00000000..e2085ab3 --- /dev/null +++ b/tests/Symfony/data/required-annotations.php @@ -0,0 +1,38 @@ += 7.4 + +namespace RequiredAnnotationTest; + +class TestAnnotations +{ + /** @required */ + public string $one; + + private string $two; + + public string $three; + + private string $four; + + /** + * @required + */ + public function setTwo(int $two): void + { + $this->two = $two; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setFour(int $four): void + { + $this->four = $four; + } + + public function getFour(): int + { + return $this->four; + } +} diff --git a/tests/Symfony/data/required-attributes.php b/tests/Symfony/data/required-attributes.php new file mode 100644 index 00000000..d847d276 --- /dev/null +++ b/tests/Symfony/data/required-attributes.php @@ -0,0 +1,38 @@ += 8.0 + +namespace RequiredAttributesTest; + +use Symfony\Contracts\Service\Attribute\Required; + +class TestAttributes +{ + #[Required] + public string $one; + + private string $two; + + public string $three; + + private string $four; + + #[Required] + public function setTwo(int $two): void + { + $this->two = $two; + } + + public function getTwo(): int + { + return $this->two; + } + + public function setFour(int $four): void + { + $this->four = $four; + } + + public function getFour(): int + { + return $this->four; + } +} diff --git a/tests/Symfony/required-autowiring-config.neon b/tests/Symfony/required-autowiring-config.neon new file mode 100644 index 00000000..3ff4183c --- /dev/null +++ b/tests/Symfony/required-autowiring-config.neon @@ -0,0 +1,6 @@ +services: + - + class: PHPStan\Symfony\RequiredAutowiringExtension + tags: + - phpstan.properties.readWriteExtension + - phpstan.additionalConstructorsExtension diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index 5a49366b..40420be0 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -5,6 +5,8 @@ use PHPStan\Testing\TypeInferenceTestCase; use ReflectionMethod; use Symfony\Component\HttpFoundation\Request; +use function class_exists; +use function strpos; class ExtensionTest extends TypeInferenceTestCase { @@ -12,18 +14,22 @@ 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'); if (class_exists('Symfony\Component\HttpFoundation\InputBag')) { yield from $this->gatherAssertTypes(__DIR__ . '/data/input_bag.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/tree_builder.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleBaseCommand.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionCommand.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionLazyCommand.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/kernel_interface.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/property_accessor.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/request_get_content.php'); $ref = new ReflectionMethod(Request::class, 'getSession'); @@ -49,12 +55,23 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/denormalizer.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/FormInterface_getErrors.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/cache.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/form_data_type.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration/WithConfigurationExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/without-configuration/WithoutConfigurationExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/anonymous/AnonymousExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/ignore-implemented/IgnoreImplementedExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/multiple-types/MultipleTypes.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor/WithConfigurationWithConstructorExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-optional-params/WithConfigurationWithConstructorOptionalParamsExtension.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-required-params/WithConfigurationWithConstructorRequiredParamsExtension.php'); } /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( @@ -71,6 +88,7 @@ public static function getAdditionalConfigFiles(): array return [ __DIR__ . '/../../../extension.neon', __DIR__ . '/extension-test.neon', + 'phar://' . __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon', ]; } diff --git a/tests/Type/Symfony/ExtensionTestWithoutContainer.php b/tests/Type/Symfony/ExtensionTestWithoutContainer.php index 997b7e02..fd1785c7 100644 --- a/tests/Type/Symfony/ExtensionTestWithoutContainer.php +++ b/tests/Type/Symfony/ExtensionTestWithoutContainer.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Symfony; use PHPStan\Testing\TypeInferenceTestCase; +use function class_exists; class ExtensionTestWithoutContainer extends TypeInferenceTestCase { @@ -30,8 +31,6 @@ public function dataAbstractController(): iterable /** * @dataProvider dataExampleController * @dataProvider dataAbstractController - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( diff --git a/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php index ab247b9f..bde62b57 100644 --- a/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Type/Symfony/console_application_loader.php b/tests/Type/Symfony/console_application_loader.php index 0ef2e0ae..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, 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 f4240c3b..16d4b7fe 100644 --- a/tests/Type/Symfony/container.xml +++ b/tests/Type/Symfony/container.xml @@ -1,18 +1,32 @@ + Foo abcdef 123 123 + %env(int:APP_INT)% 123.45 123.45 + %env(float:APP_FLOAT)% true true + %env(bool:APP_BOOL)% en es fr + + 123 + 456 + 789 + + + %env(int:APP_INT)% + %env(int:APP_INT)% + %env(int:APP_INT)% + the name @@ -23,11 +37,310 @@ another value + + + the name + the value + + + 12 + 32 + + + + + the name + the value + + + another name + another value + + + + %env(string:APP_STRING)% + %env(string:APP_STRING)% + %env(string:APP_STRING)% + + + %env(string:APP_STRING)% + %env(string:APP_STRING)% + %env(string:APP_STRING)% + + + + %env(string:APP_STRING)% + + %env(string:APP_STRING)% + %env(string:APP_STRING)% + %env(string:APP_STRING)% + + + value of a 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 @@ -38,6 +351,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Type/Symfony/data/ExampleAbstractController.php b/tests/Type/Symfony/data/ExampleAbstractController.php index 4dbaffa7..53b38066 100644 --- a/tests/Type/Symfony/data/ExampleAbstractController.php +++ b/tests/Type/Symfony/data/ExampleAbstractController.php @@ -13,6 +13,8 @@ final class ExampleAbstractController extends AbstractController public function services(): void { assertType('Foo', $this->get('foo')); + assertType('Foo', $this->get('parameterised_foo')); + assertType('Foo\Bar', $this->get('parameterised_bar')); assertType('Synthetic', $this->get('synthetic')); assertType('object', $this->get('bar')); assertType('object', $this->get(doFoo())); @@ -39,6 +41,9 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType("string", $container->getParameter('app.int_as_string')); assertType("string", $parameterBag->get('app.int_as_string')); assertType("string", $this->getParameter('app.int_as_string')); + assertType('int', $container->getParameter('app.int_as_processor')); + assertType('int', $parameterBag->get('app.int_as_processor')); + assertType('int', $this->getParameter('app.int_as_processor')); assertType('float', $container->getParameter('app.float')); assertType('float', $parameterBag->get('app.float')); assertType('float', $this->getParameter('app.float')); @@ -54,12 +59,28 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType("array", $container->getParameter('app.list')); assertType("array", $parameterBag->get('app.list')); assertType("array", $this->getParameter('app.list')); - assertType("array>", $container->getParameter('app.list_of_list')); - assertType("array>", $parameterBag->get('app.list_of_list')); - assertType("array>", $this->getParameter('app.list_of_list')); - assertType("array", $container->getParameter('app.map')); - assertType("array", $parameterBag->get('app.map')); - assertType("array", $this->getParameter('app.map')); + assertType("array", $container->getParameter('app.list_of_int')); + assertType("array", $parameterBag->get('app.list_of_int')); + assertType("array", $this->getParameter('app.list_of_int')); + assertType("array", $container->getParameter('app.list_of_int_as_processor')); + assertType("array", $parameterBag->get('app.list_of_int_as_processor')); + assertType("array", $this->getParameter('app.list_of_int_as_processor')); + assertType("array", $container->getParameter('app.list_of_list')); + assertType("array", $parameterBag->get('app.list_of_list')); + assertType("array", $this->getParameter('app.list_of_list')); + assertType("array", $container->getParameter('app.list_of_different_list')); + assertType("array", $parameterBag->get('app.list_of_different_list')); + assertType("array", $this->getParameter('app.list_of_different_list')); + assertType("array", $container->getParameter('app.array_of_list')); + assertType("array", $parameterBag->get('app.array_of_list')); + assertType("array", $this->getParameter('app.array_of_list')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $container->getParameter('app.list_of_things')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $parameterBag->get('app.list_of_things')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $this->getParameter('app.list_of_things')); + 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')); @@ -77,6 +98,8 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('true', $parameterBag->has('app.int')); assertType('true', $container->hasParameter('app.int_as_string')); assertType('true', $parameterBag->has('app.int_as_string')); + assertType('true', $container->hasParameter('app.int_as_processor')); + assertType('true', $parameterBag->has('app.int_as_processor')); assertType('true', $container->hasParameter('app.float')); assertType('true', $parameterBag->has('app.float')); assertType('true', $container->hasParameter('app.float_as_string')); diff --git a/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php index 7ca06775..edc6438a 100644 --- a/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php +++ b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php @@ -38,24 +38,36 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.float')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.float')); assertType('array|bool|float|int|string|null', $this->getParameter('app.float')); assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean')); assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean')); assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.list')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.list')); assertType('array|bool|float|int|string|null', $this->getParameter('app.list')); assertType('array|bool|float|int|string|null', $container->getParameter('app.list_of_list')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.list_of_list')); assertType('array|bool|float|int|string|null', $this->getParameter('app.list_of_list')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.array_of_list')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.array_of_list')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.array_of_list')); assertType('array|bool|float|int|string|null', $container->getParameter('app.map')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.map')); assertType('array|bool|float|int|string|null', $this->getParameter('app.map')); @@ -74,14 +86,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('bool', $parameterBag->has('app.int')); assertType('bool', $container->hasParameter('app.int_as_string')); assertType('bool', $parameterBag->has('app.int_as_string')); + assertType('bool', $container->hasParameter('app.int_as_processor')); + assertType('bool', $parameterBag->has('app.int_as_processor')); assertType('bool', $container->hasParameter('app.float')); assertType('bool', $parameterBag->has('app.float')); assertType('bool', $container->hasParameter('app.float_as_string')); assertType('bool', $parameterBag->has('app.float_as_string')); + assertType('bool', $container->hasParameter('app.float_as_processor')); + assertType('bool', $parameterBag->has('app.float_as_processor')); assertType('bool', $container->hasParameter('app.boolean')); assertType('bool', $parameterBag->has('app.boolean')); assertType('bool', $container->hasParameter('app.boolean_as_string')); assertType('bool', $parameterBag->has('app.boolean_as_string')); + assertType('bool', $container->hasParameter('app.boolean_as_processor')); + assertType('bool', $parameterBag->has('app.boolean_as_processor')); assertType('bool', $container->hasParameter('app.list')); assertType('bool', $parameterBag->has('app.list')); assertType('bool', $container->hasParameter('app.list_of_list')); diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php index a4493b2c..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,17 +15,53 @@ 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')); + assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question')); } } diff --git a/tests/Type/Symfony/data/ExampleController.php b/tests/Type/Symfony/data/ExampleController.php index 1fe8c983..c7563537 100644 --- a/tests/Type/Symfony/data/ExampleController.php +++ b/tests/Type/Symfony/data/ExampleController.php @@ -13,6 +13,8 @@ final class ExampleController extends Controller public function services(): void { assertType('Foo', $this->get('foo')); + assertType('Foo', $this->get('parameterised_foo')); + assertType('Foo\Bar', $this->get('parameterised_bar')); assertType('Synthetic', $this->get('synthetic')); assertType('object', $this->get('bar')); assertType('object', $this->get(doFoo())); @@ -39,27 +41,45 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType("string", $container->getParameter('app.int_as_string')); assertType("string", $parameterBag->get('app.int_as_string')); assertType("string", $this->getParameter('app.int_as_string')); + assertType('int', $container->getParameter('app.int_as_processor')); + assertType('int', $parameterBag->get('app.int_as_processor')); + assertType('int', $this->getParameter('app.int_as_processor')); assertType('float', $container->getParameter('app.float')); assertType('float', $parameterBag->get('app.float')); assertType('float', $this->getParameter('app.float')); assertType("string", $container->getParameter('app.float_as_string')); assertType("string", $parameterBag->get('app.float_as_string')); assertType("string", $this->getParameter('app.float_as_string')); + assertType('float', $container->getParameter('app.float_as_processor')); + assertType('float', $parameterBag->get('app.float_as_processor')); + assertType('float', $this->getParameter('app.float_as_processor')); assertType('bool', $container->getParameter('app.boolean')); assertType('bool', $parameterBag->get('app.boolean')); assertType('bool', $this->getParameter('app.boolean')); assertType("string", $container->getParameter('app.boolean_as_string')); assertType("string", $parameterBag->get('app.boolean_as_string')); assertType("string", $this->getParameter('app.boolean_as_string')); + assertType('bool', $container->getParameter('app.boolean_as_processor')); + assertType('bool', $parameterBag->get('app.boolean_as_processor')); + assertType('bool', $this->getParameter('app.boolean_as_processor')); assertType("array", $container->getParameter('app.list')); assertType("array", $parameterBag->get('app.list')); assertType("array", $this->getParameter('app.list')); - assertType("array>", $container->getParameter('app.list_of_list')); - assertType("array>", $parameterBag->get('app.list_of_list')); - assertType("array>", $this->getParameter('app.list_of_list')); - assertType("array", $container->getParameter('app.map')); - assertType("array", $parameterBag->get('app.map')); - assertType("array", $this->getParameter('app.map')); + assertType("array", $container->getParameter('app.list_of_list')); + assertType("array", $parameterBag->get('app.list_of_list')); + assertType("array", $this->getParameter('app.list_of_list')); + assertType("array", $container->getParameter('app.list_of_different_list')); + assertType("array", $parameterBag->get('app.list_of_different_list')); + assertType("array", $this->getParameter('app.list_of_different_list')); + assertType("array", $container->getParameter('app.array_of_list')); + assertType("array", $parameterBag->get('app.array_of_list')); + assertType("array", $this->getParameter('app.array_of_list')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $container->getParameter('app.list_of_things')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $parameterBag->get('app.list_of_things')); + assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $this->getParameter('app.list_of_things')); + 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("string", $container->getParameter('app.binary')); assertType("string", $parameterBag->get('app.binary')); assertType("string", $this->getParameter('app.binary')); @@ -75,14 +95,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('true', $parameterBag->has('app.int')); assertType('true', $container->hasParameter('app.int_as_string')); assertType('true', $parameterBag->has('app.int_as_string')); + assertType('true', $container->hasParameter('app.int_as_processor')); + assertType('true', $parameterBag->has('app.int_as_processor')); assertType('true', $container->hasParameter('app.float')); assertType('true', $parameterBag->has('app.float')); assertType('true', $container->hasParameter('app.float_as_string')); assertType('true', $parameterBag->has('app.float_as_string')); + assertType('true', $container->hasParameter('app.float_as_processor')); + assertType('true', $parameterBag->has('app.float_as_processor')); assertType('true', $container->hasParameter('app.boolean')); assertType('true', $parameterBag->has('app.boolean')); assertType('true', $container->hasParameter('app.boolean_as_string')); assertType('true', $parameterBag->has('app.boolean_as_string')); + assertType('true', $container->hasParameter('app.boolean_as_processor')); + assertType('true', $parameterBag->has('app.boolean_as_processor')); assertType('true', $container->hasParameter('app.list')); assertType('true', $parameterBag->has('app.list')); assertType('true', $container->hasParameter('app.list_of_list')); @@ -93,6 +119,27 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('true', $parameterBag->has('app.binary')); assertType('true', $container->hasParameter('app.constant')); assertType('true', $parameterBag->has('app.constant')); + + $key = rand(0, 1) ? 'app.string' : 'app.int'; + assertType("int|string", $container->getParameter($key)); + assertType("int|string", $parameterBag->get($key)); + assertType("int|string", $this->getParameter($key)); + assertType('true', $container->hasParameter($key)); + assertType('true', $parameterBag->has($key)); + + $key = rand(0, 1) ? 'app.string' : 'app.foo'; + assertType("array|bool|float|int|string|null", $container->getParameter($key)); + assertType("array|bool|float|int|string|null", $parameterBag->get($key)); + assertType("array|bool|float|int|string|null", $this->getParameter($key)); + assertType('bool', $container->hasParameter($key)); + assertType('bool', $parameterBag->has($key)); + + $key = rand(0, 1) ? 'app.bar' : 'app.foo'; + assertType("array|bool|float|int|string|null", $container->getParameter($key)); + assertType("array|bool|float|int|string|null", $parameterBag->get($key)); + assertType("array|bool|float|int|string|null", $this->getParameter($key)); + assertType('false', $container->hasParameter($key)); + assertType('false', $parameterBag->has($key)); } } diff --git a/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php b/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php index 2e9b7ba2..2e48dc80 100644 --- a/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php +++ b/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php @@ -38,24 +38,36 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.float')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.float')); assertType('array|bool|float|int|string|null', $this->getParameter('app.float')); assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean')); assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean')); assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_string')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_string')); assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_processor')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_processor')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_processor')); assertType('array|bool|float|int|string|null', $container->getParameter('app.list')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.list')); assertType('array|bool|float|int|string|null', $this->getParameter('app.list')); assertType('array|bool|float|int|string|null', $container->getParameter('app.list_of_list')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.list_of_list')); assertType('array|bool|float|int|string|null', $this->getParameter('app.list_of_list')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.array_of_list')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.array_of_list')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.array_of_list')); assertType('array|bool|float|int|string|null', $container->getParameter('app.map')); assertType('array|bool|float|int|string|null', $parameterBag->get('app.map')); assertType('array|bool|float|int|string|null', $this->getParameter('app.map')); @@ -74,14 +86,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface assertType('bool', $parameterBag->has('app.int')); assertType('bool', $container->hasParameter('app.int_as_string')); assertType('bool', $parameterBag->has('app.int_as_string')); + assertType('bool', $container->hasParameter('app.int_as_processor')); + assertType('bool', $parameterBag->has('app.int_as_processor')); assertType('bool', $container->hasParameter('app.float')); assertType('bool', $parameterBag->has('app.float')); assertType('bool', $container->hasParameter('app.float_as_string')); assertType('bool', $parameterBag->has('app.float_as_string')); + assertType('bool', $container->hasParameter('app.float_as_processor')); + assertType('bool', $parameterBag->has('app.float_as_processor')); assertType('bool', $container->hasParameter('app.boolean')); assertType('bool', $parameterBag->has('app.boolean')); assertType('bool', $container->hasParameter('app.boolean_as_string')); assertType('bool', $parameterBag->has('app.boolean_as_string')); + assertType('bool', $container->hasParameter('app.boolean_as_processor')); + assertType('bool', $parameterBag->has('app.boolean_as_processor')); assertType('bool', $container->hasParameter('app.list')); assertType('bool', $parameterBag->has('app.list')); assertType('bool', $container->hasParameter('app.list_of_list')); diff --git a/tests/Type/Symfony/data/ExampleOptionCommand.php b/tests/Type/Symfony/data/ExampleOptionCommand.php index b6a07574..b880173d 100644 --- a/tests/Type/Symfony/data/ExampleOptionCommand.php +++ b/tests/Type/Symfony/data/ExampleOptionCommand.php @@ -21,6 +21,7 @@ protected function configure(): void $this->addOption('c', null, InputOption::VALUE_REQUIRED); $this->addOption('d', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL); $this->addOption('e', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED); + $this->addOption('f', null, InputOption::VALUE_NEGATABLE); $this->addOption('bb', null, InputOption::VALUE_OPTIONAL, '', 1); $this->addOption('cc', null, InputOption::VALUE_REQUIRED, '', 1); @@ -35,11 +36,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int assertType('string|null', $input->getOption('c')); assertType('array', $input->getOption('d')); assertType('array', $input->getOption('e')); + assertType('bool|null', $input->getOption('f')); assertType('1|string|null', $input->getOption('bb')); assertType('1|string', $input->getOption('cc')); assertType('array', $input->getOption('dd')); assertType('array', $input->getOption('ee')); + + assertType('array{a: bool, b: string|null, c: string|null, d: array, e: array, f: bool|null, bb: 1|string|null, cc: 1|string, dd: array, ee: array, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool|null, no-interaction: bool}', $input->getOptions()); } } diff --git a/tests/Type/Symfony/data/ExampleOptionLazyCommand.php b/tests/Type/Symfony/data/ExampleOptionLazyCommand.php index 7933cd41..433e1cfe 100644 --- a/tests/Type/Symfony/data/ExampleOptionLazyCommand.php +++ b/tests/Type/Symfony/data/ExampleOptionLazyCommand.php @@ -23,6 +23,7 @@ protected function configure(): void $this->addOption('c', null, InputOption::VALUE_REQUIRED); $this->addOption('d', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL); $this->addOption('e', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED); + $this->addOption('f', null, InputOption::VALUE_NEGATABLE); $this->addOption('bb', null, InputOption::VALUE_OPTIONAL, '', 1); $this->addOption('cc', null, InputOption::VALUE_REQUIRED, '', 1); @@ -37,11 +38,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int assertType('string|null', $input->getOption('c')); assertType('array', $input->getOption('d')); assertType('array', $input->getOption('e')); + assertType('bool|null', $input->getOption('f')); assertType('1|string|null', $input->getOption('bb')); assertType('1|string', $input->getOption('cc')); assertType('array', $input->getOption('dd')); assertType('array', $input->getOption('ee')); + + assertType('array{a: bool, b: string|null, c: string|null, d: array, e: array, f: bool|null, bb: 1|string|null, cc: 1|string, dd: array, ee: array, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool|null, no-interaction: bool}', $input->getOptions()); } } diff --git a/tests/Type/Symfony/data/FormInterface_getErrors.php b/tests/Type/Symfony/data/FormInterface_getErrors.php new file mode 100644 index 00000000..a360a21d --- /dev/null +++ b/tests/Type/Symfony/data/FormInterface_getErrors.php @@ -0,0 +1,20 @@ +', $form->getErrors()); +assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false)); +assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false, true)); + +assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(true)); +assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(true, true)); + +assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false, false)); + +assertType(FormErrorIterator::class . '<'. FormError::class .'|'. FormErrorIterator::class . '>', $form->getErrors(true, false)); diff --git a/tests/Type/Symfony/data/cache.php b/tests/Type/Symfony/data/cache.php new file mode 100644 index 00000000..a8862177 --- /dev/null +++ b/tests/Type/Symfony/data/cache.php @@ -0,0 +1,42 @@ +get('foo', function (): string { + return ''; + }); + + assertType('string', $result); +}; + +/** + * @param callable():string $fn + */ +function testNonScalarCacheCallable(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void { + $result = $cache->get('foo', $fn); + + assertType('string', $result); +}; + + +/** + * @param callable():non-empty-string $fn + */ +function testCacheCallableReturnTypeGeneralization(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void { + $result = $cache->get('foo', $fn); + + assertType('string', $result); +}; + + +/** + * @param \Symfony\Contracts\Cache\CallbackInterface<\stdClass> $cb + */ + function testCacheCallbackInterface(\Symfony\Contracts\Cache\CacheInterface $cache, \Symfony\Contracts\Cache\CallbackInterface $cb): void { + $result = $cache->get('foo',$cb); + + assertType('stdClass', $result); +}; diff --git a/tests/Type/Symfony/data/envelope_all.php b/tests/Type/Symfony/data/envelope_all.php index 95521530..aac9d583 100644 --- a/tests/Type/Symfony/data/envelope_all.php +++ b/tests/Type/Symfony/data/envelope_all.php @@ -4,6 +4,6 @@ $envelope = new \Symfony\Component\Messenger\Envelope(new stdClass()); -assertType('array', $envelope->all(\Symfony\Component\Messenger\Stamp\ReceivedStamp::class)); -assertType('array', $envelope->all(random_bytes(1))); -assertType('array>', $envelope->all()); +assertType('list', $envelope->all(\Symfony\Component\Messenger\Stamp\ReceivedStamp::class)); +assertType('list', $envelope->all(random_bytes(1))); +assertType('array, list>', $envelope->all()); diff --git a/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php new file mode 100644 index 00000000..1e33cb37 --- /dev/null +++ b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php @@ -0,0 +1,17 @@ +getConfiguration($configs, $container) + ); + } +}; diff --git a/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php new file mode 100644 index 00000000..614431f2 --- /dev/null +++ b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php @@ -0,0 +1,23 @@ +getConfiguration($configs, $container) + ); + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return null; + } +} diff --git a/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php new file mode 100644 index 00000000..77c44003 --- /dev/null +++ b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php @@ -0,0 +1,18 @@ +getConfiguration($configs, $container) + ); +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php new file mode 100644 index 00000000..4ff16c39 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php new file mode 100644 index 00000000..b9d5bcc1 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php new file mode 100644 index 00000000..8eea9eb9 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php @@ -0,0 +1,16 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/with-configuration/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php new file mode 100644 index 00000000..4e8c51b5 --- /dev/null +++ b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php @@ -0,0 +1,12 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php new file mode 100644 index 00000000..dccec3e2 --- /dev/null +++ b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php @@ -0,0 +1,17 @@ +getConfiguration($configs, $container) + ); + } +} diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php new file mode 100644 index 00000000..34a673a4 --- /dev/null +++ b/tests/Type/Symfony/data/form_data_type.php @@ -0,0 +1,93 @@ + + */ +class DataClassType extends AbstractType +{ + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + assertType('GenericFormDataType\DataClass|null', $builder->getData()); + assertType('GenericFormDataType\DataClass|null', $builder->getForm()->getData()); + + $builder + ->add('foo', NumberType::class) + ->add('bar', TextType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => DataClass::class, + ]) + ; + } + +} + +class FormFactoryAwareClass +{ + + /** @var FormFactoryInterface */ + private $formFactory; + + public function __construct(FormFactoryInterface $formFactory) + { + $this->formFactory = $formFactory; + } + + public function doSomething(): void + { + $form = $this->formFactory->create(DataClassType::class, new DataClass()); + assertType('GenericFormDataType\DataClass', $form->getData()); + } + + public function doSomethingNullable(): void + { + $form = $this->formFactory->create(DataClassType::class); + assertType('GenericFormDataType\DataClass|null', $form->getData()); + } + +} + +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 4c836c5f..77b58821 100644 --- a/tests/Type/Symfony/data/input_bag.php +++ b/tests/Type/Symfony/data/input_bag.php @@ -5,8 +5,18 @@ $bag = new \Symfony\Component\HttpFoundation\InputBag(['foo' => 'bar', 'bar' => ['x']]); assertType('bool|float|int|string|null', $bag->get('foo')); + +if ($bag->has('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')); + assertType('bool|float|int|string|null', $bag->get('bar')); +} + 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 new file mode 100644 index 00000000..8d5e95f0 --- /dev/null +++ b/tests/Type/Symfony/data/property_accessor.php @@ -0,0 +1,13 @@ + 'ea']; +$propertyAccessor->setValue($array, 'foo', 'bar'); +assertType('array', $array); + +$object = new \stdClass(); +$propertyAccessor->setValue($object, 'foo', 'bar'); +assertType('stdClass', $object); diff --git a/tests/Type/Symfony/data/response_header_bag_get_cookies.php b/tests/Type/Symfony/data/response_header_bag_get_cookies.php new file mode 100644 index 00000000..f07d5e5b --- /dev/null +++ b/tests/Type/Symfony/data/response_header_bag_get_cookies.php @@ -0,0 +1,12 @@ +setCookie(Cookie::create('cookie_name')); + +assertType('array', $headerBag->getCookies()); +assertType('array', $headerBag->getCookies(ResponseHeaderBag::COOKIES_FLAT)); +assertType('array>>', $headerBag->getCookies(ResponseHeaderBag::COOKIES_ARRAY)); 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