diff --git a/.gitattributes b/.gitattributes index 823b5a81..1545ee73 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,12 @@ *.php text eol=lf tests export-ignore +tmp export-ignore .coveralls.yml export-ignore .gitattributes export-ignore .gitignore export-ignore .travis.yml export-ignore -build.xml export-ignore -phpcs.xml export-ignore +Makefile 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 83f79354..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,15 +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" @@ -36,42 +37,48 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" - - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" - + run: "composer install --no-interaction --no-progress" - name: "Lint" - run: "vendor/bin/phing 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: "7.4" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + 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: "vendor/bin/phing lint" + run: "make lint" - name: "Coding Standard" - run: "vendor/bin/phing cs" + run: "make cs" tests: name: "Tests" @@ -81,21 +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" @@ -105,18 +110,14 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + run: "composer update --no-interaction --no-progress" - name: "Tests" - run: "vendor/bin/phing tests" + run: "make tests" static-analysis: name: "PHPStan" @@ -126,18 +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" @@ -149,15 +151,11 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" - - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + run: "composer update --no-interaction --no-progress" - name: "PHPStan" - run: "vendor/bin/phing 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 new file mode 100644 index 00000000..047fe906 --- /dev/null +++ b/.github/workflows/lock-closed-issues.yml @@ -0,0 +1,23 @@ +name: 'Lock Issues' + +on: + schedule: + - cron: '5 0 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + 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. + issue-lock-reason: 'resolved' + process-only: 'issues' 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 ca398d28..7de9f3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +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 new file mode 100644 index 00000000..1ee557df --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: check +check: lint cs tests phpstan + +.PHONY: tests +tests: + php vendor/bin/phpunit + +.PHONY: lint +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: + php build-cs/vendor/bin/phpcs --standard=build-cs/phpcs.xml src tests + +.PHONY: cs-fix +cs-fix: + 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 0081f2ad..25a155c1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ This extension provides following features: * Provides correct return type for `ContainerInterface::get()` and `::has()` methods. * Provides correct return type for `Controller::get()` and `::has()` methods. +* Provides correct return type for `AbstractController::get()` and `::has()` methods. +* Provides correct return type for `ContainerInterface::getParameter()` and `::hasParameter()` methods. +* Provides correct return type for `ParameterBagInterface::get()` and `::has()` methods. +* Provides correct return type for `Controller::getParameter()` method. +* Provides correct return type for `AbstractController::getParameter()` method. * Provides correct return type for `Request::getContent()` method based on the `$asResource` parameter. * Provides correct return type for `HeaderBag::get()` method based on the `$first` parameter. * Provides correct return type for `Envelope::all()` method based on the `$stampFqcn` parameter. @@ -56,11 +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 - # or with Symfony 4.2+ - container_xml_path: var/cache/dev/srcApp_KernelDevDebugContainer.xml + containerXmlPath: var/cache/dev/srcDevDebugProjectContainer.xml + # or with Symfony 4.2+ + 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 @@ -78,35 +90,72 @@ 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. -## Console command analysis +## Analysis of Symfony Console Commands -You can opt in for more advanced analysis by providing the console application from your own application. This will allow the correct argument and option types to be inferred when accessing `$input->getArgument()` or `$input->getOption()`. +You can opt in for more advanced analysis of [Symfony Console Commands](https://symfony.com/doc/current/console.html) +by providing the console application from your own application. This will allow the correct argument and option types to be inferred when accessing `$input->getArgument()` or `$input->getOption()`. -``` +```neon parameters: symfony: - console_application_loader: tests/console-application.php + consoleApplicationLoader: tests/console-application.php ``` -For example, in a Symfony project, `console-application.php` would look something like this: +Symfony 4: ```php -require __DIR__.'/../config/bootstrap.php'; -$kernel = new \App\Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); -return new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel); +// tests/console-application.php + +use App\Kernel; +use Symfony\Bundle\FrameworkBundle\Console\Application; + +require __DIR__ . '/../config/bootstrap.php'; +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +return new Application($kernel); ``` -You may then encounter an error with PhpParser: +Symfony 5: + +```php +// tests/console-application.php + +use App\Kernel; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Dotenv\Dotenv; + +require __DIR__ . '/../vendor/autoload.php'; + +(new Dotenv())->bootEnv(__DIR__ . '/../.env'); + +$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; -```bash -Compile Error: Cannot Declare interface PhpParser\NodeVisitor, because the name is already in use +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 + If this is the case, you should create a new environment for your application that will disable inlining. In `config/packages/phpstan_env/parameters.yaml`: ```yaml 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 9acd0275..00000000 --- a/build-cs/composer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "require-dev": { - "consistence/coding-standard": "^3.10", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.4" - } -} diff --git a/build.xml b/build.xml deleted file mode 100644 index 75b98df1..00000000 --- a/build.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composer.json b/composer.json index 46dbf2d5..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": "^0.12.83" + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" }, "require-dev": { - "phing/phing": "^2.16.3", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^0.12.16", - "phpstan/phpstan-strict-rules": "^0.12.5", - "phpunit/phpunit": "^7.5.20", - "symfony/console": "^4.0", - "symfony/config": "^4.2", - "symfony/framework-bundle": "^4.0", - "symfony/http-foundation": "^4.0", - "symfony/messenger": "^4.2", - "symfony/serializer": "^4.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": "0.12-dev" - }, "phpstan": { "includes": [ "extension.neon", diff --git a/extension.neon b/extension.neon index 51ef297f..0803248f 100644 --- a/extension.neon +++ b/extension.neon @@ -1,18 +1,45 @@ parameters: dynamicConstantNames: - Symfony\Component\HttpKernel\Kernel::VERSION_ID + exceptions: + 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/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub + - 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/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 @@ -20,17 +47,35 @@ parameters: - stubs/Symfony/Component/HttpFoundation/HeaderBag.stub - stubs/Symfony/Component/HttpFoundation/ParameterBag.stub - 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/AclProviderInterface.stub - - stubs/Symfony/Component/Security/Acl/Model/MutableAclInterface.stub - - stubs/Symfony/Component/Security/Acl/Model/MutableAclProviderInterface.stub - - stubs/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.stub - - stubs/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.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 @@ -38,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: @@ -54,27 +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 + 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 @@ -126,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 @@ -142,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 @@ -210,3 +291,76 @@ services: - factory: PHPStan\Type\Symfony\Config\TreeBuilderGetRootNodeDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # KernelInterface::locateResource() return type + - + class: PHPStan\Type\Symfony\KernelInterfaceDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # ParameterBagInterface::get()/has() return type + - + 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.constantHassers%) + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # (Abstract)Controller::getParameter() return type + - + 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.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 feb4d827..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - tests/tmp - tests/Symfony/ExampleContainer.php - 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 bc33def1..f13073e1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,17 +5,11 @@ includes: - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon parameters: - excludes_analyse: + excludePaths: - tests/tmp/* - tests/*/Example*.php - tests/*/console_application_loader.php - - tests/*/envelope_all.php - - tests/*/header_bag_get.php - - tests/*/input_bag.php - - tests/*/kernel_interface.php - - tests/*/request_get_content.php - - tests/*/request_get_session.php - - tests/*/serializer.php - - tests/*/denormalizer.php + - tests/*/data/* diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..2e2f6167 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + ./src + + + + + + + + + + tests + + + + + diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index f97633c0..96f1efea 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -6,9 +6,11 @@ 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; +use PHPStan\Type\Type; use function sprintf; /** @@ -17,8 +19,7 @@ final class ContainerInterfacePrivateServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; public function __construct(ServiceMap $symfonyServiceMap) { @@ -30,22 +31,13 @@ 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 []; } - if ($node->name->name !== 'get' || !isset($node->args[0])) { + if ($node->name->name !== 'get' || !isset($node->getArgs()[0])) { return []; } @@ -53,8 +45,9 @@ public function processNode(Node $node, Scope $scope): array $isTestContainerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Test\TestContainer'))->isSuperTypeOf($argType); $isOldServiceSubscriber = (new ObjectType('Symfony\Component\DependencyInjection\ServiceSubscriberInterface'))->isSuperTypeOf($argType); - $isServiceSubscriber = (new ObjectType('Symfony\Contracts\Service\ServiceSubscriberInterface'))->isSuperTypeOf($argType); - if ($isTestContainerType->yes() || $isOldServiceSubscriber->yes() || $isServiceSubscriber->yes()) { + $isServiceSubscriber = $this->isServiceSubscriber($argType, $scope); + $isServiceLocator = (new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($argType); + if ($isTestContainerType->yes() || $isOldServiceSubscriber->yes() || $isServiceSubscriber->yes() || $isServiceLocator->yes()) { return []; } @@ -71,15 +64,31 @@ public function processNode(Node $node, Scope $scope): array return []; } - $serviceId = $this->serviceMap::getServiceIdFromNode($node->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); 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(), + ]; } } return []; } + private function isServiceSubscriber(Type $containerType, Scope $scope): TrinaryLogic + { + $serviceSubscriberInterfaceType = new ObjectType('Symfony\Contracts\Service\ServiceSubscriberInterface'); + $isContainerServiceSubscriber = $serviceSubscriberInterfaceType->isSuperTypeOf($containerType)->result; + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return $isContainerServiceSubscriber; + } + $containedClassType = new ObjectType($classReflection->getName()); + return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType)->result); + } + } diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php index 2719bb4e..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,26 +34,22 @@ 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 []; } - if ($node->name->name !== 'get' || !isset($node->args[0])) { + if ($node->name->name !== 'get' || !isset($node->getArgs()[0])) { return []; } $argType = $scope->getType($node->var); + $isContainerBagType = (new ObjectType('Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface'))->isSuperTypeOf($argType); + if ($isContainerBagType->yes()) { + return []; + } + $isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType); $isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType); $isContainerType = (new ObjectType('Symfony\Component\DependencyInjection\ContainerInterface'))->isSuperTypeOf($argType); @@ -68,12 +63,16 @@ public function processNode(Node $node, Scope $scope): array return []; } - $serviceId = $this->serviceMap::getServiceIdFromNode($node->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); - $serviceIdType = $scope->getType($node->args[0]->value); + $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 b735dbfe..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,32 +29,23 @@ 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 []; } if (!$node->name instanceof Node\Identifier || $node->name->name !== 'addArgument') { return []; } - if (!isset($node->args[3])) { + if (!isset($node->getArgs()[3])) { return []; } - $modeType = isset($node->args[1]) ? $scope->getType($node->args[1]->value) : new NullType(); - if ($modeType instanceof NullType) { + $modeType = isset($node->getArgs()[1]) ? $scope->getType($node->getArgs()[1]->value) : new NullType(); + if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(2); // InputArgument::OPTIONAL } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -63,16 +54,26 @@ public function processNode(Node $node, Scope $scope): array } $mode = $modeTypes[0]->getValue(); - $defaultType = $scope->getType($node->args[3]->value); + $defaultType = $scope->getType($node->getArgs()[3]->value); // 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 42d40ee0..2e3dc0e9 100644 --- a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php +++ b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php @@ -6,19 +6,18 @@ 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\ConstantBooleanType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; use function sprintf; /** @@ -32,32 +31,23 @@ 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 []; } if (!$node->name instanceof Node\Identifier || $node->name->name !== 'addOption') { return []; } - if (!isset($node->args[4])) { + if (!isset($node->getArgs()[4])) { return []; } - $modeType = isset($node->args[2]) ? $scope->getType($node->args[2]->value) : new NullType(); - if ($modeType instanceof NullType) { + $modeType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new NullType(); + if ($modeType->isNull()->yes()) { $modeType = new ConstantIntegerType(1); // InputOption::VALUE_NONE } - $modeTypes = TypeUtils::getConstantScalars($modeType); + $modeTypes = $modeType->getConstantScalarTypes(); if (count($modeTypes) !== 1) { return []; } @@ -66,22 +56,30 @@ public function processNode(Node $node, Scope $scope): array } $mode = $modeTypes[0]->getValue(); - $defaultType = $scope->getType($node->args[4]->value); + $defaultType = $scope->getType($node->getArgs()[4]->value); // not an array if (($mode & 8) !== 8) { - $checkType = new UnionType([new StringType(), new IntegerType(), new NullType()]); - if (($mode & 4) === 4) { // https://symfony.com/doc/current/console/input.html#options-with-optional-arguments - $checkType = TypeCombinator::union($checkType, new ConstantBooleanType(false)); - } + $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 e6de90b7..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 []; @@ -64,12 +52,12 @@ public function processNode(Node $node, Scope $scope): array if (!$node->name instanceof Node\Identifier || $node->name->name !== 'getArgument') { return []; } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return []; } - $argType = $scope->getType($node->args[0]->value); - $argStrings = TypeUtils::getConstantStrings($argType); + $argType = $scope->getType($node->getArgs()[0]->value); + $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 ee041929..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 []; @@ -64,12 +52,12 @@ public function processNode(Node $node, Scope $scope): array if (!$node->name instanceof Node\Identifier || $node->name->name !== 'getOption') { return []; } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return []; } - $optType = $scope->getType($node->args[0]->value); - $optStrings = TypeUtils::getConstantStrings($optType); + $optType = $scope->getType($node->getArgs()[0]->value); + $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 88ee908f..13b24d26 100644 --- a/src/Symfony/ConsoleApplicationResolver.php +++ b/src/Symfony/ConsoleApplicationResolver.php @@ -5,45 +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 []; } @@ -53,10 +65,22 @@ public function findCommands(ClassReflection $classReflection): array } $commands = []; - foreach ($this->consoleApplication->all() as $name => $command) { - if (!$classType->isSuperTypeOf(new ObjectType(get_class($command)))->yes()) { + foreach ($consoleApplication->all() as $name => $command) { + $commandClass = new ObjectType(get_class($command)); + $isLazyCommand = (new ObjectType('Symfony\Component\Console\Command\LazyCommand'))->isSuperTypeOf($commandClass)->yes(); + + if ($isLazyCommand && method_exists($command, 'getCommand')) { + /** @var Command $wrappedCommand */ + $wrappedCommand = $command->getCommand(); + if (!$classType->isSuperTypeOf(new ObjectType(get_class($wrappedCommand)))->yes()) { + continue; + } + } + + if (!$isLazyCommand && !$classType->isSuperTypeOf($commandClass)->yes()) { continue; } + $commands[$name] = $command; } diff --git a/src/Symfony/DefaultParameterMap.php b/src/Symfony/DefaultParameterMap.php new file mode 100644 index 00000000..3149fd7d --- /dev/null +++ b/src/Symfony/DefaultParameterMap.php @@ -0,0 +1,44 @@ +parameters = $parameters; + } + + /** + * @return ParameterDefinition[] + */ + public function getParameters(): array + { + return $this->parameters; + } + + public function getParameter(string $key): ?ParameterDefinition + { + return $this->parameters[$key] ?? null; + } + + public static function getParameterKeysFromNode(Expr $node, Scope $scope): array + { + $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 new file mode 100644 index 00000000..53acdc3c --- /dev/null +++ b/src/Symfony/FakeParameterMap.php @@ -0,0 +1,29 @@ +reflector = $reflector; + } + + public function getFiles(): array + { + try { + $this->reflector->reflectClass('Symfony\Component\HttpFoundation\InputBag'); + } catch (IdentifierNotFound $e) { + return []; + } + + return [ + __DIR__ . '/../../stubs/Symfony/Component/HttpFoundation/InputBag.stub', + __DIR__ . '/../../stubs/Symfony/Component/HttpFoundation/Request.stub', + ]; + } + +} 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 new file mode 100644 index 00000000..53b53265 --- /dev/null +++ b/src/Symfony/Parameter.php @@ -0,0 +1,38 @@ +|bool|float|int|string */ + private $value; + + /** + * @param array|bool|float|int|string $value + */ + public function __construct( + string $key, + $value + ) + { + $this->key = $key; + $this->value = $value; + } + + public function getKey(): string + { + return $this->key; + } + + /** + * @return array|bool|float|int|string + */ + public function getValue() + { + return $this->value; + } + +} diff --git a/src/Symfony/ParameterDefinition.php b/src/Symfony/ParameterDefinition.php new file mode 100644 index 00000000..1da7723b --- /dev/null +++ b/src/Symfony/ParameterDefinition.php @@ -0,0 +1,18 @@ +|bool|float|int|string + */ + public function getValue(); + +} diff --git a/src/Symfony/ParameterMap.php b/src/Symfony/ParameterMap.php new file mode 100644 index 00000000..0c551635 --- /dev/null +++ b/src/Symfony/ParameterMap.php @@ -0,0 +1,26 @@ + + */ + public static function getParameterKeysFromNode(Expr $node, Scope $scope): array; + +} diff --git a/src/Symfony/ParameterMapFactory.php b/src/Symfony/ParameterMapFactory.php new file mode 100644 index 00000000..4e318540 --- /dev/null +++ b/src/Symfony/ParameterMapFactory.php @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..4d3d3578 --- /dev/null +++ b/src/Symfony/XmlParameterMapFactory.php @@ -0,0 +1,124 @@ +containerXml = $containerXmlPath; + } + + public function create(): ParameterMap + { + if ($this->containerXml === null) { + return new FakeParameterMap(); + } + + $fileContents = file_get_contents($this->containerXml); + if ($fileContents === false) { + throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXml)); + } + + $xml = @simplexml_load_string($fileContents); + if ($xml === false) { + throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml)); + } + + /** @var Parameter[] $parameters */ + $parameters = []; + + if (count($xml->parameters) > 0) { + foreach ($xml->parameters->parameter as $def) { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); + + $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) + { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); + + $value = null; + switch ((string) $attrs->type) { + case 'collection': + $value = []; + $children = $def->children(); + if ($children === null) { + throw new ShouldNotHappenException(); + } + foreach ($children as $child) { + /** @var SimpleXMLElement $childAttrs */ + $childAttrs = $child->attributes(); + + if (isset($childAttrs->key)) { + $value[(string) $childAttrs->key] = $this->getNodeValue($child); + } else { + $value[] = $this->getNodeValue($child); + } + } + break; + + case 'string': + $value = (string) $def; + break; + + 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)); + } + + break; + + default: + $value = (string) $def; + + if (is_numeric($value)) { + if (strpos($value, '.') !== false) { + $value = (float) $value; + } else { + $value = (int) $value; + } + } elseif ($value === 'true') { + $value = true; + } elseif ($value === 'false') { + $value = false; + } + } + + return $value; + } + +} diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 47aa1bc9..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 !== 'false', - 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 edf5574c..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; } @@ -38,14 +36,15 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - $argType = $scope->getType($node->args[0]->value); + $argType = $scope->getType($node->getArgs()[0]->value); 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 71e4fb17..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->args[0])) { + if (!isset($methodCall->getArgs()[0])) { return $defaultType; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[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 25cd12dc..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,15 +38,15 @@ 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); $type = 'array'; - if (isset($methodCall->args[1])) { - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[1]->value)); + if (isset($methodCall->getArgs()[1])) { + $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 bf0712fc..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 { @@ -31,16 +34,24 @@ public function getTypeFromMethodCall( Scope $scope ): Type { - if (count($methodCall->args) === 0) { - return new ArrayType(new MixedType(), new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface'))); + if (count($methodCall->getArgs()) === 0) { + 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->args[0]->value); - if (!$argType instanceof ConstantStringType) { - return new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')); + $argType = $scope->getType($methodCall->getArgs()[0]->value); + 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 42c6878a..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,15 +31,15 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - $firstArgType = isset($methodCall->args[2]) ? $scope->getType($methodCall->args[2]->value) : new ConstantBooleanType(true); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType); + $firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true); + $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result; + $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result; $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { - $defaultArgType = isset($methodCall->args[1]) ? $scope->getType($methodCall->args[1]->value) : new NullType(); + $defaultArgType = isset($methodCall->getArgs()[1]) ? $scope->getType($methodCall->getArgs()[1]->value) : new NullType(); return TypeCombinator::union($defaultArgType, new StringType()); } @@ -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 a39af310..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::value()) + $type->describe(VerbosityLevel::precise()), ))); } diff --git a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php index 51e5ff19..75e6d0bc 100644 --- a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php @@ -8,12 +8,17 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function in_array; final class InputBagDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -32,7 +37,7 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { if ($methodReflection->getName() === 'get') { return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope); @@ -49,30 +54,32 @@ private function getGetTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - if (isset($methodCall->args[1])) { - $argType = $scope->getType($methodCall->args[1]->value); + if (isset($methodCall->getArgs()[1])) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); $isNull = (new NullType())->isSuperTypeOf($argType); - $isString = (new StringType())->isSuperTypeOf($argType); - $compare = $isNull->compareTo($isString); - if ($compare === $isString) { - return new StringType(); + if ($isNull->no()) { + return TypeCombinator::removeNull(ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType()); } } - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } private function getAllTypeFromMethodCall( MethodCall $methodCall ): Type { - if (isset($methodCall->args[0])) { - return new ArrayType(new MixedType(), new StringType()); + if (isset($methodCall->getArgs()[0])) { + return new ArrayType(new MixedType(), new MixedType(true)); } - return new ArrayType(new StringType(), new UnionType([new StringType(), new ArrayType(new MixedType(), new StringType())])); + return new ArrayType(new StringType(), new UnionType([new ArrayType(new MixedType(), new MixedType(true)), new BooleanType(), new FloatType(), new IntegerType(), new StringType()])); } } 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 24cccc40..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->args, $methodReflection->getVariants())->getReturnType(); - - if (!isset($methodCall->args[0])) { - return $defaultReturnType; + if (!isset($methodCall->getArgs()[0])) { + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[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 40a3993f..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->args, $methodReflection->getVariants())->getReturnType(); - - if (!isset($methodCall->args[0])) { - return $defaultReturnType; + if (!isset($methodCall->getArgs()[0])) { + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[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 f69908fe..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->args[0])) { - return $defaultReturnType; + if (!isset($methodCall->getArgs()[0])) { + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[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 c60e2644..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->args[0])) { - return $defaultReturnType; + if (!isset($methodCall->getArgs()[0])) { + return null; } $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - return $defaultReturnType; + return null; } - $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[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 f74f114e..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->args[2]) ? $scope->getType($methodCall->args[2]->value) : new ConstantBooleanType(true); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType); + $firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true); + $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 aaff84c0..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; } @@ -38,14 +36,15 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - $argType = $scope->getType($node->args[0]->value); + $argType = $scope->getType($node->getArgs()[0]->value); 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 new file mode 100644 index 00000000..687b0c33 --- /dev/null +++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php @@ -0,0 +1,238 @@ +className = $className; + $this->methodGet = $methodGet; + $this->methodHas = $methodHas; + $this->constantHassers = $constantHassers; + $this->parameterMap = $symfonyParameterMap; + $this->typeStringResolver = $typeStringResolver; + } + + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + $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 + { + switch ($methodReflection->getName()) { + case $this->methodGet: + return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope); + case $this->methodHas: + return $this->getHasTypeFromMethodCall($methodReflection, $methodCall, $scope); + } + throw new ShouldNotHappenException(); + } + + private function getGetTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + // 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). + $defaultReturnType = new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new BooleanType(), + new FloatType(), + new IntegerType(), + new StringType(), + new NullType(), + ]); + if (!isset($methodCall->getArgs()[0])) { + return $defaultReturnType; + } + + $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope); + if ($parameterKeys === []) { + return $defaultReturnType; + } + + $returnTypes = []; + foreach ($parameterKeys as $parameterKey) { + $parameter = $this->parameterMap->getParameter($parameterKey); + if ($parameter === null) { + return $defaultReturnType; + } + + $returnTypes[] = $this->generalizeTypeFromValue($scope, $parameter->getValue()); + } + + 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 + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof ConstantArrayType) { + if (count($type->getValueTypes()) === 0) { + return new ArrayType(new MixedType(), new MixedType()); + } + return new ArrayType($this->generalizeType($type->getKeyType()), $this->generalizeType($type->getItemType())); + } + if ($type->isConstantValue()->yes()) { + return $type->generalize(GeneralizePrecision::lessSpecific()); + } + return $traverse($type); + }); + } + + private function getHasTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) { + return null; + } + + $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope); + if ($parameterKeys === []) { + return null; + } + + $has = null; + foreach ($parameterKeys as $parameterKey) { + $parameter = $this->parameterMap->getParameter($parameterKey); + + if ($has === null) { + $has = $parameter !== null; + } elseif ( + ($has === true && $parameter === null) + || ($has === false && $parameter !== null) + ) { + return null; + } + } + + return new ConstantBooleanType($has); + } + +} diff --git a/src/Type/Symfony/RequestDynamicReturnTypeExtension.php b/src/Type/Symfony/RequestDynamicReturnTypeExtension.php index 72991824..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->args[0])) { + if (!isset($methodCall->getArgs()[0])) { return new StringType(); } - $argType = $scope->getType($methodCall->args[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $argType = $scope->getType($methodCall->getArgs()[0]->value); + $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 295d37e4..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,13 +34,18 @@ 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(); + $methodVariants = $methodReflection->getDeclaringClass()->getNativeMethod(self::GET_METHOD_NAME)->getVariants(); + $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodVariants)->getReturnType(); + + if (!TypeCombinator::containsNull($returnType)) { + return new SpecifiedTypes(); + } return $this->typeSpecifier->create( new MethodCall($node->var, self::GET_METHOD_NAME), - TypeCombinator::removeNull(ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType()), - $context + TypeCombinator::removeNull($returnType), + $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 c6c237ee..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; @@ -39,18 +43,21 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (!isset($methodCall->args[1])) { + if (!isset($methodCall->getArgs()[1])) { return new MixedType(); } - $argType = $scope->getType($methodCall->args[1]->value); - if (!$argType instanceof ConstantStringType) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); + 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 6b2f6f84..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,42 +73,84 @@ private function getGetTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - if (!isset($methodCall->args[0])) { - return $returnType; + if (!isset($methodCall->getArgs()[0])) { + return null; + } + + $parameterBag = $this->tryGetParameterBag(); + if ($parameterBag === null) { + return null; } - $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); - if ($service !== null && !$service->isSynthetic()) { - return new ObjectType($service->getClass() ?? $serviceId); + if ($service !== null && (!$service->isSynthetic() || $service->getClass() !== null)) { + 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->args[0]) || !$this->constantHassers) { - return $returnType; + if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) { + return null; } - $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); 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 e9946387..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; @@ -42,14 +43,15 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - $argType = $scope->getType($node->args[0]->value); + $argType = $scope->getType($node->getArgs()[0]->value); 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 new file mode 100644 index 00000000..a58e43ca --- /dev/null +++ b/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub @@ -0,0 +1,14 @@ +|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 new file mode 100644 index 00000000..393fa803 --- /dev/null +++ b/stubs/Symfony/Component/Form/DataTransformerInterface.stub @@ -0,0 +1,30 @@ + + * @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 e7646acf..08c64752 100644 --- a/stubs/Symfony/Component/Form/FormView.stub +++ b/stubs/Symfony/Component/Form/FormView.stub @@ -6,9 +6,17 @@ use ArrayAccess; use IteratorAggregate; /** - * @implements IteratorAggregate - * @implements ArrayAccess + * @implements IteratorAggregate + * @implements ArrayAccess */ class FormView implements ArrayAccess, IteratorAggregate { + + /** + * Returns an iterator to iterate over children (implements \IteratorAggregate). + * + * @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/InputBag.stub b/stubs/Symfony/Component/HttpFoundation/InputBag.stub new file mode 100644 index 00000000..98223ce6 --- /dev/null +++ b/stubs/Symfony/Component/HttpFoundation/InputBag.stub @@ -0,0 +1,20 @@ + + */ + public function keys(): array + { + } } diff --git a/stubs/Symfony/Component/HttpFoundation/Request.stub b/stubs/Symfony/Component/HttpFoundation/Request.stub new file mode 100644 index 00000000..0c2140cc --- /dev/null +++ b/stubs/Symfony/Component/HttpFoundation/Request.stub @@ -0,0 +1,72 @@ + + */ + public $request; + + /** + * Query string parameters ($_GET). + * + * @var InputBag + */ + public $query; + + /** + * Cookies ($_COOKIE). + * + * @var InputBag + */ + 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/Messenger/Envelope.stub b/stubs/Symfony/Component/Messenger/Envelope.stub new file mode 100644 index 00000000..c40a5ee6 --- /dev/null +++ b/stubs/Symfony/Component/Messenger/Envelope.stub @@ -0,0 +1,17 @@ + $stampFqcn + * @phpstan-return T|null + */ + public function last(string $stampFqcn): ?StampInterface + { + } +} diff --git a/stubs/Symfony/Component/Messenger/StampInterface.stub b/stubs/Symfony/Component/Messenger/StampInterface.stub new file mode 100644 index 00000000..2951ab45 --- /dev/null +++ b/stubs/Symfony/Component/Messenger/StampInterface.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/Acl/Model/AclInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub index b2ce3e58..2f509501 100644 --- a/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub +++ b/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub @@ -5,4 +5,36 @@ namespace Symfony\Component\Security\Acl\Model; interface AclInterface { + /** + * Returns all class-based ACEs associated with this ACL. + * + * @return array + */ + public function getClassAces(); + + /** + * Returns all class-field-based ACEs associated with this ACL. + * + * @param string $field + * + * @return array + */ + public function getClassFieldAces($field); + + /** + * Returns all object-based ACEs associated with this ACL. + * + * @return array + */ + public function getObjectAces(); + + /** + * Returns all object-field-based ACEs associated with this ACL. + * + * @param string $field + * + * @return array + */ + public function getObjectFieldAces($field); + } diff --git a/stubs/Symfony/Component/Security/Acl/Model/AclProviderInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/AclProviderInterface.stub deleted file mode 100644 index 97cb6880..00000000 --- a/stubs/Symfony/Component/Security/Acl/Model/AclProviderInterface.stub +++ /dev/null @@ -1,19 +0,0 @@ - $sids - * @phpstan-return AclInterface - */ - public function findAcl(ObjectIdentityInterface $oid, array $sids = []); - - /** - * @phpstan-param array $oids - * @phpstan-param array $sids - * @phpstan-return \SplObjectStorage - */ - public function findAcls(array $oids, array $sids = []); -} diff --git a/stubs/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub similarity index 63% rename from stubs/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.stub rename to stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub index 8c03dabc..335e581d 100644 --- a/stubs/Symfony/Component/Security/Acl/Model/ObjectIdentityInterface.stub +++ b/stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub @@ -2,7 +2,6 @@ namespace Symfony\Component\Security\Acl\Model; -interface ObjectIdentityInterface +interface EntryInterface { - } diff --git a/stubs/Symfony/Component/Security/Acl/Model/MutableAclInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/MutableAclInterface.stub deleted file mode 100644 index 75df80b3..00000000 --- a/stubs/Symfony/Component/Security/Acl/Model/MutableAclInterface.stub +++ /dev/null @@ -1,8 +0,0 @@ - $sids - * @phpstan-return MutableAclInterface - */ - public function findAcl(ObjectIdentityInterface $oid, array $sids = []); - - /** - * @phpstan-param array $oids - * @phpstan-param array $sids - * @phpstan-return \SplObjectStorage - */ - public function findAcls(array $oids, array $sids = []); -} diff --git a/stubs/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.stub deleted file mode 100644 index d60731dc..00000000 --- a/stubs/Symfony/Component/Security/Acl/Model/SecurityIdentityInterface.stub +++ /dev/null @@ -1,8 +0,0 @@ - $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|object[] + * @return void */ public function denormalize($denormalizer, $data, $format = null, array $context = []); } diff --git a/stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub index 83c6ddff..b7e9968b 100644 --- a/stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub +++ b/stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub @@ -2,6 +2,14 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Exception\BadMethodCallException; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Exception\ExtraAttributesException; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\RuntimeException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + interface DenormalizerInterface { /** @@ -9,7 +17,15 @@ interface DenormalizerInterface * @param string $type * @param string|null $format * @param array $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/Constraint.stub b/stubs/Symfony/Component/Validator/Constraint.stub index 7d875f2a..e7a4b501 100644 --- a/stubs/Symfony/Component/Validator/Constraint.stub +++ b/stubs/Symfony/Component/Validator/Constraint.stub @@ -4,6 +4,11 @@ namespace Symfony\Component\Validator; class Constraint { + /** + * @var array + */ + protected static $errorNames = []; + /** * @return array */ 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 @@ + + */ +class NonexistentInputBagClassTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testInputBag(): void + { + $this->analyse([__DIR__ . '/data/input_bag.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + __DIR__ . '/../../rules.neon', + ]; + } + +} diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php index 6aa4ae47..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 @@ -19,21 +21,27 @@ protected function getRule(): Rule public function testGetPrivateService(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } $this->analyse( [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } public function testGetPrivateServiceInAbstractController(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } $this->analyse( [ __DIR__ . '/ExampleAbstractController.php', ], - [] + [], ); } @@ -43,13 +51,17 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceSubscriberInterface class.'); } + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleLegacyServiceSubscriber.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -59,13 +71,17 @@ public function testGetPrivateServiceInServiceSubscriber(): void self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.'); } + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleServiceSubscriber.php', __DIR__ . '/ExampleServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleServiceSubscriberFromLegacyController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php index 3704818e..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 @@ -19,6 +21,9 @@ protected function getRule(): Rule public function testGetPrivateService(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } $this->analyse( [ __DIR__ . '/ExampleController.php', @@ -26,9 +31,9 @@ public function testGetPrivateService(): void [ [ 'Service "private" is private.', - 12, + 13, ], - ] + ], ); } @@ -38,13 +43,17 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceSubscriberInterface class.'); } + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleLegacyServiceSubscriber.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', ], - [] + [], ); } @@ -54,13 +63,17 @@ public function testGetPrivateServiceInServiceSubscriber(): void self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.'); } + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleServiceSubscriber.php', __DIR__ . '/ExampleServiceSubscriberFromAbstractController.php', __DIR__ . '/ExampleServiceSubscriberFromLegacyController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php index 87404430..8d70f1c3 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php @@ -2,12 +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\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use function class_exists; /** * @extends RuleTestCase @@ -17,36 +19,43 @@ 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(Controller::class, new Standard()), + new ServiceTypeSpecifyingExtension(AbstractController::class, self::getContainer()->getByType(Printer::class)), ]; } public function testGetPrivateService(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } $this->analyse( [ __DIR__ . '/ExampleController.php', ], - [] + [], ); } public function testGetPrivateServiceInAbstractController(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleAbstractController.php', ], - [] + [], ); } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index 776812af..c975750f 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -2,13 +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 PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use function class_exists; +use function interface_exists; /** * @extends RuleTestCase @@ -18,22 +17,14 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard()); - } - - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new ServiceTypeSpecifyingExtension(Controller::class, new Standard()), - new ServiceTypeSpecifyingExtension(AbstractController::class, new Standard()), - ]; + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), self::getContainer()->getByType(Printer::class)); } public function testGetPrivateService(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } $this->analyse( [ __DIR__ . '/ExampleController.php', @@ -41,14 +32,18 @@ public function testGetPrivateService(): void [ [ 'Service "unknown" is not registered in the container.', - 24, + 25, ], - ] + ], ); } public function testGetPrivateServiceInAbstractController(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ __DIR__ . '/ExampleAbstractController.php', @@ -56,10 +51,31 @@ public function testGetPrivateServiceInAbstractController(): void [ [ 'Service "unknown" is not registered in the container.', - 24, + 25, ], - ] + ], ); } + public function testGetPrivateServiceInLegacyServiceSubscriber(): void + { + if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) { + self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleServiceSubscriber.php', + ], + [], + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } 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/ExampleCommand.php b/tests/Rules/Symfony/ExampleCommand.php index c376d875..6dec4cbd 100644 --- a/tests/Rules/Symfony/ExampleCommand.php +++ b/tests/Rules/Symfony/ExampleCommand.php @@ -29,6 +29,7 @@ protected function configure(): void $this->addOption('b', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', [1]); $this->addOption('c', null, InputOption::VALUE_OPTIONAL, '', 1); $this->addOption('d', null, InputOption::VALUE_OPTIONAL, '', false); + $this->addOption('f', null, InputOption::VALUE_REQUIRED, '', true); /** @var string[] $defaults */ $defaults = []; diff --git a/tests/Rules/Symfony/ExampleController.php b/tests/Rules/Symfony/ExampleController.php index 65d1359b..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'); } @@ -39,4 +40,9 @@ public function unknownGuardedServiceOutsideOfContext(): void $this->get('unknown'); } + public function privateServiceFromServiceLocator(): void + { + $this->get('service_locator')->get('private'); + } + } diff --git a/tests/Rules/Symfony/ExampleServiceSubscriber.php b/tests/Rules/Symfony/ExampleServiceSubscriber.php index c9a009d2..ec9c966d 100644 --- a/tests/Rules/Symfony/ExampleServiceSubscriber.php +++ b/tests/Rules/Symfony/ExampleServiceSubscriber.php @@ -2,14 +2,31 @@ namespace PHPStan\Rules\Symfony; +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; use Symfony\Contracts\Service\ServiceSubscriberInterface; final class ExampleServiceSubscriber implements ServiceSubscriberInterface { + private ContainerInterface $locator; + + public function __construct(ContainerInterface $locator) + { + $this->locator = $locator; + } + public function privateService(): void { $this->get('private'); + $this->locator->get('private'); + } + + public function containerParameter(): void + { + /** @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 22fee153..d9970ef6 100644 --- a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php +++ b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php @@ -2,11 +2,10 @@ 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; -use PHPStan\Type\Symfony\ArgumentTypeSpecifyingExtension; /** * @extends RuleTestCase @@ -16,17 +15,7 @@ final class UndefinedArgumentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), new Standard()); - } - - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new ArgumentTypeSpecifyingExtension(new Standard()), - ]; + return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); } public function testGetArgument(): void @@ -38,10 +27,17 @@ public function testGetArgument(): void [ [ 'Command "example-rule" does not define argument "undefined".', - 41, + 42, ], - ] + ], ); } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/argument.neon', + ]; + } + } diff --git a/tests/Rules/Symfony/UndefinedOptionRuleTest.php b/tests/Rules/Symfony/UndefinedOptionRuleTest.php index 1ca949de..7f759213 100644 --- a/tests/Rules/Symfony/UndefinedOptionRuleTest.php +++ b/tests/Rules/Symfony/UndefinedOptionRuleTest.php @@ -2,11 +2,10 @@ 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; -use PHPStan\Type\Symfony\OptionTypeSpecifyingExtension; /** * @extends RuleTestCase @@ -16,17 +15,7 @@ final class UndefinedOptionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), new Standard()); - } - - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new OptionTypeSpecifyingExtension(new Standard()), - ]; + return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); } public function testGetArgument(): void @@ -38,10 +27,17 @@ public function testGetArgument(): void [ [ 'Command "example-rule" does not define option "bbb".', - 48, + 49, ], - ] + ], ); } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/option.neon', + ]; + } + } diff --git a/tests/Rules/Symfony/argument.neon b/tests/Rules/Symfony/argument.neon new file mode 100644 index 00000000..86fa3f16 --- /dev/null +++ b/tests/Rules/Symfony/argument.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Type\Symfony\ArgumentTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/tests/Rules/Symfony/container.xml b/tests/Rules/Symfony/container.xml index f21aeae3..f3261e0a 100644 --- a/tests/Rules/Symfony/container.xml +++ b/tests/Rules/Symfony/container.xml @@ -3,5 +3,10 @@ + + + + + diff --git a/tests/Rules/Symfony/option.neon b/tests/Rules/Symfony/option.neon new file mode 100644 index 00000000..30984a7a --- /dev/null +++ b/tests/Rules/Symfony/option.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Type\Symfony\OptionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/tests/Rules/data/input_bag.php b/tests/Rules/data/input_bag.php new file mode 100644 index 00000000..03699f6b --- /dev/null +++ b/tests/Rules/data/input_bag.php @@ -0,0 +1,23 @@ +query->get('foo'); + + return $this->render('test/index.html.twig', [ + 'controller_name' => 'TestController', + ]); + } +} diff --git a/tests/Symfony/DefaultParameterMapTest.php b/tests/Symfony/DefaultParameterMapTest.php new file mode 100644 index 00000000..018a68a9 --- /dev/null +++ b/tests/Symfony/DefaultParameterMapTest.php @@ -0,0 +1,144 @@ +create()->getParameter($key)); + } + + public function testGetParameterEscapedPath(): void + { + $factory = new XmlParameterMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); + $serviceMap = $factory->create(); + + self::assertNotNull($serviceMap->getParameter('app.string')); + } + + /** + * @return Iterator + */ + public function getParameterProvider(): Iterator + { + yield [ + 'unknown', + static function (?Parameter $parameter): void { + self::assertNull($parameter); + }, + ]; + yield [ + 'app.string', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.string', $parameter->getKey()); + self::assertSame('abcdef', $parameter->getValue()); + }, + ]; + yield [ + 'app.int', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.int', $parameter->getKey()); + self::assertSame(123, $parameter->getValue()); + }, + ]; + yield [ + 'app.int_as_string', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.int_as_string', $parameter->getKey()); + self::assertSame('123', $parameter->getValue()); + }, + ]; + yield [ + 'app.float', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.float', $parameter->getKey()); + self::assertSame(123.45, $parameter->getValue()); + }, + ]; + yield [ + 'app.float_as_string', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.float_as_string', $parameter->getKey()); + self::assertSame('123.45', $parameter->getValue()); + }, + ]; + yield [ + 'app.boolean', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.boolean', $parameter->getKey()); + self::assertTrue($parameter->getValue()); + }, + ]; + yield [ + 'app.boolean_as_string', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.boolean_as_string', $parameter->getKey()); + self::assertSame('true', $parameter->getValue()); + }, + ]; + yield [ + 'app.list', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.list', $parameter->getKey()); + self::assertEquals(['en', 'es', 'fr'], $parameter->getValue()); + }, + ]; + yield [ + 'app.list_of_list', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.list_of_list', $parameter->getKey()); + self::assertEquals([ + ['name' => 'the name', 'value' => 'the value'], + ['name' => 'another name', 'value' => 'another value'], + ], $parameter->getValue()); + }, + ]; + yield [ + 'app.map', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.map', $parameter->getKey()); + self::assertEquals([ + 'a' => 'value of a', + 'b' => 'value of b', + 'c' => 'value of c', + ], $parameter->getValue()); + }, + ]; + yield [ + 'app.binary', + static function (?Parameter $parameter): void { + self::assertNotNull($parameter); + self::assertSame('app.binary', $parameter->getKey()); + self::assertSame('This is a Bell char ', $parameter->getValue()); + }, + ]; + yield [ + 'app.constant', + 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 35d6b35d..b43bee49 100644 --- a/tests/Symfony/DefaultServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -26,93 +26,104 @@ 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()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertFalse($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ 'withClass', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('withClass', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertFalse($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ 'withoutPublic', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('withoutPublic', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertFalse($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ - 'publicNotFalse', - function (?Service $service): void { + 'publicNotTrue', + static function (?Service $service): void { self::assertNotNull($service); - self::assertSame('publicNotFalse', $service->getId()); + self::assertSame('publicNotTrue', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertFalse($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ - 'private', - function (?Service $service): void { + 'public', + static function (?Service $service): void { self::assertNotNull($service); - self::assertSame('private', $service->getId()); + self::assertSame('public', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertFalse($service->isPublic()); + self::assertTrue($service->isPublic()); self::assertFalse($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ 'synthetic', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('synthetic', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertTrue($service->isSynthetic()); self::assertNull($service->getAlias()); }, ]; yield [ 'alias', - function (?Service $service): void { + static function (?Service $service): void { self::assertNotNull($service); self::assertSame('alias', $service->getId()); self::assertSame('Foo', $service->getClass()); - self::assertTrue($service->isPublic()); + self::assertFalse($service->isPublic()); self::assertFalse($service->isSynthetic()); 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 8c8fbd55..f456ab51 100644 --- a/tests/Symfony/container.xml +++ b/tests/Symfony/container.xml @@ -1,13 +1,47 @@ + + abcdef + 123 + 123 + 123.45 + 123.45 + true + true + + en + es + fr + + + + the name + the value + + + another name + another value + + + + value of a + value of b + value of c + + VGhpcyBpcyBhIEJlbGwgY2hhciAH + Y-m-d\TH:i:sP + + - - + + + + diff --git a/tests/Symfony/containers/bugfix%2Fcontainer.xml b/tests/Symfony/containers/bugfix%2Fcontainer.xml index 8c8fbd55..5bed7715 100644 --- a/tests/Symfony/containers/bugfix%2Fcontainer.xml +++ b/tests/Symfony/containers/bugfix%2Fcontainer.xml @@ -1,5 +1,37 @@ + + abcdef + 123 + 123 + 123.45 + 123.45 + true + true + + en + es + fr + + + + the name + the value + + + another name + another value + + + + value of a + value of b + value of c + + VGhpcyBpcyBhIEJlbGwgY2hhciAH + Y-m-d\TH:i:sP + + 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/Config/TreeBuilderTest.php b/tests/Type/Symfony/Config/TreeBuilderTest.php deleted file mode 100644 index 67f77917..00000000 --- a/tests/Type/Symfony/Config/TreeBuilderTest.php +++ /dev/null @@ -1,211 +0,0 @@ -processFile( - __DIR__ . '/tree_builder.php', - $expression, - $type, - [ - new ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension(), - new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\ExprBuilder', ['end']), - new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeBuilder', ['end']), - new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeDefinition', ['end']), - new PassParentObjectDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeBuilder', ['arrayNode', 'scalarNode', 'booleanNode', 'integerNode', 'floatNode', 'enumNode', 'variableNode']), - new PassParentObjectDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeDefinition', ['children', 'validate', 'beforeNormalization']), - new TreeBuilderGetRootNodeDynamicReturnTypeExtension(), - ], - [new TreeBuilderDynamicReturnTypeExtension()] - ); - } - - /** - * @return \Iterator - */ - public function getProvider(): Iterator - { - yield ['$treeRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->scalarNode("protocol") - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->scalarNode("protocol") - ', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->scalarNode("protocol") - ->end() - ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->scalarNode("protocol") - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->scalarNode("protocol") - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->scalarNode("protocol") - ->end() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $treeRootNode - ->children() - ->arrayNode("protocols") - ->children() - ->booleanNode("auto_connect") - ->defaultTrue() - ->end() - ->scalarNode("default_connection") - ->defaultValue("default") - ->end() - ->integerNode("positive_value") - ->min(0) - ->end() - ->floatNode("big_value") - ->max(5E45) - ->end() - ->enumNode("delivery") - ->values(["standard", "expedited", "priority"]) - ->end() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - - yield ['$arrayRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield ['$arrayRootNode->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - yield [' - $arrayRootNode - ->children() - ->arrayNode("methods") - ->prototype("scalar") - ->defaultNull() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $arrayRootNode - ->children() - ->arrayNode("methods") - ->scalarPrototype() - ->defaultNull() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $arrayRootNode - ->children() - ->arrayNode("methods") - ->prototype("scalar") - ->validate() - ->ifNotInArray(["one", "two"]) - ->thenInvalid("%s is not a valid method.") - ->end() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield [' - $arrayRootNode - ->children() - ->arrayNode("methods") - ->prototype("array") - ->beforeNormalization() - ->ifString() - ->then(static function ($v) { - return [$v]; - }) - ->end() - ->end() - ->end() - ->end() - ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - - yield ['$variableRootNode', 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition']; - yield ['$variableRootNode->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - - yield ['$scalarRootNode', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; - yield ['$scalarRootNode->defaultValue("default")', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; - yield ['$scalarRootNode->defaultValue("default")->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - - yield ['$booleanRootNode', 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition']; - yield ['$booleanRootNode->defaultTrue()', 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition']; - yield ['$booleanRootNode->defaultTrue()->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - - yield ['$integerRootNode', 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition']; - yield ['$integerRootNode->min(0)', 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition']; - yield ['$integerRootNode->min(0)->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - - yield ['$floatRootNode', 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition']; - yield ['$floatRootNode->max(5E45)', 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition']; - yield ['$floatRootNode->max(5E45)->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - - yield ['$enumRootNode', 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition']; - yield ['$enumRootNode->values(["standard", "expedited", "priority"])', 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition']; - yield ['$enumRootNode->values(["standard", "expedited", "priority"])->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; - } - -} diff --git a/tests/Type/Symfony/Config/tree_builder.php b/tests/Type/Symfony/Config/tree_builder.php deleted file mode 100644 index f1126608..00000000 --- a/tests/Type/Symfony/Config/tree_builder.php +++ /dev/null @@ -1,29 +0,0 @@ -getRootNode(); - -$arrayTreeBuilder = new TreeBuilder('my_tree', 'array'); -$arrayRootNode = $arrayTreeBuilder->getRootNode(); - -$variableTreeBuilder = new TreeBuilder('my_tree', 'variable'); -$variableRootNode = $variableTreeBuilder->getRootNode(); - -$scalarTreeBuilder = new TreeBuilder('my_tree', 'scalar'); -$scalarRootNode = $scalarTreeBuilder->getRootNode(); - -$booleanTreeBuilder = new TreeBuilder('my_tree', 'boolean'); -$booleanRootNode = $booleanTreeBuilder->getRootNode(); - -$integerTreeBuilder = new TreeBuilder('my_tree', 'integer'); -$integerRootNode = $integerTreeBuilder->getRootNode(); - -$floatTreeBuilder = new TreeBuilder('my_tree', 'float'); -$floatRootNode = $floatTreeBuilder->getRootNode(); - -$enumTreeBuilder = new TreeBuilder('my_tree', 'enum'); -$enumRootNode = $enumTreeBuilder->getRootNode(); - -die; diff --git a/tests/Type/Symfony/EnvelopeReturnTypeExtensionTest.php b/tests/Type/Symfony/EnvelopeReturnTypeExtensionTest.php deleted file mode 100644 index ef59ccc4..00000000 --- a/tests/Type/Symfony/EnvelopeReturnTypeExtensionTest.php +++ /dev/null @@ -1,35 +0,0 @@ -processFile( - __DIR__ . '/envelope_all.php', - $expression, - $type, - [new EnvelopeReturnTypeExtension()] - ); - } - - /** - * @return \Iterator - */ - public function getProvider(): Iterator - { - yield ['$test1', 'array<' . ReceivedStamp::class . '>']; - yield ['$test2', 'array<' . StampInterface::class . '>']; - yield ['$test3', 'array>']; - } - -} diff --git a/tests/Type/Symfony/ExampleBaseCommand.php b/tests/Type/Symfony/ExampleBaseCommand.php deleted file mode 100644 index b058ee2c..00000000 --- a/tests/Type/Symfony/ExampleBaseCommand.php +++ /dev/null @@ -1,31 +0,0 @@ -addArgument('base'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $base = $input->getArgument('base'); - $aaa = $input->getArgument('aaa'); - $bbb = $input->getArgument('bbb'); - $diff = $input->getArgument('diff'); - $arr = $input->getArgument('arr'); - $both = $input->getArgument('both'); - - die; - } - -} diff --git a/tests/Type/Symfony/ExampleController.php b/tests/Type/Symfony/ExampleController.php deleted file mode 100644 index 2a75eefb..00000000 --- a/tests/Type/Symfony/ExampleController.php +++ /dev/null @@ -1,25 +0,0 @@ -get('foo'); - $service2 = $this->get('bar'); - $service3 = $this->get(doFoo()); - $service4 = $this->get(); - - $has1 = $this->has('foo'); - $has2 = $this->has('bar'); - $has3 = $this->has(doFoo()); - $has4 = $this->has(); - - die; - } - -} diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php new file mode 100644 index 00000000..40420be0 --- /dev/null +++ b/tests/Type/Symfony/ExtensionTest.php @@ -0,0 +1,95 @@ +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'); + $doc = (string) $ref->getDocComment(); + if (strpos($doc, '@return SessionInterface|null') !== false) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/request_get_session_null.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/request_get_session.php'); + } + + if (class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleController.php'); + } + + if (class_exists('Symfony\Bundle\FrameworkBundle\Controller\AbstractController')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleAbstractController.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/serializer.php'); + + if (class_exists('Symfony\Component\HttpFoundation\InputBag')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/input_bag_from_request.php'); + } + + 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 mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + 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/ExtensionTestCase.php b/tests/Type/Symfony/ExtensionTestCase.php deleted file mode 100644 index a69c6e59..00000000 --- a/tests/Type/Symfony/ExtensionTestCase.php +++ /dev/null @@ -1,96 +0,0 @@ -createBroker($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions); - $parser = $this->getParser(); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $typeSpecifier = $this->createTypeSpecifier(new Standard(), $broker); - /** @var \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver */ - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $fileTypeMapper = new FileTypeMapper( - new DirectReflectionProviderProvider($broker), - $parser, - $phpDocStringResolver, - self::getContainer()->getByType(PhpDocNodeResolver::class), - $this->createMock(Cache::class), - $this->createMock(AnonymousClassNameHelper::class) - ); - $resolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], - $this->getClassReflectionExtensionRegistryProvider(), - $parser, - $fileTypeMapper, - new PhpVersion(PHP_VERSION_ID), - new PhpDocInheritanceResolver($fileTypeMapper), - $fileHelper, - $typeSpecifier, - true, - true, - true, - [], - [], - true, - true - ); - $resolver->setAnalysedFiles([$fileHelper->normalizePath($file)]); - - $run = false; - $resolver->processNodes( - $parser->parseFile($file), - $this->createScopeFactory($broker, $typeSpecifier)->create(ScopeContext::create($file)), - function (Node $node, Scope $scope) use ($expression, $type, &$run): void { - if ($node instanceof VirtualNode) { - return; - } - if ((new Standard())->prettyPrint([$node]) !== 'die') { - return; - } - /** @var \PhpParser\Node\Stmt\Expression $expNode */ - $expNode = $this->getParser()->parseString(sprintf('getType($expNode->expr)->describe(VerbosityLevel::typeOnly()), sprintf('Expression "%s"', $expression)); - $run = true; - } - ); - self::assertTrue($run); - } - -} diff --git a/tests/Type/Symfony/ExtensionTestWithoutContainer.php b/tests/Type/Symfony/ExtensionTestWithoutContainer.php new file mode 100644 index 00000000..fd1785c7 --- /dev/null +++ b/tests/Type/Symfony/ExtensionTestWithoutContainer.php @@ -0,0 +1,52 @@ +gatherAssertTypes(__DIR__ . '/data/ExampleController.php'); + } + + /** @return mixed[] */ + public function dataAbstractController(): iterable + { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\AbstractController')) { + return; + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleAbstractController.php'); + } + + /** + * @dataProvider dataExampleController + * @dataProvider dataAbstractController + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Type/Symfony/HeaderBagDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/HeaderBagDynamicReturnTypeExtensionTest.php deleted file mode 100644 index f84f3032..00000000 --- a/tests/Type/Symfony/HeaderBagDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,37 +0,0 @@ -processFile( - __DIR__ . '/header_bag_get.php', - $expression, - $type, - [new HeaderBagDynamicReturnTypeExtension()] - ); - } - - /** - * @return \Iterator - */ - public function getProvider(): Iterator - { - yield ['$test1', 'string|null']; - yield ['$test2', 'string|null']; - yield ['$test3', 'string']; - yield ['$test5', 'string|null']; - yield ['$test6', 'string']; - yield ['$test8', 'array']; - yield ['$test9', 'array']; - } - -} diff --git a/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php new file mode 100644 index 00000000..bde62b57 --- /dev/null +++ b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php @@ -0,0 +1,38 @@ + + */ +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(ImpossibleCheckTypeMethodCallRule::class); + } + + public function testExtension(): void + { + $this->analyse([__DIR__ . '/data/request_get_session.php'], []); + } + + public function testBug178(): void + { + $this->analyse([__DIR__ . '/data/bug-178.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + __DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon', + ]; + } + +} diff --git a/tests/Type/Symfony/InputBagDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/InputBagDynamicReturnTypeExtensionTest.php deleted file mode 100644 index f1a2a520..00000000 --- a/tests/Type/Symfony/InputBagDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,40 +0,0 @@ -processFile( - __DIR__ . '/input_bag.php', - $expression, - $type, - [new InputBagDynamicReturnTypeExtension()] - ); - } - - /** - * @return \Iterator - */ - public function inputBagProvider(): Iterator - { - yield ['$test1', 'string|null']; - yield ['$test2', 'string|null']; - yield ['$test3', 'string']; - yield ['$test4', 'string']; - yield ['$test5', 'array|string>']; - yield ['$test6', 'array']; - } - -} diff --git a/tests/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtensionTest.php deleted file mode 100644 index 00d85e00..00000000 --- a/tests/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,37 +0,0 @@ -processFile( - __DIR__ . '/ExampleBaseCommand.php', - $expression, - $type, - [new InputInterfaceGetArgumentDynamicReturnTypeExtension(new ConsoleApplicationResolver(__DiR__ . '/console_application_loader.php'))] - ); - } - - /** - * @return \Iterator - */ - public function argumentTypesProvider(): Iterator - { - yield ['$base', 'string|null']; - yield ['$aaa', 'string']; - yield ['$bbb', 'string']; - yield ['$diff', 'array|string']; - yield ['$arr', 'array']; - yield ['$both', 'string|null']; - } - -} diff --git a/tests/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtensionTest.php deleted file mode 100644 index ede5f68d..00000000 --- a/tests/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,41 +0,0 @@ -processFile( - __DIR__ . '/ExampleOptionCommand.php', - $expression, - $type, - [new InputInterfaceGetOptionDynamicReturnTypeExtension(new ConsoleApplicationResolver(__DiR__ . '/console_application_loader.php'))] - ); - } - - /** - * @return \Iterator - */ - public function argumentTypesProvider(): Iterator - { - yield ['$a', 'bool']; - yield ['$b', 'string|null']; - yield ['$c', 'string|null']; - yield ['$d', 'array']; - yield ['$e', 'array']; - - yield ['$bb', 'int|string|null']; - yield ['$cc', 'int|string']; - yield ['$dd', 'array']; - yield ['$ee', 'array']; - } - -} diff --git a/tests/Type/Symfony/KernelInterfaceDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/KernelInterfaceDynamicReturnTypeExtensionTest.php deleted file mode 100644 index 40bef996..00000000 --- a/tests/Type/Symfony/KernelInterfaceDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,33 +0,0 @@ -processFile( - __DIR__ . '/kernel_interface.php', - $expression, - $type, - [new KernelInterfaceDynamicReturnTypeExtension()] - ); - } - - /** - * @return Iterator - */ - public function getProvider(): Iterator - { - yield ['$foo', 'string']; - yield ['$bar', 'string']; - yield ['$baz', 'array']; - } - -} diff --git a/tests/Type/Symfony/RequestDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/RequestDynamicReturnTypeExtensionTest.php deleted file mode 100644 index f0834674..00000000 --- a/tests/Type/Symfony/RequestDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,34 +0,0 @@ -processFile( - __DIR__ . '/request_get_content.php', - $expression, - $type, - [new RequestDynamicReturnTypeExtension()] - ); - } - - /** - * @return Iterator - */ - public function getContentProvider(): Iterator - { - yield ['$content1', 'string']; - yield ['$content2', 'string']; - yield ['$content3', 'resource']; - yield ['$content4', 'resource|string']; - } - -} diff --git a/tests/Type/Symfony/RequestTypeSpecifyingExtensionTest.php b/tests/Type/Symfony/RequestTypeSpecifyingExtensionTest.php deleted file mode 100644 index ac6f80c1..00000000 --- a/tests/Type/Symfony/RequestTypeSpecifyingExtensionTest.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -final class RequestTypeSpecifyingExtensionTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new VariableTypeReportingRule(); - } - - /** @return MethodTypeSpecifyingExtension[] */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new RequestTypeSpecifyingExtension($this->createBroker()), - ]; - } - - public function testGetSession(): void - { - $ref = new ReflectionMethod(Request::class, 'getSession'); - $doc = (string) $ref->getDocComment(); - - $checkedTypeString = SessionInterface::class; - if (strpos($doc, '@return SessionInterface|null') !== false) { - $checkedTypeString .= '|null'; - } - - $this->analyse([__DIR__ . '/request_get_session.php'], [ - [ - 'Variable $session1 is: ' . $checkedTypeString, - 7, - ], - [ - 'Variable $session2 is: ' . SessionInterface::class, - 11, - ], - ]); - } - -} diff --git a/tests/Type/Symfony/SerializerDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/SerializerDynamicReturnTypeExtensionTest.php deleted file mode 100644 index 0857127f..00000000 --- a/tests/Type/Symfony/SerializerDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,52 +0,0 @@ -processFile( - __DIR__ . '/serializer.php', - $expression, - $type, - [new SerializerDynamicReturnTypeExtension( - 'Symfony\Component\Serializer\SerializerInterface', - 'deserialize' - )] - ); - } - - /** - * @dataProvider getContentProvider - */ - public function testDenormalizerInterface(string $expression, string $type): void - { - $this->processFile( - __DIR__ . '/denormalizer.php', - $expression, - $type, - [new SerializerDynamicReturnTypeExtension( - 'Symfony\Component\Serializer\Normalizer\DenormalizerInterface', - 'denormalize' - )] - ); - } - - /** - * @return Iterator - */ - public function getContentProvider(): Iterator - { - yield ['$first', 'Bar']; - yield ['$second', 'array']; - yield ['$third', 'array>']; - } - -} diff --git a/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php deleted file mode 100644 index 8cfaa65d..00000000 --- a/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,74 +0,0 @@ -processFile( - __DIR__ . '/ExampleController.php', - $expression, - $type, - [new ServiceDynamicReturnTypeExtension(Controller::class, true, (new XmlServiceMapFactory($container))->create())] - ); - } - - /** - * @return Iterator - */ - public function servicesProvider(): Iterator - { - yield ['$service1', 'Foo', __DIR__ . '/container.xml']; - yield ['$service2', 'object', __DIR__ . '/container.xml']; - yield ['$service3', 'object', __DIR__ . '/container.xml']; - yield ['$service4', 'object', __DIR__ . '/container.xml']; - yield ['$has1', 'true', __DIR__ . '/container.xml']; - yield ['$has2', 'false', __DIR__ . '/container.xml']; - yield ['$has3', 'bool', __DIR__ . '/container.xml']; - yield ['$has4', 'bool', __DIR__ . '/container.xml']; - - yield ['$service1', 'object', null]; - yield ['$service2', 'object', null]; - yield ['$service3', 'object', null]; - yield ['$service4', 'object', null]; - yield ['$has1', 'bool', null]; - yield ['$has2', 'bool', null]; - yield ['$has3', 'bool', null]; - yield ['$has4', 'bool', null]; - } - - /** - * @dataProvider constantHassersOffProvider - */ - public function testConstantHassersOff(string $expression, string $type, ?string $container): void - { - $this->processFile( - __DIR__ . '/ExampleController.php', - $expression, - $type, - [new ServiceDynamicReturnTypeExtension(Controller::class, false, (new XmlServiceMapFactory($container))->create())] - ); - } - - /** - * @return Iterator - */ - public function constantHassersOffProvider(): Iterator - { - yield ['$has1', 'bool', __DIR__ . '/container.xml']; - yield ['$has2', 'bool', __DIR__ . '/container.xml']; - - yield ['$has1', 'bool', null]; - yield ['$has2', 'bool', null]; - } - -} diff --git a/tests/Type/Symfony/VariableTypeReportingRule.php b/tests/Type/Symfony/VariableTypeReportingRule.php deleted file mode 100644 index cbac8c06..00000000 --- a/tests/Type/Symfony/VariableTypeReportingRule.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -final class VariableTypeReportingRule implements Rule -{ - - public function getNodeType(): string - { - return Variable::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!is_string($node->name)) { - return []; - } - if (!$scope->isInFirstLevelStatement()) { - return []; - }; - if ($scope->isInExpressionAssign($node)) { - return []; - } - return [ - sprintf( - 'Variable $%s is: %s', - $node->name, - $scope->getType($node)->describe(VerbosityLevel::value()) - ), - ]; - } - -} diff --git a/tests/Type/Symfony/console_application_loader.php b/tests/Type/Symfony/console_application_loader.php index 524bc159..fb5459b5 100644 --- a/tests/Type/Symfony/console_application_loader.php +++ b/tests/Type/Symfony/console_application_loader.php @@ -3,7 +3,9 @@ use PHPStan\Type\Symfony\ExampleACommand; use PHPStan\Type\Symfony\ExampleBCommand; use PHPStan\Type\Symfony\ExampleOptionCommand; +use PHPStan\Type\Symfony\ExampleOptionLazyCommand; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\LazyCommand; require_once __DIR__ . '/../../../vendor/autoload.php'; @@ -11,4 +13,11 @@ $application->add(new ExampleACommand()); $application->add(new ExampleBCommand()); $application->add(new ExampleOptionCommand()); + +if (class_exists(LazyCommand::class)) { + $application->add(new LazyCommand('lazy-example-option', [], '', false, static fn () => new ExampleOptionLazyCommand())); +} else { + $application->add(new ExampleOptionLazyCommand()); +} + return $application; diff --git a/tests/Type/Symfony/container.xml b/tests/Type/Symfony/container.xml index 978519d4..16d4b7fe 100644 --- a/tests/Type/Symfony/container.xml +++ b/tests/Type/Symfony/container.xml @@ -1,6 +1,376 @@ + + 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 + the value + + + another name + 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 + + + value + + + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Type/Symfony/ExampleACommand.php b/tests/Type/Symfony/data/ExampleACommand.php similarity index 100% rename from tests/Type/Symfony/ExampleACommand.php rename to tests/Type/Symfony/data/ExampleACommand.php diff --git a/tests/Type/Symfony/data/ExampleAbstractController.php b/tests/Type/Symfony/data/ExampleAbstractController.php new file mode 100644 index 00000000..53b38066 --- /dev/null +++ b/tests/Type/Symfony/data/ExampleAbstractController.php @@ -0,0 +1,123 @@ +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())); + assertType('object', $this->get()); + + assertType('true', $this->has('foo')); + assertType('true', $this->has('synthetic')); + assertType('false', $this->has('bar')); + assertType('bool', $this->has(doFoo())); + assertType('bool', $this->has()); + } + + public function parameters(ContainerInterface $container, ParameterBagInterface $parameterBag): void + { + assertType('array|bool|float|int|string|null', $container->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $parameterBag->get('unknown')); + assertType('array|bool|float|int|string|null', $this->getParameter('unknown')); + assertType("string", $container->getParameter('app.string')); + assertType("string", $parameterBag->get('app.string')); + assertType("string", $this->getParameter('app.string')); + assertType('int', $container->getParameter('app.int')); + assertType('int', $parameterBag->get('app.int')); + assertType('int', $this->getParameter('app.int')); + 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('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("array", $container->getParameter('app.list')); + assertType("array", $parameterBag->get('app.list')); + assertType("array", $this->getParameter('app.list')); + 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')); + assertType("string", $container->getParameter('app.constant')); + assertType("string", $parameterBag->get('app.constant')); + assertType("string", $this->getParameter('app.constant')); + assertType("array", $this->getParameter('test_collection')); + assertType("array", $this->getParameter('non_empty_collection')); + + assertType('false', $container->hasParameter('unknown')); + assertType('false', $parameterBag->has('unknown')); + assertType('true', $container->hasParameter('app.string')); + assertType('true', $parameterBag->has('app.string')); + assertType('true', $container->hasParameter('app.int')); + 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.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.list')); + assertType('true', $parameterBag->has('app.list')); + assertType('true', $container->hasParameter('app.list_of_list')); + assertType('true', $parameterBag->has('app.list_of_list')); + assertType('true', $container->hasParameter('app.map')); + assertType('true', $parameterBag->has('app.map')); + assertType('true', $container->hasParameter('app.binary')); + assertType('true', $parameterBag->has('app.binary')); + assertType('true', $container->hasParameter('app.constant')); + assertType('true', $parameterBag->has('app.constant')); + } + +} diff --git a/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php new file mode 100644 index 00000000..edc6438a --- /dev/null +++ b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php @@ -0,0 +1,115 @@ +get('foo')); + assertType('object', $this->get('synthetic')); + assertType('object', $this->get('bar')); + assertType('object', $this->get(doFoo())); + assertType('object', $this->get()); + + assertType('bool', $this->has('foo')); + assertType('bool', $this->has('synthetic')); + assertType('bool', $this->has('bar')); + assertType('bool', $this->has(doFoo())); + assertType('bool', $this->has()); + } + + public function parameters(ContainerInterface $container, ParameterBagInterface $parameterBag): void + { + assertType('array|bool|float|int|string|null', $container->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $parameterBag->get('unknown')); + assertType('array|bool|float|int|string|null', $this->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.string')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.string')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.int')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.int')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.int')); + 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')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.binary')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.binary')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.binary')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.constant')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.constant')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.constant')); + + assertType('bool', $container->hasParameter('unknown')); + assertType('bool', $parameterBag->has('unknown')); + assertType('bool', $container->hasParameter('app.string')); + assertType('bool', $parameterBag->has('app.string')); + assertType('bool', $container->hasParameter('app.int')); + 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')); + assertType('bool', $parameterBag->has('app.list_of_list')); + assertType('bool', $container->hasParameter('app.map')); + assertType('bool', $parameterBag->has('app.map')); + assertType('bool', $container->hasParameter('app.binary')); + assertType('bool', $parameterBag->has('app.binary')); + assertType('bool', $container->hasParameter('app.constant')); + assertType('bool', $parameterBag->has('app.constant')); + } + +} diff --git a/tests/Type/Symfony/ExampleBCommand.php b/tests/Type/Symfony/data/ExampleBCommand.php similarity index 100% rename from tests/Type/Symfony/ExampleBCommand.php rename to tests/Type/Symfony/data/ExampleBCommand.php diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php new file mode 100644 index 00000000..0376429f --- /dev/null +++ b/tests/Type/Symfony/data/ExampleBaseCommand.php @@ -0,0 +1,67 @@ +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 new file mode 100644 index 00000000..c7563537 --- /dev/null +++ b/tests/Type/Symfony/data/ExampleController.php @@ -0,0 +1,145 @@ +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())); + assertType('object', $this->get()); + + assertType('true', $this->has('foo')); + assertType('true', $this->has('synthetic')); + assertType('false', $this->has('bar')); + assertType('bool', $this->has(doFoo())); + assertType('bool', $this->has()); + } + + public function parameters(ContainerInterface $container, ParameterBagInterface $parameterBag): void + { + assertType('array|bool|float|int|string|null', $container->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $parameterBag->get('unknown')); + assertType('array|bool|float|int|string|null', $this->getParameter('unknown')); + assertType("string", $container->getParameter('app.string')); + assertType("string", $parameterBag->get('app.string')); + assertType("string", $this->getParameter('app.string')); + assertType('int', $container->getParameter('app.int')); + assertType('int', $parameterBag->get('app.int')); + assertType('int', $this->getParameter('app.int')); + 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.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')); + assertType("string", $container->getParameter('app.constant')); + assertType("string", $parameterBag->get('app.constant')); + assertType("string", $this->getParameter('app.constant')); + + assertType('false', $container->hasParameter('unknown')); + assertType('false', $parameterBag->has('unknown')); + assertType('true', $container->hasParameter('app.string')); + assertType('true', $parameterBag->has('app.string')); + assertType('true', $container->hasParameter('app.int')); + 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')); + assertType('true', $parameterBag->has('app.list_of_list')); + assertType('true', $container->hasParameter('app.map')); + assertType('true', $parameterBag->has('app.map')); + assertType('true', $container->hasParameter('app.binary')); + 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 new file mode 100644 index 00000000..2e48dc80 --- /dev/null +++ b/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php @@ -0,0 +1,115 @@ +get('foo')); + assertType('object', $this->get('synthetic')); + assertType('object', $this->get('bar')); + assertType('object', $this->get(doFoo())); + assertType('object', $this->get()); + + assertType('bool', $this->has('foo')); + assertType('bool', $this->has('synthetic')); + assertType('bool', $this->has('bar')); + assertType('bool', $this->has(doFoo())); + assertType('bool', $this->has()); + } + + public function parameters(ContainerInterface $container, ParameterBagInterface $parameterBag): void + { + assertType('array|bool|float|int|string|null', $container->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $parameterBag->get('unknown')); + assertType('array|bool|float|int|string|null', $this->getParameter('unknown')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.string')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.string')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.string')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.int')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.int')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.int')); + 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')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.binary')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.binary')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.binary')); + assertType('array|bool|float|int|string|null', $container->getParameter('app.constant')); + assertType('array|bool|float|int|string|null', $parameterBag->get('app.constant')); + assertType('array|bool|float|int|string|null', $this->getParameter('app.constant')); + + assertType('bool', $container->hasParameter('unknown')); + assertType('bool', $parameterBag->has('unknown')); + assertType('bool', $container->hasParameter('app.string')); + assertType('bool', $parameterBag->has('app.string')); + assertType('bool', $container->hasParameter('app.int')); + 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')); + assertType('bool', $parameterBag->has('app.list_of_list')); + assertType('bool', $container->hasParameter('app.map')); + assertType('bool', $parameterBag->has('app.map')); + assertType('bool', $container->hasParameter('app.binary')); + assertType('bool', $parameterBag->has('app.binary')); + assertType('bool', $container->hasParameter('app.constant')); + assertType('bool', $parameterBag->has('app.constant')); + } + +} diff --git a/tests/Type/Symfony/ExampleOptionCommand.php b/tests/Type/Symfony/data/ExampleOptionCommand.php similarity index 54% rename from tests/Type/Symfony/ExampleOptionCommand.php rename to tests/Type/Symfony/data/ExampleOptionCommand.php index c18d55e2..b880173d 100644 --- a/tests/Type/Symfony/ExampleOptionCommand.php +++ b/tests/Type/Symfony/data/ExampleOptionCommand.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function PHPStan\Testing\assertType; final class ExampleOptionCommand extends Command { @@ -20,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); @@ -29,18 +31,19 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $a = $input->getOption('a'); - $b = $input->getOption('b'); - $c = $input->getOption('c'); - $d = $input->getOption('d'); - $e = $input->getOption('e'); - - $bb = $input->getOption('bb'); - $cc = $input->getOption('cc'); - $dd = $input->getOption('dd'); - $ee = $input->getOption('ee'); - - die; + assertType('bool', $input->getOption('a')); + assertType('string|null', $input->getOption('b')); + 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 new file mode 100644 index 00000000..433e1cfe --- /dev/null +++ b/tests/Type/Symfony/data/ExampleOptionLazyCommand.php @@ -0,0 +1,51 @@ +addOption('a', null, InputOption::VALUE_NONE); + $this->addOption('b', null, InputOption::VALUE_OPTIONAL); + $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); + $this->addOption('dd', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', [1]); + $this->addOption('ee', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, '', [1]); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + assertType('bool', $input->getOption('a')); + assertType('string|null', $input->getOption('b')); + 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/bug-178.php b/tests/Type/Symfony/data/bug-178.php new file mode 100644 index 00000000..9dbd5137 --- /dev/null +++ b/tests/Type/Symfony/data/bug-178.php @@ -0,0 +1,17 @@ +has('sonata.media.manager.category') && $this->has('sonata.media.manager.context')) { + // do stuff that requires both managers. + } + } + +} 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/denormalizer.php b/tests/Type/Symfony/data/denormalizer.php new file mode 100644 index 00000000..ccbb8fc2 --- /dev/null +++ b/tests/Type/Symfony/data/denormalizer.php @@ -0,0 +1,10 @@ +denormalize('bar', 'Bar', 'format')); +assertType('array', $serializer->denormalize('bar', 'Bar[]', 'format')); +assertType('array>', $serializer->denormalize('bar', 'Bar[][]', 'format')); +assertType('mixed', $serializer->denormalize('bar')); diff --git a/tests/Type/Symfony/data/envelope_all.php b/tests/Type/Symfony/data/envelope_all.php new file mode 100644 index 00000000..aac9d583 --- /dev/null +++ b/tests/Type/Symfony/data/envelope_all.php @@ -0,0 +1,9 @@ +', $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/header_bag_get.php b/tests/Type/Symfony/data/header_bag_get.php new file mode 100644 index 00000000..e3f4c561 --- /dev/null +++ b/tests/Type/Symfony/data/header_bag_get.php @@ -0,0 +1,13 @@ + ['bar']]); + +assertType('string|null', $bag->get('foo')); +assertType('string|null', $bag->get('foo', null)); +assertType('string', $bag->get('foo', 'baz')); +assertType('string|null', $bag->get('foo', null, true)); +assertType('string', $bag->get('foo', 'baz', true)); +assertType('array', $bag->get('foo', null, false)); +assertType('array', $bag->get('foo', 'baz', false)); diff --git a/tests/Type/Symfony/data/input_bag.php b/tests/Type/Symfony/data/input_bag.php new file mode 100644 index 00000000..77b58821 --- /dev/null +++ b/tests/Type/Symfony/data/input_bag.php @@ -0,0 +1,22 @@ + '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|bool|float|int|string>', $bag->all()); +assertType('array', $bag->all('bar')); diff --git a/tests/Type/Symfony/data/input_bag_from_request.php b/tests/Type/Symfony/data/input_bag_from_request.php new file mode 100644 index 00000000..7aa6057c --- /dev/null +++ b/tests/Type/Symfony/data/input_bag_from_request.php @@ -0,0 +1,22 @@ +request->get('foo')); + assertType('string|null', $request->query->get('foo')); + assertType('string|null', $request->cookies->get('foo')); + + assertType('bool|float|int|string', $request->request->get('foo', 'foo')); + assertType('string', $request->query->get('foo', 'foo')); + assertType('string', $request->cookies->get('foo', 'foo')); + } + +} diff --git a/tests/Type/Symfony/kernel_interface.php b/tests/Type/Symfony/data/kernel_interface.php similarity index 55% rename from tests/Type/Symfony/kernel_interface.php rename to tests/Type/Symfony/data/kernel_interface.php index aca451fb..e2239754 100644 --- a/tests/Type/Symfony/kernel_interface.php +++ b/tests/Type/Symfony/data/kernel_interface.php @@ -1,5 +1,7 @@ locateResource(''); -$bar = $kernel->locateResource('', null, true); -$baz = $kernel->locateResource('', null, false); - -die; +assertType('string', $kernel->locateResource('')); +assertType('string', $kernel->locateResource('', null, true)); +assertType('array', $kernel->locateResource('', null, false)); 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/request_get_content.php b/tests/Type/Symfony/data/request_get_content.php new file mode 100644 index 00000000..4038e3fa --- /dev/null +++ b/tests/Type/Symfony/data/request_get_content.php @@ -0,0 +1,11 @@ +getContent()); +assertType('string', $request->getContent(false)); +assertType('resource', $request->getContent(true)); +assertType('resource|string', $request->getContent(doBar())); diff --git a/tests/Type/Symfony/data/request_get_session.php b/tests/Type/Symfony/data/request_get_session.php new file mode 100644 index 00000000..9a0335e5 --- /dev/null +++ b/tests/Type/Symfony/data/request_get_session.php @@ -0,0 +1,14 @@ +getSession(); +assertType(SessionInterface::class, $request->getSession()); + +if ($request->hasSession()) { + assertType(SessionInterface::class, $request->getSession()); +} diff --git a/tests/Type/Symfony/data/request_get_session_null.php b/tests/Type/Symfony/data/request_get_session_null.php new file mode 100644 index 00000000..9c37979d --- /dev/null +++ b/tests/Type/Symfony/data/request_get_session_null.php @@ -0,0 +1,14 @@ +getSession(); +assertType(SessionInterface::class . '|null', $request->getSession()); + +if ($request->hasSession()) { + assertType(SessionInterface::class, $request->getSession()); +} 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/data/serializer.php b/tests/Type/Symfony/data/serializer.php new file mode 100644 index 00000000..8b75f575 --- /dev/null +++ b/tests/Type/Symfony/data/serializer.php @@ -0,0 +1,10 @@ +deserialize('bar', 'Bar', 'format')); +assertType('array', $serializer->deserialize('bar', 'Bar[]', 'format')); +assertType('array>', $serializer->deserialize('bar', 'Bar[][]', 'format')); +assertType('mixed', $serializer->deserialize('bar')); diff --git a/tests/Type/Symfony/data/tree_builder.php b/tests/Type/Symfony/data/tree_builder.php new file mode 100644 index 00000000..8c3c3270 --- /dev/null +++ b/tests/Type/Symfony/data/tree_builder.php @@ -0,0 +1,183 @@ +getRootNode(); +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->scalarNode("protocol") + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\NodeBuilder', $treeRootNode + ->children()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->arrayNode("protocols")); + +assertType('Symfony\Component\Config\Definition\Builder\NodeBuilder', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children()); + +assertType('Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol")); + +assertType('Symfony\Component\Config\Definition\Builder\NodeBuilder', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\NodeBuilder', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->booleanNode("auto_connect") + ->defaultTrue() + ->end() + ->scalarNode("default_connection") + ->defaultValue("default") + ->end() + ->integerNode("positive_value") + ->min(0) + ->end() + ->floatNode("big_value") + ->max(5E45) + ->end() + ->enumNode("delivery") + ->values(["standard", "expedited", "priority"]) + ->end() + ->end() + ->end() + ->end()); + +$arrayTreeBuilder = new TreeBuilder('my_tree', 'array'); +$arrayRootNode = $arrayTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $arrayRootNode); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $arrayRootNode->end()); +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $arrayRootNode + ->children() + ->arrayNode("methods") + ->prototype("scalar") + ->defaultNull() + ->end() + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $arrayRootNode + ->children() + ->arrayNode("methods") + ->scalarPrototype() + ->defaultNull() + ->end() + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $arrayRootNode + ->children() + ->arrayNode("methods") + ->prototype("scalar") + ->validate() + ->ifNotInArray(["one", "two"]) + ->thenInvalid("%s is not a valid method.") + ->end() + ->end() + ->end() + ->end()); + +assertType('Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', $arrayRootNode + ->children() + ->arrayNode("methods") + ->prototype("array") + ->beforeNormalization() + ->ifString() + ->then(static function ($v) { + return [$v]; + }) + ->end() + ->end() + ->end() + ->end()); + +$variableTreeBuilder = new TreeBuilder('my_tree', 'variable'); +$variableRootNode = $variableTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\VariableNodeDefinition', $variableRootNode); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $variableRootNode->end()); + +$scalarTreeBuilder = new TreeBuilder('my_tree', 'scalar'); +$scalarRootNode = $scalarTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', $scalarRootNode); +assertType('Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', $scalarRootNode->defaultValue("default")); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $scalarRootNode->defaultValue("default")->end()); + +$booleanTreeBuilder = new TreeBuilder('my_tree', 'boolean'); +$booleanRootNode = $booleanTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition', $booleanRootNode); +assertType('Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition', $booleanRootNode->defaultTrue()); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $booleanRootNode->defaultTrue()->end()); + +$integerTreeBuilder = new TreeBuilder('my_tree', 'integer'); +$integerRootNode = $integerTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', $integerRootNode); +assertType('Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', $integerRootNode->min(0)); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $integerRootNode->min(0)->end()); + +$floatTreeBuilder = new TreeBuilder('my_tree', 'float'); +$floatRootNode = $floatTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', $floatRootNode); +assertType('Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', $floatRootNode->max(5E45)); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $floatRootNode->max(5E45)->end()); + +$enumTreeBuilder = new TreeBuilder('my_tree', 'enum'); +$enumRootNode = $enumTreeBuilder->getRootNode(); + +assertType('Symfony\Component\Config\Definition\Builder\EnumNodeDefinition', $enumRootNode); +assertType('Symfony\Component\Config\Definition\Builder\EnumNodeDefinition', $enumRootNode->values(["standard", "expedited", "priority"])); +assertType('Symfony\Component\Config\Definition\Builder\TreeBuilder', $enumRootNode->values(["standard", "expedited", "priority"])->end()); diff --git a/tests/Type/Symfony/denormalizer.php b/tests/Type/Symfony/denormalizer.php deleted file mode 100644 index 208483b4..00000000 --- a/tests/Type/Symfony/denormalizer.php +++ /dev/null @@ -1,10 +0,0 @@ -denormalize('bar', 'Bar', 'format'); -$second = $serializer->denormalize('bar', 'Bar[]', 'format'); -$third = $serializer->denormalize('bar', 'Bar[][]', 'format'); -$fourth = $serializer->denormalize('bar'); - -die; diff --git a/tests/Type/Symfony/envelope_all.php b/tests/Type/Symfony/envelope_all.php deleted file mode 100644 index f2bd3ec4..00000000 --- a/tests/Type/Symfony/envelope_all.php +++ /dev/null @@ -1,9 +0,0 @@ -all(\Symfony\Component\Messenger\Stamp\ReceivedStamp::class); -$test2 = $envelope->all(random_bytes(1)); -$test3 = $envelope->all(); - -die; diff --git a/tests/Type/Symfony/extension-test.neon b/tests/Type/Symfony/extension-test.neon new file mode 100644 index 00000000..0f1d9522 --- /dev/null +++ b/tests/Type/Symfony/extension-test.neon @@ -0,0 +1,4 @@ +parameters: + symfony: + consoleApplicationLoader: console_application_loader.php + containerXmlPath: container.xml diff --git a/tests/Type/Symfony/header_bag_get.php b/tests/Type/Symfony/header_bag_get.php deleted file mode 100644 index ed2d36aa..00000000 --- a/tests/Type/Symfony/header_bag_get.php +++ /dev/null @@ -1,15 +0,0 @@ - ['bar']]); - -$test1 = $bag->get('foo'); -$test2 = $bag->get('foo', null); -$test3 = $bag->get('foo', 'baz'); - -$test5 = $bag->get('foo', null, true); -$test6 = $bag->get('foo', 'baz', true); - -$test8 = $bag->get('foo', null, false); -$test9 = $bag->get('foo', 'baz', false); - -die; diff --git a/tests/Type/Symfony/input_bag.php b/tests/Type/Symfony/input_bag.php deleted file mode 100644 index 7a36d61f..00000000 --- a/tests/Type/Symfony/input_bag.php +++ /dev/null @@ -1,12 +0,0 @@ - 'bar', 'bar' => ['x']]); - -$test1 = $bag->get('foo'); -$test2 = $bag->get('foo', null); -$test3 = $bag->get('foo', ''); -$test4 = $bag->get('foo', 'baz'); -$test5 = $bag->all(); -$test6 = $bag->all('bar'); - -die; diff --git a/tests/Type/Symfony/request_get_content.php b/tests/Type/Symfony/request_get_content.php deleted file mode 100644 index 3fbacaab..00000000 --- a/tests/Type/Symfony/request_get_content.php +++ /dev/null @@ -1,11 +0,0 @@ -getContent(); -$content2 = $request->getContent(false); -$content3 = $request->getContent(true); -$content4 = $request->getContent(doBar()); - -die; diff --git a/tests/Type/Symfony/request_get_session.php b/tests/Type/Symfony/request_get_session.php deleted file mode 100644 index b488455a..00000000 --- a/tests/Type/Symfony/request_get_session.php +++ /dev/null @@ -1,12 +0,0 @@ -getSession(); -$session1; - -if ($request->hasSession()) { - $session2 = $request->getSession(); - $session2; -} diff --git a/tests/Type/Symfony/serializer.php b/tests/Type/Symfony/serializer.php deleted file mode 100644 index fc16573e..00000000 --- a/tests/Type/Symfony/serializer.php +++ /dev/null @@ -1,10 +0,0 @@ -deserialize('bar', 'Bar', 'format'); -$second = $serializer->deserialize('bar', 'Bar[]', 'format'); -$third = $serializer->deserialize('bar', 'Bar[][]', 'format'); -$fourth = $serializer->deserialize('bar'); - -die; diff --git a/tests/phpunit.xml b/tests/phpunit.xml deleted file mode 100644 index 738b4ac6..00000000 --- a/tests/phpunit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - ../src - - - - - - - diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 00000000..37890cae --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,3 @@ +* +!cache +!.* diff --git a/tmp/cache/.gitignore b/tmp/cache/.gitignore new file mode 100644 index 00000000..125e3429 --- /dev/null +++ b/tmp/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.*