From 2e8b2606915d199f0394cc644a6f064bdca9e8d6 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Thu, 3 Oct 2024 15:44:48 +0200 Subject: [PATCH 001/220] deps: use bashunit:0.17 --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 51891fb45c..c4f626994a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -268,7 +268,7 @@ jobs: run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" - name: "Install bashunit" - run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.13.0" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.17.0" - name: "Test" run: "${{ matrix.script }}" From 728ae75699494e476731afc606febadc986c4611 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Thu, 3 Oct 2024 15:45:43 +0200 Subject: [PATCH 002/220] test: use bashunit -a exit_code to check for errors and defer the execution call inside bashunit --- .github/workflows/e2e-tests.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4d56668a56..6d486bc70e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -160,49 +160,52 @@ jobs: cd e2e/trait-caching ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ patch -b data/TraitOne.php < TraitOne.patch - OUTPUT=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") echo "$OUTPUT" - ../bashunit -a line_count 1 "$OUTPUT" + ../bashunit -a line_count 2 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" ../bashunit -a contains 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.' "$OUTPUT" - script: | cd e2e/trait-caching ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ patch -b data/TraitTwo.php < TraitTwo.patch - OUTPUT=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") echo "$OUTPUT" - ../bashunit -a line_count 1 "$OUTPUT" + ../bashunit -a line_count 2 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" ../bashunit -a contains 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.' "$OUTPUT" - script: | cd e2e/trait-caching ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ patch -b data/TraitOne.php < TraitOne.patch patch -b data/TraitTwo.php < TraitTwo.patch - OUTPUT=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/") echo "$OUTPUT" - ../bashunit -a line_count 2 "$OUTPUT" + ../bashunit -a line_count 3 "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" ../bashunit -a contains 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.' "$OUTPUT" ../bashunit -a contains 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c ignore.neon 2>&1 || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c ignore.neon") echo "$OUTPUT" ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" ../bashunit -a contains 'tests is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c phpneon.php 2>&1 || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c phpneon.php") echo "$OUTPUT" ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" ../bashunit -a contains 'src/test.php is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c excludePaths.neon 2>&1 || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c excludePaths.neon") echo "$OUTPUT" ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" ../bashunit -a contains 'tests is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" - script: | cd e2e/bad-exclude-paths - OUTPUT=$(../../bin/phpstan analyse -c phpneon2.php 2>&1 || true) + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c phpneon2.php") echo "$OUTPUT" ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" ../bashunit -a contains 'src/test.php is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" From dcd69eba14ed57b32c2f1b759cd8289772f364ac Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 11 Oct 2024 09:36:06 +0200 Subject: [PATCH 003/220] Try to quit the child process only after internal errors were accounted for --- .github/workflows/e2e-tests.yml | 7 ++++ e2e/bug-11826/.gitignore | 2 ++ e2e/bug-11826/composer.json | 8 +++++ e2e/bug-11826/phpstan.neon.dist | 8 +++++ e2e/bug-11826/rules/DummyRule.php | 35 +++++++++++++++++++ .../src/FatalErrorWhenAutoloaded.php | 11 ++++++ src/Parallel/ParallelAnalyser.php | 6 +++- 7 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 e2e/bug-11826/.gitignore create mode 100644 e2e/bug-11826/composer.json create mode 100644 e2e/bug-11826/phpstan.neon.dist create mode 100644 e2e/bug-11826/rules/DummyRule.php create mode 100644 e2e/bug-11826/src/FatalErrorWhenAutoloaded.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c4f626994a..8914cca904 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -248,6 +248,13 @@ jobs: cd e2e/bad-exclude-paths OUTPUT=$(../../bin/phpstan analyse -c ignoreReportUnmatchedFalse.neon) echo "$OUTPUT" + - script: | + cd e2e/bug-11826 + composer install + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan") + echo "$OUTPUT" + ../bashunit -a contains 'Child process error (exit code 255): PHP Fatal error' "$OUTPUT" + ../bashunit -a contains 'Result is incomplete because of severe errors.' "$OUTPUT" steps: - name: "Checkout" diff --git a/e2e/bug-11826/.gitignore b/e2e/bug-11826/.gitignore new file mode 100644 index 0000000000..de4a392c33 --- /dev/null +++ b/e2e/bug-11826/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/e2e/bug-11826/composer.json b/e2e/bug-11826/composer.json new file mode 100644 index 0000000000..e33fb45ab0 --- /dev/null +++ b/e2e/bug-11826/composer.json @@ -0,0 +1,8 @@ +{ + "autoload": { + "psr-4": { + "App\\": "src/", + "Rules\\": "rules/" + } + } +} diff --git a/e2e/bug-11826/phpstan.neon.dist b/e2e/bug-11826/phpstan.neon.dist new file mode 100644 index 0000000000..3f491198cf --- /dev/null +++ b/e2e/bug-11826/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: 9 + paths: + - src + - rules + +rules: + - Rules\DummyRule diff --git a/e2e/bug-11826/rules/DummyRule.php b/e2e/bug-11826/rules/DummyRule.php new file mode 100644 index 0000000000..0a18225531 --- /dev/null +++ b/e2e/bug-11826/rules/DummyRule.php @@ -0,0 +1,35 @@ + + */ +class DummyRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + * @return list + */ + public function processNode( + Node $node, + Scope $scope, + ): array + { + return [FatalErrorWhenAutoloaded::AUTOLOAD]; + } + +} diff --git a/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php new file mode 100644 index 0000000000..a75127a356 --- /dev/null +++ b/e2e/bug-11826/src/FatalErrorWhenAutoloaded.php @@ -0,0 +1,11 @@ +processPool->tryQuitProcess($processIdentifier); if ($exitCode === 0) { + $this->processPool->tryQuitProcess($processIdentifier); return; } if ($exitCode === null) { + $this->processPool->tryQuitProcess($processIdentifier); return; } @@ -294,6 +295,7 @@ public function analyse( continue; } + $this->processPool->tryQuitProcess($processIdentifier); return; } $internalErrors[] = new InternalError(sprintf( @@ -303,11 +305,13 @@ public function analyse( 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', ), 'running parallel worker', [], null, false); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); return; } $internalErrors[] = new InternalError(sprintf('Child process error (exit code %d): %s', $exitCode, $output), 'running parallel worker', [], null, true); $internalErrorsCount++; + $this->processPool->tryQuitProcess($processIdentifier); }); $this->processPool->attachProcess($processIdentifier, $process); } From 66664bb612b4cfb4e8a870f5d11f5af731f5e21a Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Sat, 12 Oct 2024 09:21:39 +0000 Subject: [PATCH 004/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 35b021fe66..6597076790 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^4.19.4", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.25.0.19", + "ondrejmirtes/better-reflection": "6.25.0.20", "phpstan/php-8-stubs": "0.3.111", "phpstan/phpdoc-parser": "1.32.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 916baf9f89..b3c3720962 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9d6cc3eb297a7d5f481c67f8c671b208", + "content-hash": "a26a44dbb1ffe6df5fcd90884b4be5d4", "packages": [ { "name": "clue/ndjson-react", @@ -2184,16 +2184,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.25.0.19", + "version": "6.25.0.20", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "3c20bd3dea6f5a04a729891f9dd4326feb2e288c" + "reference": "a04fde59afcca51b0b2b439e05eb74503994cf4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/3c20bd3dea6f5a04a729891f9dd4326feb2e288c", - "reference": "3c20bd3dea6f5a04a729891f9dd4326feb2e288c", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/a04fde59afcca51b0b2b439e05eb74503994cf4b", + "reference": "a04fde59afcca51b0b2b439e05eb74503994cf4b", "shasum": "" }, "require": { @@ -2250,9 +2250,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.19" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.20" }, - "time": "2024-09-10T09:53:53+00:00" + "time": "2024-10-12T09:20:20+00:00" }, { "name": "phpstan/php-8-stubs", @@ -6589,7 +6589,7 @@ "php": "^8.1", "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.99" }, From ff7521302711c2eca765d47f8606179f0ca0d19b Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Sat, 12 Oct 2024 09:30:33 +0000 Subject: [PATCH 005/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 6597076790..866afda425 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^4.19.4", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.25.0.20", + "ondrejmirtes/better-reflection": "6.25.0.21", "phpstan/php-8-stubs": "0.3.111", "phpstan/phpdoc-parser": "1.32.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index b3c3720962..aae4ed9cd0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a26a44dbb1ffe6df5fcd90884b4be5d4", + "content-hash": "a76e95ec9a6020d405d9495cb67cf0ce", "packages": [ { "name": "clue/ndjson-react", @@ -2184,16 +2184,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.25.0.20", + "version": "6.25.0.21", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "a04fde59afcca51b0b2b439e05eb74503994cf4b" + "reference": "c1bcfaa130718e4004ab8260bed4bfe96a46dc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/a04fde59afcca51b0b2b439e05eb74503994cf4b", - "reference": "a04fde59afcca51b0b2b439e05eb74503994cf4b", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c1bcfaa130718e4004ab8260bed4bfe96a46dc02", + "reference": "c1bcfaa130718e4004ab8260bed4bfe96a46dc02", "shasum": "" }, "require": { @@ -2250,9 +2250,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.20" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.21" }, - "time": "2024-10-12T09:20:20+00:00" + "time": "2024-10-12T09:29:10+00:00" }, { "name": "phpstan/php-8-stubs", From def9f5abccb4b2e761dc7886f444e56aae25d97a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 12:55:40 +0200 Subject: [PATCH 006/220] Simulate level 10 in issue bot on 1.12.x --- .github/workflows/issue-bot.yml | 3 +++ issue-bot/config.level10.neon | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 issue-bot/config.level10.neon diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index 6d6af362f1..530886afff 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -106,6 +106,9 @@ jobs: attempt_limit: 5 attempt_delay: 1000 + - name: "Add level 10 config file" + run: "cp issue-bot/config.level10.neon conf/" + - name: "Run PHPStan" working-directory: "issue-bot" timeout-minutes: 5 diff --git a/issue-bot/config.level10.neon b/issue-bot/config.level10.neon new file mode 100644 index 0000000000..5d052692c9 --- /dev/null +++ b/issue-bot/config.level10.neon @@ -0,0 +1,5 @@ +includes: + - config.level9.neon + +parameters: + checkImplicitMixed: true From 93126b4590bb1c0c934be1dc52af180144ef45ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 13:07:36 +0200 Subject: [PATCH 007/220] Revert "Simulate level 10 in issue bot on 1.12.x" This reverts commit def9f5abccb4b2e761dc7886f444e56aae25d97a. --- .github/workflows/issue-bot.yml | 3 --- issue-bot/config.level10.neon | 5 ----- 2 files changed, 8 deletions(-) delete mode 100644 issue-bot/config.level10.neon diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index 530886afff..6d6af362f1 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -106,9 +106,6 @@ jobs: attempt_limit: 5 attempt_delay: 1000 - - name: "Add level 10 config file" - run: "cp issue-bot/config.level10.neon conf/" - - name: "Run PHPStan" working-directory: "issue-bot" timeout-minutes: 5 diff --git a/issue-bot/config.level10.neon b/issue-bot/config.level10.neon deleted file mode 100644 index 5d052692c9..0000000000 --- a/issue-bot/config.level10.neon +++ /dev/null @@ -1,5 +0,0 @@ -includes: - - config.level9.neon - -parameters: - checkImplicitMixed: true From a9346394eafb588efe7af2c0650dfeb2b757a427 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 13:16:12 +0200 Subject: [PATCH 008/220] Fix test --- tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php b/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php index e8d009fe0f..2b5c9c1d40 100644 --- a/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php +++ b/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php @@ -31,7 +31,7 @@ * @param T $value * @return T */ -function id(mixed $value): mixed +function id($value) { dumpPhpDocType($value); From 6cf223840f89c972551f373ade9eea16d12e143b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 15:46:37 +0200 Subject: [PATCH 009/220] ArrayType::describe - explicit mixed should be stated explicitly --- phpstan-baseline.neon | 2 +- src/Type/ArrayType.php | 8 +-- ...ySearchFunctionTypeSpecifyingExtension.php | 5 +- .../Analyser/AnalyserIntegrationTest.php | 2 +- .../Analyser/LegacyNodeScopeResolverTest.php | 8 +-- tests/PHPStan/Analyser/TypeSpecifierTest.php | 10 ++-- tests/PHPStan/Analyser/data/param-out.php | 6 +- tests/PHPStan/Analyser/nsrt/array-chunk.php | 2 +- .../Analyser/nsrt/array-fill-keys-php8.php | 2 +- .../PHPStan/Analyser/nsrt/array-flip-php8.php | 2 +- .../nsrt/array-intersect-key-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/array-pop.php | 2 +- tests/PHPStan/Analyser/nsrt/array-reverse.php | 8 +-- .../Analyser/nsrt/array-search-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/array-shift.php | 2 +- tests/PHPStan/Analyser/nsrt/array-slice.php | 2 +- tests/PHPStan/Analyser/nsrt/array_keys.php | 2 +- tests/PHPStan/Analyser/nsrt/array_values.php | 2 +- .../PHPStan/Analyser/nsrt/assert-docblock.php | 22 +++---- .../PHPStan/Analyser/nsrt/assert-methods.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-1209.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-1233.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-3446.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-6329.php | 14 ++--- .../nsrt/bug-7144-composer-integration.php | 8 +-- tests/PHPStan/Analyser/nsrt/bug-7915.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-9662.php | 58 +++++++++---------- tests/PHPStan/Analyser/nsrt/bug-9734.php | 4 +- .../nsrt/conditional-types-inference.php | 2 +- .../Analyser/nsrt/filter-var-array.php | 6 +- tests/PHPStan/Analyser/nsrt/generics.php | 4 +- tests/PHPStan/Analyser/nsrt/globals.php | 18 +++--- .../Analyser/nsrt/has-offset-type-bug.php | 6 +- tests/PHPStan/Analyser/nsrt/memcache-get.php | 2 +- .../PHPStan/Analyser/nsrt/mixed-subtract.php | 4 +- .../PHPStan/Analyser/nsrt/mixed-to-number.php | 2 +- tests/PHPStan/Analyser/nsrt/narrow-cast.php | 4 +- .../Analyser/nsrt/native-expressions.php | 2 +- .../prestashop-breakdowns-empty-array.php | 6 +- .../Php8SignatureMapProviderTest.php | 2 +- ...nexistentOffsetInArrayDimFetchRuleTest.php | 2 +- tests/PHPStan/Rules/Classes/data/bug-5333.php | 2 +- .../CallToFunctionParametersRuleTest.php | 8 +-- .../Rules/Methods/CallMethodsRuleTest.php | 6 +- .../Methods/CallStaticMethodsRuleTest.php | 6 +- .../Rules/Methods/ReturnTypeRuleTest.php | 4 +- .../VarTagChangedExpressionTypeRuleTest.php | 8 +-- .../WrongVariableNameInVarTagRuleTest.php | 2 +- .../TypesAssignedToPropertiesRuleTest.php | 6 +- .../PHPStan/Rules/Variables/data/bug-8113.php | 12 ++-- 50 files changed, 151 insertions(+), 154 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fb4b06b279..a652b6a836 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -13,7 +13,7 @@ parameters: path: src/Analyser/AnalyserResultFinalizer.php - - message: '#^Cannot assign offset ''realCount'' to array\|string\.$#' + message: '#^Cannot assign offset ''realCount'' to array\\|string\.$#' identifier: offsetAssign.dimType count: 1 path: src/Analyser/Ignore/IgnoredErrorHelperResult.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 8fb1a219d5..488dac119e 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -128,8 +128,8 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; - $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed(); + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed(); $valueHandler = function () use ($level, $isMixedKeyType, $isMixedItemType): string { if ($isMixedKeyType || $this->keyType instanceof NeverType) { @@ -500,8 +500,8 @@ public function traverse(callable $cb): Type public function toPhpDocNode(): TypeNode { - $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; - $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->keyType->isExplicitMixed(); + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed' && !$this->itemType->isExplicitMixed(); if ($isMixedKeyType) { if ($isMixedItemType) { diff --git a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php index b382891275..68f2992096 100644 --- a/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArraySearchFunctionTypeSpecifyingExtension.php @@ -10,10 +10,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\MixedType; -use PHPStan\Type\TypeCombinator; use function strtolower; final class ArraySearchFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension @@ -45,7 +42,7 @@ public function specifyTypes( return $this->typeSpecifier->create( $arrayArg, - TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType()), + new NonEmptyArrayType(), $context, $scope, ); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 52791b8b95..d64cd88617 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1198,7 +1198,7 @@ public function testBug9459(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php'); $this->assertCount(1, $errors); - $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); + $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); $this->assertSame(10, $errors[0]->getLine()); } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 6113db22b3..d24d24338d 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -1763,7 +1763,7 @@ public function dataProperties(): array '$this->arrayPropertyOne', ], [ - 'array', + 'array', '$this->arrayPropertyOther', ], [ @@ -3423,7 +3423,7 @@ public function dataTypeFromFunctionPhpDocs(): array '$arrayParameterOne', ], [ - 'array', + 'array', '$arrayParameterOther', ], [ @@ -5779,7 +5779,7 @@ public function dataSpecifiedTypesUsingIsFunctions(): array '$null', ], [ - 'array', + 'array', '$array', ], [ @@ -9252,7 +9252,7 @@ public function dataInferPrivatePropertyTypeFromConstructor(): array '$this->bool', ], [ - 'array', + 'array', '$this->array', ], ]; diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 04abebab3c..99cbb8db71 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1218,8 +1218,8 @@ public function dataCondition(): iterable ), new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), ), - ['$a' => 'non-empty-array|null'], - ['$a' => 'mixed~non-empty-array & ~null'], + ['$a' => 'non-empty-array|null'], + ['$a' => 'mixed~non-empty-array & ~null'], ], [ new Expr\BinaryOp\BooleanAnd( @@ -1234,7 +1234,7 @@ public function dataCondition(): iterable ), [ '$foo' => 'array', - 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' + 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', // could be 'array' ], [], ], @@ -1250,7 +1250,7 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-array', + '$foo' => 'non-empty-array', 'count($foo)' => 'mixed~(0.0|int|false|null)', ], [], @@ -1267,7 +1267,7 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-array', + '$foo' => 'non-empty-array', 'count($foo)' => '2', ], [], diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 05684938b8..d172b358e1 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -315,7 +315,7 @@ function testParseStr() { echo $output['arr'][1];//baz */ - \PHPStan\Testing\assertType('array', $output); + \PHPStan\Testing\assertType('array|lowercase-string>', $output); } function fooSimilar() { @@ -392,10 +392,10 @@ function fooHeadersSent() { function fooMbParseStr() { mb_parse_str("foo=bar", $output); - assertType('array', $output); + assertType('array|string>', $output); mb_parse_str('email=mail@example.org&city=town&x=1&y[g]=3&f=1.23', $output); - assertType('array', $output); + assertType('array|string>', $output); } function fooPreg() diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php index 8e6fa67cf2..c0b79ffbae 100644 --- a/tests/PHPStan/Analyser/nsrt/array-chunk.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -11,7 +11,7 @@ public function generalArrays(array $arr): void { /** @var mixed[] $arr */ assertType('list>', array_chunk($arr, 2)); - assertType('list', array_chunk($arr, 2, true)); + assertType('list>', array_chunk($arr, 2, true)); /** @var array $arr */ assertType('list>', array_chunk($arr, 2)); diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php index 07c06367dc..4710482534 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed): void { if (is_array($mixed)) { - assertType("array<'b'>", array_fill_keys($mixed, 'b')); + assertType("array", array_fill_keys($mixed, 'b')); } else { assertType("*NEVER*", array_fill_keys($mixed, 'b')); } diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php index b8f0e6793d..2b75c17aba 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php @@ -9,7 +9,7 @@ function mixedAndSubtractedArray($mixed) if (is_array($mixed)) { assertType('array', array_flip($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_flip($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php index 4bfd3140e0..9ffde9da83 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php8.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, array $otherArrs): void /** @var array $otherArrs */ assertType('array', array_intersect_key($mixed, $otherArrs)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); /** @var array $otherArrs */ assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); /** @var array $otherArrs */ diff --git a/tests/PHPStan/Analyser/nsrt/array-pop.php b/tests/PHPStan/Analyser/nsrt/array-pop.php index 04494a96df..37a986b0ce 100644 --- a/tests/PHPStan/Analyser/nsrt/array-pop.php +++ b/tests/PHPStan/Analyser/nsrt/array-pop.php @@ -77,7 +77,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('mixed', array_pop($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('mixed', array_pop($mixed)); assertType('*ERROR*', $mixed); } diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php index 5d341b6290..6f05e9b9f0 100644 --- a/tests/PHPStan/Analyser/nsrt/array-reverse.php +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -14,8 +14,8 @@ class Foo */ public function normalArrays(array $a, array $b): void { - assertType('array', array_reverse($a)); - assertType('array', array_reverse($a, true)); + assertType('array', array_reverse($a)); + assertType('array', array_reverse($a, true)); assertType('array', array_reverse($b)); assertType('array', array_reverse($b, true)); @@ -68,8 +68,8 @@ public function list(array $a, array $b): void public function mixed(mixed $mixed): void { - assertType('array', array_reverse($mixed)); - assertType('array', array_reverse($mixed, true)); + assertType('array', array_reverse($mixed)); + assertType('array', array_reverse($mixed, true)); if (array_key_exists('foo', $mixed)) { assertType('non-empty-array', array_reverse($mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/array-search-php8.php b/tests/PHPStan/Analyser/nsrt/array-search-php8.php index c3430cc1b5..30b9527e10 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-search-php8.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, string $string): void assertType('int|string|false', array_search('foo', $mixed)); assertType('int|string|false', array_search($string, $mixed, true)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_search('foo', $mixed, true)); assertType('*NEVER*', array_search('foo', $mixed)); assertType('*NEVER*', array_search($string, $mixed, true)); diff --git a/tests/PHPStan/Analyser/nsrt/array-shift.php b/tests/PHPStan/Analyser/nsrt/array-shift.php index eb227de3f6..2d8ef21d7e 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shift.php +++ b/tests/PHPStan/Analyser/nsrt/array-shift.php @@ -77,7 +77,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('mixed', array_shift($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('mixed', array_shift($mixed)); assertType('*ERROR*', $mixed); } diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php index 87fa61e36f..e2faabd113 100644 --- a/tests/PHPStan/Analyser/nsrt/array-slice.php +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -24,7 +24,7 @@ public function nonEmpty(array $a, array $b, array $c): void */ public function fromMixed($arr): void { - assertType('array', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2)); } public function normalArrays(array $arr): void diff --git a/tests/PHPStan/Analyser/nsrt/array_keys.php b/tests/PHPStan/Analyser/nsrt/array_keys.php index 7ea0a40ff5..6808bf36b3 100644 --- a/tests/PHPStan/Analyser/nsrt/array_keys.php +++ b/tests/PHPStan/Analyser/nsrt/array_keys.php @@ -11,7 +11,7 @@ public function sayHello($mixed): void if(is_array($mixed)) { assertType('list<(int|string)>', array_keys($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_keys($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array_values.php b/tests/PHPStan/Analyser/nsrt/array_values.php index a9fc01c947..18074963a4 100644 --- a/tests/PHPStan/Analyser/nsrt/array_values.php +++ b/tests/PHPStan/Analyser/nsrt/array_values.php @@ -11,7 +11,7 @@ public function foo1($mixed): void if(is_array($mixed)) { assertType('list', array_values($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('*NEVER*', array_values($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/assert-docblock.php b/tests/PHPStan/Analyser/nsrt/assert-docblock.php index 8451a48ebd..a1cbcc9b3e 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-docblock.php +++ b/tests/PHPStan/Analyser/nsrt/assert-docblock.php @@ -56,7 +56,7 @@ function validateNotNull($value) : void {} * @param mixed[] $arr */ function takesArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); validateStringArray($arr); assertType('array', $arr); @@ -66,22 +66,22 @@ function takesArray(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfTrue(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringArrayIfTrue($arr)) { assertType('array', $arr); } else { - assertType('array', $arr); + assertType('array', $arr); } } /** * @param mixed[] $arr */ function takesArrayIfTrue1(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (!validateStringArrayIfTrue($arr)) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType('array', $arr); } @@ -91,12 +91,12 @@ function takesArrayIfTrue1(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfFalse(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (!validateStringArrayIfFalse($arr)) { assertType('array', $arr); } else { - assertType('array', $arr); + assertType('array', $arr); } } @@ -104,10 +104,10 @@ function takesArrayIfFalse(array $arr) : void { * @param mixed[] $arr */ function takesArrayIfFalse1(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringArrayIfFalse($arr)) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType('array', $arr); } @@ -117,7 +117,7 @@ function takesArrayIfFalse1(array $arr) : void { * @param mixed[] $arr */ function takesStringOrIntArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringOrIntArray($arr)) { assertType('array', $arr); @@ -130,7 +130,7 @@ function takesStringOrIntArray(array $arr) : void { * @param mixed[] $arr */ function takesStringOrNonEmptyIntArray(array $arr) : void { - assertType('array', $arr); + assertType('array', $arr); if (validateStringOrNonEmptyIntArray($arr)) { assertType('array', $arr); diff --git a/tests/PHPStan/Analyser/nsrt/assert-methods.php b/tests/PHPStan/Analyser/nsrt/assert-methods.php index f6609f3f8f..6c278c21ed 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-methods.php +++ b/tests/PHPStan/Analyser/nsrt/assert-methods.php @@ -37,7 +37,7 @@ public function doBar($mixed) public function doBar2(array $objects) { self::doFoo($objects, stdClass::class); - assertType('array', $objects); + assertType('array', $objects); } /** @@ -47,7 +47,7 @@ public function doBar2(array $objects) public function doBar3(array $strings) { self::doFoo($strings, stdClass::class); - assertType('array>', $strings); + assertType('array>', $strings); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-1209.php b/tests/PHPStan/Analyser/nsrt/bug-1209.php index fff8d13dff..4b4ce770d1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-1209.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1209.php @@ -13,7 +13,7 @@ public function sayHello($value): void { $isArray = is_array($value); if($isArray){ - assertType('array', $value); + assertType('array', $value); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-1233.php b/tests/PHPStan/Analyser/nsrt/bug-1233.php index 7d70679585..17267e0001 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-1233.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1233.php @@ -10,18 +10,18 @@ public function toArray($value): array { assertType('mixed', $value); if (is_array($value)) { - assertType('array', $value); + assertType('array', $value); return $value; } - assertType('mixed~array', $value); + assertType('mixed~array', $value); if (is_iterable($value)) { assertType('Traversable', $value); return iterator_to_array($value); } - assertType('mixed~array', $value); + assertType('mixed~array', $value); throw new \LogicException(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3446.php b/tests/PHPStan/Analyser/nsrt/bug-3446.php index cc02033da3..c01b4bdbdc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3446.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3446.php @@ -15,7 +15,7 @@ public function takesString(string $s) : void{} public function main2(string $input) : void{ if(is_array($var = json_decode($input))){ - assertType('array', $var); + assertType('array', $var); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6329.php b/tests/PHPStan/Analyser/nsrt/bug-6329.php index c6b49e87e4..b31842e05f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6329.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6329.php @@ -106,19 +106,19 @@ function true($a): void function nonEmptyArray1($a): void { if (is_array($a) && [] !== $a || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if ([] !== $a && is_array($a) || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && [] !== $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || [] !== $a && is_array($a)) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -128,11 +128,11 @@ function nonEmptyArray1($a): void function nonEmptyArray2($a): void { if (is_array($a) && count($a) > 0 || null === $a) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } if (null === $a || is_array($a) && count($a) > 0) { - assertType('non-empty-array|null', $a); + assertType('non-empty-array|null', $a); } } @@ -155,7 +155,7 @@ function inverse($a, $b, $c): void if (null !== $c && (!is_array($c) || count($c) <= 0)) { } else { - assertType('non-empty-array|null', $c); + assertType('non-empty-array|null', $c); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php index 057067411c..c60d776d38 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php @@ -32,14 +32,14 @@ public function test3(array $options): void $curlHandle = curl_init(); foreach (self::$options as $type => $curlOptions) { foreach ($curlOptions as $name => $curlOption) { - \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); if (isset($options[$type][$name])) { - \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); if ($type === 'ssl' && $name === 'verify_peer_name') { - \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); curl_setopt($curlHandle, $curlOption, $options[$type][$name] === true ? 2 : $options[$type][$name]); } else { - \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); + \PHPStan\Testing\assertType('array{http: array{header: array, proxy?: string, request_fulluri: bool}, ssl?: array}', $options); curl_setopt($curlHandle, $curlOption, $options[$type][$name]); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7915.php b/tests/PHPStan/Analyser/nsrt/bug-7915.php index 1dee1a63d5..a2e63375a7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7915.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7915.php @@ -63,7 +63,7 @@ public function setConfigMimicConditionalParamType($name, $value): void function to_utf8($data, bool $isArray = false) { if ($isArray) { - assertType('array', $data); + assertType('array', $data); if (is_array($data)) { // always true foreach ($data as $k => $value) { $data[$k] = to_utf8($value, is_array($value)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662.php b/tests/PHPStan/Analyser/nsrt/bug-9662.php index 4da94355c7..d88555a863 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9662.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9662.php @@ -11,74 +11,74 @@ */ function doFoo(string $s, $a, $strings, $mixed) { if (in_array('foo', $a, true)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('foo', $a, false)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('foo', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('0', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array('1', $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array(true, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array(false, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a, true)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a, false)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($s, $a)) { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } else { - assertType("array", $a); + assertType("array", $a); } - assertType('array', $a); + assertType('array', $a); if (in_array($mixed, $strings, true)) { assertType('non-empty-array', $strings); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9734.php b/tests/PHPStan/Analyser/nsrt/bug-9734.php index ce75fa7197..353b1d91f9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9734.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9734.php @@ -17,7 +17,7 @@ public function doFoo(array $a): void if (array_is_list($a)) { assertType('list', $a); } else { - assertType('array', $a); // could be non-empty-array + assertType('array', $a); // could be non-empty-array } } @@ -40,7 +40,7 @@ public function doFoo3(array $a): void if (array_is_list($a)) { assertType('non-empty-list', $a); } else { - assertType('non-empty-array', $a); + assertType('non-empty-array', $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php index 89bfa50a22..596e97634a 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php @@ -89,5 +89,5 @@ function invariant(bool $condition, string $message): void function (mixed $value) { invariant(is_array($value), 'must be array'); - assertType('array', $value); + assertType('array', $value); }; diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-array.php b/tests/PHPStan/Analyser/nsrt/filter-var-array.php index 38db914722..1151d370c1 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-array.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-array.php @@ -85,11 +85,11 @@ function mixedInput(mixed $input): void ], false)); // filter flag with add_empty=default - assertType('array', filter_var_array($input, FILTER_VALIDATE_INT)); + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT)); // filter flag with add_empty=true - assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, true)); + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, true)); // filter flag with add_empty=false - assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, false)); + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, false)); $filter = [ 'filter' => FILTER_VALIDATE_INT, diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 873ad66b6d..60840465c1 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -163,8 +163,8 @@ function testF($arrayOfInt, $callableOrNull) return $a; })); assertType('array', f($arrayOfInt, $callableOrNull)); - assertType('array', f($arrayOfInt, null)); - assertType('array', f($arrayOfInt, '')); + assertType('array', f($arrayOfInt, null)); + assertType('array', f($arrayOfInt, '')); } /** diff --git a/tests/PHPStan/Analyser/nsrt/globals.php b/tests/PHPStan/Analyser/nsrt/globals.php index 7b7b7b8a01..da2ae35787 100644 --- a/tests/PHPStan/Analyser/nsrt/globals.php +++ b/tests/PHPStan/Analyser/nsrt/globals.php @@ -1,11 +1,11 @@ ', $GLOBALS); +\PHPStan\Testing\assertType('array', $_SERVER); +\PHPStan\Testing\assertType('array', $_GET); +\PHPStan\Testing\assertType('array', $_POST); +\PHPStan\Testing\assertType('array', $_FILES); +\PHPStan\Testing\assertType('array', $_COOKIE); +\PHPStan\Testing\assertType('array', $_SESSION); +\PHPStan\Testing\assertType('array', $_REQUEST); +\PHPStan\Testing\assertType('array', $_ENV); diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php index 8b470ba316..d1e8fd92a0 100644 --- a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -44,14 +44,14 @@ public function doFoo(array $errorMessages): void */ public function doBar(array $result): void { - assertType('array', $result); + assertType('array', $result); assert($result['totals']['file_errors'] === 3); - assertType("array", $result); + assertType("array", $result); assertType("mixed", $result['totals']); assertType('3', $result['totals']['file_errors']); assertType('mixed', $result['totals']['errors']); assert($result['totals']['errors'] === 0); - assertType("array", $result); + assertType("array", $result); assertType("mixed", $result['totals']); assertType('3', $result['totals']['file_errors']); assertType('0', $result['totals']['errors']); diff --git a/tests/PHPStan/Analyser/nsrt/memcache-get.php b/tests/PHPStan/Analyser/nsrt/memcache-get.php index 735e85b545..533e483682 100644 --- a/tests/PHPStan/Analyser/nsrt/memcache-get.php +++ b/tests/PHPStan/Analyser/nsrt/memcache-get.php @@ -10,5 +10,5 @@ function (): void { $memcache = new Memcache(); assertType('mixed', $memcache->get("key1")); - assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); + assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); }; diff --git a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php index 59fa2afa13..c544802655 100644 --- a/tests/PHPStan/Analyser/nsrt/mixed-subtract.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-subtract.php @@ -21,7 +21,7 @@ function subtract(mixed $m, $moreThenFalsy) { assertType('bool', (bool) $m); } if (!is_array($m)) { - assertType('mixed~array', $m); + assertType('mixed~array', $m); assertType('bool', (bool) $m); } @@ -55,7 +55,7 @@ function subtract(mixed $m, $moreThenFalsy) { } if ($m != 0 && !is_array($m) && $m != null && !is_object($m)) { // subtract more types then falsy - assertType("mixed~(0|0.0|''|'0'|array|object|false|null)", $m); + assertType("mixed~(0|0.0|''|'0'|array|object|false|null)", $m); assertType('true', (bool) $m); } } diff --git a/tests/PHPStan/Analyser/nsrt/mixed-to-number.php b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php index 97cc9d0842..fee2d7bed4 100644 --- a/tests/PHPStan/Analyser/nsrt/mixed-to-number.php +++ b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php @@ -35,7 +35,7 @@ function doFoo($mixed, int $i, float $f, $numericS) { } if (!is_array($mixed)) { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('(float|int)', $mixed + $mixed); } } diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index 82e09e0bd3..fc70c128e9 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -14,9 +14,9 @@ function doFoo(string $x, array $arr): void { assertType('string', $x); if ((bool) array_search($x, $arr, true)) { - assertType('non-empty-array', $arr); + assertType('non-empty-array', $arr); } else { - assertType('array', $arr); + assertType('array', $arr); } assertType('string', $x); diff --git a/tests/PHPStan/Analyser/nsrt/native-expressions.php b/tests/PHPStan/Analyser/nsrt/native-expressions.php index 72f5a47e7e..abe041686a 100644 --- a/tests/PHPStan/Analyser/nsrt/native-expressions.php +++ b/tests/PHPStan/Analyser/nsrt/native-expressions.php @@ -34,7 +34,7 @@ public function __construct( /** @var non-empty-array */ private array $array ){ - assertType('non-empty-array', $this->array); + assertType('non-empty-array', $this->array); assertNativeType('array', $this->array); if(count($array) === 0){ throw new \InvalidArgumentException(); diff --git a/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php b/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php index b9e0920141..a115a32359 100644 --- a/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/prestashop-breakdowns-empty-array.php @@ -21,13 +21,13 @@ public function getTaxBreakdown($mixed, $arrayMixed): void foreach ($breakdowns as $type => $bd) { if (empty($bd)) { - assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); unset($breakdowns[$type]); - assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); } } - assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); + assertType('array{product_tax?: mixed, shipping_tax?: array, ecotax_tax?: array, wrapping_tax?: array}', $breakdowns); } public function doFoo(): void diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index f5511bfc33..41d62d1c36 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -107,7 +107,7 @@ public function dataFunctions(): array ]), new UnionType([ new ConstantBooleanType(false), - new ArrayType(new MixedType(true), new MixedType(true)), + new ArrayType(new MixedType(), new MixedType()), ]), false, ], diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index b45c120089..0383dadc52 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -551,7 +551,7 @@ public function testBug8356(): void { $this->analyse([__DIR__ . '/data/bug-8356.php'], [ [ - "Offset 'x' might not exist on array|string.", + "Offset 'x' might not exist on array|string.", 7, ], ]); diff --git a/tests/PHPStan/Rules/Classes/data/bug-5333.php b/tests/PHPStan/Rules/Classes/data/bug-5333.php index f38c46cddf..67845acd46 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-5333.php +++ b/tests/PHPStan/Rules/Classes/data/bug-5333.php @@ -27,7 +27,7 @@ public function sayHello($foo): Route } assertType('array', $foo); - assertNativeType('array', $foo); + assertNativeType('array', $foo); assertType('Bug5333\Route', $res); assertNativeType('Bug5333\Route', $res); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index eda1ad4ee3..98927b4bd2 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1688,22 +1688,22 @@ public function testCountArrayShift(): void if (PHP_VERSION_ID < 80000) { $errors = [ [ - 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 'Parameter #1 $var of function count expects array|Countable, array|false given.', 8, ], [ - 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 'Parameter #1 $var of function count expects array|Countable, array|false given.', 16, ], ]; } else { $errors = [ [ - 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 'Parameter #1 $value of function count expects array|Countable, array|false given.', 8, ], [ - 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 'Parameter #1 $value of function count expects array|Countable, array|false given.', 16, ], ]; diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index ed96439687..88b0eb949f 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -1949,7 +1949,7 @@ public function testOnlyRelevantUnableToResolveTemplateType(): void $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/only-relevant-unable-to-resolve-template-type.php'], [ [ - 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', + 'Parameter #1 $a of method OnlyRelevantUnableToResolve\Foo::doBaz() expects array, int given.', 41, ], [ @@ -2260,11 +2260,11 @@ public function testLiteralString(): void 58, ], [ - 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', 60, ], [ - 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', 65, ], [ diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index c3842d461b..fe23c44a83 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -546,15 +546,15 @@ public function testDiscussion7004(): void $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/discussion-7004.php'], [ [ - 'Parameter #1 $data of static method Discussion7004\Foo::fromArray1() expects array, array given.', + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray1() expects array, array given.', 46, ], [ - 'Parameter #1 $data of static method Discussion7004\Foo::fromArray2() expects array{array{newsletterName: string, subscriberCount: int}}, array given.', + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray2() expects array{array{newsletterName: string, subscriberCount: int}}, array given.', 47, ], [ - 'Parameter #1 $data of static method Discussion7004\Foo::fromArray3() expects array{newsletterName: string, subscriberCount: int}, array given.', + 'Parameter #1 $data of static method Discussion7004\Foo::fromArray3() expects array{newsletterName: string, subscriberCount: int}, array given.', 48, ], ]); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 4c19971d5e..5ebfb238e7 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -901,7 +901,7 @@ public function testMagicSerialization(): void { $this->analyse([__DIR__ . '/data/magic-serialization.php'], [ [ - 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', + 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', 23, ], [ @@ -928,7 +928,7 @@ public function testMagicSignatures(): void 43, ], [ - 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', + 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', 47, ], [ diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php index f3ea56d12f..c092304ff1 100644 --- a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -52,19 +52,19 @@ public function testBug10130(): void { $this->analyse([__DIR__ . '/data/bug-10130.php'], [ [ - 'PHPDoc tag @var with type array is not subtype of type array.', + 'PHPDoc tag @var with type array is not subtype of type array.', 14, ], [ - 'PHPDoc tag @var with type array is not subtype of type list.', + 'PHPDoc tag @var with type array is not subtype of type list.', 17, ], [ - 'PHPDoc tag @var with type array is not subtype of type array{id: int}.', + 'PHPDoc tag @var with type array is not subtype of type array{id: int}.', 20, ], [ - 'PHPDoc tag @var with type array is not subtype of type list.', + 'PHPDoc tag @var with type array is not subtype of type list.', 23, ], ]); diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 155c0d6ea4..8a1ce3ba51 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -466,7 +466,7 @@ public function dataReportWrongType(): iterable 204, ], [ - 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', + 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', 235, ], ]]; diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 3616824c7f..e75b7cec10 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -656,15 +656,15 @@ public function testBug11617(): void $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-11617.php'], [ [ - 'Property Bug11617\HelloWorld::$params (array) does not accept array.', + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', 14, ], [ - 'Property Bug11617\HelloWorld::$params (array) does not accept array.', + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', 16, ], [ - 'Property Bug11617\HelloWorld::$params (array) does not accept array.', + 'Property Bug11617\HelloWorld::$params (array) does not accept array|string>.', 21, ], ]); diff --git a/tests/PHPStan/Rules/Variables/data/bug-8113.php b/tests/PHPStan/Rules/Variables/data/bug-8113.php index 2c2807e48f..97b5841941 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-8113.php +++ b/tests/PHPStan/Rules/Variables/data/bug-8113.php @@ -20,25 +20,25 @@ function () { ), ); - assertType('array', $review); + assertType('array>', $review); if ( array_key_exists('review', $review['SurveyInvitation']) && $review['SurveyInvitation']['review'] === null ) { - assertType("array&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + assertType("array>&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); $review['Review'] = [ 'id' => null, 'text' => null, 'answer' => null, ]; - assertType("non-empty-array&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); unset($review['SurveyInvitation']['review']); - assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); } - assertType('array', $review); + assertType('array>', $review); if (array_key_exists('User', $review['Review'])) { - assertType("array&hasOffsetValue('Review', array&hasOffset('User'))", $review); + assertType("array>&hasOffsetValue('Review', array&hasOffset('User'))", $review); $review['User'] = $review['Review']['User']; assertType("hasOffsetValue('Review', array&hasOffset('User'))&hasOffsetValue('User', mixed)&non-empty-array", $review); unset($review['Review']['User']); From 4007e653b9a9fe9db7d8c4b9d977ca4175786ff1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 16:11:41 +0200 Subject: [PATCH 010/220] Issue bot - let all comments about improved `ArrayType::describe()` through --- issue-bot/src/Console/EvaluateCommand.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php index 0f8d05a8d8..f18941039b 100644 --- a/issue-bot/src/Console/EvaluateCommand.php +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -158,10 +158,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $postComments = (bool) $input->getOption('post-comments'); if ($postComments) { - if (count($toPost) > 20) { - $output->writeln('Too many comments to post, something is probably wrong.'); - return 1; - } foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { $text = sprintf( "%s After [the latest push in %s](https://github.com/phpstan/phpstan-src/compare/%s...%s), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/%s):\n\n```diff\n%s```", From 2de40d19b19b150eb4c05171e24d9377fc529c1a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 17:40:02 +0200 Subject: [PATCH 011/220] Revert "Issue bot - let all comments about improved `ArrayType::describe()` through" This reverts commit 4007e653b9a9fe9db7d8c4b9d977ca4175786ff1. --- issue-bot/src/Console/EvaluateCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php index f18941039b..0f8d05a8d8 100644 --- a/issue-bot/src/Console/EvaluateCommand.php +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -158,6 +158,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $postComments = (bool) $input->getOption('post-comments'); if ($postComments) { + if (count($toPost) > 20) { + $output->writeln('Too many comments to post, something is probably wrong.'); + return 1; + } foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { $text = sprintf( "%s After [the latest push in %s](https://github.com/phpstan/phpstan-src/compare/%s...%s), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/%s):\n\n```diff\n%s```", From 856208a53b3b1c4eada01f7e4892573d756cdf3f Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:28:48 +0000 Subject: [PATCH 012/220] Update phpdoc-parser --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 866afda425..5b816f8746 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.25.0.21", "phpstan/php-8-stubs": "0.3.111", - "phpstan/phpdoc-parser": "1.32.0", + "phpstan/phpdoc-parser": "1.33.0", "psr/http-message": "^1.1", "react/async": "^3", "react/child-process": "^0.7", diff --git a/composer.lock b/composer.lock index aae4ed9cd0..91ca3e1931 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a76e95ec9a6020d405d9495cb67cf0ce", + "content-hash": "07635a5e2bdedfa64a641735fba09971", "packages": [ { "name": "clue/ndjson-react", @@ -2288,16 +2288,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.32.0", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", "shasum": "" }, "require": { @@ -2329,9 +2329,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2024-09-26T07:23:32+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "psr/container", From 107a7e38e2173d8b8b4f18e5d592fc8ead02b96a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 13:35:47 +0200 Subject: [PATCH 013/220] Support for non-empty-array and non-empty-list array shape kind --- src/PhpDoc/TypeNodeResolver.php | 12 +++++++++++- .../Analyser/nsrt/array-shape-list-optional.php | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 6e483222d7..43f93cb8f5 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -1016,10 +1016,20 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } $arrayType = $builder->getArray(); - if ($typeNode->kind === ArrayShapeNode::KIND_LIST) { + if (in_array($typeNode->kind, [ + ArrayShapeNode::KIND_LIST, + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ], true)) { $arrayType = AccessoryArrayListType::intersectWith($arrayType); } + if (in_array($typeNode->kind, [ + ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ], true)) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + return $arrayType; } diff --git a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php index 0eaa4471d2..f059d14c4d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php +++ b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php @@ -9,16 +9,22 @@ class Foo /** * @param list{0: string, 1: int, 2?: string, 3?: string} $valid1 + * @param non-empty-list{0: string, 1: int, 2?: string, 3?: string} $valid2 + * @param non-empty-array{0?: string, 1?: int, 2?: string, 3?: string} $valid3 * @param list{0: string, 1: int, 2?: string, 4?: string} $invalid1 * @param list{0: string, 1: int, 2?: string, foo?: string} $invalid2 */ public function doFoo( $valid1, + $valid2, + $valid3, $invalid1, $invalid2 ): void { assertType('array{0: string, 1: int, 2?: string, 3?: string}&list', $valid1); + assertType('array{0: string, 1: int, 2?: string, 3?: string}&list', $valid2); + assertType('array{0?: string, 1?: int, 2?: string, 3?: string}&non-empty-array', $valid3); assertType('*NEVER*', $invalid1); assertType('*NEVER*', $invalid2); } From 96bd303c6c818b8ddd270ebc6101a585849251e4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 13:45:56 +0200 Subject: [PATCH 014/220] Fix build --- changelog-generator/phpstan.neon | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog-generator/phpstan.neon b/changelog-generator/phpstan.neon index d187d34c96..a1fee9d3a7 100644 --- a/changelog-generator/phpstan.neon +++ b/changelog-generator/phpstan.neon @@ -11,4 +11,6 @@ parameters: paths: - src - run.php - checkGenericClassInNonGenericObjectType: false + ignoreErrors: + - + identifier: missingType.generics From 67fbfaee6585c2d47485dc2a159ee76d3ed02b35 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 12 Oct 2024 13:09:35 +0200 Subject: [PATCH 015/220] Refactor IntersectionType::describe() --- phpstan-baseline.neon | 4 +- src/Type/IntersectionType.php | 152 ++++++++---- .../Analyser/LegacyNodeScopeResolverTest.php | 8 +- tests/PHPStan/Analyser/TypeSpecifierTest.php | 4 +- tests/PHPStan/Analyser/data/param-out.php | 2 +- tests/PHPStan/Analyser/nsrt/array-chunk.php | 8 +- .../Analyser/nsrt/array-column-php82.php | 6 +- tests/PHPStan/Analyser/nsrt/array-flip.php | 10 +- .../Analyser/nsrt/array-intersect-key.php | 6 +- .../nsrt/array-is-list-type-specifying.php | 8 +- tests/PHPStan/Analyser/nsrt/array-reverse.php | 2 +- .../nsrt/array-shape-list-optional.php | 8 +- tests/PHPStan/Analyser/nsrt/array-slice.php | 6 +- .../Analyser/nsrt/assert-intersected.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-10721.php | 50 ++-- .../PHPStan/Analyser/nsrt/bug-11518-types.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-2112.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-2911.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4099.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-4117.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4398.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4565.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-4708.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-6859.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-7805.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-9734.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-9985.php | 2 +- .../Analyser/nsrt/composer-array-bug.php | 4 +- .../Analyser/nsrt/conditional-vars.php | 4 +- ...namic-return-type-extension-regression.php | 4 +- .../Analyser/nsrt/has-offset-type-bug.php | 4 +- tests/PHPStan/Analyser/nsrt/list-count.php | 12 +- tests/PHPStan/Analyser/nsrt/list-type.php | 16 +- .../PHPStan/Analyser/nsrt/non-empty-array.php | 2 +- tests/PHPStan/Analyser/nsrt/shuffle.php | 44 ++-- tests/PHPStan/Analyser/nsrt/sort.php | 6 +- .../Rules/Comparison/data/bug-4708.php | 2 +- .../PHPStan/Rules/Functions/data/bug-3931.php | 2 +- .../PHPStan/Rules/Functions/data/bug-7156.php | 4 +- .../Rules/Methods/ReturnTypeRuleTest.php | 2 +- .../PHPStan/Rules/Variables/data/bug-3391.php | 4 +- .../PHPStan/Rules/Variables/data/bug-7417.php | 4 +- .../PHPStan/Rules/Variables/data/bug-8113.php | 12 +- tests/PHPStan/Type/IntersectionTypeTest.php | 217 +++++++++++++++++- tests/PHPStan/Type/TypeCombinatorTest.php | 24 +- tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 121 +++++++++- 46 files changed, 601 insertions(+), 199 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a652b6a836..122462ff00 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1314,7 +1314,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 1 + count: 3 path: src/Type/IntersectionType.php - @@ -1332,7 +1332,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 1 + count: 3 path: src/Type/IntersectionType.php - diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 2dec6cc456..496910eab3 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; @@ -45,7 +46,6 @@ use function ksort; use function md5; use function sprintf; -use function str_starts_with; use function strcasecmp; use function strlen; use function substr; @@ -347,6 +347,10 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $nonEmptyStr = false; $nonFalsyStr = false; + $isList = $this->isList()->yes(); + $isArray = $this->isArray()->yes(); + $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes(); + $describedTypes = []; foreach ($this->getSortedTypes() as $i => $type) { if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType @@ -379,10 +383,45 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { - $typesToDescribe[$i] = $type; - $skipTypeNames[] = 'array'; - continue; + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $valueTypeDescription = ''; + if (!$isMixedValueType) { + $valueTypeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription; + } else { + $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed(); + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $typeDescription = ''; + if (!$isMixedKeyType) { + $typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level)); + } elseif (!$isMixedValueType) { + $typeDescription = sprintf('<%s>', $valueType->describe($level)); + } + + $describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription; + } + continue; + } elseif ($type instanceof ConstantArrayType) { + $description = $type->describe($level); + $descriptionWithoutKind = substr($description, strlen('array')); + $begin = $isList ? 'list' : 'array'; + if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $begin = 'non-empty-' . $begin; + } + + $describedTypes[$i] = $begin . $descriptionWithoutKind; + continue; + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + continue; + } } if ($type instanceof CallableType && $type->isCommonCallable()) { @@ -404,7 +443,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $typesToDescribe[$i] = $type; } - $describedTypes = []; foreach ($baseTypes as $i => $type) { $typeDescription = $type->describe($level); @@ -418,36 +456,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } } - if ( - str_starts_with($typeDescription, 'array<') - && in_array('array', $skipTypeNames, true) - ) { - $nonEmpty = false; - $typeName = 'array'; - foreach ($typesToDescribe as $j => $typeToDescribe) { - if ( - $typeToDescribe instanceof AccessoryArrayListType - && substr($typeDescription, 0, strlen('array, ')) === 'array, ' - ) { - $typeName = 'list'; - $typeDescription = 'array<' . substr($typeDescription, strlen('array, ')); - } elseif ($typeToDescribe instanceof NonEmptyArrayType) { - $nonEmpty = true; - } else { - continue; - } - - unset($typesToDescribe[$j]); - } - - if ($nonEmpty) { - $typeName = 'non-empty-' . $typeName; - } - - $describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<')); - continue; - } - if (in_array($typeDescription, $skipTypeNames, true)) { continue; } @@ -1139,6 +1147,10 @@ public function toPhpDocNode(): TypeNode $nonEmptyStr = false; $nonFalsyStr = false; + $isList = $this->isList()->yes(); + $isArray = $this->isArray()->yes(); + $isNonEmptyArray = $this->isIterableAtLeastOnce()->yes(); + $describedTypes = []; foreach ($this->getSortedTypes() as $i => $type) { if ($type instanceof AccessoryNonEmptyStringType @@ -1168,11 +1180,70 @@ public function toPhpDocNode(): TypeNode $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { - $typesToDescribe[$i] = $type; - $skipTypeNames[] = 'array'; - continue; + + if ($isList || $isArray) { + if ($type instanceof ArrayType) { + $keyType = $type->getKeyType(); + $valueType = $type->getItemType(); + if ($isList) { + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list'); + if (!$isMixedValueType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $valueType->toPhpDocNode(), + ]); + } else { + $describedTypes[$i] = $identifierTypeNode; + } + } else { + $isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed(); + $isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed(); + $identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array'); + if (!$isMixedKeyType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $keyType->toPhpDocNode(), + $valueType->toPhpDocNode(), + ]); + } elseif (!$isMixedValueType) { + $describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [ + $valueType->toPhpDocNode(), + ]); + } else { + $describedTypes[$i] = $identifierTypeNode; + } + } + continue; + } elseif ($type instanceof ConstantArrayType) { + $constantArrayTypeNode = $type->toPhpDocNode(); + if ($constantArrayTypeNode instanceof ArrayShapeNode) { + $newKind = $constantArrayTypeNode->kind; + if ($isList) { + if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST; + } else { + $newKind = ArrayShapeNode::KIND_LIST; + } + } elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) { + $newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY; + } + + if ($newKind !== $constantArrayTypeNode->kind) { + if ($constantArrayTypeNode->sealed) { + $constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind); + } else { + $constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind); + } + } + + $describedTypes[$i] = $constantArrayTypeNode; + continue; + } + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + continue; + } } + if (!$type instanceof AccessoryType) { $baseTypes[$i] = $type; continue; @@ -1186,7 +1257,6 @@ public function toPhpDocNode(): TypeNode $typesToDescribe[$i] = $type; } - $describedTypes = []; foreach ($baseTypes as $i => $type) { $typeNode = $type->toPhpDocNode(); if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') { diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index d24d24338d..e97a5f4a06 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -4846,7 +4846,7 @@ public function dataArrayFunctions(): array 'array_pop($stringKeys)', ], [ - 'array&hasOffsetValue(\'baz\', stdClass)', + 'non-empty-array&hasOffsetValue(\'baz\', stdClass)', '$stdClassesWithIsset', ], [ @@ -8077,7 +8077,7 @@ public function dataArrayKeysInBranches(): array '$array', ], [ - 'array&hasOffsetValue(\'key\', mixed)', + 'non-empty-array&hasOffsetValue(\'key\', mixed)', '$generalArray', ], [ @@ -8563,11 +8563,11 @@ public function dataIsset(): array '$mixedIsset', ], [ - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', '$mixedArrayKeyExists', ], [ - 'array&hasOffsetValue(\'a\', int)', + 'non-empty-array&hasOffsetValue(\'a\', int)', '$integers', ], [ diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 99cbb8db71..36bf122d87 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1064,7 +1064,7 @@ public function dataCondition(): iterable new Arg(new Variable('array')), ]), [ - '$array' => 'array&hasOffset(\'foo\')', + '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ '$array' => '~hasOffset(\'foo\')', @@ -1112,7 +1112,7 @@ public function dataCondition(): iterable new Arg(new Variable('array')), ]), [ - '$array' => 'array&hasOffset(\'foo\')', + '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ '$array' => '~hasOffset(\'foo\')', diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index d172b358e1..36d7837034 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -240,7 +240,7 @@ function foo16() { function fooShuffle() { $array = ["foo" => 123, "bar" => 456]; shuffle($array); - assertType('non-empty-array<0|1, 123|456>&list', $array); + assertType('non-empty-list<123|456>', $array); $emptyArray = []; shuffle($emptyArray); diff --git a/tests/PHPStan/Analyser/nsrt/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php index c0b79ffbae..cedb50ddb7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-chunk.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk.php @@ -60,8 +60,8 @@ public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) * @param int<50, max> $bigger50 */ public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) { - assertType('list>', array_chunk($arr, $positiveInt)); - assertType('list>', array_chunk($arr, $bigger50)); + assertType('list', array_chunk($arr, $positiveInt)); + assertType('list', array_chunk($arr, $bigger50)); } /** @@ -78,11 +78,11 @@ function testLimits(array $arr, int $oneToFour, int $tooBig) { public function offsets(array $arr, array $map): void { if (array_key_exists('foo', $arr)) { - assertType('non-empty-list>', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2)); assertType('non-empty-list', array_chunk($arr, 2, true)); } if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { - assertType('non-empty-list>', array_chunk($arr, 2)); + assertType('non-empty-list', array_chunk($arr, 2)); assertType('non-empty-list', array_chunk($arr, 2, true)); } diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php index 62350f5992..07dc05f399 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -175,7 +175,7 @@ public function testImprecise5(array $array): void assertType('list', array_column($array, 'nodeName')); assertType('array', array_column($array, 'nodeName', 'tagName')); assertType('array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('array', array_column($array, 'nodeName', 'foo')); assertType('array', array_column($array, null, 'foo')); @@ -187,7 +187,7 @@ public function testObjects1(array $array): void assertType('non-empty-list', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); @@ -199,7 +199,7 @@ public function testObjects2(array $array): void assertType('array{string}', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); diff --git a/tests/PHPStan/Analyser/nsrt/array-flip.php b/tests/PHPStan/Analyser/nsrt/array-flip.php index 2f02f1e733..ef91f40c36 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip.php @@ -71,25 +71,25 @@ function foo8($mixed) function foo10(array $array) { if (array_key_exists('foo', $array)) { - assertType('array&hasOffset(\'foo\')', $array); + assertType('non-empty-array&hasOffset(\'foo\')', $array); assertType('array', array_flip($array)); } if (array_key_exists('foo', $array) && is_int($array['foo'])) { - assertType("array&hasOffsetValue('foo', int)", $array); + assertType("non-empty-array&hasOffsetValue('foo', int)", $array); assertType('array', array_flip($array)); } if (array_key_exists('foo', $array) && $array['foo'] === 17) { - assertType("array&hasOffsetValue('foo', 17)", $array); - assertType("array&hasOffsetValue(17, 'foo')", array_flip($array)); + assertType("non-empty-array&hasOffsetValue('foo', 17)", $array); + assertType("non-empty-array&hasOffsetValue(17, 'foo')", array_flip($array)); } if ( array_key_exists('foo', $array) && $array['foo'] === 17 && array_key_exists('bar', $array) && $array['bar'] === 17 ) { - assertType("array&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array); + assertType("non-empty-array&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array); assertType("*NEVER*", array_flip($array)); // this could be array&hasOffsetValue(17, 'bar') according to https://3v4l.org/1TAFk } } diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php index bf620b508b..288ba539a2 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php @@ -48,7 +48,7 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void assertType('array{}', array_intersect_key($arr, $otherArrs)); if (array_key_exists(17, $arr2)) { - assertType('array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar'])); + assertType('non-empty-array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar'])); /** @var array $otherArrs */ assertType('array', array_intersect_key($arr2, $otherArrs)); /** @var array $otherArrs */ @@ -56,7 +56,7 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void } if (array_key_exists(17, $arr2) && $arr2[17] === 'foo') { - assertType("array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar'])); + assertType("non-empty-array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar'])); /** @var array $otherArrs */ assertType('array', array_intersect_key($arr2, $otherArrs)); /** @var array $otherArrs */ @@ -79,7 +79,7 @@ public function arrayUnpacking(array $arrs, array $arrs2): void /** @param list $arr */ public function list(array $arr, array $otherArrs): void { - assertType('array<0|1, string>&list', array_intersect_key($arr, ['foo', 'bar'])); + assertType('list', array_intersect_key($arr, ['foo', 'bar'])); /** @var array $otherArrs */ assertType('array, string>', array_intersect_key($arr, $otherArrs)); /** @var array $otherArrs */ diff --git a/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php index aa21248ec6..1b788158d6 100644 --- a/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php @@ -6,7 +6,7 @@ function foo(array $foo) { if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } @@ -14,9 +14,9 @@ function foo(array $foo) { function foo2($foo) { if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { - assertType('mixed~list', $foo); + assertType('mixed~list', $foo); } } @@ -49,7 +49,7 @@ function foo4(array $foo) { /** @var array $foo */ if (array_is_list($foo)) { - assertType('list', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } diff --git a/tests/PHPStan/Analyser/nsrt/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php index 6f05e9b9f0..86a3bb72cf 100644 --- a/tests/PHPStan/Analyser/nsrt/array-reverse.php +++ b/tests/PHPStan/Analyser/nsrt/array-reverse.php @@ -73,7 +73,7 @@ public function mixed(mixed $mixed): void if (array_key_exists('foo', $mixed)) { assertType('non-empty-array', array_reverse($mixed)); - assertType("array&hasOffset('foo')", array_reverse($mixed, true)); + assertType("non-empty-array&hasOffset('foo')", array_reverse($mixed, true)); } } } diff --git a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php index f059d14c4d..10049a8317 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php +++ b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php @@ -9,7 +9,7 @@ class Foo /** * @param list{0: string, 1: int, 2?: string, 3?: string} $valid1 - * @param non-empty-list{0: string, 1: int, 2?: string, 3?: string} $valid2 + * @param non-empty-list{0?: string, 1?: int, 2?: string, 3?: string} $valid2 * @param non-empty-array{0?: string, 1?: int, 2?: string, 3?: string} $valid3 * @param list{0: string, 1: int, 2?: string, 4?: string} $invalid1 * @param list{0: string, 1: int, 2?: string, foo?: string} $invalid2 @@ -22,9 +22,9 @@ public function doFoo( $invalid2 ): void { - assertType('array{0: string, 1: int, 2?: string, 3?: string}&list', $valid1); - assertType('array{0: string, 1: int, 2?: string, 3?: string}&list', $valid2); - assertType('array{0?: string, 1?: int, 2?: string, 3?: string}&non-empty-array', $valid3); + assertType('list{0: string, 1: int, 2?: string, 3?: string}', $valid1); + assertType('non-empty-list{0?: string, 1?: int, 2?: string, 3?: string}', $valid2); + assertType('non-empty-array{0?: string, 1?: int, 2?: string, 3?: string}', $valid3); assertType('*NEVER*', $invalid1); assertType('*NEVER*', $invalid2); } diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php index e2faabd113..847f535df1 100644 --- a/tests/PHPStan/Analyser/nsrt/array-slice.php +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -15,7 +15,7 @@ class Foo public function nonEmpty(array $a, array $b, array $c): void { assertType('array', array_slice($a, 1)); - assertType('list', array_slice($b, 1)); + assertType('list', array_slice($b, 1)); assertType('array', array_slice($c, 1)); } @@ -94,11 +94,11 @@ public function offsets(array $arr): void { if (array_key_exists(1, $arr)) { assertType('non-empty-array', array_slice($arr, 1, null, false)); - assertType('hasOffset(1)&non-empty-array', array_slice($arr, 1, null, true)); + assertType('non-empty-array&hasOffset(1)', array_slice($arr, 1, null, true)); } if (array_key_exists(1, $arr) && $arr[1] === 'foo') { assertType('non-empty-array', array_slice($arr, 1, null, false)); - assertType("hasOffsetValue(1, 'foo')&non-empty-array", array_slice($arr, 1, null, true)); + assertType("non-empty-array&hasOffsetValue(1, 'foo')", array_slice($arr, 1, null, true)); } } diff --git a/tests/PHPStan/Analyser/nsrt/assert-intersected.php b/tests/PHPStan/Analyser/nsrt/assert-intersected.php index 17aa63957a..913f1e9034 100644 --- a/tests/PHPStan/Analyser/nsrt/assert-intersected.php +++ b/tests/PHPStan/Analyser/nsrt/assert-intersected.php @@ -26,5 +26,5 @@ public function assert(mixed $value): void; function intersection($assert, mixed $value): void { $assert->assert($value); - assertType('non-empty-list', $value); + assertType('non-empty-list', $value); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10721.php b/tests/PHPStan/Analyser/nsrt/bug-10721.php index cf7ce49272..52d511c163 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10721.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10721.php @@ -22,9 +22,9 @@ public function retrieve(?int $limit = 20): array assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list); shuffle($list); - assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", $list); + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", $list); - assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", array_slice($list, 0, max($limit, 1))); + assertType("non-empty-list<'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>", array_slice($list, 0, max($limit, 1))); return array_slice($list, 0, max($limit, 1)); } @@ -37,7 +37,7 @@ public function listVariants(): void assertType("array{2: 'zib', 4: 'zib 2'}", $arr); shuffle($arr); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $arr); + assertType("non-empty-list<'zib'|'zib 2'>", $arr); $list = [ 'zib', @@ -46,37 +46,37 @@ public function listVariants(): void assertType("array{'zib', 'zib 2'}", $list); shuffle($list); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $list); + assertType("non-empty-list<'zib'|'zib 2'>", $list); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2)); + assertType("list<'zib'|'zib 2'>", array_slice($list, -1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 1)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 1)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 1)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 1)); + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 1)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 1)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 1)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 1)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 2)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 2)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 2)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 2)); + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 2)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 2)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 2)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 2)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3)); + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3)); assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, -1, 3, true)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, true)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, true)); assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 1, 3, true)); // could be non-empty-array assertType("array<0|1, 'zib'|'zib 2'>", array_slice($list, 2, 3, true)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, false)); - assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, false)); - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, false)); // could be non-empty-array - assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, false)); + assertType("list<'zib'|'zib 2'>", array_slice($list, -1, 3, false)); + assertType("non-empty-list<'zib'|'zib 2'>", array_slice($list, 0, 3, false)); + assertType("list<'zib'|'zib 2'>", array_slice($list, 1, 3, false)); // could be non-empty-array + assertType("list<'zib'|'zib 2'>", array_slice($list, 2, 3, false)); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php index 4f66f4f0af..19d5aeb15f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php @@ -12,12 +12,12 @@ function blah(array $a): array { if (!array_key_exists('thing', $a)) { $a['thing'] = 'bla'; - assertType('hasOffsetValue(\'thing\', \'bla\')&non-empty-array', $a); + assertType('non-empty-array&hasOffsetValue(\'thing\', \'bla\')', $a); } else { - assertType('array&hasOffset(\'thing\')', $a); + assertType('non-empty-array&hasOffset(\'thing\')', $a); } - assertType('array&hasOffsetValue(\'thing\', mixed)', $a); + assertType('non-empty-array&hasOffsetValue(\'thing\', mixed)', $a); return $a; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2112.php b/tests/PHPStan/Analyser/nsrt/bug-2112.php index a7d33d1538..65634c415b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2112.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2112.php @@ -19,7 +19,7 @@ public function doBar(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('hasOffsetValue(0, null)&non-empty-array', $foos); + assertType('non-empty-array&hasOffsetValue(0, null)', $foos); } /** @return self[] */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php index 1d75efdcbe..3cfbd308f1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2911.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -134,6 +134,6 @@ private function getResultSettings(array $settings): array function foo(array $array): void { $array['bar'] = 'string'; - assertType("hasOffsetValue('bar', 'string')&non-empty-array", $array); + assertType("non-empty-array&hasOffsetValue('bar', 'string')", $array); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099.php b/tests/PHPStan/Analyser/nsrt/bug-4099.php index 0a8d1b4b48..5e5eb30ca2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4099.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4099.php @@ -22,20 +22,20 @@ function arrayHint(array $arr): void throw new \Exception('no key "key" found.'); } assertType('array{key: array{inner: mixed}}', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); assertType('array{inner: mixed}', $arr['key']); assertNativeType('mixed', $arr['key']); if (!array_key_exists('inner', $arr['key'])) { assertType('*NEVER*', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); assertType('*NEVER*', $arr['key']); assertNativeType("mixed~hasOffset('inner')", $arr['key']); throw new \Exception('need key.inner'); } assertType('array{key: array{inner: mixed}}', $arr); - assertNativeType('array&hasOffset(\'key\')', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4117.php b/tests/PHPStan/Analyser/nsrt/bug-4117.php index 510df695f1..14732b77b6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4117.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4117.php @@ -34,7 +34,7 @@ public function broken(int $key) if ($item) { assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item); } else { - assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(array{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); + assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item); } assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4398.php b/tests/PHPStan/Analyser/nsrt/bug-4398.php index 297484c2cf..6fd566cd62 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4398.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4398.php @@ -14,5 +14,5 @@ function (array $meters): void { assertType('array', array_reverse()); assertType('non-empty-array', array_reverse($meters)); assertType('non-empty-list<(int|string)>', array_keys($meters)); - assertType('non-empty-list', array_values($meters)); + assertType('non-empty-list', array_values($meters)); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4565.php b/tests/PHPStan/Analyser/nsrt/bug-4565.php index af941f8098..55a1c372a5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4565.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4565.php @@ -10,9 +10,9 @@ function test(array $variables) { if (!empty($variables['button'])) { assertType('non-empty-array', $attributes); $attributes['type'] = 'button'; - assertType("hasOffsetValue('type', 'button')&non-empty-array", $attributes); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); unset($attributes['href']); - assertType("array&hasOffsetValue('type', 'button')", $attributes); + assertType("non-empty-array&hasOffsetValue('type', 'button')", $attributes); } assertType('array', $attributes); return $attributes; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4708.php b/tests/PHPStan/Analyser/nsrt/bug-4708.php index d6bae28afc..b6f2302722 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4708.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4708.php @@ -57,7 +57,7 @@ function GetASCConfig() } else { - assertType("array&hasOffsetValue('bsw', string)", $result); + assertType("non-empty-array&hasOffsetValue('bsw', string)", $result); $result['bsw'] = (int) $result['bsw']; assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6859.php b/tests/PHPStan/Analyser/nsrt/bug-6859.php index 0f6d43963e..56cd257c5e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6859.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6859.php @@ -30,7 +30,7 @@ public function keys($body) public function values($body) { if (array_key_exists("someParam", $body)) { - assertType('non-empty-list', array_values($body)); + assertType('non-empty-list', array_values($body)); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7805.php b/tests/PHPStan/Analyser/nsrt/bug-7805.php index 859b504e50..ec9464ebd3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7805.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7805.php @@ -14,7 +14,7 @@ function foo(array $params) assertNativeType('array', $params); if (array_key_exists('help', $params)) { assertType('array{help: null}', $params); - assertNativeType("array&hasOffset('help')", $params); + assertNativeType("non-empty-array&hasOffset('help')", $params); unset($params['help']); assertType('array{}', $params); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9734.php b/tests/PHPStan/Analyser/nsrt/bug-9734.php index 353b1d91f9..1628ad6859 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9734.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9734.php @@ -15,7 +15,7 @@ class Foo public function doFoo(array $a): void { if (array_is_list($a)) { - assertType('list', $a); + assertType('list', $a); } else { assertType('array', $a); // could be non-empty-array } @@ -38,7 +38,7 @@ public function doFoo2(): void public function doFoo3(array $a): void { if (array_is_list($a)) { - assertType('non-empty-list', $a); + assertType('non-empty-list', $a); } else { assertType('non-empty-array', $a); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-9985.php b/tests/PHPStan/Analyser/nsrt/bug-9985.php index edbfebffc5..09a7ad92ea 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9985.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9985.php @@ -20,6 +20,6 @@ function (): void { assertType('array{}|array{a?: true, b: true}|array{a?: true, c?: true}', $warnings); if (!empty($warnings)) { - assertType('array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', $warnings); + assertType('array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', $warnings); } }; diff --git a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php index fbbcff38a8..354577f098 100644 --- a/tests/PHPStan/Analyser/nsrt/composer-array-bug.php +++ b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php @@ -44,14 +44,14 @@ public function doFoo(): void } } - assertType("array&hasOffsetValue('authors', mixed)", $this->config); + assertType("non-empty-array&hasOffsetValue('authors', mixed)", $this->config); assertType("mixed", $this->config['authors']); if (empty($this->config['authors'])) { unset($this->config['authors']); assertType("array", $this->config); } else { - assertType("array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); + assertType("non-empty-array&hasOffsetValue('authors', mixed~(0|0.0|''|'0'|array{}|false|null))", $this->config); } assertType('array', $this->config); diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php index 6d86c88014..e76b112f4d 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-vars.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php @@ -12,7 +12,7 @@ public function conditionalVarInTernary(array $innerHits): void if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { assertType('array', $innerHits); $x = array_key_exists('nearest_premise', $innerHits) - ? assertType("array&hasOffset('nearest_premise')", $innerHits) + ? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits) : assertType('array', $innerHits); assertType('array', $innerHits); @@ -25,7 +25,7 @@ public function conditionalVarInIf(array $innerHits): void if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { assertType('array', $innerHits); if (array_key_exists('nearest_premise', $innerHits)) { - assertType("array&hasOffset('nearest_premise')", $innerHits); + assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits); } else { assertType('array', $innerHits); } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php b/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php index 69fc99f613..172fa40b4f 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php @@ -46,9 +46,9 @@ public function test() } assertType('array{default?: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); } - assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); if ($exactType !== null) { - assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + assertType('non-empty-array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); unset($otherTypes['default']); assertType('array{range?: PHPStan\Type\Type}', $otherTypes); } diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php index d1e8fd92a0..eacfb06af6 100644 --- a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -65,7 +65,7 @@ public function testIsset($range): void { assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); if (isset($range['min']) || isset($range['max'])) { - assertType("array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}&non-empty-array", $range); + assertType("non-empty-array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); } else { assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); } @@ -144,7 +144,7 @@ public function doBar(array $a, int $i) public function doFoo2(array $a) { if (is_int($a['a'])) { - assertType("array&hasOffsetValue('a', *NEVER*)", $a); + assertType("non-empty-array&hasOffsetValue('a', *NEVER*)", $a); } } diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index caf0a17c87..c51ea31efc 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -351,25 +351,25 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t if (count($row) >= $twoOrThree) { assertType('array{0: int, 1: string|null, 2?: int|null}', $row); } else { - assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } if (count($row) >= $tenOrEleven) { assertType('*NEVER*', $row); } else { - assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } if (count($row) >= $twoOrMore) { - assertType('array{0: int, 1: string|null, 2?: int|null, 3?: float|null}&list', $row); + assertType('list{0: int, 1: string|null, 2?: int|null, 3?: float|null}', $row); } else { - assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } if (count($row) >= $maxThree) { - assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } else { - assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list', $row); + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } } diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index a80e8b066d..647d44f718 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -9,19 +9,19 @@ class Foo /** @param list $list */ public function directAssertion($list): void { - assertType('list', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionParamHint(array $list): void { - assertType('list', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionNullableParamHint(array $list = null): void { - assertType('list|null', $list); + assertType('list|null', $list); } /** @param list<\DateTime> $list */ @@ -37,7 +37,7 @@ public function withoutGenerics(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); } @@ -83,17 +83,17 @@ public function withFullListFunctionality(): void // These won't output errors for now but should when list type will be fully implemented /** @var list $list */ $list = []; - assertType('list', $list); + assertType('list', $list); $list[] = '1'; - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); $list[] = '2'; - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); unset($list[0]);//break list behaviour assertType('array, mixed>', $list); /** @var list $list2 */ $list2 = []; - assertType('list', $list2); + assertType('list', $list2); $list2[2] = '1';//Most likely to create a gap in indexes assertType('non-empty-array, mixed>&hasOffsetValue(2, \'1\')', $list2); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php index a7cdc6540a..af34c0da25 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array.php @@ -26,7 +26,7 @@ public function doFoo( ): void { assertType('non-empty-array', $array); - assertType('non-empty-list', $list); + assertType('non-empty-list', $list); assertType('non-empty-array', $arrayOfStrings); assertType('non-empty-list', $listOfStd); assertType('non-empty-list', $listOfStd2); diff --git a/tests/PHPStan/Analyser/nsrt/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php index c2bf853776..6b699e598a 100644 --- a/tests/PHPStan/Analyser/nsrt/shuffle.php +++ b/tests/PHPStan/Analyser/nsrt/shuffle.php @@ -13,7 +13,7 @@ public function normalArrays1(array $arr): void /** @var mixed[] $arr */ shuffle($arr); assertType('list', $arr); - assertNativeType('list', $arr); + assertNativeType('list', $arr); assertType('list>', array_keys($arr)); assertType('list', array_values($arr)); } @@ -23,7 +23,7 @@ public function normalArrays2(array $arr): void /** @var non-empty-array $arr */ shuffle($arr); assertType('non-empty-list', $arr); - assertNativeType('list', $arr); + assertNativeType('list', $arr); assertType('non-empty-list>', array_keys($arr)); assertType('non-empty-list', array_values($arr)); } @@ -33,10 +33,10 @@ public function normalArrays3(array $arr): void /** @var array $arr */ if (array_key_exists('foo', $arr)) { shuffle($arr); - assertType('non-empty-list', $arr); - assertNativeType('non-empty-list', $arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); assertType('non-empty-list>', array_keys($arr)); - assertType('non-empty-list', array_values($arr)); + assertType('non-empty-list', array_values($arr)); } } @@ -45,10 +45,10 @@ public function normalArrays4(array $arr): void /** @var array $arr */ if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { shuffle($arr); - assertType('non-empty-list', $arr); - assertNativeType('non-empty-list', $arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); assertType('non-empty-list>', array_keys($arr)); - assertType('non-empty-list', array_values($arr)); + assertType('non-empty-list', array_values($arr)); } } @@ -66,8 +66,8 @@ public function constantArrays2(array $arr): void { /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ shuffle($arr); - assertType('array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('list', $arr); + assertType('list<1|2|3>', $arr); + assertNativeType('list', $arr); assertType('list<0|1|2>', array_keys($arr)); assertType('list<1|2|3>', array_values($arr)); } @@ -76,8 +76,8 @@ public function constantArrays3(array $arr): void { $arr = [1, 2, 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); assertType('non-empty-list<0|1|2>', array_keys($arr)); assertType('non-empty-list<1|2|3>', array_values($arr)); } @@ -86,8 +86,8 @@ public function constantArrays4(array $arr): void { $arr = ['a' => 1, 'b' => 2, 'c' => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); assertType('non-empty-list<0|1|2>', array_keys($arr)); assertType('non-empty-list<1|2|3>', array_values($arr)); } @@ -96,8 +96,8 @@ public function constantArrays5(array $arr): void { $arr = [0 => 1, 3 => 2, 42 => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); - assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<1|2|3>', $arr); + assertNativeType('non-empty-list<1|2|3>', $arr); assertType('non-empty-list<0|1|2>', array_keys($arr)); assertType('non-empty-list<1|2|3>', array_values($arr)); } @@ -106,8 +106,8 @@ public function constantArrays6(array $arr): void { /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ shuffle($arr); - assertType('non-empty-array<0|1, 1|2|3|4>&list', $arr); - assertNativeType('list', $arr); + assertType('non-empty-list<1|2|3|4>', $arr); + assertNativeType('list', $arr); assertType('non-empty-list<0|1>', array_keys($arr)); assertType('non-empty-list<1|2|3|4>', array_values($arr)); } @@ -115,10 +115,10 @@ public function constantArrays6(array $arr): void public function mixed($arr): void { shuffle($arr); - assertType('list', $arr); - assertNativeType('list', $arr); + assertType('list', $arr); + assertNativeType('list', $arr); assertType('list>', array_keys($arr)); - assertType('list', array_values($arr)); + assertType('list', array_values($arr)); } public function subtractedArray($arr): void @@ -134,7 +134,7 @@ public function subtractedArray($arr): void assertType('*ERROR*', $arr); assertNativeType('*ERROR*', $arr); assertType('list', array_keys($arr)); - assertType('list', array_values($arr)); + assertType('list', array_values($arr)); } } diff --git a/tests/PHPStan/Analyser/nsrt/sort.php b/tests/PHPStan/Analyser/nsrt/sort.php index e92b006713..93dfe0d147 100644 --- a/tests/PHPStan/Analyser/nsrt/sort.php +++ b/tests/PHPStan/Analyser/nsrt/sort.php @@ -91,17 +91,17 @@ public function normalArray(array $arr): void $arr1 = $arr; sort($arr1); assertType('list', $arr1); - assertNativeType('list', $arr1); + assertNativeType('list', $arr1); $arr2 = $arr; rsort($arr2); assertType('list', $arr2); - assertNativeType('list', $arr2); + assertNativeType('list', $arr2); $arr3 = $arr; usort($arr3, fn(int $a, int $b) => $a <=> $b); assertType('list', $arr3); - assertNativeType('list', $arr3); + assertNativeType('list', $arr3); } public function mixed($arr): void diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4708.php b/tests/PHPStan/Rules/Comparison/data/bug-4708.php index 5c60d3dec1..2afa164340 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-4708.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-4708.php @@ -57,7 +57,7 @@ function GetASCConfig() } else { - assertType('array&hasOffsetValue(\'bsw\', string)', $result); + assertType('non-empty-array&hasOffsetValue(\'bsw\', string)', $result); $result['bsw'] = (int) $result['bsw']; assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-3931.php b/tests/PHPStan/Rules/Functions/data/bug-3931.php index ca84648938..424c7ca236 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-3931.php +++ b/tests/PHPStan/Rules/Functions/data/bug-3931.php @@ -11,7 +11,7 @@ */ function addSomeKey(array $arr, int $value): array { $arr['mykey'] = $value; - assertType("hasOffsetValue('mykey', int)&non-empty-array", $arr); // should preserve T + assertType("non-empty-array&hasOffsetValue('mykey', int)", $arr); // should preserve T return $arr; } diff --git a/tests/PHPStan/Rules/Functions/data/bug-7156.php b/tests/PHPStan/Rules/Functions/data/bug-7156.php index 96dd5ee26c..209a9decf5 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-7156.php +++ b/tests/PHPStan/Rules/Functions/data/bug-7156.php @@ -21,7 +21,7 @@ function foobar(array $data): void throw new \RuntimeException(); } - assertType("array&hasOffsetValue('value', string)", $data); + assertType("non-empty-array&hasOffsetValue('value', string)", $data); foo($data); } @@ -32,7 +32,7 @@ function foobar2(mixed $data): void throw new \RuntimeException(); } - assertType("array&hasOffsetValue('value', string)", $data); + assertType("non-empty-array&hasOffsetValue('value', string)", $data); foo($data); } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 5ebfb238e7..41200b9ad5 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -865,7 +865,7 @@ public function testBug8146bErrors(): void $this->checkBenevolentUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-8146b-errors.php'], [ [ - "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", + "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", 12, "Offset 'constituencies' (non-empty-list) does not accept type array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}.", ], diff --git a/tests/PHPStan/Rules/Variables/data/bug-3391.php b/tests/PHPStan/Rules/Variables/data/bug-3391.php index bfe756a020..2d57843622 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-3391.php +++ b/tests/PHPStan/Rules/Variables/data/bug-3391.php @@ -22,11 +22,11 @@ public function test() $data['foo'] = 'a'; $data['bar'] = 'b'; - assertType("hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')&non-empty-array", $data); + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); unset($data['id']); - assertType("array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); + assertType("non-empty-array&hasOffsetValue('bar', 'b')&hasOffsetValue('foo', 'a')", $data); return $data; } } diff --git a/tests/PHPStan/Rules/Variables/data/bug-7417.php b/tests/PHPStan/Rules/Variables/data/bug-7417.php index 24fb2c4a7f..fcf698a9ec 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-7417.php +++ b/tests/PHPStan/Rules/Variables/data/bug-7417.php @@ -20,9 +20,9 @@ function doFoo() { // in core.extension so this will come before it's base theme. $extensions['theme']['test_subtheme'] = 0; $extensions['theme']['test_subsubtheme'] = 0; - assertType("hasOffsetValue('theme', mixed)&non-empty-array", $extensions); + assertType("non-empty-array&hasOffsetValue('theme', mixed)", $extensions); unset($extensions['theme']['test_basetheme']); unset($extensions['theme']['test_subsubtheme']); unset($extensions['theme']['test_subtheme']); - assertType("hasOffsetValue('theme', mixed)&non-empty-array", $extensions); + assertType("non-empty-array&hasOffsetValue('theme', mixed)", $extensions); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-8113.php b/tests/PHPStan/Rules/Variables/data/bug-8113.php index 97b5841941..27ebe729ae 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-8113.php +++ b/tests/PHPStan/Rules/Variables/data/bug-8113.php @@ -26,23 +26,23 @@ function () { array_key_exists('review', $review['SurveyInvitation']) && $review['SurveyInvitation']['review'] === null ) { - assertType("array>&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + assertType("non-empty-array>&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); $review['Review'] = [ 'id' => null, 'text' => null, 'answer' => null, ]; - assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); unset($review['SurveyInvitation']['review']); assertType("non-empty-array>&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); } assertType('array>', $review); if (array_key_exists('User', $review['Review'])) { - assertType("array>&hasOffsetValue('Review', array&hasOffset('User'))", $review); + assertType("non-empty-array>&hasOffsetValue('Review', non-empty-array&hasOffset('User'))", $review); $review['User'] = $review['Review']['User']; - assertType("hasOffsetValue('Review', array&hasOffset('User'))&hasOffsetValue('User', mixed)&non-empty-array", $review); + assertType("non-empty-array&hasOffsetValue('Review', non-empty-array&hasOffset('User'))&hasOffsetValue('User', mixed)", $review); unset($review['Review']['User']); - assertType("hasOffsetValue('Review', array)&hasOffsetValue('User', array)&non-empty-array", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('User', array)", $review); } - assertType("array&hasOffsetValue('Review', array)", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)", $review); }; diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 7648a80ae9..c5111cbeae 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -436,7 +436,7 @@ public function dataDescribe(): iterable new NonEmptyArrayType(), ]), VerbosityLevel::value(), - 'non-empty-list', + 'non-empty-list', ]; yield [ @@ -491,6 +491,221 @@ public function dataDescribe(): iterable VerbosityLevel::value(), 'non-empty-array', ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new IntegerType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + VerbosityLevel::typeOnly(), + 'array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + new OversizedArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + new OversizedArrayType(), + ]), + VerbosityLevel::precise(), + 'non-empty-array&oversized-array', + ]; + + $constantArrayWithOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::typeOnly(), + 'list', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::precise(), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + VerbosityLevel::value(), + 'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + ]), + VerbosityLevel::value(), + 'non-empty-array{0?: string, 1?: string, 2?: string, 3?: string}', + ]; } /** diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 6084574e73..2694d0f3eb 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -965,7 +965,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffsetValue(\'foo\', mixed)', + 'non-empty-array&hasOffsetValue(\'foo\', mixed)', ], [ [ @@ -2267,7 +2267,7 @@ public function dataUnion(): iterable TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), ], IntersectionType::class, - 'array&hasOffsetValue(\'a\', int)', + 'non-empty-array&hasOffsetValue(\'a\', int)', ]; yield [ @@ -2288,7 +2288,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - "array&hasOffsetValue('a', mixed)", + "non-empty-array&hasOffsetValue('a', mixed)", ]; yield [ @@ -2315,7 +2315,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - "array&hasOffsetValue(0, array&hasOffsetValue('code', mixed))", + "non-empty-array&hasOffsetValue(0, non-empty-array&hasOffsetValue('code', mixed))", ]; yield [ @@ -2513,7 +2513,7 @@ public function dataUnion(): iterable ]), ], UnionType::class, - 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', ]; yield [ @@ -2600,7 +2600,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffsetValue(\'thing\', mixed)', + 'non-empty-array&hasOffsetValue(\'thing\', mixed)', ]; } @@ -3109,7 +3109,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('a')), ], IntersectionType::class, - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', ], [ [ @@ -3118,7 +3118,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('a')), ], IntersectionType::class, - 'array&hasOffset(\'a\')', + 'non-empty-array&hasOffset(\'a\')', ], [ [ @@ -3267,7 +3267,7 @@ public function dataIntersect(): iterable ]), ], IntersectionType::class, - 'array&hasOffset(\'bar\')&hasOffset(\'foo\')', + 'non-empty-array&hasOffset(\'bar\')&hasOffset(\'foo\')', ], [ [ @@ -4047,7 +4047,7 @@ public function dataIntersect(): iterable TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new HasOffsetValueType(new ConstantStringType('a'), new IntegerType())), ], IntersectionType::class, - 'array&hasOffsetValue(\'a\', 1)', + 'non-empty-array&hasOffsetValue(\'a\', 1)', ]; yield [ [ @@ -4207,7 +4207,7 @@ public function dataIntersect(): iterable new NonEmptyArrayType(), ], UnionType::class, - 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + 'array{a?: true, b: true}|non-empty-array{a?: true, c?: true}', ]; yield [ [ @@ -4243,7 +4243,7 @@ public function dataIntersect(): iterable new NonEmptyArrayType(), ], IntersectionType::class, - 'array{a?: true, c?: true}&non-empty-array', + 'non-empty-array{a?: true, c?: true}', ]; yield [ [ diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index eebdb08014..ffb42f1edf 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -4,6 +4,7 @@ use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -265,7 +266,7 @@ public function dataToPhpDocNode(): iterable yield [ new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()]), - 'list', + 'list', ]; yield [ @@ -274,7 +275,7 @@ public function dataToPhpDocNode(): iterable new NonEmptyArrayType(), new AccessoryArrayListType(), ]), - 'non-empty-list', + 'non-empty-list', ]; yield [ @@ -309,6 +310,122 @@ public function dataToPhpDocNode(): iterable ], [2], [1]), "array{0: 'foo', 1?: 'bar'}", ]; + + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new IntegerType()), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType(true)), + new AccessoryArrayListType(), + ]), + 'list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType(true)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ]), + 'non-empty-list', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ]), + 'non-empty-array', + ]; + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType(true)), + new NonEmptyArrayType(), + ]), + 'non-empty-array', + ]; + $constantArrayWithOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0: string, 1: string, 2?: string, 3?: string}', + ]; + + $constantArrayWithAllOptionalKeys = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new StringType(), + new StringType(), + new StringType(), + new StringType(), + ], [3], [0, 1, 2, 3], TrinaryLogic::createMaybe()); + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new AccessoryArrayListType(), + ]), + 'list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + 'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}', + ]; + + yield [ + new IntersectionType([ + $constantArrayWithAllOptionalKeys, + new NonEmptyArrayType(), + ]), + 'non-empty-array{0?: string, 1?: string, 2?: string, 3?: string}', + ]; } /** From 70f319a25cc75cbb22288674108cf3212b6fe735 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:01:59 +0200 Subject: [PATCH 016/220] StubParser --- conf/config.stubValidator.neon | 6 ++++ src/Parser/StubParser.php | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/Parser/StubParser.php diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index 07390c7b8f..74e943d911 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -11,6 +11,12 @@ services: arguments: php8Parser: @php8PhpParser + defaultAnalysisParser: + class: PHPStan\Parser\StubParser + arguments!: + parser: @php8PhpParser + autowired: false + nodeScopeResolverReflector: factory: @stubReflector diff --git a/src/Parser/StubParser.php b/src/Parser/StubParser.php new file mode 100644 index 0000000000..d98a2cc721 --- /dev/null +++ b/src/Parser/StubParser.php @@ -0,0 +1,56 @@ +parseString(FileReader::read($file)); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); + } + } + + /** + * @return Node\Stmt[] + */ + public function parseString(string $sourceCode): array + { + $errorHandler = new Collecting(); + $nodes = $this->parser->parse($sourceCode, $errorHandler); + if ($errorHandler->hasErrors()) { + throw new ParserErrorsException($errorHandler->getErrors(), null); + } + if ($nodes === null) { + throw new ShouldNotHappenException(); + } + + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor($this->nameResolver); + + /** @var array */ + return $nodeTraverser->traverse($nodes); + } + +} From b651a22caedaf13cfa29fe62ad38d7cc41ba0d88 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:07:10 +0200 Subject: [PATCH 017/220] Fix tests --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 2 +- tests/PHPStan/Analyser/nsrt/array-column.php | 8 ++++---- tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array-flip-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array-search-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array_keys-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array_values-php7.php | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index e97a5f4a06..8338e6f13c 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -4264,7 +4264,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - PHP_VERSION_ID < 80000 ? 'list' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$arr', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php index 2455d6ace9..017490d534 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -189,7 +189,7 @@ public function testImprecise5(array $array): void assertType('list', array_column($array, 'nodeName')); assertType('array', array_column($array, 'nodeName', 'tagName')); assertType('array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('array', array_column($array, 'nodeName', 'foo')); assertType('array', array_column($array, null, 'foo')); @@ -201,7 +201,7 @@ public function testObjects1(array $array): void assertType('non-empty-list', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); @@ -213,7 +213,7 @@ public function testObjects2(array $array): void assertType('array{string}', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('list', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); @@ -227,7 +227,7 @@ final class Foo /** @param array $a */ public function doFoo(array $a): void { - assertType('list', array_column($a, 'nodeName')); + assertType('list', array_column($a, 'nodeName')); assertType('array', array_column($a, 'nodeName', 'tagName')); } diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php index 7bbc485b5a..c0a4e8dd7a 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed): void { if (is_array($mixed)) { - assertType("array<'b'>", array_fill_keys($mixed, 'b')); + assertType("array", array_fill_keys($mixed, 'b')); } else { assertType("null", array_fill_keys($mixed, 'b')); } diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php index 0b7058de01..f62e1a6628 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php @@ -9,7 +9,7 @@ function mixedAndSubtractedArray($mixed) if (is_array($mixed)) { assertType('array', array_flip($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('null', array_flip($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php index 79b7af7b18..14dd7b5d0d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, array $otherArrs): void /** @var array $otherArrs */ assertType('array', array_intersect_key($mixed, $otherArrs)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); /** @var array $otherArrs */ assertType('null', array_intersect_key($mixed, $otherArrs)); /** @var array $otherArrs */ diff --git a/tests/PHPStan/Analyser/nsrt/array-search-php7.php b/tests/PHPStan/Analyser/nsrt/array-search-php7.php index 2cd24b7c9d..5816daf659 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-search-php7.php @@ -16,7 +16,7 @@ public function mixedAndSubtractedArray($mixed, string $string): void assertType('int|string|false', array_search('foo', $mixed)); assertType('int|string|false', array_search($string, $mixed, true)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('null', array_search('foo', $mixed, true)); assertType('null', array_search('foo', $mixed)); assertType('null', array_search($string, $mixed, true)); diff --git a/tests/PHPStan/Analyser/nsrt/array_keys-php7.php b/tests/PHPStan/Analyser/nsrt/array_keys-php7.php index 103cb0a003..9f5ca43682 100644 --- a/tests/PHPStan/Analyser/nsrt/array_keys-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array_keys-php7.php @@ -12,7 +12,7 @@ public function sayHello($mixed): void if (is_array($mixed)) { assertType('list<(int|string)>', array_keys($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('null', array_keys($mixed)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array_values-php7.php b/tests/PHPStan/Analyser/nsrt/array_values-php7.php index 10de823980..db378c95aa 100644 --- a/tests/PHPStan/Analyser/nsrt/array_values-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array_values-php7.php @@ -12,7 +12,7 @@ public function foo1($mixed): void if (is_array($mixed)) { assertType('list', array_values($mixed)); } else { - assertType('mixed~array', $mixed); + assertType('mixed~array', $mixed); assertType('null', array_values($mixed)); } } From 7ed2d67fa4db81323e4957894416be84a1022115 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:14:22 +0200 Subject: [PATCH 018/220] Fix tests --- tests/PHPStan/Levels/data/acceptTypes-5.json | 4 ++-- tests/PHPStan/Levels/data/arrayDestructuring-8.json | 4 ++-- tests/PHPStan/Levels/data/arrayDimFetches-8.json | 4 ++-- tests/PHPStan/Levels/data/casts-7.json | 6 +++--- tests/PHPStan/Levels/data/echo_-2.json | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 88d4749a45..76aac5c080 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -180,7 +180,7 @@ "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, @@ -189,4 +189,4 @@ "line": 763, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json index 7842bce806..3b033abd6d 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-8.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -1,7 +1,7 @@ [ { - "message": "Cannot use array destructuring on array|null.", + "message": "Cannot use array destructuring on array|null.", "line": 15, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-8.json b/tests/PHPStan/Levels/data/arrayDimFetches-8.json index 07568e3768..e7e6efcd13 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-8.json @@ -1,6 +1,6 @@ [ { - "message": "Offset 0 might not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 15, "ignorable": true }, @@ -9,4 +9,4 @@ "line": 50, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index d9b7a85b78..e2810b0e42 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,12 +1,12 @@ [ { - "message": "Cannot cast array|(callable(): mixed) to int.", + "message": "Cannot cast array|(callable(): mixed) to int.", "line": 20, "ignorable": true }, { - "message": "Cannot cast array|float|int to string.", + "message": "Cannot cast array|float|int to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/echo_-2.json b/tests/PHPStan/Levels/data/echo_-2.json index 1c35f8ef7c..1e30f3b562 100644 --- a/tests/PHPStan/Levels/data/echo_-2.json +++ b/tests/PHPStan/Levels/data/echo_-2.json @@ -1,6 +1,6 @@ [ { - "message": "Parameter #1 (array) of echo cannot be converted to string.", + "message": "Parameter #1 (array) of echo cannot be converted to string.", "line": 21, "ignorable": true }, @@ -9,4 +9,4 @@ "line": 21, "ignorable": true } -] \ No newline at end of file +] From 0ce1ce53ff55d9ab0a0bae7f28326167bcc5aebb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:22:20 +0200 Subject: [PATCH 019/220] Use StubParser more --- conf/config.neon | 8 +++++++- conf/config.stubValidator.neon | 7 ++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index d71fbfbdfd..4823b1c90e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1944,7 +1944,7 @@ services: stubPhpDocProvider: class: PHPStan\PhpDoc\StubPhpDocProvider arguments: - parser: @defaultAnalysisParser + parser: @stubParser # Reflection providers @@ -2045,6 +2045,12 @@ services: php8Parser: @php8Parser autowired: false + stubParser: + class: PHPStan\Parser\StubParser + arguments: + parser: @php8PhpParser + autowired: false + phpstanDiagnoseExtension: class: PHPStan\Diagnose\PHPStanDiagnoseExtension arguments: diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index 74e943d911..52eac72312 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -11,11 +11,8 @@ services: arguments: php8Parser: @php8PhpParser - defaultAnalysisParser: - class: PHPStan\Parser\StubParser - arguments!: - parser: @php8PhpParser - autowired: false + defaultAnalysisParser!: + factory: @stubParser nodeScopeResolverReflector: factory: @stubReflector From 8285a25d90f242bde0aaed94f215713446c4a664 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:24:36 +0200 Subject: [PATCH 020/220] Fix tests --- tests/PHPStan/Levels/data/acceptTypes-7.json | 8 ++++---- tests/PHPStan/Levels/data/echo_-2.json | 2 +- tests/PHPStan/Levels/data/iterable-7.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 9e0afc2f64..92912a0f9e 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -85,7 +85,7 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 531, "ignorable": true }, @@ -95,7 +95,7 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 542, "ignorable": true }, @@ -160,7 +160,7 @@ "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", "line": 735, "ignorable": true }, @@ -169,4 +169,4 @@ "line": 756, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/echo_-2.json b/tests/PHPStan/Levels/data/echo_-2.json index 1e30f3b562..330e326e8e 100644 --- a/tests/PHPStan/Levels/data/echo_-2.json +++ b/tests/PHPStan/Levels/data/echo_-2.json @@ -5,7 +5,7 @@ "ignorable": true }, { - "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", + "message": "Parameter #2 (array|(callable(): mixed)) of echo cannot be converted to string.", "line": 21, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/iterable-7.json b/tests/PHPStan/Levels/data/iterable-7.json index 74440d64d8..5c3c24924d 100644 --- a/tests/PHPStan/Levels/data/iterable-7.json +++ b/tests/PHPStan/Levels/data/iterable-7.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|false supplied for foreach, only iterables are supported.", "line": 35, "ignorable": true } -] \ No newline at end of file +] From 3128f266461cdcb559672e8519db7643666c669a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 13 Oct 2024 17:25:36 +0200 Subject: [PATCH 021/220] Improve lowercase string verbosity level --- src/Type/VerbosityLevel.php | 8 +-- tests/PHPStan/Type/VerbosityLevelTest.php | 61 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Type/VerbosityLevelTest.php diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 52b66c154f..47b0d2477c 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -131,11 +131,11 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc } if ($moreVerbose) { - return self::value(); + $verbosity = self::value(); } if ($acceptedType === null) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } $containsInvariantTemplateType = false; @@ -163,7 +163,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc }); if (!$containsInvariantTemplateType) { - return self::typeOnly(); + return $verbosity ?? self::typeOnly(); } /** @var bool $moreVerbose */ @@ -176,7 +176,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc return self::precise(); } - return $moreVerbose ? self::value() : self::typeOnly(); + return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly(); } /** diff --git a/tests/PHPStan/Type/VerbosityLevelTest.php b/tests/PHPStan/Type/VerbosityLevelTest.php new file mode 100644 index 0000000000..0ff71d5025 --- /dev/null +++ b/tests/PHPStan/Type/VerbosityLevelTest.php @@ -0,0 +1,61 @@ +assertSame($expected->getLevelValue(), $level->getLevelValue()); + } + +} From 1aec4300794594d8f87a6e018070ccdf955fd0ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 17:29:24 +0200 Subject: [PATCH 022/220] Fixed types --- tests/PHPStan/Levels/data/acceptTypes-7.json | 6 +++--- tests/PHPStan/Levels/data/echo_-7.json | 6 +++--- tests/PHPStan/Levels/data/iterable-8.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 92912a0f9e..ba67e6a9d6 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -95,12 +95,12 @@ "ignorable": true }, { - "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", "line": 542, "ignorable": true }, { - "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", + "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", "line": 543, "ignorable": true }, @@ -160,7 +160,7 @@ "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", "line": 735, "ignorable": true }, diff --git a/tests/PHPStan/Levels/data/echo_-7.json b/tests/PHPStan/Levels/data/echo_-7.json index 3f32efd774..275583f027 100644 --- a/tests/PHPStan/Levels/data/echo_-7.json +++ b/tests/PHPStan/Levels/data/echo_-7.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", + "message": "Parameter #3 (array|float|int) of echo cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter #4 (array|string) of echo cannot be converted to string.", + "message": "Parameter #4 (array|string) of echo cannot be converted to string.", "line": 21, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/iterable-8.json b/tests/PHPStan/Levels/data/iterable-8.json index 17e368b276..0a46f4b75e 100644 --- a/tests/PHPStan/Levels/data/iterable-8.json +++ b/tests/PHPStan/Levels/data/iterable-8.json @@ -1,7 +1,7 @@ [ { - "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", + "message": "Argument of an invalid type array|null supplied for foreach, only iterables are supported.", "line": 26, "ignorable": true } -] \ No newline at end of file +] From 428ef84a20ba1a2a2f7bfd12dc9aacb0e5c62e7d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 04:54:38 +0200 Subject: [PATCH 023/220] Separate FileTypeMapper for StubPhpDocProvider --- conf/config.neon | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/conf/config.neon b/conf/config.neon index 4823b1c90e..1e64b54187 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1032,6 +1032,12 @@ services: arguments: phpParser: @defaultAnalysisParser + stubFileTypeMapper: + class: PHPStan\Type\FileTypeMapper + arguments: + phpParser: @stubParser + autowired: false + - class: PHPStan\Type\TypeAliasResolver factory: PHPStan\Type\UsefulTypeAliasResolver @@ -1945,6 +1951,7 @@ services: class: PHPStan\PhpDoc\StubPhpDocProvider arguments: parser: @stubParser + fileTypeMapper: @stubFileTypeMapper # Reflection providers @@ -2045,12 +2052,19 @@ services: php8Parser: @php8Parser autowired: false - stubParser: + freshStubParser: class: PHPStan\Parser\StubParser arguments: parser: @php8PhpParser autowired: false + stubParser: + class: PHPStan\Parser\CachedParser + arguments: + originalParser: @freshStubParser + cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + autowired: false + phpstanDiagnoseExtension: class: PHPStan\Diagnose\PHPStanDiagnoseExtension arguments: From aa75de05bdb6fcccded87b6e825a32a0e19f6914 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 04:55:48 +0200 Subject: [PATCH 024/220] Fixed tests --- tests/PHPStan/Levels/data/print_-2.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Levels/data/print_-2.json b/tests/PHPStan/Levels/data/print_-2.json index d0a53d8362..5978bf90e0 100644 --- a/tests/PHPStan/Levels/data/print_-2.json +++ b/tests/PHPStan/Levels/data/print_-2.json @@ -1,12 +1,12 @@ [ { - "message": "Parameter array of print cannot be converted to string.", + "message": "Parameter array of print cannot be converted to string.", "line": 21, "ignorable": true }, { - "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", + "message": "Parameter array|(callable(): mixed) of print cannot be converted to string.", "line": 22, "ignorable": true } -] \ No newline at end of file +] From fba5607a664eddb0ac9c52b7f8defd665b9c5ef3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 05:08:56 +0200 Subject: [PATCH 025/220] Fixed tests --- tests/PHPStan/Levels/data/encapsedString-2.json | 4 ++-- tests/PHPStan/Levels/data/encapsedString-7.json | 4 ++-- tests/PHPStan/Levels/data/print_-7.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Levels/data/encapsedString-2.json b/tests/PHPStan/Levels/data/encapsedString-2.json index 01ea01b977..8035809eb6 100644 --- a/tests/PHPStan/Levels/data/encapsedString-2.json +++ b/tests/PHPStan/Levels/data/encapsedString-2.json @@ -1,11 +1,11 @@ [ { - "message": "Part $array (array) of encapsed string cannot be cast to string.", + "message": "Part $array (array) of encapsed string cannot be cast to string.", "line": 21, "ignorable": true }, { - "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrCallable (array|(callable(): mixed)) of encapsed string cannot be cast to string.", "line": 22, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/encapsedString-7.json b/tests/PHPStan/Levels/data/encapsedString-7.json index 577b87c127..c468ffbc0a 100644 --- a/tests/PHPStan/Levels/data/encapsedString-7.json +++ b/tests/PHPStan/Levels/data/encapsedString-7.json @@ -1,11 +1,11 @@ [ { - "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrFloatOrInt (array|float|int) of encapsed string cannot be cast to string.", "line": 23, "ignorable": true }, { - "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", + "message": "Part $arrayOrString (array|string) of encapsed string cannot be cast to string.", "line": 24, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/print_-7.json b/tests/PHPStan/Levels/data/print_-7.json index 84c94b44c4..532f660fad 100644 --- a/tests/PHPStan/Levels/data/print_-7.json +++ b/tests/PHPStan/Levels/data/print_-7.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter array|float|int of print cannot be converted to string.", + "message": "Parameter array|float|int of print cannot be converted to string.", "line": 23, "ignorable": true }, { - "message": "Parameter array|string of print cannot be converted to string.", + "message": "Parameter array|string of print cannot be converted to string.", "line": 24, "ignorable": true } From cf39148cd0342a76e5ebef1f7ebbdeedb831c60c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 08:46:52 +0200 Subject: [PATCH 026/220] Do not run integration tests on PHAR for now --- .github/workflows/phar.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 04cda78057..28d8d8b36b 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -103,14 +103,14 @@ jobs: - name: "Delete checksum PHAR" run: "rm tmp/phpstan.phar" - - integration-tests: - if: github.event_name == 'pull_request' - needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.0.x - with: - ref: 2.0.x - phar-checksum: ${{needs.compiler-tests.outputs.checksum}} +# +# integration-tests: +# if: github.event_name == 'pull_request' +# needs: compiler-tests +# uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.0.x +# with: +# ref: 2.0.x +# phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' From bbf44d93d852ea1b60486b9b5541851467cdbacb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 08:46:52 +0200 Subject: [PATCH 027/220] Do not run integration tests on PHAR for now --- .github/workflows/phar.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index c6c4934b44..f94cc44244 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -104,13 +104,14 @@ jobs: - name: "Delete checksum PHAR" run: "rm tmp/phpstan.phar" - integration-tests: - if: github.event_name == 'pull_request' - needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.12.x - with: - ref: 1.12.x - phar-checksum: ${{needs.compiler-tests.outputs.checksum}} +# +# integration-tests: +# if: github.event_name == 'pull_request' +# needs: compiler-tests +# uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.12.x +# with: +# ref: 1.12.x +# phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' From 40e461d2e8dff171cdf588e380eb20e782e94eb1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 10:42:49 +0200 Subject: [PATCH 028/220] react/http PHP 8.4 patch --- composer.json | 3 +++ composer.lock | 2 +- patches/Sender.patch | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 patches/Sender.patch diff --git a/composer.json b/composer.json index 5b816f8746..2614dcf584 100644 --- a/composer.json +++ b/composer.json @@ -123,6 +123,9 @@ "nette/neon": [ "patches/NetteNeonStringNode.patch", "patches/NeonParser.patch" + ], + "react/http": [ + "patches/Sender.patch" ] } }, diff --git a/composer.lock b/composer.lock index 91ca3e1931..c0f104e2b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "07635a5e2bdedfa64a641735fba09971", + "content-hash": "674f9ec5e66603e465b9ef2aaa90189a", "packages": [ { "name": "clue/ndjson-react", diff --git a/patches/Sender.patch b/patches/Sender.patch new file mode 100644 index 0000000000..e01d1ff81c --- /dev/null +++ b/patches/Sender.patch @@ -0,0 +1,11 @@ +--- src/Io/Sender.php 2024-03-27 18:20:46 ++++ src/Io/Sender.php 2024-10-14 10:19:28 +@@ -48,7 +48,7 @@ + * @param ConnectorInterface|null $connector + * @return self + */ +- public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) ++ public static function createFromLoop(LoopInterface $loop, ?ConnectorInterface $connector = null) + { + if ($connector === null) { + $connector = new Connector(array(), $loop); From c90d966cfaff4818a65c5b1d2a2e4bb6e2dda0e0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 11:02:49 +0200 Subject: [PATCH 029/220] Upgrading notes about 1st party extensions --- UPGRADING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index 5aae2eeb29..bc65ba774e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -101,6 +101,17 @@ parameters: Appending `(?)` in `ignoreErrors` is not supported. +### Changes in 1st party PHPStan extensions + +* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) + * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) + * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) +* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) + * Removed legacy options with `_` in the name + * `container_xml_path` -> use `containerXmlPath` + * `constant_hassers` -> use `constantHassers` + * `console_application_loader` -> use `consoleApplicationLoader` + ### Docker images no longer tagged without a PHP version Tags without a PHP version are no longer published - `nightly`, `2`, `latest` are no longer updated. Instead, use `nightly-php8.3`, `2-php8.3`, `latest-php8.3`. You can replace `8.3` with PHP versions `8.0`-`8.3`. From 6ac62d3b00f64e72c02b7aa477e425e90f46fa65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:40:45 +0000 Subject: [PATCH 030/220] chore(deps): update crate-ci/typos action to v1.26.0 --- .github/workflows/spelling.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 4fdafdcbd9..41282ea069 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -18,6 +18,6 @@ jobs: uses: actions/checkout@v4 - name: "Check for typos" - uses: "crate-ci/typos@v1.25.0" + uses: "crate-ci/typos@v1.26.0" with: files: "README.md src/" From 87b0dcc44c032a74c271b62dc0940f3ab241f743 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:03:46 +0000 Subject: [PATCH 031/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 80c2c2cd1c..aac23594f0 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#a45eab9318f66864e9840379d3a1976ffe9b8d63", + "jetbrains/phpstorm-stubs": "dev-master#345cde8b2bdc981a07030c18fe7236153c824a05", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index bef8714c74..c3ff064825 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bcacbb78aee0b072323001df71bce3ba", + "content-hash": "82fbdb85736d80091ff20ba37d78546d", "packages": [ { "name": "clue/ndjson-react", @@ -1442,17 +1442,17 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "a45eab9318f66864e9840379d3a1976ffe9b8d63" + "reference": "345cde8b2bdc981a07030c18fe7236153c824a05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/a45eab9318f66864e9840379d3a1976ffe9b8d63", - "reference": "a45eab9318f66864e9840379d3a1976ffe9b8d63", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/345cde8b2bdc981a07030c18fe7236153c824a05", + "reference": "345cde8b2bdc981a07030c18fe7236153c824a05", "shasum": "" }, "require-dev": { "friendsofphp/php-cs-fixer": "v3.61.1", - "nikic/php-parser": "v5.1.0", + "nikic/php-parser": "v5.3.1", "phpdocumentor/reflection-docblock": "5.4.1", "phpunit/phpunit": "11.3.0" }, @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-10-05T19:50:06+00:00" + "time": "2024-10-14T12:35:31+00:00" }, { "name": "nette/bootstrap", From 2f0fd8578fda941dabb90b820e2169b9bdfcfb2e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 16 Oct 2024 10:33:00 +0200 Subject: [PATCH 032/220] Remove dead code --- bin/phpstan | 9 --------- 1 file changed, 9 deletions(-) diff --git a/bin/phpstan b/bin/phpstan index 978d755ac1..ba27551db6 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -13,15 +13,6 @@ use Symfony\Component\Console\Helper\ProgressBar; (function () { error_reporting(E_ALL); ini_set('display_errors', 'stderr'); - if (version_compare(PHP_VERSION, '7.4.0', '<')) { - // PHP earlier than 7.4.x with OpCache triggers a bug when we intercept - // custom autoloaders' reads to discover file paths. See PHPStan #4881. - ini_set('opcache.enable', 'Off'); - } - - if (PHP_VERSION_ID < 70300) { - gc_disable(); // performance boost - } define('__PHPSTAN_RUNNING__', true); From 79319b3af98491bbbfe76851753de59ad63f75c4 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:19:43 +0000 Subject: [PATCH 033/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index aac23594f0..b57ec8dd52 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.42.0.11", - "phpstan/php-8-stubs": "0.4.2", + "phpstan/php-8-stubs": "0.4.3", "phpstan/phpdoc-parser": "^2.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index c3ff064825..271b9c0192 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "82fbdb85736d80091ff20ba37d78546d", + "content-hash": "66d7fc0ee35d6ebdf822a59598a3c9f1", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.2", + "version": "0.4.3", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "64fbb357f86728a3d0a06d57178bf968bcf82206" + "reference": "b30c6975205b4b51e7d8c635f57d29b869220a9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/64fbb357f86728a3d0a06d57178bf968bcf82206", - "reference": "64fbb357f86728a3d0a06d57178bf968bcf82206", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/b30c6975205b4b51e7d8c635f57d29b869220a9e", + "reference": "b30c6975205b4b51e7d8c635f57d29b869220a9e", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.2" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.3" }, - "time": "2024-10-09T07:25:55+00:00" + "time": "2024-10-18T00:19:10+00:00" }, { "name": "phpstan/phpdoc-parser", From a815d575dc10616e510713e01627369b10980d31 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 17 Oct 2024 10:36:01 +0200 Subject: [PATCH 034/220] Introduce TypeResult --- src/Analyser/MutatingScope.php | 4 +- .../InitializerExprTypeResolver.php | 55 +++++++++++-------- src/Type/TypeResult.php | 22 ++++++++ 3 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/Type/TypeResult.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5d6636683f..401f011dc5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -799,7 +799,7 @@ private function resolveType(string $exprString, Expr $node): Type $leftType = $this->getType($node->left); $rightType = $this->getType($node->right); - return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType); + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; } if ($node instanceof Expr\BinaryOp\NotEqual) { @@ -959,7 +959,7 @@ private function resolveType(string $exprString, Expr $node): Type return new BooleanType(); } - return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType)->type; } if ($node instanceof Expr\BinaryOp\NotIdentical) { diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 8b015c91ab..178c16b4a8 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -65,6 +65,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; +use PHPStan\Type\TypeResult; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; @@ -298,7 +299,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return $this->resolveIdenticalType( $this->getType($expr->left, $context), $this->getType($expr->right, $context), - ); + )->type; } if ($expr instanceof BinaryOp\NotIdentical) { @@ -309,7 +310,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return $this->resolveEqualType( $this->getType($expr->left, $context), $this->getType($expr->right, $context), - ); + )->type; } if ($expr instanceof BinaryOp\NotEqual) { @@ -1349,34 +1350,42 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType); } - public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanType + /** + * @return TypeResult + */ + public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResult { if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { - return new ConstantBooleanType($leftType->getValue() === $rightType->getValue()); + return new TypeResult(new ConstantBooleanType($leftType->getValue() === $rightType->getValue()), []); } $leftTypeFiniteTypes = $leftType->getFiniteTypes(); $rightTypeFiniteType = $rightType->getFiniteTypes(); if (count($leftTypeFiniteTypes) === 1 && count($rightTypeFiniteType) === 1) { - return new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])); + return new TypeResult(new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])), []); } - if ($leftType->isSuperTypeOf($rightType)->no() && $rightType->isSuperTypeOf($leftType)->no()) { - return new ConstantBooleanType(false); + $leftIsSuperTypeOfRight = $leftType->isSuperTypeOfWithReason($rightType); + $rightIsSuperTypeOfLeft = $rightType->isSuperTypeOfWithReason($leftType); + if ($leftIsSuperTypeOfRight->no() && $rightIsSuperTypeOfLeft->no()) { + return new TypeResult(new ConstantBooleanType(false), array_merge($leftIsSuperTypeOfRight->reasons, $rightIsSuperTypeOfLeft->reasons)); } if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { - return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): BooleanType => $this->resolveIdenticalType($leftValueType, $rightValueType)); + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveIdenticalType($leftValueType, $rightValueType)); } - return new BooleanType(); + return new TypeResult(new BooleanType(), []); } - public function resolveEqualType(Type $leftType, Type $rightType): BooleanType + /** + * @return TypeResult + */ + public function resolveEqualType(Type $leftType, Type $rightType): TypeResult { if ( ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) @@ -1386,16 +1395,17 @@ public function resolveEqualType(Type $leftType, Type $rightType): BooleanType } if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { - return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): BooleanType => $this->resolveEqualType($leftValueType, $rightValueType)); + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType)); } - return $leftType->looseCompare($rightType, $this->phpVersion); + return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []); } /** - * @param callable(Type, Type): BooleanType $valueComparisonCallback + * @param callable(Type, Type): TypeResult $valueComparisonCallback + * @return TypeResult */ - private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): BooleanType + private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): TypeResult { $leftKeyTypes = $leftType->getKeyTypes(); $rightKeyTypes = $rightType->getKeyTypes(); @@ -1412,7 +1422,7 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, if (count($rightKeyTypes) === 0) { if (!$leftOptional) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } continue; } @@ -1425,13 +1435,13 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, $found = true; break; } elseif (!$rightType->isOptionalKey($j)) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } } if (!$found) { if (!$leftOptional) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } continue; } @@ -1448,21 +1458,22 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, } } - $leftIdenticalToRight = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); + $leftIdenticalToRightResult = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); + $leftIdenticalToRight = $leftIdenticalToRightResult->type; if ($leftIdenticalToRight->isFalse()->yes()) { - return new ConstantBooleanType(false); + return $leftIdenticalToRightResult; } $resultType = TypeCombinator::union($resultType, $leftIdenticalToRight); } foreach (array_keys($rightKeyTypes) as $j) { if (!$rightType->isOptionalKey($j)) { - return new ConstantBooleanType(false); + return new TypeResult(new ConstantBooleanType(false), []); } $resultType = new BooleanType(); } - return $resultType->toBoolean(); + return new TypeResult($resultType->toBoolean(), []); } private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type $leftType, Type $rightType): ?Type diff --git a/src/Type/TypeResult.php b/src/Type/TypeResult.php new file mode 100644 index 0000000000..76d79326e8 --- /dev/null +++ b/src/Type/TypeResult.php @@ -0,0 +1,22 @@ + $reasons + */ + public function __construct( + public readonly Type $type, + public readonly array $reasons, + ) + { + } + +} From 34bacd74410573cf79754348231849b474b7312e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 17 Oct 2024 10:36:12 +0200 Subject: [PATCH 035/220] Show TypeResult reasons in StrictComparisonOfDifferentTypesRule --- conf/config.neon | 3 + src/Analyser/DirectInternalScopeFactory.php | 2 + src/Analyser/LazyInternalScopeFactory.php | 1 + src/Analyser/MutatingScope.php | 42 +--------- src/Analyser/RicherScopeGetTypeHelper.php | 78 +++++++++++++++++++ .../StrictComparisonOfDifferentTypesRule.php | 18 ++++- src/Testing/PHPStanTestCase.php | 20 +++-- src/Type/MixedType.php | 13 +++- ...rictComparisonOfDifferentTypesRuleTest.php | 20 +++++ .../Comparison/data/strict-comparison.php | 20 +++++ .../Rules/Methods/CallMethodsRuleTest.php | 6 ++ 11 files changed, 173 insertions(+), 50 deletions(-) create mode 100644 src/Analyser/RicherScopeGetTypeHelper.php diff --git a/conf/config.neon b/conf/config.neon index 3146a8fd3f..5ad99397a9 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -582,6 +582,9 @@ services: arguments: cacheFilePath: %resultCachePath% + - + class: PHPStan\Analyser\RicherScopeGetTypeHelper + - class: PHPStan\Cache\Cache arguments: diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 49a3395d2e..cbec53d0d1 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -34,6 +34,7 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private NodeScopeResolver $nodeScopeResolver, + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, private bool $explicitMixedInUnknownGenericNew, private bool $explicitMixedForGlobalVariables, @@ -85,6 +86,7 @@ public function create( $this->propertyReflectionFinder, $this->parser, $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, $this->constantResolver, $context, $this->phpVersion, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index fec1521768..1e2e938678 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -79,6 +79,7 @@ public function create( $this->container->getByType(PropertyReflectionFinder::class), $this->container->getService('currentPhpVersionSimpleParser'), $this->container->getByType(NodeScopeResolver::class), + $this->container->getByType(RicherScopeGetTypeHelper::class), $this->container->getByType(ConstantResolver::class), $context, $this->container->getByType(PhpVersion::class), diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 401f011dc5..90b3fdd465 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -204,6 +204,7 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private NodeScopeResolver $nodeScopeResolver, + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private ConstantResolver $constantResolver, private ScopeContext $context, private PhpVersion $phpVersion, @@ -924,46 +925,11 @@ private function resolveType(string $exprString, Expr $node): Type } if ($node instanceof Expr\BinaryOp\Identical) { - if ( - $node->left instanceof Variable - && is_string($node->left->name) - && $node->right instanceof Variable - && is_string($node->right->name) - && $node->left->name === $node->right->name - ) { - return new ConstantBooleanType(true); - } - - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - - if ( - ( - $node->left instanceof Node\Expr\PropertyFetch - || $node->left instanceof Node\Expr\StaticPropertyFetch - ) - && $rightType->isNull()->yes() - && !$this->hasPropertyNativeType($node->left) - ) { - return new BooleanType(); - } - - if ( - ( - $node->right instanceof Node\Expr\PropertyFetch - || $node->right instanceof Node\Expr\StaticPropertyFetch - ) - && $leftType->isNull()->yes() - && !$this->hasPropertyNativeType($node->right) - ) { - return new BooleanType(); - } - - return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType)->type; + return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type; } if ($node instanceof Expr\BinaryOp\NotIdentical) { - return $this->getType(new Expr\BooleanNot(new BinaryOp\Identical($node->left, $node->right))); + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type; } if ($node instanceof Expr\Instanceof_) { @@ -2654,7 +2620,7 @@ private function promoteNativeTypes(): self /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - private function hasPropertyNativeType($propertyFetch): bool + public function hasPropertyNativeType($propertyFetch): bool { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $this); if ($propertyReflection === null) { diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php new file mode 100644 index 0000000000..e60612f820 --- /dev/null +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -0,0 +1,78 @@ + + */ + public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + { + if ( + $expr->left instanceof Variable + && is_string($expr->left->name) + && $expr->right instanceof Variable + && is_string($expr->right->name) + && $expr->left->name === $expr->right->name + ) { + return new TypeResult(new ConstantBooleanType(true), []); + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + if ( + ( + $expr->left instanceof Node\Expr\PropertyFetch + || $expr->left instanceof Node\Expr\StaticPropertyFetch + ) + && $rightType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->left) + ) { + return new TypeResult(new BooleanType(), []); + } + + if ( + ( + $expr->right instanceof Node\Expr\PropertyFetch + || $expr->right instanceof Node\Expr\StaticPropertyFetch + ) + && $leftType->isNull()->yes() + && !$scope->hasPropertyNativeType($expr->right) + ) { + return new TypeResult(new BooleanType(), []); + } + + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + + /** + * @return TypeResult + */ + public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult + { + $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + $identicalType = $identicalResult->type; + if ($identicalType instanceof ConstantBooleanType) { + return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); + } + + return new TypeResult(new BooleanType(), []); + } + +} diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 0db119501f..7d9c764132 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; @@ -10,6 +11,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function count; use function sprintf; /** @@ -19,6 +21,7 @@ final class StrictComparisonOfDifferentTypesRule implements Rule { public function __construct( + private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private bool $checkAlwaysTrueStrictComparison, private bool $treatPhpDocTypesAsCertain, private bool $reportAlwaysTrueInLastCondition, @@ -34,11 +37,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node instanceof Node\Expr\BinaryOp\Identical && !$node instanceof Node\Expr\BinaryOp\NotIdentical) { + if ($node instanceof Node\Expr\BinaryOp\Identical) { + $nodeTypeResult = $this->richerScopeGetTypeHelper->getIdenticalResult($this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(), $node); + } elseif ($node instanceof Node\Expr\BinaryOp\NotIdentical) { + $nodeTypeResult = $this->richerScopeGetTypeHelper->getNotIdenticalResult($this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(), $node); + } else { return []; } - $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); + $nodeType = $nodeTypeResult->type; if (!$nodeType instanceof ConstantBooleanType) { return []; } @@ -46,7 +53,12 @@ public function processNode(Node $node, Scope $scope): array $leftType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->left) : $scope->getNativeType($node->left); $rightType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->right) : $scope->getNativeType($node->right); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $nodeTypeResult): RuleErrorBuilder { + $reasons = $nodeTypeResult->reasons; + if (count($reasons) > 0) { + return $ruleErrorBuilder->acceptsReasonsTip($reasons); + } + if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 14efcc7480..8f9fdfd44f 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -7,6 +7,7 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\ScopeFactory; use PHPStan\Analyser\TypeSpecifier; use PHPStan\BetterReflection\Reflector\ClassReflector; @@ -168,18 +169,20 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames); + $initializerExprTypeResolver = new InitializerExprTypeResolver( + $constantResolver, + $reflectionProviderProvider, + $container->getByType(PhpVersion::class), + $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + new OversizedArrayBuilder(), + $container->getParameter('usePathConstantsAsConstantString'), + ); + return new ScopeFactory( new DirectInternalScopeFactory( MutatingScope::class, $reflectionProvider, - new InitializerExprTypeResolver( - $constantResolver, - $reflectionProviderProvider, - $container->getByType(PhpVersion::class), - $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), - new OversizedArrayBuilder(), - $container->getParameter('usePathConstantsAsConstantString'), - ), + $initializerExprTypeResolver, $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class), $container->getByType(ExprPrinter::class), @@ -187,6 +190,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider new PropertyReflectionFinder(), self::getParser(), $container->getByType(NodeScopeResolver::class), + new RicherScopeGetTypeHelper($initializerExprTypeResolver), $container->getByType(PhpVersion::class), $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 20117d68b3..0b9d87394a 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -159,7 +159,18 @@ public function isSuperTypeOfWithReason(Type $type): IsSuperTypeOfResult return IsSuperTypeOfResult::createMaybe(); } - return $this->subtractedType->isSuperTypeOfWithReason($type)->negate(); + $result = $this->subtractedType->isSuperTypeOfWithReason($type)->negate(); + if ($result->no()) { + return IsSuperTypeOfResult::createNo([ + sprintf( + 'Type %s has already been eliminated from %s.', + $this->subtractedType->describe(VerbosityLevel::precise()), + $this->describe(VerbosityLevel::typeOnly()), + ), + ]); + } + + return $result; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 4721b4e78b..96712b66b9 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_INT_SIZE; @@ -22,6 +23,7 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase protected function getRule(): Rule { return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), $this->checkAlwaysTrueStrictComparison, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition, @@ -216,10 +218,12 @@ public function testStrictComparison(): void [ 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', 808, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using !== between mixed and 1 will always evaluate to true.', 812, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using === between \'foo\' and \'foo\' will always evaluate to true.', @@ -275,6 +279,16 @@ public function testStrictComparison(): void 1014, $tipText, ], + [ + 'Strict comparison using === between mixed and null will always evaluate to false.', + 1030, + 'Type null has already been eliminated from mixed.', + ], + [ + 'Strict comparison using !== between mixed and null will always evaluate to true.', + 1034, + 'Type null has already been eliminated from mixed.', + ], ], ); } @@ -419,6 +433,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void [ 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', 808, + 'Type 1|string has already been eliminated from mixed.', ], [ 'Strict comparison using === between NAN and NAN will always evaluate to false.', @@ -433,6 +448,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 1014, $tipText, ], + [ + 'Strict comparison using === between mixed and null will always evaluate to false.', + 1030, + 'Type null has already been eliminated from mixed.', + ], ], ); } diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php index 3e05dd8b83..b6389bd5ee 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -1017,3 +1017,23 @@ public function doFoo($a): void } } + +class SubtractedMixedAgainstNull +{ + + public function doFoo($m): void + { + if ($m === null) { + return; + } + + if ($m === null) { + + } + + if ($m !== null) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 7a46d849d3..b9c196420d 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -457,14 +457,17 @@ public function testCallMethods(): void [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1277, + 'Type int has already been eliminated from mixed.', ], [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1284, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #1 $parameter of method Test\SubtractedMixed::requireIntOrString() expects int|string, mixed given.', 1285, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #2 $b of method Test\ExpectsExceptionGenerics::expectsExceptionUpperBound() expects Exception, Throwable given.', @@ -793,14 +796,17 @@ public function testCallMethodsOnThisOnly(): void [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1277, + 'Type int has already been eliminated from mixed.', ], [ 'Parameter #1 $i of method Test\SubtractedMixed::requireInt() expects int, mixed given.', 1284, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #1 $parameter of method Test\SubtractedMixed::requireIntOrString() expects int|string, mixed given.', 1285, + 'Type int|string has already been eliminated from mixed.', ], [ 'Parameter #2 $b of method Test\ExpectsExceptionGenerics::expectsExceptionUpperBound() expects Exception, Throwable given.', From dc5d8f4d3eef18b1d80b8ffb3a1adfe8de6d7268 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 18 Oct 2024 09:20:02 +0200 Subject: [PATCH 036/220] Decorate reasons when comparing ConstantArrayType --- phpstan-baseline.neon | 13 +++++++++++++ src/Analyser/RicherScopeGetTypeHelper.php | 4 ++++ src/Type/Constant/ConstantArrayType.php | 2 +- .../StrictComparisonOfDifferentTypesRuleTest.php | 5 +++++ .../Rules/Comparison/data/strict-comparison.php | 14 ++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 197663fec6..73144c4ac1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,6 +66,11 @@ parameters: count: 1 path: src/Analyser/NodeScopeResolver.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Analyser/RicherScopeGetTypeHelper.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" count: 2 @@ -487,6 +492,14 @@ parameters: count: 2 path: src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php + - + message: """ + #^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of interface PHPStan\\\\Analyser\\\\Scope\\: + Use getNativeType\\(\\)$# + """ + count: 2 + path: src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" count: 2 diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php index e60612f820..ba7c6c618a 100644 --- a/src/Analyser/RicherScopeGetTypeHelper.php +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -36,6 +36,10 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult $leftType = $scope->getType($expr->left); $rightType = $scope->getType($expr->right); + if (!$scope instanceof MutatingScope) { + return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType); + } + if ( ( $expr->left instanceof Node\Expr\PropertyFetch diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 74786c7e89..1b770d6889 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -390,7 +390,7 @@ public function isSuperTypeOfWithReason(Type $type): IsSuperTypeOfResult $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOfWithReason($type->getOffsetValueType($keyType)); if ($isValueSuperType->no()) { - return $isValueSuperType; + return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason)); } $results[] = $isValueSuperType; } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 96712b66b9..01157d82e1 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -289,6 +289,11 @@ public function testStrictComparison(): void 1034, 'Type null has already been eliminated from mixed.', ], + [ + 'Strict comparison using !== between array{1, mixed, 3} and array{int, null, int} will always evaluate to true.', + 1048, + 'Offset 1: Type null has already been eliminated from mixed.', + ], ], ); } diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php index b6389bd5ee..70882c4ca4 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -1036,4 +1036,18 @@ public function doFoo($m): void } } + public function doBar($m, int $i, int $j): void + { + if ($m === null) { + return; + } + + $a = [1, $m, 3]; + $b = [$i, null, $j]; + + if ($a !== $b) { + + } + } + } From e802b85aa2cecf243410c41464e632ca58eaa658 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 18 Oct 2024 09:39:51 +0200 Subject: [PATCH 037/220] Fix --- src/Type/TypeResult.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Type/TypeResult.php b/src/Type/TypeResult.php index 76d79326e8..18942d7080 100644 --- a/src/Type/TypeResult.php +++ b/src/Type/TypeResult.php @@ -8,15 +8,21 @@ final class TypeResult { + public readonly Type $type; + + public readonly array $reasons; + /** * @param T $type * @param list $reasons */ public function __construct( - public readonly Type $type, - public readonly array $reasons, + Type $type, + array $reasons, ) { + $this->type = $type; + $this->reasons = $reasons; } } From 247ae64eaa4126e6fd9bca563d71ac95080392cf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 18 Oct 2024 09:45:25 +0200 Subject: [PATCH 038/220] Fix --- src/Type/TypeResult.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/TypeResult.php b/src/Type/TypeResult.php index 18942d7080..5d10bced1f 100644 --- a/src/Type/TypeResult.php +++ b/src/Type/TypeResult.php @@ -10,6 +10,7 @@ final class TypeResult public readonly Type $type; + /** @var list */ public readonly array $reasons; /** From c875e8309c08b8ff05043acbcb16344883786f35 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 18 Oct 2024 09:50:36 +0200 Subject: [PATCH 039/220] Work around simple-downgrader bug --- phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml b/phpcs.xml index 8a8a89df38..7ec0a21da2 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -202,6 +202,7 @@ + src/Type/TypeResult.php compiler/tests/*/data/ tests/*/Fixture/ From 4b3406e420e0a3bfc796de0e62226d7cbcbb6785 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:03:32 +0000 Subject: [PATCH 040/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index b57ec8dd52..fd100caeb6 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#345cde8b2bdc981a07030c18fe7236153c824a05", + "jetbrains/phpstorm-stubs": "dev-master#f8625adce08b146bf481a0f1bbee06e82a488059", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 271b9c0192..88147fa23b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "66d7fc0ee35d6ebdf822a59598a3c9f1", + "content-hash": "b46b210764567d0dae89d02afb1c9b92", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "345cde8b2bdc981a07030c18fe7236153c824a05" + "reference": "f8625adce08b146bf481a0f1bbee06e82a488059" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/345cde8b2bdc981a07030c18fe7236153c824a05", - "reference": "345cde8b2bdc981a07030c18fe7236153c824a05", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/f8625adce08b146bf481a0f1bbee06e82a488059", + "reference": "f8625adce08b146bf481a0f1bbee06e82a488059", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-10-14T12:35:31+00:00" + "time": "2024-10-20T18:41:15+00:00" }, { "name": "nette/bootstrap", From d73acef87f9d3ebd04bb6671d6b83755a8e62e8e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 22 Oct 2024 09:51:15 +0200 Subject: [PATCH 041/220] Fix build --- build/deprecated-8.4.neon | 7 +++++++ build/ignore-by-php-version.neon.php | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 build/deprecated-8.4.neon diff --git a/build/deprecated-8.4.neon b/build/deprecated-8.4.neon new file mode 100644 index 0000000000..6b3bd6db5e --- /dev/null +++ b/build/deprecated-8.4.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Use of constant E_STRICT is deprecated\.$#' + identifier: constant.deprecated + count: 1 + path: ../src/Analyser/FileAnalyser.php diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 2c808909bc..c250ea9eec 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -29,6 +29,10 @@ $includes[] = __DIR__ . '/datetime-php-83.neon'; } +if (PHP_VERSION_ID >= 80400) { + $includes[] = __DIR__ . '/deprecated-8.4.neon'; +} + $config = []; $config['includes'] = $includes; From 11e0ab61e187127b82f24a2e88b1e137513e1744 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Fri, 18 Oct 2024 21:37:48 +0200 Subject: [PATCH 042/220] Remove unnecessary `instanceof ConstantBooleanType` in loop analysis --- phpstan-baseline.neon | 2 +- src/Analyser/NodeScopeResolver.php | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 73144c4ac1..050d8a1d2c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -58,7 +58,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" - count: 3 + count: 2 path: src/Analyser/NodeScopeResolver.php - diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 92eae571e5..969d0be6b4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1320,12 +1320,7 @@ private function processStmtNode( $initScope = $condResult->getScope(); $condResultScope = $condResult->getScope(); $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); - if ($condTruthiness instanceof ConstantBooleanType) { - $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue()); - } else { - $condTruthinessTrinary = TrinaryLogic::createMaybe(); - } - $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); From a07996a9cad15c0c6e6e8fd57338236734a5c0dd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 11:37:39 +0200 Subject: [PATCH 043/220] OffsetAccessValueAssignmentRule optimization for huge arrays --- src/Rules/Arrays/OffsetAccessValueAssignmentRule.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 45f7b490b1..9145005d20 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -46,6 +46,10 @@ public function processNode(Node $node, Scope $scope): array } $arrayDimFetch = $node->var; + $varType = $scope->getType($arrayDimFetch->var); + if ($varType->isObject()->no()) { + return []; + } if ($node instanceof Assign || $node instanceof Expr\AssignRef) { $assignedValueType = $scope->getType($node->expr); From 1a0099dc61674ff1eb0ef8d68c90011f2206a64b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 11:47:14 +0200 Subject: [PATCH 044/220] NodeScopeResolver - refactoring before optimization --- src/Analyser/NodeScopeResolver.php | 104 ++++++++++++----------------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 969d0be6b4..afe1373704 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5055,7 +5055,7 @@ private function processAssignVar( $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); $originalValueToWrite = $valueToWrite; - $originalNativeValueToWrite = $valueToWrite; + $originalNativeValueToWrite = $nativeValueToWrite; // 3. eval assigned expr $result = $processExprCallback($scope); @@ -5076,67 +5076,9 @@ private function processAssignVar( } $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - $offsetValueTypeStack = [$offsetValueType]; - $offsetValueNativeTypeStack = [$offsetNativeValueType]; - foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { - if ($offsetType === null) { - $offsetValueType = new ConstantArrayType([], []); - - } else { - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - if ($offsetValueType instanceof ErrorType) { - $offsetValueType = new ConstantArrayType([], []); - } - } - $offsetValueTypeStack[] = $offsetValueType; - } - foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { - if ($offsetNativeType === null) { - $offsetNativeValueType = new ConstantArrayType([], []); - - } else { - $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); - if ($offsetNativeValueType instanceof ErrorType) { - $offsetNativeValueType = new ConstantArrayType([], []); - } - } - - $offsetValueNativeTypeStack[] = $offsetNativeValueType; - } - - foreach (array_reverse($offsetTypes) as $i => $offsetType) { - /** @var Type $offsetValueType */ - $offsetValueType = array_pop($offsetValueTypeStack); - if (!$offsetValueType instanceof MixedType) { - $types = [ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ]; - if ($offsetType !== null && $offsetType->isInteger()->yes()) { - $types[] = new StringType(); - } - $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); - } - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); - } - foreach (array_reverse($offsetNativeTypes) as $i => $offsetNativeType) { - /** @var Type $offsetNativeValueType */ - $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); - if (!$offsetNativeValueType instanceof MixedType) { - $types = [ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(ArrayAccess::class), - new NullType(), - ]; - if ($offsetNativeType !== null && $offsetNativeType->isInteger()->yes()) { - $types[] = new StringType(); - } - $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types)); - } - $nativeValueToWrite = $offsetNativeValueType->setOffsetValueType($offsetNativeType, $nativeValueToWrite, $i === 0); - } + $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetTypes, $offsetValueType, $valueToWrite); + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { @@ -5409,6 +5351,46 @@ static function (): void { return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); } + /** + * @param list $offsetTypes + */ + private function produceArrayDimFetchAssignValueToWrite(array $offsetTypes, Type $offsetValueType, Type $valueToWrite): Type + { + $offsetValueTypeStack = [$offsetValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + if ($offsetType === null) { + $offsetValueType = new ConstantArrayType([], []); + + } else { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + if ($offsetValueType instanceof ErrorType) { + $offsetValueType = new ConstantArrayType([], []); + } + } + + $offsetValueTypeStack[] = $offsetValueType; + } + + foreach (array_reverse($offsetTypes) as $i => $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + if (!$offsetValueType instanceof MixedType) { + $types = [ + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ]; + if ($offsetType !== null && $offsetType->isInteger()->yes()) { + $types[] = new StringType(); + } + $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); + } + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + } + + return $valueToWrite; + } + private function unwrapAssign(Expr $expr): Expr { if ($expr instanceof Assign) { From 53a15542683e5e0fd4e8f32cb9e94290edb7c39c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 11:51:55 +0200 Subject: [PATCH 045/220] processAssignVar optimization for arrays --- src/Analyser/NodeScopeResolver.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index afe1373704..f4f60df2ae 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5078,7 +5078,30 @@ private function processAssignVar( $offsetNativeValueType = $varNativeType; $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetTypes, $offsetValueType, $valueToWrite); - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + + $nativeValueToWrite = $valueToWrite; + if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + } else { + foreach ($offsetTypes as $i => $offsetType) { + $offsetNativeType = $offsetNativeTypes[$i]; + if ($offsetType === null) { + if ($offsetNativeType !== null) { + throw new ShouldNotHappenException(); + } + + continue; + } elseif ($offsetNativeType === null) { + throw new ShouldNotHappenException(); + } + if ($offsetType->equals($offsetNativeType)) { + continue; + } + + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + break; + } + } if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { From 537c12c0c3f14371ceaf59051fc5445339857a97 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 11:15:13 +0200 Subject: [PATCH 046/220] Fix performance issue with big appended arrays --- .../Analyser/AnalyserIntegrationTest.php | 6 + tests/PHPStan/Analyser/data/bug-11913.php | 274 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-11913.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index b237cdd33a..e5a45b8a10 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1448,6 +1448,12 @@ public function testBug11640(): void $this->assertNoErrors($errors); } + public function testBug11913(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11913.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/data/bug-11913.php b/tests/PHPStan/Analyser/data/bug-11913.php new file mode 100644 index 0000000000..865023c725 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11913.php @@ -0,0 +1,274 @@ + "AF", "name" => "Afghanistan", "d_code" => "0093"); + $countries[] = array("code" => "AL", "name" => "Albania", "d_code" => "00355"); + $countries[] = array("code" => "DZ", "name" => "Algeria", "d_code" => "00213"); + $countries[] = array("code" => "AS", "name" => "American Samoa", "d_code" => "001"); + $countries[] = array("code" => "AD", "name" => "Andorra", "d_code" => "00376"); + $countries[] = array("code" => "AO", "name" => "Angola", "d_code" => "00244"); + $countries[] = array("code" => "AI", "name" => "Anguilla", "d_code" => "001"); + $countries[] = array("code" => "AG", "name" => "Antigua", "d_code" => "001"); + $countries[] = array("code" => "AR", "name" => "Argentina", "d_code" => "0054"); + $countries[] = array("code" => "AM", "name" => "Armenia", "d_code" => "00374"); + $countries[] = array("code" => "AW", "name" => "Aruba", "d_code" => "00297"); + $countries[] = array("code" => "AU", "name" => "Australia", "d_code" => "0061"); + $countries[] = array("code" => "AT", "name" => "Austria", "d_code" => "0043"); + $countries[] = array("code" => "AZ", "name" => "Azerbaijan", "d_code" => "00994"); + $countries[] = array("code" => "BH", "name" => "Bahrain", "d_code" => "00973"); + $countries[] = array("code" => "BD", "name" => "Bangladesh", "d_code" => "00880"); + $countries[] = array("code" => "BB", "name" => "Barbados", "d_code" => "001"); + $countries[] = array("code" => "BY", "name" => "Belarus", "d_code" => "00375"); + $countries[] = array("code" => "BE", "name" => "Belgium", "d_code" => "0032"); + $countries[] = array("code" => "BZ", "name" => "Belize", "d_code" => "00501"); + $countries[] = array("code" => "BJ", "name" => "Benin", "d_code" => "00229"); + $countries[] = array("code" => "BM", "name" => "Bermuda", "d_code" => "001"); + $countries[] = array("code" => "BT", "name" => "Bhutan", "d_code" => "00975"); + $countries[] = array("code" => "BO", "name" => "Bolivia", "d_code" => "00591"); + $countries[] = array("code" => "BA", "name" => "Bosnia and Herzegovina", "d_code" => "00387"); + $countries[] = array("code" => "BW", "name" => "Botswana", "d_code" => "00267"); + $countries[] = array("code" => "BR", "name" => "Brazil", "d_code" => "0055"); + $countries[] = array("code" => "IO", "name" => "British Indian Ocean Territory", "d_code" => "00246"); + $countries[] = array("code" => "VG", "name" => "British Virgin Islands", "d_code" => "001"); + $countries[] = array("code" => "BN", "name" => "Brunei", "d_code" => "00673"); + $countries[] = array("code" => "BG", "name" => "Bulgaria", "d_code" => "00359"); + $countries[] = array("code" => "BF", "name" => "Burkina Faso", "d_code" => "00226"); + $countries[] = array("code" => "MM", "name" => "Burma Myanmar", "d_code" => "0095"); + $countries[] = array("code" => "BI", "name" => "Burundi", "d_code" => "00257"); + $countries[] = array("code" => "KH", "name" => "Cambodia", "d_code" => "00855"); + $countries[] = array("code" => "CM", "name" => "Cameroon", "d_code" => "00237"); + $countries[] = array("code" => "CA", "name" => "Canada", "d_code" => "001"); + $countries[] = array("code" => "CV", "name" => "Cape Verde", "d_code" => "00238"); + $countries[] = array("code" => "KY", "name" => "Cayman Islands", "d_code" => "001"); + $countries[] = array("code" => "CF", "name" => "Central African Republic", "d_code" => "00236"); + $countries[] = array("code" => "TD", "name" => "Chad", "d_code" => "00235"); + $countries[] = array("code" => "CL", "name" => "Chile", "d_code" => "0056"); + $countries[] = array("code" => "CN", "name" => "China", "d_code" => "0086"); + $countries[] = array("code" => "CO", "name" => "Colombia", "d_code" => "0057"); + $countries[] = array("code" => "KM", "name" => "Comoros", "d_code" => "00269"); + $countries[] = array("code" => "CK", "name" => "Cook Islands", "d_code" => "00682"); + $countries[] = array("code" => "CR", "name" => "Costa Rica", "d_code" => "00506"); + $countries[] = array("code" => "CI", "name" => "Côte d'Ivoire", "d_code" => "00225"); + $countries[] = array("code" => "HR", "name" => "Croatia", "d_code" => "00385"); + $countries[] = array("code" => "CU", "name" => "Cuba", "d_code" => "0053"); + $countries[] = array("code" => "CY", "name" => "Cyprus", "d_code" => "00357"); + $countries[] = array("code" => "CZ", "name" => "Czech Republic", "d_code" => "00420"); + $countries[] = array("code" => "CD", "name" => "Democratic Republic of Congo", "d_code" => "00243"); + $countries[] = array("code" => "DK", "name" => "Denmark", "d_code" => "0045"); + $countries[] = array("code" => "DJ", "name" => "Djibouti", "d_code" => "00253"); + $countries[] = array("code" => "DM", "name" => "Dominica", "d_code" => "001"); + $countries[] = array("code" => "DO", "name" => "Dominican Republic", "d_code" => "001"); + $countries[] = array("code" => "EC", "name" => "Ecuador", "d_code" => "00593"); + $countries[] = array("code" => "EG", "name" => "Egypt", "d_code" => "0020"); + $countries[] = array("code" => "SV", "name" => "El Salvador", "d_code" => "00503"); + $countries[] = array("code" => "GQ", "name" => "Equatorial Guinea", "d_code" => "00240"); + $countries[] = array("code" => "ER", "name" => "Eritrea", "d_code" => "00291"); + $countries[] = array("code" => "EE", "name" => "Estonia", "d_code" => "00372"); + $countries[] = array("code" => "ET", "name" => "Ethiopia", "d_code" => "00251"); + $countries[] = array("code" => "FK", "name" => "Falkland Islands", "d_code" => "00500"); + $countries[] = array("code" => "FO", "name" => "Faroe Islands", "d_code" => "00298"); + $countries[] = array("code" => "FM", "name" => "Federated States of Micronesia", "d_code" => "00691"); + $countries[] = array("code" => "FJ", "name" => "Fiji", "d_code" => "00679"); + $countries[] = array("code" => "FI", "name" => "Finland", "d_code" => "00358"); + $countries[] = array("code" => "FR", "name" => "France", "d_code" => "0033"); + $countries[] = array("code" => "GF", "name" => "French Guiana", "d_code" => "00594"); + $countries[] = array("code" => "PF", "name" => "French Polynesia", "d_code" => "00689"); + $countries[] = array("code" => "GA", "name" => "Gabon", "d_code" => "00241"); + $countries[] = array("code" => "GE", "name" => "Georgia", "d_code" => "00995"); + $countries[] = array("code" => "DE", "name" => "Germany", "d_code" => "0049"); + $countries[] = array("code" => "GH", "name" => "Ghana", "d_code" => "00233"); + $countries[] = array("code" => "GI", "name" => "Gibraltar", "d_code" => "00350"); + $countries[] = array("code" => "GR", "name" => "Greece", "d_code" => "0030"); + $countries[] = array("code" => "GL", "name" => "Greenland", "d_code" => "00299"); + $countries[] = array("code" => "GD", "name" => "Grenada", "d_code" => "001"); + $countries[] = array("code" => "GP", "name" => "Guadeloupe", "d_code" => "00590"); + $countries[] = array("code" => "GU", "name" => "Guam", "d_code" => "001"); + $countries[] = array("code" => "GT", "name" => "Guatemala", "d_code" => "00502"); + $countries[] = array("code" => "GN", "name" => "Guinea", "d_code" => "00224"); + $countries[] = array("code" => "GW", "name" => "Guinea-Bissau", "d_code" => "00245"); + $countries[] = array("code" => "GY", "name" => "Guyana", "d_code" => "00592"); + $countries[] = array("code" => "HT", "name" => "Haiti", "d_code" => "00509"); + $countries[] = array("code" => "HN", "name" => "Honduras", "d_code" => "00504"); + $countries[] = array("code" => "HK", "name" => "Hong Kong", "d_code" => "00852"); + $countries[] = array("code" => "HU", "name" => "Hungary", "d_code" => "0036"); + $countries[] = array("code" => "IS", "name" => "Iceland", "d_code" => "00354"); + $countries[] = array("code" => "IN", "name" => "India", "d_code" => "0091"); + $countries[] = array("code" => "ID", "name" => "Indonesia", "d_code" => "0062"); + $countries[] = array("code" => "IR", "name" => "Iran", "d_code" => "0098"); + $countries[] = array("code" => "IQ", "name" => "Iraq", "d_code" => "00964"); + $countries[] = array("code" => "IE", "name" => "Ireland", "d_code" => "00353"); + $countries[] = array("code" => "IL", "name" => "Israel", "d_code" => "00972"); + $countries[] = array("code" => "IT", "name" => "Italy", "d_code" => "0039"); + $countries[] = array("code" => "JM", "name" => "Jamaica", "d_code" => "001"); + $countries[] = array("code" => "JP", "name" => "Japan", "d_code" => "0081"); + $countries[] = array("code" => "JO", "name" => "Jordan", "d_code" => "00962"); + $countries[] = array("code" => "KZ", "name" => "Kazakhstan", "d_code" => "007"); + $countries[] = array("code" => "KE", "name" => "Kenya", "d_code" => "00254"); + $countries[] = array("code" => "KI", "name" => "Kiribati", "d_code" => "00686"); + $countries[] = array("code" => "XK", "name" => "Kosovo", "d_code" => "00381"); + $countries[] = array("code" => "KW", "name" => "Kuwait", "d_code" => "00965"); + $countries[] = array("code" => "KG", "name" => "Kyrgyzstan", "d_code" => "00996"); + $countries[] = array("code" => "LA", "name" => "Laos", "d_code" => "00856"); + $countries[] = array("code" => "LV", "name" => "Latvia", "d_code" => "00371"); + $countries[] = array("code" => "LB", "name" => "Lebanon", "d_code" => "00961"); + $countries[] = array("code" => "LS", "name" => "Lesotho", "d_code" => "00266"); + $countries[] = array("code" => "LR", "name" => "Liberia", "d_code" => "00231"); + $countries[] = array("code" => "LY", "name" => "Libya", "d_code" => "00218"); + $countries[] = array("code" => "LI", "name" => "Liechtenstein", "d_code" => "00423"); + $countries[] = array("code" => "LT", "name" => "Lithuania", "d_code" => "00370"); + $countries[] = array("code" => "LU", "name" => "Luxembourg", "d_code" => "00352"); + $countries[] = array("code" => "MO", "name" => "Macau", "d_code" => "00853"); + $countries[] = array("code" => "MK", "name" => "Macedonia", "d_code" => "00389"); + $countries[] = array("code" => "MG", "name" => "Madagascar", "d_code" => "00261"); + $countries[] = array("code" => "MW", "name" => "Malawi", "d_code" => "00265"); + $countries[] = array("code" => "MY", "name" => "Malaysia", "d_code" => "0060"); + $countries[] = array("code" => "MV", "name" => "Maldives", "d_code" => "00960"); + $countries[] = array("code" => "ML", "name" => "Mali", "d_code" => "00223"); + $countries[] = array("code" => "MT", "name" => "Malta", "d_code" => "00356"); + $countries[] = array("code" => "MH", "name" => "Marshall Islands", "d_code" => "00692"); + $countries[] = array("code" => "MQ", "name" => "Martinique", "d_code" => "00596"); + $countries[] = array("code" => "MR", "name" => "Mauritania", "d_code" => "00222"); + $countries[] = array("code" => "MU", "name" => "Mauritius", "d_code" => "00230"); + $countries[] = array("code" => "YT", "name" => "Mayotte", "d_code" => "00262"); + $countries[] = array("code" => "MX", "name" => "Mexico", "d_code" => "0052"); + $countries[] = array("code" => "MD", "name" => "Moldova", "d_code" => "00373"); + $countries[] = array("code" => "MC", "name" => "Monaco", "d_code" => "00377"); + $countries[] = array("code" => "MN", "name" => "Mongolia", "d_code" => "00976"); + $countries[] = array("code" => "ME", "name" => "Montenegro", "d_code" => "00382"); + $countries[] = array("code" => "MS", "name" => "Montserrat", "d_code" => "001"); + $countries[] = array("code" => "MA", "name" => "Morocco", "d_code" => "00212"); + $countries[] = array("code" => "MZ", "name" => "Mozambique", "d_code" => "00258"); + $countries[] = array("code" => "NA", "name" => "Namibia", "d_code" => "00264"); + $countries[] = array("code" => "NR", "name" => "Nauru", "d_code" => "00674"); + $countries[] = array("code" => "NP", "name" => "Nepal", "d_code" => "00977"); + $countries[] = array("code" => "NL", "name" => "Netherlands", "d_code" => "0031"); + $countries[] = array("code" => "AN", "name" => "Netherlands Antilles", "d_code" => "00599"); + $countries[] = array("code" => "NC", "name" => "New Caledonia", "d_code" => "00687"); + $countries[] = array("code" => "NZ", "name" => "New Zealand", "d_code" => "0064"); + $countries[] = array("code" => "NI", "name" => "Nicaragua", "d_code" => "00505"); + $countries[] = array("code" => "NE", "name" => "Niger", "d_code" => "00227"); + $countries[] = array("code" => "NG", "name" => "Nigeria", "d_code" => "00234"); + $countries[] = array("code" => "NU", "name" => "Niue", "d_code" => "00683"); + $countries[] = array("code" => "NF", "name" => "Norfolk Island", "d_code" => "00672"); + $countries[] = array("code" => "KP", "name" => "North Korea", "d_code" => "00850"); + $countries[] = array("code" => "MP", "name" => "Northern Mariana Islands", "d_code" => "001"); + $countries[] = array("code" => "NO", "name" => "Norway", "d_code" => "0047"); + $countries[] = array("code" => "OM", "name" => "Oman", "d_code" => "00968"); + $countries[] = array("code" => "PK", "name" => "Pakistan", "d_code" => "0092"); + $countries[] = array("code" => "PW", "name" => "Palau", "d_code" => "00680"); + $countries[] = array("code" => "PS", "name" => "Palestine", "d_code" => "00970"); + $countries[] = array("code" => "PA", "name" => "Panama", "d_code" => "00507"); + $countries[] = array("code" => "PG", "name" => "Papua New Guinea", "d_code" => "00675"); + $countries[] = array("code" => "PY", "name" => "Paraguay", "d_code" => "00595"); + $countries[] = array("code" => "PE", "name" => "Peru", "d_code" => "0051"); + $countries[] = array("code" => "PH", "name" => "Philippines", "d_code" => "0063"); + $countries[] = array("code" => "PL", "name" => "Poland", "d_code" => "0048"); + $countries[] = array("code" => "PT", "name" => "Portugal", "d_code" => "00351"); + $countries[] = array("code" => "PR", "name" => "Puerto Rico", "d_code" => "001"); + $countries[] = array("code" => "QA", "name" => "Qatar", "d_code" => "00974"); + $countries[] = array("code" => "CG", "name" => "Republic of the Congo", "d_code" => "00242"); + $countries[] = array("code" => "RE", "name" => "Réunion", "d_code" => "00262"); + $countries[] = array("code" => "RO", "name" => "Romania", "d_code" => "0040"); + $countries[] = array("code" => "RU", "name" => "Russia", "d_code" => "007"); + $countries[] = array("code" => "RW", "name" => "Rwanda", "d_code" => "00250"); + $countries[] = array("code" => "BL", "name" => "Saint Barthélemy", "d_code" => "00590"); + $countries[] = array("code" => "SH", "name" => "Saint Helena", "d_code" => "00290"); + $countries[] = array("code" => "KN", "name" => "Saint Kitts and Nevis", "d_code" => "001"); + $countries[] = array("code" => "MF", "name" => "Saint Martin", "d_code" => "00590"); + $countries[] = array("code" => "PM", "name" => "Saint Pierre and Miquelon", "d_code" => "00508"); + $countries[] = array("code" => "VC", "name" => "Saint Vincent and the Grenadines", "d_code" => "001"); + $countries[] = array("code" => "WS", "name" => "Samoa", "d_code" => "00685"); + $countries[] = array("code" => "SM", "name" => "San Marino", "d_code" => "00378"); + $countries[] = array("code" => "ST", "name" => "São Tomé and Príncipe", "d_code" => "00239"); + $countries[] = array("code" => "SA", "name" => "Saudi Arabia", "d_code" => "00966"); + $countries[] = array("code" => "SN", "name" => "Senegal", "d_code" => "00221"); + $countries[] = array("code" => "RS", "name" => "Serbia", "d_code" => "00381"); + $countries[] = array("code" => "SC", "name" => "Seychelles", "d_code" => "00248"); + $countries[] = array("code" => "SL", "name" => "Sierra Leone", "d_code" => "00232"); + $countries[] = array("code" => "SG", "name" => "Singapore", "d_code" => "0065"); + $countries[] = array("code" => "SK", "name" => "Slovakia", "d_code" => "00421"); + $countries[] = array("code" => "SI", "name" => "Slovenia", "d_code" => "00386"); + $countries[] = array("code" => "SB", "name" => "Solomon Islands", "d_code" => "00677"); + $countries[] = array("code" => "SO", "name" => "Somalia", "d_code" => "00252"); + $countries[] = array("code" => "ZA", "name" => "South Africa", "d_code" => "0027"); + $countries[] = array("code" => "KR", "name" => "South Korea", "d_code" => "0082"); + $countries[] = array("code" => "ES", "name" => "Spain", "d_code" => "0034"); + $countries[] = array("code" => "LK", "name" => "Sri Lanka", "d_code" => "0094"); + $countries[] = array("code" => "LC", "name" => "St. Lucia", "d_code" => "001"); + $countries[] = array("code" => "SD", "name" => "Sudan", "d_code" => "00249"); + $countries[] = array("code" => "SR", "name" => "Suriname", "d_code" => "00597"); + $countries[] = array("code" => "SZ", "name" => "Swaziland", "d_code" => "00268"); + $countries[] = array("code" => "SE", "name" => "Sweden", "d_code" => "0046"); + $countries[] = array("code" => "CH", "name" => "Switzerland", "d_code" => "0041"); + $countries[] = array("code" => "SY", "name" => "Syria", "d_code" => "00963"); + $countries[] = array("code" => "TW", "name" => "Taiwan", "d_code" => "00886"); + $countries[] = array("code" => "TJ", "name" => "Tajikistan", "d_code" => "00992"); + $countries[] = array("code" => "TZ", "name" => "Tanzania", "d_code" => "00255"); + $countries[] = array("code" => "TH", "name" => "Thailand", "d_code" => "0066"); + $countries[] = array("code" => "BS", "name" => "The Bahamas", "d_code" => "001"); + $countries[] = array("code" => "GM", "name" => "The Gambia", "d_code" => "00220"); + $countries[] = array("code" => "TL", "name" => "Timor-Leste", "d_code" => "00670"); + $countries[] = array("code" => "TG", "name" => "Togo", "d_code" => "00228"); + $countries[] = array("code" => "TK", "name" => "Tokelau", "d_code" => "00690"); + $countries[] = array("code" => "TO", "name" => "Tonga", "d_code" => "00676"); + $countries[] = array("code" => "TT", "name" => "Trinidad and Tobago", "d_code" => "001"); + $countries[] = array("code" => "TN", "name" => "Tunisia", "d_code" => "00216"); + $countries[] = array("code" => "TR", "name" => "Turkey", "d_code" => "0090"); + $countries[] = array("code" => "TM", "name" => "Turkmenistan", "d_code" => "00993"); + $countries[] = array("code" => "TC", "name" => "Turks and Caicos Islands", "d_code" => "001"); + $countries[] = array("code" => "TV", "name" => "Tuvalu", "d_code" => "00688"); + $countries[] = array("code" => "UG", "name" => "Uganda", "d_code" => "00256"); + $countries[] = array("code" => "UA", "name" => "Ukraine", "d_code" => "00380"); + $countries[] = array("code" => "AE", "name" => "United Arab Emirates", "d_code" => "00971"); + $countries[] = array("code" => "GB", "name" => "United Kingdom", "d_code" => "0044"); + $countries[] = array("code" => "US", "name" => "United States", "d_code" => "001"); + $countries[] = array("code" => "UY", "name" => "Uruguay", "d_code" => "00598"); + $countries[] = array("code" => "VI", "name" => "US Virgin Islands", "d_code" => "001"); + $countries[] = array("code" => "UZ", "name" => "Uzbekistan", "d_code" => "00998"); + $countries[] = array("code" => "VU", "name" => "Vanuatu", "d_code" => "00678"); + $countries[] = array("code" => "VA", "name" => "Vatican City", "d_code" => "0039"); + $countries[] = array("code" => "VE", "name" => "Venezuela", "d_code" => "0058"); + $countries[] = array("code" => "VN", "name" => "Vietnam", "d_code" => "0084"); + $countries[] = array("code" => "WF", "name" => "Wallis and Futuna", "d_code" => "00681"); + $countries[] = array("code" => "YE", "name" => "Yemen", "d_code" => "00967"); + $countries[] = array("code" => "ZM", "name" => "Zambia", "d_code" => "00260"); + $countries[] = array("code" => "ZW", "name" => "Zimbabwe", "d_code" => "00263"); + return $countries; + } +} From c04555b9387dea448a5649348546e9629063dda8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 12:15:51 +0200 Subject: [PATCH 047/220] Another micro optimization --- src/Rules/RuleLevelHelper.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 0f15683606..ce937ac0d0 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -14,6 +14,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\StrictMixedType; @@ -497,7 +498,15 @@ private function findTypeToCheckImplementation( } $tip = null; - if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { + if ( + $type instanceof UnionType + && count($type->getTypes()) === 2 + && $type->getTypes()[0] instanceof ObjectType + && $type->getTypes()[1] instanceof ObjectType + && $type->getTypes()[0]->getClassName() === 'PhpParser\\Node\\Arg' + && $type->getTypes()[1]->getClassName() === 'PhpParser\\Node\\VariadicPlaceholder' + && !$unionTypeCriteriaCallback($type) + ) { $tip = 'Use ->getArgs() instead of ->args.'; } From 4dfbe16ed9cdf0808027f3bcbdb26980ec39df3f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 12:36:22 +0200 Subject: [PATCH 048/220] Optimization of huge unions of oversized arrays --- src/Type/TypeCombinator.php | 40 ++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index d27fd6fbdb..f1860d1abb 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -780,8 +780,10 @@ private static function optimizeConstantArrays(array $types): array } $results = []; + $eachIsOversized = true; foreach ($types as $type) { - $results[] = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $isOversized = false; + $result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type { if (!$type instanceof ConstantArrayType) { return $traverse($type); } @@ -790,6 +792,8 @@ private static function optimizeConstantArrays(array $types): array return $type; } + $isOversized = true; + $isList = true; $valueTypes = []; $keyTypes = []; @@ -808,8 +812,8 @@ private static function optimizeConstantArrays(array $types): array $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; $innerValueType = $type->getValueTypes()[$i]; - $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type, callable $innerTraverse) use ($traverse): Type { - if ($type instanceof ArrayType) { + $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type) use ($traverse): Type { + if ($type instanceof ArrayType || $type instanceof ConstantArrayType) { return TypeCombinator::intersect($type, new OversizedArrayType()); } @@ -828,6 +832,36 @@ private static function optimizeConstantArrays(array $types): array return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); }); + + if (!$isOversized) { + $eachIsOversized = false; + } + + $results[] = $result; + } + + if ($eachIsOversized) { + $eachIsList = true; + $keyTypes = []; + $valueTypes = []; + foreach ($results as $result) { + $keyTypes[] = $result->getIterableKeyType(); + $valueTypes[] = $result->getLastIterableValueType(); + if ($result->isList()->yes()) { + continue; + } + $eachIsList = false; + } + + $keyType = self::union(...array_values($keyTypes)); + $valueType = self::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($eachIsList) { + $arrayType = self::intersect($arrayType, new AccessoryArrayListType()); + } + + return [self::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType())]; } return $results; From 61c1c85751676b30b15594c97a4fafee7da77ee7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 12:38:20 +0200 Subject: [PATCH 049/220] Fix CS --- src/Rules/RuleLevelHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index ce937ac0d0..71c348f7ec 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -27,7 +27,6 @@ use function array_merge; use function count; use function sprintf; -use function str_contains; final class RuleLevelHelper { From 34fb83ddbecc9cd4ce5fcad661011846bfb185ee Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 13:02:21 +0200 Subject: [PATCH 050/220] Revert "processAssignVar optimization for arrays" This reverts commit 5d2b6244dd2e17fb078037b56a91cf3982e974ea. --- src/Analyser/NodeScopeResolver.php | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f4f60df2ae..afe1373704 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5078,30 +5078,7 @@ private function processAssignVar( $offsetNativeValueType = $varNativeType; $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetTypes, $offsetValueType, $valueToWrite); - - $nativeValueToWrite = $valueToWrite; - if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); - } else { - foreach ($offsetTypes as $i => $offsetType) { - $offsetNativeType = $offsetNativeTypes[$i]; - if ($offsetType === null) { - if ($offsetNativeType !== null) { - throw new ShouldNotHappenException(); - } - - continue; - } elseif ($offsetNativeType === null) { - throw new ShouldNotHappenException(); - } - if ($offsetType->equals($offsetNativeType)) { - continue; - } - - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); - break; - } - } + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { From c54b257dc9c85bc3fba732068a4dcfe16d65996b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 13:08:01 +0200 Subject: [PATCH 051/220] Fix build --- phpstan-baseline.neon | 5 +++++ src/Type/TypeCombinator.php | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 050d8a1d2c..1bdc95b954 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -635,6 +635,11 @@ parameters: count: 2 path: src/Rules/RuleLevelHelper.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 2 + path: src/Rules/RuleLevelHelper.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" count: 1 diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index f1860d1abb..ccbb29f97b 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -813,7 +813,7 @@ private static function optimizeConstantArrays(array $types): array $innerValueType = $type->getValueTypes()[$i]; $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type) use ($traverse): Type { - if ($type instanceof ArrayType || $type instanceof ConstantArrayType) { + if ($type instanceof ArrayType) { return TypeCombinator::intersect($type, new OversizedArrayType()); } @@ -853,8 +853,8 @@ private static function optimizeConstantArrays(array $types): array $eachIsList = false; } - $keyType = self::union(...array_values($keyTypes)); - $valueType = self::union(...array_values($valueTypes)); + $keyType = self::union(...$keyTypes); + $valueType = self::union(...$valueTypes); $arrayType = new ArrayType($keyType, $valueType); if ($eachIsList) { From 16f63b3a48105ddce37d816e157f004fe9164cf9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 11:51:55 +0200 Subject: [PATCH 052/220] processAssignVar optimization for arrays --- src/Analyser/NodeScopeResolver.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index afe1373704..f4f60df2ae 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5078,7 +5078,30 @@ private function processAssignVar( $offsetNativeValueType = $varNativeType; $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetTypes, $offsetValueType, $valueToWrite); - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + + $nativeValueToWrite = $valueToWrite; + if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + } else { + foreach ($offsetTypes as $i => $offsetType) { + $offsetNativeType = $offsetNativeTypes[$i]; + if ($offsetType === null) { + if ($offsetNativeType !== null) { + throw new ShouldNotHappenException(); + } + + continue; + } elseif ($offsetNativeType === null) { + throw new ShouldNotHappenException(); + } + if ($offsetType->equals($offsetNativeType)) { + continue; + } + + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + break; + } + } if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { From b0df8498353291d982553306005b4669d50a8ff4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 13:10:49 +0200 Subject: [PATCH 053/220] Fix CS --- src/Rules/RuleLevelHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 81d166902f..416583546f 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -21,7 +21,6 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; From deef91983766dd61c18d4f9d819ffc94fb701cd5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 15:31:26 +0200 Subject: [PATCH 054/220] Avoid new HasOffsetValueType being intersected with oversized array --- phpstan-baseline.neon | 10 ++++++++ .../Constant/ConstantArrayTypeBuilder.php | 2 ++ src/Type/IntersectionType.php | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1bdc95b954..1521bbb1a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1170,6 +1170,11 @@ parameters: count: 3 path: src/Type/IntersectionType.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/IntersectionType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" count: 1 @@ -1180,6 +1185,11 @@ parameters: count: 2 path: src/Type/IntersectionType.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/IntersectionType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 4 diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index a306833e8d..e5caa1117a 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -128,6 +128,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { $this->degradeToGeneralArray = true; + $this->oversized = true; } return; @@ -194,6 +195,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { $this->degradeToGeneralArray = true; + $this->oversized = true; } return; diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 140e45a8ed..6a8d2ab329 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -28,6 +28,7 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -722,6 +723,30 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + if ($this->isOversizedArray()->yes()) { + return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { + // avoid new HasOffsetValueType being intersected with oversized array + if (!$type instanceof ArrayType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType instanceof ConstantStringType && !$offsetType instanceof ConstantIntegerType) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + if (!$offsetType->isSuperTypeOf($type->getKeyType())->yes()) { + return $type->setOffsetValueType($offsetType, $valueType, $unionValues); + } + + return TypeCombinator::intersect( + new ArrayType( + TypeCombinator::union($type->getKeyType(), $offsetType), + TypeCombinator::union($type->getItemType(), $valueType), + ), + new NonEmptyArrayType(), + ); + }); + } return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } From a960f74d4f7796b241b50dbc6553b14449fe21bc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 15:34:18 +0200 Subject: [PATCH 055/220] Regression test Closes https://github.com/phpstan/phpstan/issues/10847 --- .../Analyser/AnalyserIntegrationTest.php | 6 + tests/PHPStan/Analyser/data/bug-10847.php | 880 ++++++++++++++++++ 2 files changed, 886 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-10847.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e5a45b8a10..f80796bce5 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1333,6 +1333,12 @@ public function testBug10538(): void $this->assertNoErrors($errors); } + public function testBug10847(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10847.php'); + $this->assertNoErrors($errors); + } + public function testBug10772(): void { if (PHP_VERSION_ID < 80100) { diff --git a/tests/PHPStan/Analyser/data/bug-10847.php b/tests/PHPStan/Analyser/data/bug-10847.php new file mode 100644 index 0000000000..6a3dd0bbb0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10847.php @@ -0,0 +1,880 @@ +): Value>, url: string}> + */ + private const BUILTIN_FUNCTIONS = [ + // sass:color + 'red' => ['overloads' => ['$color' => [ColorFunctions::class, 'red']], 'url' => 'sass:color'], + 'green' => ['overloads' => ['$color' => [ColorFunctions::class, 'green']], 'url' => 'sass:color'], + 'blue' => ['overloads' => ['$color' => [ColorFunctions::class, 'blue']], 'url' => 'sass:color'], + 'mix' => ['overloads' => ['$color1, $color2, $weight: 50%' => [ColorFunctions::class, 'mix']], 'url' => 'sass:color'], + 'rgb' => ['overloads' => [ + '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgb'], + '$red, $green, $blue' => [ColorFunctions::class, 'rgb'], + '$color, $alpha' => [ColorFunctions::class, 'rgbTwoArgs'], + '$channels' => [ColorFunctions::class, 'rgbOneArgs'], + ], 'url' => 'sass:color'], + 'rgba' => ['overloads' => [ + '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgba'], + '$red, $green, $blue' => [ColorFunctions::class, 'rgba'], + '$color, $alpha' => [ColorFunctions::class, 'rgbaTwoArgs'], + '$channels' => [ColorFunctions::class, 'rgbaOneArgs'], + ], 'url' => 'sass:color'], + 'invert' => ['overloads' => ['$color, $weight: 100%' => [ColorFunctions::class, 'invert']], 'url' => 'sass:color'], + 'hue' => ['overloads' => ['$color' => [ColorFunctions::class, 'hue']], 'url' => 'sass:color'], + 'saturation' => ['overloads' => ['$color' => [ColorFunctions::class, 'saturation']], 'url' => 'sass:color'], + 'lightness' => ['overloads' => ['$color' => [ColorFunctions::class, 'lightness']], 'url' => 'sass:color'], + 'complement' => ['overloads' => ['$color' => [ColorFunctions::class, 'complement']], 'url' => 'sass:color'], + 'hsl' => ['overloads' => [ + '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsl'], + '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsl'], + '$hue, $saturation' => [ColorFunctions::class, 'hslTwoArgs'], + '$channels' => [ColorFunctions::class, 'hslOneArgs'], + ], 'url' => 'sass:color'], + 'hsla' => ['overloads' => [ + '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsla'], + '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsla'], + '$hue, $saturation' => [ColorFunctions::class, 'hslaTwoArgs'], + '$channels' => [ColorFunctions::class, 'hslaOneArgs'], + ], 'url' => 'sass:color'], + 'grayscale' => ['overloads' => ['$color' => [ColorFunctions::class, 'grayscale']], 'url' => 'sass:color'], + 'adjust-hue' => ['overloads' => ['$color, $degrees' => [ColorFunctions::class, 'adjustHue']], 'url' => 'sass:color'], + 'lighten' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'lighten']], 'url' => 'sass:color'], + 'darken' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'darken']], 'url' => 'sass:color'], + 'saturate' => ['overloads' => [ + '$amount' => [ColorFunctions::class, 'saturateCss'], + '$color, $amount' => [ColorFunctions::class, 'saturate'], + ], 'url' => 'sass:color'], + 'desaturate' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'desaturate']], 'url' => 'sass:color'], + 'opacify' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'], + 'fade-in' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'], + 'transparentize' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'], + 'fade-out' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'], + 'alpha' => ['overloads' => [ + '$color' => [ColorFunctions::class, 'alpha'], + '$args...' => [ColorFunctions::class, 'alphaMicrosoft'], + ], 'url' => 'sass:color'], + 'opacity' => ['overloads' => ['$color' => [ColorFunctions::class, 'opacity']], 'url' => 'sass:color'], + 'ie-hex-str' => ['overloads' => ['$color' => [ColorFunctions::class, 'ieHexStr']], 'url' => 'sass:color'], + 'adjust-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'adjust']], 'url' => 'sass:color'], + 'scale-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'scale']], 'url' => 'sass:color'], + 'change-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'change']], 'url' => 'sass:color'], + // sass:list + 'length' => ['overloads' => ['$list' => [ListFunctions::class, 'length']], 'url' => 'sass:list'], + 'nth' => ['overloads' => ['$list, $n' => [ListFunctions::class, 'nth']], 'url' => 'sass:list'], + 'set-nth' => ['overloads' => ['$list, $n, $value' => [ListFunctions::class, 'setNth']], 'url' => 'sass:list'], + 'join' => ['overloads' => ['$list1, $list2, $separator: auto, $bracketed: auto' => [ListFunctions::class, 'join']], 'url' => 'sass:list'], + 'append' => ['overloads' => ['$list, $val, $separator: auto' => [ListFunctions::class, 'append']], 'url' => 'sass:list'], + 'zip' => ['overloads' => ['$lists...' => [ListFunctions::class, 'zip']], 'url' => 'sass:list'], + 'index' => ['overloads' => ['$list, $value' => [ListFunctions::class, 'index']], 'url' => 'sass:list'], + 'is-bracketed' => ['overloads' => ['$list' => [ListFunctions::class, 'isBracketed']], 'url' => 'sass:list'], + 'list-separator' => ['overloads' => ['$list' => [ListFunctions::class, 'separator']], 'url' => 'sass:list'], + // sass:map + 'map-get' => ['overloads' => ['$map, $key, $keys...' => [MapFunctions::class, 'get']], 'url' => 'sass:map'], + 'map-merge' => ['overloads' => [ + '$map1, $map2' => [MapFunctions::class, 'mergeTwoArgs'], + '$map1, $args...' => [MapFunctions::class, 'mergeVariadic'], + ], 'url' => 'sass:map'], + 'map-remove' => ['overloads' => [ + // Because the signature below has an explicit `$key` argument, it doesn't + // allow zero keys to be passed. We want to allow that case, so we add an + // explicit overload for it. + '$map' => [MapFunctions::class, 'removeNoKeys'], + // The first argument has special handling so that the $key parameter can be + // passed by name. + '$map, $key, $keys...' => [MapFunctions::class, 'remove'], + ], 'url' => 'sass:map'], + 'map-keys' => ['overloads' => ['$map' => [MapFunctions::class, 'keys']], 'url' => 'sass:map'], + 'map-values' => ['overloads' => ['$map' => [MapFunctions::class, 'values']], 'url' => 'sass:map'], + 'map-has-key' => ['overloads' => ['map, $key, $keys...' => [MapFunctions::class, 'hasKey']], 'url' => 'sass:map'], + // sass:math + 'abs' => ['overloads' => ['$number' => [MathFunctions::class, 'abs']], 'url' => 'sass:math'], + 'ceil' => ['overloads' => ['$number' => [MathFunctions::class, 'ceil']], 'url' => 'sass:math'], + 'floor' => ['overloads' => ['$number' => [MathFunctions::class, 'floor']], 'url' => 'sass:math'], + 'max' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'max']], 'url' => 'sass:math'], + 'min' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'min']], 'url' => 'sass:math'], + 'random' => ['overloads' => ['$limit: null' => [MathFunctions::class, 'random']], 'url' => 'sass:math'], + 'percentage' => ['overloads' => ['$number' => [MathFunctions::class, 'percentage']], 'url' => 'sass:math'], + 'round' => ['overloads' => ['$number' => [MathFunctions::class, 'round']], 'url' => 'sass:math'], + 'unit' => ['overloads' => ['$number' => [MathFunctions::class, 'unit']], 'url' => 'sass:math'], + 'comparable' => ['overloads' => ['$number1, $number2' => [MathFunctions::class, 'compatible']], 'url' => 'sass:math'], + 'unitless' => ['overloads' => ['$number' => [MathFunctions::class, 'isUnitless']], 'url' => 'sass:math'], + // sass:meta + 'feature-exists' => ['overloads' => ['$feature' => [MetaFunctions::class, 'featureExists']], 'url' => 'sass:meta'], + 'inspect' => ['overloads' => ['$value' => [MetaFunctions::class, 'inspect']], 'url' => 'sass:meta'], + 'type-of' => ['overloads' => ['$value' => [MetaFunctions::class, 'typeof']], 'url' => 'sass:meta'], + // sass:selector + 'is-superselector' => ['overloads' => ['$super, $sub' => [SelectorFunctions::class, 'isSuperselector']], 'url' => 'sass:selector'], + 'simple-selectors' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'simpleSelectors']], 'url' => 'sass:selector'], + 'selector-parse' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'parse']], 'url' => 'sass:selector'], + 'selector-nest' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'nest']], 'url' => 'sass:selector'], + 'selector-append' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'append']], 'url' => 'sass:selector'], + 'selector-extend' => ['overloads' => ['$selector, $extendee, $extender' => [SelectorFunctions::class, 'extend']], 'url' => 'sass:selector'], + 'selector-replace' => ['overloads' => ['$selector, $original, $replacement' => [SelectorFunctions::class, 'replace']], 'url' => 'sass:selector'], + 'selector-unify' => ['overloads' => ['$selector1, $selector2' => [SelectorFunctions::class, 'unify']], 'url' => 'sass:selector'], + // sass:string + 'unquote' => ['overloads' => ['$string' => [StringFunctions::class, 'unquote']], 'url' => 'sass:string'], + 'quote' => ['overloads' => ['$string' => [StringFunctions::class, 'quote']], 'url' => 'sass:string'], + 'to-upper-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toUpperCase']], 'url' => 'sass:string'], + 'to-lower-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toLowerCase']], 'url' => 'sass:string'], + 'uniqueId' => ['overloads' => ['' => [StringFunctions::class, 'uniqueId']], 'url' => 'sass:string'], + 'str-length' => ['overloads' => ['$string' => [StringFunctions::class, 'length']], 'url' => 'sass:string'], + 'str-insert' => ['overloads' => ['$string, $insert, $index' => [StringFunctions::class, 'insert']], 'url' => 'sass:string'], + 'str-index' => ['overloads' => ['$string, $substring' => [StringFunctions::class, 'index']], 'url' => 'sass:string'], + 'str-slice' => ['overloads' => ['$string, $start-at, $end-at: -1' => [StringFunctions::class, 'slice']], 'url' => 'sass:string'], + ]; + + public static function has(string $name): bool + { + return isset(self::BUILTIN_FUNCTIONS[$name]); + } + + public static function get(string $name): BuiltInCallable + { + if (!isset(self::BUILTIN_FUNCTIONS[$name])) { + throw new \InvalidArgumentException("There is no builtin function named $name."); + } + + return BuiltInCallable::overloadedFunction($name, self::BUILTIN_FUNCTIONS[$name]['overloads'], self::BUILTIN_FUNCTIONS[$name]['url']); + } +} + +abstract class Value {} + +class BuiltInCallable +{ + /** + * @param array): Value> $overloads + */ + public static function overloadedFunction(string $name, array $overloads, ?string $url = null): BuiltInCallable + { + $processedOverloads = []; + + foreach ($overloads as $args => $callback) { + $overloads[] = [ + $args, + $callback, + ]; + } + + return new BuiltInCallable($name, $processedOverloads, $url); + } + + /** + * @param list): Value}> $overloads + */ + private function __construct(public readonly string $name, public readonly array $overloads, public readonly ?string $url) + { + } +} + +/** + * @internal + */ +class ColorFunctions +{ + /** + * @param list $arguments + */ + public static function rgb(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgba(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbaTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function rgbaOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function invert(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hsl(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hsla(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslaTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hslaOneArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function grayscale(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function adjustHue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function lighten(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function darken(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturateCss(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturate(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function desaturate(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function alpha(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function alphaMicrosoft(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function opacity(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function red(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function green(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function blue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mix(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hue(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function saturation(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function lightness(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function complement(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function adjust(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function scale(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function change(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function ieHexStr(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function opacify(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function transparentize(array $arguments): Value + { + return $arguments[0]; + } +} +class ListFunctions +{ + /** + * @param list $arguments + */ + public static function length(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function nth(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function setNth(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function join(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function append(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function zip(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function index(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function separator(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isBracketed(array $arguments): Value + { + return $arguments[0]; + } +} +class MapFunctions +{ + /** + * @param list $arguments + */ + public static function get(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mergeTwoArgs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function mergeVariadic(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function removeNoKeys(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function remove(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function keys(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function values(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function hasKey(array $arguments): Value + { + return $arguments[0]; + } +} +final class MathFunctions +{ + /** + * @param list $arguments + */ + public static function abs(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function ceil(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function floor(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function max(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function min(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function round(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function compatible(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isUnitless(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function unit(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function percentage(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function random(array $arguments): Value + { + return $arguments[0]; + } +} +final class MetaFunctions +{ + /** + * @param list $arguments + */ + public static function featureExists(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function inspect(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function typeof(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function keywords(array $arguments): Value + { + return $arguments[0]; + } +} +final class SelectorFunctions +{ + /** + * @param list $arguments + */ + public static function nest(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function append(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function extend(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function replace(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function unify(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function isSuperselector(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function simpleSelectors(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function parse(array $arguments): Value + { + return $arguments[0]; + } +} +final class StringFunctions +{ + /** + * @param list $arguments + */ + public static function unquote(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function quote(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function length(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function insert(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function index(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function slice(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function toUpperCase(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function toLowerCase(array $arguments): Value + { + return $arguments[0]; + } + + /** + * @param list $arguments + */ + public static function uniqueId(array $arguments): Value + { + return $arguments[0]; + } +} From 5d2512ae1e570d4961b76cf1449dba6d75aa039b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 15:40:27 +0200 Subject: [PATCH 056/220] Fix build --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index f80796bce5..ebdf3a03e4 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1335,6 +1335,10 @@ public function testBug10538(): void public function testBug10847(): void { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10847.php'); $this->assertNoErrors($errors); } From 78690539f0590860dd4f1ffb35f5f524e877a3ad Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Oct 2024 16:16:44 +0200 Subject: [PATCH 057/220] Fix --- src/Analyser/NodeScopeResolver.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f4f60df2ae..5831cdaaaf 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5079,10 +5079,10 @@ private function processAssignVar( $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetTypes, $offsetValueType, $valueToWrite); - $nativeValueToWrite = $valueToWrite; if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); } else { + $rewritten = false; foreach ($offsetTypes as $i => $offsetType) { $offsetNativeType = $offsetNativeTypes[$i]; if ($offsetType === null) { @@ -5099,8 +5099,13 @@ private function processAssignVar( } $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite); + $rewritten = true; break; } + + if (!$rewritten) { + $nativeValueToWrite = $valueToWrite; + } } if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { From f361e354a91cad1b05f8e196172cf6245aace77f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 02:01:36 +0000 Subject: [PATCH 058/220] Update crate-ci/typos action to v1.26.8 --- .github/workflows/spelling.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 41282ea069..8dadf24aba 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -18,6 +18,6 @@ jobs: uses: actions/checkout@v4 - name: "Check for typos" - uses: "crate-ci/typos@v1.26.0" + uses: "crate-ci/typos@v1.26.8" with: files: "README.md src/" From 5797c27e0d7e943b67b717f023e2ac2fd8a25140 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:03:56 +0000 Subject: [PATCH 059/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index fd100caeb6..a14b805eaa 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#f8625adce08b146bf481a0f1bbee06e82a488059", + "jetbrains/phpstorm-stubs": "dev-master#08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 88147fa23b..caf98eda55 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b46b210764567d0dae89d02afb1c9b92", + "content-hash": "092bad73652f5dc799eca247aaeb21db", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "f8625adce08b146bf481a0f1bbee06e82a488059" + "reference": "08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/f8625adce08b146bf481a0f1bbee06e82a488059", - "reference": "f8625adce08b146bf481a0f1bbee06e82a488059", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", + "reference": "08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-10-20T18:41:15+00:00" + "time": "2024-10-28T16:40:57+00:00" }, { "name": "nette/bootstrap", From 277e34b23a43b7ff0cca0c141789d0a786690e9b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 30 Oct 2024 12:58:38 +0100 Subject: [PATCH 060/220] Exclude E_DEPRECATED from error_reporting This error is filtered in https://github.com/phpstan/phpstan-src/blob/5797c27e0d7e943b67b717f023e2ac2fd8a25140/src/Analyser/FileAnalyser.php#L329 anyway. But it might still be seen when the deprecation is triggered by one of autoload.files in included Composer autoloaders. --- bin/phpstan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/phpstan b/bin/phpstan index 8d6cd25cd5..03a1a8aad7 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -11,7 +11,7 @@ use PHPStan\Internal\ComposerHelper; use Symfony\Component\Console\Helper\ProgressBar; (function () { - error_reporting(E_ALL); + error_reporting(E_ALL & ~E_DEPRECATED); ini_set('display_errors', 'stderr'); if (version_compare(PHP_VERSION, '7.4.0', '<')) { // PHP earlier than 7.4.x with OpCache triggers a bug when we intercept From 8fbcf5bdc6250cb4da57ad5945f371bb0c65f931 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Oct 2024 20:18:04 +0100 Subject: [PATCH 061/220] More precise types in immediately invoked callables --- conf/config.neon | 5 +++ src/Analyser/MutatingScope.php | 6 ++- src/Analyser/NodeScopeResolver.php | 6 +-- .../ImmediatelyInvokedClosureVisitor.php | 22 ++++++++++ tests/PHPStan/Analyser/nsrt/bug-11561.php | 40 +++++++++++++++++++ 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/Parser/ImmediatelyInvokedClosureVisitor.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11561.php diff --git a/conf/config.neon b/conf/config.neon index 5ad99397a9..7c88f87a31 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -733,6 +733,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ImmediatelyInvokedClosureVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parallel\ParallelAnalyser arguments: diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 90b3fdd465..2724ba1fca 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -46,6 +46,7 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\PropertyAssignNode; use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; @@ -4794,6 +4795,7 @@ private function processFinallyScopeVariableTypeHolders( * @param Expr\ClosureUse[] $byRefUses */ public function processClosureScope( + Expr\Closure $expr, self $closureScope, ?self $prevScope, array $byRefUses, @@ -4826,7 +4828,9 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType, 0); + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true) { + $variableType = self::generalizeType($variableType, $prevVariableType, 0); + } } } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5831cdaaaf..fd6ac13fc3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4232,7 +4232,7 @@ private function processClosureNode( } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); + $closureScope = $closureScope->processClosureScope($expr, $scope, null, $byRefUses); $closureType = $closureScope->getAnonymousFunctionReflection(); if (!$closureType instanceof ClosureType) { throw new ShouldNotHappenException(); @@ -4302,7 +4302,7 @@ private function processClosureNode( $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + $closureScope = $closureScope->processClosureScope($expr, $intermediaryClosureScope, $prevScope, $byRefUses); if ($closureScope->equals($prevScope)) { break; } @@ -4322,7 +4322,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope->processClosureScope($expr, $closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } /** diff --git a/src/Parser/ImmediatelyInvokedClosureVisitor.php b/src/Parser/ImmediatelyInvokedClosureVisitor.php new file mode 100644 index 0000000000..c77059e214 --- /dev/null +++ b/src/Parser/ImmediatelyInvokedClosureVisitor.php @@ -0,0 +1,22 @@ +name instanceof Node\Expr\Closure) { + $node->name->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11561.php b/tests/PHPStan/Analyser/nsrt/bug-11561.php new file mode 100644 index 0000000000..1a01e5f97a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11561.php @@ -0,0 +1,40 @@ += 8.0 + +namespace Bug11561; + +use function PHPStan\Testing\assertType; +use DateTime; + +/** @param array{date: DateTime} $c */ +function main(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + assertType('array{date: DateTime, id: 1}', $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); +} + + +/** @param array{date: DateTime} $c */ +function main2(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} From 6bd0a5fc3054326a2134ab8bd4d28814b3da16c7 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 1 Nov 2024 00:22:40 +0000 Subject: [PATCH 062/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index a14b805eaa..28752bbba8 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.42.0.11", - "phpstan/php-8-stubs": "0.4.3", + "phpstan/php-8-stubs": "0.4.4", "phpstan/phpdoc-parser": "^2.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index caf98eda55..2c31e7e3db 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "092bad73652f5dc799eca247aaeb21db", + "content-hash": "35d6da6b01195515a7b8fcee8a2f952b", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.3", + "version": "0.4.4", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "b30c6975205b4b51e7d8c635f57d29b869220a9e" + "reference": "c42f6e278d600b219b76d20f80f8455259bcd593" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/b30c6975205b4b51e7d8c635f57d29b869220a9e", - "reference": "b30c6975205b4b51e7d8c635f57d29b869220a9e", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/c42f6e278d600b219b76d20f80f8455259bcd593", + "reference": "c42f6e278d600b219b76d20f80f8455259bcd593", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.3" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.4" }, - "time": "2024-10-18T00:19:10+00:00" + "time": "2024-11-01T00:22:02+00:00" }, { "name": "phpstan/phpdoc-parser", From 88d5b8cf0cb2da8873dc33222b5f8b4727f0756a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 2 Nov 2024 14:34:39 +0100 Subject: [PATCH 063/220] Remove dead code in ConstantConditionRuleHelper --- src/Rules/Comparison/ConstantConditionRuleHelper.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index dc3167a38d..36b2c569d8 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -18,16 +18,6 @@ public function __construct( { } - public function shouldReportAlwaysTrueByDefault(Expr $expr): bool - { - return $expr instanceof Expr\BooleanNot - || $expr instanceof Expr\BinaryOp\BooleanOr - || $expr instanceof Expr\BinaryOp\BooleanAnd - || $expr instanceof Expr\Ternary - || $expr instanceof Expr\Isset_ - || $expr instanceof Expr\Empty_; - } - public function shouldSkip(Scope $scope, Expr $expr): bool { if ( From d5486cd84e1b59d21bbacca6c91fb0cb3b15cfb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 01:19:59 +0000 Subject: [PATCH 064/220] Update github-actions --- .github/workflows/issue-bot.yml | 2 +- .github/workflows/reflection-golden-test.yml | 2 +- .github/workflows/spelling.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index 530886afff..eca615b3ce 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -97,7 +97,7 @@ jobs: working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - - uses: Wandalen/wretry.action@v3.5.0 + - uses: Wandalen/wretry.action@v3.7.0 with: action: actions/download-artifact@v4 with: | diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index 8b84920a73..b4a0ac5fd5 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -73,7 +73,7 @@ jobs: - "8.4" steps: - - uses: Wandalen/wretry.action@v3.5.0 + - uses: Wandalen/wretry.action@v3.7.0 with: action: actions/download-artifact@v4 with: | diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 8dadf24aba..81d852e90b 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -18,6 +18,6 @@ jobs: uses: actions/checkout@v4 - name: "Check for typos" - uses: "crate-ci/typos@v1.26.8" + uses: "crate-ci/typos@v1.27.0" with: files: "README.md src/" From 595091012739d03fb364a40a51a8d30671fe9e80 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 4 Nov 2024 09:39:15 +0100 Subject: [PATCH 065/220] Add `@api` to TypeExpr See https://github.com/phpstan/phpstan/discussions/11960 --- src/Node/Expr/TypeExpr.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Node/Expr/TypeExpr.php b/src/Node/Expr/TypeExpr.php index 35e364f15c..c21a2099c3 100644 --- a/src/Node/Expr/TypeExpr.php +++ b/src/Node/Expr/TypeExpr.php @@ -6,9 +6,13 @@ use PHPStan\Node\VirtualNode; use PHPStan\Type\Type; +/** + * @api + */ final class TypeExpr extends Expr implements VirtualNode { + /** @api */ public function __construct(private Type $exprType) { parent::__construct(); From 1b4997efa65974b5a7827fe764ca14f0768a7afa Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:03:15 +0000 Subject: [PATCH 066/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 28752bbba8..1afe40674c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", + "jetbrains/phpstorm-stubs": "dev-master#1f0dca06d54cf187adb3481a9c3e7d74af01743b", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 2c31e7e3db..4a1e51a351 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "35d6da6b01195515a7b8fcee8a2f952b", + "content-hash": "350cc72e1a307581ffc8228f105bd273", "packages": [ { "name": "clue/ndjson-react", @@ -1442,19 +1442,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab" + "reference": "1f0dca06d54cf187adb3481a9c3e7d74af01743b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", - "reference": "08ee6c06d3d6021399c02ae1e4e91ae2ceaf90ab", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/1f0dca06d54cf187adb3481a9c3e7d74af01743b", + "reference": "1f0dca06d54cf187adb3481a9c3e7d74af01743b", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "v3.61.1", + "friendsofphp/php-cs-fixer": "v3.64.0", "nikic/php-parser": "v5.3.1", "phpdocumentor/reflection-docblock": "5.4.1", - "phpunit/phpunit": "11.3.0" + "phpunit/phpunit": "11.4.3" }, "default-branch": true, "type": "library", @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-10-28T16:40:57+00:00" + "time": "2024-11-04T21:28:48+00:00" }, { "name": "nette/bootstrap", From 5f064ddc5d2e998689ea0846b8d3eea46fca4921 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 5 Nov 2024 12:27:48 +0100 Subject: [PATCH 067/220] More precise types in immediately invoked callables --- src/Analyser/MutatingScope.php | 6 +-- src/Analyser/NodeScopeResolver.php | 20 ++++++++-- tests/PHPStan/Analyser/nsrt/bug-11561.php | 46 +++++++++++++++++++++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 2724ba1fca..90b3fdd465 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -46,7 +46,6 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\PropertyAssignNode; use PHPStan\Parser\ArrayMapArgVisitor; -use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; @@ -4795,7 +4794,6 @@ private function processFinallyScopeVariableTypeHolders( * @param Expr\ClosureUse[] $byRefUses */ public function processClosureScope( - Expr\Closure $expr, self $closureScope, ?self $prevScope, array $byRefUses, @@ -4828,9 +4826,7 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true) { - $variableType = self::generalizeType($variableType, $prevVariableType, 0); - } + $variableType = self::generalizeType($variableType, $prevVariableType, 0); } } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fd6ac13fc3..fc7b1e9df7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -119,6 +119,7 @@ use PHPStan\Node\VarTagChangedExpressionTypeNode; use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; +use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; @@ -4232,7 +4233,7 @@ private function processClosureNode( } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($expr, $scope, null, $byRefUses); + $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); $closureType = $closureScope->getAnonymousFunctionReflection(); if (!$closureType instanceof ClosureType) { throw new ShouldNotHappenException(); @@ -4277,6 +4278,7 @@ private function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }; + if (count($byRefUses) === 0) { $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( @@ -4292,6 +4294,7 @@ private function processClosureNode( } $count = 0; + $closureResultScope = null; do { $prevScope = $closureScope; @@ -4301,8 +4304,15 @@ private function processClosureNode( foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); } + + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { + $closureResultScope = $intermediaryClosureScope; + break; + } + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($expr, $intermediaryClosureScope, $prevScope, $byRefUses); + $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + if ($closureScope->equals($prevScope)) { break; } @@ -4312,6 +4322,10 @@ private function processClosureNode( $count++; } while ($count < self::LOOP_SCOPE_ITERATIONS); + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, @@ -4322,7 +4336,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope->processClosureScope($expr, $closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-11561.php b/tests/PHPStan/Analyser/nsrt/bug-11561.php index 1a01e5f97a..f6894d4724 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11561.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11561.php @@ -12,13 +12,13 @@ function main(mixed $c): void{ assertType('array{date: DateTime, id: 1}', $c); $x = (function() use (&$c) { - assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); + assertType("array{date: DateTime, id: 1}", $c); $c['name'] = 'ruud'; assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); return 'x'; })(); - assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); } @@ -30,11 +30,51 @@ function main2(mixed $c): void{ assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); $x = (function() use (&$c) { - assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); $c['name'] = 'ruud'; assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); return 'x'; })(); + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main3(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + } + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +} + +/** @param array{date: DateTime} $c */ +function main4(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + if (rand(0,1)) { + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'y'; + } + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + return 'x'; + })(); + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); } From 801924357745759a86d9118f1ccf04f9114d554d Mon Sep 17 00:00:00 2001 From: Jonathan Goode Date: Fri, 1 Nov 2024 16:31:47 +0000 Subject: [PATCH 068/220] Support returning an array or a string in `count_chars()` --- conf/config.neon | 7 ++- ...harsFunctionDynamicReturnTypeExtension.php | 59 +++++++++++++++++++ .../PHPStan/Analyser/nsrt/count-chars-7.4.php | 21 +++++++ .../PHPStan/Analyser/nsrt/count-chars-8.0.php | 21 +++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/count-chars-7.4.php create mode 100644 tests/PHPStan/Analyser/nsrt/count-chars-8.0.php diff --git a/conf/config.neon b/conf/config.neon index e307d1380b..24156dc09b 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1262,13 +1262,18 @@ services: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\ConstantHelper + class: PHPStan\Type\Php\ConstantHelper - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\CountCharsFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\CountFunctionTypeSpecifyingExtension tags: diff --git a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..cddb7e7989 --- /dev/null +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,59 @@ +getName() === 'count_chars'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $modeType = $scope->getType($functionCall->getArgs()[1]->value); + + if (IntegerRangeType::fromInterval(0, 2)->isSuperTypeOf($modeType)->yes()) { + $arrayType = new ArrayType(new IntegerType(), new IntegerType()); + + return $this->phpVersion->throwsValueErrorForInternalFunctions() + ? $arrayType + : TypeUtils::toBenevolentUnion(new UnionType([$arrayType, new ConstantBooleanType(false)])); + } + + $stringType = new StringType(); + + return $this->phpVersion->throwsValueErrorForInternalFunctions() + ? $stringType + : TypeUtils::toBenevolentUnion(new UnionType([$stringType, new ConstantBooleanType(false)])); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php new file mode 100644 index 0000000000..713d73a5e1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php @@ -0,0 +1,21 @@ +|false', count_chars(self::ABC, 0)); + assertType('array|false', count_chars(self::ABC, 1)); + assertType('array|false', count_chars(self::ABC, 2)); + + assertType('string|false', count_chars(self::ABC, 3)); + assertType('string|false', count_chars(self::ABC, 4)); + + assertType('string|false', count_chars(self::ABC, -1)); + assertType('string|false', count_chars(self::ABC, 5)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php new file mode 100644 index 0000000000..88a165e931 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php @@ -0,0 +1,21 @@ += 8.0 + +namespace CountChars; + +use function PHPStan\Testing\assertType; + +class Y { + const ABC = 'abcdef'; + + function doFoo(): void { + assertType('array', count_chars(self::ABC, 0)); + assertType('array', count_chars(self::ABC, 1)); + assertType('array', count_chars(self::ABC, 2)); + + assertType('string', count_chars(self::ABC, 3)); + assertType('string', count_chars(self::ABC, 4)); + + assertType('string', count_chars(self::ABC, -1)); + assertType('string', count_chars(self::ABC, 5)); + } +} From a7628e250fb78c19692d020cbfb098d8003d6891 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 00:35:30 +0900 Subject: [PATCH 069/220] Split ArrayFilterFunctionReturnTypeExtension to Helper --- conf/config.neon | 3 + phpstan-baseline.neon | 2 +- ...ArrayFilterFunctionReturnTypeExtension.php | 323 +---------------- .../ArrayFilterFunctionReturnTypeHelper.php | 340 ++++++++++++++++++ 4 files changed, 346 insertions(+), 322 deletions(-) create mode 100644 src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php diff --git a/conf/config.neon b/conf/config.neon index 7c88f87a31..f9d8ca018e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1207,6 +1207,9 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeHelper + - class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeExtension tags: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1521bbb1a0..498207dbe7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1305,7 +1305,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" count: 1 - path: src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php + path: src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php index 4672fd1a96..62dc52abf0 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php @@ -2,52 +2,16 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Closure; -use PhpParser\Node\Expr\Error; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Name; -use PhpParser\Node\Stmt\Return_; -use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BenevolentUnionType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; -use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function array_map; -use function count; -use function in_array; -use function is_string; -use function sprintf; -use function substr; final class ArrayFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private const USE_BOTH = 1; - private const USE_KEY = 2; - private const USE_ITEM = 3; - - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct(private ArrayFilterFunctionReturnTypeHelper $arrayFilterFunctionReturnTypeHelper) { } @@ -62,290 +26,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $callbackArg = $functionCall->getArgs()[1]->value ?? null; $flagArg = $functionCall->getArgs()[2]->value ?? null; - if ($arrayArg === null) { - return new ArrayType(new MixedType(), new MixedType()); - } - - $arrayArgType = $scope->getType($arrayArg); - $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); - $keyType = $arrayArgType->getIterableKeyType(); - $itemType = $arrayArgType->getIterableValueType(); - - if ($itemType instanceof NeverType || $keyType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - if ($arrayArgType instanceof MixedType) { - return new BenevolentUnionType([ - new ArrayType(new MixedType(), new MixedType()), - new NullType(), - ]); - } - - if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { - return TypeCombinator::union( - ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), - ); - } - - $mode = $this->determineMode($flagArg, $scope); - if ($mode === null) { - return new ArrayType($keyType, $itemType); - } - - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - if ($mode === self::USE_ITEM) { - $keyVar = null; - $itemVar = $callbackArg->params[0]->var; - } elseif ($mode === self::USE_KEY) { - $keyVar = $callbackArg->params[0]->var; - $itemVar = null; - } elseif ($mode === self::USE_BOTH) { - $keyVar = $callbackArg->params[1]->var ?? null; - $itemVar = $callbackArg->params[0]->var; - } - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $statement->expr); - } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - if ($mode === self::USE_ITEM) { - $keyVar = null; - $itemVar = $callbackArg->params[0]->var; - } elseif ($mode === self::USE_KEY) { - $keyVar = $callbackArg->params[0]->var; - $itemVar = null; - } elseif ($mode === self::USE_BOTH) { - $keyVar = $callbackArg->params[1]->var ?? null; - $itemVar = $callbackArg->params[0]->var; - } - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $callbackArg->expr); - } elseif ( - ($callbackArg instanceof FuncCall || $callbackArg instanceof MethodCall || $callbackArg instanceof StaticCall) - && $callbackArg->isFirstClassCallable() - ) { - [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); - $expr = clone $callbackArg; - $expr->args = $args; - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); - } else { - $constantStrings = $scope->getType($callbackArg)->getConstantStrings(); - if (count($constantStrings) > 0) { - $results = []; - [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); - - foreach ($constantStrings as $constantString) { - $funcName = self::createFunctionName($constantString->getValue()); - if ($funcName === null) { - $results[] = new ErrorType(); - continue; - } - - $expr = new FuncCall($funcName, $args); - $results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); - } - return TypeCombinator::union(...$results); - } - } - - return new ArrayType($keyType, $itemType); - } - - public function removeFalsey(Type $type): Type - { - $falseyTypes = StaticTypeFactory::falsey(); - - if (count($type->getConstantArrays()) > 0) { - $result = []; - foreach ($type->getConstantArrays() as $constantArray) { - $keys = $constantArray->getKeyTypes(); - $values = $constantArray->getValueTypes(); - - $builder = ConstantArrayTypeBuilder::createEmpty(); - - foreach ($values as $offset => $value) { - $isFalsey = $falseyTypes->isSuperTypeOf($value); - - if ($isFalsey->maybe()) { - $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); - } elseif ($isFalsey->no()) { - $builder->setOffsetValueType($keys[$offset], $value, $constantArray->isOptionalKey($offset)); - } - } - - $result[] = $builder->getArray(); - } - - return TypeCombinator::union(...$result); - } - - $keyType = $type->getIterableKeyType(); - $valueType = $type->getIterableValueType(); - - $valueType = TypeCombinator::remove($valueType, $falseyTypes); - - if ($valueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - return new ArrayType($keyType, $valueType); - } - - private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $constantArrays = $arrayType->getConstantArrays(); - if (count($constantArrays) > 0) { - $results = []; - foreach ($constantArrays as $constantArray) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - $optionalKeys = $constantArray->getOptionalKeys(); - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $itemType = $constantArray->getValueTypes()[$i]; - [$newKeyType, $newItemType, $optional] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr); - $optional = $optional || in_array($i, $optionalKeys, true); - if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) { - continue; - } - if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) { - $builder->setOffsetValueType($keyType, $itemType, $optional); - continue; - } - - $builder->setOffsetValueType($newKeyType, $newItemType, true); - } - - $results[] = $builder->getArray(); - } - - return TypeCombinator::union(...$results); - } - - [$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr); - - if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - return new ArrayType($newKeyType, $newItemType); - } - - /** - * @return array{Type, Type, bool} - */ - private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array - { - $itemVarName = null; - if ($itemVar !== null) { - if (!$itemVar instanceof Variable || !is_string($itemVar->name)) { - throw new ShouldNotHappenException(); - } - $itemVarName = $itemVar->name; - $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType()); - } - - $keyVarName = null; - if ($keyVar !== null) { - if (!$keyVar instanceof Variable || !is_string($keyVar->name)) { - throw new ShouldNotHappenException(); - } - $keyVarName = $keyVar->name; - $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType()); - } - - $booleanResult = $scope->getType($expr)->toBoolean(); - if ($booleanResult->isFalse()->yes()) { - return [new NeverType(), new NeverType(), false]; - } - - $scope = $scope->filterByTruthyValue($expr); - - return [ - $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, - $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, - !$booleanResult instanceof ConstantBooleanType, - ]; - } - - private static function createFunctionName(string $funcName): ?Name - { - if ($funcName === '') { - return null; - } - - if ($funcName[0] === '\\') { - $funcName = substr($funcName, 1); - - if ($funcName === '') { - return null; - } - - return new Name\FullyQualified($funcName); - } - - return new Name($funcName); - } - - /** - * @param self::USE_* $mode - * @return array{list, ?Variable, ?Variable} - */ - private function createDummyArgs(int $mode): array - { - if ($mode === self::USE_ITEM) { - $itemVar = new Variable('item'); - $keyVar = null; - $args = [new Arg($itemVar)]; - } elseif ($mode === self::USE_KEY) { - $itemVar = null; - $keyVar = new Variable('key'); - $args = [new Arg($keyVar)]; - } elseif ($mode === self::USE_BOTH) { - $itemVar = new Variable('item'); - $keyVar = new Variable('key'); - $args = [new Arg($itemVar), new Arg($keyVar)]; - } - return [$args, $itemVar, $keyVar]; - } - - /** - * @param non-empty-string $constantName - */ - private function getConstant(string $constantName): int - { - $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); - } - - return $valueType->getValue(); - } - - /** - * @return self::USE_*|null - */ - private function determineMode(?Expr $flagArg, Scope $scope): ?int - { - if ($flagArg === null) { - return self::USE_ITEM; - } - - $flagValues = $scope->getType($flagArg)->getConstantScalarValues(); - if (count($flagValues) !== 1) { - return null; - } - - if ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_KEY')) { - return self::USE_KEY; - } elseif ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_BOTH')) { - return self::USE_BOTH; - } - - return null; + return $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, $flagArg); } } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..5291652129 --- /dev/null +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -0,0 +1,340 @@ +getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); + $keyType = $arrayArgType->getIterableKeyType(); + $itemType = $arrayArgType->getIterableValueType(); + + if ($itemType instanceof NeverType || $keyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + if ($arrayArgType instanceof MixedType) { + return new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new NullType(), + ]); + } + + if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { + return TypeCombinator::union( + ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), + ); + } + + $mode = $this->determineMode($flagArg, $scope); + if ($mode === null) { + return new ArrayType($keyType, $itemType); + } + + if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { + $statement = $callbackArg->stmts[0]; + if ($statement instanceof Return_ && $statement->expr !== null) { + if ($mode === self::USE_ITEM) { + $keyVar = null; + $itemVar = $callbackArg->params[0]->var; + } elseif ($mode === self::USE_KEY) { + $keyVar = $callbackArg->params[0]->var; + $itemVar = null; + } elseif ($mode === self::USE_BOTH) { + $keyVar = $callbackArg->params[1]->var ?? null; + $itemVar = $callbackArg->params[0]->var; + } + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $statement->expr); + } + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { + if ($mode === self::USE_ITEM) { + $keyVar = null; + $itemVar = $callbackArg->params[0]->var; + } elseif ($mode === self::USE_KEY) { + $keyVar = $callbackArg->params[0]->var; + $itemVar = null; + } elseif ($mode === self::USE_BOTH) { + $keyVar = $callbackArg->params[1]->var ?? null; + $itemVar = $callbackArg->params[0]->var; + } + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $callbackArg->expr); + } elseif ( + ($callbackArg instanceof FuncCall || $callbackArg instanceof MethodCall || $callbackArg instanceof StaticCall) + && $callbackArg->isFirstClassCallable() + ) { + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); + $expr = clone $callbackArg; + $expr->args = $args; + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); + } else { + $constantStrings = $scope->getType($callbackArg)->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); + + foreach ($constantStrings as $constantString) { + $funcName = self::createFunctionName($constantString->getValue()); + if ($funcName === null) { + $results[] = new ErrorType(); + continue; + } + + $expr = new FuncCall($funcName, $args); + $results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); + } + return TypeCombinator::union(...$results); + } + } + + return new ArrayType($keyType, $itemType); + } + + private function removeFalsey(Type $type): Type + { + $falseyTypes = StaticTypeFactory::falsey(); + + if (count($type->getConstantArrays()) > 0) { + $result = []; + foreach ($type->getConstantArrays() as $constantArray) { + $keys = $constantArray->getKeyTypes(); + $values = $constantArray->getValueTypes(); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($values as $offset => $value) { + $isFalsey = $falseyTypes->isSuperTypeOf($value); + + if ($isFalsey->maybe()) { + $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); + } elseif ($isFalsey->no()) { + $builder->setOffsetValueType($keys[$offset], $value, $constantArray->isOptionalKey($offset)); + } + } + + $result[] = $builder->getArray(); + } + + return TypeCombinator::union(...$result); + } + + $keyType = $type->getIterableKeyType(); + $valueType = $type->getIterableValueType(); + + $valueType = TypeCombinator::remove($valueType, $falseyTypes); + + if ($valueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new ArrayType($keyType, $valueType); + } + + private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type + { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $results = []; + foreach ($constantArrays as $constantArray) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $optionalKeys = $constantArray->getOptionalKeys(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $itemType = $constantArray->getValueTypes()[$i]; + [$newKeyType, $newItemType, $optional] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr); + $optional = $optional || in_array($i, $optionalKeys, true); + if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) { + continue; + } + if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) { + $builder->setOffsetValueType($keyType, $itemType, $optional); + continue; + } + + $builder->setOffsetValueType($newKeyType, $newItemType, true); + } + + $results[] = $builder->getArray(); + } + + return TypeCombinator::union(...$results); + } + + [$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr); + + if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + return new ArrayType($newKeyType, $newItemType); + } + + /** + * @return array{Type, Type, bool} + */ + private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array + { + $itemVarName = null; + if ($itemVar !== null) { + if (!$itemVar instanceof Variable || !is_string($itemVar->name)) { + throw new ShouldNotHappenException(); + } + $itemVarName = $itemVar->name; + $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType()); + } + + $keyVarName = null; + if ($keyVar !== null) { + if (!$keyVar instanceof Variable || !is_string($keyVar->name)) { + throw new ShouldNotHappenException(); + } + $keyVarName = $keyVar->name; + $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType()); + } + + $booleanResult = $scope->getType($expr)->toBoolean(); + if ($booleanResult->isFalse()->yes()) { + return [new NeverType(), new NeverType(), false]; + } + + $scope = $scope->filterByTruthyValue($expr); + + return [ + $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, + $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, + !$booleanResult instanceof ConstantBooleanType, + ]; + } + + private static function createFunctionName(string $funcName): ?Name + { + if ($funcName === '') { + return null; + } + + if ($funcName[0] === '\\') { + $funcName = substr($funcName, 1); + + if ($funcName === '') { + return null; + } + + return new Name\FullyQualified($funcName); + } + + return new Name($funcName); + } + + /** + * @param self::USE_* $mode + * @return array{list, ?Variable, ?Variable} + */ + private function createDummyArgs(int $mode): array + { + if ($mode === self::USE_ITEM) { + $itemVar = new Variable('item'); + $keyVar = null; + $args = [new Arg($itemVar)]; + } elseif ($mode === self::USE_KEY) { + $itemVar = null; + $keyVar = new Variable('key'); + $args = [new Arg($keyVar)]; + } elseif ($mode === self::USE_BOTH) { + $itemVar = new Variable('item'); + $keyVar = new Variable('key'); + $args = [new Arg($itemVar), new Arg($keyVar)]; + } + return [$args, $itemVar, $keyVar]; + } + + /** + * @param non-empty-string $constantName + */ + private function getConstant(string $constantName): int + { + $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + } + + return $valueType->getValue(); + } + + /** + * @return self::USE_*|null + */ + private function determineMode(?Expr $flagArg, Scope $scope): ?int + { + if ($flagArg === null) { + return self::USE_ITEM; + } + + $flagValues = $scope->getType($flagArg)->getConstantScalarValues(); + if (count($flagValues) !== 1) { + return null; + } + + if ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_KEY')) { + return self::USE_KEY; + } elseif ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_BOTH')) { + return self::USE_BOTH; + } + + return null; + } + +} From 88b12f88ea625d6e70d2a5469c97d339eecc86a4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 5 Nov 2024 18:51:34 +0100 Subject: [PATCH 070/220] Introduce `UnionType::filterTypes` --- src/Analyser/MutatingScope.php | 67 +++++----------------------------- src/Type/UnionType.php | 17 +++++++++ 2 files changed, 26 insertions(+), 58 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 90b3fdd465..c24defb1f8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5594,18 +5594,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type { if ($typeWithMethod instanceof UnionType) { - $newTypes = []; - foreach ($typeWithMethod->getTypes() as $innerType) { - if (!$innerType->hasMethod($methodName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithMethod = TypeCombinator::union(...$newTypes); + $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes()); } if (!$typeWithMethod->hasMethod($methodName)->yes()) { @@ -5709,18 +5698,7 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection { if ($typeWithProperty instanceof UnionType) { - $newTypes = []; - foreach ($typeWithProperty->getTypes() as $innerType) { - if (!$innerType->hasProperty($propertyName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithProperty = TypeCombinator::union(...$newTypes); + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes()); } if (!$typeWithProperty->hasProperty($propertyName)->yes()) { return null; @@ -5749,18 +5727,7 @@ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Ex public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection { if ($typeWithConstant instanceof UnionType) { - $newTypes = []; - foreach ($typeWithConstant->getTypes() as $innerType) { - if (!$innerType->hasConstant($constantName)->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return null; - } - $typeWithConstant = TypeCombinator::union(...$newTypes); + $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes()); } if (!$typeWithConstant->hasConstant($constantName)->yes()) { return null; @@ -5804,18 +5771,10 @@ private function getNativeConstantTypes(): array public function getIterableKeyType(Type $iteratee): Type { if ($iteratee instanceof UnionType) { - $newTypes = []; - foreach ($iteratee->getTypes() as $innerType) { - if (!$innerType->isIterable()->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return $iteratee->getIterableKeyType(); + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } - $iteratee = TypeCombinator::union(...$newTypes); } return $iteratee->getIterableKeyType(); @@ -5824,18 +5783,10 @@ public function getIterableKeyType(Type $iteratee): Type public function getIterableValueType(Type $iteratee): Type { if ($iteratee instanceof UnionType) { - $newTypes = []; - foreach ($iteratee->getTypes() as $innerType) { - if (!$innerType->isIterable()->yes()) { - continue; - } - - $newTypes[] = $innerType; - } - if (count($newTypes) === 0) { - return $iteratee->getIterableValueType(); + $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes()); + if (!$filtered instanceof NeverType) { + $iteratee = $filtered; } - $iteratee = TypeCombinator::union(...$newTypes); } return $iteratee->getIterableValueType(); diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 6b828625b6..b7e794378a 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -86,6 +86,23 @@ public function getTypes(): array return $this->types; } + /** + * @param callable(Type $type): bool $filterCb + */ + public function filterTypes(callable $filterCb): Type + { + $newTypes = []; + foreach ($this->getTypes() as $innerType) { + if (!$filterCb($innerType)) { + continue; + } + + $newTypes[] = $innerType; + } + + return TypeCombinator::union(...$newTypes); + } + public function isNormalized(): bool { return $this->normalized; From 3f8c27d2d5fbe6b65fa56f997191db46cfc9aeb7 Mon Sep 17 00:00:00 2001 From: JiaJia Ji Date: Tue, 5 Nov 2024 18:52:52 +0100 Subject: [PATCH 071/220] Imagick::writeImage(s)File supporting `format` parameter --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index c9eba8373a..b678f48e5b 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -5016,9 +5016,9 @@ 'Imagick::waveImage' => ['bool', 'amplitude'=>'float', 'length'=>'float'], 'Imagick::whiteThresholdImage' => ['bool', 'threshold'=>'mixed'], 'Imagick::writeImage' => ['bool', 'filename='=>'string'], -'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImageFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'Imagick::writeImages' => ['bool', 'filename'=>'string', 'adjoin'=>'bool'], -'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource'], +'Imagick::writeImagesFile' => ['bool', 'filehandle'=>'resource', 'format='=>'?string'], 'ImagickDraw::__construct' => ['void'], 'ImagickDraw::affine' => ['bool', 'affine'=>'array'], 'ImagickDraw::annotation' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], From 9798bd44f083ed57d79b609b827c0f9cf7b00d43 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 05:21:06 +0900 Subject: [PATCH 072/220] Add ArrayFindFunctionReturnTypeExtension --- conf/config.neon | 15 ++++ src/Parser/ArrayFindArgVisitor.php | 28 +++++++ src/Reflection/ParametersAcceptorSelector.php | 32 ++++++++ .../ArrayFindFunctionReturnTypeExtension.php | 46 +++++++++++ ...rrayFindKeyFunctionReturnTypeExtension.php | 36 +++++++++ .../PHPStan/Analyser/nsrt/array-find-key.php | 62 +++++++++++++++ tests/PHPStan/Analyser/nsrt/array-find.php | 79 +++++++++++++++++++ .../CallToFunctionParametersRuleTest.php | 52 ++++++++++++ .../Rules/Functions/data/array_find.php | 53 +++++++++++++ .../Rules/Functions/data/array_find_key.php | 53 +++++++++++++ 10 files changed, 456 insertions(+) create mode 100644 src/Parser/ArrayFindArgVisitor.php create mode 100644 src/Type/Php/ArrayFindFunctionReturnTypeExtension.php create mode 100644 src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-find-key.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-find.php create mode 100644 tests/PHPStan/Rules/Functions/data/array_find.php create mode 100644 tests/PHPStan/Rules/Functions/data/array_find_key.php diff --git a/conf/config.neon b/conf/config.neon index f9d8ca018e..11e845136d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -325,6 +325,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ArrayFindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\ArrayMapArgVisitor tags: @@ -1220,6 +1225,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayFindFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFindKeyFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension tags: diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php new file mode 100644 index 0000000000..8b25b36491 --- /dev/null +++ b/src/Parser/ArrayFindArgVisitor.php @@ -0,0 +1,28 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if (in_array($functionName, ['array_find', 'array_find_key'], true)) { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 619ee2aa81..07a7c28080 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Parser\ArrayFilterArgVisitor; +use PHPStan\Parser\ArrayFindArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\ArrayWalkArgVisitor; use PHPStan\Parser\ClosureBindArgVisitor; @@ -257,6 +258,37 @@ public static function selectFromArgs( ]; } + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $argType = $scope->getType($args[0]->value); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new CallableType( + [ + new DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + if (isset($args[0])) { $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); if ( diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..220d8fa0ef --- /dev/null +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -0,0 +1,46 @@ +getName() === 'array_find'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + $arrayArg = $functionCall->getArgs()[0]->value ?? null; + $callbackArg = $functionCall->getArgs()[1]->value ?? null; + + $resultTypes = $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, null); + $resultType = TypeCombinator::union(...array_map(static fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); + + return $resultTypes->isIterableAtLeastOnce()->yes() ? $resultType : TypeCombinator::addNull($resultType); + } + +} diff --git a/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..97c514f427 --- /dev/null +++ b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'array_find_key'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + return TypeCombinator::union($arrayType->getIterableKeyType(), new NullType()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find-key.php b/tests/PHPStan/Analyser/nsrt/array-find-key.php new file mode 100644 index 0000000000..5caf828f53 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find-key.php @@ -0,0 +1,62 @@ + $array + * @param callable(mixed, array-key=): mixed $callback + * @return ?array-key + */ + function array_find_key(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean + return $key; + } + } + + return null; + } + } + +} + +namespace ArrayFindKey +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + * @phpstan-ignore missingType.callable + */ + function testMixed(array $array, callable $callback): void + { + assertType('int|string|null', array_find_key($array, $callback)); + assertType('int|string|null', array_find_key($array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable + */ + function testConstant(array $array, callable $callback): void + { + assertType("0|1|2|null", array_find_key($array, $callback)); + assertType("0|1|2|null", array_find_key($array, 'is_int')); + } + + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find_key($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("'bar'|'buz'|'foo'|null", $result); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php new file mode 100644 index 0000000000..f3b5b0b822 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -0,0 +1,79 @@ + $array + * @param callable(mixed, array-key=): mixed $callback + * @return mixed + */ + function array_find(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean + return $value; + } + } + + return null; + } + } + +} + +namespace ArrayFind +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable + */ + function testMixed(array $array, array $non_empty_array, callable $callback): void + { + assertType('mixed', array_find($array, $callback)); + assertType('int|null', array_find($array, 'is_int')); + assertType('mixed', array_find($non_empty_array, $callback)); + assertType('int|null', array_find($non_empty_array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable + */ + function testConstant(array $array, callable $callback): void + { + assertType("1|'foo'|DateTime|null", array_find($array, $callback)); + assertType('1', array_find($array, 'is_int')); + } + + /** + * @param array $array + * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable + */ + function testInt(array $array, array $non_empty_array, callable $callback): void + { + assertType('int|null', array_find($array, $callback)); + assertType('int|null', array_find($array, 'is_int')); + assertType('int|null', array_find($non_empty_array, $callback)); + // should be 'int' + assertType('int|null', array_find($non_empty_array, 'is_int')); + } + + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("1|''|null", $result); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 4b00fb720e..cec109d5f8 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -886,6 +886,58 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } + public function testArrayFindCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find.php'], [ + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 49, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 52, + ], + ]); + } + + public function testArrayFindKeyCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find_key.php'], [ + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 49, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 52, + ], + ]); + } + public function testBug5356(): void { $this->analyse([__DIR__ . '/data/bug-5356.php'], [ diff --git a/tests/PHPStan/Rules/Functions/data/array_find.php b/tests/PHPStan/Rules/Functions/data/array_find.php new file mode 100644 index 0000000000..a749bd8294 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find.php @@ -0,0 +1,53 @@ += 8.4 + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find($array, fn ($value, $key) => $key === 0); + + // ok + array_find($array, fn (string $value, int $key) => $key === 0); + + // bad parameters + array_find($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php new file mode 100644 index 0000000000..393468d42c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -0,0 +1,53 @@ += 8.4 + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find_key($array, fn ($value, $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value, int $key) => $key === 0); + + // bad parameters + array_find_key($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find_key($array, fn (string $value, int $key): array => []); +} From 13798c2b9941a97dea9842756663de953c644b93 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Fri, 18 Oct 2024 22:04:10 +0200 Subject: [PATCH 073/220] Only use last for condition to filter scope --- src/Analyser/NodeScopeResolver.php | 21 ++++++++++++------- .../PHPStan/Analyser/nsrt/for-loop-i-type.php | 8 +++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fc7b1e9df7..4a8264b674 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1315,13 +1315,18 @@ private function processStmtNode( $bodyScope = $initScope; $isIterableAtLeastOnce = TrinaryLogic::createYes(); + $lastCondExpr = $stmt->cond[count($stmt->cond) - 1] ?? null; foreach ($stmt->cond as $condExpr) { $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); $initScope = $condResult->getScope(); $condResultScope = $condResult->getScope(); - $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); - $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); + + if ($condExpr === $lastCondExpr) { + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); + } + $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); @@ -1333,8 +1338,8 @@ private function processStmtNode( do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { + if ($lastCondExpr !== null) { + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep())->getTruthyScope(); } $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { @@ -1364,8 +1369,8 @@ private function processStmtNode( } $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + if ($lastCondExpr !== null) { + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); } $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); @@ -1379,8 +1384,8 @@ private function processStmtNode( $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); - foreach ($stmt->cond as $condExpr) { - $finalScope = $finalScope->filterByFalseyValue($condExpr); + if ($lastCondExpr !== null) { + $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { diff --git a/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php index 011d15d6c7..1317b3695c 100644 --- a/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php @@ -94,4 +94,12 @@ public static function groupCapacities(array $startTimes): array return $capacities; } + + public function lastConditionResult(): void + { + for ($i = 0, $j = 5; $i < 10, $j > 0; $i++, $j--) { + assertType('int<0, max>', $i); // int<0,4> would be more precise, see https://github.com/phpstan/phpstan/issues/11872 + assertType('int<1, 5>', $j); + } + } } From d9b383fb9f4b4efd95e139df43cefe33ae1d3733 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 18:41:50 +0900 Subject: [PATCH 074/220] Make ArrayFindArgVisitor supports array_any() and array_all() --- src/Parser/ArrayFindArgVisitor.php | 2 +- .../CallToFunctionParametersRuleTest.php | 68 +++++++++++++++++-- .../Rules/Functions/data/array_all.php | 56 +++++++++++++++ .../Rules/Functions/data/array_any.php | 56 +++++++++++++++ .../Rules/Functions/data/array_find.php | 3 + .../Rules/Functions/data/array_find_key.php | 3 + 6 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/array_all.php create mode 100644 tests/PHPStan/Rules/Functions/data/array_any.php diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php index 8b25b36491..0e798eb5c0 100644 --- a/src/Parser/ArrayFindArgVisitor.php +++ b/src/Parser/ArrayFindArgVisitor.php @@ -15,7 +15,7 @@ public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { $functionName = $node->name->toLowerString(); - if (in_array($functionName, ['array_find', 'array_find_key'], true)) { + if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) { $args = $node->getRawArgs(); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index cec109d5f8..123fa4259f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -886,6 +886,66 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } + public function testArrayAllCallback(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test skipped on lower version than 8.4 (needs array_all function)'); + } + + $this->analyse([__DIR__ . '/data/array_all.php'], [ + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_all expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + + public function testArrayAnyCallback(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test skipped on lower version than 8.4 (needs array_any function)'); + } + + $this->analyse([__DIR__ . '/data/array_any.php'], [ + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 52, + ], + [ + 'Parameter #2 $callback of function array_any expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 55, + ], + ]); + } + public function testArrayFindCallback(): void { $this->analyse([__DIR__ . '/data/array_find.php'], [ @@ -903,11 +963,11 @@ public function testArrayFindCallback(): void ], [ 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, array): false given.', - 49, + 52, ], [ 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', - 52, + 55, ], ]); } @@ -929,11 +989,11 @@ public function testArrayFindKeyCallback(): void ], [ 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, array): false given.', - 49, + 52, ], [ 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', - 52, + 55, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/array_all.php b/tests/PHPStan/Rules/Functions/data/array_all.php new file mode 100644 index 0000000000..e4a3348eb3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_all.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_all( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_all( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_all($array, fn ($value, $key) => $key === 0); + + // ok + array_all($array, fn (string $value, int $key) => $key === 0); + + // ok + array_all($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_all($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_all($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_any.php b/tests/PHPStan/Rules/Functions/data/array_any.php new file mode 100644 index 0000000000..1c267ffc62 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_any.php @@ -0,0 +1,56 @@ += 8.4 + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_any( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_any( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_any($array, fn ($value, $key) => $key === 0); + + // ok + array_any($array, fn (string $value, int $key) => $key === 0); + + // ok + array_any($array, fn (string $value) => $value === 'foo'); + + // bad parameters + array_any($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_any($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find.php b/tests/PHPStan/Rules/Functions/data/array_find.php index a749bd8294..3b7d7f9e13 100644 --- a/tests/PHPStan/Rules/Functions/data/array_find.php +++ b/tests/PHPStan/Rules/Functions/data/array_find.php @@ -45,6 +45,9 @@ function(int $value, string $key) { // ok array_find($array, fn (string $value, int $key) => $key === 0); + // ok + array_find($array, fn (string $value) => $key === 0); + // bad parameters array_find($array, fn (string $item, array $key) => $key === 0); diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php index 393468d42c..ab2b7df3fb 100644 --- a/tests/PHPStan/Rules/Functions/data/array_find_key.php +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -45,6 +45,9 @@ function(int $value, string $key) { // ok array_find_key($array, fn (string $value, int $key) => $key === 0); + // ok + array_find_key($array, fn (string $value) => $value === 'foo'); + // bad parameters array_find_key($array, fn (string $item, array $key) => $key === 0); From 71d01d661a5602d19f0a313a95ff8d66fd00798b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 6 Nov 2024 11:41:43 +0100 Subject: [PATCH 075/220] xdebug_get_function_stack: fix signature --- resources/functionMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 275eea976f..cb8b4c5870 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -13272,7 +13272,7 @@ 'xdebug_get_declared_vars' => ['array'], 'xdebug_get_formatted_function_stack' => [''], 'xdebug_get_function_count' => ['int'], -'xdebug_get_function_stack' => ['array', 'message='=>'string', 'options='=>'int'], +'xdebug_get_function_stack' => ['array', 'options='=>'array{local_vars?: bool, params_as_values?: bool, from_exception?: Throwable}'], 'xdebug_get_headers' => ['array'], 'xdebug_get_monitored_functions' => ['array'], 'xdebug_get_profiler_filename' => ['string'], From d999117006c777fa14ad1f07d1eded3ff00af7ea Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 6 Nov 2024 14:45:03 +0100 Subject: [PATCH 076/220] Preserve correct UnionType subclass in `filterTypes()` --- src/Type/BenevolentUnionType.php | 10 +++ .../Generic/TemplateBenevolentUnionType.php | 17 ++++ src/Type/Generic/TemplateUnionType.php | 17 ++++ src/Type/UnionType.php | 6 ++ tests/PHPStan/Analyser/nsrt/bug-6609-83.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-6609.php | 2 +- .../Rules/Methods/ReturnTypeRuleTest.php | 5 ++ .../PHPStan/Rules/Methods/data/bug-11663.php | 79 +++++++++++++++++++ 8 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-11663.php diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index bb4a47761b..d6b43fbd85 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -19,6 +19,16 @@ public function __construct(array $types, bool $normalized = false) parent::__construct($types, $normalized); } + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof self && $result instanceof UnionType) { + return TypeUtils::toBenevolentUnion($result); + } + + return $result; + } + public function describe(VerbosityLevel $level): string { return '(' . parent::describe($level) . ')'; diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index cc630fd0dd..aea8573131 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -47,4 +47,21 @@ public function withTypes(array $types): self ); } + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + return $result; + } + } diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index cc196a07f4..dc58af565a 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -34,4 +34,21 @@ public function __construct( $this->default = $default; } + public function filterTypes(callable $filterCb): Type + { + $result = parent::filterTypes($filterCb); + if (!$result instanceof TemplateType) { + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $result, + $this->getVariance(), + $this->getStrategy(), + $this->getDefault(), + ); + } + + return $result; + } + } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index b7e794378a..f0bef45903 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -92,14 +92,20 @@ public function getTypes(): array public function filterTypes(callable $filterCb): Type { $newTypes = []; + $changed = false; foreach ($this->getTypes() as $innerType) { if (!$filterCb($innerType)) { + $changed = true; continue; } $newTypes[] = $innerType; } + if (!$changed) { + return $this; + } + return TypeCombinator::union(...$newTypes); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php index 65d7f22f1d..4a5f5bb781 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php @@ -50,7 +50,7 @@ function modify2(\DateTimeInterface $date) { */ function modify3(\DateTimeInterface $date, string $s) { $date = $date->modify($s); - assertType('DateTime|DateTimeImmutable', $date); + assertType('T of DateTime|DateTimeImmutable (method Bug6609Php83\Foo::modify3(), argument)', $date); return $date; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609.php b/tests/PHPStan/Analyser/nsrt/bug-6609.php index 046f9c8403..571f97d988 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6609.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6609.php @@ -50,7 +50,7 @@ function modify2(\DateTimeInterface $date) { */ function modify3(\DateTimeInterface $date, string $s) { $date = $date->modify($s); - assertType('(DateTime|DateTimeImmutable|false)', $date); + assertType('((T of DateTime|DateTimeImmutable (method Bug6609\Foo::modify3(), argument))|false)', $date); return $date; } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 6d374a6f1c..fcbc0643c9 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1054,4 +1054,9 @@ public function testBug10715(): void $this->analyse([__DIR__ . '/data/bug-10715.php'], []); } + public function testBug11663(): void + { + $this->analyse([__DIR__ . '/data/bug-11663.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11663.php b/tests/PHPStan/Rules/Methods/data/bug-11663.php new file mode 100644 index 0000000000..ac2ed03a93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11663.php @@ -0,0 +1,79 @@ +where('test'); + } + + /** + * @param __benevolent $template + * @return __benevolent + */ + public function test2($template) + { + return $template->where('test'); + } + + + /** + * @template T of A|B + * @param T $ab + * @return T + */ + function foo(A|B $ab): A|B + { + return $ab->doFoo(); + } + + /** + * @template T of __benevolent + * @param T $ab + * @return T + */ + function foo2(A|B $ab): A|B + { + return $ab->doFoo(); + } +} From 11998ed4eb36ef036877e14e9ce3eb8a7cb8fc76 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 6 Nov 2024 12:01:44 +0100 Subject: [PATCH 077/220] Update changelog --- changelog-2.0.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog-2.0.md b/changelog-2.0.md index b4967463c6..1e2d866a1f 100644 --- a/changelog-2.0.md +++ b/changelog-2.0.md @@ -134,7 +134,12 @@ Improvements 🔧 * Collected PHP errors cannot be ignored (https://github.com/phpstan/phpstan-src/commit/1d3f4313955dc6fa5c6ce60fa58afe765964e5b0) * Added missing rules to StubValidator (https://github.com/phpstan/phpstan-src/commit/bf19914cac1682d0eab8bf65a874ba368522311c) * Report precise offsets in errors ([#3504](https://github.com/phpstan/phpstan-src/pull/3504)), thanks @ruudk! - +* IntersectionType - always describe list as list (https://github.com/phpstan/phpstan-src/commit/f680629bc92e4dd5d7acd3bc60c9539fb047452b) +* ArrayType::describe - explicit mixed should be stated explicitly (https://github.com/phpstan/phpstan-src/commit/6cf223840f89c972551f373ade9eea16d12e143b) +* Refactor IntersectionType::describe() (https://github.com/phpstan/phpstan-src/commit/67fbfaee6585c2d47485dc2a159ee76d3ed02b35) +* Remove inefficient caching from `PhpMethodReflection` and `PhpFunctionReflection::isVariadic()` ([#3534](https://github.com/phpstan/phpstan-src/pull/3534)), thanks @staabm! +* Clean file cache from unused items (https://github.com/phpstan/phpstan-src/commit/466ad51740d629c9137a77dac28a676b71ef7197) +* Journal for used generated containers (https://github.com/phpstan/phpstan-src/commit/57c65888e6372a4056afbbacc8207d411ea8559a) Bugfixes 🐛 ===================== @@ -165,6 +170,8 @@ Function signature fixes 🤖 * Update `Locale` signatures ([#2880](https://github.com/phpstan/phpstan-src/pull/2880)), thanks @devnix! * Improved the type of the `$mode` parameter for the `count()` ([#3190](https://github.com/phpstan/phpstan-src/pull/3190)), thanks @kuma3!* Check `filter_input*` type param type ([#2271](https://github.com/phpstan/phpstan-src/pull/2271)), thanks @herndlm! * Change `curl_setopt` function signature based on 2nd arg ([#1719](https://github.com/phpstan/phpstan-src/pull/1719)), thanks @staabm! +* Support returning an array or a string in `count_chars()` ([#3596](https://github.com/phpstan/phpstan-src/pull/3596)), thanks @u01jmg3! +* xdebug_get_function_stack: fix signature ([#3605](https://github.com/phpstan/phpstan-src/pull/3605)), thanks @janedbal! Internals 🔍 @@ -194,3 +201,8 @@ Internals 🔍 * More interfaces that are not supposed to be implemented in userland (https://github.com/phpstan/phpstan-src/commit/778af2ed74ba59bfb2a69fd5b45821ccdb1107c9, https://github.com/phpstan/phpstan-src/commit/cb6ab5544a016c52f931fc390bcdf9c627819d8f) * Refactored `FunctionCallParametersCheck::check()` parameters (https://github.com/phpstan/phpstan-src/commit/710e09c41698efb1d8d3ae31791944077dbb9cc1) * Spread list usages in Reflection, Scope, Type ([#3530](https://github.com/phpstan/phpstan-src/pull/3530)), thanks @janedbal! +* Remove $isFinal dead-code in PhpFunctionReflection ([#3545](https://github.com/phpstan/phpstan-src/pull/3545)), thanks @staabm! +* Get rid of unnecessary `instanceof self` in `ConstantArrayType` ([#3552](https://github.com/phpstan/phpstan-src/pull/3552)), thanks @herndlm! +* test: use `bashunit -a` exit_code to check for errors ([#3533](https://github.com/phpstan/phpstan-src/pull/3533)), thanks @Chemaclass! +* Remove dead code ([#3575](https://github.com/phpstan/phpstan-src/pull/3575)), thanks @staabm! +* Remove dead code in ConstantConditionRuleHelper ([#3597](https://github.com/phpstan/phpstan-src/pull/3597)), thanks @staabm! From 6924e46f4af313fc70403120ff7a08a62594bb55 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 6 Nov 2024 17:35:43 +0100 Subject: [PATCH 078/220] Utilize PHP version constraint from composer.json to narrow `PHP_*` constants --- .github/workflows/e2e-tests.yml | 45 +++++ conf/config.neon | 8 +- conf/parametersSchema.neon | 8 +- e2e/composer-max-version/.gitignore | 2 + e2e/composer-max-version/composer.json | 5 + e2e/composer-max-version/test.php | 10 ++ e2e/composer-min-max-version/.gitignore | 2 + e2e/composer-min-max-version/composer.json | 5 + e2e/composer-min-max-version/test.php | 10 ++ e2e/composer-min-open-end-version/.gitignore | 2 + .../composer.json | 5 + e2e/composer-min-open-end-version/test.php | 6 + e2e/composer-min-version-v5/.gitignore | 2 + e2e/composer-min-version-v5/composer.json | 5 + e2e/composer-min-version-v5/test.php | 6 + e2e/composer-min-version-v7/.gitignore | 2 + e2e/composer-min-version-v7/composer.json | 5 + e2e/composer-min-version-v7/test.php | 6 + e2e/composer-min-version/.gitignore | 2 + e2e/composer-min-version/composer.json | 5 + e2e/composer-min-version/test.php | 6 + e2e/composer-no-versions/.gitignore | 2 + e2e/composer-no-versions/composer.json | 2 + e2e/composer-no-versions/test.php | 6 + .../phpstan.neon | 5 + e2e/composer-version-config-patch/.gitignore | 2 + .../composer.json | 5 + e2e/composer-version-config-patch/test.php | 7 + e2e/composer-version-config/.gitignore | 2 + e2e/composer-version-config/composer.json | 5 + e2e/composer-version-config/phpstan.neon | 4 + e2e/composer-version-config/test.php | 11 ++ src/Analyser/ConstantResolver.php | 69 +++++++- src/Analyser/ConstantResolverFactory.php | 5 + src/DependencyInjection/ContainerFactory.php | 13 ++ .../InvalidPhpVersionException.php | 10 ++ .../ValidateIgnoredErrorsExtension.php | 2 +- src/Php/ComposerPhpVersionFactory.php | 158 ++++++++++++++++++ src/Php/PhpVersion.php | 21 ++- src/Php/PhpVersionFactory.php | 9 +- src/Php/PhpVersionFactoryFactory.php | 17 +- src/Testing/PHPStanTestCase.php | 2 +- ...pareFunctionDynamicReturnTypeExtension.php | 38 ++++- 43 files changed, 521 insertions(+), 21 deletions(-) create mode 100644 e2e/composer-max-version/.gitignore create mode 100644 e2e/composer-max-version/composer.json create mode 100644 e2e/composer-max-version/test.php create mode 100644 e2e/composer-min-max-version/.gitignore create mode 100644 e2e/composer-min-max-version/composer.json create mode 100644 e2e/composer-min-max-version/test.php create mode 100644 e2e/composer-min-open-end-version/.gitignore create mode 100644 e2e/composer-min-open-end-version/composer.json create mode 100644 e2e/composer-min-open-end-version/test.php create mode 100644 e2e/composer-min-version-v5/.gitignore create mode 100644 e2e/composer-min-version-v5/composer.json create mode 100644 e2e/composer-min-version-v5/test.php create mode 100644 e2e/composer-min-version-v7/.gitignore create mode 100644 e2e/composer-min-version-v7/composer.json create mode 100644 e2e/composer-min-version-v7/test.php create mode 100644 e2e/composer-min-version/.gitignore create mode 100644 e2e/composer-min-version/composer.json create mode 100644 e2e/composer-min-version/test.php create mode 100644 e2e/composer-no-versions/.gitignore create mode 100644 e2e/composer-no-versions/composer.json create mode 100644 e2e/composer-no-versions/test.php create mode 100644 e2e/composer-version-config-invalid/phpstan.neon create mode 100644 e2e/composer-version-config-patch/.gitignore create mode 100644 e2e/composer-version-config-patch/composer.json create mode 100644 e2e/composer-version-config-patch/test.php create mode 100644 e2e/composer-version-config/.gitignore create mode 100644 e2e/composer-version-config/composer.json create mode 100644 e2e/composer-version-config/phpstan.neon create mode 100644 e2e/composer-version-config/test.php create mode 100644 src/DependencyInjection/InvalidPhpVersionException.php create mode 100644 src/Php/ComposerPhpVersionFactory.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9c134d199c..7577e5c04f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -292,6 +292,48 @@ jobs: - script: | cd e2e/bug-11819 ../../bin/phpstan + - script: | + cd e2e/composer-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-max-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-open-end-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v5 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version-v7 + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-min-version + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-no-versions + composer install + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config-invalid + OUTPUT=$(../bashunit -a exit_code "1" ../../bin/phpstan) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid configuration' "$OUTPUT" + ../bashunit -a contains 'Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.' "$OUTPUT" + - script: | + cd e2e/composer-version-config-patch + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 + - script: | + cd e2e/composer-version-config + composer install + ../../bin/phpstan analyze test.php --level=0 steps: - name: "Checkout" @@ -308,5 +350,8 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.17.0" + - name: "Test" run: ${{ matrix.script }} diff --git a/conf/config.neon b/conf/config.neon index 7d1bf16616..df5c15ddec 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -339,7 +339,13 @@ services: - class: PHPStan\Php\PhpVersionFactoryFactory arguments: - versionId: %phpVersion% + phpVersion: %phpVersion% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + + - + class: PHPStan\Php\ComposerPhpVersionFactory + arguments: + phpVersion: %phpVersion% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index d6e4a34882..6c7f9c40e6 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -79,7 +79,13 @@ parametersSchema: minimumNumberOfJobsPerProcess: int(), buffer: int() ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80499))), nullable()) + phpVersion: schema(anyOf( + schema(int(), min(70100), max(80499)), + structure([ + min: schema(int(), min(70100), max(80499)), + max: schema(int(), min(70100), max(80499)) + ]) + ), nullable()) polluteScopeWithLoopInitialAssignments: bool() polluteScopeWithAlwaysIterableForeach: bool() polluteScopeWithBlock: bool() diff --git a/e2e/composer-max-version/.gitignore b/e2e/composer-max-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-max-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-max-version/composer.json b/e2e/composer-max-version/composer.json new file mode 100644 index 0000000000..4d4ca141ef --- /dev/null +++ b/e2e/composer-max-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "<=8.3" + } +} diff --git a/e2e/composer-max-version/test.php b/e2e/composer-max-version/test.php new file mode 100644 index 0000000000..038f559122 --- /dev/null +++ b/e2e/composer-max-version/test.php @@ -0,0 +1,10 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('int<5, 8>', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('-1|0|1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('bool', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('bool', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/composer-min-max-version/.gitignore b/e2e/composer-min-max-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-max-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-max-version/composer.json b/e2e/composer-min-max-version/composer.json new file mode 100644 index 0000000000..869fd2ce42 --- /dev/null +++ b/e2e/composer-min-max-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">=8.1, <=8.2.99" + } +} diff --git a/e2e/composer-min-max-version/test.php b/e2e/composer-min-max-version/test.php new file mode 100644 index 0000000000..28d770f3bb --- /dev/null +++ b/e2e/composer-min-max-version/test.php @@ -0,0 +1,10 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 2>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/e2e/composer-min-open-end-version/.gitignore b/e2e/composer-min-open-end-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-open-end-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-open-end-version/composer.json b/e2e/composer-min-open-end-version/composer.json new file mode 100644 index 0000000000..b6303c6b77 --- /dev/null +++ b/e2e/composer-min-open-end-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">= 8.1" + } +} diff --git a/e2e/composer-min-open-end-version/test.php b/e2e/composer-min-open-end-version/test.php new file mode 100644 index 0000000000..d4d06a34eb --- /dev/null +++ b/e2e/composer-min-open-end-version/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version-v5/.gitignore b/e2e/composer-min-version-v5/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version-v5/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version-v5/composer.json b/e2e/composer-min-version-v5/composer.json new file mode 100644 index 0000000000..b73464d219 --- /dev/null +++ b/e2e/composer-min-version-v5/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^5.6" + } +} diff --git a/e2e/composer-min-version-v5/test.php b/e2e/composer-min-version-v5/test.php new file mode 100644 index 0000000000..2f652079a6 --- /dev/null +++ b/e2e/composer-min-version-v5/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('5', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('6', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, 99>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version-v7/.gitignore b/e2e/composer-min-version-v7/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version-v7/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version-v7/composer.json b/e2e/composer-min-version-v7/composer.json new file mode 100644 index 0000000000..9f9b263871 --- /dev/null +++ b/e2e/composer-min-version-v7/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^7" + } +} diff --git a/e2e/composer-min-version-v7/test.php b/e2e/composer-min-version-v7/test.php new file mode 100644 index 0000000000..e8876bd78f --- /dev/null +++ b/e2e/composer-min-version-v7/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('7', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-min-version/.gitignore b/e2e/composer-min-version/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-min-version/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-min-version/composer.json b/e2e/composer-min-version/composer.json new file mode 100644 index 0000000000..9be64619f1 --- /dev/null +++ b/e2e/composer-min-version/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^8.1" + } +} diff --git a/e2e/composer-min-version/test.php b/e2e/composer-min-version/test.php new file mode 100644 index 0000000000..d4d06a34eb --- /dev/null +++ b/e2e/composer-min-version/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 4>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-no-versions/.gitignore b/e2e/composer-no-versions/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-no-versions/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-no-versions/composer.json b/e2e/composer-no-versions/composer.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/e2e/composer-no-versions/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/e2e/composer-no-versions/test.php b/e2e/composer-no-versions/test.php new file mode 100644 index 0000000000..28c8a3183b --- /dev/null +++ b/e2e/composer-no-versions/test.php @@ -0,0 +1,6 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('int<5, 8>', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-version-config-invalid/phpstan.neon b/e2e/composer-version-config-invalid/phpstan.neon new file mode 100644 index 0000000000..96977def5f --- /dev/null +++ b/e2e/composer-version-config-invalid/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + phpVersion: + min: 80303 + max: 80104 + diff --git a/e2e/composer-version-config-patch/.gitignore b/e2e/composer-version-config-patch/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-version-config-patch/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-version-config-patch/composer.json b/e2e/composer-version-config-patch/composer.json new file mode 100644 index 0000000000..d6103988c8 --- /dev/null +++ b/e2e/composer-version-config-patch/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": ">=8.0.2, <8.0.15" + } +} diff --git a/e2e/composer-version-config-patch/test.php b/e2e/composer-version-config-patch/test.php new file mode 100644 index 0000000000..3f201eadab --- /dev/null +++ b/e2e/composer-version-config-patch/test.php @@ -0,0 +1,7 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('0', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<2, 15>', PHP_RELEASE_VERSION); diff --git a/e2e/composer-version-config/.gitignore b/e2e/composer-version-config/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-version-config/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-version-config/composer.json b/e2e/composer-version-config/composer.json new file mode 100644 index 0000000000..2da0adaf1c --- /dev/null +++ b/e2e/composer-version-config/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^8.0" + } +} diff --git a/e2e/composer-version-config/phpstan.neon b/e2e/composer-version-config/phpstan.neon new file mode 100644 index 0000000000..003e5e1484 --- /dev/null +++ b/e2e/composer-version-config/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80103 + max: 80304 diff --git a/e2e/composer-version-config/test.php b/e2e/composer-version-config/test.php new file mode 100644 index 0000000000..a9afaa4b65 --- /dev/null +++ b/e2e/composer-version-config/test.php @@ -0,0 +1,11 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 3>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index b209533fac..8176fe8abc 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node\Name; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; @@ -20,6 +21,7 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function in_array; +use function max; use function sprintf; use const INF; use const NAN; @@ -34,7 +36,12 @@ final class ConstantResolver /** * @param string[] $dynamicConstantNames */ - public function __construct(private ReflectionProviderProvider $reflectionProviderProvider, private array $dynamicConstantNames) + public function __construct( + private ReflectionProviderProvider $reflectionProviderProvider, + private array $dynamicConstantNames, + private ?PhpVersion $composerMinPhpVersion, + private ?PhpVersion $composerMaxPhpVersion, + ) { } @@ -77,16 +84,60 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type ]); } if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { - return IntegerRangeType::fromInterval(5, null); + $minMajor = 5; + $maxMajor = null; + + if ($this->composerMinPhpVersion !== null) { + $minMajor = max($minMajor, $this->composerMinPhpVersion->getMajorVersionId()); + } + if ($this->composerMaxPhpVersion !== null) { + $maxMajor = $this->composerMaxPhpVersion->getMajorVersionId(); + } + + return $this->createInteger($minMajor, $maxMajor); } if ($resolvedConstantName === 'PHP_MINOR_VERSION') { - return IntegerRangeType::fromInterval(0, null); + $minMinor = 0; + $maxMinor = null; + + if ( + $this->composerMinPhpVersion !== null + && $this->composerMaxPhpVersion !== null + && $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId() + ) { + $minMinor = $this->composerMinPhpVersion->getMinorVersionId(); + $maxMinor = $this->composerMaxPhpVersion->getMinorVersionId(); + } + + return $this->createInteger($minMinor, $maxMinor); } if ($resolvedConstantName === 'PHP_RELEASE_VERSION') { - return IntegerRangeType::fromInterval(0, null); + $minRelease = 0; + $maxRelease = null; + + if ( + $this->composerMinPhpVersion !== null + && $this->composerMaxPhpVersion !== null + && $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId() + && $this->composerMaxPhpVersion->getMinorVersionId() === $this->composerMinPhpVersion->getMinorVersionId() + ) { + $minRelease = $this->composerMinPhpVersion->getPatchVersionId(); + $maxRelease = $this->composerMaxPhpVersion->getPatchVersionId(); + } + + return $this->createInteger($minRelease, $maxRelease); } if ($resolvedConstantName === 'PHP_VERSION_ID') { - return IntegerRangeType::fromInterval(50207, null); + $minVersion = 50207; + $maxVersion = null; + if ($this->composerMinPhpVersion !== null) { + $minVersion = max($minVersion, $this->composerMinPhpVersion->getVersionId()); + } + if ($this->composerMaxPhpVersion !== null) { + $maxVersion = $this->composerMaxPhpVersion->getVersionId(); + } + + return $this->createInteger($minVersion, $maxVersion); } if ($resolvedConstantName === 'PHP_ZTS') { return new UnionType([ @@ -325,6 +376,14 @@ public function resolveClassConstantType(string $className, string $constantName return $constantType; } + private function createInteger(?int $min, ?int $max): Type + { + if ($min !== null && $min === $max) { + return new ConstantIntegerType($min); + } + return IntegerRangeType::fromInterval($min, $max); + } + private function getReflectionProvider(): ReflectionProvider { return $this->reflectionProviderProvider->getReflectionProvider(); diff --git a/src/Analyser/ConstantResolverFactory.php b/src/Analyser/ConstantResolverFactory.php index 67a98408a0..f111da14ec 100644 --- a/src/Analyser/ConstantResolverFactory.php +++ b/src/Analyser/ConstantResolverFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\DependencyInjection\Container; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; final class ConstantResolverFactory @@ -17,9 +18,13 @@ public function __construct( public function create(): ConstantResolver { + $composerFactory = $this->container->getByType(ComposerPhpVersionFactory::class); + return new ConstantResolver( $this->reflectionProviderProvider, $this->container->getParameter('dynamicConstantNames'), + $composerFactory->getMinVersion(), + $composerFactory->getMaxVersion(), ); } diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 7ac72d4fc4..75c79d6678 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -32,6 +32,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use function array_diff_key; +use function array_key_exists; use function array_map; use function array_merge; use function array_unique; @@ -310,6 +311,18 @@ private function validateParameters(array $parameters, array $parametersSchema): $context->path = ['parameters']; }; $processor->process($schema, $parameters); + + if ( + !array_key_exists('phpVersion', $parameters) + || !is_array($parameters['phpVersion'])) { + return; + } + + $phpVersion = $parameters['phpVersion']; + + if ($phpVersion['max'] < $phpVersion['min']) { + throw new InvalidPhpVersionException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } } /** diff --git a/src/DependencyInjection/InvalidPhpVersionException.php b/src/DependencyInjection/InvalidPhpVersionException.php new file mode 100644 index 0000000000..f9b41690a3 --- /dev/null +++ b/src/DependencyInjection/InvalidPhpVersionException.php @@ -0,0 +1,10 @@ +initialized = true; + + $phpVersion = $this->phpVersion; + + if (is_int($phpVersion)) { + throw new ShouldNotHappenException(); + } + + if (is_array($phpVersion)) { + if ($phpVersion['max'] < $phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + $this->minVersion = new PhpVersion($phpVersion['min']); + $this->maxVersion = new PhpVersion($phpVersion['max']); + + return; + } + + // don't limit minVersion... PHPStan can analyze even PHP5 + $this->maxVersion = new PhpVersion(PhpVersionFactory::MAX_PHP_VERSION); + + // fallback to composer.json based php-version constraint + $composerPhpVersion = $this->getComposerRequireVersion(); + if ($composerPhpVersion === null) { + return; + } + + $parser = new VersionParser(); + $constraint = $parser->parseConstraints($composerPhpVersion); + + if (!$constraint->getLowerBound()->isZero()) { + $minVersion = $this->buildVersion($constraint->getLowerBound()->getVersion(), false); + + if ($minVersion !== null) { + $this->minVersion = new PhpVersion($minVersion->getVersionId()); + } + } + if ($constraint->getUpperBound()->isPositiveInfinity()) { + return; + } + + $this->maxVersion = $this->buildVersion($constraint->getUpperBound()->getVersion(), true); + } + + public function getMinVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if ($this->initialized === false) { + $this->initializeVersions(); + } + + return $this->minVersion; + } + + public function getMaxVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if ($this->initialized === false) { + $this->initializeVersions(); + } + + return $this->maxVersion; + } + + private function getComposerRequireVersion(): ?string + { + $composerPhpVersion = null; + if (count($this->composerAutoloaderProjectPaths) > 0) { + $composerJsonPath = end($this->composerAutoloaderProjectPaths) . '/composer.json'; + if (is_file($composerJsonPath)) { + try { + $composerJsonContents = FileReader::read($composerJsonPath); + $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); + $requiredVersion = $composer['require']['php'] ?? null; + if (is_string($requiredVersion)) { + $composerPhpVersion = $requiredVersion; + } + } catch (CouldNotReadFileException | JsonException) { + // pass + } + } + } + return $composerPhpVersion; + } + + private function buildVersion(string $version, bool $isMaxVersion): ?PhpVersion + { + $matches = Strings::match($version, '#^(\d+)\.(\d+)(?:\.(\d+))?#'); + if ($matches === null) { + return null; + } + + $major = $matches[1]; + $minor = $matches[2]; + $patch = $matches[3] ?? 0; + $versionId = (int) sprintf('%d%02d%02d', $major, $minor, $patch); + + if ($isMaxVersion && $version === '6.0.0.0-dev') { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP5_VERSION); + } elseif ($isMaxVersion && $version === '8.0.0.0-dev') { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP7_VERSION); + } else { + $versionId = min($versionId, PhpVersionFactory::MAX_PHP_VERSION); + } + + return new PhpVersion($versionId); + } + +} diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index fcd6871c39..83ca1245b5 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -41,11 +41,26 @@ public function getVersionId(): int return $this->versionId; } + public function getMajorVersionId(): int + { + return (int) floor($this->versionId / 10000); + } + + public function getMinorVersionId(): int + { + return (int) floor(($this->versionId % 10000) / 100); + } + + public function getPatchVersionId(): int + { + return (int) floor($this->versionId % 100); + } + public function getVersionString(): string { - $first = (int) floor($this->versionId / 10000); - $second = (int) floor(($this->versionId % 10000) / 100); - $third = (int) floor($this->versionId % 100); + $first = $this->getMajorVersionId(); + $second = $this->getMinorVersionId(); + $third = $this->getPatchVersionId(); return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index d926420e77..bd1bfcabf5 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -10,6 +10,11 @@ final class PhpVersionFactory { + public const MIN_PHP_VERSION = 70100; + public const MAX_PHP_VERSION = 80499; + public const MAX_PHP5_VERSION = 50699; + public const MAX_PHP7_VERSION = 70499; + public function __construct( private ?int $versionId, private ?string $composerPhpVersion, @@ -25,8 +30,8 @@ public function create(): PhpVersion } elseif ($this->composerPhpVersion !== null) { $parts = explode('.', $this->composerPhpVersion); $tmp = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); - $tmp = max($tmp, 70100); - $versionId = min($tmp, 80499); + $tmp = max($tmp, self::MIN_PHP_VERSION); + $versionId = min($tmp, self::MAX_PHP_VERSION); $source = PhpVersion::SOURCE_COMPOSER_PLATFORM_PHP; } else { $versionId = PHP_VERSION_ID; diff --git a/src/Php/PhpVersionFactoryFactory.php b/src/Php/PhpVersionFactoryFactory.php index c578ef06fc..0190ca0e82 100644 --- a/src/Php/PhpVersionFactoryFactory.php +++ b/src/Php/PhpVersionFactoryFactory.php @@ -8,17 +8,20 @@ use PHPStan\File\FileReader; use function count; use function end; +use function is_array; use function is_file; +use function is_int; use function is_string; final class PhpVersionFactoryFactory { /** + * @param int|array{min: int, max: int}|null $phpVersion * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - private ?int $versionId, + private int|array|null $phpVersion, private array $composerAutoloaderProjectPaths, ) { @@ -43,7 +46,17 @@ public function create(): PhpVersionFactory } } - return new PhpVersionFactory($this->versionId, $composerPhpVersion); + $versionId = null; + + if (is_int($this->phpVersion)) { + $versionId = $this->phpVersion; + } + + if (is_array($this->phpVersion)) { + $versionId = $this->phpVersion['min']; + } + + return new PhpVersionFactory($versionId, $composerPhpVersion); } } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 87d5bab299..aee824bfbc 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -137,7 +137,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider } $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); - $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames); + $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, null); $initializerExprTypeResolver = new InitializerExprTypeResolver( $constantResolver, diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index 9a5592bf40..d79ebf0706 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -2,12 +2,15 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -18,6 +21,10 @@ final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ComposerPhpVersionFactory $composerPhpVersionFactory) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'version_compare'; @@ -29,19 +36,20 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 2) { + $args = $functionCall->getArgs(); + if (count($args) < 2) { return null; } - $version1Strings = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); - $version2Strings = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); + $version1Strings = $this->getVersionStrings($args[0]->value, $scope); + $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ count($version1Strings), count($version2Strings), ]; - if (isset($functionCall->getArgs()[2])) { - $operatorStrings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); + if (isset($args[2])) { + $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); $returnType = new BooleanType(); } else { @@ -77,4 +85,24 @@ public function getTypeFromFunctionCall( return TypeCombinator::union(...$types); } + /** + * @return ConstantStringType[] + */ + private function getVersionStrings(Expr $expr, Scope $scope): array + { + if ( + $expr instanceof Expr\ConstFetch + && $expr->name->toString() === 'PHP_VERSION' + && $this->composerPhpVersionFactory->getMinVersion() !== null + && $this->composerPhpVersionFactory->getMaxVersion() !== null + ) { + return [ + new ConstantStringType($this->composerPhpVersionFactory->getMinVersion()->getVersionString()), + new ConstantStringType($this->composerPhpVersionFactory->getMaxVersion()->getVersionString()), + ]; + } + + return $scope->getType($expr)->getConstantStrings(); + } + } From 6dcda722c3ced237547e2beac127470cb1791fb7 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Wed, 6 Nov 2024 12:51:52 -0600 Subject: [PATCH 079/220] Do not transform `$this` in return type even in final classes --- .github/workflows/e2e-tests.yml | 4 ++ e2e/bug-11857/.gitignore | 1 + e2e/bug-11857/composer.json | 5 ++ e2e/bug-11857/composer.lock | 18 +++++ e2e/bug-11857/phpstan.neon | 10 +++ e2e/bug-11857/src/extension.php | 36 ++++++++++ e2e/bug-11857/src/test.php | 70 +++++++++++++++++++ src/Analyser/MutatingScope.php | 2 +- src/Analyser/NodeScopeResolver.php | 2 +- src/Type/StaticType.php | 2 +- .../Rules/Methods/ReturnTypeRuleTest.php | 5 ++ .../Rules/Methods/data/bug-11857-builder.php | 49 +++++++++++++ 12 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 e2e/bug-11857/.gitignore create mode 100644 e2e/bug-11857/composer.json create mode 100644 e2e/bug-11857/composer.lock create mode 100644 e2e/bug-11857/phpstan.neon create mode 100644 e2e/bug-11857/src/extension.php create mode 100644 e2e/bug-11857/src/test.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-11857-builder.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8914cca904..4b90be1ec3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -255,6 +255,10 @@ jobs: echo "$OUTPUT" ../bashunit -a contains 'Child process error (exit code 255): PHP Fatal error' "$OUTPUT" ../bashunit -a contains 'Result is incomplete because of severe errors.' "$OUTPUT" + - script: | + cd e2e/bug-11857 + composer install + ../../bin/phpstan steps: - name: "Checkout" diff --git a/e2e/bug-11857/.gitignore b/e2e/bug-11857/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/e2e/bug-11857/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/bug-11857/composer.json b/e2e/bug-11857/composer.json new file mode 100644 index 0000000000..a072011fe8 --- /dev/null +++ b/e2e/bug-11857/composer.json @@ -0,0 +1,5 @@ +{ + "autoload-dev": { + "classmap": ["src/"] + } +} diff --git a/e2e/bug-11857/composer.lock b/e2e/bug-11857/composer.lock new file mode 100644 index 0000000000..b383d88ac5 --- /dev/null +++ b/e2e/bug-11857/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/e2e/bug-11857/phpstan.neon b/e2e/bug-11857/phpstan.neon new file mode 100644 index 0000000000..306701cd6f --- /dev/null +++ b/e2e/bug-11857/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 8 + paths: + - src + +services: + - + class: Bug11857\RelationDynamicMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/e2e/bug-11857/src/extension.php b/e2e/bug-11857/src/extension.php new file mode 100644 index 0000000000..c24d0f3653 --- /dev/null +++ b/e2e/bug-11857/src/extension.php @@ -0,0 +1,36 @@ +getName() === 'belongsTo'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { + $returnType = $methodReflection->getVariants()[0]->getReturnType(); + $argType = $scope->getType($methodCall->getArgs()[0]->value); + $modelClass = $argType->getClassStringObjectType()->getObjectClassNames()[0]; + + return new GenericObjectType($returnType->getObjectClassNames()[0], [ + new ObjectType($modelClass), + $scope->getType($methodCall->var), + ]); + } +} + diff --git a/e2e/bug-11857/src/test.php b/e2e/bug-11857/src/test.php new file mode 100644 index 0000000000..5c237f25e8 --- /dev/null +++ b/e2e/bug-11857/src/test.php @@ -0,0 +1,70 @@ + */ + public function belongsTo(string $related): BelongsTo + { + return new BelongsTo(); + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + */ +class BelongsTo {} + +class User extends Model {} + +class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function userSelf(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +class ChildPost extends Post {} + +final class Comment extends Model +{ + // This model is final, so either of these + // two methods would work. It seems that + // PHPStan is automatically converting the + // `$this` to a `self` type in the user docblock, + // but it is not doing so likewise for the `$this` + // that is returned by the dynamic return extension. + + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return BelongsTo */ + public function user2(): BelongsTo + { + /** @phpstan-ignore return.type */ + return $this->belongsTo(User::class); + } +} + +function test(ChildPost $child): void +{ + assertType('Bug11857\BelongsTo', $child->user()); + // This demonstrates why `$this` is needed in non-final models + assertType('Bug11857\BelongsTo', $child->userSelf()); // should be: Bug11857\BelongsTo +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c24defb1f8..df85c5fa35 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3015,7 +3015,7 @@ private function transformStaticType(Type $type): Type if ($type instanceof StaticType) { $classReflection = $this->getClassReflection(); $changedType = $type->changeBaseClass($classReflection); - if ($classReflection->isFinal()) { + if ($classReflection->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 4a8264b674..dda335f566 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6159,7 +6159,7 @@ private function transformStaticType(ClassReflection $declaringClass, Type $type return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type { if ($type instanceof StaticType) { $changedType = $type->changeBaseClass($declaringClass); - if ($declaringClass->isFinal()) { + if ($declaringClass->isFinal() && !$type instanceof ThisType) { $changedType = $changedType->getStaticObjectType(); } return $traverse($changedType); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 9cf4da4dc8..5fd6f7e358 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -299,7 +299,7 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop $isFinal = $classReflection->isFinal(); } $type = $type->changeBaseClass($classReflection); - if (!$isFinal) { + if (!$isFinal || $type instanceof ThisType) { return $type; } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index fcbc0643c9..eaa0465a42 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1059,4 +1059,9 @@ public function testBug11663(): void $this->analyse([__DIR__ . '/data/bug-11663.php'], []); } + public function testBug11857(): void + { + $this->analyse([__DIR__ . '/data/bug-11857-builder.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11857-builder.php b/tests/PHPStan/Rules/Methods/data/bug-11857-builder.php new file mode 100644 index 0000000000..89209ffe37 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11857-builder.php @@ -0,0 +1,49 @@ += 8.0 + +namespace Bug11857Builder; + +class Foo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} + +final class FinalFoo +{ + + /** + * @param array $attributes + * @return $this + */ + public function filter(array $attributes): static + { + return $this; + } + + /** + * @param array $attributes + * @return $this + */ + public function filterUsingRequest(array $attributes): static + { + return $this->filter($attributes); + } + +} From e3ee89952f9ab129c3fc5a96af840501edf9c6bb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 27 Oct 2024 00:36:50 +0200 Subject: [PATCH 080/220] Fix too early inference --- src/Analyser/MutatingScope.php | 3 --- .../Rules/Comparison/MatchExpressionRuleTest.php | 9 +++++++++ tests/PHPStan/Rules/Comparison/data/bug-11852.php | 13 +++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-11852.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index df85c5fa35..5659a107bc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1129,9 +1129,6 @@ private function resolveType(string $exprString, Expr $node): Type } $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); - if (count($resultType->getConstantStrings()) === 0) { - return $resultType; - } } return $resultType ?? new ConstantStringType(''); diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index b727ba76e2..dd583aa13b 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -599,4 +599,13 @@ public function testBug9436(): void $this->analyse([__DIR__ . '/data/bug-9436.php'], []); } + public function testBug11852(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-11852.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11852.php b/tests/PHPStan/Rules/Comparison/data/bug-11852.php new file mode 100644 index 0000000000..690def3bc4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11852.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug11852; + +function sayHello(int $type, string $activity): int +{ + return match("$type:$activity") { + '159:Work' => 12, + '159:education' => 19, + + default => throw new \InvalidArgumentException("unknown values $type:$activity"), + }; +} From bf85e9d774fed9422d565521a13a2e21552fcb4d Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 2 Oct 2024 14:42:17 +0200 Subject: [PATCH 081/220] Support `@readonly` PHPDoc on the class as alternative to `@immutable` --- src/Reflection/ClassReflection.php | 2 +- ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 18 ++++++++ .../Rules/Properties/data/feature-11775.php | 45 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Properties/data/feature-11775.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 0254204a63..c79f2a6bed 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1238,7 +1238,7 @@ public function isImmutable(): bool { if ($this->isImmutable === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $this->isImmutable = $resolvedPhpDoc !== null && $resolvedPhpDoc->isImmutable(); + $this->isImmutable = $resolvedPhpDoc !== null && ($resolvedPhpDoc->isImmutable() || $resolvedPhpDoc->isReadOnly()); $parentClass = $this->getParentClass(); if ($parentClass !== null && !$this->isImmutable) { diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index e5496fc646..70d5379701 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -160,4 +160,22 @@ public function testFeature7648(): void $this->analyse([__DIR__ . '/data/feature-7648.php'], []); } + public function testFeature11775(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/feature-11775.php'], [ + [ + '@readonly property Feature11775\FooImmutable::$i is assigned outside of the constructor.', + 22, + ], + [ + '@readonly property Feature11775\FooReadonly::$i is assigned outside of the constructor.', + 43, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/feature-11775.php b/tests/PHPStan/Rules/Properties/data/feature-11775.php new file mode 100644 index 0000000000..a8a4bb8555 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/feature-11775.php @@ -0,0 +1,45 @@ += 7.4 + +namespace Feature11775; + +/** @immutable */ +class FooImmutable +{ + private int $i; + + public function __construct(int $i) + { + $this->i = $i; + } + + public function getId(): int + { + return $this->i; + } + + public function setId(): void + { + $this->i = 5; + } +} + +/** @readonly */ +class FooReadonly +{ + private int $i; + + public function __construct(int $i) + { + $this->i = $i; + } + + public function getId(): int + { + return $this->i; + } + + public function setId(): void + { + $this->i = 5; + } +} From 3dd5a01b2f6ca0c82a5f79f5aa9ba01aa4b4c69f Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 6 Nov 2024 20:35:51 +0100 Subject: [PATCH 082/220] Support `for` endless loops --- src/Analyser/NodeScopeResolver.php | 13 +- .../PHPStan/Analyser/StatementResultTest.php | 126 +++++++++++++++++- ...rictComparisonOfDifferentTypesRuleTest.php | 10 +- .../Comparison/data/strict-comparison.php | 14 +- .../Rules/Missing/MissingReturnRuleTest.php | 18 +++ tests/PHPStan/Rules/Missing/data/bug-6807.php | 16 +++ tests/PHPStan/Rules/Missing/data/bug-8463.php | 26 ++++ tests/PHPStan/Rules/Missing/data/bug-9374.php | 8 ++ ...fined-variables-anonymous-function-use.php | 2 +- .../Variables/data/defined-variables.php | 6 +- tests/e2e/baseline.neon | 5 + 11 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 tests/PHPStan/Rules/Missing/data/bug-6807.php create mode 100644 tests/PHPStan/Rules/Missing/data/bug-8463.php create mode 100644 tests/PHPStan/Rules/Missing/data/bug-9374.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dda335f566..1a6a069d28 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1384,7 +1384,10 @@ private function processStmtNode( $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); + + $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { + $alwaysIterates = $alwaysIterates->and($finalScope->getType($lastCondExpr)->toBoolean()->isTrue()); $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } @@ -1411,10 +1414,18 @@ private function processStmtNode( } } + if ($alwaysIterates->yes()) { + $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; + } elseif ($isIterableAtLeastOnce->yes()) { + $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating(); + } else { + $isAlwaysTerminating = false; + } + return new StatementResult( $finalScope, $finalScopeResult->hasYield() || $hasYield, - false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, + $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $finalScopeResult->getThrowPoints()), array_merge($impurePoints, $finalScopeResult->getImpurePoints()), diff --git a/tests/PHPStan/Analyser/StatementResultTest.php b/tests/PHPStan/Analyser/StatementResultTest.php index fb3a9dd5b1..b86de8617e 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -173,6 +173,130 @@ public function dataIsAlwaysTerminating(): array 'while (true) { break; }', false, ], + [ + 'while (true) { exit; }', + true, + ], + [ + 'while (true) { while (true) { } }', + true, + ], + [ + 'while (true) { while (true) { return; } }', + true, + ], + [ + 'while (true) { while (true) { break; } }', + true, + ], + [ + 'while (true) { while (true) { exit; } }', + true, + ], + [ + 'while (true) { while (true) { break 2; } }', + false, + ], + [ + 'while (true) { while ($x) { } }', + true, + ], + [ + 'while (true) { while ($x) { return; } }', + true, + ], + [ + 'while (true) { while ($x) { break; } }', + true, + ], + [ + 'while (true) { while ($x) { exit; } }', + true, + ], + [ + 'while (true) { while ($x) { break 2; } }', + false, + ], + [ + 'for (;;) { }', + true, + ], + [ + 'for (;;) { return; }', + true, + ], + [ + 'for (;;) { break; }', + false, + ], + [ + 'for (;;) { exit; }', + true, + ], + [ + 'for (;;) { for (;;) { } }', + true, + ], + [ + 'for (;;) { for (;;) { return; } }', + true, + ], + [ + 'for (;;) { for (;;) { break; } }', + true, + ], + [ + 'for (;;) { for (;;) { exit; } }', + true, + ], + [ + 'for (;;) { for (;;) { break 2; } }', + false, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { return; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { break; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { exit; } }', + true, + ], + [ + 'for (;;) { for ($i = 0; $i< 5; $i++) { break 2; } }', + false, + ], + [ + 'for ($i = 0; $i < 5;) { }', + true, + ], + [ + 'for ($i = 0; $i < 5; $i--) { }', + true, + ], + [ + 'for (; 0, 1;) { }', + true, + ], + [ + 'for (; 1, 0;) { }', + false, + ], + [ + 'for (; "", "a";) { }', + true, + ], + [ + 'for (; "a", "";) { }', + false, + ], [ 'do { } while (doFoo());', false, @@ -231,7 +355,7 @@ public function dataIsAlwaysTerminating(): array ], [ 'for ($i = 0; $i < 10; $i++) { return; }', - false, // will be true with range types + true, ], [ 'for ($i = 0; $i < 0; $i++) { return; }', diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 01157d82e1..48de5a95b4 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -109,12 +109,12 @@ public function testStrictComparison(): void 140, ], [ - 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', - 154, + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', + 150, ], [ - 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, + 'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.', + 161, ], [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', @@ -352,7 +352,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void ], [ 'Strict comparison using === between non-empty-array and null will always evaluate to false.', - 164, + 150, ], [ 'Strict comparison using === between 1 and 2 will always evaluate to false.', diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php index 70882c4ca4..f98e7e11aa 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -146,6 +146,13 @@ public function whileWithTypeChange() public function forWithTypeChange() { + for (; $val = $this->returnArray();) { + if ($val === null) { + + } + $val = null; + } + $foo = null; for (;;) { if ($foo !== null) { @@ -159,13 +166,6 @@ public function forWithTypeChange() $foo = new self(); } } - - for (; $val = $this->returnArray();) { - if ($val === null) { - - } - $val = null; - } } private function returnArray(): array diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index ad9fc94be5..f99d9352d9 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -325,4 +325,22 @@ public function testBug9309(): void $this->analyse([__DIR__ . '/data/bug-9309.php'], []); } + public function testBug6807(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-6807.php'], []); + } + + public function testBug8463(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-8463.php'], []); + } + + public function testBug9374(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-9374.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Missing/data/bug-6807.php b/tests/PHPStan/Rules/Missing/data/bug-6807.php new file mode 100644 index 0000000000..d7ffdde9dd --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-6807.php @@ -0,0 +1,16 @@ + 5) + throw new Exception(); + + if (rand() == 1) + return 5; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-8463.php b/tests/PHPStan/Rules/Missing/data/bug-8463.php new file mode 100644 index 0000000000..c27592782f --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-8463.php @@ -0,0 +1,26 @@ + Date: Thu, 7 Nov 2024 09:23:17 +0100 Subject: [PATCH 083/220] Reorganize new rules in changelog --- changelog-2.0.md | 120 +++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/changelog-2.0.md b/changelog-2.0.md index 1e2d866a1f..1756a96aa0 100644 --- a/changelog-2.0.md +++ b/changelog-2.0.md @@ -8,72 +8,78 @@ Major new features 🚀 * **Level 10** - level 9 on steroids, treats all `mixed` types strictly, not just explicit `mixed` * **Array `list` type** ([#1751](https://github.com/phpstan/phpstan-src/pull/1751)), #3311, #8185, #6243, thanks @rvanvelzen! * Lists are arrays with sequential integer keys starting at 0 -* **Validate inline PHPDoc `@var` tag** type against native type (level 2) (https://github.com/phpstan/phpstan-src/commit/a69e3bc2f1e87f6da1e65d7935f1cc36bd5c42fe) - * Set [`reportWrongPhpDocTypeInVarTag`](https://phpstan.org/config-reference#reportwrongphpdoctypeinvartag) to `true` to have all types validated, not just native ones - * Use config option `reportAnyTypeWideningInVarTag: true` for stricter behaviour ([#2840](https://github.com/phpstan/phpstan-src/pull/2840)), thanks @janedbal! * **Lower memory consumption** thanks to breaking up of reference cycles * [Learn more »](https://phpstan.org/blog/preprocessing-ast-for-custom-rules) * In testing the memory consumption was reduced by 50–70 %. * **Enhancements in handling parameters passed by reference** * [Learn more on phpstan.org](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) * [#2941](https://github.com/phpstan/phpstan-src/pull/2941), thanks @ljmaskey! -* Check too wide private property type (level 4) (https://github.com/phpstan/phpstan-src/commit/7453f4f75fae3d635063589467842aae29d88b54) -* Always report always true conditions, except for last elseif and match arm (https://github.com/phpstan/phpstan-src/commit/565fb0f6da9cdc58e8686598015561a848693972) -* Remove "unreachable branches" rules: UnreachableIfBranchesRule, UnreachableTernaryElseBranchRule, unreachable arm error in MatchExpressionRule - * Because "always true" is always reported, these are no longer needed +* New rules (level 0): + * MagicConstantContextRule ([#2741](https://github.com/phpstan/phpstan-src/pull/2741)), #10099, thanks @staabm! + * MissingMagicSerializationMethodsRule ([#1711](https://github.com/phpstan/phpstan-src/pull/1711)), #7482, thanks @staabm! + * Check vprintf/vsprintf arguments against placeholder count ([#3126](https://github.com/phpstan/phpstan-src/pull/3126)), thanks @staabm! + * Check if required file exists ([#3294](https://github.com/phpstan/phpstan-src/pull/3294)), #3397, thanks @Bellangelo! + * Add `@readonly` rule that disallows default values ([#1391](https://github.com/phpstan/phpstan-src/pull/1391)), thanks @herndlm! + * Rule about `@phpstan-consistent-constructor` ([#1296](https://github.com/phpstan/phpstan-src/pull/1296)), thanks @canvural! + * Check code in custom PHPStan extensions for runtime reflection concepts like `is_a()` or `class_parents()` (https://github.com/phpstan/phpstan-src/commit/c4a662ac6c3ec63f063238880b243b5399c34fcc) + * Check code in custom PHPStan extensions for runtime reflection concepts like `new ReflectionMethod()` (https://github.com/phpstan/phpstan-src/commit/536306611cbb5877b6565755cd07b87f9ccfdf08) + * ApiInstanceofRule + * Report `instanceof` of classes not covered by backward compatibility promise (https://github.com/phpstan/phpstan-src/commit/ff4d02d62a7a2e2c4d928d48d31d49246dba7139) + * Report `instanceof` of classes covered by backward compatibility promise but where the assumption might change (https://github.com/phpstan/phpstan-src/commit/996bc69fa977aa64f601bd82b8a0ae39be0cbeef) + * Check that PHPStan class in class constant fetch is covered by backward compatibility promise (https://github.com/phpstan/phpstan-src/commit/9e007251ce61788f6a0319a53f1de6cf801ed233) + * Previously absent type checks: + * Check existing classes in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/6838669976bf20232abde36ecdd52b1770fa50c9) + * Check nonexistent classes in local type aliases (https://github.com/phpstan/phpstan-src/commit/2485b2e9c129e789ec3b2d7db81ca30f87c63911) + * Check unresolvable types in local type aliases (https://github.com/phpstan/phpstan-src/commit/5f7d12b2fb2809525ab0e96eeae95093204ea4d3) + * Check generics in local type aliases (https://github.com/phpstan/phpstan-src/commit/5a2d4416d94ab77a2a2e7e1bfaba4c5ed2a13c25) + * Check existing classes in `@param-out` (https://github.com/phpstan/phpstan-src/commit/30c4b9e80f51af8b5f166ba3aae93d8409c9c0ea), #10260 + * Check existing classes in `@param-closure-this` (https://github.com/phpstan/phpstan-src/commit/2fa539a39e06bcc3155b109fd8d246703ceb176d), #10933 +* New rules (level 2): + * **Validate inline PHPDoc `@var` tag** type against native type (https://github.com/phpstan/phpstan-src/commit/a69e3bc2f1e87f6da1e65d7935f1cc36bd5c42fe) + * Set [`reportWrongPhpDocTypeInVarTag`](https://phpstan.org/config-reference#reportwrongphpdoctypeinvartag) to `true` to have all types validated, not just native ones + * Use config option `reportAnyTypeWideningInVarTag: true` for stricter behaviour ([#2840](https://github.com/phpstan/phpstan-src/pull/2840)), thanks @janedbal! + * IncompatibleDefaultParameterTypeRule for closures (https://github.com/phpstan/phpstan-src/commit/0264f5bc48448c7e02a23b82eef4177d0617a82f) + * Checking truthiness of `@phpstan-pure` above functions and methods + * Check variance of template types in properties ([#2314](https://github.com/phpstan/phpstan-src/pull/2314)), thanks @jiripudil! + * Report narrowing `PHPStan\Type\Type` interface via `@var` (https://github.com/phpstan/phpstan-src/commit/713b98fb107213c28e3d8c8b4b43c5f5fc47c144), https://github.com/nunomaduro/larastan/issues/1567#issuecomment-1460445389 + * Previously absent type checks: + * Check `@mixin` PHPDoc tag above traits (https://github.com/phpstan/phpstan-src/commit/0d0de946900adf4eb3c799b1b547567536e23147) + * Check `@extends`, `@implements`, `@use` for unresolvable types (https://github.com/phpstan/phpstan-src/commit/2bb528233edb75312614166e282776f279cf2018), #11552 + * Check types in `@method` tags (https://github.com/phpstan/phpstan-src/commit/5b7e474680eaf33874b7ed6a227677adcbed9ca5) + * Check generics `@method` `@template` tags above traits (https://github.com/phpstan/phpstan-src/commit/aadbf62d3ae4517fc7a212b07130bedcef8d13ac) + * Check types in `@property` tags (https://github.com/phpstan/phpstan-src/commit/55ea2ae516df22a071ab873fdd6f748a3af0520e), #10752, #9356 +* New rule (level 3): + * ArrayUnpackingRule ([#856](https://github.com/phpstan/phpstan-src/pull/856)), thanks @canvural! +* New rules (level 4): + * Check too wide private property type (https://github.com/phpstan/phpstan-src/commit/7453f4f75fae3d635063589467842aae29d88b54) + * LogicalXorConstantConditionRule (https://github.com/phpstan/phpstan-src/commit/3a12724fd636b1bcf36c22b36e8f765d97150895, https://github.com/phpstan/phpstan-src/commit/3b011f6524254dad0f16840fdcfdbe7421548617), #7539 + * Check that each trait is used and analysed at least once (https://github.com/phpstan/phpstan-src/commit/c4d05276fb8605d6ac20acbe1cc5df31cd6c10b0) + * Report useless return values of function calls like `var_export` without `$return=true` ([#3225](https://github.com/phpstan/phpstan-src/pull/3225)), #11320, thanks @staabm! + * ConstantLooseComparisonRule (https://github.com/phpstan/phpstan-src/commit/6ebf2361a3c831dd105a815521889428c295dc9f) + * Check `new`/function call/method call/static method call on a separate line without any side effects even without `@phpstan-pure` PHPDoc tag on the declaration side + * https://github.com/phpstan/phpstan-src/commit/281a87d1ab61809076ecfa6dfc2cc86e3babe235 + * [#3020](https://github.com/phpstan/phpstan-src/pull/3020), thanks @staabm! + * [#3022](https://github.com/phpstan/phpstan-src/pull/3022), thanks @staabm! + * [#3023](https://github.com/phpstan/phpstan-src/pull/3023), thanks @staabm! + * Always report always true conditions, except for last elseif and match arm (https://github.com/phpstan/phpstan-src/commit/565fb0f6da9cdc58e8686598015561a848693972) + * Remove "unreachable branches" rules: UnreachableIfBranchesRule, UnreachableTernaryElseBranchRule, unreachable arm error in MatchExpressionRule + * Because "always true" is always reported, these are no longer needed +* New rules (level 5): + * Check preg_quote delimiter sanity ([#3252](https://github.com/phpstan/phpstan-src/pull/3252)), #11338, thanks @staabm! + * Rule for `call_user_func()` ([#2479](https://github.com/phpstan/phpstan-src/pull/2479)), thanks @staabm! + * Report useless `array_filter()` calls ([#1077](https://github.com/phpstan/phpstan-src/pull/1077)), #6840, thanks @leongersen! + * Report useless `array_values()` calls ([#2917](https://github.com/phpstan/phpstan-src/pull/2917)), thanks @kamil-zacek! + * Check array functions which require stringish values ([#3132](https://github.com/phpstan/phpstan-src/pull/3132)), #11141, #5848, #3694, #11111, thanks @schlndh! + * Check unresolvable parameters ([#1319](https://github.com/phpstan/phpstan-src/pull/1319)), thanks @rvanvelzen! + * Enforce `@no-named-arguments` (https://github.com/phpstan/phpstan-src/commit/74ba8c23696948f2647d880df72f375346f41010), #5968 +* New rules (level 6): + * Previously absent type checks: + * Check missing types in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/892b319f25f04bc1b55c3d0063b607909612fe6d) + * Check missing types in local type aliases (https://github.com/phpstan/phpstan-src/commit/ce7ffaf02d624a7fb9d38f8e5dffc9739f1233fc) + * Check missing types in `@mixin` (https://github.com/phpstan/phpstan-src/commit/3175c81f26fd5bcb4a161b24e774921870ed2533) * New option: `polluteScopeWithBlock` (defaults to `true`, `false` in `phpstan-strict-rules`) (https://github.com/phpstan/phpstan-src/commit/946cf180c960930c2c42075d0f28ff9090507272) -* Checking truthiness of `@phpstan-pure` above functions and methods -* Check `new`/function call/method call/static method call on a separate line without any side effects even without `@phpstan-pure` PHPDoc tag on the declaration side - * https://github.com/phpstan/phpstan-src/commit/281a87d1ab61809076ecfa6dfc2cc86e3babe235 - * [#3020](https://github.com/phpstan/phpstan-src/pull/3020), thanks @staabm! - * [#3022](https://github.com/phpstan/phpstan-src/pull/3022), thanks @staabm! - * [#3023](https://github.com/phpstan/phpstan-src/pull/3023), thanks @staabm! -* LogicalXorConstantConditionRule (level 4) (https://github.com/phpstan/phpstan-src/commit/3a12724fd636b1bcf36c22b36e8f765d97150895, https://github.com/phpstan/phpstan-src/commit/3b011f6524254dad0f16840fdcfdbe7421548617), #7539 -* Check that each trait is used and analysed at least once (level 4) (https://github.com/phpstan/phpstan-src/commit/c4d05276fb8605d6ac20acbe1cc5df31cd6c10b0) -* Check preg_quote delimiter sanity (level 5) ([#3252](https://github.com/phpstan/phpstan-src/pull/3252)), #11338, thanks @staabm! -* MagicConstantContextRule (level 0) ([#2741](https://github.com/phpstan/phpstan-src/pull/2741)), #10099, thanks @staabm! -* MissingMagicSerializationMethodsRule (level 0) ([#1711](https://github.com/phpstan/phpstan-src/pull/1711)), #7482, thanks @staabm! -* Check vprintf/vsprintf arguments against placeholder count (level 0) ([#3126](https://github.com/phpstan/phpstan-src/pull/3126)), thanks @staabm! -* Report useless return values of function calls like `var_export` without `$return=true` (level 4) ([#3225](https://github.com/phpstan/phpstan-src/pull/3225)), #11320, thanks @staabm! -* Rule for `call_user_func()` (level 5) ([#2479](https://github.com/phpstan/phpstan-src/pull/2479)), thanks @staabm! -* Report useless `array_filter()` calls (level 5) ([#1077](https://github.com/phpstan/phpstan-src/pull/1077)), #6840, thanks @leongersen! -* Report useless `array_values()` calls (level 5) ([#2917](https://github.com/phpstan/phpstan-src/pull/2917)), thanks @kamil-zacek! -* Check if required file exists (level 0) ([#3294](https://github.com/phpstan/phpstan-src/pull/3294)), #3397, thanks @Bellangelo! -* ConstantLooseComparisonRule - level 4 (https://github.com/phpstan/phpstan-src/commit/6ebf2361a3c831dd105a815521889428c295dc9f) -* Check array functions which require stringish values (level 5) ([#3132](https://github.com/phpstan/phpstan-src/pull/3132)), #11141, #5848, #3694, #11111, thanks @schlndh! -* Check variance of template types in properties (level 2) ([#2314](https://github.com/phpstan/phpstan-src/pull/2314)), thanks @jiripudil! -* ArrayUnpackingRule (level 3) ([#856](https://github.com/phpstan/phpstan-src/pull/856)), thanks @canvural! -* Check unresolvable parameters (level 5) ([#1319](https://github.com/phpstan/phpstan-src/pull/1319)), thanks @rvanvelzen! -* Enforce `@no-named-arguments` (level 5) (https://github.com/phpstan/phpstan-src/commit/74ba8c23696948f2647d880df72f375346f41010), #5968 * Support `@readonly` property and `@immutable` class PHPDoc ([#1295](https://github.com/phpstan/phpstan-src/pull/1295), [#1335](https://github.com/phpstan/phpstan-src/pull/1335)), #4082, thanks @herndlm! -* Add `@readonly` rule that disallows default values (level 0) ([#1391](https://github.com/phpstan/phpstan-src/pull/1391)), thanks @herndlm! -* IncompatibleDefaultParameterTypeRule for closures (level 2) (https://github.com/phpstan/phpstan-src/commit/0264f5bc48448c7e02a23b82eef4177d0617a82f) -* Added previously absent type checks (level 0) - * Check existing classes in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/6838669976bf20232abde36ecdd52b1770fa50c9) - * Check nonexistent classes in local type aliases (https://github.com/phpstan/phpstan-src/commit/2485b2e9c129e789ec3b2d7db81ca30f87c63911) - * Check unresolvable types in local type aliases (https://github.com/phpstan/phpstan-src/commit/5f7d12b2fb2809525ab0e96eeae95093204ea4d3) - * Check generics in local type aliases (https://github.com/phpstan/phpstan-src/commit/5a2d4416d94ab77a2a2e7e1bfaba4c5ed2a13c25) - * Check existing classes in `@param-out` (https://github.com/phpstan/phpstan-src/commit/30c4b9e80f51af8b5f166ba3aae93d8409c9c0ea), #10260 - * Check existing classes in `@param-closure-this` (https://github.com/phpstan/phpstan-src/commit/2fa539a39e06bcc3155b109fd8d246703ceb176d), #10933 -* Added previously absent type checks (level 2) - * Check `@mixin` PHPDoc tag above traits (https://github.com/phpstan/phpstan-src/commit/0d0de946900adf4eb3c799b1b547567536e23147) - * Check `@extends`, `@implements`, `@use` for unresolvable types (https://github.com/phpstan/phpstan-src/commit/2bb528233edb75312614166e282776f279cf2018), #11552 - * Check types in `@method` tags (https://github.com/phpstan/phpstan-src/commit/5b7e474680eaf33874b7ed6a227677adcbed9ca5) - * Check generics `@method` `@template` tags above traits (https://github.com/phpstan/phpstan-src/commit/aadbf62d3ae4517fc7a212b07130bedcef8d13ac) - * Check types in `@property` tags (https://github.com/phpstan/phpstan-src/commit/55ea2ae516df22a071ab873fdd6f748a3af0520e), #10752, #9356 -* Added previously absent type checks (level 6) - * Check missing types in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/892b319f25f04bc1b55c3d0063b607909612fe6d) - * Check missing types in local type aliases (https://github.com/phpstan/phpstan-src/commit/ce7ffaf02d624a7fb9d38f8e5dffc9739f1233fc) - * Check missing types in `@mixin` (https://github.com/phpstan/phpstan-src/commit/3175c81f26fd5bcb4a161b24e774921870ed2533) -* Rule about `@phpstan-consistent-constructor` (level 0) ([#1296](https://github.com/phpstan/phpstan-src/pull/1296)), thanks @canvural! -* Check code in custom PHPStan extensions for runtime reflection concepts like `is_a()` or `class_parents()` (level 0) (https://github.com/phpstan/phpstan-src/commit/c4a662ac6c3ec63f063238880b243b5399c34fcc) -* Check code in custom PHPStan extensions for runtime reflection concepts like `new ReflectionMethod()` (level 0) (https://github.com/phpstan/phpstan-src/commit/536306611cbb5877b6565755cd07b87f9ccfdf08) -* ApiInstanceofRule (level 0) - * Report `instanceof` of classes not covered by backward compatibility promise (https://github.com/phpstan/phpstan-src/commit/ff4d02d62a7a2e2c4d928d48d31d49246dba7139) - * Report `instanceof` of classes covered by backward compatibility promise but where the assumption might change (https://github.com/phpstan/phpstan-src/commit/996bc69fa977aa64f601bd82b8a0ae39be0cbeef) -* Check that PHPStan class in class constant fetch is covered by backward compatibility promise (level 0) (https://github.com/phpstan/phpstan-src/commit/9e007251ce61788f6a0319a53f1de6cf801ed233) * Deprecate various `instanceof *Type` in favour of new methods on `Type` interface, (https://github.com/phpstan/phpstan-src/commit/436e6d3015cbeba4645d38bc7a6a865b9c6d7c74), learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) -* Report narrowing `PHPStan\Type\Type` interface via `@var` (https://github.com/phpstan/phpstan-src/commit/713b98fb107213c28e3d8c8b4b43c5f5fc47c144), https://github.com/nunomaduro/larastan/issues/1567#issuecomment-1460445389 Improvements 🔧 From 3dc64a29f1fb003630bb7164dfcce12f5bb40fad Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:00:06 +0000 Subject: [PATCH 084/220] Update phpdoc-parser --- composer.json | 2 +- composer.lock | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 1afe40674c..23c5c46c7a 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.42.0.11", "phpstan/php-8-stubs": "0.4.4", - "phpstan/phpdoc-parser": "^2.0", + "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", "react/child-process": "^0.7", diff --git a/composer.lock b/composer.lock index 4a1e51a351..3197c29134 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "350cc72e1a307581ffc8228f105bd273", + "content-hash": "8793a11aa08550fce8d98648609fda3b", "packages": [ { "name": "clue/ndjson-react", @@ -2290,7 +2290,7 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", @@ -2316,7 +2316,6 @@ "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2332,7 +2331,7 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.x" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" }, "time": "2024-10-13T11:29:49+00:00" }, From b1b778280207c076b4e92ad0b5fea385102ea9b1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 8 Nov 2024 14:53:02 +0100 Subject: [PATCH 085/220] More precise types for `preg_match` greater than `0` --- src/Analyser/TypeSpecifier.php | 20 +++++++- tests/PHPStan/Analyser/nsrt/bug-11293.php | 62 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11293.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index fb8b5985e7..68864c18b6 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -242,11 +242,11 @@ public function specifyTypesInCondition( $expr->left instanceof FuncCall && count($expr->left->getArgs()) >= 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -336,6 +336,22 @@ public function specifyTypesInCondition( } } + if ( + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) >= 3 + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), + $context, + $rootExpr, + ); + } + if ( !$context->null() && $expr->right instanceof FuncCall diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php new file mode 100644 index 0000000000..0c190b23fc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -0,0 +1,62 @@ += 7.4 + +namespace Bug11293; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) { + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello2(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) { + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello3(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) { + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + } + + public function sayHello4(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) { + assertType('array{}', $matches); + + return; + } + + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + + public function sayHello5(string $s): void + { + if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) { + assertType('array{}', $matches); + + return; + } + + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + + public function sayHello6(string $s): void + { + if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) { + assertType('array{}', $matches); + + return; + } + + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } +} From d44f4df6bbccf05a0913b7eb1ad0ea0f6928e824 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 8 Nov 2024 15:31:11 +0100 Subject: [PATCH 086/220] Fix after merge --- src/Analyser/TypeSpecifier.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 8b6e3c4541..cee4acb569 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -340,8 +340,7 @@ public function specifyTypesInCondition( $scope, new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), $context, - $rootExpr, - ); + )->setRootExpr($expr); } if ( From a965e73470dd35568b19f0cbe4c366036821ab35 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 8 Nov 2024 16:06:14 +0100 Subject: [PATCH 087/220] Use named argument in error for variadic types When a type is variadic, and multiple named arguments are used, it's often hard to tell for which argument the error is coming. Since we have the named argument, we could use it in this situation. --- src/Rules/AttributesCheck.php | 6 +++--- src/Rules/Classes/InstantiationRule.php | 6 +++--- src/Rules/FunctionCallParametersCheck.php | 15 ++++++++++----- src/Rules/Functions/CallCallablesRule.php | 6 +++--- .../Functions/CallToFunctionParametersRule.php | 6 +++--- src/Rules/Functions/CallUserFuncRule.php | 6 +++--- src/Rules/Methods/CallMethodsRule.php | 6 +++--- src/Rules/Methods/CallStaticMethodsRule.php | 6 +++--- .../PHPStan/Rules/Methods/CallMethodsRuleTest.php | 10 +++++++++- .../Rules/Methods/data/named-arguments.php | 1 + 10 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 8633178d9f..e12f906c00 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -144,14 +144,14 @@ public function check( 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', + '%s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', + '%s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', - 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', + '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', 'Attribute class ' . $attributeClassName . ' constructorinvoked with %s, but it\'s not allowed because of @no-named-arguments.', ); diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 604038d1fa..7dd65488a0 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -209,14 +209,14 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.', 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.', 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.', + '%s of class ' . $classDisplayName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', + ' %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', 'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName, 'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.', 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', - 'Parameter %s of class ' . $classDisplayName . ' constructor contains unresolvable type.', + '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index eb714ab171..10e4e50212 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -30,6 +30,7 @@ use function array_key_exists; use function count; use function implode; +use function is_int; use function is_string; use function max; use function sprintf; @@ -331,7 +332,7 @@ public function check( $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); $errors[] = RuleErrorBuilder::message(sprintf( $wrongArgumentTypeMessage, - $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + $this->describeParameter($parameter, $argumentName ?? $i + 1), $parameterType->describe($verbosityLevel), $argumentValueType->describe($verbosityLevel), )) @@ -409,7 +410,7 @@ public function check( } $errors[] = RuleErrorBuilder::message(sprintf( - 'Parameter %s is passed by reference so it does not accept %s.', + '%s is passed by reference so it does not accept %s.', $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $propertyDescription, ))->identifier('argument.byRef')->line($argumentLine)->build(); @@ -625,11 +626,15 @@ private function processArguments( return [$errors, $newArguments]; } - private function describeParameter(ParameterReflection $parameter, ?int $position): string + private function describeParameter(ParameterReflection $parameter, int|string|null $positionOrNamed): string { $parts = []; - if ($position !== null) { - $parts[] = '#' . $position; + if (is_int($positionOrNamed)) { + $parts[] = 'Parameter #' . $positionOrNamed; + } elseif ($parameter->isVariadic() && is_string($positionOrNamed)) { + $parts[] = 'Named argument ' . $positionOrNamed . ' for variadic parameter'; + } else { + $parts[] = 'Parameter'; } $name = $parameter->getName(); diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 15c0bfb9ca..162894e266 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -126,14 +126,14 @@ public function processNode( ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', + '%s of ' . $callableDescription . ' expects %s, %s given.', 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to ' . $callableDescription, 'Missing parameter $%s in call to ' . $callableDescription . '.', 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ), ); diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 39b4ee650f..63e1b93c7a 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -57,14 +57,14 @@ public function processNode(Node $node, Scope $scope): array 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of function ' . $functionName . ' expects %s, %s given.', + '%s of function ' . $functionName . ' expects %s, %s given.', 'Result of function ' . $functionName . ' (void) is used.', - 'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.', + '%s of function ' . $functionName . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to function ' . $functionName, 'Missing parameter $%s in call to function ' . $functionName . '.', 'Unknown parameter $%s in call to function ' . $functionName . '.', 'Return type of call to function ' . $functionName . ' contains unresolvable type.', - 'Parameter %s of function ' . $functionName . ' contains unresolvable type.', + '%s of function ' . $functionName . ' contains unresolvable type.', 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 2ea1fb2777..5eb21c2128 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -73,14 +73,14 @@ public function processNode(Node $node, Scope $scope): array ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', + '%s of ' . $callableDescription . ' expects %s, %s given.', 'Result of ' . $callableDescription . ' (void) is used.', - 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + '%s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to ' . $callableDescription, 'Missing parameter $%s in call to ' . $callableDescription . '.', 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', + '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ); } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index b0d522f439..19bc52fe80 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -63,14 +63,14 @@ public function processNode(Node $node, Scope $scope): array 'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.', 'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.', 'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of method ' . $messagesMethodName . ' expects %s, %s given.', + '%s of method ' . $messagesMethodName . ' expects %s, %s given.', 'Result of method ' . $messagesMethodName . ' (void) is used.', - 'Parameter %s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', + '%s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to method ' . $messagesMethodName, 'Missing parameter $%s in call to method ' . $messagesMethodName . '.', 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', - 'Parameter %s of method ' . $messagesMethodName . ' contains unresolvable type.', + '%s of method ' . $messagesMethodName . ' contains unresolvable type.', 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 0f98eca2d9..e6b2c9dbb5 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -71,14 +71,14 @@ public function processNode(Node $node, Scope $scope): array $displayMethodName . ' invoked with %d parameters, at least %d required.', $displayMethodName . ' invoked with %d parameter, %d-%d required.', $displayMethodName . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of ' . $lowercasedMethodName . ' expects %s, %s given.', + '%s of ' . $lowercasedMethodName . ' expects %s, %s given.', 'Result of ' . $lowercasedMethodName . ' (void) is used.', - 'Parameter %s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', + '%s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.', 'Unable to resolve the template type %s in call to method ' . $lowercasedMethodName, 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', - 'Parameter %s of ' . $lowercasedMethodName . ' contains unresolvable type.', + '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', )); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index cc614cef95..a883c768d9 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -1907,7 +1907,7 @@ public function testNamedArguments(): void 91, ], [ - 'Parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', 91, ], [ @@ -1922,6 +1922,14 @@ public function testNamedArguments(): void 'Unpacked argument (...) cannot be followed by a non-unpacked argument.', 94, ], + [ + 'Named argument foo for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], + [ + 'Named argument bar for variadic parameter ...$args of method NamedArgumentsMethod\Foo::doIpsum() expects string, int given.', + 95, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/named-arguments.php b/tests/PHPStan/Rules/Methods/data/named-arguments.php index f5c779f171..25d9ef362b 100644 --- a/tests/PHPStan/Rules/Methods/data/named-arguments.php +++ b/tests/PHPStan/Rules/Methods/data/named-arguments.php @@ -92,6 +92,7 @@ public function doDolor(): void $this->doIpsum(...['a' => 1, 'foo' => 'foo']); $this->doIpsum(...['b' => 1, 'foo' => 'foo']); $this->doIpsum(...[1, 2], 'foo'); + $this->doIpsum(1, 2, foo: 1, bar: 2); } } From 5f0b1ccfa47060c209ead7116005214183c0e56f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 9 Nov 2024 10:05:14 +0100 Subject: [PATCH 088/220] Too-wide return type - do not report void in PHPDoc union type --- .../TooWideFunctionReturnTypehintRule.php | 6 +-- .../TooWideMethodReturnTypehintRule.php | 6 +-- .../TooWideFunctionReturnTypehintRuleTest.php | 10 ++++ .../TooWideMethodReturnTypehintRuleTest.php | 12 +++++ .../data/bug-11980-function.php | 49 +++++++++++++++++ .../Rules/TooWideTypehints/data/bug-11980.php | 53 +++++++++++++++++++ 6 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php create mode 100644 tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 217d8576cd..b0b11a10ae 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -11,6 +11,7 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; @@ -48,16 +49,13 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; - } - $returnType = TypeCombinator::union(...$returnTypes); $messages = []; diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index ebb22df8ae..b73fa09641 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -12,6 +12,7 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; use function count; use function sprintf; @@ -74,16 +75,13 @@ public function processNode(Node $node, Scope $scope): array foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); continue; } $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } - if (count($returnTypes) === 0) { - return []; - } - $returnType = TypeCombinator::union(...$returnTypes); if ( !$method->isPrivate() diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index b914db3141..9c86812e92 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -51,4 +51,14 @@ public function testRule(): void ]); } + public function testBug11980(): void + { + $this->analyse([__DIR__ . '/data/bug-11980-function.php'], [ + [ + 'Function Bug11980Function\process2() never returns void so it can be removed from the return type.', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 9e9588d219..cad3ca757f 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -248,4 +248,16 @@ public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, bool $this->analyse([__DIR__ . '/data/method-too-wide-return-always-check-final.php'], $expectedErrors); } + public function testBug11980(): void + { + $this->checkProtectedAndPublicMethods = true; + $this->alwaysCheckFinal = true; + $this->analyse([__DIR__ . '/data/bug-11980.php'], [ + [ + 'Method Bug11980\Demo::process2() never returns void so it can be removed from the return type.', + 37, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php new file mode 100644 index 0000000000..0db93a1cfb --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php @@ -0,0 +1,49 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } +} + +/** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ +function process2($tokens, $stackPtr) +{ + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php new file mode 100644 index 0000000000..99ab186f62 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php @@ -0,0 +1,53 @@ +> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + } + + /** + * @param array> $tokens + * @param int $stackPtr + * + * @return int|void + */ + public function process2($tokens, $stackPtr) + { + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Not a stand-alone statement. + return null; + } + + $end = 10; + + if ($tokens[$end]['code'] !== 10 + && $tokens[$end]['code'] !== 20 + ) { + // Not a stand-alone statement. + return $end; + } + } +} From ac1f53936d37c04da80023b7c88c9cfed9f0e890 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 9 Nov 2024 10:20:22 +0100 Subject: [PATCH 089/220] Fix after merge --- .../TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index f3aad9f7da..a98c638d24 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -192,7 +192,6 @@ public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, array public function testBug11980(): void { $this->checkProtectedAndPublicMethods = true; - $this->alwaysCheckFinal = true; $this->analyse([__DIR__ . '/data/bug-11980.php'], [ [ 'Method Bug11980\Demo::process2() never returns void so it can be removed from the return type.', From ccfb4ab7a19151925b9434e3245892006b3d9dcd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 9 Nov 2024 18:52:57 +0100 Subject: [PATCH 090/220] Results - make reasons unique --- src/Type/AcceptsResult.php | 4 ++-- src/Type/IsSuperTypeOfResult.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php index 4cfecb05f6..2548f3dd44 100644 --- a/src/Type/AcceptsResult.php +++ b/src/Type/AcceptsResult.php @@ -108,7 +108,7 @@ public static function extremeIdentity(self ...$operands): self } } - return new self($result, $reasons); + return new self($result, array_values(array_unique($reasons))); } public static function maxMin(self ...$operands): self @@ -125,7 +125,7 @@ public static function maxMin(self ...$operands): self } } - return new self($result, $reasons); + return new self($result, array_values(array_unique($reasons))); } } diff --git a/src/Type/IsSuperTypeOfResult.php b/src/Type/IsSuperTypeOfResult.php index 30e9fe6acc..dd28cec78f 100644 --- a/src/Type/IsSuperTypeOfResult.php +++ b/src/Type/IsSuperTypeOfResult.php @@ -153,7 +153,7 @@ private static function mergeReasons(array $operands): array } } - return $reasons; + return array_values(array_unique($reasons)); } } From a0b6b3ba95e9a0e22a47259066890c35b1c15749 Mon Sep 17 00:00:00 2001 From: "Jose M. Valera Reales" Date: Sat, 9 Nov 2024 18:55:02 +0100 Subject: [PATCH 091/220] Upgrade bashunit:0.18.0 for e2e tests --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 21071b8ed6..6d7233d38a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -253,7 +253,7 @@ jobs: run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" - name: "Install bashunit" - run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.17.0" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.18.0" - name: "Test" run: "${{ matrix.script }}" @@ -355,7 +355,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Install bashunit" - run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.17.0" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.18.0" - name: "Test" run: ${{ matrix.script }} From c55aa0586bf08b8e029576ba4a78782418b4a932 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 10 Nov 2024 13:56:27 +0100 Subject: [PATCH 092/220] Improve `for` loop initial statement scope pollution --- src/Analyser/MutatingScope.php | 62 ++++++++++ src/Analyser/NodeScopeResolver.php | 2 +- .../Analyser/ForLoopNoScopePollutionTest.php | 36 ++++++ .../data/for-loop-no-scope-pollution.php | 107 ++++++++++++++++++ .../Analyser/forLoopNoScopePollution.neon | 2 + tests/PHPStan/Analyser/nsrt/for-loop.php | 107 ++++++++++++++++++ .../Variables/DefinedVariableRuleTest.php | 9 ++ .../PHPStan/Rules/Variables/data/bug-9550.php | 32 ++++++ 8 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php create mode 100644 tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php create mode 100644 tests/PHPStan/Analyser/forLoopNoScopePollution.neon create mode 100644 tests/PHPStan/Analyser/nsrt/for-loop.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9550.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5659a107bc..3993085894 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4900,6 +4900,68 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope ); } + public function processAlwaysIterableForScopeWithoutPollute(self $finalScope, self $initScope): self + { + $expressionTypes = $this->expressionTypes; + $initScopeExpressionTypes = $initScope->expressionTypes; + foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($expressionTypes[$variableExprString])) { + if (isset($initScopeExpressionTypes[$variableExprString])) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $expressionTypes[$variableExprString] = $variableTypeHolder; + continue; + } + + $expressionTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), + ); + } + + $nativeTypes = $this->nativeExpressionTypes; + $initScopeNativeExpressionTypes = $initScope->nativeExpressionTypes; + foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($nativeTypes[$variableExprString])) { + if (isset($initScopeNativeExpressionTypes[$variableExprString])) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $nativeTypes[$variableExprString] = $variableTypeHolder; + continue; + } + + $nativeTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), + ); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1a6a069d28..31dde8718b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1410,7 +1410,7 @@ private function processStmtNode( } } else { if (!$this->polluteScopeWithLoopInitialAssignments) { - $finalScope = $finalScope->mergeWith($scope); + $finalScope = $scope->processAlwaysIterableForScopeWithoutPollute($finalScope, $initScope); } } diff --git a/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php b/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php new file mode 100644 index 0000000000..9b07f9ab2b --- /dev/null +++ b/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php @@ -0,0 +1,36 @@ +> */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/for-loop-no-scope-pollution.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/forLoopNoScopePollution.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php b/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php new file mode 100644 index 0000000000..e7c0bef9ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php @@ -0,0 +1,107 @@ +', $i); + assertNativeType('int<10, max>', $i); + assertVariableCertainty(TrinaryLogic::createMaybe(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int<0, 1>', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int<0, 1>', $b); + assertNativeType('int', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createYes(), $c); + } + + /** @param int $b */ + public function loopThatMightIterateAtLeastOnce(int $a, $b): void + { + $j = 0; + for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('int<0, max>', $i); + assertNativeType('int<0, max>', $i); + assertVariableCertainty(TrinaryLogic::createMaybe(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createMaybe(), $c); + } + + /** @param int $b */ + public function loopThatNeverIterates(int $a, $b): void + { + $j = 0; + for ($i = 0, $j = 10; $i > 10; $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('*ERROR*', $i); + assertNativeType('*ERROR*', $i); + assertVariableCertainty(TrinaryLogic::createNo(), $i); + + assertType('0', $j); + assertNativeType('0', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('*ERROR*', $c); + assertNativeType('*ERROR*', $c); + assertVariableCertainty(TrinaryLogic::createNo(), $c); + } + +} diff --git a/tests/PHPStan/Analyser/forLoopNoScopePollution.neon b/tests/PHPStan/Analyser/forLoopNoScopePollution.neon new file mode 100644 index 0000000000..47811f500e --- /dev/null +++ b/tests/PHPStan/Analyser/forLoopNoScopePollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithLoopInitialAssignments: false diff --git a/tests/PHPStan/Analyser/nsrt/for-loop.php b/tests/PHPStan/Analyser/nsrt/for-loop.php new file mode 100644 index 0000000000..f15cdeb811 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/for-loop.php @@ -0,0 +1,107 @@ +', $i); + assertNativeType('int<10, max>', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int<0, 1>', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int<0, 1>', $b); + assertNativeType('int', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createYes(), $c); + } + + /** @param int $b */ + public function loopThatMightIterateAtLeastOnce(int $a, $b): void + { + $j = 0; + for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('int<0, max>', $i); + assertNativeType('int<0, max>', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createMaybe(), $c); + } + + /** @param int $b */ + public function loopThatNeverIterates(int $a, $b): void + { + $j = 0; + for ($i = 0, $j = 10; $i > 10; $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('0', $i); + assertNativeType('0', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('10', $j); + assertNativeType('10', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('*ERROR*', $c); + assertNativeType('*ERROR*', $c); + assertVariableCertainty(TrinaryLogic::createNo(), $c); + } + +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 94f0b0ffeb..9a2346eb77 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1068,4 +1068,13 @@ public function testBug10228(): void $this->analyse([__DIR__ . '/data/bug-10228.php'], []); } + public function testBug9550(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9550.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9550.php b/tests/PHPStan/Rules/Variables/data/bug-9550.php new file mode 100644 index 0000000000..ab260ad44c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9550.php @@ -0,0 +1,32 @@ + 0; --$l) { + $vStr = mb_substr($vStrLonger, 0, $l); + if ($vStr !== $vStrLonger) { + $vStrLonger = $vStr; + $vStr = mb_substr($vStr, 0, $l - 3); + $withThreeDots = true; + } else { + $vStrLonger = $vStr; + } + $vStr = str_replace(["\0", "\t", "\n", "\r"], ['\0', '\t', '\n', '\r'], $vStr); + if (mb_strlen($vStr) <= $maxUtf8Length - ($withThreeDots ? 3 : 0)) { + break; + } + } + + return '\'' . $vStr . '\'' . ($withThreeDots ? '...' : ''); + } +} From 381c1370e7ce3b1c2d8de6c5b30913908c362eb8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 10 Nov 2024 14:57:32 +0100 Subject: [PATCH 093/220] RichParser - fix `@phpstan-ignore` with trait in the same file --- src/Parser/RichParser.php | 7 ++++- tests/PHPStan/Parser/RichParserTest.php | 39 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index b5863a4b80..fb481b9923 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -102,7 +102,12 @@ public function parseString(string $sourceCode): array } foreach ($traitCollectingVisitor->traits as $trait) { - $trait->setAttribute('linesToIgnore', array_filter($linesToIgnore, static fn (int $line): bool => $line >= $trait->getStartLine() && $line <= $trait->getEndLine(), ARRAY_FILTER_USE_KEY)); + $preexisting = $trait->getAttribute('linesToIgnore', []); + $filteredLinesToIgnore = array_filter($linesToIgnore, static fn (int $line): bool => $line >= $trait->getStartLine() && $line <= $trait->getEndLine(), ARRAY_FILTER_USE_KEY); + foreach ($preexisting as $line => $ignores) { + $filteredLinesToIgnore[$line] = $ignores; + } + $trait->setAttribute('linesToIgnore', $filteredLinesToIgnore); } return $nodes; diff --git a/tests/PHPStan/Parser/RichParserTest.php b/tests/PHPStan/Parser/RichParserTest.php index 103eb129b4..5fa112a57f 100644 --- a/tests/PHPStan/Parser/RichParserTest.php +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -261,6 +261,45 @@ public function dataLinesToIgnore(): iterable 2 => ['identifier'], ], ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 10 => ['variable.undefined'], + ], + ]; + + yield [ + 'myProperty = $b;' . PHP_EOL . + ' }' . PHP_EOL . + '}', + [ + 13 => ['variable.undefined'], + ], + ]; } /** From f2dfd5debf73336a7d5c7071b42d43db975470b9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 10 Nov 2024 15:09:45 +0100 Subject: [PATCH 094/220] Revert "Improve `for` loop initial statement scope pollution" This reverts commit c55aa0586bf08b8e029576ba4a78782418b4a932. --- src/Analyser/MutatingScope.php | 62 ---------- src/Analyser/NodeScopeResolver.php | 2 +- .../Analyser/ForLoopNoScopePollutionTest.php | 36 ------ .../data/for-loop-no-scope-pollution.php | 107 ------------------ .../Analyser/forLoopNoScopePollution.neon | 2 - tests/PHPStan/Analyser/nsrt/for-loop.php | 107 ------------------ .../Variables/DefinedVariableRuleTest.php | 9 -- .../PHPStan/Rules/Variables/data/bug-9550.php | 32 ------ 8 files changed, 1 insertion(+), 356 deletions(-) delete mode 100644 tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php delete mode 100644 tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php delete mode 100644 tests/PHPStan/Analyser/forLoopNoScopePollution.neon delete mode 100644 tests/PHPStan/Analyser/nsrt/for-loop.php delete mode 100644 tests/PHPStan/Rules/Variables/data/bug-9550.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 3993085894..5659a107bc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4900,68 +4900,6 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope ); } - public function processAlwaysIterableForScopeWithoutPollute(self $finalScope, self $initScope): self - { - $expressionTypes = $this->expressionTypes; - $initScopeExpressionTypes = $initScope->expressionTypes; - foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { - if (!isset($expressionTypes[$variableExprString])) { - if (isset($initScopeExpressionTypes[$variableExprString])) { - $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); - continue; - } - - $expressionTypes[$variableExprString] = $variableTypeHolder; - continue; - } - - $expressionTypes[$variableExprString] = new ExpressionTypeHolder( - $variableTypeHolder->getExpr(), - $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), - ); - } - - $nativeTypes = $this->nativeExpressionTypes; - $initScopeNativeExpressionTypes = $initScope->nativeExpressionTypes; - foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { - if (!isset($nativeTypes[$variableExprString])) { - if (isset($initScopeNativeExpressionTypes[$variableExprString])) { - $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); - continue; - } - - $nativeTypes[$variableExprString] = $variableTypeHolder; - continue; - } - - $nativeTypes[$variableExprString] = new ExpressionTypeHolder( - $variableTypeHolder->getExpr(), - $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), - ); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $expressionTypes, - $nativeTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - [], - [], - [], - $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, - ); - } - public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 31dde8718b..1a6a069d28 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1410,7 +1410,7 @@ private function processStmtNode( } } else { if (!$this->polluteScopeWithLoopInitialAssignments) { - $finalScope = $scope->processAlwaysIterableForScopeWithoutPollute($finalScope, $initScope); + $finalScope = $finalScope->mergeWith($scope); } } diff --git a/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php b/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php deleted file mode 100644 index 9b07f9ab2b..0000000000 --- a/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php +++ /dev/null @@ -1,36 +0,0 @@ -> */ - public function dataFileAsserts(): iterable - { - yield from $this->gatherAssertTypes(__DIR__ . '/data/for-loop-no-scope-pollution.php'); - } - - /** - * @dataProvider dataFileAsserts - * @param mixed ...$args - */ - public function testFileAsserts( - string $assertType, - string $file, - ...$args, - ): void - { - $this->assertFileAsserts($assertType, $file, ...$args); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/forLoopNoScopePollution.neon', - ]; - } - -} diff --git a/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php b/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php deleted file mode 100644 index e7c0bef9ff..0000000000 --- a/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php +++ /dev/null @@ -1,107 +0,0 @@ -', $i); - assertNativeType('int<10, max>', $i); - assertVariableCertainty(TrinaryLogic::createMaybe(), $i); - - assertType('int', $j); - assertNativeType('int', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int<0, 1>', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int<0, 1>', $b); - assertNativeType('int', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('int<0, 1>', $c); - assertNativeType('int', $c); - assertVariableCertainty(TrinaryLogic::createYes(), $c); - } - - /** @param int $b */ - public function loopThatMightIterateAtLeastOnce(int $a, $b): void - { - $j = 0; - for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { - $a = rand(0, 1); - $b = rand(0, 1); - $c = rand(0, 1); - } - - assertType('int<0, max>', $i); - assertNativeType('int<0, max>', $i); - assertVariableCertainty(TrinaryLogic::createMaybe(), $i); - - assertType('int', $j); - assertNativeType('int', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int', $b); - assertNativeType('mixed', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('int<0, 1>', $c); - assertNativeType('int', $c); - assertVariableCertainty(TrinaryLogic::createMaybe(), $c); - } - - /** @param int $b */ - public function loopThatNeverIterates(int $a, $b): void - { - $j = 0; - for ($i = 0, $j = 10; $i > 10; $i++, $j--) { - $a = rand(0, 1); - $b = rand(0, 1); - $c = rand(0, 1); - } - - assertType('*ERROR*', $i); - assertNativeType('*ERROR*', $i); - assertVariableCertainty(TrinaryLogic::createNo(), $i); - - assertType('0', $j); - assertNativeType('0', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int', $b); - assertNativeType('mixed', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('*ERROR*', $c); - assertNativeType('*ERROR*', $c); - assertVariableCertainty(TrinaryLogic::createNo(), $c); - } - -} diff --git a/tests/PHPStan/Analyser/forLoopNoScopePollution.neon b/tests/PHPStan/Analyser/forLoopNoScopePollution.neon deleted file mode 100644 index 47811f500e..0000000000 --- a/tests/PHPStan/Analyser/forLoopNoScopePollution.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - polluteScopeWithLoopInitialAssignments: false diff --git a/tests/PHPStan/Analyser/nsrt/for-loop.php b/tests/PHPStan/Analyser/nsrt/for-loop.php deleted file mode 100644 index f15cdeb811..0000000000 --- a/tests/PHPStan/Analyser/nsrt/for-loop.php +++ /dev/null @@ -1,107 +0,0 @@ -', $i); - assertNativeType('int<10, max>', $i); - assertVariableCertainty(TrinaryLogic::createYes(), $i); - - assertType('int', $j); - assertNativeType('int', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int<0, 1>', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int<0, 1>', $b); - assertNativeType('int', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('int<0, 1>', $c); - assertNativeType('int', $c); - assertVariableCertainty(TrinaryLogic::createYes(), $c); - } - - /** @param int $b */ - public function loopThatMightIterateAtLeastOnce(int $a, $b): void - { - $j = 0; - for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { - $a = rand(0, 1); - $b = rand(0, 1); - $c = rand(0, 1); - } - - assertType('int<0, max>', $i); - assertNativeType('int<0, max>', $i); - assertVariableCertainty(TrinaryLogic::createYes(), $i); - - assertType('int', $j); - assertNativeType('int', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int', $b); - assertNativeType('mixed', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('int<0, 1>', $c); - assertNativeType('int', $c); - assertVariableCertainty(TrinaryLogic::createMaybe(), $c); - } - - /** @param int $b */ - public function loopThatNeverIterates(int $a, $b): void - { - $j = 0; - for ($i = 0, $j = 10; $i > 10; $i++, $j--) { - $a = rand(0, 1); - $b = rand(0, 1); - $c = rand(0, 1); - } - - assertType('0', $i); - assertNativeType('0', $i); - assertVariableCertainty(TrinaryLogic::createYes(), $i); - - assertType('10', $j); - assertNativeType('10', $j); - assertVariableCertainty(TrinaryLogic::createYes(), $j); - - assertType('int', $a); - assertNativeType('int', $a); - assertVariableCertainty(TrinaryLogic::createYes(), $a); - - assertType('int', $b); - assertNativeType('mixed', $b); - assertVariableCertainty(TrinaryLogic::createYes(), $b); - - assertType('*ERROR*', $c); - assertNativeType('*ERROR*', $c); - assertVariableCertainty(TrinaryLogic::createNo(), $c); - } - -} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 9a2346eb77..94f0b0ffeb 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1068,13 +1068,4 @@ public function testBug10228(): void $this->analyse([__DIR__ . '/data/bug-10228.php'], []); } - public function testBug9550(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-9550.php'], []); - } - } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9550.php b/tests/PHPStan/Rules/Variables/data/bug-9550.php deleted file mode 100644 index ab260ad44c..0000000000 --- a/tests/PHPStan/Rules/Variables/data/bug-9550.php +++ /dev/null @@ -1,32 +0,0 @@ - 0; --$l) { - $vStr = mb_substr($vStrLonger, 0, $l); - if ($vStr !== $vStrLonger) { - $vStrLonger = $vStr; - $vStr = mb_substr($vStr, 0, $l - 3); - $withThreeDots = true; - } else { - $vStrLonger = $vStr; - } - $vStr = str_replace(["\0", "\t", "\n", "\r"], ['\0', '\t', '\n', '\r'], $vStr); - if (mb_strlen($vStr) <= $maxUtf8Length - ($withThreeDots ? 3 : 0)) { - break; - } - } - - return '\'' . $vStr . '\'' . ($withThreeDots ? '...' : ''); - } -} From 80c1df2d73210227776db5443dbc28c2d71fa289 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 10 Nov 2024 15:12:19 +0100 Subject: [PATCH 095/220] Too-wide return type - allow `void` return type in a union when the returned expr is originally `void` --- .../TooWideFunctionReturnTypehintRule.php | 4 ++++ .../TooWideMethodReturnTypehintRule.php | 4 ++++ .../data/bug-11980-function.php | 21 +++++++++++++++++++ .../Rules/TooWideTypehints/data/bug-11980.php | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index b0b11a10ae..3b52a47aad 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -56,6 +56,10 @@ public function processNode(Node $node, Scope $scope): array $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); + } + $returnType = TypeCombinator::union(...$returnTypes); $messages = []; diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index b73fa09641..92084ea3a0 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -82,6 +82,10 @@ public function processNode(Node $node, Scope $scope): array $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); } + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); + } + $returnType = TypeCombinator::union(...$returnTypes); if ( !$method->isPrivate() diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php index 0db93a1cfb..aaafea889d 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980-function.php @@ -46,4 +46,25 @@ function process2($tokens, $stackPtr) // Not a stand-alone statement. return $end; } + + return 1; +} + +/** @return int|void */ +function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return process_class( $code ); + } + + process_function( $code ); +} + +/** @return int */ +function process_class(int $code) { + return $code; +} + +/** @return void */ +function process_function(int $code) { } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php index 99ab186f62..d45bb08576 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11980.php @@ -49,5 +49,26 @@ public function process2($tokens, $stackPtr) // Not a stand-alone statement. return $end; } + + return 1; + } + + /** @return int|void */ + public function process3( int $code ) { + + if ( $code === \T_CLASS ) { + return $this->process_class( $code ); + } + + $this->process_function( $code ); + } + + /** @return int */ + public function process_class(int $code) { + return $code; + } + + /** @return void */ + public function process_function(int $code) { } } From 40be73a94ba6eff3e1f080826b66c99c2ec16465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sun, 10 Nov 2024 18:19:58 +0100 Subject: [PATCH 096/220] Update UPGRADING.md --- UPGRADING.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index bc65ba774e..1b76768ec4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -112,10 +112,6 @@ Appending `(?)` in `ignoreErrors` is not supported. * `constant_hassers` -> use `constantHassers` * `console_application_loader` -> use `consoleApplicationLoader` -### Docker images no longer tagged without a PHP version - -Tags without a PHP version are no longer published - `nightly`, `2`, `latest` are no longer updated. Instead, use `nightly-php8.3`, `2-php8.3`, `latest-php8.3`. You can replace `8.3` with PHP versions `8.0`-`8.3`. - ### Minor backward compatibility breaks * Removed unused config parameter `cache.nodesByFileCountMax` From eb82cb2ad17807a062fea3cd8cf31a446991a23c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 08:01:50 +0100 Subject: [PATCH 097/220] Update changelog one last time --- changelog-2.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog-2.0.md b/changelog-2.0.md index 1756a96aa0..b504a3281d 100644 --- a/changelog-2.0.md +++ b/changelog-2.0.md @@ -146,6 +146,7 @@ Improvements 🔧 * Remove inefficient caching from `PhpMethodReflection` and `PhpFunctionReflection::isVariadic()` ([#3534](https://github.com/phpstan/phpstan-src/pull/3534)), thanks @staabm! * Clean file cache from unused items (https://github.com/phpstan/phpstan-src/commit/466ad51740d629c9137a77dac28a676b71ef7197) * Journal for used generated containers (https://github.com/phpstan/phpstan-src/commit/57c65888e6372a4056afbbacc8207d411ea8559a) +* Use named argument in error for variadic types ([#3611](https://github.com/phpstan/phpstan-src/pull/3611)), thanks @ruudk! Bugfixes 🐛 ===================== @@ -210,5 +211,6 @@ Internals 🔍 * Remove $isFinal dead-code in PhpFunctionReflection ([#3545](https://github.com/phpstan/phpstan-src/pull/3545)), thanks @staabm! * Get rid of unnecessary `instanceof self` in `ConstantArrayType` ([#3552](https://github.com/phpstan/phpstan-src/pull/3552)), thanks @herndlm! * test: use `bashunit -a` exit_code to check for errors ([#3533](https://github.com/phpstan/phpstan-src/pull/3533)), thanks @Chemaclass! +* Upgrade bashunit:0.18.0 for e2e tests ([#3614](https://github.com/phpstan/phpstan-src/pull/3614)), thanks @Chemaclass! * Remove dead code ([#3575](https://github.com/phpstan/phpstan-src/pull/3575)), thanks @staabm! * Remove dead code in ConstantConditionRuleHelper ([#3597](https://github.com/phpstan/phpstan-src/pull/3597)), thanks @staabm! From 91d1b11c8740cdadea3d5f7b96af4f64c5a1f6c8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 08:44:29 +0100 Subject: [PATCH 098/220] Update PHPStan extensions --- composer.lock | 53 ++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/composer.lock b/composer.lock index 3197c29134..4a377463ad 100644 --- a/composer.lock +++ b/composer.lock @@ -4672,16 +4672,16 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "392bbe7be54b00fbe945fede6a8ef543216f3b9c" + "reference": "81833b5787e2e8f451b31218875e29e4ed600ab2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/392bbe7be54b00fbe945fede6a8ef543216f3b9c", - "reference": "392bbe7be54b00fbe945fede6a8ef543216f3b9c", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/81833b5787e2e8f451b31218875e29e4ed600ab2", + "reference": "81833b5787e2e8f451b31218875e29e4ed600ab2", "shasum": "" }, "require": { @@ -4693,7 +4693,6 @@ "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -4714,22 +4713,22 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.x" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.0" }, - "time": "2024-09-26T12:14:06+00:00" + "time": "2024-10-26T16:04:11+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "c903386c4e3d1d25a57f66458476bfb6347f1c66" + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/c903386c4e3d1d25a57f66458476bfb6347f1c66", - "reference": "c903386c4e3d1d25a57f66458476bfb6347f1c66", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/cacb6983bbdf44d5c3a7222e5ca74f61f8531806", + "reference": "cacb6983bbdf44d5c3a7222e5ca74f61f8531806", "shasum": "" }, "require": { @@ -4746,6 +4745,7 @@ }, "require-dev": { "nette/application": "^3.0", + "nette/di": "^3.1.10", "nette/forms": "^3.0", "nette/utils": "^2.3.0 || ^3.0.0", "php-parallel-lint/php-parallel-lint": "^1.2", @@ -4753,7 +4753,6 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -4775,22 +4774,22 @@ "description": "Nette Framework class reflection extension for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.x" + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.0" }, - "time": "2024-09-24T16:09:34+00:00" + "time": "2024-10-26T16:03:48+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "09e2d3b470bdda02824c626735153dfd962e3f29" + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/09e2d3b470bdda02824c626735153dfd962e3f29", - "reference": "09e2d3b470bdda02824c626735153dfd962e3f29", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/3cc855474263ad6220dfa49167cbea34ca1dd300", + "reference": "3cc855474263ad6220dfa49167cbea34ca1dd300", "shasum": "" }, "require": { @@ -4805,7 +4804,6 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -4827,22 +4825,22 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.x" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.0" }, - "time": "2024-09-24T16:07:03+00:00" + "time": "2024-10-14T03:16:27+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "e208c9311872047b903511e2e03cb0df795014b0" + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/e208c9311872047b903511e2e03cb0df795014b0", - "reference": "e208c9311872047b903511e2e03cb0df795014b0", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", "shasum": "" }, "require": { @@ -4855,7 +4853,6 @@ "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -4876,9 +4873,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.x" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" }, - "time": "2024-09-30T19:35:25+00:00" + "time": "2024-10-26T16:04:33+00:00" }, { "name": "phpunit/php-code-coverage", From ae38fe5857f403ca466907d16e1265fead5f46d0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 08:44:41 +0100 Subject: [PATCH 099/220] Remove changelog --- changelog-2.0.md | 216 ----------------------------------------------- 1 file changed, 216 deletions(-) delete mode 100644 changelog-2.0.md diff --git a/changelog-2.0.md b/changelog-2.0.md deleted file mode 100644 index b504a3281d..0000000000 --- a/changelog-2.0.md +++ /dev/null @@ -1,216 +0,0 @@ -When PHPStan 2.0 gets released, this will turn into [releases notes on GitHub](https://github.com/phpstan/phpstan/releases). - -Check out the [**UPGRADING guide**](https://github.com/phpstan/phpstan-src/blob/2.0.x/UPGRADING.md)!. - -Major new features 🚀 -===================== - -* **Level 10** - level 9 on steroids, treats all `mixed` types strictly, not just explicit `mixed` -* **Array `list` type** ([#1751](https://github.com/phpstan/phpstan-src/pull/1751)), #3311, #8185, #6243, thanks @rvanvelzen! - * Lists are arrays with sequential integer keys starting at 0 -* **Lower memory consumption** thanks to breaking up of reference cycles - * [Learn more »](https://phpstan.org/blog/preprocessing-ast-for-custom-rules) - * In testing the memory consumption was reduced by 50–70 %. -* **Enhancements in handling parameters passed by reference** - * [Learn more on phpstan.org](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) - * [#2941](https://github.com/phpstan/phpstan-src/pull/2941), thanks @ljmaskey! -* New rules (level 0): - * MagicConstantContextRule ([#2741](https://github.com/phpstan/phpstan-src/pull/2741)), #10099, thanks @staabm! - * MissingMagicSerializationMethodsRule ([#1711](https://github.com/phpstan/phpstan-src/pull/1711)), #7482, thanks @staabm! - * Check vprintf/vsprintf arguments against placeholder count ([#3126](https://github.com/phpstan/phpstan-src/pull/3126)), thanks @staabm! - * Check if required file exists ([#3294](https://github.com/phpstan/phpstan-src/pull/3294)), #3397, thanks @Bellangelo! - * Add `@readonly` rule that disallows default values ([#1391](https://github.com/phpstan/phpstan-src/pull/1391)), thanks @herndlm! - * Rule about `@phpstan-consistent-constructor` ([#1296](https://github.com/phpstan/phpstan-src/pull/1296)), thanks @canvural! - * Check code in custom PHPStan extensions for runtime reflection concepts like `is_a()` or `class_parents()` (https://github.com/phpstan/phpstan-src/commit/c4a662ac6c3ec63f063238880b243b5399c34fcc) - * Check code in custom PHPStan extensions for runtime reflection concepts like `new ReflectionMethod()` (https://github.com/phpstan/phpstan-src/commit/536306611cbb5877b6565755cd07b87f9ccfdf08) - * ApiInstanceofRule - * Report `instanceof` of classes not covered by backward compatibility promise (https://github.com/phpstan/phpstan-src/commit/ff4d02d62a7a2e2c4d928d48d31d49246dba7139) - * Report `instanceof` of classes covered by backward compatibility promise but where the assumption might change (https://github.com/phpstan/phpstan-src/commit/996bc69fa977aa64f601bd82b8a0ae39be0cbeef) - * Check that PHPStan class in class constant fetch is covered by backward compatibility promise (https://github.com/phpstan/phpstan-src/commit/9e007251ce61788f6a0319a53f1de6cf801ed233) - * Previously absent type checks: - * Check existing classes in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/6838669976bf20232abde36ecdd52b1770fa50c9) - * Check nonexistent classes in local type aliases (https://github.com/phpstan/phpstan-src/commit/2485b2e9c129e789ec3b2d7db81ca30f87c63911) - * Check unresolvable types in local type aliases (https://github.com/phpstan/phpstan-src/commit/5f7d12b2fb2809525ab0e96eeae95093204ea4d3) - * Check generics in local type aliases (https://github.com/phpstan/phpstan-src/commit/5a2d4416d94ab77a2a2e7e1bfaba4c5ed2a13c25) - * Check existing classes in `@param-out` (https://github.com/phpstan/phpstan-src/commit/30c4b9e80f51af8b5f166ba3aae93d8409c9c0ea), #10260 - * Check existing classes in `@param-closure-this` (https://github.com/phpstan/phpstan-src/commit/2fa539a39e06bcc3155b109fd8d246703ceb176d), #10933 -* New rules (level 2): - * **Validate inline PHPDoc `@var` tag** type against native type (https://github.com/phpstan/phpstan-src/commit/a69e3bc2f1e87f6da1e65d7935f1cc36bd5c42fe) - * Set [`reportWrongPhpDocTypeInVarTag`](https://phpstan.org/config-reference#reportwrongphpdoctypeinvartag) to `true` to have all types validated, not just native ones - * Use config option `reportAnyTypeWideningInVarTag: true` for stricter behaviour ([#2840](https://github.com/phpstan/phpstan-src/pull/2840)), thanks @janedbal! - * IncompatibleDefaultParameterTypeRule for closures (https://github.com/phpstan/phpstan-src/commit/0264f5bc48448c7e02a23b82eef4177d0617a82f) - * Checking truthiness of `@phpstan-pure` above functions and methods - * Check variance of template types in properties ([#2314](https://github.com/phpstan/phpstan-src/pull/2314)), thanks @jiripudil! - * Report narrowing `PHPStan\Type\Type` interface via `@var` (https://github.com/phpstan/phpstan-src/commit/713b98fb107213c28e3d8c8b4b43c5f5fc47c144), https://github.com/nunomaduro/larastan/issues/1567#issuecomment-1460445389 - * Previously absent type checks: - * Check `@mixin` PHPDoc tag above traits (https://github.com/phpstan/phpstan-src/commit/0d0de946900adf4eb3c799b1b547567536e23147) - * Check `@extends`, `@implements`, `@use` for unresolvable types (https://github.com/phpstan/phpstan-src/commit/2bb528233edb75312614166e282776f279cf2018), #11552 - * Check types in `@method` tags (https://github.com/phpstan/phpstan-src/commit/5b7e474680eaf33874b7ed6a227677adcbed9ca5) - * Check generics `@method` `@template` tags above traits (https://github.com/phpstan/phpstan-src/commit/aadbf62d3ae4517fc7a212b07130bedcef8d13ac) - * Check types in `@property` tags (https://github.com/phpstan/phpstan-src/commit/55ea2ae516df22a071ab873fdd6f748a3af0520e), #10752, #9356 -* New rule (level 3): - * ArrayUnpackingRule ([#856](https://github.com/phpstan/phpstan-src/pull/856)), thanks @canvural! -* New rules (level 4): - * Check too wide private property type (https://github.com/phpstan/phpstan-src/commit/7453f4f75fae3d635063589467842aae29d88b54) - * LogicalXorConstantConditionRule (https://github.com/phpstan/phpstan-src/commit/3a12724fd636b1bcf36c22b36e8f765d97150895, https://github.com/phpstan/phpstan-src/commit/3b011f6524254dad0f16840fdcfdbe7421548617), #7539 - * Check that each trait is used and analysed at least once (https://github.com/phpstan/phpstan-src/commit/c4d05276fb8605d6ac20acbe1cc5df31cd6c10b0) - * Report useless return values of function calls like `var_export` without `$return=true` ([#3225](https://github.com/phpstan/phpstan-src/pull/3225)), #11320, thanks @staabm! - * ConstantLooseComparisonRule (https://github.com/phpstan/phpstan-src/commit/6ebf2361a3c831dd105a815521889428c295dc9f) - * Check `new`/function call/method call/static method call on a separate line without any side effects even without `@phpstan-pure` PHPDoc tag on the declaration side - * https://github.com/phpstan/phpstan-src/commit/281a87d1ab61809076ecfa6dfc2cc86e3babe235 - * [#3020](https://github.com/phpstan/phpstan-src/pull/3020), thanks @staabm! - * [#3022](https://github.com/phpstan/phpstan-src/pull/3022), thanks @staabm! - * [#3023](https://github.com/phpstan/phpstan-src/pull/3023), thanks @staabm! - * Always report always true conditions, except for last elseif and match arm (https://github.com/phpstan/phpstan-src/commit/565fb0f6da9cdc58e8686598015561a848693972) - * Remove "unreachable branches" rules: UnreachableIfBranchesRule, UnreachableTernaryElseBranchRule, unreachable arm error in MatchExpressionRule - * Because "always true" is always reported, these are no longer needed -* New rules (level 5): - * Check preg_quote delimiter sanity ([#3252](https://github.com/phpstan/phpstan-src/pull/3252)), #11338, thanks @staabm! - * Rule for `call_user_func()` ([#2479](https://github.com/phpstan/phpstan-src/pull/2479)), thanks @staabm! - * Report useless `array_filter()` calls ([#1077](https://github.com/phpstan/phpstan-src/pull/1077)), #6840, thanks @leongersen! - * Report useless `array_values()` calls ([#2917](https://github.com/phpstan/phpstan-src/pull/2917)), thanks @kamil-zacek! - * Check array functions which require stringish values ([#3132](https://github.com/phpstan/phpstan-src/pull/3132)), #11141, #5848, #3694, #11111, thanks @schlndh! - * Check unresolvable parameters ([#1319](https://github.com/phpstan/phpstan-src/pull/1319)), thanks @rvanvelzen! - * Enforce `@no-named-arguments` (https://github.com/phpstan/phpstan-src/commit/74ba8c23696948f2647d880df72f375346f41010), #5968 -* New rules (level 6): - * Previously absent type checks: - * Check missing types in `@phpstan-self-out` (https://github.com/phpstan/phpstan-src/commit/892b319f25f04bc1b55c3d0063b607909612fe6d) - * Check missing types in local type aliases (https://github.com/phpstan/phpstan-src/commit/ce7ffaf02d624a7fb9d38f8e5dffc9739f1233fc) - * Check missing types in `@mixin` (https://github.com/phpstan/phpstan-src/commit/3175c81f26fd5bcb4a161b24e774921870ed2533) -* New option: `polluteScopeWithBlock` (defaults to `true`, `false` in `phpstan-strict-rules`) (https://github.com/phpstan/phpstan-src/commit/946cf180c960930c2c42075d0f28ff9090507272) -* Support `@readonly` property and `@immutable` class PHPDoc ([#1295](https://github.com/phpstan/phpstan-src/pull/1295), [#1335](https://github.com/phpstan/phpstan-src/pull/1335)), #4082, thanks @herndlm! -* Deprecate various `instanceof *Type` in favour of new methods on `Type` interface, (https://github.com/phpstan/phpstan-src/commit/436e6d3015cbeba4645d38bc7a6a865b9c6d7c74), learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) - - -Improvements 🔧 -===================== - -* TableErrorFormatter - always output identifiers (https://github.com/phpstan/phpstan-src/commit/fc66c24113e9fe88c3155703224eb03768846fdd) -* Config option `exceptions.check.tooWideThrowType` made true by default (https://github.com/phpstan/phpstan-src/commit/1b1da3e2ce3acf10dde03d9656638cda4f7389a4) -* Use `implicitThrows` to only look for explicit throw points in too-wide `@throws` rules when set to `false` (https://github.com/phpstan/phpstan-src/commit/a0e688c1d1e4c5e82f989b26485eb9162f47aa97) -* Rules about tooWideThrowType moved to level 4 (https://github.com/phpstan/phpstan-src/commit/d7798d7f2c47f426efe91c566e6cafd5a4e2410c) -* Both .php and .neon baselines now include error identifiers (https://github.com/phpstan/phpstan-src/commit/f38addda2b151b6e41a746a37659c0bbe9e2293b, https://github.com/phpstan/phpstan-src/commit/c8b7ea9e8f51c8bbc38dfa6b04f9a0172f5cfea0) -* PHPDoc parser: Require whitespace before description with limited start tokens (https://github.com/phpstan/phpdoc-parser/pull/128), https://github.com/phpstan/phpdoc-parser/issues/125, thanks @rvanvelzen! -* Unescape strings in PHPDoc parser (https://github.com/phpstan/phpstan-src/commit/97786ed8376b478ec541ea9df1c450c1fbfe7461) -* PHPDoc parser: add config for lines in its AST & enable ignoring errors within PHPDocs ([#2807](https://github.com/phpstan/phpstan-src/pull/2807)), thanks @janedbal! -* InvalidPhpDocTagValueRule: include PHPDoc line number in the error message (https://github.com/phpstan/phpstan-src/commit/a04e0be832900749b5b4ba22e2de21db8bfa09a0) -* No implicit wildcard in FileExcluder (https://github.com/phpstan/phpstan-src/commit/e19e6e5f8cfa706cc30e44a17276a6bc269f995c), #10299 -* Report invalid exclude paths in PHP config (https://github.com/phpstan/phpstan-src/commit/9718c14f1ffac81ba3d2bf331b4e8b4041a4d004) -* Do not generalize template types, except when in `GenericObjectType` ([#2818](https://github.com/phpstan/phpstan-src/pull/2818), [#2821](https://github.com/phpstan/phpstan-src/pull/2821)) - * This fixes following **20 issues**: #8166, #8127, #7944, #7283, #6653, #6196, #9084, #8683, #8074, #7984, #7301, #7087, #5594, #5592, #9472, #9764, #10092, #11126, #11032, #10653 -* Non-static methods cannot be used as static callables in PHP 8+ ([#2420](https://github.com/phpstan/phpstan-src/pull/2420)), thanks @staabm! -* Analysis with zero files results in non-zero exit code (https://github.com/phpstan/phpstan-src/commit/46ff440648e62617df86aa74ba905ffa99897737), #9410 -* Fail build when project config uses custom extensions outside of analysed paths - * This will only occur after a run that uses already present and valid result cache -* Returning plain strings as errors no longer supported, use RuleErrorBuilder - * Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) -* Require identifier in custom rules (https://github.com/phpstan/phpstan-src/commit/969e6fa31d5484d42dab902703cfc6820a983cfd) -* New `RuleLevelHelper::accepts()` behaviour (https://github.com/phpstan/phpstan-src/commit/941fc815db49315b8783dc466cf593e0d8a85d23), #11119, #4174 -* Infer explicit mixed when instantiating generic class with unknown template types (https://github.com/phpstan/phpstan-src/commit/089d4c6fb6eb709c44123548d33990113d174b86), #6398 -* Use explicit mixed for global array variables ([#1411](https://github.com/phpstan/phpstan-src/pull/1411)), #7082, thanks @herndlm! -* Consider implicit throw points when the only explicit one is `Throw_` (https://github.com/phpstan/phpstan-src/commit/22eef6d5ab9a4afafb2305258fea273be6cc06e4), #4912 -* Run missing type check on `@param-out` (https://github.com/phpstan/phpstan-src/commit/56b20024386d983927c64dfa895ff026bed2798c) -* Report "missing return" error closer to where the return is missing (https://github.com/phpstan/phpstan-src/commit/04f8636e6577cbcaefc944725eed74c0d7865ead) -* Report dead types even in multi-exception catch ([#2399](https://github.com/phpstan/phpstan-src/pull/2399)), thanks @JanTvrdik! -* MethodSignatureRule - look at abstract trait method (https://github.com/phpstan/phpstan-src/commit/5fd8cee591ce1b07daa5f98a1ddcdfc723f1b5eb) -* OverridingMethodRule - include template types in prototype declaring class description (https://github.com/phpstan/phpstan-src/commit/ca2c66cc4dff59ba44d52b82cb9e0aa3256240f3) -* Detect overriding `@final` method in OverridingMethodRule, #9135 -* Improve error wording of the NonexistentOffset, BooleanAndConstantConditionRule, and BooleanOrConstantConditionRule ([#1882](https://github.com/phpstan/phpstan-src/pull/1882)), thanks @VincentLanglet! -* Stricter ++/-- operator check ([#3255](https://github.com/phpstan/phpstan-src/pull/3255)), thanks @schlndh! -* Check mixed in binary operator ([#3231](https://github.com/phpstan/phpstan-src/pull/3231)), #7538, #10440, thanks @schlndh! -* Check mixed in unary operator ([#3253](https://github.com/phpstan/phpstan-src/pull/3253)), thanks @schlndh! -* Stub files validation - detect duplicate classes and functions (https://github.com/phpstan/phpstan-src/commit/ddf8d5c3859c2c75c20f525a0e2ca8b99032373a, https://github.com/phpstan/phpstan-src/commit/17e4b74335e5235d7cd6708eb687a774a0eeead4) -* NoopRule - take advantage of impure points (https://github.com/phpstan/phpstan-src/commit/a6470521b65d7424f552633c1f3827704c6262c3), #10389 -* Improve impossible type checker for void-returning functions ([#1857](https://github.com/phpstan/phpstan-src/pull/1857)), #8169, thanks @rvanvelzen! -* Check template type variance in `@param-out` (https://github.com/phpstan/phpstan-src/commit/7ceb19d3b42cf4632d10c2babb0fc5a21b6c8352), https://github.com/phpstan/phpstan/issues/8880#issuecomment-1426971473 -* Fix position variance of static method parameters ([#2313](https://github.com/phpstan/phpstan-src/pull/2313)), thanks @jiripudil! -* Empty `skipCheckGenericClasses` (https://github.com/phpstan/phpstan-src/commit/28c2c79b16cac6ba6b01f1b4d211541dd49d8a77) -* Report unnecessary nullsafe property fetch inside `??` / `isset` / `empty` with different message ([#1253](https://github.com/phpstan/phpstan-src/pull/1253)), thanks @rajyan! -* Specify explicit mixed array type via `is_array` ([#1191](https://github.com/phpstan/phpstan-src/pull/1191)), thanks @herndlm! -* TooWideMethodReturnTypehintRule - always report for final methods (https://github.com/phpstan/phpstan-src/commit/c30e9a484c8245b8126cd63444607ca74d2af761) -* Move IllegalConstructorMethodCallRule and IllegalConstructorStaticCallRule to phpstan-strict-rules (https://github.com/phpstan/phpstan-src/commit/124b30f98c182193187be0b9c2e151e477429b7a, https://github.com/phpstan/phpstan-strict-rules/commit/0c82c96f2a55d8b91bbc7ee6512c94f68a206b43) -* Check invalid PHPDocs in previously unchecked statement types (https://github.com/phpstan/phpstan-src/commit/9780d352f3264aac09ac7954f691de1877db8e01) -* InvalidPHPStanDocTagRule in StubValidator (https://github.com/phpstan/phpstan-src/commit/9c2552b7e744926d1a74c1ba8fd32c64079eed61) -* CallToConstructorStatementWithoutSideEffectsRule - report class with no constructor (https://github.com/phpstan/phpstan-src/commit/b116d25a6e4ba6c09f59af6569d9e6f6fd20aff4) -* ContainerFactory - always check duplicate files (https://github.com/phpstan/phpstan-src/commit/939a715a0636ed05752659dbe7646c1f1a574765) -* Display parent class name for anonymous class like native PHP does ([#3362](https://github.com/phpstan/phpstan-src/pull/3362)), thanks @mvorisek! -* Always report static property fetch in `isset()`, not just on PHP 8.2+ ([#3476](https://github.com/phpstan/phpstan-src/pull/3476)), thanks @ondrejmirtes! -* Revert "Dumb down parameter types in some recently added stubs" (https://github.com/phpstan/phpstan-src/commit/950a491485c46068074ca3f4f6dc5b970d41465a) -* Do not apply heuristics of `Collection<...>|Foo[]` being resolved to Collection of Foo (https://github.com/phpstan/phpstan-src/commit/fff8f095988a66f298aa4037fe8e6ba98266063c) -* Collected PHP errors cannot be ignored (https://github.com/phpstan/phpstan-src/commit/1d3f4313955dc6fa5c6ce60fa58afe765964e5b0) -* Added missing rules to StubValidator (https://github.com/phpstan/phpstan-src/commit/bf19914cac1682d0eab8bf65a874ba368522311c) -* Report precise offsets in errors ([#3504](https://github.com/phpstan/phpstan-src/pull/3504)), thanks @ruudk! -* IntersectionType - always describe list as list (https://github.com/phpstan/phpstan-src/commit/f680629bc92e4dd5d7acd3bc60c9539fb047452b) -* ArrayType::describe - explicit mixed should be stated explicitly (https://github.com/phpstan/phpstan-src/commit/6cf223840f89c972551f373ade9eea16d12e143b) -* Refactor IntersectionType::describe() (https://github.com/phpstan/phpstan-src/commit/67fbfaee6585c2d47485dc2a159ee76d3ed02b35) -* Remove inefficient caching from `PhpMethodReflection` and `PhpFunctionReflection::isVariadic()` ([#3534](https://github.com/phpstan/phpstan-src/pull/3534)), thanks @staabm! -* Clean file cache from unused items (https://github.com/phpstan/phpstan-src/commit/466ad51740d629c9137a77dac28a676b71ef7197) -* Journal for used generated containers (https://github.com/phpstan/phpstan-src/commit/57c65888e6372a4056afbbacc8207d411ea8559a) -* Use named argument in error for variadic types ([#3611](https://github.com/phpstan/phpstan-src/pull/3611)), thanks @ruudk! - -Bugfixes 🐛 -===================== - -* Fix invariance composition ([#2054](https://github.com/phpstan/phpstan-src/pull/2054)), thanks @jiripudil! -* Fix checking generic `mixed` type based on config ([#2885](https://github.com/phpstan/phpstan-src/pull/2885)), thanks @schlndh! - - -Function signature fixes 🤖 -======================= - -* Countable stub with `0|positive-int` ([#1027](https://github.com/phpstan/phpstan-src/pull/1027)), thanks @staabm! -* More precise types for bcmath function parameters ([#2217](https://github.com/phpstan/phpstan-src/pull/2217)), thanks @Warxcell! -* Specify `Imagick` parameter types ([#2334](https://github.com/phpstan/phpstan-src/pull/2334)), thanks @zonuexe! -* `max()`/`min()` should expect non-empty-array ([#2163](https://github.com/phpstan/phpstan-src/pull/2163)), thanks @staabm! -* Narrow `Closure::bind` `$newScope` param ([#2817](https://github.com/phpstan/phpstan-src/pull/2817)), thanks @mvorisek! -* `error_log` errors with `message_type=2` ([#2428](https://github.com/phpstan/phpstan-src/pull/2428)), #9380, thanks @staabm! -* Update functionMap ([#2699](https://github.com/phpstan/phpstan-src/pull/2699), [#2783](https://github.com/phpstan/phpstan-src/pull/2783)), thanks @zonuexe! -* Improve image related functions signature ([#3127](https://github.com/phpstan/phpstan-src/pull/3127)), thanks @thg2k! -* Support `FILE_NO_DEFAULT_CONTEXT` in `file()` ([#2482](https://github.com/phpstan/phpstan-src/pull/2482)), thanks @staabm! -* Fix ftp related function signatures ([#2551](https://github.com/phpstan/phpstan-src/pull/2551)), thanks @thg2k! -* More precise `file()` flags args ([#2476](https://github.com/phpstan/phpstan-src/pull/2476), [#2482](https://github.com/phpstan/phpstan-src/pull/2482)), thanks @staabm! -* More precise `flock()` operation flags ([#2477](https://github.com/phpstan/phpstan-src/pull/2477)), thanks @staabm! -* More precise `stream_socket_client()` signature ([#2519](https://github.com/phpstan/phpstan-src/pull/2519)), thanks @staabm! -* More precise `scandir()` signature ([#2518](https://github.com/phpstan/phpstan-src/pull/2518)), thanks @staabm! -* More precise `extract()` signature ([#2517](https://github.com/phpstan/phpstan-src/pull/2517)), thanks @staabm! -* More precise `RecursiveIteratorIterator::__construct()` parameter types ([#2835](https://github.com/phpstan/phpstan-src/pull/2835)), thanks @staabm! -* Update `Locale` signatures ([#2880](https://github.com/phpstan/phpstan-src/pull/2880)), thanks @devnix! -* Improved the type of the `$mode` parameter for the `count()` ([#3190](https://github.com/phpstan/phpstan-src/pull/3190)), thanks @kuma3!* Check `filter_input*` type param type ([#2271](https://github.com/phpstan/phpstan-src/pull/2271)), thanks @herndlm! -* Change `curl_setopt` function signature based on 2nd arg ([#1719](https://github.com/phpstan/phpstan-src/pull/1719)), thanks @staabm! -* Support returning an array or a string in `count_chars()` ([#3596](https://github.com/phpstan/phpstan-src/pull/3596)), thanks @u01jmg3! -* xdebug_get_function_stack: fix signature ([#3605](https://github.com/phpstan/phpstan-src/pull/3605)), thanks @janedbal! - - -Internals 🔍 -===================== - -* Tool to make optional parameters required across the codebase (https://github.com/phpstan/phpstan-src/commit/7e366e08f96e2e4095b3f02b5487e8f9531f37bf) -* A few more MutatingScope method parameters made required (https://github.com/phpstan/phpstan-src/commit/2c4c0cde75e637ac323e81def57d4a2ace952429) -* CommandHelper::begin() parameters made required (https://github.com/phpstan/phpstan-src/commit/f17cf9ec43111cb29dd50d620fb6259c0ab0d373) -* MethodTag - constructor parameter `$templateTags` is required (https://github.com/phpstan/phpstan-src/commit/5b58f83e6d8b5044d742caed9729d00178c4a9de) -* InitializerExprTypeResolver - constructor parameter `$usePathConstantsAsConstantString` made required (https://github.com/phpstan/phpstan-src/commit/f88d9ba7f56ef6c3b783aee1c909a3422c0ef3c3) -* `PhpMethodReflectionFactory::create()` - all parameters are required (https://github.com/phpstan/phpstan-src/commit/8bfbf8f254a68e4f1b15419eb950ea677fc2916e) -* FunctionCallParametersCheck - parameters `$nodeType` and `$acceptsNamedArguments` made required (https://github.com/phpstan/phpstan-src/commit/493752737c32eb878de4dfb91817761b952348e4) -* MethodParameterComparisonHelper - parameter `$ignorable` of `compare()` method made required (https://github.com/phpstan/phpstan-src/commit/f85a500288b0b8ef9a19d405c0e3d99ab57ce797) -* Parameter `$dateTimeClass` of DateTimeModifyReturnTypeExtension constructor made required (https://github.com/phpstan/phpstan-src/commit/a8cd423e842deaa7d924580665207a4b1a373115) -* NativeFunctionReflection construct parameters made required (https://github.com/phpstan/phpstan-src/commit/64ff598cd42268d2178d02efd208afe637060978) -* Cover AccessoryArrayListType constructor with BC promise (https://github.com/phpstan/phpstan-src/commit/51de9032c6e98bff2d6eb0e5b7295720ec0276b9) -* Add `PhpVersion` parameter to various `Type` methods ([#3478](https://github.com/phpstan/phpstan-src/pull/3478)), thanks @VincentLanglet! -* Move ContainerDynamicReturnTypeExtension to build/PHPStan (https://github.com/phpstan/phpstan-src/commit/5651bec661582b2d62de1b4ae9d5f27e69e3c524) -* Renamed NewOptimizedDirectorySourceLocator to OptimizedDirectorySourceLocator (https://github.com/phpstan/phpstan-src/commit/db02a30ca11c7b9839c30e0321ed403dd14f6c73) -* Remove unneded abstraction (https://github.com/phpstan/phpstan-src/commit/f302c9069274afa63ec1b4f313ca72340699e9d8) -* Introduce native return types thanks to PHP 7.4 return type covariance (https://github.com/phpstan/phpstan-src/commit/392f090066bfc9946b4ad524ffecf3d420c23114) -* ReadWritePropertiesExtension - use ExtendedPropertyReflection in parameter type (https://github.com/phpstan/phpstan-src/commit/f0a629685de2202687b9f92bd0e1a516daf2443e) -* Declare more precise `getClass()` return types in extension interfaces ([#1754](https://github.com/phpstan/phpstan-src/pull/1754)), thanks @staabm! -* (https://github.com/phpstan/phpstan-src/commit/38cb5a315e5573231d8695df343c8ee87a8c3b2e) -* HasOffsetType - put constructor parameter type natively (https://github.com/phpstan/phpstan-src/commit/b5accb3f6bbcffc8a44934539b88903e09b6a174) -* Printer is covered by BC promise (https://github.com/phpstan/phpstan-src/commit/b0858332efc7aa2f2fde7544a2a821ba81bde13b) -* More interfaces that are not supposed to be implemented in userland (https://github.com/phpstan/phpstan-src/commit/778af2ed74ba59bfb2a69fd5b45821ccdb1107c9, https://github.com/phpstan/phpstan-src/commit/cb6ab5544a016c52f931fc390bcdf9c627819d8f) -* Refactored `FunctionCallParametersCheck::check()` parameters (https://github.com/phpstan/phpstan-src/commit/710e09c41698efb1d8d3ae31791944077dbb9cc1) -* Spread list usages in Reflection, Scope, Type ([#3530](https://github.com/phpstan/phpstan-src/pull/3530)), thanks @janedbal! -* Remove $isFinal dead-code in PhpFunctionReflection ([#3545](https://github.com/phpstan/phpstan-src/pull/3545)), thanks @staabm! -* Get rid of unnecessary `instanceof self` in `ConstantArrayType` ([#3552](https://github.com/phpstan/phpstan-src/pull/3552)), thanks @herndlm! -* test: use `bashunit -a` exit_code to check for errors ([#3533](https://github.com/phpstan/phpstan-src/pull/3533)), thanks @Chemaclass! -* Upgrade bashunit:0.18.0 for e2e tests ([#3614](https://github.com/phpstan/phpstan-src/pull/3614)), thanks @Chemaclass! -* Remove dead code ([#3575](https://github.com/phpstan/phpstan-src/pull/3575)), thanks @staabm! -* Remove dead code in ConstantConditionRuleHelper ([#3597](https://github.com/phpstan/phpstan-src/pull/3597)), thanks @staabm! From 6b52c1dda46ce625de4375f1a3d39c54aa5d6b4b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 08:44:51 +0100 Subject: [PATCH 100/220] Update issue-bot --- issue-bot/composer.lock | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/issue-bot/composer.lock b/issue-bot/composer.lock index be0c051842..b4abdf4947 100644 --- a/issue-bot/composer.lock +++ b/issue-bot/composer.lock @@ -1403,16 +1403,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f347f223a7235178f056f34dc104557095998614" + "reference": "72115ab2bf1e40af1f9b238938d493ba7f3221e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f347f223a7235178f056f34dc104557095998614", - "reference": "f347f223a7235178f056f34dc104557095998614", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/72115ab2bf1e40af1f9b238938d493ba7f3221e7", + "reference": "72115ab2bf1e40af1f9b238938d493ba7f3221e7", "shasum": "" }, "require": { @@ -1421,7 +1421,6 @@ "conflict": { "phpstan/phpstan-shim": "*" }, - "default-branch": true, "bin": [ "phpstan", "phpstan.phar" @@ -1458,20 +1457,20 @@ "type": "github" } ], - "time": "2024-09-30T19:33:02+00:00" + "time": "2024-11-11T07:06:55+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.x-dev", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "e208c9311872047b903511e2e03cb0df795014b0" + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/e208c9311872047b903511e2e03cb0df795014b0", - "reference": "e208c9311872047b903511e2e03cb0df795014b0", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", + "reference": "a4a6a08bd4a461e516b9c3b8fdbf0f1883b34158", "shasum": "" }, "require": { @@ -1484,7 +1483,6 @@ "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.6" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -1505,9 +1503,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.x" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.0" }, - "time": "2024-09-30T19:35:25+00:00" + "time": "2024-10-26T16:04:33+00:00" }, { "name": "psr/cache", @@ -4471,12 +4469,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } From 6517446a99d70c75015e6fc2990adba63a662913 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 08:45:41 +0100 Subject: [PATCH 101/220] Upgrading guide will only be in phpstan/phpstan --- .github/workflows/phar.yml | 3 - UPGRADING.md | 338 ------------------------------------- 2 files changed, 341 deletions(-) delete mode 100644 UPGRADING.md diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 28d8d8b36b..dcc27d6be7 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -211,9 +211,6 @@ jobs: env: GPG_ID: ${{ steps.import-gpg.outputs.fingerprint }} - - name: "cp UPGRADING.md" - run: cp phpstan-src/UPGRADING.md phpstan-dist/UPGRADING.md - - name: "Verify PHAR" working-directory: phpstan-dist run: "gpg --verify phpstan.phar.asc" diff --git a/UPGRADING.md b/UPGRADING.md deleted file mode 100644 index 1b76768ec4..0000000000 --- a/UPGRADING.md +++ /dev/null @@ -1,338 +0,0 @@ -Upgrading from PHPStan 1.x to 2.0 -================================= - -## PHP version requirements - -PHPStan now requires PHP 7.4 or newer to run. - -## Upgrading guide for end users - -The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release** -and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users. - -Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules). - -Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`: - -```json -"require-dev": { - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0", - "phpstan/phpstan-doctrine": "^2.0", - "phpstan/phpstan-nette": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpstan/phpstan-symfony": "^2.0", - "phpstan/phpstan-webmozart-assert": "^2.0", - ... -} -``` - -Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well. - -After changing your `composer.json`, run `composer update 'phpstan/*' -W`. - -It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0. - -### Noteworthy changes to code analysis - -* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) -* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type) -* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) -* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported. - -### Removed option `checkMissingIterableValueType` - -It's strongly recommended to add the missing array typehints. - -If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`: - -```neon -parameters: - ignoreErrors: - - - identifier: missingType.iterableValue -``` - -### Removed option `checkGenericClassInNonGenericObjectType` - -It's strongly recommended to add the missing generic typehints. - -If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`: - -```neon -parameters: - ignoreErrors: - - - identifier: missingType.generics -``` - -### Removed `checkAlwaysTrue*` options - -These options have been removed because PHPStan now always behaves as if these were set to `true`: - -* `checkAlwaysTrueCheckTypeFunctionCall` -* `checkAlwaysTrueInstanceof` -* `checkAlwaysTrueStrictComparison` -* `checkAlwaysTrueLooseComparison` - -### Removed option `excludes_analyse` - -It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files). - -### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern - -If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`: - -```neon -parameters: - excludePaths: - - tests/*/data/* - - src/broken - - node_modules (?) # optional path, might not exist -``` - -If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`. - -```neon -parameters: - reportUnmatchedIgnoredErrors: false -``` - -Appending `(?)` in `ignoreErrors` is not supported. - -### Changes in 1st party PHPStan extensions - -* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) - * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) - * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) -* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) - * Removed legacy options with `_` in the name - * `container_xml_path` -> use `containerXmlPath` - * `constant_hassers` -> use `constantHassers` - * `console_application_loader` -> use `consoleApplicationLoader` - -### Minor backward compatibility breaks - -* Removed unused config parameter `cache.nodesByFileCountMax` -* Removed unused config parameter `memoryLimitFile` -* Removed unused feature toggle `disableRuntimeReflectionProvider` -* Removed unused config parameter `staticReflectionClassNamePatterns` -* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead -* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead -* `additionalConfigFiles` config parameter must be a list - -## Upgrading guide for extension developers - -> [!NOTE] -> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code. -> -> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version. - -### PHPStan now uses nikic/php-parser v5 - -See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser. - -The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class. - -Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The -`Stmt\Throw_` class has been removed. - -### PHPStan now uses phpstan/phpdoc-parser v2 - -See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser. - -### Returning plain strings as errors no longer supported, use RuleErrorBuilder - -Identifiers are also required in custom rules. - -Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) - -**Before**: - -```php -return ['My error']; -``` - -**After**: - -```php -return [ - RuleErrorBuilder::message('My error') - ->identifier('my.error') - ->build(), -]; -``` - -### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface - -Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) - -### Removed deprecated `ParametersAcceptorSelector::selectSingle()` - -Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions. - -**Before**: - -```php -$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); -``` - -**After**: - -```php -$defaultReturnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->getArgs(), - $functionReflection->getVariants() -)->getReturnType(); -``` - -If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object: - -* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html) -* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html) -* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html) -* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html) -* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction) - -**Before**: - -```php -$function = $node->getFunctionReflection(); -$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); -``` - -**After**: - -```php -$returnType = $node->getFunctionReflection()->getReturnType(); -``` - -### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters - -[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required): - -* `Expr $expr` -* `Type $type` -* `TypeSpecifierContext $context` -* `Scope $scope` - -If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable). - -[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts: - -* `array $sureTypes` -* `array $sureNotTypes` - -If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable). - -### `ConstantArrayType` no longer extends `ArrayType` - -`Type::getArrays()` now returns `list`. - -Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal. - -### Changed `TypeSpecifier::specifyTypesInCondition()` - -This method now longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable). - -### Node attributes `parent`, `previous`, `next` are no longer available - -Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules - -### Removed config parameter `scopeClass` - -As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service. - -### Removed `PHPStan\Broker\Broker` - -Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead. - -`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead. - -Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`. - -### List type is enabled for everyone - -Removed static methods from `AccessoryArrayListType` class: - -* `isListTypeEnabled()` -* `setListTypeEnabled()` -* `intersectWith()` - -Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`. - -### Minor backward compatibility breaks - -* Classes that were previously `@final` were made `final` -* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required -* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required -* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties) -* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. -* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. -* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes) -* Remove `ArrayType::generalizeKeys()` -* Remove `ArrayType::count()`, use `Type::getArraySize()` instead -* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead -* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead -* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead -* Remove unused `PHPStanTestCase::$useStaticReflectionProvider` -* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead -* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead -* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead -* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()` -* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead -* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead -* Rename `Type::isClassStringType()` to `Type::isClassString()` -* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead -* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead -* Remove `ConstantArrayType::getNextAutoIndex()` -* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type` - * Use `getFirstIterable*Type` and `getLastIterable*Type` instead -* Remove `ConstantArrayType::generalizeToArray()` -* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead -* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead -* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead -* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead -* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead -* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead -* Made `TypeUtils` thinner by removing methods: - * Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead - * Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead - * Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead - * Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) - * Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead - * Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead - * Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead - * Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead - * Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead -* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead -* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool` -* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list`, no longer `int` -* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead -* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool` -* Remove `FunctionReflection::isFinal()` -* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html) -* Remove `__set_state()` on objects that should not be serialized in cache -* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string` -* `LevelsTestCase::dataTopics()` data provider made static -* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint -* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) -* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) -Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) -* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) -* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) -* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) -* Changes around `ClassConstantReflection` - * Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection` - * Interface `ConstantReflection` renamed to `ClassConstantReflection` - * Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection` - * Interface `GlobalConstantReflection` renamed to `ConstantReflection` -* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*` - * `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor` - * `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection` - * `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant` -* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` -* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node - * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node From 1f411b96eb130636d3cb3f663c29e8674fc2129a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 09:17:29 +0100 Subject: [PATCH 102/220] Revert "Upgrading guide will only be in phpstan/phpstan" This reverts commit 6517446a99d70c75015e6fc2990adba63a662913. --- UPGRADING.md | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 UPGRADING.md diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000000..1b76768ec4 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,338 @@ +Upgrading from PHPStan 1.x to 2.0 +================================= + +## PHP version requirements + +PHPStan now requires PHP 7.4 or newer to run. + +## Upgrading guide for end users + +The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release** +and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users. + +Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules). + +Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`: + +```json +"require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-nette": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + ... +} +``` + +Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well. + +After changing your `composer.json`, run `composer update 'phpstan/*' -W`. + +It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0. + +### Noteworthy changes to code analysis + +* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference) +* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type) +* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) +* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported. + +### Removed option `checkMissingIterableValueType` + +It's strongly recommended to add the missing array typehints. + +If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.iterableValue +``` + +### Removed option `checkGenericClassInNonGenericObjectType` + +It's strongly recommended to add the missing generic typehints. + +If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`: + +```neon +parameters: + ignoreErrors: + - + identifier: missingType.generics +``` + +### Removed `checkAlwaysTrue*` options + +These options have been removed because PHPStan now always behaves as if these were set to `true`: + +* `checkAlwaysTrueCheckTypeFunctionCall` +* `checkAlwaysTrueInstanceof` +* `checkAlwaysTrueStrictComparison` +* `checkAlwaysTrueLooseComparison` + +### Removed option `excludes_analyse` + +It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files). + +### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern + +If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`: + +```neon +parameters: + excludePaths: + - tests/*/data/* + - src/broken + - node_modules (?) # optional path, might not exist +``` + +If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`. + +```neon +parameters: + reportUnmatchedIgnoredErrors: false +``` + +Appending `(?)` in `ignoreErrors` is not supported. + +### Changes in 1st party PHPStan extensions + +* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine) + * Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`) + * Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`) +* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony) + * Removed legacy options with `_` in the name + * `container_xml_path` -> use `containerXmlPath` + * `constant_hassers` -> use `constantHassers` + * `console_application_loader` -> use `consoleApplicationLoader` + +### Minor backward compatibility breaks + +* Removed unused config parameter `cache.nodesByFileCountMax` +* Removed unused config parameter `memoryLimitFile` +* Removed unused feature toggle `disableRuntimeReflectionProvider` +* Removed unused config parameter `staticReflectionClassNamePatterns` +* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead +* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead +* `additionalConfigFiles` config parameter must be a list + +## Upgrading guide for extension developers + +> [!NOTE] +> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code. +> +> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version. + +### PHPStan now uses nikic/php-parser v5 + +See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser. + +The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class. + +Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The +`Stmt\Throw_` class has been removed. + +### PHPStan now uses phpstan/phpdoc-parser v2 + +See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser. + +### Returning plain strings as errors no longer supported, use RuleErrorBuilder + +Identifiers are also required in custom rules. + +Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder) + +**Before**: + +```php +return ['My error']; +``` + +**After**: + +```php +return [ + RuleErrorBuilder::message('My error') + ->identifier('my.error') + ->build(), +]; +``` + +### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface + +Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) + +### Removed deprecated `ParametersAcceptorSelector::selectSingle()` + +Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions. + +**Before**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); +``` + +**After**: + +```php +$defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() +)->getReturnType(); +``` + +If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object: + +* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html) +* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html) +* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html) +* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html) +* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction) + +**Before**: + +```php +$function = $node->getFunctionReflection(); +$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); +``` + +**After**: + +```php +$returnType = $node->getFunctionReflection()->getReturnType(); +``` + +### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters + +[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required): + +* `Expr $expr` +* `Type $type` +* `TypeSpecifierContext $context` +* `Scope $scope` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable). + +[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts: + +* `array $sureTypes` +* `array $sureNotTypes` + +If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable). + +### `ConstantArrayType` no longer extends `ArrayType` + +`Type::getArrays()` now returns `list`. + +Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal. + +### Changed `TypeSpecifier::specifyTypesInCondition()` + +This method now longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable). + +### Node attributes `parent`, `previous`, `next` are no longer available + +Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules + +### Removed config parameter `scopeClass` + +As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service. + +### Removed `PHPStan\Broker\Broker` + +Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead. + +`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead. + +Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`. + +### List type is enabled for everyone + +Removed static methods from `AccessoryArrayListType` class: + +* `isListTypeEnabled()` +* `setListTypeEnabled()` +* `intersectWith()` + +Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`. + +### Minor backward compatibility breaks + +* Classes that were previously `@final` were made `final` +* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required +* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required +* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties) +* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument. +* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes) +* Remove `ArrayType::generalizeKeys()` +* Remove `ArrayType::count()`, use `Type::getArraySize()` instead +* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead +* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead +* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead +* Remove unused `PHPStanTestCase::$useStaticReflectionProvider` +* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead +* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead +* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead +* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()` +* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead +* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead +* Rename `Type::isClassStringType()` to `Type::isClassString()` +* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead +* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead +* Remove `ConstantArrayType::getNextAutoIndex()` +* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type` + * Use `getFirstIterable*Type` and `getLastIterable*Type` instead +* Remove `ConstantArrayType::generalizeToArray()` +* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead +* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead +* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead +* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead +* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead +* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead +* Made `TypeUtils` thinner by removing methods: + * Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead + * Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead + * Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead + * Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) + * Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead + * Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead + * Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead + * Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead + * Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead +* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead +* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool` +* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list`, no longer `int` +* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead +* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool` +* Remove `FunctionReflection::isFinal()` +* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html) +* Remove `__set_state()` on objects that should not be serialized in cache +* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string` +* `LevelsTestCase::dataTopics()` data provider made static +* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint +* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html) +* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html) +* Changes around `ClassConstantReflection` + * Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection` + * Interface `ConstantReflection` renamed to `ClassConstantReflection` + * Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection` + * Interface `GlobalConstantReflection` renamed to `ConstantReflection` +* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*` + * `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor` + * `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection` + * `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant` +* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` +* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node + * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node From edcc9e8a875990913769cc355d2a775543da16a2 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Mon, 11 Nov 2024 10:15:15 +0100 Subject: [PATCH 103/220] fix: check for existence of second arg in CountCharsFunctionDynamicReturnTypeExtension --- .../Php/CountCharsFunctionDynamicReturnTypeExtension.php | 7 +++++-- tests/PHPStan/Analyser/nsrt/count-chars-7.4.php | 1 + tests/PHPStan/Analyser/nsrt/count-chars-8.0.php | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php index cddb7e7989..e798af6bbd 100644 --- a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; @@ -35,11 +36,13 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 1) { + $args = $functionCall->getArgs(); + + if (count($args) < 1) { return null; } - $modeType = $scope->getType($functionCall->getArgs()[1]->value); + $modeType = count($args) === 2 ? $scope->getType($args[1]->value) : new ConstantIntegerType(0); if (IntegerRangeType::fromInterval(0, 2)->isSuperTypeOf($modeType)->yes()) { $arrayType = new ArrayType(new IntegerType(), new IntegerType()); diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php index 713d73a5e1..76e0bea581 100644 --- a/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php +++ b/tests/PHPStan/Analyser/nsrt/count-chars-7.4.php @@ -8,6 +8,7 @@ class X { const ABC = 'abcdef'; function doFoo(): void { + assertType('array|false', count_chars(self::ABC)); assertType('array|false', count_chars(self::ABC, 0)); assertType('array|false', count_chars(self::ABC, 1)); assertType('array|false', count_chars(self::ABC, 2)); diff --git a/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php index 88a165e931..6e0af08f03 100644 --- a/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php +++ b/tests/PHPStan/Analyser/nsrt/count-chars-8.0.php @@ -8,6 +8,7 @@ class Y { const ABC = 'abcdef'; function doFoo(): void { + assertType('array', count_chars(self::ABC)); assertType('array', count_chars(self::ABC, 0)); assertType('array', count_chars(self::ABC, 1)); assertType('array', count_chars(self::ABC, 2)); From d6412b8f9fa7f0900df5d05ca49e28aee567d688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Mon, 11 Nov 2024 13:54:39 +0100 Subject: [PATCH 104/220] ClassReflection: resolve missing template type to its default (if set) rather than bound --- src/Reflection/ClassReflection.php | 2 +- src/Type/Generic/TemplateTypeHelper.php | 11 ++++++++ src/Type/GenericTypeVariableResolver.php | 2 +- src/Type/ObjectType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-11899.php | 34 +++++++++++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11899.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index c79f2a6bed..ff45172fd8 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1374,7 +1374,7 @@ public function getActiveTemplateTypeMap(): TemplateTypeMap if ($type instanceof ErrorType) { $templateType = $templateTypeMap->getType($name); if ($templateType !== null) { - return TemplateTypeHelper::resolveToBounds($templateType); + return TemplateTypeHelper::resolveToDefaults($templateType); } } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 58460efaee..29ecd48720 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -68,6 +68,17 @@ public static function resolveTemplateTypes( }); } + public static function resolveToDefaults(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof TemplateType) { + return $traverse($type->getDefault() ?? $type->getBound()); + } + + return $traverse($type); + }); + } + public static function resolveToBounds(Type $type): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php index 8c3f7d2da5..eee42d0f1b 100644 --- a/src/Type/GenericTypeVariableResolver.php +++ b/src/Type/GenericTypeVariableResolver.php @@ -44,7 +44,7 @@ public static function getType( return new MixedType(false); } - return $bound; + return TemplateTypeHelper::resolveToDefaults($templateType); } return $type; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 3e8ac1371f..7427aa167c 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -841,7 +841,7 @@ public function getTemplateType(string $ancestorClassName, string $templateTypeN return new MixedType(false); } - return $bound; + return TemplateTypeHelper::resolveToDefaults($templateType); } return $type; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11899.php b/tests/PHPStan/Analyser/nsrt/bug-11899.php new file mode 100644 index 0000000000..c56b47dfcb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11899.php @@ -0,0 +1,34 @@ +test); +} + +/** + * @param UserTest $ut + */ +function acceptUserTest2(UserTest $ut) : void { + assertType('Bug11899\\UserTest', $ut); + assertType('Bug11899\\InvertedQuestions|null', $ut->test); +} From 753fc4d98fe8929aa8816f454d2f9a836ccd7a6b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 15:16:38 +0100 Subject: [PATCH 105/220] Fix resolving tentative return type --- .../Native/NativeMethodReflection.php | 2 +- src/Reflection/Php/PhpMethodReflection.php | 2 +- .../Methods/OverridingMethodRuleTest.php | 6 ++++++ .../Methods/data/simple-xml-element-child.php | 20 +++++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 8f1e21d7c9..731d4973fe 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -81,7 +81,7 @@ public function getPrototype(): ClassMemberReflection $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), null, $prototypeDeclaringClass); } return new MethodPrototypeReflection( diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 53f5d80054..d32c16e75e 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -118,7 +118,7 @@ public function getPrototype(): ClassMemberReflection $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { - $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType(), null, $prototypeDeclaringClass); } return new MethodPrototypeReflection( diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 5a8b677229..6b2a5a0c37 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -812,4 +812,10 @@ public function testBug9524(): void $this->analyse([__DIR__ . '/data/bug-9524.php'], []); } + public function testSimpleXmlElementChildClass(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/simple-xml-element-child.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php new file mode 100644 index 0000000000..1251bf5933 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/simple-xml-element-child.php @@ -0,0 +1,20 @@ +escapeInput($value), $namespace); + } + + private function escapeInput(?string $value): ?string + { + if ($value === null) { + return null; + } + return htmlspecialchars((string) normalizer_normalize($value), ENT_XML1, 'UTF-8'); + } +} From fd6a0f275f2a4d6dd21c450bb4d0a55d0ee1e43c Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 11 Nov 2024 12:48:50 +0100 Subject: [PATCH 106/220] Fix `for` endless loop detection --- src/Analyser/NodeScopeResolver.php | 5 ++- .../PHPStan/Analyser/StatementResultTest.php | 8 ++++ .../DeadCode/UnreachableStatementRuleTest.php | 6 +++ .../PHPStan/Rules/DeadCode/data/bug-11992.php | 37 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-11992.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1a6a069d28..5c4d86526e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1369,7 +1369,10 @@ private function processStmtNode( } $bodyScope = $bodyScope->mergeWith($initScope); + + $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { + $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); } @@ -1385,9 +1388,7 @@ private function processStmtNode( } $finalScope = $finalScope->generalizeWith($loopScope); - $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { - $alwaysIterates = $alwaysIterates->and($finalScope->getType($lastCondExpr)->toBoolean()->isTrue()); $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } diff --git a/tests/PHPStan/Analyser/StatementResultTest.php b/tests/PHPStan/Analyser/StatementResultTest.php index b86de8617e..04ca04cb26 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -297,6 +297,14 @@ public function dataIsAlwaysTerminating(): array 'for (; "a", "";) { }', false, ], + [ + 'for ($c = (0x80 | 0x40); $c & 0x80; $c = $c << 1) { }', + false, + ], + [ + 'for ($i = 0; $i < 10; $i++) { $i = 5; }', + true, + ], [ 'do { } while (doFoo());', false, diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 191ba39134..da076db1c7 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -224,4 +224,10 @@ public function testBug11179(): void $this->analyse([__DIR__ . '/data/bug-11179.php'], []); } + public function testBug11992(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11992.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11992.php b/tests/PHPStan/Rules/DeadCode/data/bug-11992.php new file mode 100644 index 0000000000..e6c35c967d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11992.php @@ -0,0 +1,37 @@ +valid(); + $it->next() + ) { + printf("name: %s\n", $it->getFilename()); + } + printf("done\n"); +} + +exampleA(); +exampleB(); +exampleC(); From 2a200beec3f2edd913b2af3bf69fbb428018dd74 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 11 Nov 2024 16:22:41 +0100 Subject: [PATCH 107/220] Fix test --- tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 6b2a5a0c37..abbe2927ab 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -814,6 +814,10 @@ public function testBug9524(): void public function testSimpleXmlElementChildClass(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/simple-xml-element-child.php'], []); } From b071fc7a626639ae6caecea0327dfbeecf55e0c5 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:03:28 +0000 Subject: [PATCH 108/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 23c5c46c7a..3400105a87 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#1f0dca06d54cf187adb3481a9c3e7d74af01743b", + "jetbrains/phpstorm-stubs": "dev-master#893a3bc59b8ff1b995220abcbbf76796926b7e9a", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 4a377463ad..d4f8f73719 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8793a11aa08550fce8d98648609fda3b", + "content-hash": "66f5013e5ca645152c225e1855311a21", "packages": [ { "name": "clue/ndjson-react", @@ -1442,18 +1442,18 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "1f0dca06d54cf187adb3481a9c3e7d74af01743b" + "reference": "893a3bc59b8ff1b995220abcbbf76796926b7e9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/1f0dca06d54cf187adb3481a9c3e7d74af01743b", - "reference": "1f0dca06d54cf187adb3481a9c3e7d74af01743b", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/893a3bc59b8ff1b995220abcbbf76796926b7e9a", + "reference": "893a3bc59b8ff1b995220abcbbf76796926b7e9a", "shasum": "" }, "require-dev": { "friendsofphp/php-cs-fixer": "v3.64.0", "nikic/php-parser": "v5.3.1", - "phpdocumentor/reflection-docblock": "5.4.1", + "phpdocumentor/reflection-docblock": "5.5.1", "phpunit/phpunit": "11.4.3" }, "default-branch": true, @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-04T21:28:48+00:00" + "time": "2024-11-10T16:24:24+00:00" }, { "name": "nette/bootstrap", From 7eef99af807040bc3f2d91fa1bdf28329b0c65b2 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:19:36 +0000 Subject: [PATCH 109/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 3400105a87..e739daa7f5 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.42.0.11", - "phpstan/php-8-stubs": "0.4.4", + "phpstan/php-8-stubs": "0.4.5", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index d4f8f73719..2d188ccc6a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "66f5013e5ca645152c225e1855311a21", + "content-hash": "92f8cc137cb93c0b9769fd3d8c10f71c", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.4", + "version": "0.4.5", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "c42f6e278d600b219b76d20f80f8455259bcd593" + "reference": "34c6f72940e784d1ccdda1b6a3249ed261f1637a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/c42f6e278d600b219b76d20f80f8455259bcd593", - "reference": "c42f6e278d600b219b76d20f80f8455259bcd593", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/34c6f72940e784d1ccdda1b6a3249ed261f1637a", + "reference": "34c6f72940e784d1ccdda1b6a3249ed261f1637a", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.4" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.5" }, - "time": "2024-11-01T00:22:02+00:00" + "time": "2024-11-12T00:19:02+00:00" }, { "name": "phpstan/phpdoc-parser", From 4edc329827908be5b617055c134740b240e21018 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 12 Nov 2024 14:28:17 +0100 Subject: [PATCH 110/220] Run integration tests again --- .github/workflows/phar.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index dcc27d6be7..adea89e232 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -103,14 +103,14 @@ jobs: - name: "Delete checksum PHAR" run: "rm tmp/phpstan.phar" -# -# integration-tests: -# if: github.event_name == 'pull_request' -# needs: compiler-tests -# uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.0.x -# with: -# ref: 2.0.x -# phar-checksum: ${{needs.compiler-tests.outputs.checksum}} + + integration-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.0.x + with: + ref: 2.0.x + phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' From 3447391001f7a5c2bfb77f66c2d0e157242c1dae Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 12 Nov 2024 17:24:37 +0100 Subject: [PATCH 111/220] Fix resolving class constant type using `self::` in a class attribute argument --- src/Rules/Classes/ClassAttributesRule.php | 9 ++++--- .../Rules/Classes/ClassAttributesRuleTest.php | 22 ++++++++++++++- .../PHPStan/Rules/Classes/data/bug-12011.php | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-12011.php diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 319379ab2a..afe9786db0 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class ClassAttributesRule implements Rule { @@ -20,14 +21,16 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassLike::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { + $classLikeNode = $node->getOriginalNode(); + return $this->attributesCheck->check( $scope, - $node->attrGroups, + $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', ); diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 6fa6252277..f0deeee108 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -22,6 +22,10 @@ class ClassAttributesRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -29,7 +33,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -148,4 +152,20 @@ public function testAllowDynamicPropertiesAttribute(): void $this->analyse([__DIR__ . '/data/allow-dynamic-properties-attribute.php'], []); } + public function testBug12011(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12011.php'], [ + [ + 'Parameter #1 $name of attribute class Bug12011\Table constructor expects string|null, int given.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-12011.php b/tests/PHPStan/Rules/Classes/data/bug-12011.php new file mode 100644 index 0000000000..feb1795d58 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12011.php @@ -0,0 +1,27 @@ + Date: Tue, 12 Nov 2024 17:35:18 +0100 Subject: [PATCH 112/220] Do not report nonexistent variable passed to by-ref parameter with checkImplicitMixed --- src/Rules/FunctionCallParametersCheck.php | 6 +++++- .../Methods/CallStaticMethodsRuleTest.php | 8 +++++++ .../PHPStan/Rules/Methods/data/bug-12015.php | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12015.php diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 40fb657bcc..623b593f5f 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -313,7 +313,11 @@ public function check( if ( !$parameter->passedByReference()->createsNewVariable() - || (!$isBuiltin && $this->checkUnresolvableParameterTypes) // bleeding edge only + || ( + !$isBuiltin + && $this->checkUnresolvableParameterTypes // bleeding edge only + && !$argumentValueType instanceof ErrorType + ) ) { $accepts = $this->ruleLevelHelper->acceptsWithReason($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 1246699ff8..fbb5e41439 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -849,4 +849,12 @@ public function testBug10872(): void $this->analyse([__DIR__ . '/data/bug-10872.php'], []); } + public function testBug12015(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12015.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12015.php b/tests/PHPStan/Rules/Methods/data/bug-12015.php new file mode 100644 index 0000000000..c2a5618cd8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12015.php @@ -0,0 +1,21 @@ + Date: Fri, 11 Oct 2024 12:42:16 +0200 Subject: [PATCH 113/220] add null to array_map(null, $a, $b) --- .../ArrayMapFunctionReturnTypeExtension.php | 36 +++++++++++++++++-- .../Analyser/nsrt/array_map_multiple.php | 4 +-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index e8e9e3a457..50094cc573 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -32,7 +32,8 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { return null; } @@ -54,10 +55,41 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, )->getReturnType(); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $argIterableValueTypes = []; + $addNull = false; + $expectedSize = null; foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { + $argType = $scope->getType($arg->value); + $argIterableValueTypes[$index] = $argType->getIterableValueType(); + if ($addNull) { + continue; + } + + $arraySizes = $argType->getArraySize()->getConstantScalarValues(); + if ($arraySizes === []) { + $addNull = true; + continue; + } + + foreach ($arraySizes as $size) { + $expectedSize ??= $size; + if ($expectedSize === $size) { + continue; + } + + $addNull = true; + break; + } + } + + foreach ($argIterableValueTypes as $index => $offsetValueType) { + if ($addNull) { + $offsetValueType = TypeCombinator::addNull($offsetValueType); + } + $arrayBuilder->setOffsetValueType( new ConstantIntegerType($index), - $scope->getType($arg->value)->getIterableValueType(), + $offsetValueType, ); } $valueType = $arrayBuilder->getArray(); diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php index ce73048a46..2e05c6160d 100644 --- a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -29,8 +29,8 @@ public function arrayMapNull(array $array, array $other): void assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); assertType('non-empty-array', array_map(null, $array)); - assertType('non-empty-array', array_map(null, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $other)); + assertType('non-empty-array', array_map(null, $array, $array)); + assertType('non-empty-array', array_map(null, $array, $other)); } } From d0e7aca3c4bd58b881e104a4d5b3b218c796e547 Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 11 Oct 2024 13:01:26 +0200 Subject: [PATCH 114/220] don't add null for array_map(null, $a, $a) --- .../ArrayMapFunctionReturnTypeExtension.php | 36 ++++++++++++++----- .../Analyser/nsrt/array_map_multiple.php | 6 +++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 50094cc573..ce29b37d2a 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -19,6 +19,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_map; +use function array_reduce; use function array_slice; use function count; @@ -55,19 +56,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, )->getReturnType(); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - $argIterableValueTypes = []; - $addNull = false; + $argTypes = []; + $areAllSameSize = true; $expectedSize = null; foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { - $argType = $scope->getType($arg->value); - $argIterableValueTypes[$index] = $argType->getIterableValueType(); - if ($addNull) { + $argTypes[$index] = $argType = $scope->getType($arg->value); + if (!$areAllSameSize || $numArgs === 2) { continue; } $arraySizes = $argType->getArraySize()->getConstantScalarValues(); if ($arraySizes === []) { - $addNull = true; + $areAllSameSize = false; continue; } @@ -77,12 +77,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, continue; } - $addNull = true; - break; + $areAllSameSize = false; + continue 2; } } - foreach ($argIterableValueTypes as $index => $offsetValueType) { + if (!$areAllSameSize) { + $firstArr = $functionCall->getArgs()[1]->value; + $identities = []; + foreach (array_slice($functionCall->getArgs(), 2) as $arg) { + $identities[] = new Node\Expr\BinaryOp\Identical($firstArr, $arg->value); + } + + $and = array_reduce( + $identities, + static fn (Node\Expr $a, Node\Expr $b) => new Node\Expr\BinaryOp\BooleanAnd($a, $b), + new Node\Expr\ConstFetch(new Node\Name('true')), + ); + $areAllSameSize = $scope->getType($and)->isTrue()->yes(); + } + + $addNull = !$areAllSameSize; + + foreach ($argTypes as $index => $argType) { + $offsetValueType = $argType->getIterableValueType(); if ($addNull) { $offsetValueType = TypeCombinator::addNull($offsetValueType); } diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php index 2e05c6160d..d986969c3e 100644 --- a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -29,8 +29,12 @@ public function arrayMapNull(array $array, array $other): void assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); assertType('non-empty-array', array_map(null, $array)); - assertType('non-empty-array', array_map(null, $array, $array)); + assertType('non-empty-array', array_map(null, $array, $array)); + assertType('non-empty-array', array_map(null, $array, $array, $array)); assertType('non-empty-array', array_map(null, $array, $other)); + + assertType('array{1}|array{true}', array_map(null, rand() ? [1] : [true])); + assertType('array{1}|array{true, false}', array_map(null, rand() ? [1] : [true, false])); } } From 5a110c48270bcfd90943401eda125433c8575537 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 13 Nov 2024 09:00:52 +0100 Subject: [PATCH 115/220] Fix build --- tests/PHPStan/Rules/Classes/data/bug-12011.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Classes/data/bug-12011.php b/tests/PHPStan/Rules/Classes/data/bug-12011.php index feb1795d58..94671eec7a 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-12011.php +++ b/tests/PHPStan/Rules/Classes/data/bug-12011.php @@ -1,4 +1,4 @@ -= 8.3 namespace Bug12011; From 6787df2bc822c98527d8dce7152e79532edbc9e2 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 13 Nov 2024 00:20:00 +0000 Subject: [PATCH 116/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index e739daa7f5..2dd8549659 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.42.0.11", - "phpstan/php-8-stubs": "0.4.5", + "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index 2d188ccc6a..57ec0c62c4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "92f8cc137cb93c0b9769fd3d8c10f71c", + "content-hash": "01763204b0f17de678b17b578dd8ffbc", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.5", + "version": "0.4.6", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "34c6f72940e784d1ccdda1b6a3249ed261f1637a" + "reference": "25ba0a11dc14a02c062392786486ada62d36b66d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/34c6f72940e784d1ccdda1b6a3249ed261f1637a", - "reference": "34c6f72940e784d1ccdda1b6a3249ed261f1637a", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/25ba0a11dc14a02c062392786486ada62d36b66d", + "reference": "25ba0a11dc14a02c062392786486ada62d36b66d", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.5" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.6" }, - "time": "2024-11-12T00:19:02+00:00" + "time": "2024-11-13T00:19:28+00:00" }, { "name": "phpstan/phpdoc-parser", From 5a132f15d6296c34ef305d19bae405c101698fab Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 13 Nov 2024 09:35:48 +0100 Subject: [PATCH 117/220] Refactor ComposerPhpVersionFactory, ConstantResolver --- conf/config.neon | 1 - src/Analyser/ConstantResolver.php | 89 ++++++++++++++----- src/Analyser/ConstantResolverFactory.php | 4 +- .../ValidateIgnoredErrorsExtension.php | 4 +- src/Php/ComposerPhpVersionFactory.php | 55 ++---------- src/Testing/PHPStanTestCase.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-4434.php | 20 ++--- .../Analyser/nsrt/predefined-constants.php | 4 +- 8 files changed, 97 insertions(+), 84 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index df5c15ddec..77b59a5d3b 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -345,7 +345,6 @@ services: - class: PHPStan\Php\ComposerPhpVersionFactory arguments: - phpVersion: %phpVersion% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 8176fe8abc..846188b985 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -3,10 +3,12 @@ namespace PHPStan\Analyser; use PhpParser\Node\Name; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -21,6 +23,8 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function in_array; +use function is_array; +use function is_int; use function max; use function sprintf; use const INF; @@ -35,12 +39,13 @@ final class ConstantResolver /** * @param string[] $dynamicConstantNames + * @param int|array{min: int, max: int}|null $phpVersion */ public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, private array $dynamicConstantNames, - private ?PhpVersion $composerMinPhpVersion, - private ?PhpVersion $composerMaxPhpVersion, + private int|array|null $phpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, ) { } @@ -83,15 +88,23 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type new AccessoryNonFalsyStringType(), ]); } + + $minPhpVersion = null; + $maxPhpVersion = null; + if (in_array($resolvedConstantName, ['PHP_VERSION_ID', 'PHP_MAJOR_VERSION', 'PHP_MINOR_VERSION', 'PHP_RELEASE_VERSION'], true)) { + $minPhpVersion = $this->getMinPhpVersion(); + $maxPhpVersion = $this->getMaxPhpVersion(); + } + if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { $minMajor = 5; $maxMajor = null; - if ($this->composerMinPhpVersion !== null) { - $minMajor = max($minMajor, $this->composerMinPhpVersion->getMajorVersionId()); + if ($minPhpVersion !== null) { + $minMajor = max($minMajor, $minPhpVersion->getMajorVersionId()); } - if ($this->composerMaxPhpVersion !== null) { - $maxMajor = $this->composerMaxPhpVersion->getMajorVersionId(); + if ($maxPhpVersion !== null) { + $maxMajor = $maxPhpVersion->getMajorVersionId(); } return $this->createInteger($minMajor, $maxMajor); @@ -101,12 +114,12 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type $maxMinor = null; if ( - $this->composerMinPhpVersion !== null - && $this->composerMaxPhpVersion !== null - && $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId() + $minPhpVersion !== null + && $maxPhpVersion !== null + && $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId() ) { - $minMinor = $this->composerMinPhpVersion->getMinorVersionId(); - $maxMinor = $this->composerMaxPhpVersion->getMinorVersionId(); + $minMinor = $minPhpVersion->getMinorVersionId(); + $maxMinor = $maxPhpVersion->getMinorVersionId(); } return $this->createInteger($minMinor, $maxMinor); @@ -116,13 +129,13 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type $maxRelease = null; if ( - $this->composerMinPhpVersion !== null - && $this->composerMaxPhpVersion !== null - && $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId() - && $this->composerMaxPhpVersion->getMinorVersionId() === $this->composerMinPhpVersion->getMinorVersionId() + $minPhpVersion !== null + && $maxPhpVersion !== null + && $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId() + && $maxPhpVersion->getMinorVersionId() === $minPhpVersion->getMinorVersionId() ) { - $minRelease = $this->composerMinPhpVersion->getPatchVersionId(); - $maxRelease = $this->composerMaxPhpVersion->getPatchVersionId(); + $minRelease = $minPhpVersion->getPatchVersionId(); + $maxRelease = $maxPhpVersion->getPatchVersionId(); } return $this->createInteger($minRelease, $maxRelease); @@ -130,11 +143,11 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'PHP_VERSION_ID') { $minVersion = 50207; $maxVersion = null; - if ($this->composerMinPhpVersion !== null) { - $minVersion = max($minVersion, $this->composerMinPhpVersion->getVersionId()); + if ($minPhpVersion !== null) { + $minVersion = max($minVersion, $minPhpVersion->getVersionId()); } - if ($this->composerMaxPhpVersion !== null) { - $maxVersion = $this->composerMaxPhpVersion->getVersionId(); + if ($maxPhpVersion !== null) { + $maxVersion = $maxPhpVersion->getVersionId(); } return $this->createInteger($minVersion, $maxVersion); @@ -351,6 +364,40 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type return null; } + private function getMinPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['min']); + } + + return $this->composerPhpVersionFactory->getMinVersion(); + } + + private function getMaxPhpVersion(): ?PhpVersion + { + if (is_int($this->phpVersion)) { + return null; + } + + if (is_array($this->phpVersion)) { + if ($this->phpVersion['max'] < $this->phpVersion['min']) { + throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); + } + + return new PhpVersion($this->phpVersion['max']); + } + + return $this->composerPhpVersionFactory->getMaxVersion(); + } + public function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType->isConstantValue()->yes() && in_array($constantName, $this->dynamicConstantNames, true)) { diff --git a/src/Analyser/ConstantResolverFactory.php b/src/Analyser/ConstantResolverFactory.php index f111da14ec..5ccc516e42 100644 --- a/src/Analyser/ConstantResolverFactory.php +++ b/src/Analyser/ConstantResolverFactory.php @@ -23,8 +23,8 @@ public function create(): ConstantResolver return new ConstantResolver( $this->reflectionProviderProvider, $this->container->getParameter('dynamicConstantNames'), - $composerFactory->getMinVersion(), - $composerFactory->getMaxVersion(), + $this->container->getParameter('phpVersion'), + $composerFactory, ); } diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 92f0712e46..4560fde44c 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -12,6 +12,7 @@ use PHPStan\Command\IgnoredRegexValidator; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileExcluder; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; use PHPStan\PhpDoc\TypeNodeResolver; @@ -65,7 +66,8 @@ public function loadConfiguration(): void $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); - $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, null); + $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); + $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory); $phpDocParserConfig = new ParserConfig([]); $ignoredRegexValidator = new IgnoredRegexValidator( diff --git a/src/Php/ComposerPhpVersionFactory.php b/src/Php/ComposerPhpVersionFactory.php index ebcf7c130e..88b81022ee 100644 --- a/src/Php/ComposerPhpVersionFactory.php +++ b/src/Php/ComposerPhpVersionFactory.php @@ -3,17 +3,10 @@ namespace PHPStan\Php; use Composer\Semver\VersionParser; -use Nette\Utils\Json; -use Nette\Utils\JsonException; use Nette\Utils\Strings; -use PHPStan\File\CouldNotReadFileException; -use PHPStan\File\FileReader; -use PHPStan\ShouldNotHappenException; +use PHPStan\Internal\ComposerHelper; use function count; use function end; -use function is_array; -use function is_file; -use function is_int; use function is_string; use function min; use function sprintf; @@ -29,11 +22,9 @@ final class ComposerPhpVersionFactory /** * @param string[] $composerAutoloaderProjectPaths - * @param int|array{min: int, max: int}|null $phpVersion */ public function __construct( private array $composerAutoloaderProjectPaths, - private int|array|null $phpVersion, ) { } @@ -42,23 +33,6 @@ private function initializeVersions(): void { $this->initialized = true; - $phpVersion = $this->phpVersion; - - if (is_int($phpVersion)) { - throw new ShouldNotHappenException(); - } - - if (is_array($phpVersion)) { - if ($phpVersion['max'] < $phpVersion['min']) { - throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); - } - - $this->minVersion = new PhpVersion($phpVersion['min']); - $this->maxVersion = new PhpVersion($phpVersion['max']); - - return; - } - // don't limit minVersion... PHPStan can analyze even PHP5 $this->maxVersion = new PhpVersion(PhpVersionFactory::MAX_PHP_VERSION); @@ -87,10 +61,6 @@ private function initializeVersions(): void public function getMinVersion(): ?PhpVersion { - if (is_int($this->phpVersion)) { - return null; - } - if ($this->initialized === false) { $this->initializeVersions(); } @@ -100,10 +70,6 @@ public function getMinVersion(): ?PhpVersion public function getMaxVersion(): ?PhpVersion { - if (is_int($this->phpVersion)) { - return null; - } - if ($this->initialized === false) { $this->initializeVersions(); } @@ -114,21 +80,18 @@ public function getMaxVersion(): ?PhpVersion private function getComposerRequireVersion(): ?string { $composerPhpVersion = null; + if (count($this->composerAutoloaderProjectPaths) > 0) { - $composerJsonPath = end($this->composerAutoloaderProjectPaths) . '/composer.json'; - if (is_file($composerJsonPath)) { - try { - $composerJsonContents = FileReader::read($composerJsonPath); - $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); - $requiredVersion = $composer['require']['php'] ?? null; - if (is_string($requiredVersion)) { - $composerPhpVersion = $requiredVersion; - } - } catch (CouldNotReadFileException | JsonException) { - // pass + $composer = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths)); + if ($composer !== null) { + $requiredVersion = $composer['require']['php'] ?? null; + + if (is_string($requiredVersion)) { + $composerPhpVersion = $requiredVersion; } } } + return $composerPhpVersion; } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index aee824bfbc..31cdfadb78 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -21,6 +21,7 @@ use PHPStan\Internal\DirectoryCreatorException; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; @@ -137,7 +138,8 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider } $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); - $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, null); + $composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class); + $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory); $initializerExprTypeResolver = new InitializerExprTypeResolver( $constantResolver, diff --git a/tests/PHPStan/Analyser/nsrt/bug-4434.php b/tests/PHPStan/Analyser/nsrt/bug-4434.php index a1f3bea048..7b48df20fd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4434.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4434.php @@ -10,14 +10,14 @@ class HelloWorld public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int<5, max>', PHP_MAJOR_VERSION); - assertType('int<5, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 7) { assertType('7', PHP_MAJOR_VERSION); assertType('7', \PHP_MAJOR_VERSION); } else { - assertType('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION); - assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', PHP_MAJOR_VERSION); + assertType('8|int<5, 6>', \PHP_MAJOR_VERSION); } } } @@ -28,14 +28,14 @@ class HelloWorld2 public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int<5, max>', PHP_MAJOR_VERSION); - assertType('int<5, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 100) { - assertType('100', PHP_MAJOR_VERSION); - assertType('100', \PHP_MAJOR_VERSION); + assertType('*NEVER*', PHP_MAJOR_VERSION); + assertType('*NEVER*', \PHP_MAJOR_VERSION); } else { - assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION); - assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 8>', PHP_MAJOR_VERSION); + assertType('int<5, 8>', \PHP_MAJOR_VERSION); } } } diff --git a/tests/PHPStan/Analyser/nsrt/predefined-constants.php b/tests/PHPStan/Analyser/nsrt/predefined-constants.php index 70dceda653..9d9d1b1fc5 100644 --- a/tests/PHPStan/Analyser/nsrt/predefined-constants.php +++ b/tests/PHPStan/Analyser/nsrt/predefined-constants.php @@ -4,10 +4,10 @@ // core, https://www.php.net/manual/en/reserved.constants.php assertType('non-falsy-string', PHP_VERSION); -assertType('int<5, max>', PHP_MAJOR_VERSION); +assertType('int<5, 8>', PHP_MAJOR_VERSION); assertType('int<0, max>', PHP_MINOR_VERSION); assertType('int<0, max>', PHP_RELEASE_VERSION); -assertType('int<50207, max>', PHP_VERSION_ID); +assertType('int<50207, 80499>', PHP_VERSION_ID); assertType('string', PHP_EXTRA_VERSION); assertType('0|1', PHP_ZTS); assertType('0|1', PHP_DEBUG); From 9717564bb5e229512113841c57950712e998353b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 13 Nov 2024 12:56:50 +0100 Subject: [PATCH 118/220] Utilize phpVersion.min+max in VersionCompareFunctionDynamicReturnTypeExtension --- .github/workflows/e2e-tests.yml | 4 +++ conf/config.neon | 2 ++ .../.gitignore | 2 ++ .../composer.json | 5 ++++ .../phpstan.neon | 4 +++ .../test.php | 11 +++++++ ...pareFunctionDynamicReturnTypeExtension.php | 30 ++++++++++++++----- 7 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 e2e/composer-and-phpstan-version-config/.gitignore create mode 100644 e2e/composer-and-phpstan-version-config/composer.json create mode 100644 e2e/composer-and-phpstan-version-config/phpstan.neon create mode 100644 e2e/composer-and-phpstan-version-config/test.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6d7233d38a..96505cbf9c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -296,6 +296,10 @@ jobs: - script: | cd e2e/bug-11819 ../../bin/phpstan + - script: | + cd e2e/composer-and-phpstan-version-config + composer install --ignore-platform-reqs + ../../bin/phpstan analyze test.php --level=0 - script: | cd e2e/composer-max-version composer install diff --git a/conf/config.neon b/conf/config.neon index 77b59a5d3b..739e8247dc 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1683,6 +1683,8 @@ services: - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension + arguments: + configPhpVersion: %phpVersion% tags: - phpstan.broker.dynamicFunctionReturnTypeExtension diff --git a/e2e/composer-and-phpstan-version-config/.gitignore b/e2e/composer-and-phpstan-version-config/.gitignore new file mode 100644 index 0000000000..3a9875b460 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/composer-and-phpstan-version-config/composer.json b/e2e/composer-and-phpstan-version-config/composer.json new file mode 100644 index 0000000000..8fe9570814 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^7.0" + } +} diff --git a/e2e/composer-and-phpstan-version-config/phpstan.neon b/e2e/composer-and-phpstan-version-config/phpstan.neon new file mode 100644 index 0000000000..003e5e1484 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80103 + max: 80304 diff --git a/e2e/composer-and-phpstan-version-config/test.php b/e2e/composer-and-phpstan-version-config/test.php new file mode 100644 index 0000000000..a9afaa4b65 --- /dev/null +++ b/e2e/composer-and-phpstan-version-config/test.php @@ -0,0 +1,11 @@ +', PHP_VERSION_ID); +\PHPStan\Testing\assertType('8', PHP_MAJOR_VERSION); +\PHPStan\Testing\assertType('int<1, 3>', PHP_MINOR_VERSION); +\PHPStan\Testing\assertType('int<0, max>', PHP_RELEASE_VERSION); + +\PHPStan\Testing\assertType('1', version_compare(PHP_VERSION, '7.0.0')); +\PHPStan\Testing\assertType('false', version_compare(PHP_VERSION, '7.0.0', '<')); +\PHPStan\Testing\assertType('true', version_compare(PHP_VERSION, '7.0.0', '>')); diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index d79ebf0706..7ca5d8bac9 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -16,12 +17,19 @@ use PHPStan\Type\TypeCombinator; use function array_filter; use function count; +use function is_array; use function version_compare; final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private ComposerPhpVersionFactory $composerPhpVersionFactory) + /** + * @param int|array{min: int, max: int}|null $configPhpVersion + */ + public function __construct( + private int|array|null $configPhpVersion, + private ComposerPhpVersionFactory $composerPhpVersionFactory, + ) { } @@ -93,13 +101,21 @@ private function getVersionStrings(Expr $expr, Scope $scope): array if ( $expr instanceof Expr\ConstFetch && $expr->name->toString() === 'PHP_VERSION' - && $this->composerPhpVersionFactory->getMinVersion() !== null - && $this->composerPhpVersionFactory->getMaxVersion() !== null ) { - return [ - new ConstantStringType($this->composerPhpVersionFactory->getMinVersion()->getVersionString()), - new ConstantStringType($this->composerPhpVersionFactory->getMaxVersion()->getVersionString()), - ]; + if (is_array($this->configPhpVersion)) { + $minVersion = new PhpVersion($this->configPhpVersion['min']); + $maxVersion = new PhpVersion($this->configPhpVersion['max']); + } else { + $minVersion = $this->composerPhpVersionFactory->getMinVersion(); + $maxVersion = $this->composerPhpVersionFactory->getMaxVersion(); + } + + if ($minVersion !== null && $maxVersion !== null) { + return [ + new ConstantStringType($minVersion->getVersionString()), + new ConstantStringType($maxVersion->getVersionString()), + ]; + } } return $scope->getType($expr)->getConstantStrings(); From f243636374fee78ec566f27d422bf114e1357ec3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 13 Nov 2024 13:41:48 +0100 Subject: [PATCH 119/220] More details php version information in diagnose --- conf/config.neon | 1 + src/Diagnose/PHPStanDiagnoseExtension.php | 49 ++++++++++++++++++++--- src/Php/PhpVersion.php | 8 ++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index 739e8247dc..3f4414b606 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -2108,6 +2108,7 @@ services: arguments: composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% allConfigFiles: %allConfigFiles% + configPhpVersion: %phpVersion% autowired: false # Error formatters diff --git a/src/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index 7c3057190c..35cb6a862e 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -7,6 +7,7 @@ use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileHelper; use PHPStan\Internal\ComposerHelper; +use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use ReflectionClass; use function array_key_exists; @@ -17,6 +18,7 @@ use function explode; use function implode; use function in_array; +use function is_array; use function is_file; use function is_readable; use function sprintf; @@ -29,14 +31,17 @@ final class PHPStanDiagnoseExtension implements DiagnoseExtension { /** + * @param int|array{min: int, max: int}|null $configPhpVersion * @param string[] $composerAutoloaderProjectPaths * @param string [] $allConfigFiles */ public function __construct( private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private FileHelper $fileHelper, private array $composerAutoloaderProjectPaths, private array $allConfigFiles, + private ComposerPhpVersionFactory $composerPhpVersionFactory, ) { } @@ -48,11 +53,45 @@ public function print(Output $output): void 'PHP runtime version: %s', $phpRuntimeVersion->getVersionString(), )); - $output->writeLineFormatted(sprintf( - 'PHP version for analysis: %s (from %s)', - $this->phpVersion->getVersionString(), - $this->phpVersion->getSourceLabel(), - )); + + if ( + $this->phpVersion->getSource() === PhpVersion::SOURCE_CONFIG + && is_array($this->configPhpVersion) + ) { + $minVersion = new PhpVersion($this->configPhpVersion['min']); + $maxVersion = new PhpVersion($this->configPhpVersion['max']); + + $output->writeLineFormatted(sprintf( + 'PHP version for analysis: %s-%s (from %s)', + $minVersion->getVersionString(), + $maxVersion->getVersionString(), + $this->phpVersion->getSourceLabel(), + )); + + } else { + $minComposerPhpVersion = $this->composerPhpVersionFactory->getMinVersion(); + $maxComposerPhpVersion = $this->composerPhpVersionFactory->getMaxVersion(); + if ($minComposerPhpVersion !== null && $maxComposerPhpVersion !== null) { + if ($minComposerPhpVersion->getVersionId() !== $maxComposerPhpVersion->getVersionId()) { + $output->writeLineFormatted(sprintf( + 'PHP composer.json required version: %s-%s', + $minComposerPhpVersion->getVersionString(), + $maxComposerPhpVersion->getVersionString(), + )); + } else { + $output->writeLineFormatted(sprintf( + 'PHP composer.json required version: %s', + $minComposerPhpVersion->getVersionString(), + )); + } + } + + $output->writeLineFormatted(sprintf( + 'PHP version for analysis: %s (from %s)', + $this->phpVersion->getVersionString(), + $this->phpVersion->getSourceLabel(), + )); + } $output->writeLineFormatted(''); $output->writeLineFormatted(sprintf( diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 83ca1245b5..8520f6488d 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -22,6 +22,14 @@ public function __construct(private int $versionId, private int $source = self:: { } + /** + * @return self::SOURCE_* + */ + public function getSource(): int + { + return $this->source; + } + public function getSourceLabel(): string { switch ($this->source) { From ceac3098a70f94cc99bc6574a7ce9cb96e9981aa Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 13 Nov 2024 14:11:48 +0100 Subject: [PATCH 120/220] Fix extract signature --- resources/functionMap.php | 4 ++-- resources/functionMap_bleedingEdge.php | 2 +- resources/functionMap_php74delta.php | 20 +++++++++---------- tests/PHPStan/Analyser/nsrt/extract.php | 18 +++++++++++++++++ .../CallToFunctionParametersRuleTest.php | 5 +++++ .../Rules/Functions/data/bug-11759.php | 5 +++++ 6 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-11759.php diff --git a/resources/functionMap.php b/resources/functionMap.php index b678f48e5b..46492450d9 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -286,7 +286,7 @@ 'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...args='=>'array'], 'array_merge' => ['array', 'arr1'=>'array', '...args='=>'array'], 'array_merge_recursive' => ['array', 'arr1'=>'array', '...args='=>'array'], -'array_multisort' => ['bool', '&rw_array1'=>'array', 'array1_sort_order='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], +'array_multisort' => ['bool', 'array1'=>'array', 'array1_sort_order='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], 'array_pad' => ['array', 'input'=>'array', 'pad_size'=>'int', 'pad_value'=>'mixed'], 'array_pop' => ['mixed', '&rw_stack'=>'array'], 'array_product' => ['int|float', 'input'=>'array'], @@ -2637,7 +2637,7 @@ 'explode' => ['list|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'number'=>'float'], 'extension_loaded' => ['bool', 'extension_name'=>'string'], -'extract' => ['0|positive-int', '&rw_var_array'=>'array', 'extract_type='=>'int', 'prefix='=>'string|null'], +'extract' => ['0|positive-int', 'array'=>'array', 'flags='=>'int', 'prefix='=>'string|null'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php index 11dd4fa773..9d26a518b6 100644 --- a/resources/functionMap_bleedingEdge.php +++ b/resources/functionMap_bleedingEdge.php @@ -146,7 +146,7 @@ 'scandir' => ['list|false', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING| SCANDIR_SORT_NONE', 'context='=>'resource'], 'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int-mask', 'context='=>'resource'], 'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'crypto_method='=>'STREAM_CRYPTO_METHOD_SSLv2_CLIENT|STREAM_CRYPTO_METHOD_SSLv3_CLIENT|STREAM_CRYPTO_METHOD_SSLv23_CLIENT|STREAM_CRYPTO_METHOD_ANY_CLIENT|STREAM_CRYPTO_METHOD_TLS_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT|STREAM_CRYPTO_METHOD_SSLv2_SERVER|STREAM_CRYPTO_METHOD_SSLv3_SERVER|STREAM_CRYPTO_METHOD_SSLv23_SERVER|STREAM_CRYPTO_METHOD_ANY_SERVER|STREAM_CRYPTO_METHOD_TLS_SERVER|STREAM_CRYPTO_METHOD_TLSv1_0_SERVER|STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER|STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'session_stream='=>'resource'], - 'extract' => ['0|positive-int', '&rw_var_array'=>'array', 'extract_type='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], + 'extract' => ['0|positive-int', 'array'=>'array', 'flags='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], 'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], 'Locale::composeLocale' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], 'locale_compose' => ['string|false', 'subtags'=>'array{language:string, script?:string, region?:string, variant?:array, private?:array, extlang?:array, variant0?:string, variant1?:string, variant2?:string, variant3?:string, variant4?:string, variant5?:string, variant6?:string, variant7?:string, variant8?:string, variant9?:string, variant10?:string, variant11?:string, variant12?:string, variant13?:string, variant14?:string, private0?:string, private1?:string, private2?:string, private3?:string, private4?:string, private5?:string, private6?:string, private7?:string, private8?:string, private9?:string, private10?:string, private11?:string, private12?:string, private13?:string, private14?:string, extlang0?:string, extlang1?:string, extlang2?:string}'], diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index b37fe5a7a5..bb05fe2b6a 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -22,21 +22,21 @@ */ return [ 'new' => [ - 'FFI::addr' => ['FFI\CData', '&ptr'=>'FFI\CData'], - 'FFI::alignof' => ['int', '&ptr'=>'mixed'], + 'FFI::addr' => ['FFI\CData', 'ptr'=>'FFI\CData'], + 'FFI::alignof' => ['int', 'ptr'=>'mixed'], 'FFI::arrayType' => ['FFI\CType', 'type'=>'string|FFI\CType', 'dims'=>'array'], - 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', '&ptr'=>''], + 'FFI::cast' => ['FFI\CData', 'type'=>'string|FFI\CType', 'ptr'=>''], 'FFI::cdef' => ['FFI', 'code='=>'string', 'lib='=>'?string'], - 'FFI::free' => ['void', '&ptr'=>'FFI\CData'], + 'FFI::free' => ['void', 'ptr'=>'FFI\CData'], 'FFI::load' => ['FFI', 'filename'=>'string'], - 'FFI::memcmp' => ['int', '&ptr1'=>'FFI\CData|string', '&ptr2'=>'FFI\CData|string', 'size'=>'int'], - 'FFI::memcpy' => ['void', '&dst'=>'FFI\CData', '&src'=>'string|FFI\CData', 'size'=>'int'], - 'FFI::memset' => ['void', '&ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], + 'FFI::memcmp' => ['int', 'ptr1'=>'FFI\CData|string', 'ptr2'=>'FFI\CData|string', 'size'=>'int'], + 'FFI::memcpy' => ['void', 'dst'=>'FFI\CData', 'src'=>'string|FFI\CData', 'size'=>'int'], + 'FFI::memset' => ['void', 'ptr'=>'FFI\CData', 'ch'=>'int', 'size'=>'int'], 'FFI::new' => ['FFI\CData', 'type'=>'string|FFI\CType', 'owned='=>'bool', 'persistent='=>'bool'], 'FFI::scope' => ['FFI', 'scope_name'=>'string'], - 'FFI::sizeof' => ['int', '&ptr'=>'FFI\CData|FFI\CType'], - 'FFI::string' => ['string', '&ptr'=>'FFI\CData', 'size='=>'int'], - 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], + 'FFI::sizeof' => ['int', 'ptr'=>'FFI\CData|FFI\CType'], + 'FFI::string' => ['string', 'ptr'=>'FFI\CData', 'size='=>'int'], + 'FFI::typeof' => ['FFI\CType', 'ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'positive-int'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], diff --git a/tests/PHPStan/Analyser/nsrt/extract.php b/tests/PHPStan/Analyser/nsrt/extract.php index c57f2ef46d..dff42bc482 100644 --- a/tests/PHPStan/Analyser/nsrt/extract.php +++ b/tests/PHPStan/Analyser/nsrt/extract.php @@ -66,3 +66,21 @@ function doTyped3(array $vars): void assertVariableCertainty(TrinaryLogic::createNo(), $none); } + +function doTyped4(): void +{ + extract(['foo' => 42]); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} + + +function doTyped5(): void +{ + $foo = ['foo' => 42]; + extract($foo); + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('42', $foo); +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 123fa4259f..c105b2fe64 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1898,4 +1898,9 @@ public function testBug9224(): void $this->analyse([__DIR__ . '/data/bug-9224.php'], []); } + public function testBug11759(): void + { + $this->analyse([__DIR__ . '/data/bug-11759.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11759.php b/tests/PHPStan/Rules/Functions/data/bug-11759.php new file mode 100644 index 0000000000..1a6cb12c7d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11759.php @@ -0,0 +1,5 @@ + 42 ]); From b7440412395a85e2265fefe46d303be587de24df Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 13 Nov 2024 17:24:13 +0100 Subject: [PATCH 121/220] Update fidry/cpu-core-counter --- composer.json | 2 +- composer.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 2614dcf584..9aac57806f 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "composer/ca-bundle": "^1.2", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", - "fidry/cpu-core-counter": "^0.5.0", + "fidry/cpu-core-counter": "^1.2", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", diff --git a/composer.lock b/composer.lock index c0f104e2b7..7a215f2f76 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "674f9ec5e66603e465b9ef2aaa90189a", + "content-hash": "07b46dce51ea41c025511b9415b3274c", "packages": [ { "name": "clue/ndjson-react", @@ -421,16 +421,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "0.5.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -438,13 +438,13 @@ }, "require-dev": { "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", "phpstan/phpstan": "^1.9.2", "phpstan/phpstan-deprecation-rules": "^1.0.0", "phpstan/phpstan-phpunit": "^1.2.2", "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, "type": "library", @@ -470,7 +470,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -478,7 +478,7 @@ "type": "github" } ], - "time": "2022-12-24T12:35:10+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "fig/http-message-util", From 79385bd09325dd8a3f7b28c770cd781bd8fc61e7 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Thu, 14 Nov 2024 23:16:22 +0100 Subject: [PATCH 122/220] feat: add TypeCombinator::removeTruthy method --- src/Type/TypeCombinator.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index ccbb29f97b..03ddffd988 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1341,4 +1341,9 @@ public static function removeFalsey(Type $type): Type return self::remove($type, StaticTypeFactory::falsey()); } + public static function removeTruthy(Type $type): Type + { + return self::remove($type, StaticTypeFactory::truthy()); + } + } From f83b559d56a1950e7db6f5827de29a91b568e5c3 Mon Sep 17 00:00:00 2001 From: Giovanni Giacobbi Date: Fri, 15 Nov 2024 12:09:53 +0100 Subject: [PATCH 123/220] Improve signature for get_defined_constants() --- stubs/core.stub | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stubs/core.stub b/stubs/core.stub index 853222642c..abd719b4f5 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -328,3 +328,8 @@ function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} * @return ($string is lowercase-string ? lowercase-string : string) */ function rtrim(string $string, string $characters = " \n\r\t\v\x00"): string {} + +/** + * @return ($categorize is true ? array> : array) + */ +function get_defined_constants(bool $categorize = false): array {} From 5e3a3643a1de9000c585aa11fca528492656d8e7 Mon Sep 17 00:00:00 2001 From: Giovanni Giacobbi Date: Sat, 16 Nov 2024 11:59:01 +0100 Subject: [PATCH 124/220] Add basic type narrowing for `$a != ''` --- src/Analyser/TypeSpecifier.php | 93 ++++++++- tests/PHPStan/Analyser/TypeSpecifierTest.php | 4 +- tests/PHPStan/Analyser/nsrt/equal-narrow.php | 180 ++++++++++++++++++ .../Analyser/nsrt/integer-range-types.php | 2 +- tests/PHPStan/Analyser/nsrt/narrow-cast.php | 2 +- .../Analyser/nsrt/non-empty-string.php | 4 +- .../ElseIfConstantConditionRuleTest.php | 4 + .../Rules/Comparison/data/bug-11674.php | 8 +- 8 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/equal-narrow.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 68864c18b6..088236bac5 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -43,6 +43,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; @@ -1610,7 +1611,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy } /** - * @return array{Expr, ConstantScalarType}|null + * @return array{Expr, ConstantScalarType, Type}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { @@ -1632,13 +1633,13 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\ && !$rightExpr instanceof ConstFetch && !$rightExpr instanceof ClassConstFetch ) { - return [$binaryOperation->right, $leftType]; + return [$binaryOperation->right, $leftType, $rightType]; } elseif ( $rightType instanceof ConstantScalarType && !$leftExpr instanceof ConstFetch && !$leftExpr instanceof ClassConstFetch ) { - return [$binaryOperation->left, $rightType]; + return [$binaryOperation->left, $rightType, $leftType]; } return null; @@ -1949,7 +1950,21 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; - if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) { + $otherType = $expressions[2]; + + if (!$context->null() && $constantType->getValue() === null) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantArrayType([], []), + ]; + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + + if (!$context->null() && $constantType->getValue() === false) { return $this->specifyTypesInCondition( $scope, $exprNode, @@ -1967,6 +1982,52 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif ); } + if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new StringType(), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType('0'), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + + if (!$context->null() && $constantType->getValue() === '') { + /* There is a difference between php 7.x and 8.x on the equality + * behavior between zero and the empty string, so to be conservative + * we leave it untouched regardless of the language version */ + if ($context->true()) { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + ]; + } else { + $trueTypes = [ + new NullType(), + new ConstantBooleanType(false), + new ConstantStringType(''), + ]; + } + return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + } + if ( $exprNode instanceof FuncCall && $exprNode->name instanceof Name @@ -2060,11 +2121,13 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes { + // Normalize to: fn() === expr $leftExpr = $expr->left; $rightExpr = $expr->right; if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; } + $unwrappedLeftExpr = $leftExpr; if ($leftExpr instanceof AlwaysRememberedExpr) { $unwrappedLeftExpr = $leftExpr->getExpr(); @@ -2073,8 +2136,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty if ($rightExpr instanceof AlwaysRememberedExpr) { $unwrappedRightExpr = $rightExpr->getExpr(); } + $rightType = $scope->getType($rightExpr); + // (count($a) === $b) if ( !$context->null() && $unwrappedLeftExpr instanceof FuncCall @@ -2139,6 +2204,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // strlen($a) === $b if ( !$context->null() && $unwrappedLeftExpr instanceof FuncCall @@ -2175,6 +2241,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // preg_match($a) === $b if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2190,6 +2257,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty ); } + // get_class($a) === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2209,6 +2277,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // get_class($a) === 'Foo' if ( $context->truthy() && $unwrappedLeftExpr instanceof FuncCall @@ -2289,6 +2358,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // $a::class === 'Foo' if ( $context->true() && $unwrappedLeftExpr instanceof ClassConstFetch && @@ -2311,6 +2381,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } $leftType = $scope->getType($leftExpr); + + // 'Foo' === $a::class if ( $context->true() && $unwrappedRightExpr instanceof ClassConstFetch && @@ -2356,7 +2428,11 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $types = null; if ( count($leftType->getFiniteTypes()) === 1 - || ($context->true() && $leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes()) + || ( + $context->true() + && $leftType->isConstantValue()->yes() + && !$rightType->equals($leftType) + && $rightType->isSuperTypeOf($leftType)->yes()) ) { $types = $this->create( $rightExpr, @@ -2379,7 +2455,12 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } if ( count($rightType->getFiniteTypes()) === 1 - || ($context->true() && $rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes()) + || ( + $context->true() + && $rightType->isConstantValue()->yes() + && !$leftType->equals($rightType) + && $leftType->isSuperTypeOf($rightType)->yes() + ) ) { $leftTypes = $this->create( $leftExpr, diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index ced1959085..999c7169a3 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -477,8 +477,8 @@ public function dataCondition(): iterable new Variable('foo'), new Expr\ConstFetch(new Name('null')), ), - ['$foo' => self::SURE_NOT_TRUTHY], - ['$foo' => self::SURE_NOT_FALSEY], + ['$foo' => '0|0.0|\'\'|array{}|false|null'], + ['$foo' => '~0|0.0|\'\'|array{}|false|null'], ], [ new Expr\BinaryOp\Identical( diff --git a/tests/PHPStan/Analyser/nsrt/equal-narrow.php b/tests/PHPStan/Analyser/nsrt/equal-narrow.php new file mode 100644 index 0000000000..774377f400 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/equal-narrow.php @@ -0,0 +1,180 @@ +|int<1, max>|non-empty-string", $y); + } + + if ($z == null) { + assertType("0|0.0|''|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doFalse($x, $y, $z): void +{ + if ($x == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + if (false != $x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if (!$x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($y == false) { + assertType("0|''|'0'|null", $y); + } else { + assertType("int|int<1, max>|non-falsy-string", $y); + } + + if ($z == false) { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } else { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doTrue($x, $y, $z): void +{ + if ($x == true) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + if (true != $x) { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } else { + assertType("1|'x'|object|true", $x); + } + + if ($x) { + assertType("1|'x'|object|true", $x); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $x); + } + + if ($y == true) { + assertType("int|int<1, max>|non-falsy-string", $y); + } else { + assertType("0|''|'0'|null", $y); + } + + if ($z == true) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $z); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doZero($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == 0) { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } else { + assertType("1|''|'x'|array{}|object|true", $x); + } + if (0 != $x) { + assertType("1|''|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|'0'|'x'|false|null", $x); + } + + if ($y == 0) { + assertType("0|string|null", $y); + } else { + assertType("int|int<1, max>|string", $y); + } + + if ($z == 0) { + assertType("0|0.0|string|false|null", $z); + } else { + assertType("mixed~(0|0.0|'0'|false|null)", $z); + } +} + +/** + * @param 0|0.0|1|''|'0'|'x'|array{}|bool|object|null $x + * @param int|string|null $y + * @param mixed $z + */ +function doEmptyString($x, $y, $z): void +{ + // PHP 7.x/8.x compatibility: Keep zero in both cases + if ($x == '') { + assertType("0|0.0|''|false|null", $x); + } else { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } + if ('' != $x) { + assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); + } else { + assertType("0|0.0|''|false|null", $x); + } + + if ($y == '') { + assertType("0|''|null", $y); + } else { + assertType("int|non-empty-string", $y); + } + + if ($z == '') { + assertType("0|0.0|''|false|null", $z); + } else { + assertType("mixed~(''|false|null)", $z); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index cd35c77953..876a791352 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -449,7 +449,7 @@ public function zeroIssues($positive, $negative) function subtract($m) { if ($m != 0) { - assertType("mixed", $m); // could be "mixed~0|0.0|''|'0'|array{}|false|null" + assertType("mixed~(0|0.0|'0'|false|null)", $m); // could be "mixed~(0|0.0|''|'0'|false|null)" assertType('int', (int) $m); } if ($m !== 0) { diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index 82e09e0bd3..1da8858473 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -33,7 +33,7 @@ function castString($x, string $s, bool $b) { if ((string) $x) { assertType('int<-5, 5>', $x); } else { - assertType('int<-5, 5>', $x); + assertType('0', $x); } if ((string) $b) { diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index da8426c905..5400b1d31a 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -422,8 +422,8 @@ function subtract($m) { assertType('non-falsy-string', (string) $m); } if ($m != '') { - assertType("mixed", $m); - assertType('string', (string) $m); + assertType("mixed~(''|false|null)", $m); + assertType('non-empty-string', (string) $m); } if ($m !== '') { assertType("mixed~''", $m); diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index bf67146684..6c51033309 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -135,6 +135,10 @@ public function testBug11674(): void 'Elseif condition is always false.', 28, ], + [ + 'Elseif condition is always false.', + 36, + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11674.php b/tests/PHPStan/Rules/Comparison/data/bug-11674.php index 7af6660da4..6156b8e1cb 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-11674.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-11674.php @@ -10,7 +10,7 @@ function show() : void { if ((int) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // might be "0" } } @@ -18,7 +18,7 @@ function show2() : void { if ((float) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // might be "0" } } @@ -26,7 +26,7 @@ function show3() : void { if ((bool) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // not possible } } @@ -34,7 +34,7 @@ function show4() : void { if ((string) $this->param) { echo 1; } elseif ($this->param) { - echo 2; + echo 2; // not possible } } } From 65ddbcb6fe3d248076c49af0d1532d0dc4f75460 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 16 Nov 2024 12:02:35 +0100 Subject: [PATCH 125/220] Fix after merge --- src/Analyser/TypeSpecifier.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6628c7bdb1..17032ec814 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1903,7 +1903,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new ConstantStringType(''), new ConstantArrayType([], []), ]; - return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); } if (!$context->null() && $constantType->getValue() === false) { @@ -1943,7 +1943,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new ConstantStringType('0'), ]; } - return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); } if (!$context->null() && $constantType->getValue() === '') { @@ -1965,7 +1965,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif new ConstantStringType(''), ]; } - return $this->create($exprNode, new UnionType($trueTypes), $context, false, $scope, $rootExpr); + return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr); } if ( From 25a06dd34e23c454e5764bfa17d2ce9d9dbd1250 Mon Sep 17 00:00:00 2001 From: Giovanni Giacobbi Date: Sat, 16 Nov 2024 16:54:39 +0100 Subject: [PATCH 126/220] Use the correct type for final constants --- src/Reflection/ClassConstantReflection.php | 3 ++- src/Reflection/ClassReflection.php | 2 ++ src/Reflection/InitializerExprTypeResolver.php | 1 + tests/PHPStan/Analyser/nsrt/class-constant-types.php | 7 +++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index f54fc37ba4..00874b9081 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -28,6 +28,7 @@ public function __construct( private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, + private bool $isFinal, ) { } @@ -124,7 +125,7 @@ public function isPublic(): bool public function isFinal(): bool { - return $this->reflection->isFinal(); + return $this->isFinal || $this->reflection->isFinal(); } public function isDeprecated(): TrinaryLogic diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index ff45172fd8..02705f32cd 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1080,6 +1080,7 @@ public function getConstant(string $name): ClassConstantReflection $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); $varTags = $resolvedPhpDoc->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { $phpDocType = $varTags[0]->getType(); @@ -1101,6 +1102,7 @@ public function getConstant(string $name): ClassConstantReflection $deprecatedDescription, $isDeprecated, $isInternal, + $isFinal, ); } return $this->constants[$name]; diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 178c16b4a8..8cedcd1272 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1987,6 +1987,7 @@ function (Type $type, callable $traverse): Type { $constantReflection = $constantClassReflection->getConstant($constantName); if ( !$constantClassReflection->isFinal() + && !$constantReflection->isFinal() && !$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType() ) { diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php index 9d60af25c5..8f19f7659e 100644 --- a/tests/PHPStan/Analyser/nsrt/class-constant-types.php +++ b/tests/PHPStan/Analyser/nsrt/class-constant-types.php @@ -15,6 +15,9 @@ class Foo /** @var string */ private const PRIVATE_TYPE = 'foo'; + /** @final */ + const FINAL_TYPE = 'zoo'; + public function doFoo() { assertType('1', self::NO_TYPE); @@ -28,6 +31,10 @@ public function doFoo() assertType('\'foo\'', self::PRIVATE_TYPE); assertType('string', static::PRIVATE_TYPE); assertType('string', $this::PRIVATE_TYPE); + + assertType('\'zoo\'', self::FINAL_TYPE); + assertType('\'zoo\'', static::FINAL_TYPE); + assertType('\'zoo\'', $this::FINAL_TYPE); } } From d9d93b7e11a2f919f79824baab499a1a76952ab5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 16 Nov 2024 12:49:01 +0100 Subject: [PATCH 127/220] Simplified ArrayType with Mixed --- src/Type/ArrayType.php | 5 +++++ .../CallToFunctionParametersRuleTest.php | 11 ++++++++++- tests/PHPStan/Rules/Functions/data/bug-12051.php | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12051.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 01ca997eee..c3fc5050b1 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -21,6 +21,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; +use PHPStan\Type\Generic\TemplateStrictMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\MaybeCallableTypeTrait; @@ -50,6 +51,10 @@ public function __construct(Type $keyType, private Type $itemType) if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { $keyType = new MixedType(); } + if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { + $keyType = new UnionType([new StringType(), new IntegerType()]); + } + $this->keyType = $keyType; } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index c105b2fe64..768d75551f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -21,12 +21,14 @@ class CallToFunctionParametersRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), ); } @@ -1903,4 +1905,11 @@ public function testBug11759(): void $this->analyse([__DIR__ . '/data/bug-11759.php'], []); } + public function testBug12051(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12051.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12051.php b/tests/PHPStan/Rules/Functions/data/bug-12051.php new file mode 100644 index 0000000000..d271c7bf52 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12051.php @@ -0,0 +1,15 @@ + $a */ +function foo($a): void { + print "ok\n"; +} + +/** + * @param array $a + */ +function bar($a): void { + foo($a); +} From 09f7c00bc60fc80eab808e876c422d08d87aeff2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 14 May 2024 14:29:13 +0200 Subject: [PATCH 128/220] `fgetcsv` accepts `null` for `$length` --- resources/functionMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 46492450d9..eda76bc797 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -2932,7 +2932,7 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], From 30d20e6ee2dd93206aed2e26187aba0fc679d5c9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 17 Nov 2024 16:35:34 +0100 Subject: [PATCH 129/220] FunctionCallParametersCheck: Add native parameter type --- src/Rules/FunctionCallParametersCheck.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 9704d5e82e..130dd0bef8 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -53,7 +53,6 @@ public function __construct( } /** - * @param Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType * @return list */ @@ -61,7 +60,7 @@ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, bool $isBuiltin, - $funcCall, + Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, string $nodeType, TrinaryLogic $acceptsNamedArguments, string $singleInsufficientParameterMessage, From a2eddcc71a01d00888069dd85358a091070639f1 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 19 Nov 2024 00:03:40 +0000 Subject: [PATCH 130/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 55b58e1a56..15e25f2a80 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#893a3bc59b8ff1b995220abcbbf76796926b7e9a", + "jetbrains/phpstorm-stubs": "dev-master#fda684a4826c1caf59efe1a1bf68d08c11aaddbf", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index cda1b9fb41..8ee3e675ed 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b0700d67568e34950c8009d83dee8a3c", + "content-hash": "ca308caf852aa82ffe555d1235c75efa", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "893a3bc59b8ff1b995220abcbbf76796926b7e9a" + "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/893a3bc59b8ff1b995220abcbbf76796926b7e9a", - "reference": "893a3bc59b8ff1b995220abcbbf76796926b7e9a", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/fda684a4826c1caf59efe1a1bf68d08c11aaddbf", + "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-10T16:24:24+00:00" + "time": "2024-11-15T09:42:33+00:00" }, { "name": "nette/bootstrap", From b049d8d7d7e25df00c80b1ec585beb2dd2896123 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 19 Nov 2024 10:45:21 +0100 Subject: [PATCH 131/220] `Closure::bind` and `bindTo` return benevolent union with null --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index eda76bc797..620138e0bc 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -994,8 +994,8 @@ 'closelog' => ['bool'], 'Closure::__construct' => ['void'], 'Closure::__invoke' => ['', '...args='=>''], -'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|string|null'], -'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|string|null'], +'Closure::bind' => ['__benevolent', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|string|null'], +'Closure::bindTo' => ['__benevolent', 'new'=>'?object', 'newscope='=>'object|string|null'], 'Closure::call' => ['', 'to'=>'object', '...parameters='=>''], 'Closure::fromCallable' => ['Closure', 'callable'=>'callable'], 'clusterObj::convertToString' => ['string'], From 4a6565e140ddbc8965fca52159e8ef314cd482d5 Mon Sep 17 00:00:00 2001 From: Sven Reichel Date: Mon, 18 Nov 2024 11:57:27 +0100 Subject: [PATCH 132/220] 3rd parameter of htmlentities|htmlspecialchars allows null --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 05268af81e..baaef68a8c 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -3957,8 +3957,8 @@ 'HRTime\StopWatch::start' => ['void'], 'HRTime\StopWatch::stop' => ['void'], 'html_entity_decode' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string'], -'htmlentities' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string', 'double_encode='=>'bool'], -'htmlspecialchars' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string', 'double_encode='=>'bool'], +'htmlentities' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string|null', 'double_encode='=>'bool'], +'htmlspecialchars' => ['string', 'string'=>'string', 'quote_style='=>'int', 'encoding='=>'string|null', 'double_encode='=>'bool'], 'htmlspecialchars_decode' => ['string', 'string'=>'string', 'quote_style='=>'int'], 'http\Env\Request::__construct' => ['void'], 'http\Env\Request::getCookie' => ['mixed', 'name='=>'string', 'type='=>'mixed', 'defval='=>'mixed', 'delete='=>'bool|false'], From 5b77fa68d623dd89408171615f50352fd7d19ff0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 19 Nov 2024 10:56:22 +0100 Subject: [PATCH 133/220] Fix test --- .../Reflection/SignatureMap/Php8SignatureMapProviderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index 41d62d1c36..9302b20f69 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -206,7 +206,7 @@ public function dataMethods(): array 'variadic' => false, ], ], - new UnionType([ + new BenevolentUnionType([ new ObjectType('Closure'), new NullType(), ]), From cb9be914e7675176babaeab8647c149bec3347ed Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 19 Nov 2024 10:59:55 +0100 Subject: [PATCH 134/220] Fix sprintf dynamic return type --- ...intfFunctionDynamicReturnTypeExtension.php | 23 +++++----- tests/PHPStan/Analyser/nsrt/bug-12065.php | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12065.php diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index dc30f2afdd..e379c4cc3c 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -29,6 +29,7 @@ use function count; use function in_array; use function intval; +use function is_array; use function is_string; use function preg_match; use function sprintf; @@ -69,7 +70,7 @@ public function getTypeFromFunctionCall( static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes() ); - $singlePlaceholderEarlyReturn = null; + $singlePlaceholderEarlyReturn = []; $allPatternsNonEmpty = count($formatStrings) !== 0; $allPatternsNonFalsy = count($formatStrings) !== 0; foreach ($formatStrings as $constantString) { @@ -93,8 +94,11 @@ public function getTypeFromFunctionCall( $allPatternsNonFalsy = false; } - // The printf format is %[argnum$][flags][width][.precision]specifier. - if (preg_match('/^%(?P[0-9]*\$)?(?P[0-9]*)\.?[0-9]*(?P[sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) { + if ( + is_array($singlePlaceholderEarlyReturn) + // The printf format is %[argnum$][flags][width][.precision]specifier. + && preg_match('/^%(?P[0-9]*\$)?(?P[0-9]*)\.?[0-9]*(?P[sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1 + ) { if ($matches['argnum'] !== '') { // invalid positional argument if ($matches['argnum'] === '0$') { @@ -122,24 +126,22 @@ public function getTypeFromFunctionCall( $constArgTypes = $checkArgType->getConstantScalarTypes(); } if ($constArgTypes !== []) { - $result = []; $printfArgs = array_fill(0, count($args) - 1, ''); foreach ($constArgTypes as $constArgType) { $printfArgs[$checkArg - 1] = $constArgType->getValue(); try { - $result[] = new ConstantStringType(@sprintf($constantString->getValue(), ...$printfArgs)); + $singlePlaceholderEarlyReturn[] = new ConstantStringType(@sprintf($constantString->getValue(), ...$printfArgs)); } catch (Throwable) { continue 2; } } - $singlePlaceholderEarlyReturn = TypeCombinator::union(...$result); continue; } - $singlePlaceholderEarlyReturn = $checkArgType->toString(); + $singlePlaceholderEarlyReturn[] = $checkArgType->toString(); } elseif ($matches['specifier'] !== 's') { - $singlePlaceholderEarlyReturn = $this->getStringReturnType( + $singlePlaceholderEarlyReturn[] = $this->getStringReturnType( new AccessoryNumericStringType(), $isLowercase, ); @@ -149,11 +151,10 @@ public function getTypeFromFunctionCall( } $singlePlaceholderEarlyReturn = null; - break; } - if ($singlePlaceholderEarlyReturn !== null) { - return $singlePlaceholderEarlyReturn; + if (is_array($singlePlaceholderEarlyReturn) && count($singlePlaceholderEarlyReturn) > 0) { + return TypeCombinator::union(...$singlePlaceholderEarlyReturn); } if ($allPatternsNonFalsy) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12065.php b/tests/PHPStan/Analyser/nsrt/bug-12065.php new file mode 100644 index 0000000000..e0f9353eec --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12065.php @@ -0,0 +1,43 @@ + $key + * @param bool $preserveKeys + * + * @return void + */ + public function bar2( + string $key, + bool $preserveKeys, + ): void { + $format = $preserveKeys ? '%s' : '%d'; + + $_key = sprintf($format, $key); + assertType("string", $_key); + } +} From a34c2f4c688c7394d034058f59e1c6d70da3e2ed Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 19 Nov 2024 16:38:41 +0200 Subject: [PATCH 135/220] Update curl_setopt string values and allow nullable --- src/Reflection/ParametersAcceptorSelector.php | 69 +++++++++++++++---- .../CallToFunctionParametersRuleTest.php | 26 +++++-- .../Rules/Functions/data/curl_setopt.php | 6 ++ 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index fd2f2b8ae1..82af7d4267 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -918,49 +918,91 @@ private static function getCurlOptValueType(int $curlOpt): ?Type } } + $nullableStringConstants = [ + 'CURLOPT_ACCEPT_ENCODING', + 'CURLOPT_CUSTOMREQUEST', + 'CURLOPT_DNS_INTERFACE', + 'CURLOPT_DNS_LOCAL_IP4', + 'CURLOPT_DNS_LOCAL_IP6', + 'CURLOPT_DOH_URL', + 'CURLOPT_FTP_ACCOUNT', + 'CURLOPT_FTPPORT', + 'CURLOPT_HSTS', + 'CURLOPT_KRBLEVEL', + 'CURLOPT_RANGE', + 'CURLOPT_RTSP_SESSION_ID', + 'CURLOPT_UNIX_SOCKET_PATH', + 'CURLOPT_XOAUTH2_BEARER', + ]; + foreach ($nullableStringConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new UnionType([ + new NullType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + ]); + } + } + $nonEmptyStringConstants = [ 'CURLOPT_ABSTRACT_UNIX_SOCKET', + 'CURLOPT_ALTSVC', + 'CURLOPT_AWS_SIGV4', 'CURLOPT_CAINFO', 'CURLOPT_CAPATH', 'CURLOPT_COOKIE', 'CURLOPT_COOKIEJAR', 'CURLOPT_COOKIELIST', - 'CURLOPT_CUSTOMREQUEST', 'CURLOPT_DEFAULT_PROTOCOL', - 'CURLOPT_DNS_INTERFACE', - 'CURLOPT_DNS_LOCAL_IP4', - 'CURLOPT_DNS_LOCAL_IP6', + 'CURLOPT_DNS_SERVERS', 'CURLOPT_EGDSOCKET', - 'CURLOPT_FTPPORT', + 'CURLOPT_FTP_ALTERNATIVE_TO_USER', 'CURLOPT_INTERFACE', 'CURLOPT_KEYPASSWD', 'CURLOPT_KRB4LEVEL', 'CURLOPT_LOGIN_OPTIONS', + 'CURLOPT_MAIL_AUTH', + 'CURLOPT_MAIL_FROM', + 'CURLOPT_NOPROXY', + 'CURLOPT_PASSWORD', 'CURLOPT_PINNEDPUBLICKEY', - 'CURLOPT_PROXY_SERVICE_NAME', + 'CURLOPT_PROTOCOLS_STR', 'CURLOPT_PROXY_CAINFO', 'CURLOPT_PROXY_CAPATH', 'CURLOPT_PROXY_CRLFILE', + 'CURLOPT_PROXY_ISSUERCERT', 'CURLOPT_PROXY_KEYPASSWD', 'CURLOPT_PROXY_PINNEDPUBLICKEY', + 'CURLOPT_PROXY_SERVICE_NAME', + 'CURLOPT_PROXY_SSL_CIPHER_LIST', 'CURLOPT_PROXY_SSLCERT', 'CURLOPT_PROXY_SSLCERTTYPE', - 'CURLOPT_PROXY_SSL_CIPHER_LIST', - 'CURLOPT_PROXY_TLS13_CIPHERS', 'CURLOPT_PROXY_SSLKEY', 'CURLOPT_PROXY_SSLKEYTYPE', + 'CURLOPT_PROXY_TLS13_CIPHERS', 'CURLOPT_PROXY_TLSAUTH_PASSWORD', 'CURLOPT_PROXY_TLSAUTH_TYPE', 'CURLOPT_PROXY_TLSAUTH_USERNAME', + 'CURLOPT_PROXYPASSWORD', + 'CURLOPT_PROXYUSERNAME', 'CURLOPT_PROXYUSERPWD', 'CURLOPT_RANDOM_FILE', - 'CURLOPT_RANGE', + 'CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REFERER', + 'CURLOPT_REQUEST_TARGET', + 'CURLOPT_RTSP_STREAM_URI', + 'CURLOPT_RTSP_TRANSPORT', + 'CURLOPT_SASL_AUTHZID', 'CURLOPT_SERVICE_NAME', + 'CURLOPT_SOCKS5_GSSAPI_SERVICE', 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5', - 'CURLOPT_SSH_PUBLIC_KEYFILE', + 'CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256', 'CURLOPT_SSH_PRIVATE_KEYFILE', + 'CURLOPT_SSH_PUBLIC_KEYFILE', 'CURLOPT_SSL_CIPHER_LIST', + 'CURLOPT_SSL_EC_CURVES', 'CURLOPT_SSLCERT', 'CURLOPT_SSLCERTPASSWD', 'CURLOPT_SSLCERTTYPE', @@ -970,13 +1012,14 @@ private static function getCurlOptValueType(int $curlOpt): ?Type 'CURLOPT_SSLKEYPASSWD', 'CURLOPT_SSLKEYTYPE', 'CURLOPT_TLS13_CIPHERS', - 'CURLOPT_UNIX_SOCKET_PATH', + 'CURLOPT_TLSAUTH_PASSWORD', + 'CURLOPT_TLSAUTH_TYPE', + 'CURLOPT_TLSAUTH_USERNAME', + 'CURLOPT_TRANSFER_ENCODING', 'CURLOPT_URL', 'CURLOPT_USERAGENT', 'CURLOPT_USERNAME', - 'CURLOPT_PASSWORD', 'CURLOPT_USERPWD', - 'CURLOPT_XOAUTH2_BEARER', ]; foreach ($nonEmptyStringConstants as $constName) { if (defined($constName) && constant($constName) === $curlOpt) { diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 7d28abb0fc..15213c15ea 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1316,33 +1316,45 @@ public function testCurlSetOpt(): void 'Parameter #3 $value of function curl_setopt expects array, int given.', 17, ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string, null given.', + 18, + ], [ 'Parameter #3 $value of function curl_setopt expects bool, int given.', - 19, + 20, ], [ 'Parameter #3 $value of function curl_setopt expects bool, string given.', - 20, + 21, ], [ 'Parameter #3 $value of function curl_setopt expects int, string given.', - 22, + 23, ], [ 'Parameter #3 $value of function curl_setopt expects array, string given.', - 24, + 25, ], [ 'Parameter #3 $value of function curl_setopt expects resource, string given.', - 26, + 27, ], [ 'Parameter #3 $value of function curl_setopt expects array|string, int given.', - 28, + 29, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string, \'\' given.', + 31, + ], + [ + 'Parameter #3 $value of function curl_setopt expects non-empty-string|null, \'\' given.', + 32, ], [ 'Parameter #3 $value of function curl_setopt expects array, array given.', - 67, + 73, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php index 76d3136a2d..24987ddbc9 100644 --- a/tests/PHPStan/Rules/Functions/data/curl_setopt.php +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -15,6 +15,7 @@ public function errors(int $i, string $s) { // expecting string curl_setopt($curl, CURLOPT_URL, $i); curl_setopt($curl, CURLOPT_HTTPHEADER, $i); + curl_setopt($curl, CURLOPT_ABSTRACT_UNIX_SOCKET, null); // expecting bool curl_setopt($curl, CURLOPT_AUTOREFERER, $i); curl_setopt($curl, CURLOPT_RETURNTRANSFER, $s); @@ -26,6 +27,9 @@ public function errors(int $i, string $s) { curl_setopt($curl, CURLOPT_FILE, $s); // expecting string or array curl_setopt($curl, CURLOPT_POSTFIELDS, $i); + // expecting non empty string + curl_setopt($curl, CURLOPT_URL, ''); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, ''); } /** @@ -41,6 +45,8 @@ public function allGood(string $url, array $header) { curl_setopt($curl, CURLOPT_AUTOREFERER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, null); $fp = fopen("example_homepage.txt", "w"); if ($fp === false) { From 3b47bc000091256f645c22e99d8823332a6cb43d Mon Sep 17 00:00:00 2001 From: Claude Pache Date: Tue, 19 Nov 2024 18:45:08 +0100 Subject: [PATCH 136/220] bccomp: more precise return type --- resources/functionMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index baaef68a8c..2b1df1a790 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -418,7 +418,7 @@ 'bbcode_set_arg_parser' => ['bool', 'bbcode_container'=>'resource', 'bbcode_arg_parser'=>'resource'], 'bbcode_set_flags' => ['bool', 'bbcode_container'=>'resource', 'flags'=>'int', 'mode='=>'int'], 'bcadd' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], -'bccomp' => ['int', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], +'bccomp' => ['0|1|-1', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bcdiv' => ['numeric-string|null', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bcmod' => ['numeric-string|null', 'left_operand'=>'string', 'right_operand'=>'numeric-string', 'scale='=>'int'], 'bcmul' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'], From 68d01a5f68c1e0897fd54e80c2b896ed98ae03a1 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Tue, 19 Nov 2024 19:33:41 +0000 Subject: [PATCH 137/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 18 +++++++++--------- ...ctionEnumCaseDynamicReturnTypeExtension.php | 8 ++------ ...eflectionEnumDynamicReturnTypeExtension.php | 5 +---- .../adapter-reflection-enum-return-types.php | 9 +++++---- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 15e25f2a80..73f1cc620f 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.42.0.11", + "ondrejmirtes/better-reflection": "6.43.0.2", "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 8ee3e675ed..d2adb5606c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ca308caf852aa82ffe555d1235c75efa", + "content-hash": "a4991601b7590d9dc65443dfb5d34d16", "packages": [ { "name": "clue/ndjson-react", @@ -2187,22 +2187,22 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.42.0.11", + "version": "6.43.0.2", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "1ff390ad28758b6baa20c9a45b17926bdd850e44" + "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/1ff390ad28758b6baa20c9a45b17926bdd850e44", - "reference": "1ff390ad28758b6baa20c9a45b17926bdd850e44", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c34ee726f9abc5a7057b0dacdf1c0991c9090584", + "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584", "shasum": "" }, "require": { "ext-json": "*", "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", - "nikic/php-parser": "^5.1.0", + "nikic/php-parser": "^5.3.1", "php": "^7.4 || ^8.0" }, "conflict": { @@ -2212,7 +2212,7 @@ "doctrine/coding-standard": "^12.0.0", "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", - "phpunit/phpunit": "^11.3.3", + "phpunit/phpunit": "^11.4.3", "rector/rector": "0.14.3" }, "suggest": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.42.0.11" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.2" }, - "time": "2024-10-12T09:30:53+00:00" + "time": "2024-11-19T19:32:34+00:00" }, { "name": "phpstan/php-8-stubs", diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php index d242403fb6..4e1cfbcea6 100644 --- a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php @@ -4,9 +4,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionType; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; @@ -56,9 +54,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ($methodReflection->getName() === 'getType') { return new UnionType([ - new ObjectType(ReflectionIntersectionType::class), - new ObjectType(ReflectionNamedType::class), - new ObjectType(ReflectionUnionType::class), + new ObjectType(ReflectionType::class), new NullType(), ]); } diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php index f0e25e9296..dfa3218442 100644 --- a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php @@ -72,10 +72,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (in_array($methodReflection->getName(), ['getStartLine', 'getEndLine'], true)) { - return new UnionType([ - new IntegerType(), - new ConstantBooleanType(false), - ]); + return new IntegerType(); } if ($methodReflection->getName() === 'getReflectionConstant') { diff --git a/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php index 4002e1ce16..d21d17e739 100644 --- a/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php +++ b/tests/PHPStan/Analyser/nsrt/adapter-reflection-enum-return-types.php @@ -5,12 +5,13 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumUnitCase; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionType; use function PHPStan\Testing\assertType; function (ReflectionEnum $r, string $s): void { assertType('non-empty-string|false', $r->getFileName()); - assertType('int|false', $r->getStartLine()); - assertType('int|false', $r->getEndLine()); + assertType('int', $r->getStartLine()); + assertType('int', $r->getEndLine()); assertType('string|false', $r->getDocComment()); assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant|false', $r->getReflectionConstant($s)); assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass|false', $r->getParentClass()); @@ -20,10 +21,10 @@ function (ReflectionEnum $r, string $s): void { function (ReflectionEnumBackedCase $r): void { assertType('string|false', $r->getDocComment()); - assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType|PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType|PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType|null', $r->getType()); + assertType(ReflectionType::class . '|null', $r->getType()); }; function (ReflectionEnumUnitCase $r): void { assertType('string|false', $r->getDocComment()); - assertType('PHPStan\BetterReflection\Reflection\Adapter\ReflectionIntersectionType|PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType|PHPStan\BetterReflection\Reflection\Adapter\ReflectionUnionType|null', $r->getType()); + assertType(ReflectionType::class . '|null', $r->getType()); }; From ca2c937dff543ee80599ac11a1bd4235164e2b6a Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" <25754+pmjones@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:22:48 -0600 Subject: [PATCH 138/220] Introduce uppercase-string --- conf/config.neon | 10 + phpstan-baseline.neon | 5 + resources/functionMap.php | 4 +- src/PhpDoc/TypeNodeResolver.php | 11 + .../InitializerExprTypeResolver.php | 6 + src/Rules/Api/ApiInstanceofTypeRule.php | 2 + .../StrictComparisonOfDifferentTypesRule.php | 11 +- src/Type/Accessory/AccessoryArrayListType.php | 5 + .../Accessory/AccessoryLiteralStringType.php | 5 + .../AccessoryLowercaseStringType.php | 5 + .../Accessory/AccessoryNonEmptyStringType.php | 5 + .../Accessory/AccessoryNonFalsyStringType.php | 5 + .../Accessory/AccessoryNumericStringType.php | 5 + .../AccessoryUppercaseStringType.php | 390 ++++++++++++++++++ src/Type/Accessory/HasOffsetType.php | 5 + src/Type/Accessory/HasOffsetValueType.php | 5 + src/Type/Accessory/NonEmptyArrayType.php | 5 + src/Type/Accessory/OversizedArrayType.php | 5 + src/Type/ArrayType.php | 5 + src/Type/CallableType.php | 5 + src/Type/ClassStringType.php | 5 + src/Type/ClosureType.php | 5 + src/Type/Constant/ConstantStringType.php | 11 + src/Type/FloatType.php | 5 + src/Type/IntersectionType.php | 14 +- src/Type/IterableType.php | 5 + src/Type/JustNullableTypeTrait.php | 5 + src/Type/MixedType.php | 17 + src/Type/NeverType.php | 5 + src/Type/NullType.php | 5 + src/Type/ObjectType.php | 5 + ...lodeFunctionDynamicReturnTypeExtension.php | 11 +- .../ImplodeFunctionReturnTypeExtension.php | 4 + .../Php/ParseStrParameterOutTypeExtension.php | 60 +++ ...aceFunctionsDynamicReturnTypeExtension.php | 5 + .../StrCaseFunctionsReturnTypeExtension.php | 21 + .../Php/StrPadFunctionReturnTypeExtension.php | 4 + .../StrRepeatFunctionReturnTypeExtension.php | 5 + .../Php/SubstrDynamicReturnTypeExtension.php | 4 + ...TrimFunctionDynamicReturnTypeExtension.php | 52 +++ src/Type/StaticType.php | 5 + src/Type/StrictMixedType.php | 5 + src/Type/StringType.php | 5 + src/Type/Traits/LateResolvableTypeTrait.php | 5 + src/Type/Traits/ObjectTypeTrait.php | 5 + src/Type/Type.php | 2 + src/Type/UnionType.php | 5 + src/Type/VerbosityLevel.php | 6 +- src/Type/VoidType.php | 5 + stubs/core.stub | 19 +- .../Analyser/LegacyNodeScopeResolverTest.php | 6 +- tests/PHPStan/Analyser/data/explode-php74.php | 3 +- tests/PHPStan/Analyser/data/explode-php80.php | 3 +- tests/PHPStan/Analyser/data/param-out.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-4711.php | 4 +- .../nsrt/isset-coalesce-empty-type.php | 2 +- .../PHPStan/Analyser/nsrt/literal-string.php | 14 +- ...-subtr.php => lowercase-string-substr.php} | 0 tests/PHPStan/Analyser/nsrt/more-types.php | 6 + .../Analyser/nsrt/non-empty-string.php | 8 +- .../Analyser/nsrt/non-falsy-string.php | 4 +- tests/PHPStan/Analyser/nsrt/str-casing.php | 24 +- .../nsrt/uppercase-string-implode.php | 22 + .../Analyser/nsrt/uppercase-string-pad.php | 23 ++ .../Analyser/nsrt/uppercase-string-parse.php | 36 ++ .../Analyser/nsrt/uppercase-string-repeat.php | 19 + .../nsrt/uppercase-string-replace.php | 42 ++ .../Analyser/nsrt/uppercase-string-substr.php | 30 ++ .../Analyser/nsrt/uppercase-string-trim.php | 29 ++ ...rictComparisonOfDifferentTypesRuleTest.php | 48 +++ .../Comparison/data/uppercase-string.php | 31 ++ .../CallToFunctionParametersRuleTest.php | 2 +- .../Rules/Methods/CallMethodsRuleTest.php | 23 ++ .../Rules/Methods/data/uppercase-string.php | 33 ++ .../Type/Constant/ConstantStringTypeTest.php | 12 +- .../Constant/OversizedArrayBuilderTest.php | 9 + tests/PHPStan/Type/IntersectionTypeTest.php | 16 + tests/PHPStan/Type/TypeCombinatorTest.php | 114 +++++ tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 11 + 79 files changed, 1321 insertions(+), 66 deletions(-) create mode 100644 src/Type/Accessory/AccessoryUppercaseStringType.php create mode 100644 src/Type/Php/ParseStrParameterOutTypeExtension.php create mode 100644 src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php rename tests/PHPStan/Analyser/nsrt/{lowercase-string-subtr.php => lowercase-string-substr.php} (100%) create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-repeat.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-replace.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-substr.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php create mode 100644 tests/PHPStan/Rules/Comparison/data/uppercase-string.php create mode 100644 tests/PHPStan/Rules/Methods/data/uppercase-string.php diff --git a/conf/config.neon b/conf/config.neon index 11e845136d..f8ad1af8b3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1564,6 +1564,11 @@ services: tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\ParseStrParameterOutTypeExtension + tags: + - phpstan.functionParameterOutTypeExtension + - class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension tags: @@ -1774,6 +1779,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\TrimFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension tags: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 498207dbe7..1662d1fa9a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -715,6 +715,11 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryNumericStringType.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryUppercaseStringType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 diff --git a/resources/functionMap.php b/resources/functionMap.php index 620138e0bc..9891593eb3 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -6362,7 +6362,7 @@ 'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], 'mb_strtolower' => ['lowercase-string', 'str'=>'string', 'encoding='=>'string'], -'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtoupper' => ['uppercase-string', 'str'=>'string', 'encoding='=>'string'], 'mb_strwidth' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mb_substitute_character' => ['mixed', 'substchar='=>'mixed'], 'mb_substr' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], @@ -12087,7 +12087,7 @@ 'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], 'strtolower' => ['lowercase-string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], -'strtoupper' => ['string', 'str'=>'string'], +'strtoupper' => ['uppercase-string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], 'strval' => ['string', 'var'=>'__stringAndStringable|int|float|bool|resource|null'], diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 43f93cb8f5..744265adf6 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -50,6 +50,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -223,6 +224,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'lowercase-string': return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + case 'uppercase-string': + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); @@ -303,6 +307,13 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryLowercaseStringType(), ]); + case 'non-empty-uppercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryUppercaseStringType(), + ]); + case 'truthy-string': case 'non-falsy-string': return new IntersectionType([ diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 8cedcd1272..a55fd7831d 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -31,6 +31,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -480,10 +481,15 @@ public function resolveConcatType(Type $left, Type $right): Type if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } + if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { $allRightConstantsZeroOrMore = false; diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php index 225e65b000..ccda700696 100644 --- a/src/Rules/Api/ApiInstanceofTypeRule.php +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -16,6 +16,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; @@ -86,6 +87,7 @@ final class ApiInstanceofTypeRule implements Rule AccessoryNumericStringType::class => 'Type::isNumericString()', AccessoryLiteralStringType::class => 'Type::isLiteralString()', AccessoryLowercaseStringType::class => 'Type::isLowercaseString()', + AccessoryUppercaseStringType::class => 'Type::isUppercaseString()', AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', HasMethodType::class => 'Type::hasMethod()', diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 7d9c764132..d155be352a 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -75,19 +75,26 @@ public function processNode(Node $node, Scope $scope): array }; $verbosity = VerbosityLevel::value(); + if ( ( $leftType->isConstantScalarValue()->yes() && !$leftType->isString()->no() && !$rightType->isConstantScalarValue()->yes() && !$rightType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) ) || ( $rightType->isConstantScalarValue()->yes() && !$rightType->isString()->no() && !$leftType->isConstantScalarValue()->yes() && !$leftType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) ) ) { $verbosity = VerbosityLevel::precise(); diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 491af5b813..c2eea34349 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -414,6 +414,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 700063570e..d0970ba346 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -313,6 +313,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index fe57034af1..672998fdb8 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -309,6 +309,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 6587e85d35..117779e376 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -310,6 +310,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 2a56264475..4bfb46f436 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -310,6 +310,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 1ab8d31eff..79c83c7b0f 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -312,6 +312,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php new file mode 100644 index 0000000000..3ce287d614 --- /dev/null +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -0,0 +1,390 @@ +acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + return new AcceptsResult($type->isUppercaseString(), []); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + return $this->isSuperTypeOfWithReason($type)->result; + } + + public function isSuperTypeOfWithReason(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOfWithReason($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isUppercaseString(), []); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + return $this->isSubTypeOfWithReason($otherType)->result; + } + + public function isSubTypeOfWithReason(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOfWithReason($this); + } + + return (new IsSuperTypeOfResult($otherType->isUppercaseString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOfWithReason($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'uppercase-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isUppercaseString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toBoolean(): BooleanType + { + return new BooleanType(); + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('uppercase-string'); + } + +} diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index f1ccf3c2a2..23650ed8d3 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -329,6 +329,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 600fa6ca63..b673a3e7eb 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -385,6 +385,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index c822edcc7a..7b9312e6c5 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -390,6 +390,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 4d39431f7b..3de317ba0e 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -379,6 +379,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index c3fc5050b1..74b47dea0b 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -373,6 +373,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index d9bbea57e6..f182372fc0 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -606,6 +606,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 2f58e27d15..30e761b660 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -79,6 +79,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index bbe6d5a73e..d12dda936e 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -723,6 +723,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 6350d17cef..f0e78cf903 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -24,6 +24,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -51,6 +52,7 @@ use function key; use function strlen; use function strtolower; +use function strtoupper; use function substr; use function substr_count; @@ -356,6 +358,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtoupper($this->value) === $this->value); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isInteger()->yes()) { @@ -467,6 +474,10 @@ public function generalize(GeneralizePrecision $precision): Type $accessories[] = new AccessoryLowercaseStringType(); } + if (strtoupper($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + return new IntersectionType($accessories); } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index bd5b2a9082..40ea01b5db 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -239,6 +239,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 6a8d2ab329..72fecc7de6 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -26,6 +26,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -341,11 +342,14 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType ) { - if ($type instanceof AccessoryLowercaseStringType && !$level->isPrecise()) { + if ( + ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) + && !$level->isPrecise() + ) { continue; } - if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; } @@ -659,6 +663,11 @@ public function isLowercaseString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); } + public function isUppercaseString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); @@ -1166,6 +1175,7 @@ public function toPhpDocNode(): TypeNode || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 662d2e1b0c..2d0c632298 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -387,6 +387,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index df6bee28b8..9545fff1da 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -157,6 +157,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 0b9d87394a..3ae9434a95 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -25,6 +25,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -963,6 +964,22 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $uppercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryUppercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($uppercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 45f9cb05f3..a3d8daf1fe 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -496,6 +496,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 7b60d5d3cb..2bd76023f8 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -305,6 +305,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 7427aa167c..9c633d62da 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1046,6 +1046,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index ccad08cefb..d675eee45c 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -57,8 +58,16 @@ public function getTypeFromFunctionCall( } $stringType = $scope->getType($args[1]->value); + $accessory = []; if ($stringType->isLowercaseString()->yes()) { - $returnValueType = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $returnValueType = new IntersectionType($accessory); } else { $returnValueType = new StringType(); } diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index d29e20a7cc..5c680b5b62 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; @@ -94,6 +95,9 @@ private function implode(Type $arrayType, Type $separatorType): Type if ($arrayType->getIterableValueType()->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($arrayType->getIterableValueType()->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php new file mode 100644 index 0000000000..e0c7aa47e5 --- /dev/null +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -0,0 +1,60 @@ +getName()), ['parse_str', 'mb_parse_str'], true) + && $parameter->getName() === 'result'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $valueType = new IntersectionType($accessory); + } else { + $valueType = new StringType(); + } + + return new ArrayType( + new UnionType([new StringType(), new IntegerType()]), + new UnionType([new ArrayType(new MixedType(), new MixedType()), $valueType]), + ); + } + +} diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index c80c10d9c0..db046bbe10 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; @@ -100,6 +101,10 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( $accessories[] = new AccessoryLowercaseStringType(); } + if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + if (count($accessories) > 0) { $accessories[] = new StringType(); return new IntersectionType($accessories); diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 149caff081..db36561ab6 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; @@ -23,6 +24,7 @@ use function is_callable; use function mb_check_encoding; use const MB_CASE_LOWER; +use const MB_CASE_UPPER; final class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -70,6 +72,8 @@ public function getTypeFromFunctionCall( $modes = []; $keepLowercase = false; $forceLowercase = false; + $keepUppercase = false; + $forceUppercase = false; if ($fnName === 'mb_convert_case') { $modeType = $scope->getType($args[1]->value); @@ -85,6 +89,16 @@ public function getTypeFromFunctionCall( 3, // MB_CASE_FOLD, 7, // MB_CASE_FOLD_SIMPLE ])) === 0; + $forceUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + ])) === 0; + $keepUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE + ])) === 0; } } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { if (count($args) >= 2) { @@ -97,6 +111,10 @@ public function getTypeFromFunctionCall( $forceLowercase = true; } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'], true)) { $keepLowercase = true; + } elseif (in_array($fnName, ['strtoupper', 'mb_strtoupper'], true)) { + $forceUppercase = true; + } elseif (in_array($fnName, ['ucfirst', 'mb_ucfirst'], true)) { + $keepUppercase = true; } $constantStrings = array_map(static fn ($type) => $type->getValue(), $argType->getConstantStrings()); @@ -127,6 +145,9 @@ public function getTypeFromFunctionCall( if ($forceLowercase || ($keepLowercase && $argType->isLowercaseString()->yes())) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($forceUppercase || ($keepUppercase && $argType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if ($argType->isNumericString()->yes()) { $accessoryTypes[] = new AccessoryNumericStringType(); diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php index 92b0ec286d..d556fff1be 100644 --- a/src/Type/Php/StrPadFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; @@ -57,6 +58,9 @@ public function getTypeFromFunctionCall( if ($inputType->isLowercaseString()->yes() && ($padStringType === null || $padStringType->isLowercaseString()->yes())) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($inputType->isUppercaseString()->yes() && ($padStringType === null || $padStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index 632fea85f0..98afacb7d7 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -98,6 +99,10 @@ public function getTypeFromFunctionCall( $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($inputType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 4364f2374f..df1d0cf060 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -99,6 +100,9 @@ public function getTypeFromFunctionCall( if ($string->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($string->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { $isNotEmpty = true; if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..df06d98b43 --- /dev/null +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,52 @@ +getName(), ['trim', 'rtrim', 'ltrim'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + return new IntersectionType($accessory); + } + + return new StringType(); + } + +} diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 5fd6f7e358..7056fc5901 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -580,6 +580,11 @@ public function isLowercaseString(): TrinaryLogic return $this->getStaticObjectType()->isLowercaseString(); } + public function isUppercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isUppercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->getStaticObjectType()->isClassStringType(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 13b0fee7de..c839779a55 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -290,6 +290,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 9fcd1e1895..c49cf7823c 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -245,6 +245,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 76855da1f4..e6355875ca 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -468,6 +468,11 @@ public function isLowercaseString(): TrinaryLogic return $this->resolve()->isLowercaseString(); } + public function isUppercaseString(): TrinaryLogic + { + return $this->resolve()->isUppercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->resolve()->isClassStringType(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 8ca9c69c29..7cf3659a50 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -203,6 +203,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 6c5064f4ea..6562ba2005 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -274,6 +274,8 @@ public function isLiteralString(): TrinaryLogic; public function isLowercaseString(): TrinaryLogic; + public function isUppercaseString(): TrinaryLogic; + public function isClassStringType(): TrinaryLogic; public function isVoid(): TrinaryLogic; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f0bef45903..6f4cbf462b 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -650,6 +650,11 @@ public function isLowercaseString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); } + public function isUppercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 47b0d2477c..988b2a2405 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -8,6 +8,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; @@ -108,7 +109,10 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = true; return $type; } - if ($type instanceof AccessoryLowercaseStringType) { + if ( + $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { $moreVerbose = true; $veryVerbose = true; return $type; diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 4353c6efdd..18b3d719c1 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -217,6 +217,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/stubs/core.stub b/stubs/core.stub index abd719b4f5..d6c1463c81 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -69,13 +69,13 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out ($string is lowercase-string ? array|lowercase-string> : array|string>) $result + * @param-out array|string> $result */ function parse_str(string $string, array &$result): void {} /** * @param array $result - * @param-out array|string> $result + * @param-out array|string> $result */ function mb_parse_str(string $string, array &$result): bool {} @@ -314,21 +314,6 @@ function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable */ function abs($num) {} -/** - * @return ($string is lowercase-string ? lowercase-string : string) - */ -function trim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string ? lowercase-string : string) - */ -function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string ? lowercase-string : string) - */ -function rtrim(string $string, string $characters = " \n\r\t\v\x00"): string {} - /** * @return ($categorize is true ? array> : array) */ diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index aea1961e32..5434782186 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -1129,11 +1129,11 @@ public function dataArrayDestructuring(): array '$fourthStringArrayForeachList', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$dateArray[\'Y\']', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$dateArray[\'m\']', ], [ @@ -1141,7 +1141,7 @@ public function dataArrayDestructuring(): array '$dateArray[\'d\']', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$intArrayForRewritingFirstElement[0]', ], [ diff --git a/tests/PHPStan/Analyser/data/explode-php74.php b/tests/PHPStan/Analyser/data/explode-php74.php index efdd404424..b205b1d0be 100644 --- a/tests/PHPStan/Analyser/data/explode-php74.php +++ b/tests/PHPStan/Analyser/data/explode-php74.php @@ -9,6 +9,7 @@ class ExplodingStrings public function doFoo(string $s): void { assertType('non-empty-list|false', explode($s, 'foo')); - assertType('non-empty-list|false', explode($s, 'FOO')); + assertType('non-empty-list|false', explode($s, 'FOO')); + assertType('non-empty-list|false', explode($s, 'Foo')); } } diff --git a/tests/PHPStan/Analyser/data/explode-php80.php b/tests/PHPStan/Analyser/data/explode-php80.php index 4fa4539356..1c01239587 100644 --- a/tests/PHPStan/Analyser/data/explode-php80.php +++ b/tests/PHPStan/Analyser/data/explode-php80.php @@ -9,6 +9,7 @@ class ExplodingStrings public function doFoo(string $s): void { assertType('non-empty-list', explode($s, 'foo')); - assertType('non-empty-list', explode($s, 'FOO')); + assertType('non-empty-list', explode($s, 'FOO')); + assertType('non-empty-list', explode($s, 'Foo')); } } diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 05684938b8..341a4069d4 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -392,10 +392,10 @@ function fooHeadersSent() { function fooMbParseStr() { mb_parse_str("foo=bar", $output); - assertType('array', $output); + assertType('array', $output); mb_parse_str('email=mail@example.org&city=town&x=1&y[g]=3&f=1.23', $output); - assertType('array', $output); + assertType('array', $output); } function fooPreg() diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php index 8fbed765a9..9050c74c15 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4711.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4711.php @@ -12,8 +12,8 @@ function x(string $string): void { return; } - assertType('non-empty-list', explode($string, '')); - assertType('non-empty-list', explode($string[0], '')); + assertType('non-empty-list', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); } } diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php index a38116b682..8ffb1ddb89 100644 --- a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php @@ -472,7 +472,7 @@ function coalesce() assertType('int<0, max>', rand() ?? false); - assertType('0|lowercase-string', preg_replace('', '', '') ?? 0); + assertType('0|(lowercase-string&uppercase-string)', preg_replace('', '', '') ?? 0); $foo = new FooCoalesce(); diff --git a/tests/PHPStan/Analyser/nsrt/literal-string.php b/tests/PHPStan/Analyser/nsrt/literal-string.php index 93bf8949d9..c30fbdac80 100644 --- a/tests/PHPStan/Analyser/nsrt/literal-string.php +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -37,8 +37,9 @@ public function doFoo($literalString, string $string, $numericString) str_repeat('a', 99) ); assertType('literal-string&lowercase-string&non-falsy-string', str_repeat('a', 100)); - assertType('literal-string&lowercase-string&non-empty-string&numeric-string', str_repeat('0', 100)); // could be non-falsy-string - assertType('literal-string&lowercase-string&non-falsy-string&numeric-string', str_repeat('1', 100)); + assertType('literal-string&non-falsy-string&uppercase-string', str_repeat('A', 100)); + assertType('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', str_repeat('0', 100)); // could be non-falsy-string + assertType('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', str_repeat('1', 100)); // Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ assertType('non-empty-string', str_repeat($numericString, 100)); @@ -51,13 +52,14 @@ public function doFoo($literalString, string $string, $numericString) assertType("non-empty-string", str_repeat($numericString, 2)); assertType("literal-string", str_repeat($literalString, 1)); $x = rand(1,2); - assertType("literal-string&lowercase-string&non-falsy-string", str_repeat(' 1 ', $x)); - assertType("literal-string&lowercase-string&non-falsy-string", str_repeat('+1', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat(' 1 ', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat('+1', $x)); assertType("literal-string&lowercase-string&non-falsy-string", str_repeat('1e9', $x)); - assertType("literal-string&lowercase-string&non-falsy-string&numeric-string", str_repeat('19', $x)); + assertType("literal-string&non-falsy-string&uppercase-string", str_repeat('1E9', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string", str_repeat('19', $x)); $x = rand(0,2); - assertType("literal-string&lowercase-string", str_repeat('19', $x)); + assertType("literal-string&lowercase-string&uppercase-string", str_repeat('19', $x)); $x = rand(-10,-1); assertType("*NEVER*", str_repeat('19', $x)); diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-substr.php similarity index 100% rename from tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php rename to tests/PHPStan/Analyser/nsrt/lowercase-string-substr.php diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php index 9c3296c3d8..e98bc06a76 100644 --- a/tests/PHPStan/Analyser/nsrt/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -19,6 +19,8 @@ class Foo * @param non-empty-mixed $nonEmptyMixed * @param lowercase-string $lowercaseString * @param non-empty-lowercase-string $nonEmptyLowercaseString + * @param uppercase-string $uppercaseString + * @param non-empty-uppercase-string $nonEmptyUppercaseString */ public function doFoo( $pureCallable, @@ -32,6 +34,8 @@ public function doFoo( $nonEmptyMixed, $lowercaseString, $nonEmptyLowercaseString, + $uppercaseString, + $nonEmptyUppercaseString, ): void { assertType('pure-callable(): mixed', $pureCallable); @@ -45,6 +49,8 @@ public function doFoo( assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); assertType('lowercase-string', $lowercaseString); assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); + assertType('uppercase-string', $uppercaseString); + assertType('non-empty-string&uppercase-string', $nonEmptyUppercaseString); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 5400b1d31a..cd831db4d8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -318,12 +318,12 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', escapeshellcmd($s)); assertType('non-empty-string', escapeshellcmd($nonEmpty)); - assertType('string', strtoupper($s)); - assertType('non-empty-string', strtoupper($nonEmpty)); + assertType('uppercase-string', strtoupper($s)); + assertType('non-empty-string&uppercase-string', strtoupper($nonEmpty)); assertType('lowercase-string', strtolower($s)); assertType('lowercase-string&non-empty-string', strtolower($nonEmpty)); - assertType('string', mb_strtoupper($s)); - assertType('non-empty-string', mb_strtoupper($nonEmpty)); + assertType('uppercase-string', mb_strtoupper($s)); + assertType('non-empty-string&uppercase-string', mb_strtoupper($nonEmpty)); assertType('lowercase-string', mb_strtolower($s)); assertType('lowercase-string&non-empty-string', mb_strtolower($nonEmpty)); assertType('string', lcfirst($s)); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index 4127dd5870..c5fd9fc1d8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -87,9 +87,9 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', escapeshellarg($nonFalsey)); assertType('non-falsy-string', escapeshellcmd($nonFalsey)); - assertType('non-falsy-string', strtoupper($nonFalsey)); + assertType('non-falsy-string&uppercase-string', strtoupper($nonFalsey)); assertType('lowercase-string&non-falsy-string', strtolower($nonFalsey)); - assertType('non-falsy-string', mb_strtoupper($nonFalsey)); + assertType('non-falsy-string&uppercase-string', mb_strtoupper($nonFalsey)); assertType('lowercase-string&non-falsy-string', mb_strtolower($nonFalsey)); assertType('non-falsy-string', lcfirst($nonFalsey)); assertType('non-falsy-string', ucfirst($nonFalsey)); diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php index 3f0c76f250..ebdbd8054d 100644 --- a/tests/PHPStan/Analyser/nsrt/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -39,52 +39,52 @@ public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMo assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); assertType("lowercase-string&numeric-string", strtolower($numericS)); - assertType("numeric-string", strtoupper($numericS)); + assertType("numeric-string&uppercase-string", strtoupper($numericS)); assertType("lowercase-string&numeric-string", mb_strtolower($numericS)); - assertType("numeric-string", mb_strtoupper($numericS)); + assertType("numeric-string&uppercase-string", mb_strtoupper($numericS)); assertType("numeric-string", lcfirst($numericS)); assertType("numeric-string", ucfirst($numericS)); assertType("numeric-string", ucwords($numericS)); - assertType("numeric-string", mb_convert_case($numericS, MB_CASE_UPPER)); + assertType("numeric-string&uppercase-string", mb_convert_case($numericS, MB_CASE_UPPER)); assertType("lowercase-string&numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); assertType("numeric-string", mb_convert_case($numericS, $mixed)); assertType("numeric-string", mb_convert_kana($numericS)); assertType("numeric-string", mb_convert_kana($numericS, $mixed)); assertType("lowercase-string&non-empty-string", strtolower($nonE)); - assertType("non-empty-string", strtoupper($nonE)); + assertType("non-empty-string&uppercase-string", strtoupper($nonE)); assertType("lowercase-string&non-empty-string", mb_strtolower($nonE)); - assertType("non-empty-string", mb_strtoupper($nonE)); + assertType("non-empty-string&uppercase-string", mb_strtoupper($nonE)); assertType("non-empty-string", lcfirst($nonE)); assertType("non-empty-string", ucfirst($nonE)); assertType("non-empty-string", ucwords($nonE)); - assertType("non-empty-string", mb_convert_case($nonE, MB_CASE_UPPER)); + assertType("non-empty-string&uppercase-string", mb_convert_case($nonE, MB_CASE_UPPER)); assertType("lowercase-string&non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); assertType("non-empty-string", mb_convert_case($nonE, $mixed)); assertType("non-empty-string", mb_convert_kana($nonE)); assertType("non-empty-string", mb_convert_kana($nonE, $mixed)); assertType("lowercase-string", strtolower($literal)); - assertType("string", strtoupper($literal)); + assertType("uppercase-string", strtoupper($literal)); assertType("lowercase-string", mb_strtolower($literal)); - assertType("string", mb_strtoupper($literal)); + assertType("uppercase-string", mb_strtoupper($literal)); assertType("string", lcfirst($literal)); assertType("string", ucfirst($literal)); assertType("string", ucwords($literal)); - assertType("string", mb_convert_case($literal, MB_CASE_UPPER)); + assertType("uppercase-string", mb_convert_case($literal, MB_CASE_UPPER)); assertType("lowercase-string", mb_convert_case($literal, MB_CASE_LOWER)); assertType("string", mb_convert_case($literal, $mixed)); assertType("string", mb_convert_kana($literal)); assertType("string", mb_convert_kana($literal, $mixed)); assertType("lowercase-string", strtolower($lowercaseS)); - assertType("string", strtoupper($lowercaseS)); + assertType("uppercase-string", strtoupper($lowercaseS)); assertType("lowercase-string", mb_strtolower($lowercaseS)); - assertType("string", mb_strtoupper($lowercaseS)); + assertType("uppercase-string", mb_strtoupper($lowercaseS)); assertType("lowercase-string", lcfirst($lowercaseS)); assertType("string", ucfirst($lowercaseS)); assertType("string", ucwords($lowercaseS)); - assertType("string", mb_convert_case($lowercaseS, MB_CASE_UPPER)); + assertType("uppercase-string", mb_convert_case($lowercaseS, MB_CASE_UPPER)); assertType("lowercase-string", mb_convert_case($lowercaseS, MB_CASE_LOWER)); assertType("string", mb_convert_case($lowercaseS, $mixed)); assertType("lowercase-string", mb_convert_case($lowercaseS, rand(0, 1) ? MB_CASE_LOWER : MB_CASE_LOWER_SIMPLE)); diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php new file mode 100644 index 0000000000..2ddf808da2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php @@ -0,0 +1,22 @@ + $commonStrings + * @param array $uppercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $uppercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $uppercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('uppercase-string', implode($ls, $uppercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php new file mode 100644 index 0000000000..7045582dc4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php @@ -0,0 +1,23 @@ += 8.0 + +namespace UppercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param uppercase-string $uppercase + */ + public function doSubstr(string $uppercase): void + { + assertType('uppercase-string', substr($uppercase, 5)); + assertType('uppercase-string', substr($uppercase, -5)); + assertType('uppercase-string', substr($uppercase, 0, 5)); + } + + /** + * @param uppercase-string $uppercase + */ + public function doMbSubstr(string $uppercase): void + { + assertType('uppercase-string', mb_substr($uppercase, 5)); + assertType('uppercase-string', mb_substr($uppercase, -5)); + assertType('uppercase-string', mb_substr($uppercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php new file mode 100644 index 0000000000..0c24268faf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -0,0 +1,29 @@ +analyse([__DIR__ . '/data/lowercase-string.php'], $errors); } + public function testUppercaseString(): void + { + $errors = [ + [ + "Strict comparison using === between uppercase-string and 'ab' will always evaluate to false.", + 10, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between 'ab' and uppercase-string will always evaluate to false.", + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between 'ab' and uppercase-string will always evaluate to true.", + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between uppercase-string and 'aBc' will always evaluate to false.", + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between uppercase-string and 'aBc' will always evaluate to true.", + 16, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + "Strict comparison using === between uppercase-string|false and 'ab' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } else { + $errors[] = [ + "Strict comparison using === between uppercase-string and 'ab' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/uppercase-string.php'], $errors); + } + public function testBug10493(): void { $this->checkAlwaysTrueStrictComparison = true; diff --git a/tests/PHPStan/Rules/Comparison/data/uppercase-string.php b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php new file mode 100644 index 0000000000..fbd7a3eaf7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php @@ -0,0 +1,31 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/uppercase-string.php'], [ + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, \'NotUpperCase\' given.', + 26, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, string given.', + 28, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, numeric-string given.', + 30, + ], + ]); + } + public function testBug10159(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/uppercase-string.php b/tests/PHPStan/Rules/Methods/data/uppercase-string.php new file mode 100644 index 0000000000..de7500ee8c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/uppercase-string.php @@ -0,0 +1,33 @@ +acceptUppercaseString('NotUpperCase'); + $this->acceptUppercaseString('UPPERCASE'); + $this->acceptUppercaseString($string); + $this->acceptUppercaseString($uppercaseString); + $this->acceptUppercaseString($numericString); + $this->acceptUppercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 036d2b0f1f..2098a1f02b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -154,12 +154,12 @@ public function testGeneralize(): void $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&uppercase-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php index 2e2bcf976c..cd278837d6 100644 --- a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -31,6 +31,11 @@ public function dataBuild(): iterable 'non-empty-array&oversized-array', ]; + yield [ + '[1, 2, 3, ...[1, \'FOO\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + yield [ '[1, 2, 2 => 3]', 'non-empty-list&oversized-array', @@ -51,6 +56,10 @@ public function dataBuild(): iterable '[1, \'foo\' => 2, 3]', 'non-empty-array&oversized-array', ]; + yield [ + '[1, \'FOO\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; } /** diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 7c9bcc0722..0b5c5c0818 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -8,6 +8,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -465,6 +466,21 @@ public function dataDescribe(): iterable VerbosityLevel::precise(), 'lowercase-string', ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::value(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::precise(), + 'uppercase-string', + ]; } /** diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 55dc30542b..0e125a4a81 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -23,6 +23,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; @@ -2009,6 +2010,54 @@ public function dataUnion(): iterable UnionType::class, 'literal-string|lowercase-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'numeric-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-falsy-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-empty-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'literal-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -3934,6 +3983,54 @@ public function dataIntersect(): iterable IntersectionType::class, 'literal-string&lowercase-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'numeric-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-falsy-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-empty-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'literal-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&uppercase-string', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -4423,6 +4520,23 @@ public function dataIntersect(): iterable ConstantStringType::class, '\'foo\'', ]; + + yield [ + [ + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + ConstantStringType::class, + '\'FOO\'', + ]; } /** diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index eebdb08014..ee4e6b55c7 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -223,6 +224,11 @@ public function dataToPhpDocNode(): iterable 'lowercase-string', ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + 'uppercase-string', + ]; + yield [ new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), 'non-empty-string', @@ -333,6 +339,11 @@ public function dataToPhpDocNodeWithoutCheckingEquals(): iterable '(literal-string & lowercase-string & non-falsy-string)', ]; + yield [ + new ConstantStringType("FOO\nBAR\nBAZ"), + '(literal-string & non-falsy-string & uppercase-string)', + ]; + yield [ new ConstantIntegerType(PHP_INT_MIN), (string) PHP_INT_MIN, From 95fbd572ad224f72eb5b4886049ab35873d32e6e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Nov 2024 09:12:30 +0100 Subject: [PATCH 139/220] ParseStrParameterOutTypeExtension - use explicit mixed array item type --- src/Type/Php/ParseStrParameterOutTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php index e0c7aa47e5..caabc319bf 100644 --- a/src/Type/Php/ParseStrParameterOutTypeExtension.php +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -53,7 +53,7 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function return new ArrayType( new UnionType([new StringType(), new IntegerType()]), - new UnionType([new ArrayType(new MixedType(), new MixedType()), $valueType]), + new UnionType([new ArrayType(new MixedType(), new MixedType(true)), $valueType]), ); } From 08df07a3e247336b406cc71d5cedb3ffb8121a6b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Nov 2024 09:22:26 +0100 Subject: [PATCH 140/220] Regenerate baseline --- phpstan-baseline.neon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 141752f21b..3b47d2e5fc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -784,7 +784,8 @@ parameters: path: src/Type/Accessory/AccessoryUppercaseStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' + identifier: phpstanApi.instanceofType count: 1 path: src/Type/Accessory/HasMethodType.php From f61d3247db160c875a2e1893aaee519963830f99 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Nov 2024 09:27:30 +0100 Subject: [PATCH 141/220] Patch wrong namespace prefixing in PHAR in WindowsRegistryLogicalFinder --- compiler/build/scoper.inc.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index 0ea6df31ec..ba198a0c91 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -218,6 +218,12 @@ function (string $filePath, string $prefix, string $content): string { return str_replace(sprintf('use %s\\PhpParser;', $prefix), 'use PhpParser;', $content); }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/fidry/cpu-core-counter/src/Finder/WindowsRegistryLogicalFinder.php') { + return $content; + } + return str_replace(sprintf('%s\\\\reg query', $prefix), 'reg query', $content); + }, ], 'exclude-namespaces' => [ 'PHPStan', From 162f774858b461b9cc89f0c604c798381053dc32 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Nov 2024 09:33:46 +0100 Subject: [PATCH 142/220] Fix `static` return type in php-8-stubs --- .../SignatureMap/Php8SignatureMapProvider.php | 13 +++++++++++-- tests/PHPStan/Analyser/nsrt/bug-12077.php | 11 +++++++++++ .../SignatureMap/Php8SignatureMapProviderTest.php | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12077.php diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index 7bfdf37f9b..59cbedb60c 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; @@ -50,6 +51,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private PhpVersion $phpVersion, private InitializerExprTypeResolver $initializerExprTypeResolver, + private ReflectionProviderProvider $reflectionProviderProvider, ) { $this->map = new Php8StubsMap($phpVersion->getVersionId()); @@ -392,6 +394,13 @@ private function getSignature( $phpDocReturnType = $phpDoc->getReturnTag()->getType(); } } + + $classReflection = null; + if ($className !== null) { + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + $classReflection = $reflectionProvider->getClass($className); + } + $parameters = []; $variadic = false; foreach ($function->getParams() as $param) { @@ -399,7 +408,7 @@ private function getSignature( if (!$name instanceof Variable || !is_string($name->name)) { throw new ShouldNotHappenException(); } - $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); + $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection); $parameters[] = new ParameterSignature( $name->name, $param->default !== null || $param->variadic, @@ -417,7 +426,7 @@ private function getSignature( $variadic = $variadic || $param->variadic; } - $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), null); + $returnType = ParserNodeTypeToPHPStanType::resolve($function->getReturnType(), $classReflection); return new FunctionSignature( $parameters, diff --git a/tests/PHPStan/Analyser/nsrt/bug-12077.php b/tests/PHPStan/Analyser/nsrt/bug-12077.php new file mode 100644 index 0000000000..07163ecaa1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12077.php @@ -0,0 +1,11 @@ += 8.3 + +namespace Bug12077; + +use ReflectionMethod; +use function PHPStan\Testing\assertType; + +function (): void { + $methodInfo = ReflectionMethod::createFromMethodName("Exception::getMessage"); + assertType(ReflectionMethod::class, $methodInfo); +}; diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index 57cb091e19..f912e0d78e 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -162,6 +163,7 @@ private function createProvider(): Php8SignatureMapProvider self::getContainer()->getByType(FileTypeMapper::class), $phpVersion, self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getContainer()->getByType(ReflectionProviderProvider::class), ); } From 01bf65c59918a6e686b420007a4088e61d49b151 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 20 Nov 2024 13:42:31 +0100 Subject: [PATCH 143/220] Integer::toString() and Float::toString() are uppercase-string --- src/Type/FloatType.php | 2 + src/Type/IntegerRangeType.php | 3 ++ src/Type/IntegerType.php | 2 + .../PHPStan/Analyser/nsrt/array-fill-keys.php | 2 +- .../Analyser/nsrt/array-key-exists.php | 8 +-- tests/PHPStan/Analyser/nsrt/bug-10863.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-11129.php | 54 +++++++++---------- tests/PHPStan/Analyser/nsrt/bug-11716.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4587.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-7387.php | 8 +-- tests/PHPStan/Analyser/nsrt/bug-8635.php | 2 +- .../Analyser/nsrt/cast-to-numeric-string.php | 42 +++++++-------- tests/PHPStan/Analyser/nsrt/filter-var.php | 4 +- tests/PHPStan/Analyser/nsrt/generics.php | 6 +-- tests/PHPStan/Analyser/nsrt/key-exists.php | 8 +-- .../non-empty-string-replace-functions.php | 4 +- .../PHPStan/Analyser/nsrt/range-to-string.php | 2 +- .../nsrt/set-type-type-specifying.php | 4 +- tests/PHPStan/Analyser/nsrt/strval.php | 6 +-- .../PHPStan/Rules/Generics/data/bug-6301.php | 2 +- 20 files changed, 88 insertions(+), 81 deletions(-) diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 40ea01b5db..d086024868 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonArrayTypeTrait; @@ -134,6 +135,7 @@ public function toString(): Type { return new IntersectionType([ new StringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 248d994266..9d8e735f35 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -13,6 +13,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use function array_filter; @@ -487,6 +488,7 @@ public function toString(): Type return new IntersectionType([ new StringType(), new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), new AccessoryNonFalsyStringType(), ]); @@ -495,6 +497,7 @@ public function toString(): Type return new IntersectionType([ new StringType(), new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 354067f0a9..535928c045 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -8,6 +8,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonArrayTypeTrait; @@ -83,6 +84,7 @@ public function toString(): Type return new IntersectionType([ new StringType(), new AccessoryLowercaseStringType(), + new AccessoryUppercaseStringType(), new AccessoryNumericStringType(), ]); } diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php index 5054186320..00aa87fc69 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php @@ -91,7 +91,7 @@ function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, assertType("array", array_fill_keys($foo, null)); assertType("array", array_fill_keys($bar, null)); assertType("array<'foo', null>", array_fill_keys($baz, null)); - assertType("array", array_fill_keys($floats, null)); + assertType("array", array_fill_keys($floats, null)); assertType("array", array_fill_keys($mixed, null)); assertType('array', array_fill_keys($list, null)); assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); diff --git a/tests/PHPStan/Analyser/nsrt/array-key-exists.php b/tests/PHPStan/Analyser/nsrt/array-key-exists.php index 3ae615b2d9..a7246dd043 100644 --- a/tests/PHPStan/Analyser/nsrt/array-key-exists.php +++ b/tests/PHPStan/Analyser/nsrt/array-key-exists.php @@ -50,16 +50,16 @@ public function doBar(array $a, array $b, array $c, int $key1, string $key2, int assertType('int', $key1); } if (array_key_exists($key2, $a)) { - assertType('lowercase-string&numeric-string', $key2); + assertType('lowercase-string&numeric-string&uppercase-string', $key2); } if (array_key_exists($key3, $a)) { - assertType('int|(lowercase-string&numeric-string)', $key3); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); } if (array_key_exists($key4, $a)) { - assertType('(int|(lowercase-string&numeric-string))', $key4); + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); } if (array_key_exists($key5, $a)) { - assertType('int|(lowercase-string&numeric-string)', $key5); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); } if (array_key_exists($key1, $b)) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-10863.php b/tests/PHPStan/Analyser/nsrt/bug-10863.php index 275bc3774e..c88d354382 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10863.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10863.php @@ -12,7 +12,7 @@ class Foo */ public function doFoo($b): void { - assertType('lowercase-string&non-falsy-string', '@' . $b); + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); } /** @@ -20,7 +20,7 @@ public function doFoo($b): void */ public function doFoo2($b): void { - assertType('lowercase-string&non-falsy-string', '@' . $b); + assertType('lowercase-string&non-falsy-string&uppercase-string', '@' . $b); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11129.php b/tests/PHPStan/Analyser/nsrt/bug-11129.php index 1f60b4089f..a845bf1d2a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11129.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11129.php @@ -21,14 +21,14 @@ public function foo( $maybeNegativeConstStrings, $maybeNonNumericConstStrings, $maybeFloatConstStrings, bool $bool, float $float ): void { - assertType('lowercase-string&non-falsy-string', '0'.$i); - assertType('lowercase-string&non-falsy-string&numeric-string', $i.'0'); + assertType('lowercase-string&non-falsy-string&uppercase-string', '0'.$i); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $i.'0'); - assertType('lowercase-string&non-falsy-string&numeric-string', '0'.$positiveInt); - assertType('lowercase-string&non-falsy-string&numeric-string', $positiveInt.'0'); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '0'.$positiveInt); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $positiveInt.'0'); - assertType('lowercase-string&non-falsy-string', '0'.$negativeInt); - assertType('lowercase-string&non-falsy-string&numeric-string', $negativeInt.'0'); + assertType('lowercase-string&non-falsy-string&uppercase-string', '0'.$negativeInt); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $negativeInt.'0'); assertType("'00'|'01'|'02'", '0'.$positiveConstStrings); assertType( "'00'|'10'|'20'", $positiveConstStrings.'0'); @@ -39,36 +39,36 @@ public function foo( assertType("'00'|'01'|'0a'", '0'.$maybeNonNumericConstStrings); assertType("'00'|'10'|'a0'", $maybeNonNumericConstStrings.'0'); - assertType('lowercase-string&non-falsy-string&numeric-string', $i.$positiveConstStrings); - assertType('lowercase-string&non-falsy-string', $positiveConstStrings.$i); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $i.$positiveConstStrings); + assertType('lowercase-string&non-falsy-string&uppercase-string', $positiveConstStrings.$i); - assertType('lowercase-string&non-falsy-string', $i.$maybeNegativeConstStrings); - assertType('lowercase-string&non-falsy-string', $maybeNegativeConstStrings.$i); + assertType('lowercase-string&non-falsy-string&uppercase-string', $i.$maybeNegativeConstStrings); + assertType('lowercase-string&non-falsy-string&uppercase-string', $maybeNegativeConstStrings.$i); assertType('lowercase-string&non-falsy-string', $i.$maybeNonNumericConstStrings); assertType('lowercase-string&non-falsy-string', $maybeNonNumericConstStrings.$i); - assertType('lowercase-string&non-falsy-string', $i.$maybeFloatConstStrings); // could be 'lowercase-string&non-falsy-string&numeric-string' - assertType('lowercase-string&non-falsy-string', $maybeFloatConstStrings.$i); + assertType('lowercase-string&non-falsy-string&uppercase-string', $i.$maybeFloatConstStrings); // could be 'lowercase-string&non-falsy-string&numeric-string' + assertType('lowercase-string&non-falsy-string&uppercase-string', $maybeFloatConstStrings.$i); - assertType('lowercase-string&non-empty-string&numeric-string', $i.$bool); - assertType('lowercase-string&non-empty-string', $bool.$i); - assertType('lowercase-string&non-falsy-string&numeric-string', $positiveInt.$bool); - assertType('lowercase-string&non-falsy-string&numeric-string', $bool.$positiveInt); - assertType('lowercase-string&non-falsy-string&numeric-string', $negativeInt.$bool); - assertType('lowercase-string&non-falsy-string', $bool.$negativeInt); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $i.$bool); + assertType('lowercase-string&non-empty-string&uppercase-string', $bool.$i); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $positiveInt.$bool); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $bool.$positiveInt); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $negativeInt.$bool); + assertType('lowercase-string&non-falsy-string&uppercase-string', $bool.$negativeInt); - assertType('lowercase-string&non-falsy-string', $i.$i); - assertType('lowercase-string&non-falsy-string', $negativeInt.$negativeInt); - assertType('lowercase-string&non-falsy-string', $maybeNegativeConstStrings.$negativeInt); - assertType('lowercase-string&non-falsy-string', $negativeInt.$maybeNegativeConstStrings); + assertType('lowercase-string&non-falsy-string&uppercase-string', $i.$i); + assertType('lowercase-string&non-falsy-string&uppercase-string', $negativeInt.$negativeInt); + assertType('lowercase-string&non-falsy-string&uppercase-string', $maybeNegativeConstStrings.$negativeInt); + assertType('lowercase-string&non-falsy-string&uppercase-string', $negativeInt.$maybeNegativeConstStrings); // https://3v4l.org/BCS2K - assertType('non-falsy-string', $float.$float); - assertType('non-falsy-string&numeric-string', $float.$positiveInt); - assertType('non-falsy-string', $float.$negativeInt); - assertType('non-falsy-string', $float.$i); - assertType('non-falsy-string', $i.$float); // could be 'non-falsy-string&numeric-string' + assertType('non-falsy-string&uppercase-string', $float.$float); + assertType('non-falsy-string&numeric-string&uppercase-string', $float.$positiveInt); + assertType('non-falsy-string&uppercase-string', $float.$negativeInt); + assertType('non-falsy-string&uppercase-string', $float.$i); + assertType('non-falsy-string&uppercase-string', $i.$float); // could be 'non-falsy-string&numeric-string&uppercase-string' assertType('non-falsy-string', $numericString.$float); assertType('non-falsy-string', $numericString.$maybeFloatConstStrings); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php index 83c10f3225..a2e86ffbec 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11716.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11716.php @@ -75,7 +75,7 @@ function narrowKey($mixed, string $s, int $i, array $generalArr, array $intKeyed assertType('int', $i); if (isset($intKeyedArr[$s])) { - assertType("lowercase-string&numeric-string", $s); + assertType("lowercase-string&numeric-string&uppercase-string", $s); } else { assertType('string', $s); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4587.php b/tests/PHPStan/Analyser/nsrt/bug-4587.php index 623fea93af..061c953a28 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4587.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4587.php @@ -27,11 +27,11 @@ public function b(): void $type = array_map(static function (array $result): array { assertType('array{a: int}', $result); $result['a'] = (string) $result['a']; - assertType('array{a: lowercase-string&numeric-string}', $result); + assertType('array{a: lowercase-string&numeric-string&uppercase-string}', $result); return $result; }, $results); - assertType('list', $type); + assertType('list', $type); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php index 4f28696fb9..1b283a7990 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -29,7 +29,7 @@ public function inputTypes(int $i, float $f, string $s, int $intRange) { public function specifiers(int $i) { // https://3v4l.org/fmVIg - assertType('lowercase-string&numeric-string', sprintf('%14s', $i)); + assertType('lowercase-string&numeric-string&uppercase-string', sprintf('%14s', $i)); assertType('lowercase-string&numeric-string', sprintf('%d', $i)); @@ -59,9 +59,9 @@ public function specifiers(int $i) { */ public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { // https://3v4l.org/vVL0c - assertType('lowercase-string&numeric-string', sprintf('%2$6s', $mixed, $i)); - assertType('lowercase-string&non-falsy-string&numeric-string', sprintf('%2$6s', $mixed, $posInt)); - assertType('lowercase-string&non-falsy-string&numeric-string', sprintf('%2$6s', $mixed, $negInt)); + assertType('lowercase-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $i)); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $posInt)); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', sprintf('%2$6s', $mixed, $negInt)); assertType("' 1'|' 2'|' 3'|' 4'|' 5'", sprintf('%2$6s', $mixed, $nonZeroIntRange)); // https://3v4l.org/1ECIq diff --git a/tests/PHPStan/Analyser/nsrt/bug-8635.php b/tests/PHPStan/Analyser/nsrt/bug-8635.php index fe49aa8a2d..46d5b728b8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8635.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8635.php @@ -8,6 +8,6 @@ class HelloWorld { public function EchoInt(int $value): void { - assertType('lowercase-string&numeric-string', "$value"); + assertType('lowercase-string&numeric-string&uppercase-string', "$value"); } } diff --git a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php index 7c13500af2..dfe47aa39b 100644 --- a/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php +++ b/tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php @@ -13,13 +13,13 @@ * @param 1 $constantInt */ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('lowercase-string&numeric-string', (string)$a); - assertType('numeric-string', (string)$b); + assertType('lowercase-string&numeric-string&uppercase-string', (string)$a); + assertType('numeric-string&uppercase-string', (string)$b); assertType('numeric-string', (string)$numeric); assertType('numeric-string', (string)$numeric2); - assertType('numeric-string', (string)$number); - assertType('lowercase-string&non-falsy-string&numeric-string', (string)$positive); - assertType('lowercase-string&non-falsy-string&numeric-string', (string)$negative); + assertType('numeric-string&uppercase-string', (string)$number); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string)$positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string)$negative); assertType("'1'", (string)$constantInt); } @@ -32,32 +32,32 @@ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negativ * @param 1 $constantInt */ function concatEmptyString(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('lowercase-string&numeric-string', '' . $a); - assertType('numeric-string', '' . $b); + assertType('lowercase-string&numeric-string&uppercase-string', '' . $a); + assertType('numeric-string&uppercase-string', '' . $b); assertType('numeric-string', '' . $numeric); assertType('numeric-string', '' . $numeric2); - assertType('numeric-string', '' . $number); - assertType('lowercase-string&non-falsy-string&numeric-string', '' . $positive); - assertType('lowercase-string&non-falsy-string&numeric-string', '' . $negative); + assertType('numeric-string&uppercase-string', '' . $number); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '' . $positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', '' . $negative); assertType("'1'", '' . $constantInt); - assertType('lowercase-string&numeric-string', $a . ''); - assertType('numeric-string', $b . ''); + assertType('lowercase-string&numeric-string&uppercase-string', $a . ''); + assertType('numeric-string&uppercase-string', $b . ''); assertType('numeric-string', $numeric . ''); assertType('numeric-string', $numeric2 . ''); - assertType('numeric-string', $number . ''); - assertType('lowercase-string&non-falsy-string&numeric-string', $positive . ''); - assertType('lowercase-string&non-falsy-string&numeric-string', $negative . ''); + assertType('numeric-string&uppercase-string', $number . ''); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $positive . ''); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $negative . ''); assertType("'1'", $constantInt . ''); } function concatAssignEmptyString(int $i, float $f) { $i .= ''; - assertType('lowercase-string&numeric-string', $i); + assertType('lowercase-string&numeric-string&uppercase-string', $i); $s = ''; $s .= $f; - assertType('numeric-string', $s); + assertType('numeric-string&uppercase-string', $s); } /** @@ -66,13 +66,13 @@ function concatAssignEmptyString(int $i, float $f) { */ function integerRangeToString($positive, $negative) { - assertType('lowercase-string&numeric-string', (string) $positive); - assertType('lowercase-string&numeric-string', (string) $negative); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $positive); + assertType('lowercase-string&numeric-string&uppercase-string', (string) $negative); if ($positive !== 0) { - assertType('lowercase-string&non-falsy-string&numeric-string', (string) $positive); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $positive); } if ($negative !== 0) { - assertType('lowercase-string&non-falsy-string&numeric-string', (string) $negative); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', (string) $negative); } } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index 28580c448c..abe5331fc6 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -154,11 +154,11 @@ public function scalars(bool $bool, float $float, int $int, string $string, int assertType("''|'1'", filter_var($bool)); assertType("'1'", filter_var(true)); assertType("''", filter_var(false)); - assertType('numeric-string', filter_var($float)); + assertType('numeric-string&uppercase-string', filter_var($float)); assertType("'17'", filter_var(17.0)); assertType("'17.1'", filter_var(17.1)); assertType("'1.0E-50'", filter_var(1e-50)); - assertType('lowercase-string&numeric-string', filter_var($int)); + assertType('lowercase-string&numeric-string&uppercase-string', filter_var($int)); assertType("'0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", filter_var($intRange)); assertType("'17'", filter_var(17)); assertType('string', filter_var($string)); diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 873ad66b6d..80f10030e8 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -147,10 +147,10 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('Closure(int): (lowercase-string&numeric-string)', function (int $a): string { + assertType('Closure(int): (lowercase-string&numeric-string&uppercase-string)', function (int $a): string { return (string)$a; }); - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); assertType('Closure(mixed): string', function ($a): string { @@ -224,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** diff --git a/tests/PHPStan/Analyser/nsrt/key-exists.php b/tests/PHPStan/Analyser/nsrt/key-exists.php index 3c85802e73..23a2d0930d 100644 --- a/tests/PHPStan/Analyser/nsrt/key-exists.php +++ b/tests/PHPStan/Analyser/nsrt/key-exists.php @@ -49,16 +49,16 @@ public function doBar(array $a, array $b, array $c, int $key1, string $key2, int assertType('int', $key1); } if (key_exists($key2, $a)) { - assertType('lowercase-string&numeric-string', $key2); + assertType('lowercase-string&numeric-string&uppercase-string', $key2); } if (key_exists($key3, $a)) { - assertType('int|(lowercase-string&numeric-string)', $key3); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key3); } if (key_exists($key4, $a)) { - assertType('(int|(lowercase-string&numeric-string))', $key4); + assertType('(int|(lowercase-string&numeric-string&uppercase-string))', $key4); } if (key_exists($key5, $a)) { - assertType('int|(lowercase-string&numeric-string)', $key5); + assertType('int|(lowercase-string&numeric-string&uppercase-string)', $key5); } if (key_exists($key1, $b)) { diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php index 3627b8a409..a774b4eba4 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -26,9 +26,9 @@ public function replace(string $search, string $replacement, string $subject){ function foo(float $f) { $s = (string) $f; - assertType('numeric-string', $s); + assertType('numeric-string&uppercase-string', $s); $price = str_replace(',', '.', $s); - assertType('non-empty-string', $price); + assertType('non-empty-string&uppercase-string', $price); } } diff --git a/tests/PHPStan/Analyser/nsrt/range-to-string.php b/tests/PHPStan/Analyser/nsrt/range-to-string.php index 2d70eb4bab..49bb179309 100644 --- a/tests/PHPStan/Analyser/nsrt/range-to-string.php +++ b/tests/PHPStan/Analyser/nsrt/range-to-string.php @@ -17,6 +17,6 @@ public function sayHello($i, $ii, $maxlong, $toolong): void assertType("'10'|'5'|'6'|'7'|'8'|'9'", (string) $i); assertType("'-1'|'-10'|'-2'|'-3'|'-4'|'-5'|'-6'|'-7'|'-8'|'-9'|'0'|'1'|'10'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'", (string) $ii); assertType("'0'|'1'|'10'|'100'|'101'|'102'|'103'|'104'|'105'|'106'|'107'|'108'|'109'|'11'|'110'|'111'|'112'|'113'|'114'|'115'|'116'|'117'|'118'|'119'|'12'|'120'|'121'|'122'|'123'|'124'|'125'|'126'|'127'|'128'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'2'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'3'|'30'|'31'|'32'|'33'|'34'|'35'|'36'|'37'|'38'|'39'|'4'|'40'|'41'|'42'|'43'|'44'|'45'|'46'|'47'|'48'|'49'|'5'|'50'|'51'|'52'|'53'|'54'|'55'|'56'|'57'|'58'|'59'|'6'|'60'|'61'|'62'|'63'|'64'|'65'|'66'|'67'|'68'|'69'|'7'|'70'|'71'|'72'|'73'|'74'|'75'|'76'|'77'|'78'|'79'|'8'|'80'|'81'|'82'|'83'|'84'|'85'|'86'|'87'|'88'|'89'|'9'|'90'|'91'|'92'|'93'|'94'|'95'|'96'|'97'|'98'|'99'", (string) $maxlong); - assertType("lowercase-string&numeric-string", (string) $toolong); + assertType("lowercase-string&numeric-string&uppercase-string", (string) $toolong); } } diff --git a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php index 1ab4badbe5..f7513c6045 100644 --- a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -11,10 +11,10 @@ function doString(string $s, int $i, float $f, array $a, object $o) assertType('string', $s); settype($i, 'string'); - assertType('lowercase-string&numeric-string', $i); + assertType('lowercase-string&numeric-string&uppercase-string', $i); settype($f, 'string'); - assertType('numeric-string', $f); + assertType('numeric-string&uppercase-string', $f); settype($a, 'string'); assertType('*ERROR*', $a); diff --git a/tests/PHPStan/Analyser/nsrt/strval.php b/tests/PHPStan/Analyser/nsrt/strval.php index a6c4397893..b28f31549b 100644 --- a/tests/PHPStan/Analyser/nsrt/strval.php +++ b/tests/PHPStan/Analyser/nsrt/strval.php @@ -17,9 +17,9 @@ function strvalTest(string $string, string $class): void assertType('\'1\'', strval(true)); assertType('\'\'|\'1\'', strval(rand(0, 1) === 0)); assertType('\'42\'', strval(42)); - assertType('lowercase-string&numeric-string', strval(rand())); - assertType('numeric-string', strval(rand() * 0.1)); - assertType('lowercase-string&numeric-string', strval(strval(rand()))); + assertType('lowercase-string&numeric-string&uppercase-string', strval(rand())); + assertType('numeric-string&uppercase-string', strval(rand() * 0.1)); + assertType('lowercase-string&numeric-string&uppercase-string', strval(strval(rand()))); assertType('class-string', strval($class)); assertType('string', strval(new \Exception())); assertType('*ERROR*', strval(new \stdClass())); diff --git a/tests/PHPStan/Rules/Generics/data/bug-6301.php b/tests/PHPStan/Rules/Generics/data/bug-6301.php index 73dff935f2..8d32598294 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-6301.php +++ b/tests/PHPStan/Rules/Generics/data/bug-6301.php @@ -22,7 +22,7 @@ public function str($s) * @param literal-string $literalString */ public function foo(int $i, $nonEmpty, $numericString, $literalString):void { - assertType('lowercase-string&numeric-string', $this->str((string) $i)); + assertType('lowercase-string&numeric-string&uppercase-string', $this->str((string) $i)); assertType('non-empty-string', $this->str($nonEmpty)); assertType('numeric-string', $this->str($numericString)); assertType('literal-string', $this->str($literalString)); From 78ac6f229b8b0e94f00f06e185a22a438435aec7 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 20 Nov 2024 14:20:15 +0100 Subject: [PATCH 144/220] Fix subtracting enums inside `in_array` Co-authored-by: Ondrej Mirtes --- ...InArrayFunctionTypeSpecifyingExtension.php | 4 +- tests/PHPStan/Analyser/nsrt/enum-in-array.php | 187 ++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/enum-in-array.php diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index 62c022c8e1..997cfea752 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -110,7 +110,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $context->true() || ( $context->false() - && count($arrayValueType->getFiniteTypes()) === 1 + && count($arrayValueType->getFiniteTypes()) > 0 + && count($needleType->getFiniteTypes()) > 0 + && $arrayType->isIterableAtLeastOnce()->yes() ) ) { $specifiedTypes = $this->typeSpecifier->create( diff --git a/tests/PHPStan/Analyser/nsrt/enum-in-array.php b/tests/PHPStan/Analyser/nsrt/enum-in-array.php new file mode 100644 index 0000000000..ca34261bc8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/enum-in-array.php @@ -0,0 +1,187 @@ += 8.1 + +use function PHPStan\Testing\assertType; + +enum MyEnum: string +{ + + case A = 'a'; + case B = 'b'; + case C = 'c'; + + const SET_AB = [self::A, self::B]; + const SET_C = [self::C]; + const SET_ABC = [self::A, self::B, self::C]; + + public function test1(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::A|MyEnum::B', $enum); + } elseif (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test2(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_ABC, true)) { + assertType('MyEnum::A|MyEnum::B|MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function test3(): void + { + foreach (self::cases() as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function test4(): void + { + foreach ([MyEnum::C] as $enum) { + if (in_array($enum, MyEnum::SET_C, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative1(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } else { + assertType('MyEnum::A|MyEnum::B', $enum); + } + } + } + + public function testNegative2(): void + { + foreach (self::cases() as $enum) { + if (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('MyEnum::C', $enum); + } elseif (!in_array($enum, MyEnum::SET_AB, true)) { + assertType('*NEVER*', $enum); + } + } + } + + public function testNegative3(): void + { + foreach ([MyEnum::C] as $enum) { + if (!in_array($enum, MyEnum::SET_C, true)) { + assertType('*NEVER*', $enum); + } + } + } + + /** + * @param array $array + */ + public function testNegative4(MyEnum $enum, array $array): void + { + if (!in_array($enum, $array, true)) { + assertType('MyEnum', $enum); + assertType('array', $array); + } else { + assertType('MyEnum', $enum); + assertType('non-empty-array', $array); + } + } + +} + +class InArrayEnum +{ + + /** @param list $list */ + public function testPositive(MyEnum $enum, array $list): void + { + if (in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('list', $list); + } + + /** @param list $list */ + public function testNegative(MyEnum $enum, array $list): void + { + if (!in_array($enum, $list, true)) { + return; + } + + assertType(MyEnum::class, $enum); + assertType('non-empty-list', $list); + } + +} + + +class InArrayOtherFiniteType { + + const SET_AB = ['a', 'b']; + const SET_C = ['c']; + const SET_ABC = ['a', 'b', 'c']; + + public function test1(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_AB, true)) { + assertType("'a'|'b'", $item); + } elseif (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test2(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_ABC, true)) { + assertType("'a'|'b'|'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } + + public function test3(): void + { + foreach (['a', 'b', 'c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType("'a'|'b'", $item); + } + } + } + public function test4(): void + { + foreach (['c'] as $item) { + if (in_array($item, self::SET_C, true)) { + assertType("'c'", $item); + } else { + assertType('*NEVER*', $item); + } + } + } +} From c0bfae6a2efe3daa7df1ab08802960f0fa305ce2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 22 Nov 2024 19:09:12 +0100 Subject: [PATCH 145/220] Refactor TryRemove/Accepts for DateTime|DateTimeImmutable and Exception|Error --- phpstan-baseline.neon | 2 +- src/Type/ObjectType.php | 34 +++++++++++++--------------------- src/Type/UnionType.php | 28 ++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3b47d2e5fc..d3e9a54a9d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1422,7 +1422,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 6 + count: 3 path: src/Type/ObjectType.php - diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index a51539df05..3290fb97c7 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -5,11 +5,6 @@ use ArrayAccess; use Closure; use Countable; -use DateTime; -use DateTimeImmutable; -use DateTimeInterface; -use Error; -use Exception; use Iterator; use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; @@ -46,7 +41,6 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; -use Throwable; use Traversable; use function array_key_exists; use function array_map; @@ -1560,23 +1554,21 @@ private function getInterfaces(): array public function tryRemove(Type $typeToRemove): ?Type { - if ($this->getClassName() === DateTimeInterface::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { - return new ObjectType(DateTime::class); - } - - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { - return new ObjectType(DateTimeImmutable::class); - } - } + if ($typeToRemove instanceof ObjectType) { + foreach (UnionType::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if ($this->getClassName() !== $baseClass) { + continue; + } - if ($this->getClassName() === Throwable::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Error::class) { - return new ObjectType(Exception::class); // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - } + foreach ($classes as $index => $class) { + if ($typeToRemove->getClassName() === $class) { + unset($classes[$index]); - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Exception::class) { // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - return new ObjectType(Error::class); + return TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + } + } } } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 1505fa1bb2..5d7627b306 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,6 +5,8 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Error; +use Exception; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -26,6 +28,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use Throwable; use function array_diff_assoc; use function array_fill_keys; use function array_map; @@ -45,6 +48,11 @@ class UnionType implements CompoundType use NonGeneralizableTypeTrait; + public const EQUAL_UNION_CLASSES = [ + DateTimeInterface::class => [DateTimeImmutable::class, DateTime::class], + Throwable::class => [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + ]; + private bool $sortedTypes = false; /** @var array */ @@ -183,14 +191,18 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ( - $type->equals(new ObjectType(DateTimeInterface::class)) - && $this->accepts( - new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]), - $strictTypes, - )->yes() - ) { - return AcceptsResult::createYes(); + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if (!$type->equals(new ObjectType($baseClass))) { + continue; + } + + $union = TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + if ($this->accepts($union, $strictTypes)->yes()) { + return AcceptsResult::createYes(); + } + break; } $result = AcceptsResult::createNo(); From d9082ecf3c80d8ba169f0c2b15fed20613212bf0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 Nov 2024 11:23:12 +0100 Subject: [PATCH 146/220] Implement Scope->getPhpVersion() --- src/Analyser/MutatingScope.php | 11 +++ src/Analyser/Scope.php | 3 + src/Php/PhpVersions.php | 26 +++++++ src/Rules/Methods/FinalPrivateMethodRule.php | 9 +-- tests/PHPStan/Php/PhpVersionsTest.php | 68 +++++++++++++++++++ .../Methods/FinalPrivateMethodRuleTest.php | 36 ++++++++-- .../data/final-private-method-phpversions.php | 43 ++++++++++++ 7 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 src/Php/PhpVersions.php create mode 100644 tests/PHPStan/Php/PhpVersionsTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b8dcee4fd8..61f16a5119 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -47,6 +47,7 @@ use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; @@ -5721,4 +5722,14 @@ public function getIterableValueType(Type $iteratee): Type return $iteratee->getIterableValueType(); } + public function getPhpVersion(): PhpVersions + { + $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); + if (!$this->hasExpressionType($versionExpr)->yes()) { + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); + } + + return new PhpVersions($this->getType($versionExpr)); + } + } diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index a14c9d108d..bf89cbdf48 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; @@ -136,4 +137,6 @@ public function filterByFalseyValue(Expr $expr): self; public function isInFirstLevelStatement(): bool; + public function getPhpVersion(): PhpVersions; + } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php new file mode 100644 index 0000000000..a85c6c4a42 --- /dev/null +++ b/src/Php/PhpVersions.php @@ -0,0 +1,26 @@ +isSuperTypeOf($this->phpVersions)->result; + } + +} diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php index b205234dc2..7331e21bc1 100644 --- a/src/Rules/Methods/FinalPrivateMethodRule.php +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -14,12 +13,6 @@ final class FinalPrivateMethodRule implements Rule { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function getNodeType(): string { return InClassMethodNode::class; @@ -28,7 +21,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - if (!$this->phpVersion->producesWarningForFinalPrivateMethods()) { + if ($scope->getPhpVersion()->producesWarningForFinalPrivateMethods()->no()) { return []; } diff --git a/tests/PHPStan/Php/PhpVersionsTest.php b/tests/PHPStan/Php/PhpVersionsTest.php new file mode 100644 index 0000000000..b1563e2eb1 --- /dev/null +++ b/tests/PHPStan/Php/PhpVersionsTest.php @@ -0,0 +1,68 @@ +assertSame( + $expected->describe(), + $phpVersions->producesWarningForFinalPrivateMethods()->describe(), + ); + } + + public function dataProducesWarningForFinalPrivateMethods(): iterable + { + yield [ + TrinaryLogic::createNo(), + new ConstantIntegerType(70400), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80000), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80100), + ]; + + yield [ + TrinaryLogic::createYes(), + IntegerRangeType::fromInterval(80000, null), + ]; + + yield [ + TrinaryLogic::createMaybe(), + IntegerRangeType::fromInterval(null, 80000), + ]; + + yield [ + TrinaryLogic::createNo(), + IntegerRangeType::fromInterval(70200, 70400), + ]; + + yield [ + TrinaryLogic::createMaybe(), + new UnionType([ + IntegerRangeType::fromInterval(70200, 70400), + IntegerRangeType::fromInterval(80200, 80400), + ]), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php index 64a4b89c0f..05be45a380 100644 --- a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -5,18 +5,15 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** @extends RuleTestCase */ class FinalPrivateMethodRuleTest extends RuleTestCase { - private int $phpVersionId; - protected function getRule(): Rule { - return new FinalPrivateMethodRule( - new PhpVersion($this->phpVersionId), - ); + return new FinalPrivateMethodRule(); } public function dataRule(): array @@ -44,8 +41,35 @@ public function dataRule(): array */ public function testRule(int $phpVersion, array $errors): void { - $this->phpVersionId = $phpVersion; + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } + $this->analyse([__DIR__ . '/data/final-private-method.php'], $errors); } + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-phpversions.php'], [ + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp8orHigher::foo() cannot be final as it is never overridden by other classes.', + 9, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp74OrHigher::foo() cannot be final as it is never overridden by other classes.', + 29, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarBaz::foo() cannot be final as it is never overridden by other classes.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php b/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php new file mode 100644 index 0000000000..c9424720d1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php @@ -0,0 +1,43 @@ += 80000) { + class FooBarPhp8orHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 80000) { + class FooBarPhp7 + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID > 70400) { + class FooBarPhp74OrHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 80100) { + class FooBarBaz + { + + final private function foo(): void + { + } + } +} From adcabb3078603f46c811c7dc460174c9df551310 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 20 Nov 2024 15:48:39 +0100 Subject: [PATCH 147/220] TooWidePropertyTypeRule: dont skip even always-read properties --- src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index 8d77cb01b4..68a513f295 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -61,9 +61,6 @@ public function processNode(Node $node, Scope $scope): array continue; } foreach ($this->extensionProvider->getExtensions() as $extension) { - if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { - continue 2; - } if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { continue 2; } From 993db818fa59ae4f715a94a9e938aca69e7060a6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 24 Nov 2024 09:17:40 +0100 Subject: [PATCH 148/220] Revert "TooWidePropertyTypeRule: dont skip even always-read properties" This reverts commit adcabb3078603f46c811c7dc460174c9df551310. --- src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index 68a513f295..8d77cb01b4 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -61,6 +61,9 @@ public function processNode(Node $node, Scope $scope): array continue; } foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { + continue 2; + } if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { continue 2; } From e3867c05576f0b08656ce83f6ac2f6503ff22f94 Mon Sep 17 00:00:00 2001 From: schlndh Date: Sun, 24 Nov 2024 09:24:08 +0100 Subject: [PATCH 149/220] Bleeding edge - check that values passed to array_sum/product are castable to number --- conf/bleedingEdge.neon | 1 + conf/config.level5.neon | 6 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + .../ParameterCastableToNumberRule.php | 84 +++++++++ .../ParameterCastableToNumberRuleTest.php | 162 ++++++++++++++++++ .../Rules/Functions/data/bug-11883.php | 14 ++ ...aram-castable-to-number-functions-enum.php | 14 ++ ...astable-to-number-functions-named-args.php | 15 ++ .../param-castable-to-number-functions.php | 45 +++++ 10 files changed, 343 insertions(+) create mode 100644 src/Rules/Functions/ParameterCastableToNumberRule.php create mode 100644 tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-11883.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 8e06b22fda..7227e369b3 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,5 +1,6 @@ parameters: featureToggles: bleedingEdge: true + checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] stricterFunctionMap: true diff --git a/conf/config.level5.neon b/conf/config.level5.neon index b89a6dbddf..fd3835fbf1 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -5,6 +5,10 @@ parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +conditionalTags: + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% + rules: - PHPStan\Rules\DateTimeInstantiationRule - PHPStan\Rules\Functions\CallUserFuncRule @@ -36,3 +40,5 @@ services: treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Functions\ParameterCastableToNumberRule diff --git a/conf/config.neon b/conf/config.neon index 4679d5f31c..ba4bbb2f62 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -22,6 +22,7 @@ parameters: tooWideThrowType: true featureToggles: bleedingEdge: false + checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: [] stricterFunctionMap: false fileExtensions: diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 6c7f9c40e6..f73011dcad 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -28,6 +28,7 @@ parametersSchema: ]) featureToggles: structure([ bleedingEdge: bool(), + checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() ]) diff --git a/src/Rules/Functions/ParameterCastableToNumberRule.php b/src/Rules/Functions/ParameterCastableToNumberRule.php new file mode 100644 index 0000000000..640c73a440 --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToNumberRule.php @@ -0,0 +1,84 @@ + + */ +final class ParameterCastableToNumberRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_sum', 'array_product'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + + if (count($origArgs) !== 1) { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to number, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + $error = $this->parameterCastableToStringCheck->checkParameter( + $origArgs[0], + $scope, + $errorMessage, + static fn (Type $t) => $t->toNumber(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $origArgs[0], + 0, + $functionParameters[0] ?? null, + ), + ); + + return $error !== null + ? [$error] + : []; + } + +} diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php new file mode 100644 index 0000000000..77b64c3a30 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -0,0 +1,162 @@ + + */ +class ParameterCastableToNumberRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array> given.', + 20, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 21, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 22, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 23, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 24, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 25, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array> given.', + 27, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 28, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 29, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 30, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 31, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 32, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [ + [ + 'Parameter $array of function array_sum expects an array of values castable to number, array> given.', + 7, + ], + [ + 'Parameter $array of function array_product expects an array of values castable to number, array> given.', + 8, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 12, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 13, + ], + ]); + } + + public function testBug11883(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11883.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 13, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 14, + ], + ]); + } + + /** + * @param list $errors + * @return list + */ + private function hackPhp74ErrorMessages(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function array_sum', + '$array of function array_product', + 'array', + ], + [ + '$input of function array_sum', + '$input of function array_product', + 'array', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11883.php b/tests/PHPStan/Rules/Functions/data/bug-11883.php new file mode 100644 index 0000000000..a14174777b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11883.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug11883; + +enum SomeEnum: int +{ + case A = 1; + case B = 2; +} + +$enums1 = [SomeEnum::A, SomeEnum::B]; + +var_dump(array_sum($enums1)); +var_dump(array_product($enums1)); diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php new file mode 100644 index 0000000000..91e9f1f686 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php @@ -0,0 +1,14 @@ += 8.1 + +namespace ParamCastableToNumberFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_sum([FooEnum::A]); + array_product([FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php new file mode 100644 index 0000000000..4fdc546062 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ParamCastableToNumberFunctionsNamedArgs; + +function invalidUsages() +{ + var_dump(array_sum(array: [[0]])); + var_dump(array_product(array: [[0]])); +} + +function validUsages() +{ + var_dump(array_sum(array: [1])); + var_dump(array_product(array: [1])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php new file mode 100644 index 0000000000..9e7c5da4d2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php @@ -0,0 +1,45 @@ +7.7'), 5, 5.5, null])); + var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('7.7'), 5, 5.5, null])); +} From 2d637dab640f194f19b2c50a565b13e331c0227c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 Nov 2024 09:25:51 +0100 Subject: [PATCH 150/220] `Scope::getPhpVersion()` allows array via phpVersion min+max config --- src/Analyser/DirectInternalScopeFactory.php | 5 +++ src/Analyser/LazyInternalScopeFactory.php | 1 + src/Analyser/MutatingScope.php | 6 ++++ src/Testing/PHPStanTestCase.php | 1 + .../FinalPrivateMethodRuleConfigPhpTest.php | 34 +++++++++++++++++++ ...final-private-method-config-phpversion.php | 11 ++++++ .../data/final-private-php-version.neon | 4 +++ 7 files changed, 62 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-php-version.neon diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 99f4287dfe..74b43d80c9 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -19,6 +19,9 @@ final class DirectInternalScopeFactory implements InternalScopeFactory { + /** + * @param int|array{min: int, max: int}|null $configPhpVersion + */ public function __construct( private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -31,6 +34,7 @@ public function __construct( private NodeScopeResolver $nodeScopeResolver, private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private ConstantResolver $constantResolver, ) { @@ -78,6 +82,7 @@ public function create( $this->constantResolver, $context, $this->phpVersion, + $this->configPhpVersion, $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 79d34fb54f..ac5b757991 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -67,6 +67,7 @@ public function create( $this->container->getByType(ConstantResolver::class), $context, $this->container->getByType(PhpVersion::class), + $this->container->getParameter('phpVersion'), $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 61f16a5119..e7afd055e4 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -144,6 +144,7 @@ use function get_class; use function implode; use function in_array; +use function is_array; use function is_bool; use function is_numeric; use function is_string; @@ -184,6 +185,7 @@ final class MutatingScope implements Scope private static int $resolveClosureTypeDepth = 0; /** + * @param int|array{min: int, max: int}|null $configPhpVersion * @param array $expressionTypes * @param array $conditionalExpressions * @param list $inClosureBindScopeClasses @@ -207,6 +209,7 @@ public function __construct( private ConstantResolver $constantResolver, private ScopeContext $context, private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private bool $declareStrictTypes = false, private PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, @@ -5726,6 +5729,9 @@ public function getPhpVersion(): PhpVersions { $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); if (!$this->hasExpressionType($versionExpr)->yes()) { + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 31cdfadb78..849fbfac11 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -163,6 +163,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $container->getByType(NodeScopeResolver::class), new RicherScopeGetTypeHelper($initializerExprTypeResolver), $container->getByType(PhpVersion::class), + $container->getParameter('phpVersion'), $constantResolver, ), ); diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php new file mode 100644 index 0000000000..dddd414b72 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php @@ -0,0 +1,34 @@ + */ +class FinalPrivateMethodRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateMethodRule(); + } + + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-config-phpversion.php'], [ + [ + 'Private method FinalPrivateMethodConfigPhpVersions\PhpVersionViaNEONConfg::foo() cannot be final as it is never overridden by other classes.', + 8, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/final-private-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php new file mode 100644 index 0000000000..6880568c93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php @@ -0,0 +1,11 @@ + Date: Tue, 26 Nov 2024 01:06:55 +0900 Subject: [PATCH 151/220] Last value was not recognized when passing an associative array as an argument --- src/Rules/FunctionCallParametersCheck.php | 17 +++++--- .../Rules/Classes/InstantiationRuleTest.php | 5 +++ .../PHPStan/Rules/Classes/data/bug-11815.php | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-11815.php diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 130dd0bef8..d320c24d60 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -30,6 +30,7 @@ use function array_key_exists; use function count; use function implode; +use function in_array; use function is_int; use function is_string; use function max; @@ -128,30 +129,32 @@ public function check( if ($arg->unpack) { $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { - $minKeys = null; + $maxKeys = null; foreach ($arrays as $array) { $countType = $array->getArraySize(); if ($countType instanceof ConstantIntegerType) { $keysCount = $countType->getValue(); } elseif ($countType instanceof IntegerRangeType) { - $keysCount = $countType->getMin(); + $keysCount = $countType->getMax(); if ($keysCount === null) { throw new ShouldNotHappenException(); } } else { throw new ShouldNotHappenException(); } - if ($minKeys !== null && $keysCount >= $minKeys) { + if ($maxKeys !== null && $keysCount >= $maxKeys) { continue; } - $minKeys = $keysCount; + $maxKeys = $keysCount; } - for ($j = 0; $j < $minKeys; $j++) { + for ($j = 0; $j < $maxKeys; $j++) { $types = []; $commonKey = null; + $isOptionalKey = false; foreach ($arrays as $constantArray) { + $isOptionalKey = in_array($j, $constantArray->getOptionalKeys(), true); $types[] = $constantArray->getValueTypes()[$j]; $keyType = $constantArray->getKeyTypes()[$j]; if ($commonKey === null) { @@ -165,6 +168,10 @@ public function check( $keyArgumentName = $commonKey; $hasNamedArguments = true; } + if ($isOptionalKey) { + continue; + } + $arguments[] = [ $arg->value, TypeCombinator::union(...$types), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 657c47bd75..43b9091daf 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -499,4 +499,9 @@ public function testBug10248(): void $this->analyse([__DIR__ . '/data/bug-10248.php'], []); } + public function testBug11815(): void + { + $this->analyse([__DIR__ . '/data/bug-11815.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-11815.php b/tests/PHPStan/Rules/Classes/data/bug-11815.php new file mode 100644 index 0000000000..c08dccb9ea --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11815.php @@ -0,0 +1,43 @@ += 8.2 + +declare(strict_types = 1); + +class Dimensions +{ + public function __construct( + public int $width, + public int $height, + ) { + } +} + +class StoreProcessorResult +{ + public function __construct( + public string $path, + public string $mimetype, + public Dimensions $dimensions, + public int $filesize, + public true|null $identical = null, + ) { + } +} + +/** + * @return array{path: string, identical?: true} + */ +function getPath(): array +{ + $data = ['path' => 'some/path']; + if ((bool)rand(0, 1)) { + $data['identical'] = true; + } + return $data; +} + +$data = getPath(); +$data['dimensions'] = new Dimensions(100, 100); +$data['mimetype'] = 'image/png'; +$data['filesize'] = 123456; + +$dto = new StoreProcessorResult(...$data); From 970117e52512790507cd232d4146055fab1daccf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 24 Nov 2024 21:51:21 +0100 Subject: [PATCH 152/220] Remove sha256 definition --- resources/functionMap.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 9891593eb3..611ac517ec 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -10448,8 +10448,6 @@ 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], 'sha1' => ['non-falsy-string&lowercase-string', 'str'=>'string', 'raw_output='=>'bool'], 'sha1_file' => ['(non-falsy-string&lowercase-string)|false', 'filename'=>'string', 'raw_output='=>'bool'], -'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], 'shapefileObj::__construct' => ['void', 'filename'=>'string', 'type'=>'int'], 'shapefileObj::addPoint' => ['int', 'point'=>'pointObj'], 'shapefileObj::addShape' => ['int', 'shape'=>'shapeObj'], From a0e007aee46a0a3ac56d74789f1107a87d0f5740 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 Nov 2024 11:26:38 +0100 Subject: [PATCH 153/220] non-capturing catch support-detection is scope php-version dependent --- src/Php/PhpVersions.php | 5 +++++ src/Rules/Exceptions/NoncapturingCatchRule.php | 7 +------ .../Rules/Exceptions/NoncapturingCatchRuleTest.php | 14 ++++++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index a85c6c4a42..7bdc70e3bf 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -18,6 +18,11 @@ public function __construct( { } + public function supportsNoncapturingCatches(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + public function producesWarningForFinalPrivateMethods(): TrinaryLogic { return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php index 499b62e154..a4d91a9ba2 100644 --- a/src/Rules/Exceptions/NoncapturingCatchRule.php +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,10 +13,6 @@ final class NoncapturingCatchRule implements Rule { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getNodeType(): string { return Node\Stmt\Catch_::class; @@ -28,7 +23,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ($this->phpVersion->supportsNoncapturingCatches()) { + if ($scope->getPhpVersion()->supportsNoncapturingCatches()->yes()) { return []; } diff --git a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php index 9c8181f4a3..8139960e66 100644 --- a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,11 +13,9 @@ class NoncapturingCatchRuleTest extends RuleTestCase { - private PhpVersion $phpVersion; - protected function getRule(): Rule { - return new NoncapturingCatchRule($this->phpVersion); + return new NoncapturingCatchRule(); } public function dataRule(): array @@ -49,7 +48,14 @@ public function dataRule(): array */ public function testRule(int $phpVersion, array $expectedErrors): void { - $this->phpVersion = new PhpVersion($phpVersion); + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } $this->analyse([ __DIR__ . '/data/noncapturing-catch.php', From d35a2f464d7756022b12acc83c994bcb39d07ac9 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:03:38 +0000 Subject: [PATCH 154/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 73f1cc620f..71c48bfbc7 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#fda684a4826c1caf59efe1a1bf68d08c11aaddbf", + "jetbrains/phpstorm-stubs": "dev-master#9efcc4aa48b9c752c68de25f6240fcced0c95151", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index d2adb5606c..6a7f1ff905 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a4991601b7590d9dc65443dfb5d34d16", + "content-hash": "274e72a3422f81d92070bbbedbb9f8f5", "packages": [ { "name": "clue/ndjson-react", @@ -1442,18 +1442,18 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf" + "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/fda684a4826c1caf59efe1a1bf68d08c11aaddbf", - "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/9efcc4aa48b9c752c68de25f6240fcced0c95151", + "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151", "shasum": "" }, "require-dev": { "friendsofphp/php-cs-fixer": "v3.64.0", "nikic/php-parser": "v5.3.1", - "phpdocumentor/reflection-docblock": "5.5.1", + "phpdocumentor/reflection-docblock": "5.6.0", "phpunit/phpunit": "11.4.3" }, "default-branch": true, @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-15T09:42:33+00:00" + "time": "2024-11-25T11:21:35+00:00" }, { "name": "nette/bootstrap", From 96f721d40a0492e2f33a2316efaa50da0d385b0f Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Tue, 26 Nov 2024 20:00:44 +0000 Subject: [PATCH 155/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 71c48bfbc7..8963aa75c3 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.43.0.2", + "ondrejmirtes/better-reflection": "6.43.0.4", "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 6a7f1ff905..a1f8f8e83a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "274e72a3422f81d92070bbbedbb9f8f5", + "content-hash": "1866d60acbec835ca58159c3cb6c07a8", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.43.0.2", + "version": "6.43.0.4", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584" + "reference": "6b869bb58972b7877509e8a24757b4108effcd79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c34ee726f9abc5a7057b0dacdf1c0991c9090584", - "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/6b869bb58972b7877509e8a24757b4108effcd79", + "reference": "6b869bb58972b7877509e8a24757b4108effcd79", "shasum": "" }, "require": { @@ -2213,7 +2213,7 @@ "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", "phpunit/phpunit": "^11.4.3", - "rector/rector": "0.14.3" + "rector/rector": "1.2.10" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.2" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.4" }, - "time": "2024-11-19T19:32:34+00:00" + "time": "2024-11-26T19:58:24+00:00" }, { "name": "phpstan/php-8-stubs", From 17da15181a75ec1bd184959185005e6e857e1220 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Tue, 26 Nov 2024 20:52:02 +0000 Subject: [PATCH 156/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8963aa75c3..988292f2d6 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.43.0.4", + "ondrejmirtes/better-reflection": "6.44.0.2", "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index a1f8f8e83a..d705cd728f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1866d60acbec835ca58159c3cb6c07a8", + "content-hash": "6c77a249a859b7eb74df36c091206c59", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.43.0.4", + "version": "6.44.0.2", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "6b869bb58972b7877509e8a24757b4108effcd79" + "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/6b869bb58972b7877509e8a24757b4108effcd79", - "reference": "6b869bb58972b7877509e8a24757b4108effcd79", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/50fe615bd6665bbc541b6ddb419bff11fc0718a1", + "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.4" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.2" }, - "time": "2024-11-26T19:58:24+00:00" + "time": "2024-11-26T20:50:48+00:00" }, { "name": "phpstan/php-8-stubs", From 1c4c1009a523a709d69cbe417580dd8aa44b4245 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 27 Nov 2024 00:21:18 +0000 Subject: [PATCH 157/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 988292f2d6..da38b8d286 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.2", - "phpstan/php-8-stubs": "0.4.6", + "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index d705cd728f..569d899634 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c77a249a859b7eb74df36c091206c59", + "content-hash": "d934490a94126dd32923c058834ef486", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.6", + "version": "0.4.7", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "25ba0a11dc14a02c062392786486ada62d36b66d" + "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/25ba0a11dc14a02c062392786486ada62d36b66d", - "reference": "25ba0a11dc14a02c062392786486ada62d36b66d", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/794d410e0de8779afb4706d8667d2b3a11a3865a", + "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.6" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.7" }, - "time": "2024-11-13T00:19:28+00:00" + "time": "2024-11-27T00:20:37+00:00" }, { "name": "phpstan/phpdoc-parser", From 5ec47623fa31f63912d904ba90693eac64b0de0c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 10:00:39 +0100 Subject: [PATCH 158/220] PropertyHookType should not be prefixed in BetterReflection --- compiler/build/scoper.inc.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index 957cfe721d..0d0008d341 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -226,6 +226,13 @@ function (string $filePath, string $prefix, string $content): string { sprintf('\\\\%s', $prefix), ], '', $content); }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/ondrejmirtes/better-reflection')) { + return $content; + } + + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); + }, function (string $filePath, string $prefix, string $content): string { if ( $filePath !== 'vendor/nette/utils/src/Utils/Strings.php' From 265a39bb014534095b2f1651de27f4c10e770c09 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Wed, 27 Nov 2024 10:50:13 +0000 Subject: [PATCH 159/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index da38b8d286..593163649e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.2", + "ondrejmirtes/better-reflection": "6.44.0.3", "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 569d899634..a31b6c1e33 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d934490a94126dd32923c058834ef486", + "content-hash": "402089d9a3d76a9b791773a44ed74775", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.2", + "version": "6.44.0.3", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1" + "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/50fe615bd6665bbc541b6ddb419bff11fc0718a1", - "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c30607531b1ae173ea69a72c21ae598f15fef4af", + "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.2" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.3" }, - "time": "2024-11-26T20:50:48+00:00" + "time": "2024-11-27T10:47:28+00:00" }, { "name": "phpstan/php-8-stubs", From 1abeeb77771fc3fece5c7196f75dfc1478840515 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 14:01:24 +0100 Subject: [PATCH 160/220] Un-finalize Printer See https://github.com/rectorphp/rector-src/pull/6517#issuecomment-2503502706 --- build/PHPStan/Build/FinalClassRule.php | 2 ++ src/Node/Printer/Printer.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build/PHPStan/Build/FinalClassRule.php b/build/PHPStan/Build/FinalClassRule.php index a4758648c6..10f30a8d48 100644 --- a/build/PHPStan/Build/FinalClassRule.php +++ b/build/PHPStan/Build/FinalClassRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\File\FileHelper; use PHPStan\Node\InClassNode; +use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\Php\DummyParameter; @@ -54,6 +55,7 @@ public function processNode(Node $node, Scope $scope): array ExtendedFunctionVariant::class, DummyParameter::class, PhpFunctionFromParserNodeReflection::class, + Printer::class, ], true)) { return []; } diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 131376d66d..744c857f8f 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -22,7 +22,7 @@ /** * @api */ -final class Printer extends Standard +class Printer extends Standard { public function __construct() From 07d7c44d50677874086cb8796a97482d406d3442 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 14:26:42 +0100 Subject: [PATCH 161/220] Revert "Un-finalize Printer" This reverts commit 1abeeb77771fc3fece5c7196f75dfc1478840515. --- build/PHPStan/Build/FinalClassRule.php | 2 -- src/Node/Printer/Printer.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build/PHPStan/Build/FinalClassRule.php b/build/PHPStan/Build/FinalClassRule.php index 10f30a8d48..a4758648c6 100644 --- a/build/PHPStan/Build/FinalClassRule.php +++ b/build/PHPStan/Build/FinalClassRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\File\FileHelper; use PHPStan\Node\InClassNode; -use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\Php\DummyParameter; @@ -55,7 +54,6 @@ public function processNode(Node $node, Scope $scope): array ExtendedFunctionVariant::class, DummyParameter::class, PhpFunctionFromParserNodeReflection::class, - Printer::class, ], true)) { return []; } diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 744c857f8f..131376d66d 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -22,7 +22,7 @@ /** * @api */ -class Printer extends Standard +final class Printer extends Standard { public function __construct() From fa9212fc7e68f9609dbfc5b1042ce83c5b34450d Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 27 Nov 2024 19:26:13 +0700 Subject: [PATCH 162/220] Remove shortArraySyntax definiton on Printer::construct() It seems alreayd short array syntax by default --- src/Node/Printer/Printer.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 131376d66d..6f00bc8031 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -24,12 +24,6 @@ */ final class Printer extends Standard { - - public function __construct() - { - parent::__construct(['shortArraySyntax' => true]); - } - protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); From 68803e997f4ab10c98a014e4a6d057b99e08b6b7 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Wed, 27 Nov 2024 13:47:17 +0000 Subject: [PATCH 163/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 593163649e..684e819342 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.3", + "ondrejmirtes/better-reflection": "6.44.0.4", "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index a31b6c1e33..1f3a161391 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "402089d9a3d76a9b791773a44ed74775", + "content-hash": "ac31d4286789897fa7f8d303b33a72b0", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.3", + "version": "6.44.0.4", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af" + "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c30607531b1ae173ea69a72c21ae598f15fef4af", - "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/3865f1f8779d4e3562886ee261ccfdbf2da15650", + "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.3" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.4" }, - "time": "2024-11-27T10:47:28+00:00" + "time": "2024-11-27T13:45:40+00:00" }, { "name": "phpstan/php-8-stubs", From e1b1cad3c7da1ef175b039c4b1af37212d52a22f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 15:04:32 +0100 Subject: [PATCH 164/220] Fix CS --- src/Node/Printer/Printer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 6f00bc8031..6d9dab1062 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -24,6 +24,7 @@ */ final class Printer extends Standard { + protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); From f9e84e05bf9b4e15a4b019d4e372b71ba98e42e8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:03:11 +0000 Subject: [PATCH 165/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 684e819342..1505dfeb90 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#9efcc4aa48b9c752c68de25f6240fcced0c95151", + "jetbrains/phpstorm-stubs": "dev-master#7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 1f3a161391..b40729b153 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac31d4286789897fa7f8d303b33a72b0", + "content-hash": "c1cf63bf474629a0d7b5fea00c2ec54c", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151" + "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/9efcc4aa48b9c752c68de25f6240fcced0c95151", - "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-25T11:21:35+00:00" + "time": "2024-11-27T14:37:09+00:00" }, { "name": "nette/bootstrap", From 0b925a9f5a3b664d1a02c9d6b9d1721263a34a06 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 22:25:38 +0100 Subject: [PATCH 166/220] Retain list type when assigning to offset 1 of `non-empty-list` --- src/Type/IntersectionType.php | 9 +++++- tests/PHPStan/Analyser/nsrt/list-type.php | 25 +++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 12 +++++++ .../Rules/Properties/data/bug-12131.php | 31 +++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100755 tests/PHPStan/Rules/Properties/data/bug-12131.php diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 72fecc7de6..d41b79e951 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -756,7 +756,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni ); }); } - return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ($offsetType !== null && $this->isList()->yes() && $this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = AccessoryArrayListType::intersectWith($result); + } + + return $result; } public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index a80e8b066d..26640a5141 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -106,4 +106,29 @@ public function testUnset(array $list): void assertType('array|int<3, max>, int>', $list); } + /** @param list $list */ + public function testSetOffsetExplicitlyWithoutGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[1] = 19; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); + $list[0] = 21; + assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); + + $list[2] = 23; + assertType('non-empty-array, int>&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)&hasOffsetValue(2, 23)', $list); + } + + /** @param list $list */ + public function testSetOffsetExplicitlyWithGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[2] = 21; + assertType('non-empty-array, int>&hasOffsetValue(0, 17)&hasOffsetValue(2, 21)', $list); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index ff9f5be5ae..4c87845d24 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -692,4 +692,16 @@ public function testBug11617(): void ]); } + public function testBug12131(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12131.php'], [ + [ + 'Property Bug12131\Test::$array (non-empty-list) does not accept non-empty-array, int>.', + 29, + 'non-empty-array, int> might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php new file mode 100755 index 0000000000..6f7f8d83d8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -0,0 +1,31 @@ += 7.4 + +namespace Bug12131; + +class Test +{ + /** + * @var non-empty-list + */ + public array $array; + + public function __construct() + { + $this->array = array_fill(0, 10, 1); + } + + public function setAtZero(): void + { + $this->array[0] = 1; + } + + public function setAtOne(): void + { + $this->array[1] = 1; + } + + public function setAtTwo(): void + { + $this->array[2] = 1; + } +} From 62c6a0a8b654224e4c6f0b5e111740b4a2d260e4 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 22:37:57 +0100 Subject: [PATCH 167/220] Remove unnecessary test code --- tests/PHPStan/Analyser/nsrt/list-type.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index 26640a5141..40d94efff3 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -116,9 +116,6 @@ public function testSetOffsetExplicitlyWithoutGap(array $list): void assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); $list[0] = 21; assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); - - $list[2] = 23; - assertType('non-empty-array, int>&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)&hasOffsetValue(2, 23)', $list); } /** @param list $list */ From ae2b6a709ddcd9a4260587565809bd7f1a50d2bd Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:21:27 +0000 Subject: [PATCH 168/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 1505dfeb90..a9260b897c 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.4", - "phpstan/php-8-stubs": "0.4.7", + "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index b40729b153..1135a6a574 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c1cf63bf474629a0d7b5fea00c2ec54c", + "content-hash": "d1310e0c489abe492a66dced167d3f1e", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.7", + "version": "0.4.8", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a" + "reference": "8a6278b2b9c9781cb969c4128da361b78eead604" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/794d410e0de8779afb4706d8667d2b3a11a3865a", - "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/8a6278b2b9c9781cb969c4128da361b78eead604", + "reference": "8a6278b2b9c9781cb969c4128da361b78eead604", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.7" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.8" }, - "time": "2024-11-27T00:20:37+00:00" + "time": "2024-11-28T00:20:48+00:00" }, { "name": "phpstan/phpdoc-parser", From c95367d52b04d524f1fb154e944ddb1ab8cab9a8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 28 Nov 2024 20:40:22 +0000 Subject: [PATCH 169/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index a9260b897c..912996ab0d 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.4", + "ondrejmirtes/better-reflection": "6.44.0.5", "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 1135a6a574..7441a9d501 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d1310e0c489abe492a66dced167d3f1e", + "content-hash": "53bd280e3693edf29e8871fa22ad20f7", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.4", + "version": "6.44.0.5", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650" + "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/3865f1f8779d4e3562886ee261ccfdbf2da15650", - "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b6d0a9adbe61544f6e8992611590a4e61487a638", + "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.4" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.5" }, - "time": "2024-11-27T13:45:40+00:00" + "time": "2024-11-28T20:34:45+00:00" }, { "name": "phpstan/php-8-stubs", From 86197c9987529b5545c0a09ac6ad92581087526f Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 28 Nov 2024 21:07:43 +0000 Subject: [PATCH 170/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 912996ab0d..0f28344835 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.5", + "ondrejmirtes/better-reflection": "6.44.0.6", "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 7441a9d501..7cd32ee1ad 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "53bd280e3693edf29e8871fa22ad20f7", + "content-hash": "aa42dc6e1a7a8d5fc9844d231515e30f", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.5", + "version": "6.44.0.6", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638" + "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b6d0a9adbe61544f6e8992611590a4e61487a638", - "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/d942fd0af0214bb1250a55c2560f061b7b0c4bd4", + "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.5" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.6" }, - "time": "2024-11-28T20:34:45+00:00" + "time": "2024-11-28T21:05:45+00:00" }, { "name": "phpstan/php-8-stubs", From e6dc705b29b90dcc5f9773377c05aecbfe9fba3a Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz <80641364+jakubtobiasz@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:00:31 +0100 Subject: [PATCH 171/220] Sanity checks around hooked properties in interfaces and classes --- .github/workflows/lint.yml | 2 +- Makefile | 10 ++ build/collision-detector.json | 2 +- conf/config.level0.neon | 1 + src/Node/ClassPropertyNode.php | 18 +++ src/Php/PhpVersion.php | 5 + .../Properties/PropertiesInInterfaceRule.php | 59 ++++++- src/Rules/Properties/PropertyInClassRule.php | 113 +++++++++++++ .../PropertiesInInterfaceRuleTest.php | 96 ++++++++++- .../Properties/PropertyInClassRuleTest.php | 150 ++++++++++++++++++ .../abstract-hooked-properties-in-class.php | 10 ++ ...abstract-hooked-properties-with-bodies.php | 26 +++ ...on-hooked-properties-in-abstract-class.php | 10 ++ .../data/hooked-properties-in-class.php | 11 ++ ...ked-properties-without-bodies-in-class.php | 10 ++ ...ct-hooked-properties-in-abstract-class.php | 35 ++++ ...on-abstract-hooked-properties-in-class.php | 10 ++ .../data/properties-in-interface.php | 2 + .../property-hooks-bodies-in-interface.php | 20 +++ .../data/property-hooks-in-interface.php | 10 ++ ...property-hooks-visibility-in-interface.php | 12 ++ 21 files changed, 600 insertions(+), 12 deletions(-) create mode 100644 src/Rules/Properties/PropertyInClassRule.php create mode 100644 tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-with-bodies.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4680ddb82d..86ee004f07 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -116,7 +116,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.3" + php-version: "8.4" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/Makefile b/Makefile index 3282ee2c4e..be44969c3c 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,16 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \ --exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \ --exclude tests/PHPStan/Rules/Classes/data/bug-11592.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-with-bodies.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ src tests cs: diff --git a/build/collision-detector.json b/build/collision-detector.json index 21228704f5..12de9af1d3 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -12,5 +12,5 @@ "../tests/notAutoloaded", "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", "../tests/PHPStan/Levels/data/stubs/function.php" - ] + ] } diff --git a/conf/config.level0.neon b/conf/config.level0.neon index d4927a56c4..8c2a23a9d4 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -96,6 +96,7 @@ rules: - PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule - PHPStan\Rules\Properties\PropertiesInInterfaceRule - PHPStan\Rules\Properties\PropertyAttributesRule + - PHPStan\Rules\Properties\PropertyInClassRule - PHPStan\Rules\Properties\ReadOnlyPropertyRule - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule - PHPStan\Rules\Regexp\RegularExpressionPatternRule diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index 3f500b62c1..c8602aef79 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -111,6 +111,11 @@ public function isAllowedPrivateMutation(): bool return $this->isAllowedPrivateMutation; } + public function isAbstract(): bool + { + return (bool) ($this->flags & Modifiers::ABSTRACT); + } + public function getNativeType(): ?Type { return $this->type; @@ -142,4 +147,17 @@ public function getSubNodeNames(): array return []; } + public function hasHooks(): bool + { + return $this->getHooks() !== []; + } + + /** + * @return Node\PropertyHook[] + */ + public function getHooks(): array + { + return $this->originalNode->hooks; + } + } diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 8520f6488d..e636945cc2 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -347,6 +347,11 @@ public function supportsPregCaptureOnlyNamedGroups(): bool return $this->versionId >= 80200; } + public function supportsPropertyHooks(): bool + { + return $this->versionId >= 80400; + } + public function hasDateTimeExceptions(): bool { return $this->versionId >= 80300; diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php index d9f62fa618..df5354adb3 100644 --- a/src/Rules/Properties/PropertiesInInterfaceRule.php +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,6 +15,10 @@ final class PropertiesInInterfaceRule implements Rule { + public function __construct(private PhpVersion $phpVersion) + { + } + public function getNodeType(): string { return ClassPropertyNode::class; @@ -25,12 +30,54 @@ public function processNode(Node $node, Scope $scope): array return []; } - return [ - RuleErrorBuilder::message('Interfaces may not include properties.') - ->nonIgnorable() - ->identifier('property.inInterface') - ->build(), - ]; + if (!$this->phpVersion->supportsPropertyHooks()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include properties.') + ->nonIgnorable() + ->identifier('property.inInterface') + ->build(), + ]; + } + + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Interfaces can only include hooked properties.') + ->nonIgnorable() + ->identifier('property.nonHookedInInterface') + ->build(), + ]; + } + + if (!$node->isPublic()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include non-public properties.') + ->nonIgnorable() + ->identifier('property.nonPublicInInterface') + ->build(), + ]; + } + + if ($this->hasAnyHookBody($node)) { + return [ + RuleErrorBuilder::message('Interfaces cannot include property hooks with bodies.') + ->nonIgnorable() + ->identifier('property.hookBodyInInterface') + ->build(), + ]; + } + + return []; + } + + private function hasAnyHookBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body !== null) { + return true; + } + } + + return false; } } diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php new file mode 100644 index 0000000000..3500959541 --- /dev/null +++ b/src/Rules/Properties/PropertyInClassRule.php @@ -0,0 +1,113 @@ + + */ +final class PropertyInClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->isClass()) { + return []; + } + + if (!$this->phpVersion->supportsPropertyHooks()) { + if ($node->hasHooks()) { + return [ + RuleErrorBuilder::message('Property hooks are supported only on PHP 8.4 and later.') + ->nonIgnorable() + ->identifier('property.hooksNotSupported') + ->build(), + ]; + } + + return []; + } + + if ($node->isAbstract()) { + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Only hooked properties can be declared abstract.') + ->nonIgnorable() + ->identifier('property.abstractNonHooked') + ->build(), + ]; + } + + if (!$this->isAtLeastOneHookBodyEmpty($node)) { + return [ + RuleErrorBuilder::message('Abstract properties must specify at least one abstract hook.') + ->nonIgnorable() + ->identifier('property.abstractWithoutAbstractHook') + ->build(), + ]; + } + + if (!$classReflection->isAbstract()) { + return [ + RuleErrorBuilder::message('Non-abstract classes cannot include abstract properties.') + ->nonIgnorable() + ->identifier('property.abstract') + ->build(), + ]; + } + + return []; + } + + if (!$this->doAllHooksHaveBody($node)) { + return [ + RuleErrorBuilder::message('Non-abstract properties cannot include hooks without bodies.') + ->nonIgnorable() + ->identifier('property.hookWithoutBody') + ->build(), + ]; + } + + return []; + } + + private function doAllHooksHaveBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return false; + } + } + + return true; + } + + private function isAtLeastOneHookBodyEmpty(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return true; + } + } + + return false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php index ecf4597d97..de425931da 100644 --- a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,21 +15,107 @@ class PropertiesInInterfaceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new PropertiesInInterfaceRule(); + return new PropertiesInInterfaceRule(new PhpVersion(PHP_VERSION_ID)); } - public function testRule(): void + public function testPhp83AndPropertiesInInterface(): void { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ + [ + 'Interfaces cannot include properties.', + 7, + ], + [ + 'Interfaces cannot include properties.', + 9, + ], + [ + 'Interfaces cannot include properties.', + 11, + ], + ]); + } + + public function testPhp83AndPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-in-interface.php'], [ + [ + 'Interfaces cannot include properties.', + 7, + ], + [ + 'Interfaces cannot include properties.', + 9, + ], + ]); + } + + public function testPhp84AndPropertiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ [ - 'Interfaces may not include properties.', + 'Interfaces can only include hooked properties.', + 9, + ], + [ + 'Interfaces can only include hooked properties.', + 11, + ], + ]); + } + + public function testPhp84AndNonPublicPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-visibility-in-interface.php'], [ + [ + 'Interfaces cannot include non-public properties.', 7, ], [ - 'Interfaces may not include properties.', + 'Interfaces cannot include non-public properties.', 9, ], ]); } + public function testPhp84AndPropertyHooksWithBodiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-bodies-in-interface.php'], [ + [ + 'Interfaces cannot include property hooks with bodies.', + 7, + ], + [ + 'Interfaces cannot include property hooks with bodies.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php new file mode 100644 index 0000000000..0b2ca5ba09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php @@ -0,0 +1,150 @@ + + */ +class PropertyInClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyInClassRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testPhpLessThan84AndHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-in-class.php'], [ + [ + 'Property hooks are supported only on PHP 8.4 and later.', + 7, + ], + ]); + } + + public function testPhp84AndHookedPropertiesWithoutBodiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-without-bodies-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract classes cannot include abstract properties.', + 7, + ], + [ + 'Non-abstract classes cannot include abstract properties.', + 9, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-abstract-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 25, + ], + ]); + } + + public function testPhp84AndAbstractNonHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-non-hooked-properties-in-abstract-class.php'], [ + [ + 'Only hooked properties can be declared abstract.', + 7, + ], + [ + 'Only hooked properties can be declared abstract.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesWithBodies(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-with-bodies.php'], [ + [ + 'Abstract properties must specify at least one abstract hook.', + 7, + ], + [ + 'Abstract properties must specify at least one abstract hook.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php new file mode 100644 index 0000000000..d035d36810 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } + + public abstract string $lastName { + get => $this->lastName; + set => $this->lastName = $value; + } + + public abstract string $middleName { + get => $this->name; + set; + } + + public abstract string $familyName { + get; + set; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..b34e66a886 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php new file mode 100644 index 0000000000..dc839f0d2c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php @@ -0,0 +1,10 @@ + Date: Mon, 11 Nov 2024 17:15:04 +0100 Subject: [PATCH 172/220] get_defined_vars() return type contains known variables Co-Authored-By: Ruud Kamphuis --- conf/config.neon | 5 ++ ...DefinedVarsFunctionReturnTypeExtension.php | 46 +++++++++++++++++++ .../Analyser/nsrt/get-defined-vars.php | 46 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/get-defined-vars.php diff --git a/conf/config.neon b/conf/config.neon index f8ad1af8b3..19b6388858 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1525,6 +1525,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GetDefinedVarsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..35999b424b --- /dev/null +++ b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php @@ -0,0 +1,46 @@ +getName() === 'get_defined_vars'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if ($scope->canAnyVariableExist()) { + return new ArrayType( + new StringType(), + new MixedType(), + ); + } + + $typeBuilder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($scope->getDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), false); + } + + foreach ($scope->getMaybeDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), true); + } + + return $typeBuilder->getArray(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/get-defined-vars.php b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php new file mode 100644 index 0000000000..345d54dbd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php @@ -0,0 +1,46 @@ +', get_defined_vars()); // any variable can exist + +function doFoo(int $param) { + $local = "foo"; + assertType('array{param: int, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'local\'}', array_keys(get_defined_vars())); +} + +function doBar(int $param) { + global $global; + $local = "foo"; + assertType('array{param: int, global: mixed, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'global\', \'local\'}', array_keys(get_defined_vars())); +} + +function doConditional(int $param) { + $local = "foo"; + if(true) { + $conditional = "bar"; + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); + } else { + $other = "baz"; + assertType('array{param: int, local: \'foo\', other: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); +} + +function doRandom(int $param) { + $local = "foo"; + if(rand(0, 1)) { + $random1 = "bar"; + assertType('array{param: int, local: \'foo\', random1: \'bar\'}', get_defined_vars()); + } else { + $random2 = "baz"; + assertType('array{param: int, local: \'foo\', random2: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', random2?: \'baz\', random1?: \'bar\'}', get_defined_vars()); +} From 5efffc694288b20e591709748a34aae7239e173e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 30 Nov 2024 07:55:27 +0100 Subject: [PATCH 173/220] Lazier return in `UnionType->isSuperTypeOfWithReason()` --- src/Type/UnionType.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 6f4cbf462b..14eb812267 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -245,10 +245,15 @@ public function isSuperTypeOfWithReason(Type $otherType): IsSuperTypeOfResult return $otherType->isSubTypeOfWithReason($this); } - $result = IsSuperTypeOfResult::createNo()->or(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOfWithReason($otherType), $this->types)); - if ($result->yes()) { - return $result; + $results = []; + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOfWithReason($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); if ($otherType instanceof TemplateUnionType) { return $result->or($otherType->isSubTypeOfWithReason($this)); From 25d712e9dd8ed236ff2b94a35e5add932b9a81a8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 20 Nov 2024 20:48:04 +0800 Subject: [PATCH 174/220] Fix `iterator_to_array` return type with generators --- ...atorToArrayFunctionReturnTypeExtension.php | 9 +++++- .../Analyser/nsrt/iterator_to_array.php | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index 3eba789175..623e3c0bcd 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -8,7 +8,9 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use function strtolower; @@ -29,7 +31,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType(); + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); + + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); + } + $isList = false; if (isset($arguments[1])) { diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php index 64ecbdeb05..4c7ddbc2b0 100644 --- a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -2,6 +2,7 @@ namespace IteratorToArray; +use stdClass; use Traversable; use function iterator_to_array; use function PHPStan\Testing\assertType; @@ -31,4 +32,33 @@ public function testNotPreservingKeys(Traversable $foo) { assertType('list', iterator_to_array($foo, false)); } + + public function testBehaviorOnGenerators(): void + { + $generator1 = static function (): iterable { + yield 0 => 1; + yield true => 2; + yield 2 => 3; + yield null => 4; + }; + $generator2 = static function (): iterable { + yield 0 => 1; + yield 'a' => 2; + yield null => 3; + yield true => 4; + }; + + assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + } + + public function testOnGeneratorsWithIllegalKeysForArray(): void + { + $illegalGenerator = static function (): iterable { + yield 'a' => 'b'; + yield new stdClass => 'c'; + }; + + assertType('*NEVER*', iterator_to_array($illegalGenerator())); + } } From bd4452864826dd690faac1ddc7f060c44fcea3da Mon Sep 17 00:00:00 2001 From: schlndh Date: Sat, 30 Nov 2024 09:01:04 +0100 Subject: [PATCH 175/220] skip param castable to X on non-arrays Fixes bug 12146 --- src/Rules/ParameterCastableToStringCheck.php | 8 +-- ...plodeParameterCastableToStringRuleTest.php | 12 ++++- .../ParameterCastableToNumberRuleTest.php | 16 +++++- .../ParameterCastableToStringRuleTest.php | 16 +++++- .../SortParameterCastableToStringRuleTest.php | 12 ++++- .../Rules/Functions/data/bug-12146.php | 49 +++++++++++++++++++ 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12146.php diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php index 2d4dea0dbd..9753863557 100644 --- a/src/Rules/ParameterCastableToStringCheck.php +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -36,11 +36,13 @@ public function checkParameter( $scope, $parameter->value, '', - static fn (Type $type): bool => !$castFn($type->getIterableValueType()) instanceof ErrorType, + static fn (Type $type): bool => $type->isArray()->yes() && !$castFn($type->getIterableValueType()) instanceof ErrorType, ); - if ($typeResult->getType() instanceof ErrorType - || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType) { + if ( + ! $typeResult->getType()->isArray()->yes() + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType + ) { return null; } diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php index 8111e9a965..65e4714487 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -17,7 +17,7 @@ class ImplodeParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testNamedArguments(): void @@ -100,4 +100,14 @@ public function testBug8467a(): void $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 28, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php index 77b64c3a30..e4708c5f4c 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -19,7 +19,7 @@ class ParameterCastableToNumberRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -130,6 +130,20 @@ public function testBug11883(): void ]); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 16, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 22, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php index 83b0f40567..0ac6304231 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -19,7 +19,7 @@ class ParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -196,6 +196,20 @@ public function testBug11141(): void ]); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 34, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 40, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php index 8c0105a424..2fc8264821 100644 --- a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -19,7 +19,7 @@ class SortParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -145,6 +145,16 @@ public function testBug11167(): void $this->analyse([__DIR__ . '/data/bug-11167.php'], []); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 46, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/data/bug-12146.php b/tests/PHPStan/Rules/Functions/data/bug-12146.php new file mode 100644 index 0000000000..bd0bf858c3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12146.php @@ -0,0 +1,49 @@ +|array $validArrayUnion valid + * @param array|array<\stdClass> $invalidArrayUnion invalid, report + * @param ?array<\stdClass> $nullableInvalidArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @param array<\stdClass>|\SplFixedArray $arrayOrSplArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @return void + */ +function foo($mixed, $validArrayUnion, $invalidArrayUnion, $nullableInvalidArray, $arrayOrSplArray) { + var_dump(array_sum($mixed)); + var_dump(array_sum($validArrayUnion)); + var_dump(array_sum($invalidArrayUnion)); + var_dump(array_sum($nullableInvalidArray)); + var_dump(array_sum($arrayOrSplArray)); + + var_dump(array_product($mixed)); + var_dump(array_product($validArrayUnion)); + var_dump(array_product($invalidArrayUnion)); + var_dump(array_product($nullableInvalidArray)); + var_dump(array_product($arrayOrSplArray)); + + var_dump(implode(',', $mixed)); + var_dump(implode(',', $validArrayUnion)); + var_dump(implode(',', $invalidArrayUnion)); + var_dump(implode(',', $nullableInvalidArray)); + var_dump(implode(',', $arrayOrSplArray)); + + var_dump(array_intersect($mixed, [5])); + var_dump(array_intersect($validArrayUnion, [5])); + var_dump(array_intersect($invalidArrayUnion, [5])); + var_dump(array_intersect($nullableInvalidArray, [5])); + var_dump(array_intersect($arrayOrSplArray, [5])); + + var_dump(array_fill_keys($mixed, 1)); + var_dump(array_fill_keys($validArrayUnion, 1)); + var_dump(array_fill_keys($invalidArrayUnion, 1)); + var_dump(array_fill_keys($nullableInvalidArray, 1)); + var_dump(array_fill_keys($arrayOrSplArray, 1)); + + var_dump(array_unique($mixed)); + var_dump(array_unique($validArrayUnion)); + var_dump(array_unique($invalidArrayUnion)); + var_dump(array_unique($nullableInvalidArray)); + var_dump(array_unique($arrayOrSplArray)); +} From 48f899084c0786d84ae86463bfc25711e4aacd36 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 30 Nov 2024 14:49:14 +0100 Subject: [PATCH 176/220] 5x Faster `IntersectionType->getEnumCases()` --- src/Type/IntersectionType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index d41b79e951..519649a384 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -842,7 +842,7 @@ public function getEnumCases(): array foreach ($this->types as $type) { $oneType = []; foreach ($type->getEnumCases() as $enumCase) { - $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase; + $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase; } $compare[] = $oneType; } From c5860144ae7c00bc860ae8eb41d7343137829467 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 30 Nov 2024 16:09:56 +0100 Subject: [PATCH 177/220] `MixedType::toArrayKey()` returns BenevolentUnionType --- src/Type/MixedType.php | 2 +- .../Analyser/LegacyNodeScopeResolverTest.php | 4 ++-- tests/PHPStan/Analyser/nsrt/array-column-php82.php | 12 ++++++------ tests/PHPStan/Analyser/nsrt/array-column.php | 14 +++++++------- tests/PHPStan/Analyser/nsrt/array-flip-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array-flip-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/array-flip.php | 2 +- tests/PHPStan/Analyser/nsrt/non-empty-array.php | 2 +- .../data/slevomat-foreach-array-key-exists-bug.php | 10 +++++----- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 3ae9434a95..6ca4e4bad8 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -600,7 +600,7 @@ public function toArray(): Type public function toArrayKey(): Type { - return new UnionType([new IntegerType(), new StringType()]); + return new BenevolentUnionType([new IntegerType(), new StringType()]); } public function isIterable(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 5434782186..42b0d24959 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -3328,7 +3328,7 @@ public function dataLiteralArraysKeys(): array "'BooleansArray'", ], [ - 'int|string', + '(int|string)', "'UnknownConstantArray'", ], ]; @@ -9147,7 +9147,7 @@ public function dataGeneralizeScope(): array { return [ [ - 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php index 62350f5992..d53ab61f00 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -177,8 +177,8 @@ public function testImprecise5(array $array): void assertType('array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('array', array_column($array, 'nodeName', 'foo')); - assertType('array', array_column($array, null, 'foo')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); } /** @param non-empty-array $array */ @@ -189,8 +189,8 @@ public function testObjects1(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } /** @param array{DOMElement} $array */ @@ -201,8 +201,8 @@ public function testObjects2(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php index 2455d6ace9..3e3cedbbff 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -191,8 +191,8 @@ public function testImprecise5(array $array): void assertType('array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('array', array_column($array, 'nodeName', 'foo')); - assertType('array', array_column($array, null, 'foo')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); } /** @param non-empty-array $array */ @@ -203,8 +203,8 @@ public function testObjects1(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } /** @param array{DOMElement} $array */ @@ -215,8 +215,8 @@ public function testObjects2(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } @@ -228,7 +228,7 @@ final class Foo public function doFoo(array $a): void { assertType('list', array_column($a, 'nodeName')); - assertType('array', array_column($a, 'nodeName', 'tagName')); + assertType('array', array_column($a, 'nodeName', 'tagName')); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php index 0b7058de01..0f14074f5c 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { assertType('mixed~array', $mixed); assertType('null', array_flip($mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php index b8f0e6793d..be439d0427 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { assertType('mixed~array', $mixed); assertType('*NEVER*', array_flip($mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/array-flip.php b/tests/PHPStan/Analyser/nsrt/array-flip.php index 2f02f1e733..b6a6eb6c66 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip.php @@ -20,7 +20,7 @@ function foo3($list) { $flip = array_flip($list); - assertType('array', $flip); + assertType('array<(int|string)>', $flip); } /** diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php index a7cdc6540a..9f95a09c0a 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array.php @@ -54,7 +54,7 @@ public function arrayFunctions($array, $list, $stringArray): void assertType('non-empty-array', array_replace($array, [])); assertType('non-empty-array', array_replace($array, $array)); - assertType('non-empty-array', array_flip($array)); + assertType('non-empty-array<(int|string)>', array_flip($array)); assertType('non-empty-array', array_flip($stringArray)); } } diff --git a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php index bddbb8f06d..0588be365b 100644 --- a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php +++ b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php @@ -15,26 +15,26 @@ public function doFoo(array $percentageIntervals, array $changes): void if ($percentageInterval->isInInterval((float) $changeInPercents)) { $key = $percentageInterval->getFormatted(); if (array_key_exists($key, $intervalResults)) { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key]['itemsCount'] += $itemsCount; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]); } else { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key] = [ 'itemsCount' => $itemsCount, 'interval' => $percentageInterval, ]; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); } } } } - assertType('array', $intervalResults); + assertType('array', $intervalResults); foreach ($intervalResults as $data) { echo $data['interval']; } From f8d27d5a803d7923a60e267cc8a78bfc2b8b9e10 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:22:35 +0000 Subject: [PATCH 178/220] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 0f28344835..5e0a1f1e01 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.6", - "phpstan/php-8-stubs": "0.4.8", + "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index 7cd32ee1ad..22c9a79b6e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa42dc6e1a7a8d5fc9844d231515e30f", + "content-hash": "db7c74816a1b1cd707c98ae62551ce35", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.8", + "version": "0.4.9", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "8a6278b2b9c9781cb969c4128da361b78eead604" + "reference": "1857c330fea6e795af1f7435ed02a18652e7dd8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/8a6278b2b9c9781cb969c4128da361b78eead604", - "reference": "8a6278b2b9c9781cb969c4128da361b78eead604", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/1857c330fea6e795af1f7435ed02a18652e7dd8c", + "reference": "1857c330fea6e795af1f7435ed02a18652e7dd8c", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.8" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.9" }, - "time": "2024-11-28T00:20:48+00:00" + "time": "2024-12-02T00:21:59+00:00" }, { "name": "phpstan/phpdoc-parser", From 90933b318e332c88f675c8aa946cdfb5788a51fe Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 2 Dec 2024 22:54:08 +0200 Subject: [PATCH 179/220] Remove incorrect CURLOPT_ACCEPT_ENCODING alias --- src/Reflection/ParametersAcceptorSelector.php | 3 +-- .../CallToFunctionParametersRuleTest.php | 26 ++++++++++++------- .../Rules/Functions/data/curl_setopt.php | 4 +++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 82af7d4267..703d345806 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -919,7 +919,6 @@ private static function getCurlOptValueType(int $curlOpt): ?Type } $nullableStringConstants = [ - 'CURLOPT_ACCEPT_ENCODING', 'CURLOPT_CUSTOMREQUEST', 'CURLOPT_DNS_INTERFACE', 'CURLOPT_DNS_LOCAL_IP4', @@ -1032,7 +1031,7 @@ private static function getCurlOptValueType(int $curlOpt): ?Type $stringConstants = [ 'CURLOPT_COOKIEFILE', - 'CURLOPT_ENCODING', + 'CURLOPT_ENCODING', // Alias: CURLOPT_ACCEPT_ENCODING 'CURLOPT_PRE_PROXY', 'CURLOPT_PRIVATE', 'CURLOPT_PROXY', diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index af3d4af2c3..89017fe2cf 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1321,40 +1321,48 @@ public function testCurlSetOpt(): void 18, ], [ - 'Parameter #3 $value of function curl_setopt expects bool, int given.', + 'Parameter #3 $value of function curl_setopt expects string, int given.', + 19, + ], + [ + 'Parameter #3 $value of function curl_setopt expects string, int given.', 20, ], + [ + 'Parameter #3 $value of function curl_setopt expects bool, int given.', + 22, + ], [ 'Parameter #3 $value of function curl_setopt expects bool, string given.', - 21, + 23, ], [ 'Parameter #3 $value of function curl_setopt expects int, string given.', - 23, + 25, ], [ 'Parameter #3 $value of function curl_setopt expects array, string given.', - 25, + 27, ], [ 'Parameter #3 $value of function curl_setopt expects resource, string given.', - 27, + 29, ], [ 'Parameter #3 $value of function curl_setopt expects array|string, int given.', - 29, + 31, ], [ 'Parameter #3 $value of function curl_setopt expects non-empty-string, \'\' given.', - 31, + 33, ], [ 'Parameter #3 $value of function curl_setopt expects non-empty-string|null, \'\' given.', - 32, + 34, ], [ 'Parameter #3 $value of function curl_setopt expects array, array given.', - 73, + 77, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php index 24987ddbc9..bc5b8f6cea 100644 --- a/tests/PHPStan/Rules/Functions/data/curl_setopt.php +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -16,6 +16,8 @@ public function errors(int $i, string $s) { curl_setopt($curl, CURLOPT_URL, $i); curl_setopt($curl, CURLOPT_HTTPHEADER, $i); curl_setopt($curl, CURLOPT_ABSTRACT_UNIX_SOCKET, null); + curl_setopt($curl, CURLOPT_ENCODING, $i); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, $i); // expecting bool curl_setopt($curl, CURLOPT_AUTOREFERER, $i); curl_setopt($curl, CURLOPT_RETURNTRANSFER, $s); @@ -62,6 +64,8 @@ public function allGood(string $url, array $header) { curl_setopt($curl, CURLOPT_PRE_PROXY, ''); curl_setopt($curl, CURLOPT_PROXY, ''); curl_setopt($curl, CURLOPT_PRIVATE, ''); + curl_setopt($curl, CURLOPT_ENCODING, ''); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, ''); } public function bug9263() { From 28dfac80bb4a05891ad4da675677c2077098d080 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:03:57 +0000 Subject: [PATCH 180/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 5e0a1f1e01..c678b5998f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "jetbrains/phpstorm-stubs": "dev-master#bb981ec60b3838e56473a078edf7d0739ca20403", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 22c9a79b6e..8557d36dae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "db7c74816a1b1cd707c98ae62551ce35", + "content-hash": "ee384a5c11fbc1dd087ab8dc956b8d73", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7" + "reference": "bb981ec60b3838e56473a078edf7d0739ca20403" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", - "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bb981ec60b3838e56473a078edf7d0739ca20403", + "reference": "bb981ec60b3838e56473a078edf7d0739ca20403", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-27T14:37:09+00:00" + "time": "2024-11-27T16:45:26+00:00" }, { "name": "nette/bootstrap", From fbcad414543b6f6fae4acc2801de21bfa1348887 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 5 Dec 2024 12:44:05 +0100 Subject: [PATCH 181/220] Fix `fgetcsv` return type; never returns null --- resources/functionMap_php80delta.php | 1 + tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php | 12 ++++++++++++ tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php | 12 ++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php create mode 100644 tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index bb267a5e49..879dc310e4 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -46,6 +46,7 @@ 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'fgetcsv' => ['list|array{0: null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], 'floor' => ['float', 'number'=>'float'], diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php new file mode 100644 index 0000000000..8a38465040 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php @@ -0,0 +1,12 @@ +|false|null', fgetcsv($resource)); // nullable when invalid argument is given (https://3v4l.org/4WmR5#v7.4.30) +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php new file mode 100644 index 0000000000..fccf29931c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php @@ -0,0 +1,12 @@ += 8.0 + +declare(strict_types = 1); + +namespace TestFGetCsvPhp8; + +use function PHPStan\Testing\assertType; + +function test($resource): void +{ + assertType('list|false', fgetcsv($resource)); +} From 7b4c9afd090d89d595eb113831bc4b79b45d22e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinbrink?= Date: Thu, 5 Dec 2024 12:15:02 +0100 Subject: [PATCH 182/220] Workaround bug in slevomat/coding-standard TypeNameMatchesFileName For each root namespace, the slevomat rule considers the left-most match of the given directory in the absolute path of the file. That is, for /home/user/src/phpstan-src/ the root namespace PHPStan is not assigned to /home/user/src/phpstan-src/src, but to /home/user/src, which is obviously wrong. The bug is known as slevomat/coding-standard#1249 for a long time, but yet to be fixed. To avoid issues for developers of PHPStan, we can set a basepath of "." in the PHP CodeSniffer config, which causes paths to be evaluated relative to the current directory, avoiding false-positives in the path leading up to the phpstan-src directory. --- phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml b/phpcs.xml index 7ec0a21da2..fa9198745f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,7 @@ + From 4f142f59424b7d49856fdba9d70ac4d3ef184d36 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Fri, 6 Dec 2024 20:09:22 +0800 Subject: [PATCH 183/220] Fix `iterator_to_array` to early return when `$preserveKeys` is false --- ...atorToArrayFunctionReturnTypeExtension.php | 27 ++++++++----------- .../Analyser/nsrt/iterator_to_array.php | 3 +++ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index 623e3c0bcd..d1dc17f176 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -31,33 +31,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); - - if ($arrayKeyType instanceof ErrorType) { - return new NeverType(true); - } - - $isList = false; if (isset($arguments[1])) { $preserveKeysType = $scope->getType($arguments[1]->value); if ($preserveKeysType->isFalse()->yes()) { - $arrayKeyType = new IntegerType(); - $isList = true; + return AccessoryArrayListType::intersectWith(new ArrayType( + new IntegerType(), + $traversableType->getIterableValueType(), + )); } } - $arrayType = new ArrayType( - $arrayKeyType, - $traversableType->getIterableValueType(), - ); + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); - if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); } - return $arrayType; + return new ArrayType( + $arrayKeyType, + $traversableType->getIterableValueType(), + ); } } diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php index 4c7ddbc2b0..038b36f7fd 100644 --- a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -49,7 +49,9 @@ public function testBehaviorOnGenerators(): void }; assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('list<1|2|3|4>', iterator_to_array($generator1(), false)); assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + assertType('list<1|2|3|4>', iterator_to_array($generator2(), false)); } public function testOnGeneratorsWithIllegalKeysForArray(): void @@ -60,5 +62,6 @@ public function testOnGeneratorsWithIllegalKeysForArray(): void }; assertType('*NEVER*', iterator_to_array($illegalGenerator())); + assertType('list<\'b\'|\'c\'>', iterator_to_array($illegalGenerator(), false)); } } From 9a718ee52c07c6b62c0eceac3bf170ac45740b6a Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Wed, 4 Dec 2024 20:26:32 -0500 Subject: [PATCH 184/220] Fix-GH-12021 --- ...mptyStringFunctionsReturnTypeExtension.php | 30 +++++++++++++++++++ .../Analyser/nsrt/non-empty-string.php | 9 ++++++ .../Analyser/nsrt/non-falsy-string.php | 5 ++++ 3 files changed, 44 insertions(+) diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index 083914085a..893627aae2 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -2,17 +2,20 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; use function in_array; +use const ENT_SUBSTITUTE; final class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -45,6 +48,15 @@ public function getTypeFromFunctionCall( return null; } + if (in_array($functionReflection->getName(), [ + 'htmlspecialchars', + 'htmlentities', + ], true)) { + if (!$this->isSubstituteFlagSet($args, $scope)) { + return new StringType(); + } + } + $argType = $scope->getType($args[0]->value); if ($argType->isNonFalsyString()->yes()) { return new IntersectionType([ @@ -62,4 +74,22 @@ public function getTypeFromFunctionCall( return new StringType(); } + /** + * @param Arg[] $args + */ + private function isSubstituteFlagSet( + array $args, + Scope $scope, + ): bool + { + if (!isset($args[1])) { + return true; + } + $flagsType = $scope->getType($args[1]->value); + if (!$flagsType instanceof ConstantIntegerType) { + return false; + } + return (bool) ($flagsType->getValue() & ENT_SUBSTITUTE); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index cd831db4d8..59d1af3931 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -8,6 +8,7 @@ use function strtolower; use function strtoupper; use function ucfirst; +use const ENT_SUBSTITUTE; class Foo { @@ -333,9 +334,17 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', ucwords($s)); assertType('non-empty-string', ucwords($nonEmpty)); assertType('string', htmlspecialchars($s)); + assertType('string', htmlspecialchars($s, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($s, 0)); assertType('non-empty-string', htmlspecialchars($nonEmpty)); + assertType('non-empty-string', htmlspecialchars($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonEmpty, 0)); assertType('string', htmlentities($s)); + assertType('string', htmlentities($s, ENT_SUBSTITUTE)); + assertType('string', htmlentities($s, 0)); assertType('non-empty-string', htmlentities($nonEmpty)); + assertType('non-empty-string', htmlentities($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonEmpty, 0)); assertType('string', urlencode($s)); assertType('non-empty-string', urlencode($nonEmpty)); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index c5fd9fc1d8..744bdaf3b5 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -3,6 +3,7 @@ namespace NonFalseyString; use function PHPStan\Testing\assertType; +use const ENT_SUBSTITUTE; class Foo { /** @@ -95,7 +96,11 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', ucfirst($nonFalsey)); assertType('non-falsy-string', ucwords($nonFalsey)); assertType('non-falsy-string', htmlspecialchars($nonFalsey)); + assertType('non-falsy-string', htmlspecialchars($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonFalsey, 0)); assertType('non-falsy-string', htmlentities($nonFalsey)); + assertType('non-falsy-string', htmlentities($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonFalsey, 0)); assertType('non-falsy-string', urlencode($nonFalsey)); assertType('non-falsy-string', urldecode($nonFalsey)); From acb109f0c4ad8871c11d6df962a78255b313f463 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Dec 2024 09:56:05 +0100 Subject: [PATCH 185/220] Try reproduce a bug locally --- .../Rules/Keywords/RequireFileExistsRuleTest.php | 14 ++++++++++++++ tests/PHPStan/Rules/Keywords/data/bug-12203.php | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/PHPStan/Rules/Keywords/data/bug-12203.php diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 6bd3e1dfd7..6bc5dc45c2 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -121,4 +121,18 @@ public function testBug11738(): void $this->analyse([__DIR__ . '/data/bug-11738/bug-11738.php'], []); } + public function testBug12203(): void + { + $this->analyse([__DIR__ . '/data/bug-12203.php'], [ + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 5, + ], + [ + 'Path in require_once() "' . __DIR__ . '/data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 6, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Keywords/data/bug-12203.php b/tests/PHPStan/Rules/Keywords/data/bug-12203.php new file mode 100644 index 0000000000..d64dcc6ebe --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/bug-12203.php @@ -0,0 +1,6 @@ + Date: Mon, 9 Dec 2024 10:06:07 +0100 Subject: [PATCH 186/220] Fix --- tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 6bc5dc45c2..732819b506 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -8,6 +8,7 @@ use function implode; use function realpath; use function set_include_path; +use const DIRECTORY_SEPARATOR; use const PATH_SEPARATOR; /** @@ -129,7 +130,7 @@ public function testBug12203(): void 5, ], [ - 'Path in require_once() "' . __DIR__ . '/data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', 6, ], ]); From 621e16829817e412f948420f24640971ee84e667 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Dec 2024 10:10:24 +0100 Subject: [PATCH 187/220] Optimization - do not enter anonymous classes during loop analysis --- src/Analyser/NodeScopeResolver.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5c4d86526e..5f518077a2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -846,6 +846,9 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\Trait_) { return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { + if (!$context->isTopLevel()) { + return new StatementResult($scope, false, false, [], [], []); + } $hasYield = false; $throwPoints = []; $impurePoints = []; From 09eab21c16ec92d238feb95520b5edf16a5a0d1d Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 9 Dec 2024 10:38:29 +0100 Subject: [PATCH 188/220] Add regression test for array self-append --- .../Analyser/AnalyserIntegrationTest.php | 6 ++ tests/PHPStan/Analyser/data/bug-6948.php | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-6948.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index ebdf3a03e4..a4ee70e41f 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -981,6 +981,12 @@ public function testArrayUnion(): void $this->assertNoErrors($errors); } + public function testBug6948(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6948.php'); + $this->assertNoErrors($errors); + } + public function testBug7963(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7963.php'); diff --git a/tests/PHPStan/Analyser/data/bug-6948.php b/tests/PHPStan/Analyser/data/bug-6948.php new file mode 100644 index 0000000000..592f0af752 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6948.php @@ -0,0 +1,64 @@ + Date: Mon, 9 Dec 2024 21:56:00 +0100 Subject: [PATCH 189/220] Array map on multiple elements is a list --- .../Php/ArrayMapFunctionReturnTypeExtension.php | 10 ++++++---- .../PHPStan/Analyser/nsrt/array_map_multiple.php | 10 +++++----- .../PHPStan/Rules/Methods/ReturnTypeRuleTest.php | 5 +++++ tests/PHPStan/Rules/Methods/data/bug-12223.php | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12223.php diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index ce29b37d2a..31c239af58 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -168,10 +168,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } } else { - $mappedArrayType = TypeCombinator::intersect(new ArrayType( - new IntegerType(), - $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)); + $mappedArrayType = AccessoryArrayListType::intersectWith( + TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)), + ); } if ($arrayType->isIterableAtLeastOnce()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php index d986969c3e..2918ebeb89 100644 --- a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -15,7 +15,7 @@ public function doFoo(int $i, string $s): void return rand(0, 1) ? $a : $b; }, ['foo' => $i], ['bar' => $s]); - assertType('non-empty-array', $result); + assertType('non-empty-list', $result); } /** @@ -26,12 +26,12 @@ public function arrayMapNull(array $array, array $other): void { assertType('array{}', array_map(null, [])); assertType('array{foo: true}', array_map(null, ['foo' => true])); - assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); + assertType('non-empty-list', array_map(null, [1, 2, 3], [4, 5, 6])); assertType('non-empty-array', array_map(null, $array)); - assertType('non-empty-array', array_map(null, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $other)); + assertType('non-empty-list', array_map(null, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $other)); assertType('array{1}|array{true}', array_map(null, rand() ? [1] : [true])); assertType('array{1}|array{true, false}', array_map(null, rand() ? [1] : [true, false])); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index eaa0465a42..13082d79b0 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1064,4 +1064,9 @@ public function testBug11857(): void $this->analyse([__DIR__ . '/data/bug-11857-builder.php'], []); } + public function testBug12223(): void + { + $this->analyse([__DIR__ . '/data/bug-12223.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12223.php b/tests/PHPStan/Rules/Methods/data/bug-12223.php new file mode 100644 index 0000000000..29334bf835 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12223.php @@ -0,0 +1,15 @@ + + */ + public function sayHello(): array + { + $a = [1 => 'foo', 3 => 'bar', 5 => 'baz']; + return array_map(static fn(string $s, int $i): string => $s . $i, $a, array_keys($a)); + } +} From 71b25bc5c50b84433ec608284630ca8c25d97781 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 10 Dec 2024 11:47:07 +0100 Subject: [PATCH 190/220] Fix after merge --- src/Type/Php/ArrayMapFunctionReturnTypeExtension.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index e79a889dd7..145756b971 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -168,12 +168,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } } else { - $mappedArrayType = AccessoryArrayListType::intersectWith( - TypeCombinator::intersect(new ArrayType( - new IntegerType(), - $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)), - ); + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, + ), new AccessoryArrayListType(), ...TypeUtils::getAccessoryTypes($arrayType)); } if ($arrayType->isIterableAtLeastOnce()->yes()) { From 9ca11225524a12176640d0c519cd835a3a6596af Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 1 Dec 2024 02:45:14 +0800 Subject: [PATCH 191/220] Introduce `ClassAsClassConstantRule` --- Makefile | 1 + conf/config.level0.neon | 1 + .../Constants/ClassAsClassConstantRule.php | 40 +++++++++++++++++++ .../ClassAsClassConstantRuleTest.php | 33 +++++++++++++++ .../data/class-as-class-constant.php | 17 ++++++++ 5 files changed, 92 insertions(+) create mode 100644 src/Rules/Constants/ClassAsClassConstantRule.php create mode 100644 tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php create mode 100644 tests/PHPStan/Rules/Constants/data/class-as-class-constant.php diff --git a/Makefile b/Makefile index 3282ee2c4e..11807088ca 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/first-class-instantiation-callable.php \ --exclude tests/PHPStan/Rules/Classes/data/instantiation-callable.php \ --exclude tests/PHPStan/Rules/Classes/data/bug-9402.php \ + --exclude tests/PHPStan/Rules/Constants/data/class-as-class-constant.php \ --exclude tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php \ --exclude tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php \ --exclude tests/PHPStan/Rules/Methods/data/bug-10043.php \ diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 1382d99ee1..15f1048b86 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -68,6 +68,7 @@ rules: - PHPStan\Rules\Classes\NonClassAttributeClassRule - PHPStan\Rules\Classes\ReadOnlyClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule + - PHPStan\Rules\Constants\ClassAsClassConstantRule - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - PHPStan\Rules\Constants\FinalConstantRule - PHPStan\Rules\Constants\NativeTypedClassConstantRule diff --git a/src/Rules/Constants/ClassAsClassConstantRule.php b/src/Rules/Constants/ClassAsClassConstantRule.php new file mode 100644 index 0000000000..b10d2d0080 --- /dev/null +++ b/src/Rules/Constants/ClassAsClassConstantRule.php @@ -0,0 +1,40 @@ + + */ +final class ClassAsClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + foreach ($node->consts as $const) { + if ($const->name->toLowerString() !== 'class') { + continue; + } + + $errors[] = RuleErrorBuilder::message('A class constant must not be called \'class\'; it is reserved for class name fetching.') + ->line($const->getStartLine()) + ->identifier('classConstant.class') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php new file mode 100644 index 0000000000..1a8fda4bf6 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php @@ -0,0 +1,33 @@ + + */ +class ClassAsClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ClassAsClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-as-class-constant.php'], [ + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 9, + ], + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php new file mode 100644 index 0000000000..fdf86f77f1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php @@ -0,0 +1,17 @@ + Date: Sun, 1 Dec 2024 02:58:42 +0800 Subject: [PATCH 192/220] Use native PHPDocs for `Rule` and `RuleTestCase` --- src/Rules/Rule.php | 6 +++--- src/Testing/RuleTestCase.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index e145e4b530..f0ebf3be85 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -20,18 +20,18 @@ * Learn more: https://phpstan.org/developing-extensions/rules * * @api - * @phpstan-template TNodeType of Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return (string|RuleError)[] errors */ public function processNode(Node $node, Scope $scope): array; diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index b507b05ae6..b3ff058c11 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -45,7 +45,7 @@ abstract class RuleTestCase extends PHPStanTestCase private ?Analyser $analyser = null; /** - * @phpstan-return TRule + * @return TRule */ abstract protected function getRule(): Rule; From 4d3a9abb165484e18d0d9d787842d1328843b684 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 10 Dec 2024 21:56:01 +0800 Subject: [PATCH 193/220] Use native PHPDocs wherever possible --- src/Collectors/Collector.php | 8 ++++---- src/Collectors/Registry.php | 9 +++------ src/DependencyInjection/Container.php | 7 +++---- src/DependencyInjection/Nette/NetteContainer.php | 7 +++---- src/Rules/DirectRegistry.php | 9 +++------ src/Rules/LazyRegistry.php | 9 +++------ src/Rules/Registry.php | 6 ++---- stubs/bleedingEdge/Rule.stub | 6 +++--- 8 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php index e046f89750..d7c87c8ecc 100644 --- a/src/Collectors/Collector.php +++ b/src/Collectors/Collector.php @@ -20,19 +20,19 @@ * Learn more: https://phpstan.org/developing-extensions/collectors * * @api - * @phpstan-template-covariant TNodeType of Node - * @phpstan-template-covariant TValue + * @template-covariant TNodeType of Node + * @template-covariant TValue */ interface Collector { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return TValue|null Collected data */ public function processNode(Node $node, Scope $scope); diff --git a/src/Collectors/Registry.php b/src/Collectors/Registry.php index 11586e3097..cc0ae09a97 100644 --- a/src/Collectors/Registry.php +++ b/src/Collectors/Registry.php @@ -27,10 +27,8 @@ public function __construct(array $collectors) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Collector[] + * @param class-string $nodeType + * @return array> */ public function getCollectors(string $nodeType): array { @@ -48,8 +46,7 @@ public function getCollectors(string $nodeType): array } /** - * @phpstan-var array> $selectedCollectors - * @var Collector[] $selectedCollectors + * @var array> $selectedCollectors */ $selectedCollectors = $this->cache[$nodeType]; diff --git a/src/DependencyInjection/Container.php b/src/DependencyInjection/Container.php index 07a7a574e1..cd785677b1 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -14,10 +14,9 @@ public function hasService(string $serviceName): bool; public function getService(string $serviceName); /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className); diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 7546993297..914d0a43f1 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -32,10 +32,9 @@ public function getService(string $serviceName) } /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { diff --git a/src/Rules/DirectRegistry.php b/src/Rules/DirectRegistry.php index 0dfb5d71ec..13c8bfe3a2 100644 --- a/src/Rules/DirectRegistry.php +++ b/src/Rules/DirectRegistry.php @@ -30,10 +30,8 @@ public function __construct(array $rules) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -51,8 +49,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/LazyRegistry.php b/src/Rules/LazyRegistry.php index f1b9181923..ec5b1dc13b 100644 --- a/src/Rules/LazyRegistry.php +++ b/src/Rules/LazyRegistry.php @@ -24,10 +24,8 @@ public function __construct(private Container $container) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -46,8 +44,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/Registry.php b/src/Rules/Registry.php index 36c609811b..792f4428f0 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -9,10 +9,8 @@ interface Registry /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array; diff --git a/stubs/bleedingEdge/Rule.stub b/stubs/bleedingEdge/Rule.stub index 0a86ea9d2c..0e721b0497 100644 --- a/stubs/bleedingEdge/Rule.stub +++ b/stubs/bleedingEdge/Rule.stub @@ -7,18 +7,18 @@ use PHPStan\Analyser\Scope; /** * @api - * @phpstan-template TNodeType of Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return list */ public function processNode(Node $node, Scope $scope): array; From e9e419c12a1f0ec973ce05e1bfec08d4f7a2b957 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 09:45:38 +0100 Subject: [PATCH 194/220] RegexArrayShapeMatcher: fix regex wildcard omitted from type --- src/Type/Regex/RegexGroupParser.php | 16 +++++++++++----- tests/PHPStan/Analyser/nsrt/bug-12211.php | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12211.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index dba7fcfe4e..946e492aa7 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -535,13 +535,19 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap if ( $appendLiterals && $onlyLiterals !== null - && (!in_array($value, ['.'], true) || $isEscaped || $inCharacterClass) ) { - if ($onlyLiterals === []) { - $onlyLiterals = [$value]; + if ( + in_array($value, ['.'], true) + && !($isEscaped || $inCharacterClass) + ) { + $onlyLiterals = null; } else { - foreach ($onlyLiterals as &$literal) { - $literal .= $value; + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php new file mode 100644 index 0000000000..72a268c506 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -0,0 +1,16 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12211; + +use function PHPStan\Testing\assertType; + +const REGEX = '((m.x))'; + +function foo(string $text): void { + assert(preg_match(REGEX, $text, $match) === 1); + assertType('array{string, non-falsy-string}', $match); +} + + From f390b54abf7a2c423ddfe292542e822072d8039e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 10:47:06 +0100 Subject: [PATCH 195/220] RegexArrayShapeMatcher: fix regex alternatives in capture group are concatenated --- src/Type/Regex/RegexGroupParser.php | 25 ++++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-12210.php | 27 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12210.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 946e492aa7..75db1668c7 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -446,7 +446,30 @@ private function walkGroupAst( } if ($ast->getId() === '#alternation') { - $inAlternation = true; + $newLiterals = []; + foreach ($children as $child) { + $walkResult = $this->walkGroupAst( + $child, + true, + $inClass, + $patternModifiers, + $walkResult->onlyLiterals([]), + ); + + if ($newLiterals === null) { + continue; + } + + if (count($walkResult->getOnlyLiterals() ?? []) > 0) { + foreach ($walkResult->getOnlyLiterals() as $alternationLiterals) { + $newLiterals[] = $alternationLiterals; + } + } else { + $newLiterals = null; + } + } + + return $walkResult->onlyLiterals($newLiterals); } // [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php new file mode 100644 index 0000000000..165b61b63e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -0,0 +1,27 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12210; + +use function PHPStan\Testing\assertType; + +function bug12210a(string $text): void { + assert(preg_match('(((sum|min|max)))', $text, $match) === 1); + assertType("array{string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); +} + +function bug12210b(string $text): void { + assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210c(string $text): void { + assert(preg_match('(((su.|min|max)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210d(string $text): void { + assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} From 9a6e12cf9d000420842922194384f5b7d6342370 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 11:01:03 +0100 Subject: [PATCH 196/220] Added regression test --- tests/PHPStan/Analyser/nsrt/bug-12173.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12173.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12173.php b/tests/PHPStan/Analyser/nsrt/bug-12173.php new file mode 100644 index 0000000000..e92ce7da4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12173.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug12173; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function parse(string $string): void + { + $regex = '#.*(?(apple|orange)).*#'; + + if (preg_match($regex, $string, $matches) !== 1) { + throw new \Exception('Invalid input'); + } + + assertType("'apple'|'orange'", $matches['fruit']);; + } +} From ecdd6c6d2734c34893e5875f9e77b99f6d72fe63 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 11:32:03 +0100 Subject: [PATCH 197/220] RegexArrayShapeMatcher: Don't narrow 'J' modifier --- src/Type/Regex/RegexGroupParser.php | 12 +++++++- tests/PHPStan/Analyser/nsrt/bug-12126.php | 36 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12126.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 75db1668c7..c818426111 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -34,6 +34,10 @@ final class RegexGroupParser { + private const NOT_SUPPORTED_MODIFIERS = [ + 'J', // rare modifier too complicated to support + ]; + private static ?Parser $parser = null; public function __construct( @@ -67,8 +71,14 @@ public function parseGroups(string $regex): ?array return null; } - $captureOnlyNamed = false; $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; + foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { + if (str_contains($modifiers, $notSupportedModifier)) { + return null; + } + } + + $captureOnlyNamed = false; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12126.php b/tests/PHPStan/Analyser/nsrt/bug-12126.php new file mode 100644 index 0000000000..c494d8d60d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12126.php @@ -0,0 +1,36 @@ += 7.4 + +namespace Bug12126; + +use function PHPStan\Testing\assertType; + + +class HelloWorld +{ + public function sayHello(): void + { + $options = ['footest', 'testfoo']; + $key = array_rand($options, 1); + + $regex = '/foo(?Ptest)|test(?Pfoo)/J'; + if (!preg_match_all($regex, $options[$key], $matches, PREG_SET_ORDER)) { + return; + } + + assertType('list>', $matches); + // could be assertType("list", $matches); + if (!preg_match_all($regex, $options[$key], $matches, PREG_PATTERN_ORDER)) { + return; + } + + assertType('array>', $matches); + // could be assertType("array{0: list, test: list<'foo'|'test'>, 1: list<'test'|''>, 2: list<''|'foo'>}", $matches); + + if (!preg_match($regex, $options[$key], $matches)) { + return; + } + + assertType('array', $matches); + // could be assertType("array{0: list, test: 'foo', 1: '', 2: 'foo'}|array{0: list, test: 'test', 1: 'test', 2: ''}", $matches); + } +} From e98335bd09659c4567b1259f524f6ff7b785c6e7 Mon Sep 17 00:00:00 2001 From: vindic Date: Thu, 12 Dec 2024 10:19:28 +0200 Subject: [PATCH 198/220] Fix apcu_cache_info and apcu_sma_info signatures --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index e2fcdaf7e6..9543cf7cd3 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -209,7 +209,7 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array', 'limited='=>'bool'], +'apcu_cache_info' => ['array|false', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], @@ -220,7 +220,7 @@ 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array', 'limited='=>'bool'], +'apcu_sma_info' => ['array|false', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], From e7e80934023abc94a4f4bb9066ba6d6db26f6cde Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 12 Dec 2024 09:53:14 +0100 Subject: [PATCH 199/220] Make these benevolent --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 9543cf7cd3..b49b97178c 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -209,7 +209,7 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array|false', 'limited='=>'bool'], +'apcu_cache_info' => ['__benevolent|false>', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], @@ -220,7 +220,7 @@ 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array|false', 'limited='=>'bool'], +'apcu_sma_info' => ['__benevolent', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], From 627ad2a75c564ce81793f1a25056d73421a11351 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 12 Dec 2024 12:44:39 +0000 Subject: [PATCH 200/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index c678b5998f..166ac44388 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.6", + "ondrejmirtes/better-reflection": "6.46.0.0", "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 8557d36dae..f619dbd9c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee384a5c11fbc1dd087ab8dc956b8d73", + "content-hash": "9dad77eea28263a8f7daa01a1b3dd47a", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.6", + "version": "6.46.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4" + "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/d942fd0af0214bb1250a55c2560f061b7b0c4bd4", - "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/242b3e1cdb59a81585be5722b6c44ae44c74c671", + "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671", "shasum": "" }, "require": { @@ -2212,7 +2212,7 @@ "doctrine/coding-standard": "^12.0.0", "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", - "phpunit/phpunit": "^11.4.3", + "phpunit/phpunit": "^11.5.1", "rector/rector": "1.2.10" }, "suggest": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.6" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.46.0.0" }, - "time": "2024-11-28T21:05:45+00:00" + "time": "2024-12-12T12:40:29+00:00" }, { "name": "phpstan/php-8-stubs", From 0edf18d140c5b640a63317c3017088322a566180 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Dec 2024 20:06:47 +0100 Subject: [PATCH 201/220] Added `strictRulesInstalled` parameter --- conf/config.neon | 1 + conf/parametersSchema.neon | 1 + 2 files changed, 2 insertions(+) diff --git a/conf/config.neon b/conf/config.neon index 9b382375c3..40e57d400d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -51,6 +51,7 @@ parameters: checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false checkDynamicProperties: false + strictRulesInstalled: false deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index f73011dcad..3dbe4e87ec 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -57,6 +57,7 @@ parametersSchema: checkTooWideReturnTypesInProtectedAndPublicMethods: bool() checkUninitializedProperties: bool() checkDynamicProperties: bool() + strictRulesInstalled: bool() deprecationRulesInstalled: bool() inferPrivatePropertyTypeFromConstructor: bool() From edc13ad57c10c8ba6ad590fa1b8b7be621a7ba95 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 15 Dec 2024 13:54:08 +0100 Subject: [PATCH 202/220] More precise reflection-classes return types --- resources/functionMap.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 611ac517ec..d21c346e0e 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9934,10 +9934,10 @@ 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], 'ReflectionClass::getDefaultProperties' => ['array'], 'ReflectionClass::getDocComment' => ['string|false'], -'ReflectionClass::getEndLine' => ['int|false'], +'ReflectionClass::getEndLine' => ['positive-int|false'], 'ReflectionClass::getExtension' => ['ReflectionExtension|null'], 'ReflectionClass::getExtensionName' => ['string|false'], -'ReflectionClass::getFileName' => ['string|false'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], 'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], @@ -9951,7 +9951,7 @@ 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], 'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], +'ReflectionClass::getStartLine' => ['positive-int|false'], 'ReflectionClass::getStaticProperties' => ['array'], 'ReflectionClass::getStaticPropertyValue' => ['mixed', 'name'=>'string', 'default='=>'mixed'], 'ReflectionClass::getTraitAliases' => ['array'], @@ -10012,10 +10012,10 @@ 'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], 'ReflectionFunction::getClosureThis' => ['bool'], 'ReflectionFunction::getDocComment' => ['string|false'], -'ReflectionFunction::getEndLine' => ['int|false'], +'ReflectionFunction::getEndLine' => ['positive-int|false'], 'ReflectionFunction::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunction::getExtensionName' => ['string|false'], -'ReflectionFunction::getFileName' => ['string|false'], +'ReflectionFunction::getFileName' => ['non-empty-string|false'], 'ReflectionFunction::getName' => ['non-empty-string'], 'ReflectionFunction::getNamespaceName' => ['string'], 'ReflectionFunction::getNumberOfParameters' => ['int'], @@ -10023,7 +10023,7 @@ 'ReflectionFunction::getParameters' => ['list'], 'ReflectionFunction::getReturnType' => ['?ReflectionType'], 'ReflectionFunction::getShortName' => ['string'], -'ReflectionFunction::getStartLine' => ['int|false'], +'ReflectionFunction::getStartLine' => ['positive-int|false'], 'ReflectionFunction::getStaticVariables' => ['array'], 'ReflectionFunction::inNamespace' => ['bool'], 'ReflectionFunction::invoke' => ['mixed', '...args='=>'mixed'], @@ -10041,10 +10041,10 @@ 'ReflectionFunctionAbstract::getClosureScopeClass' => ['ReflectionClass|null'], 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], -'ReflectionFunctionAbstract::getEndLine' => ['int|false'], +'ReflectionFunctionAbstract::getEndLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], -'ReflectionFunctionAbstract::getFileName' => ['string|false'], +'ReflectionFunctionAbstract::getFileName' => ['non-empty-string|false'], 'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], 'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], @@ -10052,7 +10052,7 @@ 'ReflectionFunctionAbstract::getParameters' => ['list'], 'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::getShortName' => ['string'], -'ReflectionFunctionAbstract::getStartLine' => ['int|false'], +'ReflectionFunctionAbstract::getStartLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getStaticVariables' => ['array'], 'ReflectionFunctionAbstract::hasReturnType' => ['bool'], 'ReflectionFunctionAbstract::inNamespace' => ['bool'], From 5d07949d88c8554bad583f8e5584fdb7534d737c Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 15 Dec 2024 13:32:32 +0100 Subject: [PATCH 203/220] Remove incorrect doc leftover from 1.x --- src/Type/Type.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Type/Type.php b/src/Type/Type.php index 8021baae62..0badf2930c 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -64,13 +64,6 @@ public function getConstantArrays(): array; /** @return list */ public function getConstantStrings(): array; - /** - * This is like accepts() but gives reasons - * why the type was not/might not be accepted in some non-intuitive scenarios. - * - * In PHPStan 2.0 this method will be removed and the return type of accepts() - * will change to AcceptsResult. - */ public function accepts(Type $type, bool $strictTypes): AcceptsResult; public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; From de0553c5f03685f235f484433e8d439b3a2b2cb0 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:56:23 +0000 Subject: [PATCH 204/220] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 166ac44388..8670cde3ee 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#bb981ec60b3838e56473a078edf7d0739ca20403", + "jetbrains/phpstorm-stubs": "dev-master#0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index f619dbd9c9..265045a14f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9dad77eea28263a8f7daa01a1b3dd47a", + "content-hash": "3f90069e33e3f9bd4610862a5a5aed2e", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "bb981ec60b3838e56473a078edf7d0739ca20403" + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bb981ec60b3838e56473a078edf7d0739ca20403", - "reference": "bb981ec60b3838e56473a078edf7d0739ca20403", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-27T16:45:26+00:00" + "time": "2024-12-14T08:03:12+00:00" }, { "name": "nette/bootstrap", From 2d6468685d134686d0987b7bd0cf2788bf4ad3b3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 16 Dec 2024 14:02:26 +0100 Subject: [PATCH 205/220] 10% faster FunctionCallParametersCheck --- src/Rules/FunctionCallParametersCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 623b593f5f..69c8467515 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -88,8 +88,7 @@ public function check( $hasNamedArguments = false; $hasUnpackedArgument = false; $errors = []; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); + foreach ($args as $arg) { if ($hasNamedArguments && $arg->unpack) { $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') ->identifier('argument.unpackAfterNamed') @@ -113,6 +112,7 @@ public function check( $argumentName = $arg->name->toString(); } if ($arg->unpack) { + $type = $scope->getType($arg->value); $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { $minKeys = null; @@ -191,7 +191,7 @@ public function check( if (!$hasNamedArguments) { $invokedParametersCount = count($arguments); - foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + foreach ($arguments as [$argumentValue, $argumentValueType, $unpack, $argumentName]) { if ($unpack) { $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); break; From b66058fe93b5187d0b6755b7c7111133c5722e10 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 17 Dec 2024 08:50:44 +0000 Subject: [PATCH 206/220] Add support for internal classes that overload offset access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Mirtes --- src/Type/ObjectType.php | 14 ++- ...nexistentOffsetInArrayDimFetchRuleTest.php | 26 ++++++ ...s-overload-offset-access-invalid-php84.php | 91 +++++++++++++++++++ ...classes-overload-offset-access-invalid.php | 52 +++++++++++ ...l-classes-overload-offset-access-php84.php | 74 +++++++++++++++ ...nternal-classes-overload-offset-access.php | 37 ++++++++ 6 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-php84.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access.php diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 9c633d62da..fab7c056b0 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -67,7 +67,19 @@ class ObjectType implements TypeWithClassName, SubtractableType use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; + private const EXTRA_OFFSET_CLASSES = [ + 'DOMNamedNodeMap', // Only read and existence + 'Dom\NamedNodeMap', // Only read and existence + 'DOMNodeList', // Only read and existence + 'Dom\NodeList', // Only read and existence + 'Dom\HTMLCollection', // Only read and existence + 'Dom\DtdNamedNodeMap', // Only read and existence + 'PDORow', // Only read and existence + 'ResourceBundle', // Only read + 'FFI\CData', // Very funky and weird + 'SimpleXMLElement', + 'Threaded', + ]; private ?Type $subtractedType; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2838f2cbd9..6699adb5f7 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -898,4 +898,30 @@ public function testBug2634(): void $this->analyse([__DIR__ . '/data/bug-2634.php'], []); } + public function testInternalClassesWithOverloadedOffsetAccess(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccess84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-php84.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid-php84.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php new file mode 100644 index 0000000000..f57bd14122 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php @@ -0,0 +1,91 @@ + Date: Tue, 17 Dec 2024 09:34:14 +0100 Subject: [PATCH 207/220] Faster `MutatingScope->getNodeKey()` --- src/Analyser/MutatingScope.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5659a107bc..52ae676397 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -710,15 +710,16 @@ private function getNodeKey(Expr $node): string { $key = $this->exprPrinter->printExpr($node); + $attributes = $node->getAttributes(); if ( $node instanceof Node\FunctionLike - && $node->hasAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) - && $node->hasAttribute('startFilePos') + && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && (($attributes['startFilePos'] ?? null) !== null) ) { - $key .= '/*' . $node->getAttribute('startFilePos') . '*/'; + $key .= '/*' . $attributes['startFilePos'] . '*/'; } - if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; } From 73d0f13cdfb39503044c8b1f404941935809146b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:32:44 +0100 Subject: [PATCH 208/220] Fix `DOMDocument::create*()` return types --- resources/functionMap.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index d21c346e0e..1b8e4c28a4 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -1898,15 +1898,15 @@ 'DOMCharacterData::substringData' => ['string', 'offset'=>'int', 'count'=>'int'], 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], -'DOMDocument::createAttribute' => ['DOMAttr', 'name'=>'string'], -'DOMDocument::createAttributeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], -'DOMDocument::createCDATASection' => ['DOMCDATASection', 'data'=>'string'], +'DOMDocument::createAttribute' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createAttributeNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], +'DOMDocument::createCDATASection' => ['__benevolent', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment'], -'DOMDocument::createElement' => ['DOMElement', 'name'=>'string', 'value='=>'string'], -'DOMDocument::createElementNS' => ['DOMElement', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], -'DOMDocument::createEntityReference' => ['DOMEntityReference', 'name'=>'string'], -'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction', 'target'=>'string', 'data='=>'string'], +'DOMDocument::createElement' => ['__benevolent', 'name'=>'string', 'value='=>'string'], +'DOMDocument::createElementNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], +'DOMDocument::createEntityReference' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createProcessingInstruction' => ['__benevolent', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText', 'content'=>'string'], 'DOMDocument::getElementById' => ['DOMElement|null', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], From c053dbc01983f6dd78a78b8154a00afb64088b33 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:35:44 +0100 Subject: [PATCH 209/220] Support `#` comments in regex with `x` modifier --- src/Type/Regex/RegexGroupParser.php | 20 +++++++++----- tests/PHPStan/Analyser/nsrt/bug-12242.php | 32 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12242.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index c818426111..a98fb20b42 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -23,6 +23,7 @@ use function count; use function in_array; use function is_int; +use function preg_replace; use function rtrim; use function sscanf; use function str_contains; @@ -64,13 +65,6 @@ public function parseGroups(string $regex): ?array return null; } - $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); - try { - $ast = self::$parser->parse($rawRegex); - } catch (Exception) { - return null; - } - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { if (str_contains($modifiers, $notSupportedModifier)) { @@ -78,6 +72,18 @@ public function parseGroups(string $regex): ?array } } + if (str_contains($modifiers, 'x')) { + // in freespacing mode the # character starts a comment and runs until the end of the line + $regex = preg_replace('/[^?]#.*/', '', $regex) ?? ''; + } + + $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); + try { + $ast = self::$parser->parse($rawRegex); + } catch (Exception) { + return null; + } + $captureOnlyNamed = false; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php new file mode 100644 index 0000000000..cb6d424567 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -0,0 +1,32 @@ += 7.4 + +namespace Bug12242; + +use function PHPStan\Testing\assertType; + +function foo(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*) + # ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} + +function bar(string $str): void +{ + $regexp = '/^ + (\w+) # column type [1] + [\(] # ( + ?([\d,]*) # size or size, precision [2] + [\)] # ) + ?\s* # whitespace + (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] + $/x'; + if (preg_match($regexp, $str, $matches)) { + assertType('array{string, non-empty-string, string, string}', $matches); + } +} From 7dc4cc62e885d3895af515129b19dc50e75c1d01 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:52:29 +0100 Subject: [PATCH 210/220] Fix regex comment support eating too much chars --- src/Type/Regex/RegexGroupParser.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12242.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index a98fb20b42..beea95bce0 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -74,7 +74,7 @@ public function parseGroups(string $regex): ?array if (str_contains($modifiers, 'x')) { // in freespacing mode the # character starts a comment and runs until the end of the line - $regex = preg_replace('/[^?]#.*/', '', $regex) ?? ''; + $regex = preg_replace('/(?regexExpressionHelper->removeDelimitersAndModifiers($regex); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php index cb6d424567..4d065367a2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12242.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -30,3 +30,14 @@ function bar(string $str): void assertType('array{string, non-empty-string, string, string}', $matches); } } + +function foobar(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*)# a comment immediately behind with a closing parenthesis ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} From 5dfc5830f3b37b8b23e73514c0d6a43c65c57152 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 19 Dec 2024 16:52:07 +0100 Subject: [PATCH 211/220] Fix typo --- src/Rules/AttributesCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index e04381033e..6976c7d049 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -151,7 +151,7 @@ public function check( 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', - 'Attribute class ' . $attributeClassName . ' constructorinvoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'attribute', $attributeConstructor->acceptsNamedArguments(), From 202dd81d113dd8262977fc32c895386e38bda166 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Dec 2024 10:00:02 +0100 Subject: [PATCH 212/220] Readonly classes cannot be combined with `#[AllowDynamicProperties]` + check trait attributes --- Makefile | 2 + conf/config.level0.neon | 1 + src/Rules/Classes/ClassAttributesRule.php | 32 ++++++- src/Rules/Traits/TraitAttributesRule.php | 51 ++++++++++ .../Rules/Classes/ClassAttributesRuleTest.php | 24 +++++ .../PHPStan/Rules/Classes/data/bug-12281.php | 16 ++++ .../Rules/Traits/TraitAttributesRuleTest.php | 96 +++++++++++++++++++ tests/PHPStan/Rules/Traits/data/bug-12011.php | 26 +++++ tests/PHPStan/Rules/Traits/data/bug-12281.php | 19 ++++ .../Rules/Traits/data/trait-attributes.php | 30 ++++++ 10 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/Rules/Traits/TraitAttributesRule.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-12281.php create mode 100644 tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php create mode 100644 tests/PHPStan/Rules/Traits/data/bug-12011.php create mode 100644 tests/PHPStan/Rules/Traits/data/bug-12281.php create mode 100644 tests/PHPStan/Rules/Traits/data/trait-attributes.php diff --git a/Makefile b/Makefile index ad6075e3bf..d8566bfdb0 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,8 @@ lint: --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \ --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \ --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \ + --exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \ src tests cs: diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 3964727b8d..c84cf8f5f7 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -103,6 +103,7 @@ rules: - PHPStan\Rules\Regexp\RegularExpressionPatternRule - PHPStan\Rules\Traits\ConflictingTraitConstantsRule - PHPStan\Rules\Traits\ConstantsInTraitsRule + - PHPStan\Rules\Traits\TraitAttributesRule - PHPStan\Rules\Types\InvalidTypesInUnionRule - PHPStan\Rules\Variables\UnsetRule - PHPStan\Rules\Whitespace\FileWhitespaceRule diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index afe9786db0..190be4331b 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -8,6 +8,9 @@ use PHPStan\Node\InClassNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule @@ -28,12 +31,39 @@ public function processNode(Node $node, Scope $scope): array { $classLikeNode = $node->getOriginalNode(); - return $this->attributesCheck->check( + $errors = $this->attributesCheck->check( $scope, $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', ); + + $classReflection = $node->getClassReflection(); + if ( + $classReflection->isReadOnly() + || $classReflection->isEnum() + || $classReflection->isInterface() + ) { + $typeName = 'readonly class'; + $identifier = 'class.allowDynamicPropertiesReadonly'; + if ($classReflection->isEnum()) { + $typeName = 'enum'; + $identifier = 'enum.allowDynamicProperties'; + } + if ($classReflection->isInterface()) { + $typeName = 'interface'; + $identifier = 'interface.allowDynamicProperties'; + } + + if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName)) + ->identifier($identifier) + ->nonIgnorable() + ->build(); + } + } + + return $errors; } } diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php new file mode 100644 index 0000000000..2b9fa768d0 --- /dev/null +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -0,0 +1,51 @@ + + */ +final class TraitAttributesRule implements Rule +{ + + public function __construct( + private AttributesCheck $attributesCheck, + ) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + $errors = $this->attributesCheck->check( + $scope, + $originalNode->attrGroups, + Attribute::TARGET_CLASS, + 'class', + ); + + if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.') + ->identifier('trait.allowDynamicProperties') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 1d72b629a8..18e7c7a1fd 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -167,4 +167,28 @@ public function testBug12011(): void ]); } + public function testBug12281(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with readonly class.', + 05, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with enum.', + 12, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with interface.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-12281.php b/tests/PHPStan/Rules/Classes/data/bug-12281.php new file mode 100644 index 0000000000..293d9e5e41 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12281.php @@ -0,0 +1,16 @@ += 8.2 + +namespace Bug12281; + +#[\AllowDynamicProperties] +readonly class BlogData { /* … */ } + +/** @readonly */ +#[\AllowDynamicProperties] +class BlogDataPhpdoc { /* … */ } + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php new file mode 100644 index 0000000000..a3be5ead04 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -0,0 +1,96 @@ + + */ +class TraitAttributesRuleTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new TraitAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), + new NullsafeCheck(), + new PhpVersion(80000), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/trait-attributes.php'], [ + [ + 'Attribute class TraitAttributes\AbstractAttribute is abstract.', + 8, + ], + [ + 'Attribute class TraitAttributes\MyTargettedAttribute does not have the class target.', + 20, + ], + ]); + } + + public function testBug12011(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12011.php'], [ + [ + 'Parameter #1 $name of attribute class Bug12011Trait\Table constructor expects string|null, int given.', + 8, + ], + ]); + } + + public function testBug12281(): void + { + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with trait.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12011.php b/tests/PHPStan/Rules/Traits/data/bug-12011.php new file mode 100644 index 0000000000..32b09d38d3 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12011.php @@ -0,0 +1,26 @@ += 8.3 + +namespace Bug12011Trait; + +use Attribute; + + +#[Table(self::TABLE_NAME)] +trait MyTrait +{ + private const int TABLE_NAME = 1; +} + +class X { + use MyTrait; +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Table +{ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + ) { + } +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12281.php b/tests/PHPStan/Rules/Traits/data/bug-12281.php new file mode 100644 index 0000000000..da7d088f1a --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12281.php @@ -0,0 +1,19 @@ += 8.2 + +namespace Bug12281Traits; + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +trait BlogDataTrait { /* … */ } + +class Uses +{ + + use BlogDataTrait; + +} diff --git a/tests/PHPStan/Rules/Traits/data/trait-attributes.php b/tests/PHPStan/Rules/Traits/data/trait-attributes.php new file mode 100644 index 0000000000..67906a2dfe --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/trait-attributes.php @@ -0,0 +1,30 @@ + Date: Fri, 20 Dec 2024 10:04:20 +0100 Subject: [PATCH 213/220] Named argument detection is scope-PHP version dependent --- src/Analyser/MutatingScope.php | 6 ++-- src/Php/PhpVersions.php | 5 +++ src/Rules/FunctionCallParametersCheck.php | 4 +-- .../Analyser/Bug9307CallMethodsRuleTest.php | 4 +-- .../nsrt/bug-2600-php-version-scope.php | 26 ++++++++++++++ .../Rules/Classes/ClassAttributesRuleTest.php | 2 -- .../ClassConstantAttributesRuleTest.php | 2 -- .../ForbiddenNameCheckExtensionRuleTest.php | 3 +- .../Rules/Classes/InstantiationRuleTest.php | 11 ++++-- .../EnumCases/EnumCaseAttributesRuleTest.php | 2 -- .../ArrowFunctionAttributesRuleTest.php | 2 -- .../Rules/Functions/CallCallablesRuleTest.php | 6 ++-- .../CallToFunctionParametersRuleTest.php | 13 +++---- .../Rules/Functions/CallUserFuncRuleTest.php | 3 +- .../Functions/ClosureAttributesRuleTest.php | 2 -- .../Functions/FunctionAttributesRuleTest.php | 2 -- .../Functions/ParamAttributesRuleTest.php | 2 -- .../Rules/Methods/CallMethodsRuleTest.php | 30 ++++++++++++---- .../Methods/CallStaticMethodsRuleTest.php | 6 ++-- .../Methods/MethodAttributesRuleTest.php | 7 ---- ...llow-named-arguments-php-version-scope.php | 35 +++++++++++++++++++ .../Properties/PropertyAttributesRuleTest.php | 2 -- 22 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php create mode 100644 tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index aa143cc3af..832c73b1c0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3139,7 +3139,7 @@ private function enterFunctionLike( $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()->yes()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType()); @@ -3154,7 +3154,7 @@ private function enterFunctionLike( $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()->yes()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType()); @@ -3629,7 +3629,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type ); } if ($isVariadic) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( $type, false, diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 7bdc70e3bf..74474b28b0 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -28,4 +28,9 @@ public function producesWarningForFinalPrivateMethods(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsNamedArguments(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index c39da64ce1..50371ab23c 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -42,7 +41,6 @@ final class FunctionCallParametersCheck public function __construct( private RuleLevelHelper $ruleLevelHelper, private NullsafeCheck $nullsafeCheck, - private PhpVersion $phpVersion, private UnresolvableTypeHelper $unresolvableTypeHelper, private PropertyReflectionFinder $propertyReflectionFinder, private bool $checkArgumentTypes, @@ -201,7 +199,7 @@ public function check( ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { + if ($hasNamedArguments && !$scope->getPhpVersion()->supportsNamedArguments()->yes() && !(bool) $funcCall->getAttribute('isAttribute', false)) { $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') ->identifier('argument.namedNotSupported') ->line($funcCall->getStartLine()) diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index a49a650286..ff1be83109 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; @@ -12,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,7 +24,7 @@ protected function getRule(): Rule $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php new file mode 100644 index 0000000000..bf13358857 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php @@ -0,0 +1,26 @@ + 7.4 + +namespace Bug2600PhpVersionScope; + +use function PHPStan\Testing\assertType; + +if (PHP_VERSION_ID >= 80000) { + class Foo8 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + } +} else { + class Foo9 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 18e7c7a1fd..d4a9dc96d5 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -35,7 +34,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index d5787f8344..64b9783877 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index 4907d1d7ec..5286e079fc 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +25,7 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 43b9091daf..e4155ce55b 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +25,7 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), @@ -290,6 +289,10 @@ public function testBug4056(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/instantiation-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to InstantiationNamedArguments\Foo constructor.', @@ -501,6 +504,10 @@ public function testBug10248(): void public function testBug11815(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/bug-11815.php'], []); } diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 52428ced2b..d61265e3e7 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\EnumCases; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80100), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index af9266b7e5..1be1cfae69 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 31a92a4f92..a8ffcaf800 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -27,7 +26,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -158,6 +156,10 @@ public function testRule(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to closure.', diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 89017fe2cf..a6513f03cb 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -28,7 +27,7 @@ protected function getRule(): Rule $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -477,6 +476,10 @@ public function testGenericFunction(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $errors = [ [ 'Missing parameter $j (int) in call to function FunctionNamedArguments\foo.', @@ -491,12 +494,6 @@ public function testNamedArguments(): void 14, ], ]; - if (PHP_VERSION_ID < 80000) { - $errors[] = [ - 'Missing parameter $arr1 (array) in call to function array_merge.', - 14, - ]; - } require_once __DIR__ . '/data/named-arguments-define.php'; $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index f4eb4c2c6e..ccff7cb19d 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,7 +20,7 @@ class CallUserFuncRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); + return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index dbab699a2b..41cc608457 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 6d028b1b96..1961e8e81c 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 842d0513a1..678738ac50 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index bd20b469b8..0da6911a43 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -28,15 +27,13 @@ class CallMethodsRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; - private int $phpVersion = PHP_VERSION_ID; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -1822,12 +1819,29 @@ public function testDisallowNamedArguments(): void ]); } + public function testDisallowNamedArgumentsInPhpVersionScope(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/disallow-named-arguments-php-version-scope.php'], [ + [ + 'Named arguments are supported only on PHP 8.0 and later.', + 26, + ], + ]); + } + public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/named-arguments.php'], [ [ @@ -2104,7 +2118,11 @@ public function testBug4800(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; + + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/bug-4800.php'], [ [ 'Missing parameter $bar (string) in call to method Bug4800\HelloWorld2::a().', diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 80d61e299e..51210125cf 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -47,7 +46,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -414,6 +412,10 @@ public function testNamedArguments(): void { $this->checkThisOnly = false; + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/static-method-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to static method StaticMethodNamedArguments\Foo::doFoo().', diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index ef5ca25aef..c87aabde90 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -21,8 +20,6 @@ class MethodAttributesRuleTest extends RuleTestCase { - private int $phpVersion; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -32,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -51,8 +47,6 @@ protected function getRule(): Rule public function testRule(): void { - $this->phpVersion = 80000; - $this->analyse([__DIR__ . '/data/method-attributes.php'], [ [ 'Attribute class MethodAttributes\Foo does not have the method target.', @@ -63,7 +57,6 @@ public function testRule(): void public function testBug5898(): void { - $this->phpVersion = 70400; $this->analyse([__DIR__ . '/data/bug-5898.php'], []); } diff --git a/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php new file mode 100644 index 0000000000..174fef244d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php @@ -0,0 +1,35 @@ += 80000) { + class Foo + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} else { + class FooBar + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 02b75d1007..6f4a6121ec 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -29,7 +28,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, From defd40604f38b0f62b25cfeee99c21d90dc960ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Dec 2024 20:31:20 +0100 Subject: [PATCH 214/220] Fix preg_match() group containing start/end meta characters --- src/Type/Regex/RegexGroupParser.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12297.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12297.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index beea95bce0..a8a9755e8a 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -527,7 +527,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool if ($literal !== '' && $literal !== '0') { $isNonFalsy = true; } - return false; + return $literal === ''; } foreach ($node->getChildren() as $child) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12297.php b/tests/PHPStan/Analyser/nsrt/bug-12297.php new file mode 100644 index 0000000000..4a956e42a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12297.php @@ -0,0 +1,19 @@ + Date: Fri, 20 Dec 2024 10:12:43 +0100 Subject: [PATCH 215/220] Fix test --- tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index a3be5ead04..fb35b438a0 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Traits; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -35,7 +34,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -54,6 +52,10 @@ protected function getRule(): Rule public function testRule(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/trait-attributes.php'], [ [ 'Attribute class TraitAttributes\AbstractAttribute is abstract.', @@ -85,6 +87,10 @@ public function testBug12011(): void public function testBug12281(): void { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ [ 'Attribute class AllowDynamicProperties cannot be used with trait.', From 90e48fa876696f221874a2766c2bf3fc1bea0ec0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 20 Dec 2024 10:16:44 +0100 Subject: [PATCH 216/220] Attributes rules use `In*Node` virtual nodes for more precise Scope --- src/Rules/Functions/ArrowFunctionAttributesRule.php | 7 ++++--- src/Rules/Functions/ClosureAttributesRule.php | 7 ++++--- src/Rules/Functions/FunctionAttributesRule.php | 7 ++++--- src/Rules/Methods/MethodAttributesRule.php | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index b849f968aa..67af9eb5e0 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class ArrowFunctionAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\ArrowFunction::class; + return InArrowFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 841ae2f46c..fc206e19d4 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClosureNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class ClosureAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\Closure::class; + return InClosureNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 153c222091..9c5ad24d73 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class FunctionAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\Function_::class; + return InFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index fefc7bac89..433931baf3 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class MethodAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', ); From f6c556f625078b39caff3d67fafd6ae49957333e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Dec 2024 10:24:48 +0100 Subject: [PATCH 217/220] Scope: use `scope->getConstant` instead --- src/Analyser/MutatingScope.php | 40 +++++++++++------ src/Php/PhpVersions.php | 5 +++ .../PHPStan/Analyser/ScopePhpVersionTest.php | 43 +++++++++++++++++++ .../Analyser/data/scope-constants-global.php | 9 ++++ .../data/scope-constants-namespace.php | 9 ++++ 5 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/ScopePhpVersionTest.php create mode 100644 tests/PHPStan/Analyser/data/scope-constants-global.php create mode 100644 tests/PHPStan/Analyser/data/scope-constants-namespace.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 832c73b1c0..f883c95578 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -610,12 +610,7 @@ public function hasConstant(Name $name): bool return $this->fileHasCompilerHaltStatementCalls(); } - if (!$name->isFullyQualified() && $this->getNamespace() !== null) { - if ($this->hasExpressionType(new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])))->yes()) { - return true; - } - } - if ($this->hasExpressionType(new ConstFetch(new FullyQualified($name->toString())))->yes()) { + if ($this->getGlobalConstantType($name) !== null) { return true; } @@ -5686,6 +5681,25 @@ private function getConstantTypes(): array return $constantTypes; } + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); + } + + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); + + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); + } + } + + return null; + } + /** * @return array */ @@ -5728,15 +5742,15 @@ public function getIterableValueType(Type $iteratee): Type public function getPhpVersion(): PhpVersions { - $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); - if (!$this->hasExpressionType($versionExpr)->yes()) { - if (is_array($this->configPhpVersion)) { - return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); - } - return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); + if ($constType !== null) { + return new PhpVersions($constType); } - return new PhpVersions($this->getType($versionExpr)); + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 74474b28b0..229dccb72d 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -18,6 +18,11 @@ public function __construct( { } + public function getType(): Type + { + return $this->phpVersions; + } + public function supportsNoncapturingCatches(): TrinaryLogic { return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; diff --git a/tests/PHPStan/Analyser/ScopePhpVersionTest.php b/tests/PHPStan/Analyser/ScopePhpVersionTest.php new file mode 100644 index 0000000000..fac3b8a066 --- /dev/null +++ b/tests/PHPStan/Analyser/ScopePhpVersionTest.php @@ -0,0 +1,43 @@ +', + __DIR__ . '/data/scope-constants-global.php', + ], + [ + 'int<80000, 80499>', + __DIR__ . '/data/scope-constants-namespace.php', + ], + ]; + } + + /** + * @dataProvider dataTestPhpVersion + */ + public function testPhpVersion(string $expected, string $file): void + { + self::processFile($file, function (Node $node, Scope $scope) use ($expected): void { + if (!($node instanceof Exit_)) { + return; + } + $this->assertSame( + $expected, + $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise()), + ); + }); + } + +} diff --git a/tests/PHPStan/Analyser/data/scope-constants-global.php b/tests/PHPStan/Analyser/data/scope-constants-global.php new file mode 100644 index 0000000000..56eba41c9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-constants-global.php @@ -0,0 +1,9 @@ + Date: Fri, 20 Dec 2024 14:52:28 +0100 Subject: [PATCH 218/220] Bleeding edge - UnusedFunctionParametersCheck: report precise line --- conf/bleedingEdge.neon | 1 + conf/config.neon | 3 ++ conf/parametersSchema.neon | 1 + .../UnusedConstructorParametersRule.php | 7 ++-- src/Rules/Functions/UnusedClosureUsesRule.php | 9 +---- src/Rules/UnusedFunctionParametersCheck.php | 35 +++++++++++++------ .../UnusedConstructorParametersRuleTest.php | 20 ++++++++++- .../Functions/UnusedClosureUsesRuleTest.php | 6 ++-- 8 files changed, 55 insertions(+), 27 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 7227e369b3..76dd0d8904 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -4,3 +4,4 @@ parameters: checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] stricterFunctionMap: true + reportPreciseLineForUnusedFunctionParameter: true diff --git a/conf/config.neon b/conf/config.neon index 40e57d400d..d77e889531 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -25,6 +25,7 @@ parameters: checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: [] stricterFunctionMap: false + reportPreciseLineForUnusedFunctionParameter: false fileExtensions: - php checkAdvancedIsset: false @@ -1043,6 +1044,8 @@ services: - class: PHPStan\Rules\UnusedFunctionParametersCheck + arguments: + reportExactLine: %featureToggles.reportPreciseLineForUnusedFunctionParameter% - class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 3dbe4e87ec..f7328f7863 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -31,6 +31,7 @@ parametersSchema: checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() + reportPreciseLineForUnusedFunctionParameter: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index e42a60d5f7..8b38392470 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -15,7 +15,6 @@ use function array_map; use function array_values; use function count; -use function is_string; use function sprintf; use function strtolower; @@ -56,11 +55,11 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Param $parameter): string { - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { throw new ShouldNotHappenException(); } - return $parameter->var->name; + return $parameter->var; }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), $originalNode->stmts, $message, diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index c019d69240..ed9639e41f 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -6,10 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; -use PHPStan\ShouldNotHappenException; use function array_map; use function count; -use function is_string; /** * @implements Rule @@ -34,12 +32,7 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Node\ClosureUse $use): string { - if (!is_string($use->var->name)) { - throw new ShouldNotHappenException(); - } - return $use->var->name; - }, $node->uses), + array_map(static fn (Node\ClosureUse $use): Node\Expr\Variable => $use->var, $node->uses), $node->stmts, 'Anonymous function has an unused use $%s.', 'closure.unusedUse', diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 4fbe76d20d..628041a032 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,11 +3,13 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use function array_fill_keys; -use function array_keys; +use function array_combine; +use function array_map; use function array_merge; use function is_array; use function is_string; @@ -16,25 +18,34 @@ final class UnusedFunctionParametersCheck { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $reportExactLine, + ) { } /** - * @param string[] $parameterNames + * @param Variable[] $parameterVars * @param Node[] $statements * @param 'constructor.unusedParameter'|'closure.unusedUse' $identifier * @return list */ public function getUnusedParameters( Scope $scope, - array $parameterNames, + array $parameterVars, array $statements, string $unusedParameterMessage, string $identifier, ): array { - $unusedParameters = array_fill_keys($parameterNames, true); + $parameterNames = array_map(static function (Variable $variable): string { + if (!is_string($variable->name)) { + throw new ShouldNotHappenException(); + } + return $variable->name; + }, $parameterVars); + $unusedParameters = array_combine($parameterNames, $parameterVars); foreach ($this->getUsedVariables($scope, $statements) as $variableName) { if (!isset($unusedParameters[$variableName])) { continue; @@ -43,10 +54,12 @@ public function getUnusedParameters( unset($unusedParameters[$variableName]); } $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name), - )->identifier($identifier)->build(); + foreach ($unusedParameters as $name => $variable) { + $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $name))->identifier($identifier); + if ($this->reportExactLine) { + $errorBuilder->line($variable->getStartLine()); + } + $errors[] = $errorBuilder->build(); } return $errors; @@ -66,7 +79,7 @@ private function getUsedVariables(Scope $scope, $node): array return $scope->getDefinedVariables(); } } - if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') { + if ($node instanceof Variable && is_string($node->name) && $node->name !== 'this') { return [$node->name]; } if ($node instanceof Node\ClosureUse && is_string($node->var->name)) { diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index b6530920df..beb402c267 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -12,15 +12,19 @@ class UnusedConstructorParametersRuleTest extends RuleTestCase { + private bool $reportExactLine = true; + protected function getRule(): Rule { return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck( $this->createReflectionProvider(), + $this->reportExactLine, )); } - public function testUnusedConstructorParameters(): void + public function testUnusedConstructorParametersNoExactLine(): void { + $this->reportExactLine = false; $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ [ 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', @@ -33,6 +37,20 @@ public function testUnusedConstructorParameters(): void ]); } + public function testUnusedConstructorParameters(): void + { + $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', + 19, + ], + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $anotherUnusedParameter.', + 20, + ], + ]); + } + public function testPromotedProperties(): void { $this->analyse([__DIR__ . '/data/unused-constructor-parameters-promoted-properties.php'], []); diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 0d033268d8..38a555afda 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -14,7 +14,7 @@ class UnusedClosureUsesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider())); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider(), true)); } public function testUnusedClosureUses(): void @@ -22,11 +22,11 @@ public function testUnusedClosureUses(): void $this->analyse([__DIR__ . '/data/unused-closure-uses.php'], [ [ 'Anonymous function has an unused use $unused.', - 3, + 6, ], [ 'Anonymous function has an unused use $anotherUnused.', - 3, + 7, ], [ 'Anonymous function has an unused use $usedInClosureUse.', From 72b31c081c52682736318d62a09b60f740d9ce41 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Fri, 20 Dec 2024 19:31:57 +0000 Subject: [PATCH 219/220] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 8670cde3ee..30dfc158c3 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.46.0.0", + "ondrejmirtes/better-reflection": "6.49.0.0", "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 265045a14f..731974732a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3f90069e33e3f9bd4610862a5a5aed2e", + "content-hash": "7887860dff8af8b2ff60352d573b1aba", "packages": [ { "name": "clue/ndjson-react", @@ -2187,21 +2187,21 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.46.0.0", + "version": "6.49.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671" + "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/242b3e1cdb59a81585be5722b6c44ae44c74c671", - "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", + "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", + "jetbrains/phpstorm-stubs": "dev-master#b61d4a5f40c3940be440d85355fef4e2416b8527", "nikic/php-parser": "^5.3.1", "php": "^7.4 || ^8.0" }, @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.46.0.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.49.0.0" }, - "time": "2024-12-12T12:40:29+00:00" + "time": "2024-12-20T19:27:15+00:00" }, { "name": "phpstan/php-8-stubs", From 46b98196c0643eeb3b0eb3c5e91e10099e00f5c1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 21 Dec 2024 15:55:41 +0100 Subject: [PATCH 220/220] Prepare for 2.1.x-dev --- .github/workflows/apiref.yml | 4 ++-- .github/workflows/issue-bot.yml | 4 ++-- .github/workflows/pr-base-on-previous-branch.yml | 4 ++-- .github/workflows/update-phpstorm-stubs.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 7700ceb911..24ba44284b 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: branches: - - "2.0.x" + - "2.1.x" paths: - 'src/**' - 'composer.lock' @@ -14,7 +14,7 @@ on: - '.github/workflows/apiref.yml' env: - COMPOSER_ROOT_VERSION: "2.0.x-dev" + COMPOSER_ROOT_VERSION: "2.1.x-dev" concurrency: group: apigen-${{ github.ref }} # will be canceled on subsequent pushes in branch diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index c3d3488fea..3165350af2 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -11,7 +11,7 @@ on: - 'changelog-generator/**' push: branches: - - "2.0.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' @@ -164,7 +164,7 @@ jobs: - name: "Evaluate results - push" working-directory: "issue-bot" - if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.0.x'" + if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.1.x'" env: GITHUB_PAT: ${{ secrets.PHPSTAN_BOT_TOKEN }} PHPSTAN_SRC_COMMIT_BEFORE: ${{ github.event.before }} diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml index 7c2f57188d..34ef71bb83 100644 --- a/.github/workflows/pr-base-on-previous-branch.yml +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -7,7 +7,7 @@ on: types: - opened branches: - - '2.1.x' + - '2.2.x' jobs: @@ -19,6 +19,6 @@ jobs: - name: Comment PR uses: peter-evans/create-or-update-comment@v4 with: - body: "You've opened the pull request against the latest branch 2.1.x. PHPStan 2.1 is not going to be released for months. If your code is relevant on 2.0.x and you want it to be released sooner, please rebase your pull request and change its target to 2.0.x." + body: "You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index 320b4eba1b..396be1c0be 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -16,7 +16,7 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 with: - ref: 2.0.x + ref: 2.1.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP"