diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 267998da..00000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -coverage_clover: tests/tmp/clover.xml -json_path: tests/tmp/coveralls.json diff --git a/.gitattributes b/.gitattributes index 24fe5a9f..1545ee73 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +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/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 new file mode 100644 index 00000000..88543fb5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,161 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build" + +on: + pull_request: + push: + branches: + - "2.0.x" + +jobs: + lint: + name: "Lint" + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Validate Composer" + run: "composer validate" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Lint" + run: "make lint" + + coding-standard: + name: "Coding Standard" + + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Checkout build-cs" + uses: actions/checkout@v4 + with: + repository: "phpstan/build-cs" + path: "build-cs" + ref: "2.x" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.2" + + - name: "Validate Composer" + run: "composer validate" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install build-cs dependencies" + working-directory: "build-cs" + run: "composer install --no-interaction --no-progress" + + - name: "Lint" + run: "make lint" + + - name: "Coding Standard" + run: "make cs" + + tests: + name: "Tests" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + dependencies: + - "lowest" + - "highest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress" + + - name: "Tests" + run: "make tests" + + static-analysis: + name: "PHPStan" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + dependencies: + - "lowest" + - "highest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mbstring + tools: composer:v2 + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress" + + - name: "PHPStan" + run: "make phpstan" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 00000000..a8535014 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml 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 new file mode 100644 index 00000000..b8c96d48 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create release" + +on: + push: + tags: + - '*' + +jobs: + deploy: + name: "Deploy" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: Generate changelog + id: changelog + uses: metcalfc/changelog-generator@v4.6.2 + with: + myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: "Create release" + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: ${{ steps.changelog.outputs.changelog }} 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/.travis.yml b/.travis.yml deleted file mode 100644 index 2ec9a3e6..00000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: php -php: - - 7.1 - - 7.2 -env: - - dependencies=lowest - - dependencies=highest -before_script: - - composer self-update - - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --no-interaction; fi; - - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi; -script: - - vendor/bin/phing - - > - wget https://github.com/maglnet/ComposerRequireChecker/releases/download/0.2.1/composer-require-checker.phar - && php composer-require-checker.phar check composer.json -after_script: - - php vendor/bin/coveralls -v 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 c22d4c98..25a155c1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,79 @@ # PHPStan Symfony Framework extensions and rules -[![Build Status](https://travis-ci.org/phpstan/phpstan-symfony.svg)](https://travis-ci.org/phpstan/phpstan-symfony) +[![Build](https://github.com/phpstan/phpstan-symfony/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-symfony/actions) [![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-symfony/v/stable)](https://packagist.org/packages/phpstan/phpstan-symfony) [![License](https://poser.pugx.org/phpstan/phpstan-symfony/license)](https://packagist.org/packages/phpstan/phpstan-symfony) -* [PHPStan](https://github.com/phpstan/phpstan) +* [PHPStan](https://phpstan.org/) 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. +* Provides correct return type for `InputBag::get()` method based on the `$default` parameter. +* Provides correct return type for `InputBag::all()` method based on the `$key` parameter. +* Provides correct return types for `TreeBuilder` and `NodeDefinition` objects. * Notifies you when you try to get an unregistered service from the container. * Notifies you when you try to get a private service from the container. +* Optionally correct return types for `InputInterface::getArgument()`, `::getOption`, `::hasArgument`, and `::hasOption`. -## Usage + +## Installation To use this extension, require it in [Composer](https://getcomposer.org/): -```bash +``` composer require --dev phpstan/phpstan-symfony ``` -And include extension.neon in your project's PHPStan config: +If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! + +
+ Manual installation + +If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: ``` includes: - - vendor/phpstan/phpstan-symfony/extension.neon -parameters: - symfony: - container_xml_path: %rootDir%/../../../var/cache/dev/srcDevDebugProjectContainer.xml + - vendor/phpstan/phpstan-symfony/extension.neon +``` + +To perform framework-specific checks, include also this file: + +``` +includes: + - vendor/phpstan/phpstan-symfony/rules.neon ``` +
-You have to provide a path to `srcDevDebugProjectContainer.xml` or similar xml file describing your container. +# Configuration + +You have to provide a path to `srcDevDebugProjectContainer.xml` or similar XML file describing your container. + +```yaml +parameters: + symfony: + containerXmlPath: var/cache/dev/srcDevDebugProjectContainer.xml + # or with Symfony 4.2+ + containerXmlPath: var/cache/dev/srcApp_KernelDevDebugContainer.xml + # or with Symfony 5+ + 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 @@ -44,12 +85,86 @@ if ($this->has('service')) { } ``` -In that case, you can disable the `::has()` method return type resolving like this: +In that case, you can disable the `::has()` method return type resolving like this: ``` parameters: symfony: - constant_hassers: false + constantHassers: false ``` Be aware that it may hide genuine errors in your application. + +## Analysis of Symfony Console Commands + +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: + consoleApplicationLoader: tests/console-application.php +``` + +Symfony 4: + +```php +// 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); +``` + +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; + +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 +parameters: + container.dumper.inline_class_loader: false +``` + +Call the new env in your `console-application.php`: + +```php +$kernel = new \App\Kernel('phpstan_env', (bool) $_SERVER['APP_DEBUG']); +``` diff --git a/build.xml b/build.xml deleted file mode 100644 index 4c3e8284..00000000 --- a/build.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composer.json b/composer.json index cdbf671d..c03d2c99 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,10 @@ { "name": "phpstan/phpstan-symfony", + "type": "phpstan-extension", "description": "Symfony Framework extensions and rules for PHPStan", - "license": ["MIT"], + "license": [ + "MIT" + ], "authors": [ { "name": "Lukáš Unger", @@ -9,40 +12,52 @@ "homepage": "https://lookyman.net/" } ], - "minimum-stability": "dev", - "prefer-stable": true, - "extra": { - "branch-alias": { - "dev-master": "0.11-dev" - } - }, "require": { - "php": "^7.1", + "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^0.11", - "nikic/php-parser": "^4.0" - }, - "require-dev": { - "consistence/coding-standard": "^3.0.1", - "jakub-onderka/php-parallel-lint": "^1.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", - "phpunit/phpunit": "^7.0", - "phing/phing": "^2.16.0", - "phpstan/phpstan-strict-rules": "^0.11", - "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^4.5.2", - "phpstan/phpstan-phpunit": "^0.11", - "symfony/framework-bundle": "^3.0 || ^4.0" + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "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": { + "sort-packages": true + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, "autoload": { "psr-4": { "PHPStan\\": "src/" } }, "autoload-dev": { - "classmap": ["tests/"] - } + "classmap": [ + "tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/extension.neon b/extension.neon index cf080067..0803248f 100644 --- a/extension.neon +++ b/extension.neon @@ -1,42 +1,366 @@ parameters: + dynamicConstantNames: + - Symfony\Component\HttpKernel\Kernel::VERSION_ID + exceptions: + uncheckedExceptionClasses: + - 'Symfony\Component\Console\Exception\InvalidArgumentException' symfony: - constant_hassers: true + containerXmlPath: null + constantHassers: true + consoleApplicationLoader: null + stubFiles: + - stubs/Psr/Cache/CacheException.stub + - stubs/Psr/Cache/CacheItemInterface.stub + - stubs/Psr/Cache/InvalidArgumentException.stub + - stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub + - stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub + - stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.stub + - stubs/Symfony/Bundle/FrameworkBundle/Test/TestContainer.stub + - stubs/Symfony/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 + - stubs/Symfony/Component/HttpFoundation/Cookie.stub + - 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/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 + - stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub + - 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 -rules: - - PHPStan\Rules\Symfony\ContainerInterfacePrivateServiceRule - - PHPStan\Rules\Symfony\ContainerInterfaceUnknownServiceRule +parametersSchema: + symfony: structure([ + containerXmlPath: schema(string(), nullable()) + constantHassers: bool() + consoleApplicationLoader: schema(string(), nullable()) + ]) services: + # console resolver + - + factory: PHPStan\Symfony\ConsoleApplicationResolver + arguments: + 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% - - class: @symfony.serviceMapFactory::create() + 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 - - class: 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.constantHassers%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - class: 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] - - class: 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 - - class: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Component\DependencyInjection\ContainerInterface) + factory: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Component\DependencyInjection\ContainerInterface) tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] - - class: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller) + factory: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Psr\Container\ContainerInterface) tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] - - class: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController) + factory: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller) + tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] + - + factory: PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController) tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] # Request::getContent() return type - - class: PHPStan\Type\Symfony\RequestDynamicReturnTypeExtension + factory: PHPStan\Type\Symfony\RequestDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Request::getSession() type specification + - + factory: PHPStan\Type\Symfony\RequestTypeSpecifyingExtension + tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] + + # InputBag::get() return type + - + factory: PHPStan\Type\Symfony\InputBagDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # HeaderBag::get() return type + - + factory: PHPStan\Type\Symfony\HeaderBagDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # SerializerInterface::deserialize() return type + - + factory: PHPStan\Type\Symfony\SerializerDynamicReturnTypeExtension(Symfony\Component\Serializer\SerializerInterface, deserialize) + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # DenormalizerInterface::denormalize() return type + - + factory: PHPStan\Type\Symfony\SerializerDynamicReturnTypeExtension(Symfony\Component\Serializer\Normalizer\DenormalizerInterface, denormalize) + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Envelope::all() return type + - + 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 + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # InputInterface::hasArgument() type specification + - + factory: PHPStan\Type\Symfony\ArgumentTypeSpecifyingExtension + tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] + + # InputInterface::hasArgument() return type + - + factory: PHPStan\Type\Symfony\InputInterfaceHasArgumentDynamicReturnTypeExtension + 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 + tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension] + + # InputInterface::hasOption() return type + - + factory: PHPStan\Type\Symfony\InputInterfaceHasOptionDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # ArrayNodeDefinition::*prototype() return type + - + factory: PHPStan\Type\Symfony\Config\ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # ExprBuilder::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\ExprBuilder + methods: [end] + + # NodeBuilder::*node() return type + - + factory: PHPStan\Type\Symfony\Config\PassParentObjectDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeBuilder + methods: [arrayNode, scalarNode, booleanNode, integerNode, floatNode, enumNode, variableNode] + + # NodeBuilder::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeBuilder + methods: [end] + + # NodeDefinition::children() return type + - + factory: PHPStan\Type\Symfony\Config\PassParentObjectDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeDefinition + methods: [children, validate, beforeNormalization] + + # NodeDefinition::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeDefinition + methods: [end] + + # new TreeBuilder() return type + - + factory: PHPStan\Type\Symfony\Config\TreeBuilderDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] + + # TreeBuilder::getRootNode() return type + - + 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 2ff108e3..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,45 +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 45df5546..f13073e1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,11 +1,15 @@ includes: + - extension.neon + - rules.neon - vendor/phpstan/phpstan-phpunit/extension.neon - 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: - - */tests/tmp/* - - */tests/*/ExampleContainer.php - - */tests/*/ExampleController.php - - */tests/*/request_get_content.php + excludePaths: + - tests/tmp/* + - tests/*/Example*.php + - tests/*/console_application_loader.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/rules.neon b/rules.neon new file mode 100644 index 00000000..cedcea7a --- /dev/null +++ b/rules.neon @@ -0,0 +1,8 @@ +rules: + - PHPStan\Rules\Symfony\ContainerInterfacePrivateServiceRule + - PHPStan\Rules\Symfony\ContainerInterfaceUnknownServiceRule + - PHPStan\Rules\Symfony\UndefinedArgumentRule + - PHPStan\Rules\Symfony\InvalidArgumentDefaultValueRule + - PHPStan\Rules\Symfony\UndefinedOptionRule + - PHPStan\Rules\Symfony\InvalidOptionDefaultValueRule + diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index 89aa45df..96f1efea 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -6,15 +6,20 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ServiceMap; +use PHPStan\TrinaryLogic; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use function sprintf; +/** + * @implements Rule + */ final class ContainerInterfacePrivateServiceRule implements Rule { - /** @var ServiceMap */ - private $serviceMap; + private ServiceMap $serviceMap; public function __construct(ServiceMap $symfonyServiceMap) { @@ -26,39 +31,64 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param MethodCall $node - * @param Scope $scope - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { 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); + + $isTestContainerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Test\TestContainer'))->isSuperTypeOf($argType); + $isOldServiceSubscriber = (new ObjectType('Symfony\Component\DependencyInjection\ServiceSubscriberInterface'))->isSuperTypeOf($argType); + $isServiceSubscriber = $this->isServiceSubscriber($argType, $scope); + $isServiceLocator = (new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($argType); + if ($isTestContainerType->yes() || $isOldServiceSubscriber->yes() || $isServiceSubscriber->yes() || $isServiceLocator->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); - $isTestContainerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Test\TestContainer'))->isSuperTypeOf($argType); - if ($isTestContainerType->yes() || (!$isControllerType->yes() && !$isAbstractControllerType->yes() && !$isContainerType->yes())) { + $isPsrContainerType = (new ObjectType('Psr\Container\ContainerInterface'))->isSuperTypeOf($argType); + if ( + !$isControllerType->yes() + && !$isAbstractControllerType->yes() + && !$isContainerType->yes() + && !$isPsrContainerType->yes() + ) { return []; } - $serviceId = 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 a567cd49..23444b6b 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -4,23 +4,26 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; -use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Symfony\ServiceMap; use PHPStan\Type\ObjectType; use PHPStan\Type\Symfony\Helper; +use function sprintf; +/** + * @implements Rule + */ 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; @@ -31,34 +34,45 @@ public function getNodeType(): string return MethodCall::class; } - /** - * @param MethodCall $node - * @param Scope $scope - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { 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); - if (!$isControllerType->yes() && !$isAbstractControllerType->yes() && !$isContainerType->yes()) { + $isPsrContainerType = (new ObjectType('Psr\Container\ContainerInterface'))->isSuperTypeOf($argType); + if ( + !$isControllerType->yes() + && !$isAbstractControllerType->yes() + && !$isContainerType->yes() + && !$isPsrContainerType->yes() + ) { return []; } - $serviceId = 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 && !$scope->isSpecified(Helper::createMarkerNode($node->var, $scope->getType($node->args[0]->value), $this->printer))) { - return [sprintf('Service "%s" is not registered in the container.', $serviceId)]; + $serviceIdType = $scope->getType($node->getArgs()[0]->value); + if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { + return [ + RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId)) + ->identifier('symfonyContainer.serviceNotFound') + ->build(), + ]; } } diff --git a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php new file mode 100644 index 00000000..5435d4c9 --- /dev/null +++ b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php @@ -0,0 +1,82 @@ + + */ +final class InvalidArgumentDefaultValueRule implements Rule +{ + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + 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->getArgs()[3])) { + return []; + } + + $modeType = isset($node->getArgs()[1]) ? $scope->getType($node->getArgs()[1]->value) : new NullType(); + if ($modeType->isNull()->yes()) { + $modeType = new ConstantIntegerType(2); // InputArgument::OPTIONAL + } + $modeTypes = $modeType->getConstantScalarTypes(); + if (count($modeTypes) !== 1) { + return []; + } + if (!$modeTypes[0] instanceof ConstantIntegerType) { + return []; + } + $mode = $modeTypes[0]->getValue(); + + $defaultType = $scope->getType($node->getArgs()[3]->value); + + // not an array + if (($mode & 4) !== 4 && !(new UnionType([new StringType(), new NullType()]))->isSuperTypeOf($defaultType)->yes()) { + 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 [ + 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 new file mode 100644 index 00000000..2e3dc0e9 --- /dev/null +++ b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php @@ -0,0 +1,88 @@ + + */ +final class InvalidOptionDefaultValueRule implements Rule +{ + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + 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->getArgs()[4])) { + return []; + } + + $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 = $modeType->getConstantScalarTypes(); + if (count($modeTypes) !== 1) { + return []; + } + if (!$modeTypes[0] instanceof ConstantIntegerType) { + return []; + } + $mode = $modeTypes[0]->getValue(); + + $defaultType = $scope->getType($node->getArgs()[4]->value); + + // not an array + if (($mode & 8) !== 8) { + $checkType = new UnionType([new StringType(), new IntegerType(), new NullType(), new BooleanType()]); + if (!$checkType->isSuperTypeOf($defaultType)->yes()) { + 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 [ + 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 new file mode 100644 index 00000000..ee36a23c --- /dev/null +++ b/src/Rules/Symfony/UndefinedArgumentRule.php @@ -0,0 +1,84 @@ + + */ +final class UndefinedArgumentRule implements Rule +{ + + private ConsoleApplicationResolver $consoleApplicationResolver; + + private Printer $printer; + + public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer) + { + $this->consoleApplicationResolver = $consoleApplicationResolver; + $this->printer = $printer; + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf(new ObjectType($classReflection->getName()))->yes()) { + return []; + } + if (!(new ObjectType('Symfony\Component\Console\Input\InputInterface'))->isSuperTypeOf($scope->getType($node->var))->yes()) { + return []; + } + if (!$node->name instanceof Node\Identifier || $node->name->name !== 'getArgument') { + return []; + } + if (!isset($node->getArgs()[0])) { + return []; + } + + $argType = $scope->getType($node->getArgs()[0]->value); + $argStrings = $argType->getConstantStrings(); + if (count($argStrings) !== 1) { + return []; + } + $argName = $argStrings[0]->getValue(); + + $errors = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $name => $command) { + try { + $command->mergeApplicationDefinition(); + $command->getDefinition()->getArgument($argName); + } catch (InvalidArgumentException $e) { + if ($scope->getType(Helper::createMarkerNode($node->var, $argType, $this->printer))->equals($argType)) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define argument "%s".', $name, $argName)) + ->identifier('symfonyConsole.argumentNotFound') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Symfony/UndefinedOptionRule.php b/src/Rules/Symfony/UndefinedOptionRule.php new file mode 100644 index 00000000..39a6a4ac --- /dev/null +++ b/src/Rules/Symfony/UndefinedOptionRule.php @@ -0,0 +1,84 @@ + + */ +final class UndefinedOptionRule implements Rule +{ + + private ConsoleApplicationResolver $consoleApplicationResolver; + + private Printer $printer; + + public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer) + { + $this->consoleApplicationResolver = $consoleApplicationResolver; + $this->printer = $printer; + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf(new ObjectType($classReflection->getName()))->yes()) { + return []; + } + if (!(new ObjectType('Symfony\Component\Console\Input\InputInterface'))->isSuperTypeOf($scope->getType($node->var))->yes()) { + return []; + } + if (!$node->name instanceof Node\Identifier || $node->name->name !== 'getOption') { + return []; + } + if (!isset($node->getArgs()[0])) { + return []; + } + + $optType = $scope->getType($node->getArgs()[0]->value); + $optStrings = $optType->getConstantStrings(); + if (count($optStrings) !== 1) { + return []; + } + $optName = $optStrings[0]->getValue(); + + $errors = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $name => $command) { + try { + $command->mergeApplicationDefinition(); + $command->getDefinition()->getOption($optName); + } catch (InvalidArgumentException $e) { + if ($scope->getType(Helper::createMarkerNode($node->var, $optType, $this->printer))->equals($optType)) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define option "%s".', $name, $optName)) + ->identifier('symfonyConsole.optionNotFound') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Symfony/ConsoleApplicationResolver.php b/src/Symfony/ConsoleApplicationResolver.php new file mode 100644 index 00000000..13b24d26 --- /dev/null +++ b/src/Symfony/ConsoleApplicationResolver.php @@ -0,0 +1,90 @@ +consoleApplicationLoader = $consoleApplicationLoader; + } + + public function hasConsoleApplicationLoader(): bool + { + 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(sprintf('Cannot load console application. Check the parameters.symfony.consoleApplicationLoader setting in PHPStan\'s config. The offending value is "%s".', $this->consoleApplicationLoader)); + } + + return $this->consoleApplication = require $this->consoleApplicationLoader; + } + + /** + * @return Command[] + */ + public function findCommands(ClassReflection $classReflection): array + { + $consoleApplication = $this->getConsoleApplication(); + if ($consoleApplication === null) { + return []; + } + + $classType = new ObjectType($classReflection->getName()); + if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf($classType)->yes()) { + return []; + } + + $commands = []; + 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; + } + + return $commands; + } + +} 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 new file mode 100644 index 00000000..5d3bccd0 --- /dev/null +++ b/src/Symfony/DefaultServiceMap.php @@ -0,0 +1,42 @@ +services = $services; + } + + /** + * @return ServiceDefinition[] + */ + public function getServices(): array + { + return $this->services; + } + + public function getService(string $id): ?ServiceDefinition + { + return $this->services[$id] ?? null; + } + + public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string + { + $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 71da7e9a..bbd2d8a3 100644 --- a/src/Symfony/ServiceMap.php +++ b/src/Symfony/ServiceMap.php @@ -4,40 +4,20 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\TypeUtils; -use function count; -final class ServiceMap +/** + * @api + */ +interface ServiceMap { - /** @var \PHPStan\Symfony\ServiceDefinition[] */ - private $services; - - /** - * @param \PHPStan\Symfony\ServiceDefinition[] $services - */ - public function __construct(array $services) - { - $this->services = $services; - } - /** - * @return \PHPStan\Symfony\ServiceDefinition[] + * @return ServiceDefinition[] */ - public function getServices(): array - { - return $this->services; - } + public function getServices(): array; - public function getService(string $id): ?ServiceDefinition - { - return $this->services[$id] ?? null; - } + public function getService(string $id): ?ServiceDefinition; - public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string - { - $strings = TypeUtils::getConstantStrings($scope->getType($node)); - return count($strings) === 1 ? $strings[0]->getValue() : null; - } + public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string; } 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 6b8a892d..ac79cb30 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -2,7 +2,11 @@ namespace PHPStan\Symfony; -use function simplexml_load_file; +use SimpleXMLElement; +use function count; +use function file_get_contents; +use function ksort; +use function simplexml_load_string; use function sprintf; use function strpos; use function substr; @@ -10,44 +14,65 @@ final class XmlServiceMapFactory implements ServiceMapFactory { - /** @var string */ - 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 { - $xml = @simplexml_load_file($this->containerXml); + if ($this->containerXml === null) { + return new FakeServiceMap(); + } + + $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 not exists', $this->containerXml)); + 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; + } - if ($service->getAlias() !== null) { - $aliases[] = $service; - } else { - $services[$service->getId()] = $service; + $serviceTags = []; + foreach ($def->tag as $tag) { + $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? []; + $tagName = $tagAttrs['name']; + unset($tagAttrs['name']); + + $serviceTags[] = new ServiceTag($tagName, $tagAttrs); + } + + $service = new Service( + $this->cleanServiceId((string) $attrs->id), + isset($attrs->class) ? (string) $attrs->class : null, + isset($attrs->public) && (string) $attrs->public === 'true', + isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', + isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null, + $serviceTags, + ); + + if ($service->getAlias() !== null) { + $aliases[] = $service; + } else { + $services[$service->getId()] = $service; + } } } foreach ($aliases as $service) { @@ -61,11 +86,18 @@ public function create(): ServiceMap $services[$alias]->getClass(), $service->isPublic(), $service->isSynthetic(), - $alias + $alias, ); } - return new ServiceMap($services); + 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 new file mode 100644 index 00000000..5c21f021 --- /dev/null +++ b/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php @@ -0,0 +1,56 @@ +printer = $printer; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool + { + return $methodReflection->getName() === 'hasArgument' && !$context->null(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + if (!isset($node->getArgs()[0])) { + return new SpecifiedTypes(); + } + $argType = $scope->getType($node->getArgs()[0]->value); + return $this->typeSpecifier->create( + Helper::createMarkerNode($node->var, $argType, $this->printer), + $argType, + $context, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} 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 new file mode 100644 index 00000000..1dae22e2 --- /dev/null +++ b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php @@ -0,0 +1,82 @@ + 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition', + 'scalar' => 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', + 'boolean' => 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition', + 'integer' => 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', + 'float' => 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', + 'array' => 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', + 'enum' => 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition', + ]; + + public function getClass(): string + { + return 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'prototype' || in_array($methodReflection->getName(), self::PROTOTYPE_METHODS, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $calledOnType = $scope->getType($methodCall->var); + + $defaultType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + + if ($methodReflection->getName() === 'prototype') { + if (!isset($methodCall->getArgs()[0])) { + return $defaultType; + } + + $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) { + $type = $argStrings[0]->getValue(); + + return new ParentObjectType(self::MAPPING[$type], $calledOnType); + } + } + + return new ParentObjectType( + $defaultType->describe(VerbosityLevel::typeOnly()), + $calledOnType, + ); + } + +} diff --git a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php new file mode 100644 index 00000000..800d9dbc --- /dev/null +++ b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php @@ -0,0 +1,61 @@ +className = $className; + $this->methods = $methods; + } + + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), $this->methods, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $calledOnType = $scope->getType($methodCall->var); + + $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 new file mode 100644 index 00000000..034d5d80 --- /dev/null +++ b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php @@ -0,0 +1,56 @@ +className = $className; + $this->methods = $methods; + } + + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), $this->methods, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + $calledOnType = $scope->getType($methodCall->var); + if ($calledOnType instanceof ParentObjectType) { + return $calledOnType->getParent(); + } + + return null; + } + +} diff --git a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php new file mode 100644 index 00000000..4f266c50 --- /dev/null +++ b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php @@ -0,0 +1,58 @@ + 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition', + 'scalar' => 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', + 'boolean' => 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition', + 'integer' => 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', + 'float' => 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', + 'array' => 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', + 'enum' => 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition', + ]; + + public function getClass(): string + { + return 'Symfony\Component\Config\Definition\Builder\TreeBuilder'; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + if (!$methodCall->class instanceof Name) { + throw new ShouldNotHappenException(); + } + + $className = $scope->resolveName($methodCall->class); + + $type = 'array'; + + 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(); + } + } + + return new TreeBuilderType($className, self::MAPPING[$type]); + } + +} diff --git a/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php new file mode 100644 index 00000000..2be2b574 --- /dev/null +++ b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'getRootNode'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + $calledOnType = $scope->getType($methodCall->var); + if ($calledOnType instanceof TreeBuilderType) { + return new ParentObjectType( + $calledOnType->getRootNodeClassName(), + $calledOnType, + ); + } + + return null; + } + +} diff --git a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php new file mode 100644 index 00000000..19baf926 --- /dev/null +++ b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php @@ -0,0 +1,31 @@ +parent = $parent; + } + + public function getParent(): Type + { + return $this->parent; + } + + protected function describeAdditionalCacheKey(): string + { + return $this->parent->describe(VerbosityLevel::cache()); + } + +} diff --git a/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php new file mode 100644 index 00000000..93c713f1 --- /dev/null +++ b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php @@ -0,0 +1,29 @@ +rootNodeClassName = $rootNodeClassName; + } + + public function getRootNodeClassName(): string + { + return $this->rootNodeClassName; + } + + protected function describeAdditionalCacheKey(): string + { + return $this->getRootNodeClassName(); + } + +} diff --git a/src/Type/Symfony/EnvelopeReturnTypeExtension.php b/src/Type/Symfony/EnvelopeReturnTypeExtension.php new file mode 100644 index 00000000..06e08772 --- /dev/null +++ b/src/Type/Symfony/EnvelopeReturnTypeExtension.php @@ -0,0 +1,57 @@ +getName() === 'all'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + 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->getArgs()[0]->value); + if (count($argType->getConstantStrings()) === 0) { + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType()); + } + + $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 new file mode 100644 index 00000000..f50dc4e8 --- /dev/null +++ b/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php @@ -0,0 +1,53 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + $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->getArgs()[1]) ? $scope->getType($methodCall->getArgs()[1]->value) : new NullType(); + + return TypeCombinator::union($defaultArgType, new StringType()); + } + if ($compareTypes === $isFalseType) { + return new ArrayType(new IntegerType(), new StringType()); + } + + 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 new file mode 100644 index 00000000..75e6d0bc --- /dev/null +++ b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php @@ -0,0 +1,85 @@ +getName(), ['get', 'all'], true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + if ($methodReflection->getName() === 'get') { + return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope); + } + + if ($methodReflection->getName() === 'all') { + return $this->getAllTypeFromMethodCall($methodCall); + } + + throw new ShouldNotHappenException(); + } + + private function getGetTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + if (isset($methodCall->getArgs()[1])) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); + $isNull = (new NullType())->isSuperTypeOf($argType); + if ($isNull->no()) { + return TypeCombinator::removeNull(ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType()); + } + } + + return null; + } + + private function getAllTypeFromMethodCall( + MethodCall $methodCall + ): Type + { + if (isset($methodCall->getArgs()[0])) { + return new ArrayType(new MixedType(), new MixedType(true)); + } + + 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 new file mode 100644 index 00000000..88bd7b0e --- /dev/null +++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php @@ -0,0 +1,99 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getArgument'; + } + + 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(); + + $argTypes = []; + $canBeNullInInteract = false; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $argument = $command->getDefinition()->getArgument($argName); + if ($argument->isArray()) { + $argType = new ArrayType(new IntegerType(), new StringType()); + if (!$argument->isRequired() && $argument->getDefault() !== []) { + $argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault())); + } + } else { + $argType = new StringType(); + if (!$argument->isRequired()) { + $argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault())); + } else { + $canBeNullInInteract = true; + } + } + $argTypes[] = $argType; + } catch (InvalidArgumentException $e) { + // noop + } + } + + 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 new file mode 100644 index 00000000..6d0346cf --- /dev/null +++ b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php @@ -0,0 +1,69 @@ +consoleApplicationResolver = $consoleApplicationResolver; + $this->getOptionTypeHelper = $getOptionTypeHelper; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getOption'; + } + + 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; + } + + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + if (count($optStrings) !== 1) { + return null; + } + $optName = $optStrings[0]->getValue(); + + $optTypes = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $option = $command->getDefinition()->getOption($optName); + $optTypes[] = $this->getOptionTypeHelper->getOptionType($scope, $option); + } catch (InvalidArgumentException $e) { + // noop + } + } + + 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 new file mode 100644 index 00000000..34bffcea --- /dev/null +++ b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php @@ -0,0 +1,84 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'hasArgument'; + } + + 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(); + + 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 { + $command->mergeApplicationDefinition(); + $command->getDefinition()->getArgument($argName); + $returnTypes[] = true; + } catch (InvalidArgumentException $e) { + $returnTypes[] = false; + } + } + + if (count($returnTypes) === 0) { + return null; + } + + $returnTypes = array_unique($returnTypes); + return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null; + } + +} diff --git a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php new file mode 100644 index 00000000..e4f8b5b1 --- /dev/null +++ b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php @@ -0,0 +1,72 @@ +consoleApplicationResolver = $consoleApplicationResolver; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'hasOption'; + } + + 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; + } + + $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings(); + if (count($optStrings) !== 1) { + return null; + } + $optName = $optStrings[0]->getValue(); + + $returnTypes = []; + foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) { + try { + $command->mergeApplicationDefinition(); + $command->getDefinition()->getOption($optName); + $returnTypes[] = true; + } catch (InvalidArgumentException $e) { + $returnTypes[] = false; + } + } + + if (count($returnTypes) === 0) { + return null; + } + + $returnTypes = array_unique($returnTypes); + return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null; + } + +} diff --git a/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php new file mode 100644 index 00000000..810de2f6 --- /dev/null +++ b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName() === 'locateResource'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type + { + $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) { + return new StringType(); + } + if ($compareTypes === $isFalseType) { + return new ArrayType(new IntegerType(), new StringType()); + } + + 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 new file mode 100644 index 00000000..8cd9dbd5 --- /dev/null +++ b/src/Type/Symfony/OptionTypeSpecifyingExtension.php @@ -0,0 +1,56 @@ +printer = $printer; + } + + public function getClass(): string + { + return 'Symfony\Component\Console\Input\InputInterface'; + } + + public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool + { + return $methodReflection->getName() === 'hasOption' && !$context->null(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + if (!isset($node->getArgs()[0])) { + return new SpecifiedTypes(); + } + $argType = $scope->getType($node->getArgs()[0]->value); + return $this->typeSpecifier->create( + Helper::createMarkerNode($node->var, $argType, $this->printer), + $argType, + $context, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} 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 new file mode 100644 index 00000000..40d38493 --- /dev/null +++ b/src/Type/Symfony/RequestTypeSpecifyingExtension.php @@ -0,0 +1,57 @@ +getName() === self::HAS_METHOD_NAME && !$context->null(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $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($returnType), + $context, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} 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 new file mode 100755 index 00000000..84f256e4 --- /dev/null +++ b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php @@ -0,0 +1,73 @@ +class = $class; + $this->method = $method; + } + + public function getClass(): string + { + return $this->class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === $this->method; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + if (!isset($methodCall->getArgs()[1])) { + return new MixedType(); + } + + $argType = $scope->getType($methodCall->getArgs()[1]->value); + if (count($argType->getConstantStrings()) === 0) { + return new MixedType(); + } + + $types = []; + foreach ($argType->getConstantStrings() as $constantString) { + $types[] = $this->getType($constantString->getValue()); + } + + return TypeCombinator::union(...$types); + } + + private function getType(string $objectName): Type + { + if (substr($objectName, -2) === '[]') { + // The key type is determined by the data + return new ArrayType(new MixedType(false), $this->getType(substr($objectName, 0, -2))); + } + + return new ObjectType($objectName); + } + +} diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php index 48e8aacf..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 $symfonyServiceMap; + 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->symfonyServiceMap = $symfonyServiceMap; + $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 = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { - $service = $this->symfonyServiceMap->getService($serviceId); - if ($service !== null && !$service->isSynthetic()) { - return new ObjectType($service->getClass() ?? $serviceId); + $service = $this->serviceMap->getService($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 = ServiceMap::getServiceIdFromNode($methodCall->args[0]->value, $scope); + $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { - $service = $this->symfonyServiceMap->getService($serviceId); + $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/ContainerBuilder.stub b/stubs/Symfony/Component/DependencyInjection/ContainerBuilder.stub new file mode 100644 index 00000000..457734fc --- /dev/null +++ b/stubs/Symfony/Component/DependencyInjection/ContainerBuilder.stub @@ -0,0 +1,7 @@ + $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 @@ +> + */ + public static function getSubscribedEvents(); +} diff --git a/stubs/Symfony/Component/EventDispatcher/GenericEvent.stub b/stubs/Symfony/Component/EventDispatcher/GenericEvent.stub new file mode 100644 index 00000000..6e57a7df --- /dev/null +++ b/stubs/Symfony/Component/EventDispatcher/GenericEvent.stub @@ -0,0 +1,11 @@ + + */ +class GenericEvent implements \IteratorAggregate +{ + +} 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/ChoiceList/Loader/ChoiceLoaderInterface.stub b/stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub new file mode 100644 index 00000000..0388b898 --- /dev/null +++ b/stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub @@ -0,0 +1,22 @@ + $values + * @param callable|null $value + * + * @return array + */ + public function loadChoicesForValues(array $values, $value = null); + + /** + * @param array $choices + * @param callable|null $value + * + * @return array + */ + public function loadValuesForChoices(array $choices, $value = null); +} 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 @@ +> + * @extends FormConfigBuilderInterface + */ +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 new file mode 100644 index 00000000..4bd21229 --- /dev/null +++ b/stubs/Symfony/Component/Form/FormInterface.stub @@ -0,0 +1,24 @@ +> + * @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 new file mode 100644 index 00000000..a03d5e1c --- /dev/null +++ b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub @@ -0,0 +1,27 @@ + $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 new file mode 100644 index 00000000..8536656a --- /dev/null +++ b/stubs/Symfony/Component/Form/FormTypeInterface.stub @@ -0,0 +1,27 @@ + $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 new file mode 100644 index 00000000..08c64752 --- /dev/null +++ b/stubs/Symfony/Component/Form/FormView.stub @@ -0,0 +1,22 @@ + + * @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 new file mode 100644 index 00000000..cfb45fa3 --- /dev/null +++ b/stubs/Symfony/Component/HttpFoundation/Cookie.stub @@ -0,0 +1,50 @@ + + */ +class HeaderBag implements \IteratorAggregate +{ + + /** + * @phpstan-return \Traversable + */ + public function getIterator() {} + +} 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 @@ + + */ +class ParameterBag implements \IteratorAggregate +{ + /** + * @return list + */ + public function keys(): array + { + } +} diff --git a/stubs/Symfony/Component/HttpFoundation/Request.stub b/stubs/Symfony/Component/HttpFoundation/Request.stub 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/HttpFoundation/Session.stub b/stubs/Symfony/Component/HttpFoundation/Session.stub new file mode 100644 index 00000000..25485fe6 --- /dev/null +++ b/stubs/Symfony/Component/HttpFoundation/Session.stub @@ -0,0 +1,16 @@ + + */ +class Session implements \IteratorAggregate +{ + + /** + * @phpstan-return \Traversable + */ + public function getIterator() {} + +} 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/PropertyAccess/PropertyPathInterface.stub b/stubs/Symfony/Component/PropertyAccess/PropertyPathInterface.stub new file mode 100644 index 00000000..d687193e --- /dev/null +++ b/stubs/Symfony/Component/PropertyAccess/PropertyPathInterface.stub @@ -0,0 +1,10 @@ + + */ +interface PropertyPathInterface extends \Traversable +{ +} diff --git a/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub new file mode 100644 index 00000000..2f509501 --- /dev/null +++ b/stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub @@ -0,0 +1,40 @@ + + */ + 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/EntryInterface.stub b/stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub new file mode 100644 index 00000000..335e581d --- /dev/null +++ b/stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub @@ -0,0 +1,7 @@ + $context + * @return bool + */ + public function supportsDecoding($format, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Encoder/DecoderInterface.stub b/stubs/Symfony/Component/Serializer/Encoder/DecoderInterface.stub new file mode 100644 index 00000000..e19f772e --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Encoder/DecoderInterface.stub @@ -0,0 +1,25 @@ + $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, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub new file mode 100644 index 00000000..11e374eb --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub @@ -0,0 +1,25 @@ + $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, 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 bool + */ + public function supportsDenormalization($data, $type, $format = null, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Normalizer/ContextAwareNormalizerInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/ContextAwareNormalizerInterface.stub new file mode 100644 index 00000000..aaf97be5 --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Normalizer/ContextAwareNormalizerInterface.stub @@ -0,0 +1,14 @@ + $context + * @return bool + */ + public function supportsNormalization($data, $format = null, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Normalizer/DenormalizableInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/DenormalizableInterface.stub new file mode 100644 index 00000000..7e8cb7e9 --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Normalizer/DenormalizableInterface.stub @@ -0,0 +1,16 @@ +|string|int|float|bool $data + * @param string|null $format + * @param array $context + * + * @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 new file mode 100644 index 00000000..b7e9968b --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub @@ -0,0 +1,40 @@ + $context + * @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 = []); + + /** + * @param mixed $data + * @param string $type + * @param string|null $format + * @param array $context + * @return bool + */ + public function supportsDenormalization($data, $type, $format = null, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub new file mode 100644 index 00000000..c7958c55 --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub @@ -0,0 +1,14 @@ + $context + * @return array|string|int|float|bool + */ + public function normalize(NormalizerInterface $normalizer, $format = null, array $context = []); +} diff --git a/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub new file mode 100644 index 00000000..ba86b6b6 --- /dev/null +++ b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub @@ -0,0 +1,35 @@ + $context + * + * @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, array $context = []); +} diff --git a/stubs/Symfony/Component/Validator/Constraint.stub b/stubs/Symfony/Component/Validator/Constraint.stub new file mode 100644 index 00000000..e7a4b501 --- /dev/null +++ b/stubs/Symfony/Component/Validator/Constraint.stub @@ -0,0 +1,21 @@ + + */ + protected static $errorNames = []; + + /** + * @return array + */ + public function getRequiredOptions(); + + /** + * @return string|array + */ + public function getTargets(); +} diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub new file mode 100644 index 00000000..fd1c7b9b --- /dev/null +++ b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub @@ -0,0 +1,12 @@ + + * @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, \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 Node implements \IteratorAggregate +{ + +} diff --git a/tests/Rules/NonexistentInputBagClassTest.php b/tests/Rules/NonexistentInputBagClassTest.php new file mode 100644 index 00000000..5dc674d4 --- /dev/null +++ b/tests/Rules/NonexistentInputBagClassTest.php @@ -0,0 +1,32 @@ + + */ +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 new file mode 100644 index 00000000..bbecb2e8 --- /dev/null +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php @@ -0,0 +1,88 @@ + + */ +final class ContainerInterfacePrivateServiceRuleFakeTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(null))->create()); + } + + 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', + ], + [], + ); + } + + public function testGetPrivateServiceInLegacyServiceSubscriber(): void + { + if (!interface_exists('Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface')) { + 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', + ], + [], + ); + } + + public function testGetPrivateServiceInServiceSubscriber(): void + { + if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) { + 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 944405e7..dfa3d2b7 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php @@ -5,7 +5,12 @@ use PHPStan\Rules\Rule; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; +use function class_exists; +use function interface_exists; +/** + * @extends RuleTestCase + */ final class ContainerInterfacePrivateServiceRuleTest extends RuleTestCase { @@ -16,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', @@ -23,9 +31,49 @@ public function testGetPrivateService(): void [ [ 'Service "private" is private.', - 12, + 13, ], - ] + ], + ); + } + + public function testGetPrivateServiceInLegacyServiceSubscriber(): void + { + if (!interface_exists('Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface')) { + 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', + ], + [], + ); + } + + public function testGetPrivateServiceInServiceSubscriber(): void + { + if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) { + 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 new file mode 100644 index 00000000..8d70f1c3 --- /dev/null +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php @@ -0,0 +1,62 @@ + + */ +final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), self::getContainer()->getByType(Printer::class)); + } + + /** + * @return MethodTypeSpecifyingExtension[] + */ + protected function getMethodTypeSpecifyingExtensions(): array + { + return [ + 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 2fcc32ed..c975750f 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -2,44 +2,80 @@ 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\Controller; +use function class_exists; +use function interface_exists; +/** + * @extends RuleTestCase + */ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), self::getContainer()->getByType(Printer::class)); } - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array + public function testGetPrivateService(): void { - return [ - new ServiceTypeSpecifyingExtension(Controller::class, new Standard()), - ]; + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( + [ + __DIR__ . '/ExampleController.php', + ], + [ + [ + 'Service "unknown" is not registered in the container.', + 25, + ], + ], + ); } - public function testGetPrivateService(): void + public function testGetPrivateServiceInAbstractController(): void { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( [ - __DIR__ . '/ExampleController.php', + __DIR__ . '/ExampleAbstractController.php', ], [ [ '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 new file mode 100644 index 00000000..22e5900e --- /dev/null +++ b/tests/Rules/Symfony/ExampleAbstractController.php @@ -0,0 +1,43 @@ +get('private'); + } + + public function privateServiceInTestContainer(): void + { + /** @var TestContainer $container */ + $container = doFoo(); + $container->get('private'); + } + + public function unknownService(): void + { + $this->get('unknown'); + } + + public function unknownGuardedServiceInsideContext(): void + { + if ($this->has('unknown')) { // phpcs:ignore + $this->get('unknown'); + } + } + + public function unknownGuardedServiceOutsideOfContext(): void + { + if (!$this->has('unknown')) { + return; + } + $this->get('unknown'); + } + +} diff --git a/tests/Rules/Symfony/ExampleCommand.php b/tests/Rules/Symfony/ExampleCommand.php new file mode 100644 index 00000000..6dec4cbd --- /dev/null +++ b/tests/Rules/Symfony/ExampleCommand.php @@ -0,0 +1,58 @@ +setName('example-rule'); + + $this->addArgument('arg'); + + $this->addArgument('foo1', null, '', null); + $this->addArgument('bar1', null, '', ''); + $this->addArgument('baz1', null, '', 1); + $this->addArgument('quz1', null, '', ['']); + + $this->addArgument('quz2', InputArgument::IS_ARRAY, '', ['a' => 'b']); + + $this->addOption('aaa'); + + $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 = []; + $this->addOption('e', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', $defaults); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $input->getArgument('arg'); + $input->getArgument('undefined'); + + if ($input->hasArgument('guarded')) { + $input->getArgument('guarded'); + } + + $input->getOption('aaa'); + $input->getOption('bbb'); + + if ($input->hasOption('ccc')) { + $input->getOption('ccc'); + } + + return 0; + } + +} 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/ExampleLegacyServiceSubscriber.php b/tests/Rules/Symfony/ExampleLegacyServiceSubscriber.php new file mode 100644 index 00000000..63f93679 --- /dev/null +++ b/tests/Rules/Symfony/ExampleLegacyServiceSubscriber.php @@ -0,0 +1,23 @@ +get('private'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromAbstractController.php b/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromAbstractController.php new file mode 100644 index 00000000..a090ea6e --- /dev/null +++ b/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromAbstractController.php @@ -0,0 +1,24 @@ +get('private'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromLegacyController.php b/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromLegacyController.php new file mode 100644 index 00000000..af6af3dd --- /dev/null +++ b/tests/Rules/Symfony/ExampleLegacyServiceSubscriberFromLegacyController.php @@ -0,0 +1,24 @@ +get('private'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/ExampleServiceSubscriber.php b/tests/Rules/Symfony/ExampleServiceSubscriber.php new file mode 100644 index 00000000..ec9c966d --- /dev/null +++ b/tests/Rules/Symfony/ExampleServiceSubscriber.php @@ -0,0 +1,40 @@ +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'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/ExampleServiceSubscriberFromAbstractController.php b/tests/Rules/Symfony/ExampleServiceSubscriberFromAbstractController.php new file mode 100644 index 00000000..ea7c6b36 --- /dev/null +++ b/tests/Rules/Symfony/ExampleServiceSubscriberFromAbstractController.php @@ -0,0 +1,23 @@ +get('private'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/ExampleServiceSubscriberFromLegacyController.php b/tests/Rules/Symfony/ExampleServiceSubscriberFromLegacyController.php new file mode 100644 index 00000000..73610870 --- /dev/null +++ b/tests/Rules/Symfony/ExampleServiceSubscriberFromLegacyController.php @@ -0,0 +1,24 @@ +get('private'); + } + + /** + * @return string[] + */ + public static function getSubscribedServices(): array + { + return []; + } + +} diff --git a/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php new file mode 100644 index 00000000..bc6a6563 --- /dev/null +++ b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php @@ -0,0 +1,42 @@ + + */ +final class InvalidArgumentDefaultValueRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidArgumentDefaultValueRule(); + } + + public function testGetArgument(): void + { + $this->analyse( + [ + __DIR__ . '/ExampleCommand.php', + ], + [ + [ + 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, int given.', + 22, + ], + [ + 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, array given.', + 23, + ], + [ + '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 new file mode 100644 index 00000000..2dcbbcd1 --- /dev/null +++ b/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php @@ -0,0 +1,34 @@ + + */ +final class InvalidOptionDefaultValueRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidOptionDefaultValueRule(); + } + + public function testGetArgument(): void + { + $this->analyse( + [ + __DIR__ . '/ExampleCommand.php', + ], + [ + [ + '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 new file mode 100644 index 00000000..d9970ef6 --- /dev/null +++ b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php @@ -0,0 +1,43 @@ + + */ +final class UndefinedArgumentRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); + } + + public function testGetArgument(): void + { + $this->analyse( + [ + __DIR__ . '/ExampleCommand.php', + ], + [ + [ + 'Command "example-rule" does not define argument "undefined".', + 42, + ], + ], + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/argument.neon', + ]; + } + +} diff --git a/tests/Rules/Symfony/UndefinedOptionRuleTest.php b/tests/Rules/Symfony/UndefinedOptionRuleTest.php new file mode 100644 index 00000000..7f759213 --- /dev/null +++ b/tests/Rules/Symfony/UndefinedOptionRuleTest.php @@ -0,0 +1,43 @@ + + */ +final class UndefinedOptionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class)); + } + + public function testGetArgument(): void + { + $this->analyse( + [ + __DIR__ . '/ExampleCommand.php', + ], + [ + [ + 'Command "example-rule" does not define option "bbb".', + 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/console_application_loader.php b/tests/Rules/Symfony/console_application_loader.php new file mode 100644 index 00000000..05f8ed51 --- /dev/null +++ b/tests/Rules/Symfony/console_application_loader.php @@ -0,0 +1,10 @@ +add(new ExampleCommand()); +return $application; 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/ServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php similarity index 61% rename from tests/Symfony/ServiceMapTest.php rename to tests/Symfony/DefaultServiceMapTest.php index 13ad2d9e..b43bee49 100644 --- a/tests/Symfony/ServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -5,7 +5,7 @@ use Iterator; use PHPUnit\Framework\TestCase; -final class ServiceMapTest extends TestCase +final class DefaultServiceMapTest extends TestCase { /** @@ -17,91 +17,113 @@ public function testGetService(string $id, callable $validator): void $validator($factory->create()->getService($id)); } + public function testGetContainerEscapedPath(): void + { + $factory = new XmlServiceMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); + $serviceMap = $factory->create(); + + self::assertNotNull($serviceMap->getService('withClass')); + } + + /** + * @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/NeonTest.php b/tests/Symfony/NeonTest.php deleted file mode 100644 index a6fec3f8..00000000 --- a/tests/Symfony/NeonTest.php +++ /dev/null @@ -1,48 +0,0 @@ -getClassName($key)); - - @unlink($generatedContainer); - self::assertFileNotExists($generatedContainer); - - $class = $loader->load(function (Compiler $compiler): void { - $compiler->addExtension('rules', new RulesExtension()); - $compiler->addConfig(['parameters' => ['rootDir' => __DIR__]]); - $compiler->loadConfig(__DIR__ . '/config.neon'); - $compiler->loadConfig(__DIR__ . '/../../extension.neon'); - }, $key); - /** @var \Nette\DI\Container $container */ - $container = new $class(); - - self::assertSame([ - 'rootDir' => __DIR__, - 'symfony' => [ - 'container_xml_path' => __DIR__ . '/container.xml', - 'constant_hassers' => true, - ], - ], $container->getParameters()); - - self::assertCount(2, $container->findByTag('phpstan.rules.rule')); - self::assertCount(4, $container->findByTag('phpstan.broker.dynamicMethodReturnTypeExtension')); - self::assertCount(3, $container->findByTag('phpstan.typeSpecifier.methodTypeSpecifyingExtension')); - self::assertInstanceOf(ServiceMap::class, $container->getByType(ServiceMap::class)); - } - -} 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 d5787020..00000000 --- a/tests/Symfony/config.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - symfony: - container_xml_path: %rootDir%/container.xml - -services: - - PhpParser\PrettyPrinter\Standard 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 new file mode 100644 index 00000000..5bed7715 --- /dev/null +++ b/tests/Symfony/containers/bugfix%2Fcontainer.xml @@ -0,0 +1,45 @@ + + + + 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/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 9cd9d2a6..00000000 --- a/tests/Type/Symfony/ExtensionTestCase.php +++ /dev/null @@ -1,74 +0,0 @@ -createBroker([$extension]); - $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); - /** @var \PHPStan\PhpDoc\TypeNodeResolver $typeNodeResolver */ - $typeNodeResolver = self::getContainer()->getByType(TypeNodeResolver::class); - $resolver = new NodeScopeResolver( - $broker, - $parser, - new FileTypeMapper(...[ // PHPStan commit 7b23c31 broke the constructor so we have to use splat here - $parser, - $phpDocStringResolver, - $this->createMock(Cache::class), - $this->createMock(AnonymousClassNameHelper::class), // PHPStan commit 4fcdccc broke the helper so we have to use a mock here - $typeNodeResolver, - ]), - $fileHelper, - $typeSpecifier, - 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 ((new Standard())->prettyPrint([$node]) !== 'die') { - return; - } - /** @var \PhpParser\Node\Stmt\Expression $expNode */ - $expNode = $this->getParser()->parseString(sprintf('getType($expNode->expr)->describe(VerbosityLevel::typeOnly())); - $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/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/RequestDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/RequestDynamicReturnTypeExtensionTest.php deleted file mode 100644 index 2f697e4d..00000000 --- a/tests/Type/Symfony/RequestDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,31 +0,0 @@ -processFile( - __DIR__ . '/request_get_content.php', - $expression, - $type, - new RequestDynamicReturnTypeExtension() - ); - } - - public function getContentProvider(): Iterator - { - yield ['$content1', 'string']; - yield ['$content2', 'string']; - yield ['$content3', 'resource']; - yield ['$content4', 'resource|string']; - } - -} diff --git a/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php deleted file mode 100644 index a1ef5100..00000000 --- a/tests/Type/Symfony/ServiceDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,56 +0,0 @@ -processFile( - __DIR__ . '/ExampleController.php', - $expression, - $type, - new ServiceDynamicReturnTypeExtension(Controller::class, true, (new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create()) - ); - } - - public function servicesProvider(): Iterator - { - yield ['$service1', 'Foo']; - yield ['$service2', 'object']; - yield ['$service3', 'object']; - yield ['$service4', 'object']; - yield ['$has1', 'true']; - yield ['$has2', 'false']; - yield ['$has3', 'bool']; - yield ['$has4', 'bool']; - } - - /** - * @dataProvider constantHassersOffProvider - */ - public function testConstantHassersOff(string $expression, string $type): void - { - $this->processFile( - __DIR__ . '/ExampleController.php', - $expression, - $type, - new ServiceDynamicReturnTypeExtension(Controller::class, false, (new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create()) - ); - } - - public function constantHassersOffProvider(): Iterator - { - yield ['$has1', 'bool']; - yield ['$has2', 'bool']; - } - -} diff --git a/tests/Type/Symfony/console_application_loader.php b/tests/Type/Symfony/console_application_loader.php new file mode 100644 index 00000000..fb5459b5 --- /dev/null +++ b/tests/Type/Symfony/console_application_loader.php @@ -0,0 +1,23 @@ +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/data/ExampleACommand.php b/tests/Type/Symfony/data/ExampleACommand.php new file mode 100644 index 00000000..4ba27410 --- /dev/null +++ b/tests/Type/Symfony/data/ExampleACommand.php @@ -0,0 +1,21 @@ +setName('example-a'); + + $this->addArgument('aaa', null, '', 'aaa'); + $this->addArgument('both'); + $this->addArgument('diff', null, '', 'ddd'); + $this->addArgument('arr', InputArgument::IS_ARRAY, '', ['arr']); + } + +} 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/data/ExampleBCommand.php b/tests/Type/Symfony/data/ExampleBCommand.php new file mode 100644 index 00000000..b6b00dc2 --- /dev/null +++ b/tests/Type/Symfony/data/ExampleBCommand.php @@ -0,0 +1,20 @@ +setName('example-b'); + + $this->addArgument('both'); + $this->addArgument('bbb', null, '', 'bbb'); + $this->addArgument('diff', InputArgument::IS_ARRAY, '', ['diff']); + } + +} 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/data/ExampleOptionCommand.php b/tests/Type/Symfony/data/ExampleOptionCommand.php new file mode 100644 index 00000000..b880173d --- /dev/null +++ b/tests/Type/Symfony/data/ExampleOptionCommand.php @@ -0,0 +1,49 @@ +setName('example-option'); + + $this->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/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/data/kernel_interface.php b/tests/Type/Symfony/data/kernel_interface.php new file mode 100644 index 00000000..e2239754 --- /dev/null +++ b/tests/Type/Symfony/data/kernel_interface.php @@ -0,0 +1,19 @@ +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/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/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/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 @@ +* +!.*