diff --git a/.gitattributes b/.gitattributes
index 17405d6e..1545ee73 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -7,6 +7,6 @@ tmp export-ignore
.gitignore export-ignore
.travis.yml export-ignore
Makefile export-ignore
-phpcs.xml export-ignore
phpstan.neon export-ignore
+phpstan-baseline.neon export-ignore
phpunit.xml export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 15b77335..00000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: composer
- directory: "/"
- schedule:
- interval: monthly
- open-pull-requests-limit: 10
- - package-ecosystem: composer
- directory: "/build-cs"
- schedule:
- interval: monthly
- open-pull-requests-limit: 10
- - package-ecosystem: github-actions
- directory: "/"
- schedule:
- interval: monthly
- open-pull-requests-limit: 10
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 00000000..d3f5961e
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,19 @@
+{
+ "extends": [
+ "config:base",
+ "schedule:weekly"
+ ],
+ "rangeStrategy": "update-lockfile",
+ "packageRules": [
+ {
+ "matchPaths": ["+(composer.json)"],
+ "enabled": true,
+ "groupName": "root-composer"
+ },
+ {
+ "matchPaths": [".github/**"],
+ "enabled": true,
+ "groupName": "github-actions"
+ }
+ ]
+}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bccd397b..88543fb5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ on:
pull_request:
push:
branches:
- - "master"
+ - "2.0.x"
jobs:
lint:
@@ -16,16 +16,16 @@ jobs:
strategy:
matrix:
php-version:
- - "7.1"
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
+ - "8.2"
+ - "8.3"
+ - "8.4"
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: actions/checkout@v4
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -39,27 +39,30 @@ jobs:
- name: "Install dependencies"
run: "composer install --no-interaction --no-progress"
- - name: "Downgrade PHPUnit"
- if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3'
- run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
-
- name: "Lint"
run: "make lint"
- coding-standards:
+ coding-standard:
name: "Coding Standard"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: actions/checkout@v4
+
+ - name: "Checkout build-cs"
+ uses: actions/checkout@v4
+ with:
+ repository: "phpstan/build-cs"
+ path: "build-cs"
+ ref: "2.x"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
- php-version: "8.0"
+ php-version: "8.2"
- name: "Validate Composer"
run: "composer validate"
@@ -67,6 +70,10 @@ jobs:
- name: "Install dependencies"
run: "composer install --no-interaction --no-progress"
+ - name: "Install build-cs dependencies"
+ working-directory: "build-cs"
+ run: "composer install --no-interaction --no-progress"
+
- name: "Lint"
run: "make lint"
@@ -81,22 +88,19 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "7.1"
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
+ - "8.2"
+ - "8.3"
+ - "8.4"
dependencies:
- "lowest"
- "highest"
- exclude:
- - php-version: "7.1"
- dependencies: "highest"
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: actions/checkout@v4
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -112,10 +116,6 @@ jobs:
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress"
- - name: "Downgrade PHPUnit"
- if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3'
- run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
-
- name: "Tests"
run: "make tests"
@@ -127,19 +127,19 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "7.1"
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
+ - "8.2"
+ - "8.3"
+ - "8.4"
dependencies:
- "lowest"
- "highest"
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: actions/checkout@v4
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -157,9 +157,5 @@ jobs:
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress"
- - name: "Downgrade PHPUnit"
- if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3'
- run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
-
- name: "PHPStan"
run: "make phpstan"
diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml
new file mode 100644
index 00000000..a8535014
--- /dev/null
+++ b/.github/workflows/create-tag.yml
@@ -0,0 +1,53 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Create tag"
+
+on:
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Next version'
+ required: true
+ default: 'patch'
+ type: choice
+ options:
+ - patch
+ - minor
+
+jobs:
+ create-tag:
+ name: "Create tag"
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.PHPSTAN_BOT_TOKEN }}
+
+ - name: 'Get Previous tag'
+ id: previoustag
+ uses: "WyriHaximus/github-action-get-previous-tag@v1"
+ env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: 'Get next versions'
+ id: semvers
+ uses: "WyriHaximus/github-action-next-semvers@v1"
+ with:
+ version: ${{ steps.previoustag.outputs.tag }}
+
+ - name: "Create new minor tag"
+ uses: rickstaa/action-create-tag@v1
+ if: inputs.version == 'minor'
+ with:
+ tag: ${{ steps.semvers.outputs.minor }}
+ message: ${{ steps.semvers.outputs.minor }}
+
+ - name: "Create new patch tag"
+ uses: rickstaa/action-create-tag@v1
+ if: inputs.version == 'patch'
+ with:
+ tag: ${{ steps.semvers.outputs.patch }}
+ message: ${{ steps.semvers.outputs.patch }}
diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml
index 960c1ba5..047fe906 100644
--- a/.github/workflows/lock-closed-issues.yml
+++ b/.github/workflows/lock-closed-issues.yml
@@ -2,20 +2,20 @@ name: 'Lock Issues'
on:
schedule:
- - cron: '0 0 * * *'
+ - cron: '5 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- - uses: dessant/lock-threads@v2
+ - uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
- issue-lock-inactive-days: '31'
- issue-exclude-created-before: ''
- issue-exclude-labels: ''
- issue-lock-labels: ''
- issue-lock-comment: >
+ issue-inactive-days: '31'
+ exclude-issue-created-before: ''
+ exclude-any-issue-labels: ''
+ add-issue-labels: ''
+ issue-comment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml
new file mode 100644
index 00000000..1ba4fd77
--- /dev/null
+++ b/.github/workflows/release-toot.yml
@@ -0,0 +1,21 @@
+name: Toot release
+
+# More triggers
+# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release
+on:
+ release:
+ types: [published]
+
+jobs:
+ toot:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: cbrgm/mastodon-github-action@v2
+ if: ${{ !github.event.repository.private }}
+ with:
+ # GitHub event payload
+ # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release
+ message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan"
+ env:
+ MASTODON_URL: https://phpc.social
+ MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml
new file mode 100644
index 00000000..09b39ded
--- /dev/null
+++ b/.github/workflows/release-tweet.yml
@@ -0,0 +1,24 @@
+name: Tweet release
+
+# More triggers
+# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release
+on:
+ release:
+ types: [published]
+
+jobs:
+ tweet:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: Eomm/why-don-t-you-tweet@v1
+ if: ${{ !github.event.repository.private }}
+ with:
+ # GitHub event payload
+ # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release
+ tweet-message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan"
+ env:
+ # Get your tokens from https://developer.twitter.com/apps
+ TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
+ TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
+ TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
+ TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 225470a6..b8c96d48 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,19 +14,19 @@ jobs:
steps:
- name: "Checkout"
- uses: "actions/checkout@v2"
+ uses: actions/checkout@v4
- name: Generate changelog
id: changelog
- uses: metcalfc/changelog-generator@v1.0.0
+ uses: metcalfc/changelog-generator@v4.6.2
with:
- myToken: ${{ secrets.GITHUB_TOKEN }}
+ myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }}
- name: "Create release"
id: create-release
uses: actions/create-release@v1
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
diff --git a/.gitignore b/.gitignore
index d6a83e59..7de9f3c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/tests/tmp
+/build-cs
/vendor
-composer.lock
+/composer.lock
.phpunit.result.cache
diff --git a/LICENSE b/LICENSE
index 0b9f74d9..cb2e557c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2017 Lukáš Unger
+Copyright (c) 2025 PHPStan s.r.o.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index fe917d3b..1ee557df 100644
--- a/Makefile
+++ b/Makefile
@@ -10,14 +10,24 @@ lint:
php vendor/bin/parallel-lint --colors \
src tests
+.PHONY: cs-install
+cs-install:
+ git clone https://github.com/phpstan/build-cs.git || true
+ git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x
+ composer install --working-dir build-cs
+
.PHONY: cs
cs:
- composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs
+ php build-cs/vendor/bin/phpcs --standard=build-cs/phpcs.xml src tests
.PHONY: cs-fix
cs-fix:
- php build-cs/vendor/bin/phpcbf
+ php build-cs/vendor/bin/phpcbf --standard=build-cs/phpcs.xml src tests
.PHONY: phpstan
phpstan:
php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests
+
+.PHONY: phpstan-generate-baseline
+phpstan-generate-baseline:
+ php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon
diff --git a/README.md b/README.md
index ef670e6b..25a155c1 100644
--- a/README.md
+++ b/README.md
@@ -61,14 +61,18 @@ You have to provide a path to `srcDevDebugProjectContainer.xml` or similar XML f
```yaml
parameters:
symfony:
- container_xml_path: var/cache/dev/srcDevDebugProjectContainer.xml
+ containerXmlPath: var/cache/dev/srcDevDebugProjectContainer.xml
# or with Symfony 4.2+
- container_xml_path: var/cache/dev/srcApp_KernelDevDebugContainer.xml
+ containerXmlPath: var/cache/dev/srcApp_KernelDevDebugContainer.xml
# or with Symfony 5+
- container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
+ containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
# If you're using PHP config files for Symfony 5.3+, you also need this for auto-loading of `Symfony\Config`:
scanDirectories:
- var/cache/dev/Symfony/Config
+ # If you're using PHP config files (including the ones under packages/*.php) for Symfony 5.3+,
+ # you need this to load the helper functions (i.e. service(), env()):
+ scanFiles:
+ - vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php
```
## Constant hassers
@@ -86,7 +90,7 @@ In that case, you can disable the `::has()` method return type resolving like th
```
parameters:
symfony:
- constant_hassers: false
+ constantHassers: false
```
Be aware that it may hide genuine errors in your application.
@@ -99,7 +103,7 @@ by providing the console application from your own application. This will allow
```neon
parameters:
symfony:
- console_application_loader: tests/console-application.php
+ consoleApplicationLoader: tests/console-application.php
```
Symfony 4:
@@ -132,6 +136,22 @@ $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
return new Application($kernel);
```
+[Single Command Application](https://symfony.com/doc/current/components/console/single_command_tool.html):
+
+```php
+// tests/console-application.php
+
+use App\Application; // where Application extends Symfony\Component\Console\SingleCommandApplication
+use Symfony\Component\Console;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+$application = new Console\Application();
+$application->add(new Application());
+
+return $application;
+```
+
You may then encounter an error with PhpParser:
> Compile Error: Cannot Declare interface PhpParser\NodeVisitor, because the name is already in use
diff --git a/build-cs/.gitignore b/build-cs/.gitignore
deleted file mode 100644
index ff72e2d0..00000000
--- a/build-cs/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/composer.lock
-/vendor
diff --git a/build-cs/composer.json b/build-cs/composer.json
deleted file mode 100644
index ed7744e1..00000000
--- a/build-cs/composer.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "require-dev": {
- "consistence-community/coding-standard": "^3.10",
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
- "slevomat/coding-standard": "^6.4"
- }
-}
diff --git a/composer.json b/composer.json
index 06adbfbf..c03d2c99 100644
--- a/composer.json
+++ b/composer.json
@@ -13,36 +13,34 @@
}
],
"require": {
- "php": "^7.1 || ^8.0",
+ "php": "^7.4 || ^8.0",
"ext-simplexml": "*",
- "phpstan/phpstan": "^1.0"
+ "phpstan/phpstan": "^2.1.13"
},
"conflict": {
"symfony/framework-bundle": "<3.0"
},
"require-dev": {
- "nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
- "phpstan/phpstan-phpunit": "^1.0",
- "phpstan/phpstan-strict-rules": "^1.0",
- "phpunit/phpunit": "^9.5",
- "symfony/config": "^4.2 || ^5.0",
- "symfony/console": "^4.0 || ^5.0",
- "symfony/framework-bundle": "^4.4 || ^5.0",
- "symfony/http-foundation": "^4.0 || ^5.0",
- "symfony/messenger": "^4.2 || ^5.0",
- "symfony/serializer": "^4.0 || ^5.0"
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "psr/container": "1.1.2",
+ "symfony/config": "^5.4 || ^6.1",
+ "symfony/console": "^5.4 || ^6.1",
+ "symfony/dependency-injection": "^5.4 || ^6.1",
+ "symfony/form": "^5.4 || ^6.1",
+ "symfony/framework-bundle": "^5.4 || ^6.1",
+ "symfony/http-foundation": "^5.4 || ^6.1",
+ "symfony/messenger": "^5.4",
+ "symfony/polyfill-php80": "^1.24",
+ "symfony/serializer": "^5.4",
+ "symfony/service-contracts": "^2.2.0"
},
"config": {
- "platform": {
- "php": "7.4.6"
- },
"sort-packages": true
},
"extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- },
"phpstan": {
"includes": [
"extension.neon",
diff --git a/extension.neon b/extension.neon
index 4b36a06c..0803248f 100644
--- a/extension.neon
+++ b/extension.neon
@@ -5,21 +5,41 @@ parameters:
uncheckedExceptionClasses:
- 'Symfony\Component\Console\Exception\InvalidArgumentException'
symfony:
- container_xml_path: null
- constant_hassers: true
- console_application_loader: null
+ containerXmlPath: null
+ constantHassers: true
+ consoleApplicationLoader: null
stubFiles:
+ - stubs/Psr/Cache/CacheException.stub
+ - stubs/Psr/Cache/CacheItemInterface.stub
+ - stubs/Psr/Cache/InvalidArgumentException.stub
+ - stubs/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.stub
+ - stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
- stubs/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.stub
- stubs/Symfony/Bundle/FrameworkBundle/Test/TestContainer.stub
- - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub
+ - stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.stub
+ - stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub
+ - stubs/Symfony/Component/Console/Command.stub
+ - stubs/Symfony/Component/Console/Exception/ExceptionInterface.stub
+ - stubs/Symfony/Component/Console/Exception/InvalidArgumentException.stub
+ - stubs/Symfony/Component/Console/Exception/LogicException.stub
+ - stubs/Symfony/Component/Console/Helper/HelperInterface.stub
+ - stubs/Symfony/Component/Console/Output/OutputInterface.stub
- stubs/Symfony/Component/DependencyInjection/ContainerBuilder.stub
- stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub
- stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub
- stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub
- stubs/Symfony/Component/EventDispatcher/GenericEvent.stub
+ - stubs/Symfony/Component/Form/AbstractType.stub
+ - stubs/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.stub
+ - stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub
+ - stubs/Symfony/Component/Form/Exception/RuntimeException.stub
+ - stubs/Symfony/Component/Form/Exception/TransformationFailedException.stub
- stubs/Symfony/Component/Form/DataTransformerInterface.stub
- stubs/Symfony/Component/Form/FormBuilderInterface.stub
+ - stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
+ - stubs/Symfony/Component/Form/FormConfigInterface.stub
- stubs/Symfony/Component/Form/FormInterface.stub
+ - stubs/Symfony/Component/Form/FormFactoryInterface.stub
- stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
- stubs/Symfony/Component/Form/FormTypeInterface.stub
- stubs/Symfony/Component/Form/FormView.stub
@@ -29,13 +49,33 @@ parameters:
- stubs/Symfony/Component/HttpFoundation/Session.stub
- stubs/Symfony/Component/Messenger/StampInterface.stub
- stubs/Symfony/Component/Messenger/Envelope.stub
+ - stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub
+ - stubs/Symfony/Component/OptionsResolver/Options.stub
+ - stubs/Symfony/Component/Process/Exception/LogicException.stub
- stubs/Symfony/Component/Process/Process.stub
+ - stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub
+ - stubs/Symfony/Component/PropertyAccess/Exception/ExceptionInterface.stub
+ - stubs/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.stub
+ - stubs/Symfony/Component/PropertyAccess/Exception/RuntimeException.stub
+ - stubs/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.stub
+ - stubs/Symfony/Component/PropertyAccess/PropertyAccessorInterface.stub
- stubs/Symfony/Component/PropertyAccess/PropertyPathInterface.stub
- stubs/Symfony/Component/Security/Acl/Model/AclInterface.stub
- stubs/Symfony/Component/Security/Acl/Model/EntryInterface.stub
+ - stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub
+ - stubs/Symfony/Component/Security/Core/Authorization/Voter/Voter.stub
+ - stubs/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.stub
- stubs/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.stub
- stubs/Symfony/Component/Serializer/Encoder/DecoderInterface.stub
- stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub
+ - stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub
+ - stubs/Symfony/Component/Serializer/Exception/CircularReferenceException.stub
+ - stubs/Symfony/Component/Serializer/Exception/ExceptionInterface.stub
+ - stubs/Symfony/Component/Serializer/Exception/ExtraAttributesException.stub
+ - stubs/Symfony/Component/Serializer/Exception/InvalidArgumentException.stub
+ - stubs/Symfony/Component/Serializer/Exception/LogicException.stub
+ - stubs/Symfony/Component/Serializer/Exception/RuntimeException.stub
+ - stubs/Symfony/Component/Serializer/Exception/UnexpectedValueException.stub
- stubs/Symfony/Component/Serializer/Normalizer/ContextAwareDenormalizerInterface.stub
- stubs/Symfony/Component/Serializer/Normalizer/ContextAwareNormalizerInterface.stub
- stubs/Symfony/Component/Serializer/Normalizer/DenormalizableInterface.stub
@@ -43,15 +83,21 @@ parameters:
- stubs/Symfony/Component/Serializer/Normalizer/NormalizableInterface.stub
- stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub
- stubs/Symfony/Component/Validator/Constraint.stub
+ - stubs/Symfony/Component/Validator/Constraints/Composite.stub
+ - stubs/Symfony/Component/Validator/Constraints/Compound.stub
- stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub
- stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub
+ - stubs/Symfony/Contracts/Cache/CacheInterface.stub
+ - stubs/Symfony/Contracts/Cache/CallbackInterface.stub
+ - stubs/Symfony/Contracts/Cache/ItemInterface.stub
+ - stubs/Symfony/Contracts/Service/ServiceSubscriberInterface.stub
- stubs/Twig/Node/Node.stub
parametersSchema:
symfony: structure([
- container_xml_path: schema(string(), nullable())
- constant_hassers: bool()
- console_application_loader: schema(string(), nullable())
+ containerXmlPath: schema(string(), nullable())
+ constantHassers: bool()
+ consoleApplicationLoader: schema(string(), nullable())
])
services:
@@ -59,34 +105,45 @@ services:
-
factory: PHPStan\Symfony\ConsoleApplicationResolver
arguments:
- consoleApplicationLoader: %symfony.console_application_loader%
+ consoleApplicationLoader: %symfony.consoleApplicationLoader%
# service map
symfony.serviceMapFactory:
class: PHPStan\Symfony\ServiceMapFactory
- factory: PHPStan\Symfony\XmlServiceMapFactory(%symfony.container_xml_path%)
+ factory: PHPStan\Symfony\XmlServiceMapFactory
+ arguments:
+ containerXmlPath: %symfony.containerXmlPath%
-
factory: @symfony.serviceMapFactory::create()
# parameter map
symfony.parameterMapFactory:
class: PHPStan\Symfony\ParameterMapFactory
- factory: PHPStan\Symfony\XmlParameterMapFactory(%symfony.container_xml_path%)
+ factory: PHPStan\Symfony\XmlParameterMapFactory
+ arguments:
+ containerXmlPath: %symfony.containerXmlPath%
-
factory: @symfony.parameterMapFactory::create()
+ # message map
+ symfony.messageMapFactory:
+ class: PHPStan\Symfony\MessageMapFactory
+ factory: PHPStan\Symfony\MessageMapFactory
+ -
+ factory: @symfony.messageMapFactory::create()
+
# ControllerTrait::get()/has() return type
-
- factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
- factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
- factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
- factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
# ControllerTrait::has() type specification
@@ -138,6 +195,11 @@ services:
factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+ # Messenger HandleTrait::handle() return type
+ -
+ class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension
+ tags: [phpstan.broker.expressionTypeResolverExtension]
+
# InputInterface::getArgument() return type
-
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension
@@ -154,10 +216,17 @@ services:
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
# InputInterface::getOption() return type
+ -
+ factory: PHPStan\Type\Symfony\GetOptionTypeHelper
-
factory: PHPStan\Type\Symfony\InputInterfaceGetOptionDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+ # InputInterface::getOptions() return type
+ -
+ factory: PHPStan\Type\Symfony\InputInterfaceGetOptionsDynamicReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
# InputInterface::hasOption() type specification
-
factory: PHPStan\Type\Symfony\OptionTypeSpecifyingExtension
@@ -230,22 +299,68 @@ services:
# ParameterBagInterface::get()/has() return type
-
- factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
# ContainerInterface::getParameter()/hasParameter() return type
-
- factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter', %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter', %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
# (Abstract)Controller::getParameter() return type
-
- factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
- factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null, %symfony.constant_hassers%)
+ factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null, %symfony.constantHassers%)
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
class: PHPStan\Symfony\InputBagStubFilesExtension
tags:
- phpstan.stubFilesExtension
+ -
+ class: PHPStan\Symfony\SymfonyDiagnoseExtension
+ tags:
+ - phpstan.diagnoseExtension
+
+ # FormInterface::getErrors() return type
+ -
+ factory: PHPStan\Type\Symfony\Form\FormInterfaceDynamicReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
+ # Command::getHelper() return type
+ -
+ factory: PHPStan\Type\Symfony\CommandGetHelperDynamicReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
+ # ResponseHeaderBag::getCookies() return type
+ -
+ factory: PHPStan\Type\Symfony\ResponseHeaderBagDynamicReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
+ # InputBag::get() type specification
+ -
+ factory: PHPStan\Type\Symfony\InputBagTypeSpecifyingExtension
+ tags: [phpstan.typeSpecifier.methodTypeSpecifyingExtension]
+
+ # Additional constructors and initialization checks for @required autowiring
+ -
+ class: PHPStan\Symfony\RequiredAutowiringExtension
+ tags:
+ - phpstan.properties.readWriteExtension
+ - phpstan.additionalConstructorsExtension
+
+ # CacheInterface::get() return type
+ -
+ factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
+ # Extension::getConfiguration() return type
+ -
+ factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension
+ tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
+
+ -
+ class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension
+ tags:
+ - phpstan.resultCacheMetaExtension
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index 2807efec..00000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
-
- src
- tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- tests/tmp
- tests/*/data
-
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 00000000..79e87db9
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,79 @@
+parameters:
+ ignoreErrors:
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Rules/Symfony/UndefinedArgumentRule.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Rules/Symfony/UndefinedOptionRule.php
+
+ -
+ message: '#^Although PHPStan\\Reflection\\Php\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it''s not guaranteed to always stay the same\.$#'
+ identifier: phpstanApi.instanceofAssumption
+ count: 1
+ path: src/Symfony/RequiredAutowiringExtension.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Call to function method_exists\(\) with Symfony\\Component\\Console\\Input\\InputOption and ''isNegatable'' will always evaluate to true\.$#'
+ identifier: function.alreadyNarrowedType
+ count: 1
+ path: src/Type/Symfony/GetOptionTypeHelper.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Call to internal method Symfony\\Component\\Console\\Command\\Command\:\:mergeApplicationDefinition\(\) from outside its root namespace Symfony\.$#'
+ identifier: method.internal
+ count: 1
+ path: src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php
+
+ -
+ message: '#^Accessing PHPStan\\Rules\\Methods\\CallMethodsRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#'
+ identifier: phpstanApi.classConstant
+ count: 1
+ path: tests/Rules/NonexistentInputBagClassTest.php
+
+ -
+ message: '#^Accessing PHPStan\\Rules\\Properties\\UninitializedPropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#'
+ identifier: phpstanApi.classConstant
+ count: 1
+ path: tests/Symfony/RequiredAutowiringExtensionTest.php
+
+ -
+ message: '#^Accessing PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeMethodCallRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#'
+ identifier: phpstanApi.classConstant
+ count: 1
+ path: tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php
diff --git a/phpstan.neon b/phpstan.neon
index 39998f59..f13073e1 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -5,6 +5,7 @@ includes:
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- phar://phpstan.phar/conf/bleedingEdge.neon
+ - phpstan-baseline.neon
parameters:
excludePaths:
diff --git a/phpunit.xml b/phpunit.xml
index f9f3afe2..2e2f6167 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -10,7 +10,7 @@
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
- xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
+ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml"
>
diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php
index 4c9a6af1..96f1efea 100644
--- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php
+++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php
@@ -6,7 +6,7 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Symfony\ServiceMap;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ObjectType;
@@ -19,8 +19,7 @@
final class ContainerInterfacePrivateServiceRule implements Rule
{
- /** @var ServiceMap */
- private $serviceMap;
+ private ServiceMap $serviceMap;
public function __construct(ServiceMap $symfonyServiceMap)
{
@@ -32,17 +31,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- }
-
if (!$node->name instanceof Node\Identifier) {
return [];
}
@@ -78,7 +68,11 @@ public function processNode(Node $node, Scope $scope): array
if ($serviceId !== null) {
$service = $this->serviceMap->getService($serviceId);
if ($service !== null && !$service->isPublic()) {
- return [sprintf('Service "%s" is private.', $serviceId)];
+ return [
+ RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId))
+ ->identifier('symfonyContainer.privateService')
+ ->build(),
+ ];
}
}
@@ -88,13 +82,13 @@ public function processNode(Node $node, Scope $scope): array
private function isServiceSubscriber(Type $containerType, Scope $scope): TrinaryLogic
{
$serviceSubscriberInterfaceType = new ObjectType('Symfony\Contracts\Service\ServiceSubscriberInterface');
- $isContainerServiceSubscriber = $serviceSubscriberInterfaceType->isSuperTypeOf($containerType);
+ $isContainerServiceSubscriber = $serviceSubscriberInterfaceType->isSuperTypeOf($containerType)->result;
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return $isContainerServiceSubscriber;
}
$containedClassType = new ObjectType($classReflection->getName());
- return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType));
+ return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType)->result);
}
}
diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php
index cb0ae4e6..23444b6b 100644
--- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php
+++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php
@@ -4,13 +4,14 @@
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Symfony\ServiceMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Symfony\Helper;
+use function sprintf;
/**
* @implements Rule
@@ -18,13 +19,11 @@
final class ContainerInterfaceUnknownServiceRule implements Rule
{
- /** @var ServiceMap */
- private $serviceMap;
+ private ServiceMap $serviceMap;
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- public function __construct(ServiceMap $symfonyServiceMap, Standard $printer)
+ public function __construct(ServiceMap $symfonyServiceMap, Printer $printer)
{
$this->serviceMap = $symfonyServiceMap;
$this->printer = $printer;
@@ -35,17 +34,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- }
-
if (!$node->name instanceof Node\Identifier) {
return [];
}
@@ -78,7 +68,11 @@ public function processNode(Node $node, Scope $scope): array
$service = $this->serviceMap->getService($serviceId);
$serviceIdType = $scope->getType($node->getArgs()[0]->value);
if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) {
- return [sprintf('Service "%s" is not registered in the container.', $serviceId)];
+ return [
+ RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId))
+ ->identifier('symfonyContainer.serviceNotFound')
+ ->build(),
+ ];
}
}
diff --git a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php
index c29f11ed..5435d4c9 100644
--- a/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php
+++ b/src/Rules/Symfony/InvalidArgumentDefaultValueRule.php
@@ -6,16 +6,16 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
-use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
+use function count;
use function sprintf;
/**
@@ -29,17 +29,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- };
-
if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf($scope->getType($node->var))->yes()) {
return [];
}
@@ -51,10 +42,10 @@ public function processNode(Node $node, Scope $scope): array
}
$modeType = isset($node->getArgs()[1]) ? $scope->getType($node->getArgs()[1]->value) : new NullType();
- if ($modeType instanceof NullType) {
+ if ($modeType->isNull()->yes()) {
$modeType = new ConstantIntegerType(2); // InputArgument::OPTIONAL
}
- $modeTypes = TypeUtils::getConstantScalars($modeType);
+ $modeTypes = $modeType->getConstantScalarTypes();
if (count($modeTypes) !== 1) {
return [];
}
@@ -67,12 +58,22 @@ public function processNode(Node $node, Scope $scope): array
// not an array
if (($mode & 4) !== 4 && !(new UnionType([new StringType(), new NullType()]))->isSuperTypeOf($defaultType)->yes()) {
- return [sprintf('Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))];
+ return [
+ RuleErrorBuilder::message(sprintf(
+ 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects string|null, %s given.',
+ $defaultType->describe(VerbosityLevel::typeOnly()),
+ ))->identifier('argument.type')->build(),
+ ];
}
// is array
if (($mode & 4) === 4 && !(new UnionType([new ArrayType(new IntegerType(), new StringType()), new NullType()]))->isSuperTypeOf($defaultType)->yes()) {
- return [sprintf('Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))];
+ return [
+ RuleErrorBuilder::message(sprintf(
+ 'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, %s given.',
+ $defaultType->describe(VerbosityLevel::typeOnly()),
+ ))->identifier('argument.type')->build(),
+ ];
}
return [];
diff --git a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php
index 3eb2df1b..2e3dc0e9 100644
--- a/src/Rules/Symfony/InvalidOptionDefaultValueRule.php
+++ b/src/Rules/Symfony/InvalidOptionDefaultValueRule.php
@@ -6,7 +6,7 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
@@ -15,9 +15,9 @@
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
-use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
+use function count;
use function sprintf;
/**
@@ -31,17 +31,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- };
-
if (!(new ObjectType('Symfony\Component\Console\Command\Command'))->isSuperTypeOf($scope->getType($node->var))->yes()) {
return [];
}
@@ -53,10 +44,10 @@ public function processNode(Node $node, Scope $scope): array
}
$modeType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new NullType();
- if ($modeType instanceof NullType) {
+ if ($modeType->isNull()->yes()) {
$modeType = new ConstantIntegerType(1); // InputOption::VALUE_NONE
}
- $modeTypes = TypeUtils::getConstantScalars($modeType);
+ $modeTypes = $modeType->getConstantScalarTypes();
if (count($modeTypes) !== 1) {
return [];
}
@@ -71,13 +62,24 @@ public function processNode(Node $node, Scope $scope): array
if (($mode & 8) !== 8) {
$checkType = new UnionType([new StringType(), new IntegerType(), new NullType(), new BooleanType()]);
if (!$checkType->isSuperTypeOf($defaultType)->yes()) {
- return [sprintf('Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects %s, %s given.', $checkType->describe(VerbosityLevel::typeOnly()), $defaultType->describe(VerbosityLevel::typeOnly()))];
+ return [
+ RuleErrorBuilder::message(sprintf(
+ 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects %s, %s given.',
+ $checkType->describe(VerbosityLevel::typeOnly()),
+ $defaultType->describe(VerbosityLevel::typeOnly()),
+ ))->identifier('argument.type')->build(),
+ ];
}
}
// is array
if (($mode & 8) === 8 && !(new UnionType([new ArrayType(new MixedType(), new StringType()), new NullType()]))->isSuperTypeOf($defaultType)->yes()) {
- return [sprintf('Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, %s given.', $defaultType->describe(VerbosityLevel::typeOnly()))];
+ return [
+ RuleErrorBuilder::message(sprintf(
+ 'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, %s given.',
+ $defaultType->describe(VerbosityLevel::typeOnly()),
+ ))->identifier('argument.type')->build(),
+ ];
}
return [];
diff --git a/src/Rules/Symfony/UndefinedArgumentRule.php b/src/Rules/Symfony/UndefinedArgumentRule.php
index 59f71ed6..ee36a23c 100644
--- a/src/Rules/Symfony/UndefinedArgumentRule.php
+++ b/src/Rules/Symfony/UndefinedArgumentRule.php
@@ -5,14 +5,13 @@
use InvalidArgumentException;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Symfony\Helper;
-use PHPStan\Type\TypeUtils;
use function count;
use function sprintf;
@@ -22,13 +21,11 @@
final class UndefinedArgumentRule implements Rule
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Standard $printer)
+ public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer)
{
$this->consoleApplicationResolver = $consoleApplicationResolver;
$this->printer = $printer;
@@ -39,17 +36,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- };
-
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return [];
@@ -69,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
}
$argType = $scope->getType($node->getArgs()[0]->value);
- $argStrings = TypeUtils::getConstantStrings($argType);
+ $argStrings = $argType->getConstantStrings();
if (count($argStrings) !== 1) {
return [];
}
@@ -84,7 +72,9 @@ public function processNode(Node $node, Scope $scope): array
if ($scope->getType(Helper::createMarkerNode($node->var, $argType, $this->printer))->equals($argType)) {
continue;
}
- $errors[] = sprintf('Command "%s" does not define argument "%s".', $name, $argName);
+ $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define argument "%s".', $name, $argName))
+ ->identifier('symfonyConsole.argumentNotFound')
+ ->build();
}
}
diff --git a/src/Rules/Symfony/UndefinedOptionRule.php b/src/Rules/Symfony/UndefinedOptionRule.php
index 29e08819..39a6a4ac 100644
--- a/src/Rules/Symfony/UndefinedOptionRule.php
+++ b/src/Rules/Symfony/UndefinedOptionRule.php
@@ -5,14 +5,13 @@
use InvalidArgumentException;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
-use PHPStan\ShouldNotHappenException;
+use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Symfony\Helper;
-use PHPStan\Type\TypeUtils;
use function count;
use function sprintf;
@@ -22,13 +21,11 @@
final class UndefinedOptionRule implements Rule
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Standard $printer)
+ public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, Printer $printer)
{
$this->consoleApplicationResolver = $consoleApplicationResolver;
$this->printer = $printer;
@@ -39,17 +36,8 @@ public function getNodeType(): string
return MethodCall::class;
}
- /**
- * @param \PhpParser\Node $node
- * @param \PHPStan\Analyser\Scope $scope
- * @return (string|\PHPStan\Rules\RuleError)[] errors
- */
public function processNode(Node $node, Scope $scope): array
{
- if (!$node instanceof MethodCall) {
- throw new ShouldNotHappenException();
- };
-
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return [];
@@ -69,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
}
$optType = $scope->getType($node->getArgs()[0]->value);
- $optStrings = TypeUtils::getConstantStrings($optType);
+ $optStrings = $optType->getConstantStrings();
if (count($optStrings) !== 1) {
return [];
}
@@ -84,7 +72,9 @@ public function processNode(Node $node, Scope $scope): array
if ($scope->getType(Helper::createMarkerNode($node->var, $optType, $this->printer))->equals($optType)) {
continue;
}
- $errors[] = sprintf('Command "%s" does not define option "%s".', $name, $optName);
+ $errors[] = RuleErrorBuilder::message(sprintf('Command "%s" does not define option "%s".', $name, $optName))
+ ->identifier('symfonyConsole.optionNotFound')
+ ->build();
}
}
diff --git a/src/Symfony/ConsoleApplicationResolver.php b/src/Symfony/ConsoleApplicationResolver.php
index a493cbf0..13b24d26 100644
--- a/src/Symfony/ConsoleApplicationResolver.php
+++ b/src/Symfony/ConsoleApplicationResolver.php
@@ -5,46 +5,57 @@
use PHPStan\Reflection\ClassReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ObjectType;
+use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use function file_exists;
use function get_class;
use function is_readable;
+use function method_exists;
+use function sprintf;
final class ConsoleApplicationResolver
{
- /** @var \Symfony\Component\Console\Application|null */
- private $consoleApplication;
+ private ?string $consoleApplicationLoader = null;
+
+ private ?Application $consoleApplication = null;
public function __construct(?string $consoleApplicationLoader)
{
- if ($consoleApplicationLoader === null) {
- return;
- }
- $this->consoleApplication = $this->loadConsoleApplication($consoleApplicationLoader);
+ $this->consoleApplicationLoader = $consoleApplicationLoader;
}
- /**
- * @return \Symfony\Component\Console\Application|null
- * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint
- */
- private function loadConsoleApplication(string $consoleApplicationLoader)
+ public function hasConsoleApplicationLoader(): bool
{
- if (!file_exists($consoleApplicationLoader)
- || !is_readable($consoleApplicationLoader)
+ return $this->consoleApplicationLoader !== null;
+ }
+
+ private function getConsoleApplication(): ?Application
+ {
+ if ($this->consoleApplicationLoader === null) {
+ return null;
+ }
+
+ if ($this->consoleApplication !== null) {
+ return $this->consoleApplication;
+ }
+
+ if (!file_exists($this->consoleApplicationLoader)
+ || !is_readable($this->consoleApplicationLoader)
) {
- throw new ShouldNotHappenException();
+ throw new ShouldNotHappenException(sprintf('Cannot load console application. Check the parameters.symfony.consoleApplicationLoader setting in PHPStan\'s config. The offending value is "%s".', $this->consoleApplicationLoader));
}
- return require $consoleApplicationLoader;
+ return $this->consoleApplication = require $this->consoleApplicationLoader;
}
/**
- * @return \Symfony\Component\Console\Command\Command[]
+ * @return Command[]
*/
public function findCommands(ClassReflection $classReflection): array
{
- if ($this->consoleApplication === null) {
+ $consoleApplication = $this->getConsoleApplication();
+ if ($consoleApplication === null) {
return [];
}
@@ -54,7 +65,7 @@ public function findCommands(ClassReflection $classReflection): array
}
$commands = [];
- foreach ($this->consoleApplication->all() as $name => $command) {
+ foreach ($consoleApplication->all() as $name => $command) {
$commandClass = new ObjectType(get_class($command));
$isLazyCommand = (new ObjectType('Symfony\Component\Console\Command\LazyCommand'))->isSuperTypeOf($commandClass)->yes();
diff --git a/src/Symfony/DefaultParameterMap.php b/src/Symfony/DefaultParameterMap.php
index a58ed9fc..3149fd7d 100644
--- a/src/Symfony/DefaultParameterMap.php
+++ b/src/Symfony/DefaultParameterMap.php
@@ -4,17 +4,17 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
-use PHPStan\Type\TypeUtils;
-use function count;
+use PHPStan\Type\Type;
+use function array_map;
final class DefaultParameterMap implements ParameterMap
{
- /** @var \PHPStan\Symfony\ParameterDefinition[] */
- private $parameters;
+ /** @var ParameterDefinition[] */
+ private array $parameters;
/**
- * @param \PHPStan\Symfony\ParameterDefinition[] $parameters
+ * @param ParameterDefinition[] $parameters
*/
public function __construct(array $parameters)
{
@@ -22,7 +22,7 @@ public function __construct(array $parameters)
}
/**
- * @return \PHPStan\Symfony\ParameterDefinition[]
+ * @return ParameterDefinition[]
*/
public function getParameters(): array
{
@@ -34,10 +34,11 @@ public function getParameter(string $key): ?ParameterDefinition
return $this->parameters[$key] ?? null;
}
- public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string
+ public static function getParameterKeysFromNode(Expr $node, Scope $scope): array
{
- $strings = TypeUtils::getConstantStrings($scope->getType($node));
- return count($strings) === 1 ? $strings[0]->getValue() : null;
+ $strings = $scope->getType($node)->getConstantStrings();
+
+ return array_map(static fn (Type $type) => $type->getValue(), $strings);
}
}
diff --git a/src/Symfony/DefaultServiceMap.php b/src/Symfony/DefaultServiceMap.php
index 6339b450..5d3bccd0 100644
--- a/src/Symfony/DefaultServiceMap.php
+++ b/src/Symfony/DefaultServiceMap.php
@@ -4,17 +4,16 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
-use PHPStan\Type\TypeUtils;
use function count;
final class DefaultServiceMap implements ServiceMap
{
- /** @var \PHPStan\Symfony\ServiceDefinition[] */
- private $services;
+ /** @var ServiceDefinition[] */
+ private array $services;
/**
- * @param \PHPStan\Symfony\ServiceDefinition[] $services
+ * @param ServiceDefinition[] $services
*/
public function __construct(array $services)
{
@@ -22,7 +21,7 @@ public function __construct(array $services)
}
/**
- * @return \PHPStan\Symfony\ServiceDefinition[]
+ * @return ServiceDefinition[]
*/
public function getServices(): array
{
@@ -36,7 +35,7 @@ public function getService(string $id): ?ServiceDefinition
public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string
{
- $strings = TypeUtils::getConstantStrings($scope->getType($node));
+ $strings = $scope->getType($node)->getConstantStrings();
return count($strings) === 1 ? $strings[0]->getValue() : null;
}
diff --git a/src/Symfony/FakeParameterMap.php b/src/Symfony/FakeParameterMap.php
index 7e723411..53acdc3c 100644
--- a/src/Symfony/FakeParameterMap.php
+++ b/src/Symfony/FakeParameterMap.php
@@ -9,7 +9,7 @@ final class FakeParameterMap implements ParameterMap
{
/**
- * @return \PHPStan\Symfony\ParameterDefinition[]
+ * @return ParameterDefinition[]
*/
public function getParameters(): array
{
@@ -21,9 +21,9 @@ public function getParameter(string $key): ?ParameterDefinition
return null;
}
- public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string
+ public static function getParameterKeysFromNode(Expr $node, Scope $scope): array
{
- return null;
+ return [];
}
}
diff --git a/src/Symfony/FakeServiceMap.php b/src/Symfony/FakeServiceMap.php
index d05fe8ed..88f9edac 100644
--- a/src/Symfony/FakeServiceMap.php
+++ b/src/Symfony/FakeServiceMap.php
@@ -9,7 +9,7 @@ final class FakeServiceMap implements ServiceMap
{
/**
- * @return \PHPStan\Symfony\ServiceDefinition[]
+ * @return ServiceDefinition[]
*/
public function getServices(): array
{
diff --git a/src/Symfony/InputBagStubFilesExtension.php b/src/Symfony/InputBagStubFilesExtension.php
index 94fd2104..6ce36f4b 100644
--- a/src/Symfony/InputBagStubFilesExtension.php
+++ b/src/Symfony/InputBagStubFilesExtension.php
@@ -2,14 +2,27 @@
namespace PHPStan\Symfony;
+use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
+use PHPStan\BetterReflection\Reflector\Reflector;
use PHPStan\PhpDoc\StubFilesExtension;
class InputBagStubFilesExtension implements StubFilesExtension
{
+ private Reflector $reflector;
+
+ public function __construct(
+ Reflector $reflector
+ )
+ {
+ $this->reflector = $reflector;
+ }
+
public function getFiles(): array
{
- if (!class_exists('Symfony\Component\HttpFoundation\InputBag')) {
+ try {
+ $this->reflector->reflectClass('Symfony\Component\HttpFoundation\InputBag');
+ } catch (IdentifierNotFound $e) {
return [];
}
diff --git a/src/Symfony/MessageMap.php b/src/Symfony/MessageMap.php
new file mode 100644
index 00000000..97bb8734
--- /dev/null
+++ b/src/Symfony/MessageMap.php
@@ -0,0 +1,24 @@
+ */
+ private array $messageMap;
+
+ /** @param array $messageMap */
+ public function __construct(array $messageMap)
+ {
+ $this->messageMap = $messageMap;
+ }
+
+ public function getTypeForClass(string $class): ?Type
+ {
+ return $this->messageMap[$class] ?? null;
+ }
+
+}
diff --git a/src/Symfony/MessageMapFactory.php b/src/Symfony/MessageMapFactory.php
new file mode 100644
index 00000000..3d7663ca
--- /dev/null
+++ b/src/Symfony/MessageMapFactory.php
@@ -0,0 +1,152 @@
+serviceMap = $symfonyServiceMap;
+ $this->reflectionProvider = $reflectionProvider;
+ }
+
+ public function create(): MessageMap
+ {
+ $returnTypesMap = [];
+
+ foreach ($this->serviceMap->getServices() as $service) {
+ $serviceClass = $service->getClass();
+
+ if ($serviceClass === null) {
+ continue;
+ }
+
+ foreach ($service->getTags() as $tag) {
+ if ($tag->getName() !== self::MESSENGER_HANDLER_TAG) {
+ continue;
+ }
+
+ if (!$this->reflectionProvider->hasClass($serviceClass)) {
+ continue;
+ }
+
+ $reflectionClass = $this->reflectionProvider->getClass($serviceClass);
+
+ /** @var array{handles?: class-string, method?: string} $tagAttributes */
+ $tagAttributes = $tag->getAttributes();
+
+ if (isset($tagAttributes['handles'])) {
+ $handles = [$tagAttributes['handles'] => ['method' => $tagAttributes['method'] ?? self::DEFAULT_HANDLER_METHOD]];
+ } else {
+ $handles = $this->guessHandledMessages($reflectionClass);
+ }
+
+ foreach ($handles as $messageClassName => $options) {
+ $methodName = $options['method'] ?? self::DEFAULT_HANDLER_METHOD;
+
+ if (!$reflectionClass->hasNativeMethod($methodName)) {
+ continue;
+ }
+
+ $methodReflection = $reflectionClass->getNativeMethod($methodName);
+
+ foreach ($methodReflection->getVariants() as $variant) {
+ $returnTypesMap[$messageClassName][] = $variant->getReturnType();
+ }
+ }
+ }
+ }
+
+ $messageMap = [];
+ foreach ($returnTypesMap as $messageClassName => $returnTypes) {
+ if (count($returnTypes) !== 1) {
+ continue;
+ }
+
+ $messageMap[$messageClassName] = $returnTypes[0];
+ }
+
+ return new MessageMap($messageMap);
+ }
+
+ /** @return iterable> */
+ private function guessHandledMessages(ClassReflection $reflectionClass): iterable
+ {
+ if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) {
+ $className = $reflectionClass->getName();
+
+ foreach ($className::getHandledMessages() as $index => $value) {
+ $containOptions = self::containOptions($index, $value);
+ if ($containOptions === true) {
+ yield $index => $value;
+ } elseif ($containOptions === false) {
+ yield $value => ['method' => self::DEFAULT_HANDLER_METHOD];
+ }
+ }
+
+ return;
+ }
+
+ if (!$reflectionClass->hasNativeMethod(self::DEFAULT_HANDLER_METHOD)) {
+ return;
+ }
+
+ $methodReflection = $reflectionClass->getNativeMethod(self::DEFAULT_HANDLER_METHOD);
+
+ $variants = $methodReflection->getVariants();
+ if (count($variants) !== 1) {
+ return;
+ }
+
+ $parameters = $variants[0]->getParameters();
+
+ if (count($parameters) !== 1) {
+ return;
+ }
+
+ $classNames = $parameters[0]->getType()->getObjectClassNames();
+
+ if (count($classNames) !== 1) {
+ return;
+ }
+
+ yield $classNames[0] => ['method' => self::DEFAULT_HANDLER_METHOD];
+ }
+
+ /**
+ * @param mixed $index
+ * @param mixed $value
+ * @phpstan-assert-if-true =class-string $index
+ * @phpstan-assert-if-true =array $value
+ * @phpstan-assert-if-false =int $index
+ * @phpstan-assert-if-false =class-string $value
+ */
+ private static function containOptions($index, $value): ?bool
+ {
+ if (is_string($index) && class_exists($index) && is_array($value)) {
+ return true;
+ } elseif (is_int($index) && is_string($value) && class_exists($value)) {
+ return false;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/Symfony/Parameter.php b/src/Symfony/Parameter.php
index 1df679bc..53b53265 100644
--- a/src/Symfony/Parameter.php
+++ b/src/Symfony/Parameter.php
@@ -5,14 +5,12 @@
final class Parameter implements ParameterDefinition
{
- /** @var string */
- private $key;
+ private string $key;
/** @var array|bool|float|int|string */
private $value;
/**
- * @param string $key
* @param array|bool|float|int|string $value
*/
public function __construct(
diff --git a/src/Symfony/ParameterDefinition.php b/src/Symfony/ParameterDefinition.php
index e1aa2eaa..1da7723b 100644
--- a/src/Symfony/ParameterDefinition.php
+++ b/src/Symfony/ParameterDefinition.php
@@ -2,6 +2,9 @@
namespace PHPStan\Symfony;
+/**
+ * @api
+ */
interface ParameterDefinition
{
diff --git a/src/Symfony/ParameterMap.php b/src/Symfony/ParameterMap.php
index fc14fb2c..0c551635 100644
--- a/src/Symfony/ParameterMap.php
+++ b/src/Symfony/ParameterMap.php
@@ -5,16 +5,22 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
+/**
+ * @api
+ */
interface ParameterMap
{
/**
- * @return \PHPStan\Symfony\ParameterDefinition[]
+ * @return ParameterDefinition[]
*/
public function getParameters(): array;
public function getParameter(string $key): ?ParameterDefinition;
- public static function getParameterKeyFromNode(Expr $node, Scope $scope): ?string;
+ /**
+ * @return array
+ */
+ public static function getParameterKeysFromNode(Expr $node, Scope $scope): array;
}
diff --git a/src/Symfony/RequiredAutowiringExtension.php b/src/Symfony/RequiredAutowiringExtension.php
new file mode 100644
index 00000000..7d8d195d
--- /dev/null
+++ b/src/Symfony/RequiredAutowiringExtension.php
@@ -0,0 +1,90 @@
+fileTypeMapper = $fileTypeMapper;
+ }
+
+ public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool
+ {
+ return false;
+ }
+
+ public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool
+ {
+ return false;
+ }
+
+ public function isInitialized(PropertyReflection $property, string $propertyName): bool
+ {
+ // If the property is public, check for @required on the property itself
+ if (!$property->isPublic()) {
+ return false;
+ }
+
+ if ($property->getDocComment() !== null && $this->isRequiredFromDocComment($property->getDocComment())) {
+ return true;
+ }
+
+ // Check for the attribute version
+ if ($property instanceof PhpPropertyReflection && count($property->getNativeReflection()->getAttributes('Symfony\Contracts\Service\Attribute\Required')) > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getAdditionalConstructors(ClassReflection $classReflection): array
+ {
+ $additionalConstructors = [];
+ $nativeReflection = $classReflection->getNativeReflection();
+
+ foreach ($nativeReflection->getMethods() as $method) {
+ if (!$method->isPublic()) {
+ continue;
+ }
+
+ if ($method->getDocComment() !== false && $this->isRequiredFromDocComment($method->getDocComment())) {
+ $additionalConstructors[] = $method->getName();
+ }
+
+ if (count($method->getAttributes('Symfony\Contracts\Service\Attribute\Required')) === 0) {
+ continue;
+ }
+
+ $additionalConstructors[] = $method->getName();
+ }
+
+ return $additionalConstructors;
+ }
+
+ private function isRequiredFromDocComment(string $docComment): bool
+ {
+ $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc(null, null, null, null, $docComment);
+
+ foreach ($phpDoc->getPhpDocNodes() as $node) {
+ // @required tag is available, meaning this property is always initialized
+ if (count($node->getTagsByName('@required')) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/Symfony/Service.php b/src/Symfony/Service.php
index c31324f5..44c0d1d7 100644
--- a/src/Symfony/Service.php
+++ b/src/Symfony/Service.php
@@ -5,27 +5,27 @@
final class Service implements ServiceDefinition
{
- /** @var string */
- private $id;
+ private string $id;
- /** @var string|null */
- private $class;
+ private ?string $class = null;
- /** @var bool */
- private $public;
+ private bool $public;
- /** @var bool */
- private $synthetic;
+ private bool $synthetic;
- /** @var string|null */
- private $alias;
+ private ?string $alias = null;
+ /** @var ServiceTag[] */
+ private array $tags;
+
+ /** @param ServiceTag[] $tags */
public function __construct(
string $id,
?string $class,
bool $public,
bool $synthetic,
- ?string $alias
+ ?string $alias,
+ array $tags = []
)
{
$this->id = $id;
@@ -33,6 +33,7 @@ public function __construct(
$this->public = $public;
$this->synthetic = $synthetic;
$this->alias = $alias;
+ $this->tags = $tags;
}
public function getId(): string
@@ -60,4 +61,9 @@ public function getAlias(): ?string
return $this->alias;
}
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
}
diff --git a/src/Symfony/ServiceDefinition.php b/src/Symfony/ServiceDefinition.php
index c7cdcd18..3862fa8d 100644
--- a/src/Symfony/ServiceDefinition.php
+++ b/src/Symfony/ServiceDefinition.php
@@ -2,6 +2,9 @@
namespace PHPStan\Symfony;
+/**
+ * @api
+ */
interface ServiceDefinition
{
@@ -15,4 +18,7 @@ public function isSynthetic(): bool;
public function getAlias(): ?string;
+ /** @return ServiceTag[] */
+ public function getTags(): array;
+
}
diff --git a/src/Symfony/ServiceMap.php b/src/Symfony/ServiceMap.php
index b954dc72..bbd2d8a3 100644
--- a/src/Symfony/ServiceMap.php
+++ b/src/Symfony/ServiceMap.php
@@ -5,11 +5,14 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
+/**
+ * @api
+ */
interface ServiceMap
{
/**
- * @return \PHPStan\Symfony\ServiceDefinition[]
+ * @return ServiceDefinition[]
*/
public function getServices(): array;
diff --git a/src/Symfony/ServiceTag.php b/src/Symfony/ServiceTag.php
new file mode 100644
index 00000000..3b22ee34
--- /dev/null
+++ b/src/Symfony/ServiceTag.php
@@ -0,0 +1,30 @@
+ */
+ private array $attributes;
+
+ /** @param array $attributes */
+ public function __construct(string $name, array $attributes = [])
+ {
+ $this->name = $name;
+ $this->attributes = $attributes;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+}
diff --git a/src/Symfony/ServiceTagDefinition.php b/src/Symfony/ServiceTagDefinition.php
new file mode 100644
index 00000000..b0f66d9c
--- /dev/null
+++ b/src/Symfony/ServiceTagDefinition.php
@@ -0,0 +1,13 @@
+ */
+ public function getAttributes(): array;
+
+}
diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php
new file mode 100644
index 00000000..8e2f8028
--- /dev/null
+++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php
@@ -0,0 +1,62 @@
+parameterMap = $parameterMap;
+ $this->serviceMap = $serviceMap;
+ }
+
+ public function getKey(): string
+ {
+ return 'symfonyDiContainer';
+ }
+
+ public function getHash(): string
+ {
+ $services = $parameters = [];
+
+ foreach ($this->parameterMap->getParameters() as $parameter) {
+ $parameters[$parameter->getKey()] = $parameter->getValue();
+ }
+ ksort($parameters);
+
+ foreach ($this->serviceMap->getServices() as $service) {
+ $serviceTags = array_map(
+ static fn (ServiceTag $tag) => [
+ 'name' => $tag->getName(),
+ 'attributes' => $tag->getAttributes(),
+ ],
+ $service->getTags(),
+ );
+ sort($serviceTags);
+
+ $services[$service->getId()] = [
+ 'class' => $service->getClass(),
+ 'public' => $service->isPublic() ? 'yes' : 'no',
+ 'synthetic' => $service->isSynthetic() ? 'yes' : 'no',
+ 'alias' => $service->getAlias(),
+ 'tags' => $serviceTags,
+ ];
+ }
+ ksort($services);
+
+ return hash('sha256', var_export(['parameters' => $parameters, 'services' => $services], true));
+ }
+
+}
diff --git a/src/Symfony/SymfonyDiagnoseExtension.php b/src/Symfony/SymfonyDiagnoseExtension.php
new file mode 100644
index 00000000..38b19754
--- /dev/null
+++ b/src/Symfony/SymfonyDiagnoseExtension.php
@@ -0,0 +1,28 @@
+consoleApplicationResolver = $consoleApplicationResolver;
+ }
+
+ public function print(Output $output): void
+ {
+ $output->writeLineFormatted(sprintf(
+ 'Symfony\'s consoleApplicationLoader: %s',
+ $this->consoleApplicationResolver->hasConsoleApplicationLoader() ? 'In use' : 'No',
+ ));
+ $output->writeLineFormatted('');
+ }
+
+}
diff --git a/src/Symfony/XmlContainerNotExistsException.php b/src/Symfony/XmlContainerNotExistsException.php
index 7238cc6e..59b600de 100644
--- a/src/Symfony/XmlContainerNotExistsException.php
+++ b/src/Symfony/XmlContainerNotExistsException.php
@@ -2,7 +2,9 @@
namespace PHPStan\Symfony;
-final class XmlContainerNotExistsException extends \InvalidArgumentException
+use InvalidArgumentException;
+
+final class XmlContainerNotExistsException extends InvalidArgumentException
{
}
diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php
index 6a7575f4..4d3d3578 100644
--- a/src/Symfony/XmlParameterMapFactory.php
+++ b/src/Symfony/XmlParameterMapFactory.php
@@ -2,17 +2,26 @@
namespace PHPStan\Symfony;
+use InvalidArgumentException;
+use PHPStan\ShouldNotHappenException;
+use SimpleXMLElement;
+use function base64_decode;
+use function count;
+use function file_get_contents;
+use function is_numeric;
+use function ksort;
+use function simplexml_load_string;
use function sprintf;
+use function strpos;
final class XmlParameterMapFactory implements ParameterMapFactory
{
- /** @var string|null */
- private $containerXml;
+ private ?string $containerXml = null;
- public function __construct(?string $containerXml)
+ public function __construct(?string $containerXmlPath)
{
- $this->containerXml = $containerXml;
+ $this->containerXml = $containerXmlPath;
}
public function create(): ParameterMap
@@ -31,37 +40,46 @@ public function create(): ParameterMap
throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml));
}
- /** @var \PHPStan\Symfony\Parameter[] $parameters */
+ /** @var Parameter[] $parameters */
$parameters = [];
- foreach ($xml->parameters->parameter as $def) {
- /** @var \SimpleXMLElement $attrs */
- $attrs = $def->attributes();
- $parameter = new Parameter(
- (string) $attrs->key,
- $this->getNodeValue($def)
- );
+ if (count($xml->parameters) > 0) {
+ foreach ($xml->parameters->parameter as $def) {
+ /** @var SimpleXMLElement $attrs */
+ $attrs = $def->attributes();
- $parameters[$parameter->getKey()] = $parameter;
+ $parameter = new Parameter(
+ (string) $attrs->key,
+ $this->getNodeValue($def),
+ );
+
+ $parameters[$parameter->getKey()] = $parameter;
+ }
}
+ ksort($parameters);
+
return new DefaultParameterMap($parameters);
}
/**
* @return array|bool|float|int|string
*/
- private function getNodeValue(\SimpleXMLElement $def)
+ private function getNodeValue(SimpleXMLElement $def)
{
- /** @var \SimpleXMLElement $attrs */
+ /** @var SimpleXMLElement $attrs */
$attrs = $def->attributes();
$value = null;
switch ((string) $attrs->type) {
case 'collection':
$value = [];
- foreach ($def->children() as $child) {
- /** @var \SimpleXMLElement $childAttrs */
+ $children = $def->children();
+ if ($children === null) {
+ throw new ShouldNotHappenException();
+ }
+ foreach ($children as $child) {
+ /** @var SimpleXMLElement $childAttrs */
$childAttrs = $child->attributes();
if (isset($childAttrs->key)) {
@@ -79,7 +97,7 @@ private function getNodeValue(\SimpleXMLElement $def)
case 'binary':
$value = base64_decode((string) $def, true);
if ($value === false) {
- throw new \InvalidArgumentException(sprintf('Parameter "%s" of binary type is not valid base64 encoded string.', (string) $attrs->key));
+ throw new InvalidArgumentException(sprintf('Parameter "%s" of binary type is not valid base64 encoded string.', (string) $attrs->key));
}
break;
diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php
index 409ef847..ac79cb30 100644
--- a/src/Symfony/XmlServiceMapFactory.php
+++ b/src/Symfony/XmlServiceMapFactory.php
@@ -2,6 +2,10 @@
namespace PHPStan\Symfony;
+use SimpleXMLElement;
+use function count;
+use function file_get_contents;
+use function ksort;
use function simplexml_load_string;
use function sprintf;
use function strpos;
@@ -10,12 +14,11 @@
final class XmlServiceMapFactory implements ServiceMapFactory
{
- /** @var string|null */
- private $containerXml;
+ private ?string $containerXml = null;
- public function __construct(?string $containerXml)
+ public function __construct(?string $containerXmlPath)
{
- $this->containerXml = $containerXml;
+ $this->containerXml = $containerXmlPath;
}
public function create(): ServiceMap
@@ -34,29 +37,42 @@ public function create(): ServiceMap
throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml));
}
- /** @var \PHPStan\Symfony\Service[] $services */
+ /** @var Service[] $services */
$services = [];
- /** @var \PHPStan\Symfony\Service[] $aliases */
+ /** @var Service[] $aliases */
$aliases = [];
- foreach ($xml->services->service as $def) {
- /** @var \SimpleXMLElement $attrs */
- $attrs = $def->attributes();
- if (!isset($attrs->id)) {
- continue;
- }
- $service = new Service(
- strpos((string) $attrs->id, '.') === 0 ? substr((string) $attrs->id, 1) : (string) $attrs->id,
- isset($attrs->class) ? (string) $attrs->class : null,
- isset($attrs->public) && (string) $attrs->public === 'true',
- isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
- isset($attrs->alias) ? (string) $attrs->alias : null
- );
+ if (count($xml->services) > 0) {
+ foreach ($xml->services->service as $def) {
+ /** @var SimpleXMLElement $attrs */
+ $attrs = $def->attributes();
+ if (!isset($attrs->id)) {
+ continue;
+ }
+
+ $serviceTags = [];
+ foreach ($def->tag as $tag) {
+ $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? [];
+ $tagName = $tagAttrs['name'];
+ unset($tagAttrs['name']);
+
+ $serviceTags[] = new ServiceTag($tagName, $tagAttrs);
+ }
- if ($service->getAlias() !== null) {
- $aliases[] = $service;
- } else {
- $services[$service->getId()] = $service;
+ $service = new Service(
+ $this->cleanServiceId((string) $attrs->id),
+ isset($attrs->class) ? (string) $attrs->class : null,
+ isset($attrs->public) && (string) $attrs->public === 'true',
+ isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
+ isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null,
+ $serviceTags,
+ );
+
+ if ($service->getAlias() !== null) {
+ $aliases[] = $service;
+ } else {
+ $services[$service->getId()] = $service;
+ }
}
}
foreach ($aliases as $service) {
@@ -70,11 +86,18 @@ public function create(): ServiceMap
$services[$alias]->getClass(),
$service->isPublic(),
$service->isSynthetic(),
- $alias
+ $alias,
);
}
+ ksort($services);
+
return new DefaultServiceMap($services);
}
+ private function cleanServiceId(string $id): string
+ {
+ return strpos($id, '.') === 0 ? substr($id, 1) : $id;
+ }
+
}
diff --git a/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php b/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php
index 74b6981f..5c21f021 100644
--- a/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php
+++ b/src/Type/Symfony/ArgumentTypeSpecifyingExtension.php
@@ -3,25 +3,23 @@
namespace PHPStan\Type\Symfony;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\MethodTypeSpecifyingExtension;
final class ArgumentTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- /** @var \PHPStan\Analyser\TypeSpecifier */
- private $typeSpecifier;
+ private TypeSpecifier $typeSpecifier;
- public function __construct(Standard $printer)
+ public function __construct(Printer $printer)
{
$this->printer = $printer;
}
@@ -45,7 +43,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
return $this->typeSpecifier->create(
Helper::createMarkerNode($node->var, $argType, $this->printer),
$argType,
- $context
+ $context,
+ $scope,
);
}
diff --git a/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php
new file mode 100644
index 00000000..0862ce61
--- /dev/null
+++ b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php
@@ -0,0 +1,48 @@
+getName() === 'get';
+ }
+
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
+ {
+ if (!isset($methodCall->getArgs()[1])) {
+ return null;
+ }
+
+ $callbackReturnType = $scope->getType($methodCall->getArgs()[1]->value);
+ if ($callbackReturnType->isCallable()->yes()) {
+ $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
+ $scope,
+ $methodCall->getArgs(),
+ $callbackReturnType->getCallableParametersAcceptors($scope),
+ );
+ $returnType = $parametersAcceptor->getReturnType();
+
+ // generalize template parameters
+ return $returnType->generalize(GeneralizePrecision::templateArgument());
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php
new file mode 100644
index 00000000..fba70cfd
--- /dev/null
+++ b/src/Type/Symfony/CommandGetHelperDynamicReturnTypeExtension.php
@@ -0,0 +1,67 @@
+consoleApplicationResolver = $consoleApplicationResolver;
+ }
+
+ public function getClass(): string
+ {
+ return 'Symfony\Component\Console\Command\Command';
+ }
+
+ public function isMethodSupported(MethodReflection $methodReflection): bool
+ {
+ return $methodReflection->getName() === 'getHelper';
+ }
+
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
+ {
+ if (!isset($methodCall->getArgs()[0])) {
+ return null;
+ }
+
+ $classReflection = $scope->getClassReflection();
+ if ($classReflection === null) {
+ return null;
+ }
+
+ $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
+ if (count($argStrings) !== 1) {
+ return null;
+ }
+ $argName = $argStrings[0]->getValue();
+
+ $returnTypes = [];
+ foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
+ try {
+ $command->mergeApplicationDefinition();
+ $returnTypes[] = new ObjectType(get_class($command->getHelper($argName)));
+ } catch (Throwable $e) {
+ // no-op
+ }
+ }
+
+ return count($returnTypes) > 0 ? TypeCombinator::union(...$returnTypes) : null;
+ }
+
+}
diff --git a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php
index cd1d4e51..1dae22e2 100644
--- a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php
@@ -9,8 +9,9 @@
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType;
use PHPStan\Type\Type;
-use PHPStan\Type\TypeUtils;
use PHPStan\Type\VerbosityLevel;
+use function count;
+use function in_array;
final class ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
@@ -53,14 +54,18 @@ public function getTypeFromMethodCall(
{
$calledOnType = $scope->getType($methodCall->var);
- $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ $defaultType = ParametersAcceptorSelector::selectFromArgs(
+ $scope,
+ $methodCall->getArgs(),
+ $methodReflection->getVariants(),
+ )->getReturnType();
if ($methodReflection->getName() === 'prototype') {
if (!isset($methodCall->getArgs()[0])) {
return $defaultType;
}
- $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value));
+ $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) {
$type = $argStrings[0]->getValue();
@@ -70,7 +75,7 @@ public function getTypeFromMethodCall(
return new ParentObjectType(
$defaultType->describe(VerbosityLevel::typeOnly()),
- $calledOnType
+ $calledOnType,
);
}
diff --git a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php
index 5d19b60f..800d9dbc 100644
--- a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php
@@ -10,18 +10,19 @@
use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
+use function in_array;
final class PassParentObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var string */
- private $className;
+ /** @var class-string */
+ private string $className;
/** @var string[] */
- private $methods;
+ private array $methods;
/**
- * @param string $className
+ * @param class-string $className
* @param string[] $methods
*/
public function __construct(string $className, array $methods)
@@ -48,7 +49,11 @@ public function getTypeFromMethodCall(
{
$calledOnType = $scope->getType($methodCall->var);
- $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ $defaultType = ParametersAcceptorSelector::selectFromArgs(
+ $scope,
+ $methodCall->getArgs(),
+ $methodReflection->getVariants(),
+ )->getReturnType();
return new ParentObjectType($defaultType->describe(VerbosityLevel::typeOnly()), $calledOnType);
}
diff --git a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php
index 8dd47a90..034d5d80 100644
--- a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php
@@ -5,22 +5,22 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType;
use PHPStan\Type\Type;
+use function in_array;
final class ReturnParentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var string */
- private $className;
+ /** @var class-string */
+ private string $className;
/** @var string[] */
- private $methods;
+ private array $methods;
/**
- * @param string $className
+ * @param class-string $className
* @param string[] $methods
*/
public function __construct(string $className, array $methods)
@@ -43,14 +43,14 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
$calledOnType = $scope->getType($methodCall->var);
if ($calledOnType instanceof ParentObjectType) {
return $calledOnType->getParent();
}
- return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ return null;
}
}
diff --git a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php
index a5f4f724..4f266c50 100644
--- a/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php
@@ -6,10 +6,11 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
+use PHPStan\ShouldNotHappenException;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Symfony\Config\ValueObject\TreeBuilderType;
use PHPStan\Type\Type;
-use PHPStan\Type\TypeUtils;
+use function count;
final class TreeBuilderDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
@@ -37,7 +38,7 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
{
if (!$methodCall->class instanceof Name) {
- throw new \PHPStan\ShouldNotHappenException();
+ throw new ShouldNotHappenException();
}
$className = $scope->resolveName($methodCall->class);
@@ -45,7 +46,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
$type = 'array';
if (isset($methodCall->getArgs()[1])) {
- $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[1]->value));
+ $argStrings = $scope->getType($methodCall->getArgs()[1]->value)->getConstantStrings();
if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) {
$type = $argStrings[0]->getValue();
}
diff --git a/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php
index 4b7c1b3a..2be2b574 100644
--- a/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/Config/TreeBuilderGetRootNodeDynamicReturnTypeExtension.php
@@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Symfony\Config\ValueObject\ParentObjectType;
use PHPStan\Type\Symfony\Config\ValueObject\TreeBuilderType;
@@ -28,20 +27,17 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
$calledOnType = $scope->getType($methodCall->var);
-
- $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
-
if ($calledOnType instanceof TreeBuilderType) {
return new ParentObjectType(
$calledOnType->getRootNodeClassName(),
- $calledOnType
+ $calledOnType,
);
}
- return $defaultType;
+ return null;
}
}
diff --git a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php
index 56aec24f..19baf926 100644
--- a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php
+++ b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php
@@ -9,8 +9,7 @@
class ParentObjectType extends ObjectType
{
- /** @var Type */
- private $parent;
+ private Type $parent;
public function __construct(string $className, Type $parent)
{
diff --git a/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php
index 71bc4fc2..93c713f1 100644
--- a/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php
+++ b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php
@@ -7,8 +7,7 @@
class TreeBuilderType extends ObjectType
{
- /** @var string */
- private $rootNodeClassName;
+ private string $rootNodeClassName;
public function __construct(string $className, string $rootNodeClassName)
{
diff --git a/src/Type/Symfony/EnvelopeReturnTypeExtension.php b/src/Type/Symfony/EnvelopeReturnTypeExtension.php
index e4e6a34c..06e08772 100644
--- a/src/Type/Symfony/EnvelopeReturnTypeExtension.php
+++ b/src/Type/Symfony/EnvelopeReturnTypeExtension.php
@@ -5,12 +5,15 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
+use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\ArrayType;
-use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
-use PHPStan\Type\MixedType;
+use PHPStan\Type\Generic\GenericClassStringType;
+use PHPStan\Type\IntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use function count;
final class EnvelopeReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
@@ -32,15 +35,23 @@ public function getTypeFromMethodCall(
): Type
{
if (count($methodCall->getArgs()) === 0) {
- return new ArrayType(new MixedType(), new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')));
+ return new ArrayType(
+ new GenericClassStringType(new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')),
+ TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType()),
+ );
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
- if (!$argType instanceof ConstantStringType) {
- return new ArrayType(new MixedType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface'));
+ if (count($argType->getConstantStrings()) === 0) {
+ return TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType('Symfony\Component\Messenger\Stamp\StampInterface')), new AccessoryArrayListType());
}
- return new ArrayType(new MixedType(), new ObjectType($argType->getValue()));
+ $objectTypes = [];
+ foreach ($argType->getConstantStrings() as $constantString) {
+ $objectTypes[] = new ObjectType($constantString->getValue());
+ }
+
+ return TypeCombinator::intersect(new ArrayType(new IntegerType(), TypeCombinator::union(...$objectTypes)), new AccessoryArrayListType());
}
}
diff --git a/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php
new file mode 100644
index 00000000..d975d9d1
--- /dev/null
+++ b/src/Type/Symfony/ExtensionGetConfigurationReturnTypeExtension.php
@@ -0,0 +1,104 @@
+reflectionProvider = $reflectionProvider;
+ }
+
+ public function getClass(): string
+ {
+ return 'Symfony\Component\DependencyInjection\Extension\Extension';
+ }
+
+ public function isMethodSupported(MethodReflection $methodReflection): bool
+ {
+ return $methodReflection->getName() === 'getConfiguration'
+ && $methodReflection->getDeclaringClass()->getName() === 'Symfony\Component\DependencyInjection\Extension\Extension';
+ }
+
+ public function getTypeFromMethodCall(
+ MethodReflection $methodReflection,
+ MethodCall $methodCall,
+ Scope $scope
+ ): ?Type
+ {
+ $types = [];
+ $extensionType = $scope->getType($methodCall->var);
+ $classes = $extensionType->getObjectClassNames();
+
+ foreach ($classes as $extensionName) {
+ if (str_contains($extensionName, "\0")) {
+ $types[] = new NullType();
+ continue;
+ }
+
+ $lastBackslash = strrpos($extensionName, '\\');
+ if ($lastBackslash === false) {
+ $types[] = new NullType();
+ continue;
+ }
+
+ $configurationName = substr_replace($extensionName, '\Configuration', $lastBackslash);
+ if (!$this->reflectionProvider->hasClass($configurationName)) {
+ $types[] = new NullType();
+ continue;
+ }
+
+ $reflection = $this->reflectionProvider->getClass($configurationName);
+ if ($this->hasRequiredConstructor($reflection)) {
+ $types[] = new NullType();
+ continue;
+ }
+
+ $types[] = new ObjectType($configurationName);
+ }
+
+ return TypeCombinator::union(...$types);
+ }
+
+ private function hasRequiredConstructor(ClassReflection $class): bool
+ {
+ if (!$class->hasConstructor()) {
+ return false;
+ }
+
+ $constructor = $class->getConstructor();
+ foreach ($constructor->getVariants() as $variant) {
+ $anyRequired = false;
+ foreach ($variant->getParameters() as $parameter) {
+ if (!$parameter->isOptional()) {
+ $anyRequired = true;
+ break;
+ }
+ }
+
+ if (!$anyRequired) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php
new file mode 100644
index 00000000..f80ddeb9
--- /dev/null
+++ b/src/Type/Symfony/Form/FormInterfaceDynamicReturnTypeExtension.php
@@ -0,0 +1,64 @@
+getName() === 'getErrors';
+ }
+
+ public function getTypeFromMethodCall(
+ MethodReflection $methodReflection,
+ MethodCall $methodCall,
+ Scope $scope
+ ): Type
+ {
+ if (!isset($methodCall->getArgs()[1])) {
+ return new GenericObjectType(FormErrorIterator::class, [new ObjectType(FormError::class)]);
+ }
+
+ $firstArgType = $scope->getType($methodCall->getArgs()[0]->value);
+ $secondArgType = $scope->getType($methodCall->getArgs()[1]->value);
+
+ $firstIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result;
+ $firstIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result;
+ $secondIsTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($secondArgType)->result;
+ $secondIsFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($secondArgType)->result;
+
+ $firstCompareType = $firstIsTrueType->compareTo($firstIsFalseType);
+ $secondCompareType = $secondIsTrueType->compareTo($secondIsFalseType);
+
+ if ($firstCompareType === $firstIsTrueType && $secondCompareType === $secondIsFalseType) {
+ return new GenericObjectType(FormErrorIterator::class, [
+ new UnionType([
+ new ObjectType(FormError::class),
+ new ObjectType(FormErrorIterator::class),
+ ]),
+ ]);
+ }
+
+ return new GenericObjectType(FormErrorIterator::class, [new ObjectType(FormError::class)]);
+ }
+
+}
diff --git a/src/Type/Symfony/GetOptionTypeHelper.php b/src/Type/Symfony/GetOptionTypeHelper.php
new file mode 100644
index 00000000..6ddf87d8
--- /dev/null
+++ b/src/Type/Symfony/GetOptionTypeHelper.php
@@ -0,0 +1,41 @@
+acceptValue()) {
+ if (method_exists($option, 'isNegatable') && $option->isNegatable()) {
+ return new UnionType([new BooleanType(), new NullType()]);
+ }
+
+ return new BooleanType();
+ }
+
+ $optType = TypeCombinator::union(new StringType(), new NullType());
+ if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) {
+ $optType = TypeCombinator::removeNull($optType);
+ }
+ if ($option->isArray()) {
+ $optType = new ArrayType(new IntegerType(), $optType);
+ }
+
+ return TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault()));
+ }
+
+}
diff --git a/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php b/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php
index 5b05a711..f50dc4e8 100644
--- a/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/HeaderBagDynamicReturnTypeExtension.php
@@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
@@ -32,11 +31,11 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
$firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true);
- $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType);
- $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType);
+ $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result;
+ $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result;
$compareTypes = $isTrueType->compareTo($isFalseType);
if ($compareTypes === $isTrueType) {
@@ -48,7 +47,7 @@ public function getTypeFromMethodCall(
return new ArrayType(new IntegerType(), new StringType());
}
- return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ return null;
}
}
diff --git a/src/Type/Symfony/Helper.php b/src/Type/Symfony/Helper.php
index 099b64c2..4aad820c 100644
--- a/src/Type/Symfony/Helper.php
+++ b/src/Type/Symfony/Helper.php
@@ -17,7 +17,7 @@ public static function createMarkerNode(Expr $expr, Type $type, PrettyPrinterAbs
return new Expr\Variable(md5(sprintf(
'%s::%s',
$printer->prettyPrintExpr($expr),
- $type->describe(VerbosityLevel::precise())
+ $type->describe(VerbosityLevel::precise()),
)));
}
diff --git a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php
index b4875829..75e6d0bc 100644
--- a/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/InputBagDynamicReturnTypeExtension.php
@@ -18,6 +18,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
+use function in_array;
final class InputBagDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
@@ -36,7 +37,7 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
if ($methodReflection->getName() === 'get') {
return $this->getGetTypeFromMethodCall($methodReflection, $methodCall, $scope);
@@ -53,17 +54,21 @@ private function getGetTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
if (isset($methodCall->getArgs()[1])) {
$argType = $scope->getType($methodCall->getArgs()[1]->value);
$isNull = (new NullType())->isSuperTypeOf($argType);
if ($isNull->no()) {
- return TypeCombinator::removeNull(ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType());
+ return TypeCombinator::removeNull(ParametersAcceptorSelector::selectFromArgs(
+ $scope,
+ $methodCall->getArgs(),
+ $methodReflection->getVariants(),
+ )->getReturnType());
}
}
- return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ return null;
}
private function getAllTypeFromMethodCall(
diff --git a/src/Type/Symfony/InputBagTypeSpecifyingExtension.php b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php
new file mode 100644
index 00000000..11cd39ef
--- /dev/null
+++ b/src/Type/Symfony/InputBagTypeSpecifyingExtension.php
@@ -0,0 +1,50 @@
+getName() === self::HAS_METHOD_NAME && $context->false();
+ }
+
+ public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
+ {
+ return $this->typeSpecifier->create(
+ new MethodCall($node->var, self::GET_METHOD_NAME, $node->getArgs()),
+ new NullType(),
+ $context->negate(),
+ $scope,
+ );
+ }
+
+ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
+ {
+ $this->typeSpecifier = $typeSpecifier;
+ }
+
+}
diff --git a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php
index 15e75d0e..88bd7b0e 100644
--- a/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/InputInterfaceGetArgumentDynamicReturnTypeExtension.php
@@ -6,22 +6,21 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
+use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
-use PHPStan\Type\TypeUtils;
use function count;
+use function in_array;
final class InputInterfaceGetArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
{
@@ -38,26 +37,25 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
return $methodReflection->getName() === 'getArgument';
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
- $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
-
if (!isset($methodCall->getArgs()[0])) {
- return $defaultReturnType;
+ return null;
}
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
- return $defaultReturnType;
+ return null;
}
- $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value));
+ $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($argStrings) !== 1) {
- return $defaultReturnType;
+ return null;
}
$argName = $argStrings[0]->getValue();
$argTypes = [];
+ $canBeNullInInteract = false;
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
$command->mergeApplicationDefinition();
@@ -71,6 +69,8 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
$argType = new StringType();
if (!$argument->isRequired()) {
$argType = TypeCombinator::union($argType, $scope->getTypeFromValue($argument->getDefault()));
+ } else {
+ $canBeNullInInteract = true;
}
}
$argTypes[] = $argType;
@@ -79,7 +79,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
}
}
- return count($argTypes) > 0 ? TypeCombinator::union(...$argTypes) : $defaultReturnType;
+ if (count($argTypes) === 0) {
+ return null;
+ }
+
+ $method = $scope->getFunction();
+ if (
+ $canBeNullInInteract
+ && $method instanceof MethodReflection
+ && ($method->getName() === 'interact' || $method->getName() === 'initialize')
+ && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true)
+ ) {
+ $argTypes[] = new NullType();
+ }
+
+ return TypeCombinator::union(...$argTypes);
}
}
diff --git a/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php
index fb397311..6d0346cf 100644
--- a/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/InputInterfaceGetOptionDynamicReturnTypeExtension.php
@@ -6,28 +6,23 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Symfony\ConsoleApplicationResolver;
-use PHPStan\Type\ArrayType;
-use PHPStan\Type\BooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
-use PHPStan\Type\IntegerType;
-use PHPStan\Type\NullType;
-use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
-use PHPStan\Type\TypeUtils;
use function count;
final class InputInterfaceGetOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
- public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
+ private GetOptionTypeHelper $getOptionTypeHelper;
+
+ public function __construct(ConsoleApplicationResolver $consoleApplicationResolver, GetOptionTypeHelper $getOptionTypeHelper)
{
$this->consoleApplicationResolver = $consoleApplicationResolver;
+ $this->getOptionTypeHelper = $getOptionTypeHelper;
}
public function getClass(): string
@@ -40,22 +35,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
return $methodReflection->getName() === 'getOption';
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
- $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
-
if (!isset($methodCall->getArgs()[0])) {
- return $defaultReturnType;
+ return null;
}
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
- return $defaultReturnType;
+ return null;
}
- $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value));
+ $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($optStrings) !== 1) {
- return $defaultReturnType;
+ return null;
}
$optName = $optStrings[0]->getValue();
@@ -64,25 +57,13 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
try {
$command->mergeApplicationDefinition();
$option = $command->getDefinition()->getOption($optName);
- if (!$option->acceptValue()) {
- $optType = new BooleanType();
- } else {
- $optType = TypeCombinator::union(new StringType(), new NullType());
- if ($option->isValueRequired() && ($option->isArray() || $option->getDefault() !== null)) {
- $optType = TypeCombinator::removeNull($optType);
- }
- if ($option->isArray()) {
- $optType = new ArrayType(new IntegerType(), $optType);
- }
- $optType = TypeCombinator::union($optType, $scope->getTypeFromValue($option->getDefault()));
- }
- $optTypes[] = $optType;
+ $optTypes[] = $this->getOptionTypeHelper->getOptionType($scope, $option);
} catch (InvalidArgumentException $e) {
// noop
}
}
- return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : $defaultReturnType;
+ return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : null;
}
}
diff --git a/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php
new file mode 100644
index 00000000..3105621d
--- /dev/null
+++ b/src/Type/Symfony/InputInterfaceGetOptionsDynamicReturnTypeExtension.php
@@ -0,0 +1,67 @@
+consoleApplicationResolver = $consoleApplicationResolver;
+ $this->getOptionTypeHelper = $getOptionTypeHelper;
+ }
+
+ public function getClass(): string
+ {
+ return 'Symfony\Component\Console\Input\InputInterface';
+ }
+
+ public function isMethodSupported(MethodReflection $methodReflection): bool
+ {
+ return $methodReflection->getName() === 'getOptions';
+ }
+
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
+ {
+ $classReflection = $scope->getClassReflection();
+ if ($classReflection === null) {
+ return null;
+ }
+
+ $optTypes = [];
+ foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
+ try {
+ $command->mergeApplicationDefinition();
+ $options = $command->getDefinition()->getOptions();
+ $builder = ConstantArrayTypeBuilder::createEmpty();
+ foreach ($options as $name => $option) {
+ $optionType = $this->getOptionTypeHelper->getOptionType($scope, $option);
+ $builder->setOffsetValueType(new ConstantStringType($name), $optionType);
+ }
+
+ $optTypes[] = $builder->getArray();
+ } catch (InvalidArgumentException $e) {
+ // noop
+ }
+ }
+
+ return count($optTypes) > 0 ? TypeCombinator::union(...$optTypes) : null;
+ }
+
+}
diff --git a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php
index 112a2bcf..34bffcea 100644
--- a/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/InputInterfaceHasArgumentDynamicReturnTypeExtension.php
@@ -7,19 +7,17 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Symfony\ConsoleApplicationResolver;
-use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
-use PHPStan\Type\TypeUtils;
use function array_unique;
use function count;
+use function in_array;
final class InputInterfaceHasArgumentDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
{
@@ -36,25 +34,34 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
return $methodReflection->getName() === 'hasArgument';
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
- $defaultReturnType = new BooleanType();
-
if (!isset($methodCall->getArgs()[0])) {
- return $defaultReturnType;
+ return null;
}
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
- return $defaultReturnType;
+ return null;
}
- $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value));
+ $argStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($argStrings) !== 1) {
- return $defaultReturnType;
+ return null;
}
$argName = $argStrings[0]->getValue();
+ if ($argName === 'command') {
+ $method = $scope->getFunction();
+ if (
+ $method instanceof MethodReflection
+ && ($method->getName() === 'interact' || $method->getName() === 'initialize')
+ && in_array('Symfony\Component\Console\Command\Command', $method->getDeclaringClass()->getParentClassesNames(), true)
+ ) {
+ return null;
+ }
+ }
+
$returnTypes = [];
foreach ($this->consoleApplicationResolver->findCommands($classReflection) as $command) {
try {
@@ -67,11 +74,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
}
if (count($returnTypes) === 0) {
- return $defaultReturnType;
+ return null;
}
$returnTypes = array_unique($returnTypes);
- return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType;
+ return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null;
}
}
diff --git a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php
index eec7201c..e4f8b5b1 100644
--- a/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/InputInterfaceHasOptionDynamicReturnTypeExtension.php
@@ -7,19 +7,16 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Symfony\ConsoleApplicationResolver;
-use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
-use PHPStan\Type\TypeUtils;
use function array_unique;
use function count;
final class InputInterfaceHasOptionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var \PHPStan\Symfony\ConsoleApplicationResolver */
- private $consoleApplicationResolver;
+ private ConsoleApplicationResolver $consoleApplicationResolver;
public function __construct(ConsoleApplicationResolver $consoleApplicationResolver)
{
@@ -36,22 +33,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
return $methodReflection->getName() === 'hasOption';
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
- $defaultReturnType = new BooleanType();
-
if (!isset($methodCall->getArgs()[0])) {
- return $defaultReturnType;
+ return null;
}
$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
- return $defaultReturnType;
+ return null;
}
- $optStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->getArgs()[0]->value));
+ $optStrings = $scope->getType($methodCall->getArgs()[0]->value)->getConstantStrings();
if (count($optStrings) !== 1) {
- return $defaultReturnType;
+ return null;
}
$optName = $optStrings[0]->getValue();
@@ -67,11 +62,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
}
if (count($returnTypes) === 0) {
- return $defaultReturnType;
+ return null;
}
$returnTypes = array_unique($returnTypes);
- return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : $defaultReturnType;
+ return count($returnTypes) === 1 ? new ConstantBooleanType($returnTypes[0]) : null;
}
}
diff --git a/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php
index 5a2adae5..810de2f6 100644
--- a/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/KernelInterfaceDynamicReturnTypeExtension.php
@@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
@@ -30,11 +29,11 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
$firstArgType = isset($methodCall->getArgs()[2]) ? $scope->getType($methodCall->getArgs()[2]->value) : new ConstantBooleanType(true);
- $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType);
- $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType);
+ $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($firstArgType)->result;
+ $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($firstArgType)->result;
$compareTypes = $isTrueType->compareTo($isFalseType);
if ($compareTypes === $isTrueType) {
@@ -44,7 +43,7 @@ public function getTypeFromMethodCall(
return new ArrayType(new IntegerType(), new StringType());
}
- return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ return null;
}
}
diff --git a/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php
new file mode 100644
index 00000000..2c7b1fbe
--- /dev/null
+++ b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php
@@ -0,0 +1,89 @@
+messageMapFactory = $symfonyMessageMapFactory;
+ }
+
+ public function getType(Expr $expr, Scope $scope): ?Type
+ {
+ if ($this->isSupported($expr, $scope)) {
+ $args = $expr->getArgs();
+ if (count($args) !== 1) {
+ return null;
+ }
+
+ $arg = $args[0]->value;
+ $argClassNames = $scope->getType($arg)->getObjectClassNames();
+
+ if (count($argClassNames) === 1) {
+ $messageMap = $this->getMessageMap();
+ $returnType = $messageMap->getTypeForClass($argClassNames[0]);
+
+ if (!is_null($returnType)) {
+ return $returnType;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private function getMessageMap(): MessageMap
+ {
+ if ($this->messageMap === null) {
+ $this->messageMap = $this->messageMapFactory->create();
+ }
+
+ return $this->messageMap;
+ }
+
+ /**
+ * @phpstan-assert-if-true =MethodCall $expr
+ */
+ private function isSupported(Expr $expr, Scope $scope): bool
+ {
+ if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->name !== self::TRAIT_METHOD_NAME) {
+ return false;
+ }
+
+ if (!$scope->isInClass()) {
+ return false;
+ }
+
+ $reflectionClass = $scope->getClassReflection()->getNativeReflection();
+
+ if (!$reflectionClass->hasMethod(self::TRAIT_METHOD_NAME)) {
+ return false;
+ }
+
+ $methodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME);
+ $declaringClassReflection = $methodReflection->getBetterReflection()->getDeclaringClass();
+
+ return $declaringClassReflection->getName() === self::TRAIT_NAME;
+ }
+
+}
diff --git a/src/Type/Symfony/OptionTypeSpecifyingExtension.php b/src/Type/Symfony/OptionTypeSpecifyingExtension.php
index 347cf591..8cd9dbd5 100644
--- a/src/Type/Symfony/OptionTypeSpecifyingExtension.php
+++ b/src/Type/Symfony/OptionTypeSpecifyingExtension.php
@@ -3,25 +3,23 @@
namespace PHPStan\Type\Symfony;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\MethodTypeSpecifyingExtension;
final class OptionTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- /** @var \PHPStan\Analyser\TypeSpecifier */
- private $typeSpecifier;
+ private TypeSpecifier $typeSpecifier;
- public function __construct(Standard $printer)
+ public function __construct(Printer $printer)
{
$this->printer = $printer;
}
@@ -45,7 +43,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
return $this->typeSpecifier->create(
Helper::createMarkerNode($node->var, $argType, $this->printer),
$argType,
- $context
+ $context,
+ $scope,
);
}
diff --git a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php
index 8e79cf59..687b0c33 100644
--- a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php
@@ -4,15 +4,15 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
+use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Symfony\ParameterMap;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
+use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
-use PHPStan\Type\ConstantType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\GeneralizePrecision;
@@ -21,35 +21,57 @@
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
+use Symfony\Component\DependencyInjection\EnvVarProcessor;
+use function array_filter;
+use function array_keys;
+use function array_map;
+use function array_values;
+use function class_exists;
+use function count;
use function in_array;
+use function is_array;
+use function is_int;
+use function is_string;
+use function preg_match;
+use function strlen;
final class ParameterDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var string */
- private $className;
+ /** @var class-string */
+ private string $className;
- /** @var string|null */
- private $methodGet;
+ private ?string $methodGet = null;
- /** @var string|null */
- private $methodHas;
+ private ?string $methodHas = null;
- /** @var bool */
- private $constantHassers;
+ private bool $constantHassers;
- /** @var \PHPStan\Symfony\ParameterMap */
- private $parameterMap;
+ private ParameterMap $parameterMap;
- public function __construct(string $className, ?string $methodGet, ?string $methodHas, bool $constantHassers, ParameterMap $symfonyParameterMap)
+ private TypeStringResolver $typeStringResolver;
+
+ /**
+ * @param class-string $className
+ */
+ public function __construct(
+ string $className,
+ ?string $methodGet,
+ ?string $methodHas,
+ bool $constantHassers,
+ ParameterMap $symfonyParameterMap,
+ TypeStringResolver $typeStringResolver
+ )
{
$this->className = $className;
$this->methodGet = $methodGet;
$this->methodHas = $methodHas;
$this->constantHassers = $constantHassers;
$this->parameterMap = $symfonyParameterMap;
+ $this->typeStringResolver = $typeStringResolver;
}
public function getClass(): string
@@ -59,14 +81,12 @@ public function getClass(): string
public function isMethodSupported(MethodReflection $methodReflection): bool
{
- $methods = array_filter([$this->methodGet, $this->methodHas], function (?string $method): bool {
- return $method !== null;
- });
+ $methods = array_filter([$this->methodGet, $this->methodHas], static fn (?string $method): bool => $method !== null);
return in_array($methodReflection->getName(), $methods, true);
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
switch ($methodReflection->getName()) {
case $this->methodGet:
@@ -85,7 +105,7 @@ private function getGetTypeFromMethodCall(
{
// We don't use the method's return type because this won't work properly with lowest and
// highest versions of Symfony ("mixed" for lowest, "array|bool|float|integer|string|null" for highest).
- $returnType = new UnionType([
+ $defaultReturnType = new UnionType([
new ArrayType(new MixedType(), new MixedType()),
new BooleanType(),
new FloatType(),
@@ -94,18 +114,77 @@ private function getGetTypeFromMethodCall(
new NullType(),
]);
if (!isset($methodCall->getArgs()[0])) {
- return $returnType;
+ return $defaultReturnType;
+ }
+
+ $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope);
+ if ($parameterKeys === []) {
+ return $defaultReturnType;
}
- $parameterKey = $this->parameterMap::getParameterKeyFromNode($methodCall->getArgs()[0]->value, $scope);
- if ($parameterKey !== null) {
+ $returnTypes = [];
+ foreach ($parameterKeys as $parameterKey) {
$parameter = $this->parameterMap->getParameter($parameterKey);
- if ($parameter !== null) {
- return $this->generalizeType($scope->getTypeFromValue($parameter->getValue()));
+ if ($parameter === null) {
+ return $defaultReturnType;
}
+
+ $returnTypes[] = $this->generalizeTypeFromValue($scope, $parameter->getValue());
}
- return $returnType;
+ return TypeCombinator::union(...$returnTypes);
+ }
+
+ /**
+ * @param array|bool|float|int|string $value
+ */
+ private function generalizeTypeFromValue(Scope $scope, $value): Type
+ {
+ if (is_array($value) && $value !== []) {
+ $hasOnlyStringKey = true;
+ foreach (array_keys($value) as $key) {
+ if (is_int($key)) {
+ $hasOnlyStringKey = false;
+ break;
+ }
+ }
+
+ if ($hasOnlyStringKey) {
+ $keyTypes = [];
+ $valueTypes = [];
+ foreach ($value as $key => $element) {
+ $keyType = $scope->getTypeFromValue($key);
+ $keyStringTypes = $keyType->getConstantStrings();
+ if (count($keyStringTypes) !== 1) {
+ throw new ShouldNotHappenException();
+ }
+ $keyTypes[] = $keyStringTypes[0];
+ $valueTypes[] = $this->generalizeTypeFromValue($scope, $element);
+ }
+
+ return ConstantArrayTypeBuilder::createFromConstantArray(
+ new ConstantArrayType($keyTypes, $valueTypes),
+ )->getArray();
+ }
+
+ return new ArrayType(
+ TypeCombinator::union(...array_map(fn ($item): Type => $this->generalizeTypeFromValue($scope, $item), array_keys($value))),
+ TypeCombinator::union(...array_map(fn ($item): Type => $this->generalizeTypeFromValue($scope, $item), array_values($value))),
+ );
+ }
+
+ if (
+ class_exists(EnvVarProcessor::class)
+ && is_string($value)
+ && preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1
+ && strlen($matches[0]) === strlen($value)
+ ) {
+ $providedTypes = EnvVarProcessor::getProvidedTypes();
+
+ return $this->typeStringResolver->resolve($providedTypes[$matches[1]] ?? 'bool|int|float|string|array');
+ }
+
+ return $this->generalizeType($scope->getTypeFromValue($value));
}
private function generalizeType(Type $type): Type
@@ -117,7 +196,7 @@ private function generalizeType(Type $type): Type
}
return new ArrayType($this->generalizeType($type->getKeyType()), $this->generalizeType($type->getItemType()));
}
- if ($type instanceof ConstantType) {
+ if ($type->isConstantValue()->yes()) {
return $type->generalize(GeneralizePrecision::lessSpecific());
}
return $traverse($type);
@@ -128,20 +207,32 @@ private function getHasTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
- $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) {
- return $returnType;
+ return null;
+ }
+
+ $parameterKeys = $this->parameterMap::getParameterKeysFromNode($methodCall->getArgs()[0]->value, $scope);
+ if ($parameterKeys === []) {
+ return null;
}
- $parameterKey = $this->parameterMap::getParameterKeyFromNode($methodCall->getArgs()[0]->value, $scope);
- if ($parameterKey !== null) {
+ $has = null;
+ foreach ($parameterKeys as $parameterKey) {
$parameter = $this->parameterMap->getParameter($parameterKey);
- return new ConstantBooleanType($parameter !== null);
+
+ if ($has === null) {
+ $has = $parameter !== null;
+ } elseif (
+ ($has === true && $parameter === null)
+ || ($has === false && $parameter !== null)
+ ) {
+ return null;
+ }
}
- return $returnType;
+ return new ConstantBooleanType($has);
}
}
diff --git a/src/Type/Symfony/RequestDynamicReturnTypeExtension.php b/src/Type/Symfony/RequestDynamicReturnTypeExtension.php
index 284ddf01..fd0a3a00 100644
--- a/src/Type/Symfony/RequestDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/RequestDynamicReturnTypeExtension.php
@@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ResourceType;
@@ -29,15 +28,15 @@ public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
if (!isset($methodCall->getArgs()[0])) {
return new StringType();
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
- $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType);
- $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType);
+ $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType)->result;
+ $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType)->result;
$compareTypes = $isTrueType->compareTo($isFalseType);
if ($compareTypes === $isTrueType) {
return new ResourceType();
@@ -46,7 +45,7 @@ public function getTypeFromMethodCall(
return new StringType();
}
- return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
+ return null;
}
}
diff --git a/src/Type/Symfony/RequestTypeSpecifyingExtension.php b/src/Type/Symfony/RequestTypeSpecifyingExtension.php
index 66372a7b..40d38493 100644
--- a/src/Type/Symfony/RequestTypeSpecifyingExtension.php
+++ b/src/Type/Symfony/RequestTypeSpecifyingExtension.php
@@ -8,7 +8,6 @@
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
-use PHPStan\Broker\Broker;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\MethodTypeSpecifyingExtension;
@@ -21,16 +20,7 @@ final class RequestTypeSpecifyingExtension implements MethodTypeSpecifyingExtens
private const HAS_METHOD_NAME = 'hasSession';
private const GET_METHOD_NAME = 'getSession';
- /** @var Broker */
- private $broker;
-
- /** @var TypeSpecifier */
- private $typeSpecifier;
-
- public function __construct(Broker $broker)
- {
- $this->broker = $broker;
- }
+ private TypeSpecifier $typeSpecifier;
public function getClass(): string
{
@@ -44,9 +34,8 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
- $classReflection = $this->broker->getClass(self::REQUEST_CLASS);
- $methodVariants = $classReflection->getNativeMethod(self::GET_METHOD_NAME)->getVariants();
- $returnType = ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType();
+ $methodVariants = $methodReflection->getDeclaringClass()->getNativeMethod(self::GET_METHOD_NAME)->getVariants();
+ $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodVariants)->getReturnType();
if (!TypeCombinator::containsNull($returnType)) {
return new SpecifiedTypes();
@@ -55,7 +44,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
return $this->typeSpecifier->create(
new MethodCall($node->var, self::GET_METHOD_NAME),
TypeCombinator::removeNull($returnType),
- $context
+ $context,
+ $scope,
);
}
diff --git a/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php b/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php
new file mode 100644
index 00000000..5a95bf98
--- /dev/null
+++ b/src/Type/Symfony/ResponseHeaderBagDynamicReturnTypeExtension.php
@@ -0,0 +1,65 @@
+getName() === 'getCookies';
+ }
+
+ public function getTypeFromMethodCall(
+ MethodReflection $methodReflection,
+ MethodCall $methodCall,
+ Scope $scope
+ ): Type
+ {
+ if (isset($methodCall->getArgs()[0])) {
+ $node = $methodCall->getArgs()[0]->value;
+
+ if (
+ $node instanceof ClassConstFetch &&
+ $node->class instanceof Name &&
+ $node->name instanceof Identifier &&
+ $node->class->toString() === ResponseHeaderBag::class &&
+ $node->name->name === 'COOKIES_ARRAY'
+ ) {
+ return new ArrayType(
+ new StringType(),
+ new ArrayType(
+ new StringType(),
+ new ArrayType(
+ new StringType(),
+ new ObjectType(Cookie::class),
+ ),
+ ),
+ );
+ }
+ }
+
+ return new ArrayType(new IntegerType(), new ObjectType(Cookie::class));
+ }
+
+}
diff --git a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php
index fda9b048..84f256e4 100755
--- a/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/SerializerDynamicReturnTypeExtension.php
@@ -6,21 +6,25 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ArrayType;
-use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use function count;
+use function substr;
class SerializerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var string */
- private $class;
+ /** @var class-string */
+ private string $class;
- /** @var string */
- private $method;
+ private string $method;
+ /**
+ * @param class-string $class
+ */
public function __construct(string $class, string $method)
{
$this->class = $class;
@@ -44,13 +48,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
}
$argType = $scope->getType($methodCall->getArgs()[1]->value);
- if (!$argType instanceof ConstantStringType) {
+ if (count($argType->getConstantStrings()) === 0) {
return new MixedType();
}
- $objectName = $argType->getValue();
+ $types = [];
+ foreach ($argType->getConstantStrings() as $constantString) {
+ $types[] = $this->getType($constantString->getValue());
+ }
- return $this->getType($objectName);
+ return TypeCombinator::union(...$types);
}
private function getType(string $objectName): Type
diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php
index 650aaa42..0667d30c 100644
--- a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php
+++ b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php
@@ -5,32 +5,47 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
-use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
+use PHPStan\Symfony\ParameterMap;
+use PHPStan\Symfony\ServiceDefinition;
use PHPStan\Symfony\ServiceMap;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use function class_exists;
use function in_array;
+use function is_string;
final class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
- /** @var string */
- private $className;
+ /** @var class-string */
+ private string $className;
- /** @var bool */
- private $constantHassers;
+ private bool $constantHassers;
- /** @var \PHPStan\Symfony\ServiceMap */
- private $serviceMap;
+ private ServiceMap $serviceMap;
- public function __construct(string $className, bool $constantHassers, ServiceMap $symfonyServiceMap)
+ private ParameterMap $parameterMap;
+
+ private ?ParameterBag $parameterBag = null;
+
+ /**
+ * @param class-string $className
+ */
+ public function __construct(
+ string $className,
+ bool $constantHassers,
+ ServiceMap $symfonyServiceMap,
+ ParameterMap $symfonyParameterMap
+ )
{
$this->className = $className;
$this->constantHassers = $constantHassers;
$this->serviceMap = $symfonyServiceMap;
+ $this->parameterMap = $symfonyParameterMap;
}
public function getClass(): string
@@ -43,7 +58,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
return in_array($methodReflection->getName(), ['get', 'has'], true);
}
- public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
+ public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
switch ($methodReflection->getName()) {
case 'get':
@@ -58,33 +73,60 @@ private function getGetTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
- $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
if (!isset($methodCall->getArgs()[0])) {
- return $returnType;
+ return null;
+ }
+
+ $parameterBag = $this->tryGetParameterBag();
+ if ($parameterBag === null) {
+ return null;
}
$serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope);
if ($serviceId !== null) {
$service = $this->serviceMap->getService($serviceId);
if ($service !== null && (!$service->isSynthetic() || $service->getClass() !== null)) {
- return new ObjectType($service->getClass() ?? $serviceId);
+ return new ObjectType($this->determineServiceClass($parameterBag, $service) ?? $serviceId);
}
}
- return $returnType;
+ return null;
+ }
+
+ private function tryGetParameterBag(): ?ParameterBag
+ {
+ if ($this->parameterBag !== null) {
+ return $this->parameterBag;
+ }
+
+ return $this->parameterBag = $this->tryCreateParameterBag();
+ }
+
+ private function tryCreateParameterBag(): ?ParameterBag
+ {
+ if (!class_exists(ParameterBag::class)) {
+ return null;
+ }
+
+ $parameters = [];
+
+ foreach ($this->parameterMap->getParameters() as $parameterDefinition) {
+ $parameters[$parameterDefinition->getKey()] = $parameterDefinition->getValue();
+ }
+
+ return new ParameterBag($parameters);
}
private function getHasTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
- ): Type
+ ): ?Type
{
- $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
if (!isset($methodCall->getArgs()[0]) || !$this->constantHassers) {
- return $returnType;
+ return null;
}
$serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope);
@@ -93,7 +135,22 @@ private function getHasTypeFromMethodCall(
return new ConstantBooleanType($service !== null && $service->isPublic());
}
- return $returnType;
+ return null;
+ }
+
+ private function determineServiceClass(ParameterBag $parameterBag, ServiceDefinition $service): ?string
+ {
+ $class = $service->getClass();
+ if ($class === null) {
+ return null;
+ }
+
+ $value = $parameterBag->resolveValue($class);
+ if (!is_string($value)) {
+ return null;
+ }
+
+ return $value;
}
}
diff --git a/src/Type/Symfony/ServiceTypeSpecifyingExtension.php b/src/Type/Symfony/ServiceTypeSpecifyingExtension.php
index a5aad54e..dd767ccb 100644
--- a/src/Type/Symfony/ServiceTypeSpecifyingExtension.php
+++ b/src/Type/Symfony/ServiceTypeSpecifyingExtension.php
@@ -3,28 +3,29 @@
namespace PHPStan\Type\Symfony;
use PhpParser\Node\Expr\MethodCall;
-use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\MethodTypeSpecifyingExtension;
final class ServiceTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
- /** @var string */
- private $className;
+ /** @var class-string */
+ private string $className;
- /** @var \PhpParser\PrettyPrinter\Standard */
- private $printer;
+ private Printer $printer;
- /** @var \PHPStan\Analyser\TypeSpecifier */
- private $typeSpecifier;
+ private TypeSpecifier $typeSpecifier;
- public function __construct(string $className, Standard $printer)
+ /**
+ * @param class-string $className
+ */
+ public function __construct(string $className, Printer $printer)
{
$this->className = $className;
$this->printer = $printer;
@@ -49,7 +50,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod
return $this->typeSpecifier->create(
Helper::createMarkerNode($node->var, $argType, $this->printer),
$argType,
- $context
+ $context,
+ $scope,
);
}
diff --git a/stubs/Psr/Cache/CacheException.stub b/stubs/Psr/Cache/CacheException.stub
new file mode 100644
index 00000000..1be3e49b
--- /dev/null
+++ b/stubs/Psr/Cache/CacheException.stub
@@ -0,0 +1,7 @@
+
+ * @template TData
+ *
+ * @param class-string $type
+ * @param TData $data
+ * @param array $options
+ *
+ * @phpstan-return ($data is null ? FormInterface : FormInterface)
+ */
+ protected function createForm(string $type, $data = null, array $options = []): FormInterface
+ {
+ }
+}
diff --git a/stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub b/stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
new file mode 100644
index 00000000..ec54f1eb
--- /dev/null
+++ b/stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
@@ -0,0 +1,13 @@
+ $config
+ *
+ * @return string|string[]
+ */
+ public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId);
+}
diff --git a/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub b/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub
new file mode 100644
index 00000000..5c6b54fd
--- /dev/null
+++ b/stubs/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.stub
@@ -0,0 +1,15 @@
+ $config
+ *
+ * @return string[]
+ */
+ public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array;
+}
diff --git a/stubs/Symfony/Component/Console/Command.stub b/stubs/Symfony/Component/Console/Command.stub
new file mode 100644
index 00000000..c656ef6a
--- /dev/null
+++ b/stubs/Symfony/Component/Console/Command.stub
@@ -0,0 +1,17 @@
+ $messages
+ * @param int-mask-of $options
+ */
+ public function write($messages, bool $newline = false, int $options = 0): void;
+
+ /**
+ * @param string|iterable $messages
+ * @param int-mask-of $options
+ */
+ public function writeln($messages, int $options = 0): void;
+}
diff --git a/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub b/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub
index d0b04792..b1229723 100644
--- a/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub
+++ b/stubs/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.stub
@@ -8,6 +8,8 @@ interface ExtensionInterface
{
/**
* @param array $configs
+ *
+ * @throws \InvalidArgumentException
*/
public function load(array $configs, ContainerBuilder $container): void;
}
diff --git a/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub b/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub
index e4ad8fc4..a58e43ca 100644
--- a/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub
+++ b/stubs/Symfony/Component/EventDispatcher/EventDispatcherInterface.stub
@@ -10,5 +10,5 @@ interface EventDispatcherInterface
*
* @return TEvent
*/
- public function dispatch(object $event, string $eventName = null): object;
+ public function dispatch(object $event, ?string $eventName = null): object;
}
diff --git a/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub b/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub
index 35a77c93..62474d10 100644
--- a/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub
+++ b/stubs/Symfony/Component/EventDispatcher/EventSubscriberInterface.stub
@@ -5,7 +5,7 @@ namespace Symfony\Component\EventDispatcher;
interface EventSubscriberInterface
{
/**
- * @return array|array|array>>
+ * @return array>
*/
public static function getSubscribedEvents();
}
diff --git a/stubs/Symfony/Component/Form/AbstractType.stub b/stubs/Symfony/Component/Form/AbstractType.stub
new file mode 100644
index 00000000..e99b746c
--- /dev/null
+++ b/stubs/Symfony/Component/Form/AbstractType.stub
@@ -0,0 +1,31 @@
+
+ */
+abstract class AbstractType implements FormTypeInterface
+{
+
+ /**
+ * @param FormBuilderInterface $builder
+ * @param array $options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void;
+
+ /**
+ * @param FormInterface $form
+ * @param array $options
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options): void;
+
+ /**
+ * @param FormInterface $form
+ * @param array $options
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options): void;
+
+}
diff --git a/stubs/Symfony/Component/Form/DataTransformerInterface.stub b/stubs/Symfony/Component/Form/DataTransformerInterface.stub
index edd4411e..393fa803 100644
--- a/stubs/Symfony/Component/Form/DataTransformerInterface.stub
+++ b/stubs/Symfony/Component/Form/DataTransformerInterface.stub
@@ -14,6 +14,8 @@ interface DataTransformerInterface
* @phpstan-param T|null $value The value in the original representation
*
* @phpstan-return R|null The value in the transformed representation
+ *
+ * @throws TransformationFailedException
*/
public function transform($value);
@@ -21,6 +23,8 @@ interface DataTransformerInterface
* @phpstan-param R|null $value The value in the transformed representation
*
* @phpstan-return T|null The value in the original representation
+ *
+ * @throws TransformationFailedException
*/
public function reverseTransform($value);
}
diff --git a/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub b/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub
new file mode 100644
index 00000000..306015dc
--- /dev/null
+++ b/stubs/Symfony/Component/Form/Exception/ExceptionInterface.stub
@@ -0,0 +1,7 @@
+
+ * @template TData
+ *
+ * @extends \Traversable>
+ * @extends FormConfigBuilderInterface
*/
-interface FormBuilderInterface extends \Traversable
+interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
{
+ /**
+ * @return FormInterface
+ */
+ public function getForm(): FormInterface;
+
}
diff --git a/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
new file mode 100644
index 00000000..a167ce43
--- /dev/null
+++ b/stubs/Symfony/Component/Form/FormConfigBuilderInterface.stub
@@ -0,0 +1,13 @@
+
+ */
+interface FormConfigBuilderInterface extends FormConfigInterface
+{
+
+}
diff --git a/stubs/Symfony/Component/Form/FormConfigInterface.stub b/stubs/Symfony/Component/Form/FormConfigInterface.stub
new file mode 100644
index 00000000..942d467b
--- /dev/null
+++ b/stubs/Symfony/Component/Form/FormConfigInterface.stub
@@ -0,0 +1,16 @@
+
+ * @template TData
+ *
+ * @param class-string $type
+ * @param TData $data
+ * @param array $options
+ *
+ * @phpstan-return ($data is null ? FormInterface : FormInterface)
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function create(string $type = FormType::class, $data = null, array $options = []): FormInterface;
+
+ /**
+ * @template TFormType of FormTypeInterface
+ * @template TData
+ *
+ * @param class-string $type
+ * @param TData $data
+ * @param array $options
+ *
+ * @phpstan-return ($data is null ? FormInterface : FormInterface)
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function createNamed(string $name, string $type = FormType::class, $data = null, array $options = []): FormInterface;
+}
diff --git a/stubs/Symfony/Component/Form/FormInterface.stub b/stubs/Symfony/Component/Form/FormInterface.stub
index 868c4ae2..4bd21229 100644
--- a/stubs/Symfony/Component/Form/FormInterface.stub
+++ b/stubs/Symfony/Component/Form/FormInterface.stub
@@ -3,9 +3,22 @@
namespace Symfony\Component\Form;
/**
- * @extends \Traversable
+ * @template TData
+ *
+ * @extends \ArrayAccess>
+ * @extends \Traversable>
*/
-interface FormInterface extends \Traversable
+interface FormInterface extends \ArrayAccess, \Traversable, \Countable
{
+ /**
+ * @param TData $modelData
+ *
+ * @return $this
+ */
+ public function setData($modelData): FormInterface;
+ /**
+ * @return TData
+ */
+ public function getData();
}
diff --git a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
index 25b6f25d..a03d5e1c 100644
--- a/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
+++ b/stubs/Symfony/Component/Form/FormTypeExtensionInterface.stub
@@ -2,19 +2,25 @@
namespace Symfony\Component\Form;
+/**
+ * @template TData
+ */
interface FormTypeExtensionInterface
{
/**
+ * @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
/**
+ * @phpstan-param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options): void;
/**
+ * @phpstan-param FormInterface $form
* @param array $options
*/
public function finishView(FormView $view, FormInterface $form, array $options): void;
diff --git a/stubs/Symfony/Component/Form/FormTypeInterface.stub b/stubs/Symfony/Component/Form/FormTypeInterface.stub
index cebbc1c2..8536656a 100644
--- a/stubs/Symfony/Component/Form/FormTypeInterface.stub
+++ b/stubs/Symfony/Component/Form/FormTypeInterface.stub
@@ -2,19 +2,25 @@
namespace Symfony\Component\Form;
+/**
+ * @template TData
+ */
interface FormTypeInterface
{
/**
+ * @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
/**
+ * @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options): void;
/**
+ * @param FormInterface $form
* @param array $options
*/
public function finishView(FormView $view, FormInterface $form, array $options): void;
diff --git a/stubs/Symfony/Component/Form/FormView.stub b/stubs/Symfony/Component/Form/FormView.stub
index 8f920ed6..08c64752 100644
--- a/stubs/Symfony/Component/Form/FormView.stub
+++ b/stubs/Symfony/Component/Form/FormView.stub
@@ -6,8 +6,8 @@ use ArrayAccess;
use IteratorAggregate;
/**
- * @implements IteratorAggregate
- * @implements ArrayAccess
+ * @implements IteratorAggregate
+ * @implements ArrayAccess
*/
class FormView implements ArrayAccess, IteratorAggregate
{
@@ -15,7 +15,7 @@ class FormView implements ArrayAccess, IteratorAggregate
/**
* Returns an iterator to iterate over children (implements \IteratorAggregate).
*
- * @return \ArrayIterator The iterator
+ * @return \ArrayIterator The iterator
*/
public function getIterator();
diff --git a/stubs/Symfony/Component/HttpFoundation/Cookie.stub b/stubs/Symfony/Component/HttpFoundation/Cookie.stub
index 40ee45ab..cfb45fa3 100644
--- a/stubs/Symfony/Component/HttpFoundation/Cookie.stub
+++ b/stubs/Symfony/Component/HttpFoundation/Cookie.stub
@@ -21,7 +21,7 @@ class Cookie
*
* @throws \InvalidArgumentException
*/
- public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null);
+ public function __construct(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null);
/**
* @param string $name The name of the cookie
@@ -36,7 +36,7 @@ class Cookie
*
* @throws \InvalidArgumentException
*/
- public function create(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, string $sameSite = null): self;
+ public function create(string $name, ?string $value = null, $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = false, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): self;
/**
* @return self::SAMESITE_*|null
diff --git a/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub b/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub
index 00a40741..0a6858e3 100644
--- a/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub
+++ b/stubs/Symfony/Component/HttpFoundation/ParameterBag.stub
@@ -7,5 +7,10 @@ namespace Symfony\Component\HttpFoundation;
*/
class ParameterBag implements \IteratorAggregate
{
-
+ /**
+ * @return list
+ */
+ public function keys(): array
+ {
+ }
}
diff --git a/stubs/Symfony/Component/HttpFoundation/Request.stub b/stubs/Symfony/Component/HttpFoundation/Request.stub
index 00d03b09..0c2140cc 100644
--- a/stubs/Symfony/Component/HttpFoundation/Request.stub
+++ b/stubs/Symfony/Component/HttpFoundation/Request.stub
@@ -26,4 +26,47 @@ class Request
*/
public $cookies;
+ /**
+ * @return string[]
+ */
+ public static function getTrustedProxies(): array;
+
+ /**
+ * @return string[]
+ */
+ public static function getTrustedHosts(): array;
+
+ /**
+ * @param string $format
+ *
+ * @return string[]
+ */
+ public static function getMimeTypes($format): array;
+
+ /**
+ * @param string|null $format
+ * @param string|string[] $mimeTypes
+ */
+ public function setFormat($format, $mimeTypes): void;
+
+ /**
+ * @return string[]
+ */
+ public function getLanguages(): array;
+
+ /**
+ * @return string[]
+ */
+ public function getCharsets(): array;
+
+ /**
+ * @return string[]
+ */
+ public function getEncodings(): array;
+
+ /**
+ * @return string[]
+ */
+ public function getAcceptableContentTypes(): array;
+
}
diff --git a/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub
new file mode 100644
index 00000000..2856e32b
--- /dev/null
+++ b/stubs/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.stub
@@ -0,0 +1,7 @@
+, value-of>
+ */
+interface Options extends \ArrayAccess, \Countable
+{
+ /**
+ * @param key-of $offset
+ *
+ * @return bool
+ */
+ public function offsetExists($offset);
+
+ /**
+ * @template TOffset of key-of
+ * @param TOffset $offset
+ * @return TArray[TOffset]
+ */
+ public function offsetGet($offset);
+
+ /**
+ * @template TOffset of key-of
+ * @param TOffset|null $offset
+ * @param TArray[TOffset] $value
+ *
+ * @return void
+ */
+ public function offsetSet($offset, $value);
+
+ /**
+ * @template TOffset of key-of
+ * @param TOffset $offset
+ *
+ * @return void
+ */
+ public function offsetUnset($offset);
+}
diff --git a/stubs/Symfony/Component/Process/Exception/LogicException.stub b/stubs/Symfony/Component/Process/Exception/LogicException.stub
new file mode 100644
index 00000000..cb781d6a
--- /dev/null
+++ b/stubs/Symfony/Component/Process/Exception/LogicException.stub
@@ -0,0 +1,8 @@
+
*/
class Process implements \IteratorAggregate
{
+ /**
+ * @param int $flags
+ *
+ * @return \Generator
+ *
+ * @throws LogicException in case the output has been disabled
+ * @throws LogicException In case the process is not started
+ */
+ public function getIterator(int $flags = 0): \Generator
+ {
+
+ }
+
}
diff --git a/stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub b/stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub
new file mode 100644
index 00000000..a763b784
--- /dev/null
+++ b/stubs/Symfony/Component/PropertyAccess/Exception/AccessException.stub
@@ -0,0 +1,7 @@
+
+ * @phpstan-param T &$objectOrArray
+ * @phpstan-param-out ($objectOrArray is object ? T : array) $objectOrArray
+ * @phpstan-param string|PropertyPathInterface $propertyPath
+ * @phpstan-param mixed $value
+ *
+ * @return void
+ *
+ * @throws Exception\InvalidArgumentException If the property path is invalid
+ * @throws Exception\AccessException If a property/index does not exist or is not public
+ * @throws Exception\UnexpectedTypeException If a value within the path is neither object nor array
+ */
+ public function setValue(&$objectOrArray, $propertyPath, $value);
+
+}
diff --git a/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub b/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub
new file mode 100644
index 00000000..204a9c40
--- /dev/null
+++ b/stubs/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.stub
@@ -0,0 +1,7 @@
+ $context
* @return mixed
+ *
+ * @throws UnexpectedValueException
*/
public function decode($data, $format, array $context = []);
/**
* @param string $format Format name
+ * @param array $context
* @return bool
*/
- public function supportsDecoding($format);
+ public function supportsDecoding($format, array $context = []);
}
diff --git a/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub
index eb6fc97c..11e374eb 100644
--- a/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub
+++ b/stubs/Symfony/Component/Serializer/Encoder/EncoderInterface.stub
@@ -2,6 +2,8 @@
namespace Symfony\Component\Serializer\Encoder;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
interface EncoderInterface
{
/**
@@ -9,12 +11,15 @@ interface EncoderInterface
* @param string $format
* @param array $context
* @return string
+ *
+ * @throws UnexpectedValueException
*/
public function encode($data, $format, array $context = []);
/**
* @param string $format Format name
+ * @param array $context
* @return bool
*/
- public function supportsEncoding($format);
+ public function supportsEncoding($format, array $context = []);
}
diff --git a/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub b/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub
new file mode 100644
index 00000000..3c569971
--- /dev/null
+++ b/stubs/Symfony/Component/Serializer/Exception/BadMethodCallException.stub
@@ -0,0 +1,7 @@
+ $context
- * @return object|array
+ * @return mixed
+ *
+ * @throws BadMethodCallException
+ * @throws InvalidArgumentException
+ * @throws UnexpectedValueException
+ * @throws ExtraAttributesException
+ * @throws LogicException
+ * @throws RuntimeException
+ * @throws ExceptionInterface
*/
public function denormalize($data, $type, $format = null, array $context = []);
@@ -17,7 +33,8 @@ interface DenormalizerInterface
* @param mixed $data
* @param string $type
* @param string|null $format
+ * @param array $context
* @return bool
*/
- public function supportsDenormalization($data, $type, $format = null);
+ public function supportsDenormalization($data, $type, $format = null, array $context = []);
}
diff --git a/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub
index a65b5bed..ba86b6b6 100644
--- a/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub
+++ b/stubs/Symfony/Component/Serializer/Normalizer/NormalizerInterface.stub
@@ -2,20 +2,34 @@
namespace Symfony\Component\Serializer\Normalizer;
+use ArrayObject;
+use Symfony\Component\Serializer\Exception\CircularReferenceException;
+use Symfony\Component\Serializer\Exception\ExceptionInterface;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\LogicException;
+
interface NormalizerInterface
{
/**
* @param mixed $object
* @param string|null $format
* @param array $context
- * @return array|string|int|float|bool|null
+ *
+ * @return array|ArrayObject|string|int|float|bool|null
+ *
+ * @throws InvalidArgumentException
+ * @throws CircularReferenceException
+ * @throws LogicException
+ * @throws ExceptionInterface
*/
public function normalize($object, $format = null, array $context = []);
/**
* @param mixed $data
* @param string|null $format
+ * @param array $context
+ *
* @return bool
*/
- public function supportsNormalization($data, $format = null);
+ public function supportsNormalization($data, $format = null, array $context = []);
}
diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub
index ef3312bb..fd1c7b9b 100644
--- a/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub
+++ b/stubs/Symfony/Component/Validator/ConstraintViolationInterface.stub
@@ -2,6 +2,11 @@
namespace Symfony\Component\Validator;
+/**
+ * @method Constraint|null getConstraint() Returns the constraint whose validation caused the violation. Not implementing it is deprecated since Symfony 6.3.
+ * @method mixed getCause() Returns the cause of the violation. Not implementing it is deprecated since Symfony 6.2.
+ * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1.
+ */
interface ConstraintViolationInterface
{
}
diff --git a/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub b/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub
index 423bdfb0..00a2a9d5 100644
--- a/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub
+++ b/stubs/Symfony/Component/Validator/ConstraintViolationListInterface.stub
@@ -3,8 +3,11 @@
namespace Symfony\Component\Validator;
/**
+ * @extends \ArrayAccess
* @extends \Traversable
+ *
+ * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1.
*/
-interface ConstraintViolationListInterface extends \Traversable
+interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess
{
}
diff --git a/stubs/Symfony/Component/Validator/Constraints/Composite.stub b/stubs/Symfony/Component/Validator/Constraints/Composite.stub
new file mode 100644
index 00000000..8344ea94
--- /dev/null
+++ b/stubs/Symfony/Component/Validator/Constraints/Composite.stub
@@ -0,0 +1,9 @@
+ $options
+ * @return array
+ */
+ abstract protected function getConstraints(array $options): array;
+}
diff --git a/stubs/Symfony/Contracts/Cache/CacheInterface.stub b/stubs/Symfony/Contracts/Cache/CacheInterface.stub
new file mode 100644
index 00000000..a361ead4
--- /dev/null
+++ b/stubs/Symfony/Contracts/Cache/CacheInterface.stub
@@ -0,0 +1,19 @@
+|callable(\Symfony\Contracts\Cache\ItemInterface, bool): T $callback
+ * @param array $metadata
+ * @return T
+ *
+ * @throws InvalidArgumentException
+ */
+ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null);
+}
diff --git a/stubs/Symfony/Contracts/Cache/CallbackInterface.stub b/stubs/Symfony/Contracts/Cache/CallbackInterface.stub
new file mode 100644
index 00000000..9b5a6e1a
--- /dev/null
+++ b/stubs/Symfony/Contracts/Cache/CallbackInterface.stub
@@ -0,0 +1,16 @@
+getByType(CallMethodsRule::class);
}
diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php
index 81925291..bbecb2e8 100644
--- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php
+++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php
@@ -5,6 +5,8 @@
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
+use function class_exists;
+use function interface_exists;
/**
* @extends RuleTestCase
@@ -26,7 +28,7 @@ public function testGetPrivateService(): void
[
__DIR__ . '/ExampleController.php',
],
- []
+ [],
);
}
@@ -39,7 +41,7 @@ public function testGetPrivateServiceInAbstractController(): void
[
__DIR__ . '/ExampleAbstractController.php',
],
- []
+ [],
);
}
@@ -59,7 +61,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void
__DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php',
],
- []
+ [],
);
}
@@ -79,7 +81,7 @@ public function testGetPrivateServiceInServiceSubscriber(): void
__DIR__ . '/ExampleServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleServiceSubscriberFromLegacyController.php',
],
- []
+ [],
);
}
diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php
index b3a8fa31..dfa3d2b7 100644
--- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php
+++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php
@@ -5,6 +5,8 @@
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
+use function class_exists;
+use function interface_exists;
/**
* @extends RuleTestCase
@@ -29,9 +31,9 @@ public function testGetPrivateService(): void
[
[
'Service "private" is private.',
- 12,
+ 13,
],
- ]
+ ],
);
}
@@ -51,7 +53,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void
__DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php',
],
- []
+ [],
);
}
@@ -71,7 +73,7 @@ public function testGetPrivateServiceInServiceSubscriber(): void
__DIR__ . '/ExampleServiceSubscriberFromAbstractController.php',
__DIR__ . '/ExampleServiceSubscriberFromLegacyController.php',
],
- []
+ [],
);
}
diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php
index 33e3015d..8d70f1c3 100644
--- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php
+++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php
@@ -2,11 +2,14 @@
namespace PHPStan\Rules\Symfony;
-use PhpParser\PrettyPrinter\Standard;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
+use PHPStan\Type\MethodTypeSpecifyingExtension;
use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use function class_exists;
/**
* @extends RuleTestCase
@@ -16,16 +19,16 @@ final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase
protected function getRule(): Rule
{
- return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), new Standard());
+ return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), self::getContainer()->getByType(Printer::class));
}
/**
- * @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
+ * @return MethodTypeSpecifyingExtension[]
*/
protected function getMethodTypeSpecifyingExtensions(): array
{
return [
- new ServiceTypeSpecifyingExtension('Symfony\Bundle\FrameworkBundle\Controller\Controller', new Standard()),
+ new ServiceTypeSpecifyingExtension(AbstractController::class, self::getContainer()->getByType(Printer::class)),
];
}
@@ -38,7 +41,7 @@ public function testGetPrivateService(): void
[
__DIR__ . '/ExampleController.php',
],
- []
+ [],
);
}
@@ -52,7 +55,7 @@ public function testGetPrivateServiceInAbstractController(): void
[
__DIR__ . '/ExampleAbstractController.php',
],
- []
+ [],
);
}
diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php
index 297158a6..c975750f 100644
--- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php
+++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php
@@ -2,10 +2,12 @@
namespace PHPStan\Rules\Symfony;
-use PhpParser\PrettyPrinter\Standard;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\XmlServiceMapFactory;
use PHPStan\Testing\RuleTestCase;
+use function class_exists;
+use function interface_exists;
/**
* @extends RuleTestCase
@@ -15,7 +17,7 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase
protected function getRule(): Rule
{
- return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard());
+ return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), self::getContainer()->getByType(Printer::class));
}
public function testGetPrivateService(): void
@@ -30,9 +32,9 @@ public function testGetPrivateService(): void
[
[
'Service "unknown" is not registered in the container.',
- 24,
+ 25,
],
- ]
+ ],
);
}
@@ -49,9 +51,9 @@ public function testGetPrivateServiceInAbstractController(): void
[
[
'Service "unknown" is not registered in the container.',
- 24,
+ 25,
],
- ]
+ ],
);
}
@@ -65,7 +67,7 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void
[
__DIR__ . '/ExampleServiceSubscriber.php',
],
- []
+ [],
);
}
diff --git a/tests/Rules/Symfony/ExampleAbstractController.php b/tests/Rules/Symfony/ExampleAbstractController.php
index 4f0d2abd..22e5900e 100644
--- a/tests/Rules/Symfony/ExampleAbstractController.php
+++ b/tests/Rules/Symfony/ExampleAbstractController.php
@@ -3,6 +3,7 @@
namespace PHPStan\Rules\Symfony;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Bundle\FrameworkBundle\Test\TestContainer;
final class ExampleAbstractController extends AbstractController
{
@@ -14,7 +15,7 @@ public function privateService(): void
public function privateServiceInTestContainer(): void
{
- /** @var \Symfony\Bundle\FrameworkBundle\Test\TestContainer $container */
+ /** @var TestContainer $container */
$container = doFoo();
$container->get('private');
}
diff --git a/tests/Rules/Symfony/ExampleController.php b/tests/Rules/Symfony/ExampleController.php
index edbdaaf5..5b9e1ca4 100644
--- a/tests/Rules/Symfony/ExampleController.php
+++ b/tests/Rules/Symfony/ExampleController.php
@@ -3,6 +3,7 @@
namespace PHPStan\Rules\Symfony;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Bundle\FrameworkBundle\Test\TestContainer;
final class ExampleController extends Controller
{
@@ -14,7 +15,7 @@ public function privateService(): void
public function privateServiceInTestContainer(): void
{
- /** @var \Symfony\Bundle\FrameworkBundle\Test\TestContainer $container */
+ /** @var TestContainer $container */
$container = doFoo();
$container->get('private');
}
diff --git a/tests/Rules/Symfony/ExampleServiceSubscriber.php b/tests/Rules/Symfony/ExampleServiceSubscriber.php
index 3d7f294d..ec9c966d 100644
--- a/tests/Rules/Symfony/ExampleServiceSubscriber.php
+++ b/tests/Rules/Symfony/ExampleServiceSubscriber.php
@@ -3,13 +3,13 @@
namespace PHPStan\Rules\Symfony;
use Psr\Container\ContainerInterface;
+use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
final class ExampleServiceSubscriber implements ServiceSubscriberInterface
{
- /** @var ContainerInterface */
- private $locator;
+ private ContainerInterface $locator;
public function __construct(ContainerInterface $locator)
{
@@ -24,7 +24,7 @@ public function privateService(): void
public function containerParameter(): void
{
- /** @var \Symfony\Component\DependencyInjection\ParameterBag\ContainerBag $containerBag */
+ /** @var ContainerBag $containerBag */
$containerBag = doFoo();
$containerBag->get('parameter_name');
}
diff --git a/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php
index 5e4f2ee6..bc6a6563 100644
--- a/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php
+++ b/tests/Rules/Symfony/InvalidArgumentDefaultValueRuleTest.php
@@ -35,7 +35,7 @@ public function testGetArgument(): void
'Parameter #4 $default of method Symfony\Component\Console\Command\Command::addArgument() expects array|null, array given.',
25,
],
- ]
+ ],
);
}
diff --git a/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php b/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php
index 3e5e03d4..2dcbbcd1 100644
--- a/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php
+++ b/tests/Rules/Symfony/InvalidOptionDefaultValueRuleTest.php
@@ -27,7 +27,7 @@ public function testGetArgument(): void
'Parameter #5 $default of method Symfony\Component\Console\Command\Command::addOption() expects array|null, array given.',
29,
],
- ]
+ ],
);
}
diff --git a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php
index b3f9e56e..d9970ef6 100644
--- a/tests/Rules/Symfony/UndefinedArgumentRuleTest.php
+++ b/tests/Rules/Symfony/UndefinedArgumentRuleTest.php
@@ -2,7 +2,7 @@
namespace PHPStan\Rules\Symfony;
-use PhpParser\PrettyPrinter\Standard;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Testing\RuleTestCase;
@@ -15,7 +15,7 @@ final class UndefinedArgumentRuleTest extends RuleTestCase
protected function getRule(): Rule
{
- return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), new Standard());
+ return new UndefinedArgumentRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class));
}
public function testGetArgument(): void
@@ -29,7 +29,7 @@ public function testGetArgument(): void
'Command "example-rule" does not define argument "undefined".',
42,
],
- ]
+ ],
);
}
diff --git a/tests/Rules/Symfony/UndefinedOptionRuleTest.php b/tests/Rules/Symfony/UndefinedOptionRuleTest.php
index 32ebe773..7f759213 100644
--- a/tests/Rules/Symfony/UndefinedOptionRuleTest.php
+++ b/tests/Rules/Symfony/UndefinedOptionRuleTest.php
@@ -2,7 +2,7 @@
namespace PHPStan\Rules\Symfony;
-use PhpParser\PrettyPrinter\Standard;
+use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Symfony\ConsoleApplicationResolver;
use PHPStan\Testing\RuleTestCase;
@@ -15,7 +15,7 @@ final class UndefinedOptionRuleTest extends RuleTestCase
protected function getRule(): Rule
{
- return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), new Standard());
+ return new UndefinedOptionRule(new ConsoleApplicationResolver(__DIR__ . '/console_application_loader.php'), self::getContainer()->getByType(Printer::class));
}
public function testGetArgument(): void
@@ -29,7 +29,7 @@ public function testGetArgument(): void
'Command "example-rule" does not define option "bbb".',
49,
],
- ]
+ ],
);
}
diff --git a/tests/Symfony/DefaultParameterMapTest.php b/tests/Symfony/DefaultParameterMapTest.php
index 0b97d078..018a68a9 100644
--- a/tests/Symfony/DefaultParameterMapTest.php
+++ b/tests/Symfony/DefaultParameterMapTest.php
@@ -26,19 +26,19 @@ public function testGetParameterEscapedPath(): void
}
/**
- * @return \Iterator
+ * @return Iterator
*/
public function getParameterProvider(): Iterator
{
yield [
'unknown',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNull($parameter);
},
];
yield [
'app.string',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.string', $parameter->getKey());
self::assertSame('abcdef', $parameter->getValue());
@@ -46,7 +46,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.int',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.int', $parameter->getKey());
self::assertSame(123, $parameter->getValue());
@@ -54,7 +54,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.int_as_string',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.int_as_string', $parameter->getKey());
self::assertSame('123', $parameter->getValue());
@@ -62,7 +62,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.float',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.float', $parameter->getKey());
self::assertSame(123.45, $parameter->getValue());
@@ -70,7 +70,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.float_as_string',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.float_as_string', $parameter->getKey());
self::assertSame('123.45', $parameter->getValue());
@@ -78,7 +78,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.boolean',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.boolean', $parameter->getKey());
self::assertTrue($parameter->getValue());
@@ -86,7 +86,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.boolean_as_string',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.boolean_as_string', $parameter->getKey());
self::assertSame('true', $parameter->getValue());
@@ -94,7 +94,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.list',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.list', $parameter->getKey());
self::assertEquals(['en', 'es', 'fr'], $parameter->getValue());
@@ -102,7 +102,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.list_of_list',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.list_of_list', $parameter->getKey());
self::assertEquals([
@@ -113,7 +113,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.map',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.map', $parameter->getKey());
self::assertEquals([
@@ -125,7 +125,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.binary',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.binary', $parameter->getKey());
self::assertSame('This is a Bell char ', $parameter->getValue());
@@ -133,7 +133,7 @@ function (?Parameter $parameter): void {
];
yield [
'app.constant',
- function (?Parameter $parameter): void {
+ static function (?Parameter $parameter): void {
self::assertNotNull($parameter);
self::assertSame('app.constant', $parameter->getKey());
self::assertSame('Y-m-d\TH:i:sP', $parameter->getValue());
diff --git a/tests/Symfony/DefaultServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php
index 8616ffaf..b43bee49 100644
--- a/tests/Symfony/DefaultServiceMapTest.php
+++ b/tests/Symfony/DefaultServiceMapTest.php
@@ -26,19 +26,19 @@ public function testGetContainerEscapedPath(): void
}
/**
- * @return \Iterator
+ * @return Iterator
*/
public function getServiceProvider(): Iterator
{
yield [
'unknown',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNull($service);
},
];
yield [
'withoutClass',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('withoutClass', $service->getId());
self::assertNull($service->getClass());
@@ -49,7 +49,7 @@ function (?Service $service): void {
];
yield [
'withClass',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('withClass', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -60,7 +60,7 @@ function (?Service $service): void {
];
yield [
'withoutPublic',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('withoutPublic', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -71,7 +71,7 @@ function (?Service $service): void {
];
yield [
'publicNotTrue',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('publicNotTrue', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -82,7 +82,7 @@ function (?Service $service): void {
];
yield [
'public',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('public', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -93,7 +93,7 @@ function (?Service $service): void {
];
yield [
'synthetic',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('synthetic', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -104,7 +104,7 @@ function (?Service $service): void {
];
yield [
'alias',
- function (?Service $service): void {
+ static function (?Service $service): void {
self::assertNotNull($service);
self::assertSame('alias', $service->getId());
self::assertSame('Foo', $service->getClass());
@@ -113,6 +113,17 @@ function (?Service $service): void {
self::assertSame('withClass', $service->getAlias());
},
];
+ yield [
+ 'aliasForInlined',
+ static function (?Service $service): void {
+ self::assertNotNull($service);
+ self::assertSame('aliasForInlined', $service->getId());
+ self::assertNull($service->getClass());
+ self::assertFalse($service->isPublic());
+ self::assertFalse($service->isSynthetic());
+ self::assertSame('inlined', $service->getAlias());
+ },
+ ];
}
}
diff --git a/tests/Symfony/RequiredAutowiringExtensionTest.php b/tests/Symfony/RequiredAutowiringExtensionTest.php
new file mode 100644
index 00000000..93fb3822
--- /dev/null
+++ b/tests/Symfony/RequiredAutowiringExtensionTest.php
@@ -0,0 +1,65 @@
+
+ */
+final class RequiredAutowiringExtensionTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ $container = self::getContainer();
+ $container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG);
+
+ return $container->getByType(UninitializedPropertyRule::class);
+ }
+
+ public function testRequiredAnnotations(): void
+ {
+ $this->analyse([__DIR__ . '/data/required-annotations.php'], [
+ [
+ 'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $three. Give it default value or assign it in the constructor.',
+ 12,
+ ],
+ [
+ 'Class RequiredAnnotationTest\TestAnnotations has an uninitialized property $four. Give it default value or assign it in the constructor.',
+ 14,
+ ],
+ ]);
+ }
+
+ public function testRequiredAttributes(): void
+ {
+ if (!class_exists(Required::class)) {
+ self::markTestSkipped('Required symfony/service-contracts@3.2.1 or higher is not installed');
+ }
+
+ $this->analyse([__DIR__ . '/data/required-attributes.php'], [
+ [
+ 'Class RequiredAttributesTest\TestAttributes has an uninitialized property $three. Give it default value or assign it in the constructor.',
+ 14,
+ ],
+ [
+ 'Class RequiredAttributesTest\TestAttributes has an uninitialized property $four. Give it default value or assign it in the constructor.',
+ 16,
+ ],
+ ]);
+ }
+
+ public static function getAdditionalConfigFiles(): array
+ {
+ return [
+ __DIR__ . '/required-autowiring-config.neon',
+ ];
+ }
+
+}
diff --git a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php
new file mode 100644
index 00000000..f5c8503f
--- /dev/null
+++ b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php
@@ -0,0 +1,294 @@
+ $sameHashContents
+ * @param ContainerContents $invalidatingContent
+ *
+ * @dataProvider provideContainerHashIsCalculatedCorrectlyCases
+ */
+ public function testContainerHashIsCalculatedCorrectly(
+ array $sameHashContents,
+ array $invalidatingContent
+ ): void
+ {
+ $hash = null;
+
+ self::assertGreaterThan(0, count($sameHashContents));
+
+ foreach ($sameHashContents as $content) {
+ $currentHash = (new SymfonyContainerResultCacheMetaExtension(
+ $content['parameters'] ?? new DefaultParameterMap([]),
+ $content['services'] ?? new DefaultServiceMap([]),
+ ))->getHash();
+
+ if ($hash === null) {
+ $hash = $currentHash;
+ } else {
+ self::assertSame($hash, $currentHash);
+ }
+ }
+
+ self::assertNotSame(
+ $hash,
+ (new SymfonyContainerResultCacheMetaExtension(
+ $invalidatingContent['parameters'] ?? new DefaultParameterMap([]),
+ $invalidatingContent['services'] ?? new DefaultServiceMap([]),
+ ))->getHash(),
+ );
+ }
+
+ /**
+ * @return iterable, ContainerContents}>
+ */
+ public static function provideContainerHashIsCalculatedCorrectlyCases(): iterable
+ {
+ yield 'service "class" changes' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'Bar', true, false, null),
+ ]),
+ ],
+ // Swapping services order in XML file does not affect the calculated hash
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Bar', 'Bar', true, false, null),
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'BarAdapter', true, false, null),
+ ]),
+ ],
+ ];
+
+ yield 'service visibility changes' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', false, false, null),
+ ]),
+ ],
+ ];
+
+ yield 'service syntheticity changes' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, true, null),
+ ]),
+ ],
+ ];
+
+ yield 'service alias changes' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'Bar', true, false, null),
+ new Service('Baz', null, true, false, 'Foo'),
+ ]),
+ ],
+ // Swapping services order in XML file does not affect the calculated hash
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Baz', null, true, false, 'Foo'),
+ new Service('Bar', 'Bar', true, false, null),
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'Bar', true, false, null),
+ new Service('Baz', null, true, false, 'Bar'),
+ ]),
+ ],
+ ];
+
+ yield 'service tag attributes changes' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ new ServiceTag('foo.baz', ['baz' => 'baz']),
+ ]),
+ ]),
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.baz', ['baz' => 'baz']),
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ ]),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ new ServiceTag('foo.baz', ['baz' => 'buzz']),
+ ]),
+ ]),
+ ],
+ ];
+
+ yield 'service tag added' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ ]),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ new ServiceTag('foo.baz', ['baz' => 'baz']),
+ ]),
+ ]),
+ ],
+ ];
+
+ yield 'service tag removed' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ new ServiceTag('foo.baz', ['baz' => 'baz']),
+ ]),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null, [
+ new ServiceTag('foo.bar', ['baz' => 'bar']),
+ ]),
+ ]),
+ ],
+ ];
+
+ yield 'new service added' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'Bar', true, false, null),
+ ]),
+ ],
+ ];
+
+ yield 'service removed' => [
+ [
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ new Service('Bar', 'Bar', true, false, null),
+ ]),
+ ],
+ ],
+ [
+ 'services' => new DefaultServiceMap([
+ new Service('Foo', 'Foo', true, false, null),
+ ]),
+ ],
+ ];
+
+ yield 'parameter value changes' => [
+ [
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ new Parameter('bar', 'bar'),
+ ]),
+ ],
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('bar', 'bar'),
+ new Parameter('foo', 'foo'),
+ ]),
+ ],
+ ],
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ new Parameter('bar', 'buzz'),
+ ]),
+ ],
+ ];
+
+ yield 'new parameter added' => [
+ [
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ ]),
+ ],
+ ],
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ new Parameter('bar', 'bar'),
+ ]),
+ ],
+ ];
+
+ yield 'parameter removed' => [
+ [
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ new Parameter('bar', 'bar'),
+ ]),
+ ],
+ ],
+ [
+ 'parameters' => new DefaultParameterMap([
+ new Parameter('foo', 'foo'),
+ ]),
+ ],
+ ];
+ }
+
+}
diff --git a/tests/Symfony/config.neon b/tests/Symfony/config.neon
deleted file mode 100644
index e4f42c64..00000000
--- a/tests/Symfony/config.neon
+++ /dev/null
@@ -1,3 +0,0 @@
-parameters:
- symfony:
- container_xml_path: container.xml
diff --git a/tests/Symfony/container.xml b/tests/Symfony/container.xml
index 4be3bd98..f456ab51 100644
--- a/tests/Symfony/container.xml
+++ b/tests/Symfony/container.xml
@@ -41,5 +41,7 @@
+
+
diff --git a/tests/Symfony/data/required-annotations.php b/tests/Symfony/data/required-annotations.php
new file mode 100644
index 00000000..e2085ab3
--- /dev/null
+++ b/tests/Symfony/data/required-annotations.php
@@ -0,0 +1,38 @@
+= 7.4
+
+namespace RequiredAnnotationTest;
+
+class TestAnnotations
+{
+ /** @required */
+ public string $one;
+
+ private string $two;
+
+ public string $three;
+
+ private string $four;
+
+ /**
+ * @required
+ */
+ public function setTwo(int $two): void
+ {
+ $this->two = $two;
+ }
+
+ public function getTwo(): int
+ {
+ return $this->two;
+ }
+
+ public function setFour(int $four): void
+ {
+ $this->four = $four;
+ }
+
+ public function getFour(): int
+ {
+ return $this->four;
+ }
+}
diff --git a/tests/Symfony/data/required-attributes.php b/tests/Symfony/data/required-attributes.php
new file mode 100644
index 00000000..d847d276
--- /dev/null
+++ b/tests/Symfony/data/required-attributes.php
@@ -0,0 +1,38 @@
+= 8.0
+
+namespace RequiredAttributesTest;
+
+use Symfony\Contracts\Service\Attribute\Required;
+
+class TestAttributes
+{
+ #[Required]
+ public string $one;
+
+ private string $two;
+
+ public string $three;
+
+ private string $four;
+
+ #[Required]
+ public function setTwo(int $two): void
+ {
+ $this->two = $two;
+ }
+
+ public function getTwo(): int
+ {
+ return $this->two;
+ }
+
+ public function setFour(int $four): void
+ {
+ $this->four = $four;
+ }
+
+ public function getFour(): int
+ {
+ return $this->four;
+ }
+}
diff --git a/tests/Symfony/required-autowiring-config.neon b/tests/Symfony/required-autowiring-config.neon
new file mode 100644
index 00000000..3ff4183c
--- /dev/null
+++ b/tests/Symfony/required-autowiring-config.neon
@@ -0,0 +1,6 @@
+services:
+ -
+ class: PHPStan\Symfony\RequiredAutowiringExtension
+ tags:
+ - phpstan.properties.readWriteExtension
+ - phpstan.additionalConstructorsExtension
diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php
index 5a49366b..40420be0 100644
--- a/tests/Type/Symfony/ExtensionTest.php
+++ b/tests/Type/Symfony/ExtensionTest.php
@@ -5,6 +5,8 @@
use PHPStan\Testing\TypeInferenceTestCase;
use ReflectionMethod;
use Symfony\Component\HttpFoundation\Request;
+use function class_exists;
+use function strpos;
class ExtensionTest extends TypeInferenceTestCase
{
@@ -12,18 +14,22 @@ class ExtensionTest extends TypeInferenceTestCase
/** @return mixed[] */
public function dataFileAsserts(): iterable
{
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/messenger_handle_trait.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/envelope_all.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/header_bag_get.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/response_header_bag_get_cookies.php');
if (class_exists('Symfony\Component\HttpFoundation\InputBag')) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/input_bag.php');
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/tree_builder.php');
+
yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleBaseCommand.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionCommand.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionLazyCommand.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/kernel_interface.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/property_accessor.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/request_get_content.php');
$ref = new ReflectionMethod(Request::class, 'getSession');
@@ -49,12 +55,23 @@ public function dataFileAsserts(): iterable
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/denormalizer.php');
+
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/FormInterface_getErrors.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/cache.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/form_data_type.php');
+
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration/WithConfigurationExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/without-configuration/WithoutConfigurationExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/anonymous/AnonymousExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/ignore-implemented/IgnoreImplementedExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/multiple-types/MultipleTypes.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor/WithConfigurationWithConstructorExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-optional-params/WithConfigurationWithConstructorOptionalParamsExtension.php');
+ yield from $this->gatherAssertTypes(__DIR__ . '/data/extension/with-configuration-with-constructor-required-params/WithConfigurationWithConstructorRequiredParamsExtension.php');
}
/**
* @dataProvider dataFileAsserts
- * @param string $assertType
- * @param string $file
* @param mixed ...$args
*/
public function testFileAsserts(
@@ -71,6 +88,7 @@ public static function getAdditionalConfigFiles(): array
return [
__DIR__ . '/../../../extension.neon',
__DIR__ . '/extension-test.neon',
+ 'phar://' . __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon',
];
}
diff --git a/tests/Type/Symfony/ExtensionTestWithoutContainer.php b/tests/Type/Symfony/ExtensionTestWithoutContainer.php
index 997b7e02..fd1785c7 100644
--- a/tests/Type/Symfony/ExtensionTestWithoutContainer.php
+++ b/tests/Type/Symfony/ExtensionTestWithoutContainer.php
@@ -3,6 +3,7 @@
namespace PHPStan\Type\Symfony;
use PHPStan\Testing\TypeInferenceTestCase;
+use function class_exists;
class ExtensionTestWithoutContainer extends TypeInferenceTestCase
{
@@ -30,8 +31,6 @@ public function dataAbstractController(): iterable
/**
* @dataProvider dataExampleController
* @dataProvider dataAbstractController
- * @param string $assertType
- * @param string $file
* @param mixed ...$args
*/
public function testFileAsserts(
diff --git a/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php
index ab247b9f..bde62b57 100644
--- a/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php
+++ b/tests/Type/Symfony/ImpossibleCheckTypeMethodCallRuleTest.php
@@ -4,11 +4,12 @@
use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule;
use PHPStan\Rules\Rule;
+use PHPStan\Testing\RuleTestCase;
/**
- * @extends \PHPStan\Testing\RuleTestCase
+ * @extends RuleTestCase
*/
-class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase
+class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase
{
protected function getRule(): Rule
diff --git a/tests/Type/Symfony/console_application_loader.php b/tests/Type/Symfony/console_application_loader.php
index 0ef2e0ae..fb5459b5 100644
--- a/tests/Type/Symfony/console_application_loader.php
+++ b/tests/Type/Symfony/console_application_loader.php
@@ -15,9 +15,7 @@
$application->add(new ExampleOptionCommand());
if (class_exists(LazyCommand::class)) {
- $application->add(new LazyCommand('lazy-example-option', [], '', false, function () {
- return new ExampleOptionLazyCommand();
- }));
+ $application->add(new LazyCommand('lazy-example-option', [], '', false, static fn () => new ExampleOptionLazyCommand()));
} else {
$application->add(new ExampleOptionLazyCommand());
}
diff --git a/tests/Type/Symfony/container.xml b/tests/Type/Symfony/container.xml
index f4240c3b..16d4b7fe 100644
--- a/tests/Type/Symfony/container.xml
+++ b/tests/Type/Symfony/container.xml
@@ -1,18 +1,32 @@
+ Foo
abcdef
123
123
+ %env(int:APP_INT)%
123.45
123.45
+ %env(float:APP_FLOAT)%
true
true
+ %env(bool:APP_BOOL)%
en
es
fr
+
+ 123
+ 456
+ 789
+
+
+ %env(int:APP_INT)%
+ %env(int:APP_INT)%
+ %env(int:APP_INT)%
+
the name
@@ -23,11 +37,310 @@
another value
+
+
+ the name
+ the value
+
+
+ 12
+ 32
+
+
+
+
+ the name
+ the value
+
+
+ another name
+ another value
+
+
+
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+
+
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+
+
+
+ %env(string:APP_STRING)%
+
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+ %env(string:APP_STRING)%
+
+
+
value of a
value of b
value of c
+
+ v1
+ v2
+ v3
+ v4
+ v5
+ v6
+ v7
+ v8
+ v9
+ v10
+ v11
+ v12
+ v13
+ v14
+ v15
+ v16
+ v17
+ v18
+ v19
+ v20
+ v21
+ v22
+ v23
+ v24
+ v25
+ v26
+ v27
+ v28
+ v29
+ v30
+ v31
+ v32
+ v33
+ v34
+ v35
+ v36
+ v37
+ v38
+ v39
+ v40
+ v41
+ v42
+ v43
+ v44
+ v45
+ v46
+ v47
+ v48
+ v49
+ v50
+ v51
+ v52
+ v53
+ v54
+ v55
+ v56
+ v57
+ v58
+ v59
+ v60
+ v61
+ v62
+ v63
+ v64
+ v65
+ v66
+ v67
+ v68
+ v69
+ v70
+ v71
+ v72
+ v73
+ v74
+ v75
+ v76
+ v77
+ v78
+ v79
+ v80
+ v81
+ v82
+ v83
+ v84
+ v85
+ v86
+ v87
+ v88
+ v89
+ v90
+ v91
+ v92
+ v93
+ v94
+ v95
+ v96
+ v97
+ v98
+ v99
+ v100
+ v101
+ v102
+ v103
+ v104
+ v105
+ v106
+ v107
+ v108
+ v109
+ v110
+ v111
+ v112
+ v113
+ v114
+ v115
+ v116
+ v117
+ v118
+ v119
+ v120
+ v121
+ v122
+ v123
+ v124
+ v125
+ v126
+ v127
+ v128
+ v129
+ v130
+ v131
+ v132
+ v133
+ v134
+ v135
+ v136
+ v137
+ v138
+ v139
+ v140
+ v141
+ v142
+ v143
+ v144
+ v145
+ v146
+ v147
+ v148
+ v149
+ v150
+ v151
+ v152
+ v153
+ v154
+ v155
+ v156
+ v157
+ v158
+ v159
+ v160
+ v161
+ v162
+ v163
+ v164
+ v165
+ v166
+ v167
+ v168
+ v169
+ v170
+ v171
+ v172
+ v173
+ v174
+ v175
+ v176
+ v177
+ v178
+ v179
+ v180
+ v181
+ v182
+ v183
+ v184
+ v185
+ v186
+ v187
+ v188
+ v189
+ v190
+ v191
+ v192
+ v193
+ v194
+ v195
+ v196
+ v197
+ v198
+ v199
+ v200
+ v201
+ v202
+ v203
+ v204
+ v205
+ v206
+ v207
+ v208
+ v209
+ v210
+ v211
+ v212
+ v213
+ v214
+ v215
+ v216
+ v217
+ v218
+ v219
+ v220
+ v221
+ v222
+ v223
+ v224
+ v225
+ v226
+ v227
+ v228
+ v229
+ v230
+ v231
+ v232
+ v233
+ v234
+ v235
+ v236
+ v237
+ v238
+ v239
+ v240
+ v241
+ v242
+ v243
+ v244
+ v245
+ v246
+ v247
+ v248
+ v249
+ v250
+ v251
+ v252
+ v253
+ v254
+ v255
+ v256
+ v257
+
VGhpcyBpcyBhIEJlbGwgY2hhciAH
Y-m-d\TH:i:sP
@@ -38,6 +351,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Type/Symfony/data/ExampleAbstractController.php b/tests/Type/Symfony/data/ExampleAbstractController.php
index 4dbaffa7..53b38066 100644
--- a/tests/Type/Symfony/data/ExampleAbstractController.php
+++ b/tests/Type/Symfony/data/ExampleAbstractController.php
@@ -13,6 +13,8 @@ final class ExampleAbstractController extends AbstractController
public function services(): void
{
assertType('Foo', $this->get('foo'));
+ assertType('Foo', $this->get('parameterised_foo'));
+ assertType('Foo\Bar', $this->get('parameterised_bar'));
assertType('Synthetic', $this->get('synthetic'));
assertType('object', $this->get('bar'));
assertType('object', $this->get(doFoo()));
@@ -39,6 +41,9 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType("string", $container->getParameter('app.int_as_string'));
assertType("string", $parameterBag->get('app.int_as_string'));
assertType("string", $this->getParameter('app.int_as_string'));
+ assertType('int', $container->getParameter('app.int_as_processor'));
+ assertType('int', $parameterBag->get('app.int_as_processor'));
+ assertType('int', $this->getParameter('app.int_as_processor'));
assertType('float', $container->getParameter('app.float'));
assertType('float', $parameterBag->get('app.float'));
assertType('float', $this->getParameter('app.float'));
@@ -54,12 +59,28 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType("array", $container->getParameter('app.list'));
assertType("array", $parameterBag->get('app.list'));
assertType("array", $this->getParameter('app.list'));
- assertType("array>", $container->getParameter('app.list_of_list'));
- assertType("array>", $parameterBag->get('app.list_of_list'));
- assertType("array>", $this->getParameter('app.list_of_list'));
- assertType("array", $container->getParameter('app.map'));
- assertType("array", $parameterBag->get('app.map'));
- assertType("array", $this->getParameter('app.map'));
+ assertType("array", $container->getParameter('app.list_of_int'));
+ assertType("array", $parameterBag->get('app.list_of_int'));
+ assertType("array", $this->getParameter('app.list_of_int'));
+ assertType("array", $container->getParameter('app.list_of_int_as_processor'));
+ assertType("array", $parameterBag->get('app.list_of_int_as_processor'));
+ assertType("array", $this->getParameter('app.list_of_int_as_processor'));
+ assertType("array", $container->getParameter('app.list_of_list'));
+ assertType("array", $parameterBag->get('app.list_of_list'));
+ assertType("array", $this->getParameter('app.list_of_list'));
+ assertType("array", $container->getParameter('app.list_of_different_list'));
+ assertType("array", $parameterBag->get('app.list_of_different_list'));
+ assertType("array", $this->getParameter('app.list_of_different_list'));
+ assertType("array", $container->getParameter('app.array_of_list'));
+ assertType("array", $parameterBag->get('app.array_of_list'));
+ assertType("array", $this->getParameter('app.array_of_list'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $container->getParameter('app.list_of_things'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $parameterBag->get('app.list_of_things'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $this->getParameter('app.list_of_things'));
+ assertType("array{a: string, b: string, c: string}", $container->getParameter('app.map'));
+ assertType("array{a: string, b: string, c: string}", $parameterBag->get('app.map'));
+ assertType("array{a: string, b: string, c: string}", $this->getParameter('app.map'));
+ assertType("non-falsy-string", implode(',', $this->getParameter('app.hugemap')));
assertType("string", $container->getParameter('app.binary'));
assertType("string", $parameterBag->get('app.binary'));
assertType("string", $this->getParameter('app.binary'));
@@ -77,6 +98,8 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('true', $parameterBag->has('app.int'));
assertType('true', $container->hasParameter('app.int_as_string'));
assertType('true', $parameterBag->has('app.int_as_string'));
+ assertType('true', $container->hasParameter('app.int_as_processor'));
+ assertType('true', $parameterBag->has('app.int_as_processor'));
assertType('true', $container->hasParameter('app.float'));
assertType('true', $parameterBag->has('app.float'));
assertType('true', $container->hasParameter('app.float_as_string'));
diff --git a/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php
index 7ca06775..edc6438a 100644
--- a/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php
+++ b/tests/Type/Symfony/data/ExampleAbstractControllerWithoutContainer.php
@@ -38,24 +38,36 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.float'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.float'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.float'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.list'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.list'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.list'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.list_of_list'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.list_of_list'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.list_of_list'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.array_of_list'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.array_of_list'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.array_of_list'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.map'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.map'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.map'));
@@ -74,14 +86,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('bool', $parameterBag->has('app.int'));
assertType('bool', $container->hasParameter('app.int_as_string'));
assertType('bool', $parameterBag->has('app.int_as_string'));
+ assertType('bool', $container->hasParameter('app.int_as_processor'));
+ assertType('bool', $parameterBag->has('app.int_as_processor'));
assertType('bool', $container->hasParameter('app.float'));
assertType('bool', $parameterBag->has('app.float'));
assertType('bool', $container->hasParameter('app.float_as_string'));
assertType('bool', $parameterBag->has('app.float_as_string'));
+ assertType('bool', $container->hasParameter('app.float_as_processor'));
+ assertType('bool', $parameterBag->has('app.float_as_processor'));
assertType('bool', $container->hasParameter('app.boolean'));
assertType('bool', $parameterBag->has('app.boolean'));
assertType('bool', $container->hasParameter('app.boolean_as_string'));
assertType('bool', $parameterBag->has('app.boolean_as_string'));
+ assertType('bool', $container->hasParameter('app.boolean_as_processor'));
+ assertType('bool', $parameterBag->has('app.boolean_as_processor'));
assertType('bool', $container->hasParameter('app.list'));
assertType('bool', $parameterBag->has('app.list'));
assertType('bool', $container->hasParameter('app.list_of_list'));
diff --git a/tests/Type/Symfony/data/ExampleBaseCommand.php b/tests/Type/Symfony/data/ExampleBaseCommand.php
index a4493b2c..0376429f 100644
--- a/tests/Type/Symfony/data/ExampleBaseCommand.php
+++ b/tests/Type/Symfony/data/ExampleBaseCommand.php
@@ -3,6 +3,7 @@
namespace PHPStan\Type\Symfony;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function PHPStan\Testing\assertType;
@@ -14,17 +15,53 @@ protected function configure(): void
{
parent::configure();
+ $this->addArgument('required', InputArgument::REQUIRED);
$this->addArgument('base');
}
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ assertType('bool', $input->hasArgument('command'));
+ assertType('string|null', $input->getArgument('command'));
+
+ assertType('string|null', $input->getArgument('base'));
+ assertType('string', $input->getArgument('aaa'));
+ assertType('string', $input->getArgument('bbb'));
+ assertType('string|null', $input->getArgument('required'));
+ assertType('array|string', $input->getArgument('diff'));
+ assertType('array', $input->getArgument('arr'));
+ assertType('string|null', $input->getArgument('both'));
+ assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question'));
+ }
+
+ protected function interact(InputInterface $input, OutputInterface $output): void
+ {
+ assertType('bool', $input->hasArgument('command'));
+ assertType('string|null', $input->getArgument('command'));
+
+ assertType('string|null', $input->getArgument('base'));
+ assertType('string', $input->getArgument('aaa'));
+ assertType('string', $input->getArgument('bbb'));
+ assertType('string|null', $input->getArgument('required'));
+ assertType('array|string', $input->getArgument('diff'));
+ assertType('array', $input->getArgument('arr'));
+ assertType('string|null', $input->getArgument('both'));
+ assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question'));
+ }
+
protected function execute(InputInterface $input, OutputInterface $output): int
{
+ assertType('true', $input->hasArgument('command'));
+ assertType('string', $input->getArgument('command'));
+
assertType('string|null', $input->getArgument('base'));
assertType('string', $input->getArgument('aaa'));
assertType('string', $input->getArgument('bbb'));
+ assertType('string', $input->getArgument('required'));
assertType('array|string', $input->getArgument('diff'));
assertType('array', $input->getArgument('arr'));
assertType('string|null', $input->getArgument('both'));
+ assertType('Symfony\Component\Console\Helper\QuestionHelper', $this->getHelper('question'));
}
}
diff --git a/tests/Type/Symfony/data/ExampleController.php b/tests/Type/Symfony/data/ExampleController.php
index 1fe8c983..c7563537 100644
--- a/tests/Type/Symfony/data/ExampleController.php
+++ b/tests/Type/Symfony/data/ExampleController.php
@@ -13,6 +13,8 @@ final class ExampleController extends Controller
public function services(): void
{
assertType('Foo', $this->get('foo'));
+ assertType('Foo', $this->get('parameterised_foo'));
+ assertType('Foo\Bar', $this->get('parameterised_bar'));
assertType('Synthetic', $this->get('synthetic'));
assertType('object', $this->get('bar'));
assertType('object', $this->get(doFoo()));
@@ -39,27 +41,45 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType("string", $container->getParameter('app.int_as_string'));
assertType("string", $parameterBag->get('app.int_as_string'));
assertType("string", $this->getParameter('app.int_as_string'));
+ assertType('int', $container->getParameter('app.int_as_processor'));
+ assertType('int', $parameterBag->get('app.int_as_processor'));
+ assertType('int', $this->getParameter('app.int_as_processor'));
assertType('float', $container->getParameter('app.float'));
assertType('float', $parameterBag->get('app.float'));
assertType('float', $this->getParameter('app.float'));
assertType("string", $container->getParameter('app.float_as_string'));
assertType("string", $parameterBag->get('app.float_as_string'));
assertType("string", $this->getParameter('app.float_as_string'));
+ assertType('float', $container->getParameter('app.float_as_processor'));
+ assertType('float', $parameterBag->get('app.float_as_processor'));
+ assertType('float', $this->getParameter('app.float_as_processor'));
assertType('bool', $container->getParameter('app.boolean'));
assertType('bool', $parameterBag->get('app.boolean'));
assertType('bool', $this->getParameter('app.boolean'));
assertType("string", $container->getParameter('app.boolean_as_string'));
assertType("string", $parameterBag->get('app.boolean_as_string'));
assertType("string", $this->getParameter('app.boolean_as_string'));
+ assertType('bool', $container->getParameter('app.boolean_as_processor'));
+ assertType('bool', $parameterBag->get('app.boolean_as_processor'));
+ assertType('bool', $this->getParameter('app.boolean_as_processor'));
assertType("array", $container->getParameter('app.list'));
assertType("array", $parameterBag->get('app.list'));
assertType("array", $this->getParameter('app.list'));
- assertType("array>", $container->getParameter('app.list_of_list'));
- assertType("array>", $parameterBag->get('app.list_of_list'));
- assertType("array>", $this->getParameter('app.list_of_list'));
- assertType("array", $container->getParameter('app.map'));
- assertType("array", $parameterBag->get('app.map'));
- assertType("array", $this->getParameter('app.map'));
+ assertType("array", $container->getParameter('app.list_of_list'));
+ assertType("array", $parameterBag->get('app.list_of_list'));
+ assertType("array", $this->getParameter('app.list_of_list'));
+ assertType("array", $container->getParameter('app.list_of_different_list'));
+ assertType("array", $parameterBag->get('app.list_of_different_list'));
+ assertType("array", $this->getParameter('app.list_of_different_list'));
+ assertType("array", $container->getParameter('app.array_of_list'));
+ assertType("array", $parameterBag->get('app.array_of_list'));
+ assertType("array", $this->getParameter('app.array_of_list'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $container->getParameter('app.list_of_things'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $parameterBag->get('app.list_of_things'));
+ assertType("array{url: string, endpoint: string, version: string, payment: array{default: array{username: string, password: string, signature: string}}, api: array{mode: string, default: array{username: string, password: string, signature: string}}}", $this->getParameter('app.list_of_things'));
+ assertType("array{a: string, b: string, c: string}", $container->getParameter('app.map'));
+ assertType("array{a: string, b: string, c: string}", $parameterBag->get('app.map'));
+ assertType("array{a: string, b: string, c: string}", $this->getParameter('app.map'));
assertType("string", $container->getParameter('app.binary'));
assertType("string", $parameterBag->get('app.binary'));
assertType("string", $this->getParameter('app.binary'));
@@ -75,14 +95,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('true', $parameterBag->has('app.int'));
assertType('true', $container->hasParameter('app.int_as_string'));
assertType('true', $parameterBag->has('app.int_as_string'));
+ assertType('true', $container->hasParameter('app.int_as_processor'));
+ assertType('true', $parameterBag->has('app.int_as_processor'));
assertType('true', $container->hasParameter('app.float'));
assertType('true', $parameterBag->has('app.float'));
assertType('true', $container->hasParameter('app.float_as_string'));
assertType('true', $parameterBag->has('app.float_as_string'));
+ assertType('true', $container->hasParameter('app.float_as_processor'));
+ assertType('true', $parameterBag->has('app.float_as_processor'));
assertType('true', $container->hasParameter('app.boolean'));
assertType('true', $parameterBag->has('app.boolean'));
assertType('true', $container->hasParameter('app.boolean_as_string'));
assertType('true', $parameterBag->has('app.boolean_as_string'));
+ assertType('true', $container->hasParameter('app.boolean_as_processor'));
+ assertType('true', $parameterBag->has('app.boolean_as_processor'));
assertType('true', $container->hasParameter('app.list'));
assertType('true', $parameterBag->has('app.list'));
assertType('true', $container->hasParameter('app.list_of_list'));
@@ -93,6 +119,27 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('true', $parameterBag->has('app.binary'));
assertType('true', $container->hasParameter('app.constant'));
assertType('true', $parameterBag->has('app.constant'));
+
+ $key = rand(0, 1) ? 'app.string' : 'app.int';
+ assertType("int|string", $container->getParameter($key));
+ assertType("int|string", $parameterBag->get($key));
+ assertType("int|string", $this->getParameter($key));
+ assertType('true', $container->hasParameter($key));
+ assertType('true', $parameterBag->has($key));
+
+ $key = rand(0, 1) ? 'app.string' : 'app.foo';
+ assertType("array|bool|float|int|string|null", $container->getParameter($key));
+ assertType("array|bool|float|int|string|null", $parameterBag->get($key));
+ assertType("array|bool|float|int|string|null", $this->getParameter($key));
+ assertType('bool', $container->hasParameter($key));
+ assertType('bool', $parameterBag->has($key));
+
+ $key = rand(0, 1) ? 'app.bar' : 'app.foo';
+ assertType("array|bool|float|int|string|null", $container->getParameter($key));
+ assertType("array|bool|float|int|string|null", $parameterBag->get($key));
+ assertType("array|bool|float|int|string|null", $this->getParameter($key));
+ assertType('false', $container->hasParameter($key));
+ assertType('false', $parameterBag->has($key));
}
}
diff --git a/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php b/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php
index 2e9b7ba2..2e48dc80 100644
--- a/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php
+++ b/tests/Type/Symfony/data/ExampleControllerWithoutContainer.php
@@ -38,24 +38,36 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.int_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.int_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.int_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.float'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.float'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.float'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.float_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.float_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.float_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_string'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_string'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_string'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.boolean_as_processor'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.boolean_as_processor'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.boolean_as_processor'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.list'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.list'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.list'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.list_of_list'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.list_of_list'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.list_of_list'));
+ assertType('array|bool|float|int|string|null', $container->getParameter('app.array_of_list'));
+ assertType('array|bool|float|int|string|null', $parameterBag->get('app.array_of_list'));
+ assertType('array|bool|float|int|string|null', $this->getParameter('app.array_of_list'));
assertType('array|bool|float|int|string|null', $container->getParameter('app.map'));
assertType('array|bool|float|int|string|null', $parameterBag->get('app.map'));
assertType('array|bool|float|int|string|null', $this->getParameter('app.map'));
@@ -74,14 +86,20 @@ public function parameters(ContainerInterface $container, ParameterBagInterface
assertType('bool', $parameterBag->has('app.int'));
assertType('bool', $container->hasParameter('app.int_as_string'));
assertType('bool', $parameterBag->has('app.int_as_string'));
+ assertType('bool', $container->hasParameter('app.int_as_processor'));
+ assertType('bool', $parameterBag->has('app.int_as_processor'));
assertType('bool', $container->hasParameter('app.float'));
assertType('bool', $parameterBag->has('app.float'));
assertType('bool', $container->hasParameter('app.float_as_string'));
assertType('bool', $parameterBag->has('app.float_as_string'));
+ assertType('bool', $container->hasParameter('app.float_as_processor'));
+ assertType('bool', $parameterBag->has('app.float_as_processor'));
assertType('bool', $container->hasParameter('app.boolean'));
assertType('bool', $parameterBag->has('app.boolean'));
assertType('bool', $container->hasParameter('app.boolean_as_string'));
assertType('bool', $parameterBag->has('app.boolean_as_string'));
+ assertType('bool', $container->hasParameter('app.boolean_as_processor'));
+ assertType('bool', $parameterBag->has('app.boolean_as_processor'));
assertType('bool', $container->hasParameter('app.list'));
assertType('bool', $parameterBag->has('app.list'));
assertType('bool', $container->hasParameter('app.list_of_list'));
diff --git a/tests/Type/Symfony/data/ExampleOptionCommand.php b/tests/Type/Symfony/data/ExampleOptionCommand.php
index b6a07574..b880173d 100644
--- a/tests/Type/Symfony/data/ExampleOptionCommand.php
+++ b/tests/Type/Symfony/data/ExampleOptionCommand.php
@@ -21,6 +21,7 @@ protected function configure(): void
$this->addOption('c', null, InputOption::VALUE_REQUIRED);
$this->addOption('d', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL);
$this->addOption('e', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED);
+ $this->addOption('f', null, InputOption::VALUE_NEGATABLE);
$this->addOption('bb', null, InputOption::VALUE_OPTIONAL, '', 1);
$this->addOption('cc', null, InputOption::VALUE_REQUIRED, '', 1);
@@ -35,11 +36,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
assertType('string|null', $input->getOption('c'));
assertType('array', $input->getOption('d'));
assertType('array', $input->getOption('e'));
+ assertType('bool|null', $input->getOption('f'));
assertType('1|string|null', $input->getOption('bb'));
assertType('1|string', $input->getOption('cc'));
assertType('array', $input->getOption('dd'));
assertType('array', $input->getOption('ee'));
+
+ assertType('array{a: bool, b: string|null, c: string|null, d: array, e: array, f: bool|null, bb: 1|string|null, cc: 1|string, dd: array, ee: array, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool|null, no-interaction: bool}', $input->getOptions());
}
}
diff --git a/tests/Type/Symfony/data/ExampleOptionLazyCommand.php b/tests/Type/Symfony/data/ExampleOptionLazyCommand.php
index 7933cd41..433e1cfe 100644
--- a/tests/Type/Symfony/data/ExampleOptionLazyCommand.php
+++ b/tests/Type/Symfony/data/ExampleOptionLazyCommand.php
@@ -23,6 +23,7 @@ protected function configure(): void
$this->addOption('c', null, InputOption::VALUE_REQUIRED);
$this->addOption('d', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL);
$this->addOption('e', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED);
+ $this->addOption('f', null, InputOption::VALUE_NEGATABLE);
$this->addOption('bb', null, InputOption::VALUE_OPTIONAL, '', 1);
$this->addOption('cc', null, InputOption::VALUE_REQUIRED, '', 1);
@@ -37,11 +38,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
assertType('string|null', $input->getOption('c'));
assertType('array', $input->getOption('d'));
assertType('array', $input->getOption('e'));
+ assertType('bool|null', $input->getOption('f'));
assertType('1|string|null', $input->getOption('bb'));
assertType('1|string', $input->getOption('cc'));
assertType('array', $input->getOption('dd'));
assertType('array', $input->getOption('ee'));
+
+ assertType('array{a: bool, b: string|null, c: string|null, d: array, e: array, f: bool|null, bb: 1|string|null, cc: 1|string, dd: array, ee: array, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool|null, no-interaction: bool}', $input->getOptions());
}
}
diff --git a/tests/Type/Symfony/data/FormInterface_getErrors.php b/tests/Type/Symfony/data/FormInterface_getErrors.php
new file mode 100644
index 00000000..a360a21d
--- /dev/null
+++ b/tests/Type/Symfony/data/FormInterface_getErrors.php
@@ -0,0 +1,20 @@
+', $form->getErrors());
+assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false));
+assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false, true));
+
+assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(true));
+assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(true, true));
+
+assertType(FormErrorIterator::class . '<'. FormError::class .'>', $form->getErrors(false, false));
+
+assertType(FormErrorIterator::class . '<'. FormError::class .'|'. FormErrorIterator::class . '>', $form->getErrors(true, false));
diff --git a/tests/Type/Symfony/data/cache.php b/tests/Type/Symfony/data/cache.php
new file mode 100644
index 00000000..a8862177
--- /dev/null
+++ b/tests/Type/Symfony/data/cache.php
@@ -0,0 +1,42 @@
+get('foo', function (): string {
+ return '';
+ });
+
+ assertType('string', $result);
+};
+
+/**
+ * @param callable():string $fn
+ */
+function testNonScalarCacheCallable(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void {
+ $result = $cache->get('foo', $fn);
+
+ assertType('string', $result);
+};
+
+
+/**
+ * @param callable():non-empty-string $fn
+ */
+function testCacheCallableReturnTypeGeneralization(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void {
+ $result = $cache->get('foo', $fn);
+
+ assertType('string', $result);
+};
+
+
+/**
+ * @param \Symfony\Contracts\Cache\CallbackInterface<\stdClass> $cb
+ */
+ function testCacheCallbackInterface(\Symfony\Contracts\Cache\CacheInterface $cache, \Symfony\Contracts\Cache\CallbackInterface $cb): void {
+ $result = $cache->get('foo',$cb);
+
+ assertType('stdClass', $result);
+};
diff --git a/tests/Type/Symfony/data/envelope_all.php b/tests/Type/Symfony/data/envelope_all.php
index 95521530..aac9d583 100644
--- a/tests/Type/Symfony/data/envelope_all.php
+++ b/tests/Type/Symfony/data/envelope_all.php
@@ -4,6 +4,6 @@
$envelope = new \Symfony\Component\Messenger\Envelope(new stdClass());
-assertType('array', $envelope->all(\Symfony\Component\Messenger\Stamp\ReceivedStamp::class));
-assertType('array', $envelope->all(random_bytes(1)));
-assertType('array>', $envelope->all());
+assertType('list', $envelope->all(\Symfony\Component\Messenger\Stamp\ReceivedStamp::class));
+assertType('list', $envelope->all(random_bytes(1)));
+assertType('array, list>', $envelope->all());
diff --git a/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php
new file mode 100644
index 00000000..1e33cb37
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/anonymous/AnonymousExtension.php
@@ -0,0 +1,17 @@
+getConfiguration($configs, $container)
+ );
+ }
+};
diff --git a/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php
new file mode 100644
index 00000000..614431f2
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/ignore-implemented/IgnoreImplementedExtension.php
@@ -0,0 +1,23 @@
+getConfiguration($configs, $container)
+ );
+ }
+
+ public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface
+ {
+ return null;
+ }
+}
diff --git a/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php
new file mode 100644
index 00000000..77c44003
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/multiple-types/MultipleTypes.php
@@ -0,0 +1,18 @@
+getConfiguration($configs, $container)
+ );
+}
diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php
new file mode 100644
index 00000000..4ff16c39
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-optional-params/Configuration.php
@@ -0,0 +1,16 @@
+getConfiguration($configs, $container)
+ );
+ }
+}
diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php
new file mode 100644
index 00000000..b9d5bcc1
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor-required-params/Configuration.php
@@ -0,0 +1,16 @@
+getConfiguration($configs, $container)
+ );
+ }
+}
diff --git a/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php
new file mode 100644
index 00000000..8eea9eb9
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/with-configuration-with-constructor/Configuration.php
@@ -0,0 +1,16 @@
+getConfiguration($configs, $container)
+ );
+ }
+}
diff --git a/tests/Type/Symfony/data/extension/with-configuration/Configuration.php b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php
new file mode 100644
index 00000000..4e8c51b5
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/with-configuration/Configuration.php
@@ -0,0 +1,12 @@
+getConfiguration($configs, $container)
+ );
+ }
+}
diff --git a/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php
new file mode 100644
index 00000000..dccec3e2
--- /dev/null
+++ b/tests/Type/Symfony/data/extension/without-configuration/WithoutConfigurationExtension.php
@@ -0,0 +1,17 @@
+getConfiguration($configs, $container)
+ );
+ }
+}
diff --git a/tests/Type/Symfony/data/form_data_type.php b/tests/Type/Symfony/data/form_data_type.php
new file mode 100644
index 00000000..34a673a4
--- /dev/null
+++ b/tests/Type/Symfony/data/form_data_type.php
@@ -0,0 +1,93 @@
+
+ */
+class DataClassType extends AbstractType
+{
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ assertType('GenericFormDataType\DataClass|null', $builder->getData());
+ assertType('GenericFormDataType\DataClass|null', $builder->getForm()->getData());
+
+ $builder
+ ->add('foo', NumberType::class)
+ ->add('bar', TextType::class)
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver
+ ->setDefaults([
+ 'data_class' => DataClass::class,
+ ])
+ ;
+ }
+
+}
+
+class FormFactoryAwareClass
+{
+
+ /** @var FormFactoryInterface */
+ private $formFactory;
+
+ public function __construct(FormFactoryInterface $formFactory)
+ {
+ $this->formFactory = $formFactory;
+ }
+
+ public function doSomething(): void
+ {
+ $form = $this->formFactory->create(DataClassType::class, new DataClass());
+ assertType('GenericFormDataType\DataClass', $form->getData());
+ }
+
+ public function doSomethingNullable(): void
+ {
+ $form = $this->formFactory->create(DataClassType::class);
+ assertType('GenericFormDataType\DataClass|null', $form->getData());
+ }
+
+}
+
+class FormController extends AbstractController
+{
+
+ public function doSomething(): void
+ {
+ $form = $this->createForm(DataClassType::class, new DataClass());
+ assertType('GenericFormDataType\DataClass', $form->getData());
+ }
+
+ public function doSomethingNullable(): void
+ {
+ $form = $this->createForm(DataClassType::class);
+ assertType('GenericFormDataType\DataClass|null', $form->getData());
+ }
+
+}
diff --git a/tests/Type/Symfony/data/input_bag.php b/tests/Type/Symfony/data/input_bag.php
index 4c836c5f..77b58821 100644
--- a/tests/Type/Symfony/data/input_bag.php
+++ b/tests/Type/Symfony/data/input_bag.php
@@ -5,8 +5,18 @@
$bag = new \Symfony\Component\HttpFoundation\InputBag(['foo' => 'bar', 'bar' => ['x']]);
assertType('bool|float|int|string|null', $bag->get('foo'));
+
+if ($bag->has('foo')) {
+ // Because `has` rely on `array_key_exists` we can still have set the NULL value.
+ assertType('bool|float|int|string|null', $bag->get('foo'));
+ assertType('bool|float|int|string|null', $bag->get('bar'));
+} else {
+ assertType('null', $bag->get('foo'));
+ assertType('bool|float|int|string|null', $bag->get('bar'));
+}
+
assertType('bool|float|int|string|null', $bag->get('foo', null));
assertType('bool|float|int|string', $bag->get('foo', ''));
assertType('bool|float|int|string', $bag->get('foo', 'baz'));
-assertType('array', $bag->all());
-assertType('array', $bag->all('bar'));
+assertType('array|bool|float|int|string>', $bag->all());
+assertType('array', $bag->all('bar'));
diff --git a/tests/Type/Symfony/data/messenger_handle_trait.php b/tests/Type/Symfony/data/messenger_handle_trait.php
new file mode 100644
index 00000000..7a86d482
--- /dev/null
+++ b/tests/Type/Symfony/data/messenger_handle_trait.php
@@ -0,0 +1,113 @@
+ ['method' => 'handleInt'];
+ yield FloatQuery::class => ['method' => 'handleFloat'];
+ yield StringQuery::class => ['method' => 'handleString'];
+ }
+
+ public function __invoke(BooleanQuery $query): bool
+ {
+ return true;
+ }
+
+ public function handleInt(IntQuery $query): int
+ {
+ return 0;
+ }
+
+ public function handleFloat(FloatQuery $query): float
+ {
+ return 0.0;
+ }
+
+ public function handleString(StringQuery $query): string
+ {
+ return 'string result';
+ }
+}
+
+class TaggedQuery {}
+class TaggedResult {}
+class TaggedHandler
+{
+ public function handle(TaggedQuery $query): TaggedResult
+ {
+ return new TaggedResult();
+ }
+}
+
+class MultiHandlesForInTheSameHandlerQuery {}
+class MultiHandlesForInTheSameHandler implements MessageSubscriberInterface
+{
+ public static function getHandledMessages(): iterable
+ {
+ yield MultiHandlesForInTheSameHandlerQuery::class;
+ yield MultiHandlesForInTheSameHandlerQuery::class => ['priority' => '0'];
+ }
+
+ public function __invoke(MultiHandlesForInTheSameHandlerQuery $query): bool
+ {
+ return true;
+ }
+}
+
+class MultiHandlersForTheSameMessageQuery {}
+class MultiHandlersForTheSameMessageHandler1
+{
+ public function __invoke(MultiHandlersForTheSameMessageQuery $query): bool
+ {
+ return true;
+ }
+}
+class MultiHandlersForTheSameMessageHandler2
+{
+ public function __invoke(MultiHandlersForTheSameMessageQuery $query): bool
+ {
+ return false;
+ }
+}
+
+class HandleTraitClass {
+ use HandleTrait;
+
+ public function __invoke()
+ {
+ assertType(RegularQueryResult::class, $this->handle(new RegularQuery()));
+
+ assertType('bool', $this->handle(new BooleanQuery()));
+ assertType('int', $this->handle(new IntQuery()));
+ assertType('float', $this->handle(new FloatQuery()));
+ assertType('string', $this->handle(new StringQuery()));
+
+ assertType(TaggedResult::class, $this->handle(new TaggedQuery()));
+
+ // HandleTrait will throw exception in fact due to multiple handle methods/handlers per single query
+ assertType('mixed', $this->handle(new MultiHandlesForInTheSameHandlerQuery()));
+ assertType('mixed', $this->handle(new MultiHandlersForTheSameMessageQuery()));
+ }
+}
diff --git a/tests/Type/Symfony/data/property_accessor.php b/tests/Type/Symfony/data/property_accessor.php
new file mode 100644
index 00000000..8d5e95f0
--- /dev/null
+++ b/tests/Type/Symfony/data/property_accessor.php
@@ -0,0 +1,13 @@
+ 'ea'];
+$propertyAccessor->setValue($array, 'foo', 'bar');
+assertType('array', $array);
+
+$object = new \stdClass();
+$propertyAccessor->setValue($object, 'foo', 'bar');
+assertType('stdClass', $object);
diff --git a/tests/Type/Symfony/data/response_header_bag_get_cookies.php b/tests/Type/Symfony/data/response_header_bag_get_cookies.php
new file mode 100644
index 00000000..f07d5e5b
--- /dev/null
+++ b/tests/Type/Symfony/data/response_header_bag_get_cookies.php
@@ -0,0 +1,12 @@
+setCookie(Cookie::create('cookie_name'));
+
+assertType('array', $headerBag->getCookies());
+assertType('array', $headerBag->getCookies(ResponseHeaderBag::COOKIES_FLAT));
+assertType('array>>', $headerBag->getCookies(ResponseHeaderBag::COOKIES_ARRAY));
diff --git a/tests/Type/Symfony/extension-test.neon b/tests/Type/Symfony/extension-test.neon
index f7dc1353..0f1d9522 100644
--- a/tests/Type/Symfony/extension-test.neon
+++ b/tests/Type/Symfony/extension-test.neon
@@ -1,4 +1,4 @@
parameters:
symfony:
- console_application_loader: console_application_loader.php
- container_xml_path: container.xml
+ consoleApplicationLoader: console_application_loader.php
+ containerXmlPath: container.xml