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
-[](https://travis-ci.org/phpstan/phpstan-symfony)
+[](https://github.com/phpstan/phpstan-symfony/actions)
[](https://packagist.org/packages/phpstan/phpstan-symfony)
[](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 @@
+*
+!.*