From 6735af80fe4d1d5fc06abfb04d2f20f0f6c8fc33 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 28 Nov 2023 15:38:53 +0100 Subject: [PATCH 01/16] Fix !isset() with ArrayDimFetch commit 389b651f9b241428846bcd7a5ed44efda005ae83 Author: Markus Staab Date: Tue Nov 28 15:27:18 2023 +0100 Discard changes to src/Analyser/MutatingScope.php commit 0860ca3366f1db5e6824fafd43c16f797241cdbe Author: Markus Staab Date: Tue Nov 28 15:24:47 2023 +0100 Discard changes to src/Node/Printer/Printer.php commit f22efeff198ccc99d34a59ab42524bd8d0b95a46 Author: Markus Staab Date: Tue Nov 28 15:24:20 2023 +0100 Discard changes to src/Node/IssetExpr.php commit 2d0dbff0ae89484bacdad4c0ac9453e608d34806 Author: Markus Staab Date: Thu Nov 2 17:07:01 2023 +0100 Discard changes to src/Analyser/TypeSpecifier.php commit 9cfc551d4f9e076bfabfb5fc0cdeed9613ed54f7 Author: Markus Staab Date: Thu Nov 2 17:06:28 2023 +0100 Discard changes to tests/PHPStan/Analyser/TypeSpecifierTest.php commit 2f75d65a6480f4e646878e249c29df85cdc66281 Author: Markus Staab Date: Tue Oct 31 11:44:35 2023 +0100 move certainty into IssetExpr commit 4c4caab66047b0243558b61ead6977f121d5876d Author: Markus Staab Date: Mon Oct 30 21:13:03 2023 +0100 NotIssetExpr -> IssetExpr commit 8c0eaf92e58fae25e95afc89aecb6e563c9cf948 Author: Markus Staab Date: Fri Oct 6 00:03:38 2023 +0200 fix commit 700c744ab004cc158927afd85dba6ec7889d5844 Author: Markus Staab Date: Thu Oct 5 23:58:39 2023 +0200 Added regression test commit 99580ee965071f454d99eee5ff0cea82cb299efb Author: Markus Staab Date: Thu Oct 5 23:50:14 2023 +0200 Added regression test commit 8ad159f316c84f90a5a1ed22d908db0a89fb5710 Author: Markus Staab Date: Thu Oct 5 23:41:10 2023 +0200 Added regression test commit 0665831728f047d7f78cb810cae7388c7eaf23cb Author: Markus Staab Date: Thu Oct 5 22:17:14 2023 +0200 fix commit 270500e2e496b40a43b74b5c9dacb660b16ef478 Author: Markus Staab Date: Thu Oct 5 22:08:00 2023 +0200 cs commit b7609eb553159b9eb31db90049c82abff4833472 Author: Markus Staab Date: Thu Oct 5 22:00:36 2023 +0200 support regular variables commit b0d8ee041b355de1b0690bf60db6beee4ecc4b44 Author: Markus Staab Date: Wed Oct 4 21:36:05 2023 +0200 fix collision commit f3672dd72482033694960d49cfaea09b380239c1 Author: Markus Staab Date: Wed Oct 4 21:31:23 2023 +0200 cs commit 08bb6712e9792dec29e2979d14fe0278fc14afed Author: Markus Staab Date: Wed Oct 4 21:31:02 2023 +0200 fix commit 8c73fb3a5e5d3878fb3af99aff15277f401d6fd6 Author: Markus Staab Date: Wed Oct 4 17:50:24 2023 +0200 fix regression in bug-7224 commit aa6c58e2ceea20990259d62700048c0f303b90b7 Author: Markus Staab Date: Wed Oct 4 17:06:20 2023 +0200 Added regression test commit 6dfc1e3338be88be0730692fe4cf1e3914aaf695 Author: Markus Staab Date: Wed Oct 4 17:02:43 2023 +0200 Added regression test commit 6623061d72e17a054cc55ac7c1788ae5dacb9708 Author: Markus Staab Date: Wed Oct 4 17:00:36 2023 +0200 Added regression test commit 9a0041937e5a1b8b6cb021708d86b363aa933796 Author: Markus Staab Date: Wed Oct 4 16:58:45 2023 +0200 Added regression test commit b1fc1ddd844b2e492aa344ed5460de159e70c294 Author: Markus Staab Date: Wed Oct 4 16:57:18 2023 +0200 Added regression test commit 745e24b7f70d5d4be38d533baacb4d772fcfd7c5 Author: Markus Staab Date: Wed Oct 4 16:54:03 2023 +0200 Added regression test commit 069d259a21c588c0e5db9720c280552d3aee48e3 Author: Markus Staab Date: Wed Oct 4 15:32:51 2023 +0200 cleanup after review commit de1eab93339ad0996fa74f5a2ad59e48897ea472 Author: Markus Staab Date: Wed Oct 4 10:11:26 2023 +0200 fix commit 347e5a81f31f637232445782c20f4143e59440ed Author: Markus Staab Date: Wed Oct 4 09:58:58 2023 +0200 cs commit b5b7d59219bb7e9b1f3e1cbe6b755b9d0679747b Author: Markus Staab Date: Wed Oct 4 09:54:50 2023 +0200 fix tests commit 053b291881637f61aa1daba6f016afcfc9208cc9 Author: Markus Staab Date: Wed Oct 4 09:52:37 2023 +0200 fix commit 7b2b95691cabbe4bcb1a7c8f286449ee8eda2135 Author: Markus Staab Date: Wed Oct 4 09:27:17 2023 +0200 fix commit 4c4b293c2653fbdb4cba907366b4f7f012c06cb0 Author: Markus Staab Date: Wed Oct 4 09:14:17 2023 +0200 more tests commit f26b55605f0265f79c55b5a640a54a1d4a3e2726 Author: Markus Staab Date: Tue Oct 3 22:04:59 2023 +0200 not isset commit e6266103fe81a05aff608da46ad4a23166fa13bc Author: Markus Staab Date: Tue Oct 3 22:02:59 2023 +0200 Revert "drive by cleanup" This reverts commit 73105182565b9539d24a842b82b7416dc101f2f4. commit 6949740ace5d9798a831972ac888bbc60f1647b9 Author: Markus Staab Date: Tue Oct 3 22:02:08 2023 +0200 impl commit 26d67563bc84f449b0f5bca17209b14f6810150f Author: Markus Staab Date: Tue Oct 3 18:55:21 2023 +0200 Create UnsetExpr.php commit ec321e8a1e450857fab0afdeb9f049f525ab488b Author: Markus Staab Date: Tue Oct 3 18:36:42 2023 +0200 more tests commit 800a67800dc81991c645d40095741c7e787d282d Author: Markus Staab Date: Tue Oct 3 13:21:01 2023 +0200 add falsy-isset tests commit 2f8eaa1c6fc6913641c25003aaa8af1067185252 Author: Markus Staab Date: Sat Sep 30 18:59:19 2023 +0200 fix test expectations commit 459fa953508dce683b519397002d26e14fcfab11 Author: Markus Staab Date: Sat Sep 30 15:01:08 2023 +0200 Fix !isset() with ArrayDimFetch commit 9c1c44ef88f4190bfc794fedb672b13e5c3957c7 Author: Markus Staab Date: Sat Sep 30 14:55:24 2023 +0200 Added regression test --- phpstan-baseline.neon | 2 +- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-9908.php | 27 +++ tests/PHPStan/Analyser/data/falsy-isset.php | 179 ++++++++++++++++++ .../Analyser/data/has-offset-type-bug.php | 6 +- tests/PHPStan/Analyser/data/tagged-unions.php | 2 +- ...nexistentOffsetInArrayDimFetchRuleTest.php | 20 ++ tests/PHPStan/Rules/Arrays/data/bug-5128.php | 14 ++ tests/PHPStan/Rules/Arrays/data/bug-5128b.php | 50 +++++ tests/PHPStan/Rules/Arrays/data/bug-8724.php | 25 +++ tests/PHPStan/Rules/Arrays/data/bug-8724b.php | 20 ++ .../Rules/Methods/CallMethodsRuleTest.php | 11 ++ .../PHPStan/Rules/Methods/data/bug-9908b.php | 28 +++ .../Variables/DefinedVariableRuleTest.php | 12 ++ .../PHPStan/Rules/Variables/data/bug-9426.php | 20 ++ 15 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-9908.php create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-5128.php create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-5128b.php create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-8724.php create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-8724b.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9908b.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9426.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b3abea2989..81a0abfe88 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -81,7 +81,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 3 + count: 4 path: src/Analyser/TypeSpecifier.php - diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index f883ef05fb..ece4f4693c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1368,6 +1368,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-coalesce.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-ternary-certainty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9908.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7915.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9714.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9105.php'); diff --git a/tests/PHPStan/Analyser/data/bug-9908.php b/tests/PHPStan/Analyser/data/bug-9908.php new file mode 100644 index 0000000000..ae887a342d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9908.php @@ -0,0 +1,27 @@ + 'string']; + } + + assertType("array{}|array{bar: 'string'}", $a); + if (isset($a['bar'])) { + assertType("array{bar: 'string'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertType('array{}', $a); + } + + assertType('array{}|array{bar: 1}', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index bce229826a..7dfb0f7212 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -6,6 +6,161 @@ use function PHPStan\Testing\assertVariableCertainty; use PHPStan\TrinaryLogic; +class ArrayOffset +{ + public function undefinedVar(): void + { + if (isset($a['bar'])) { + assertType("*ERROR*", $a); + } else { + assertType("*ERROR*", $a); + } + } + + public function definedVar($a): void + { + if (isset($a['bar'])) { + assertType("mixed~null", $a); + } else { + assertType("mixed", $a); + } + } + + /** + * @param array{bar?: null}|array{bar?: 'hello'} $a + */ + public function optionalOffsetNull($a): void + { + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('array{bar?: null}', $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 1}|array{bar?: null}", $a); + } + + /** + * @param array{bar?: 'world'}|array{bar?: 'hello'} $a + */ + public function optionalOffsetNonNull($a): void + { + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}|array{bar: 'world'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('array{}', $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{}|array{bar: 1}", $a); + } + + public function maybeCertainNull(): void + { + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + + assertType("array{bar: 'hello'}|array{bar: null}", $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('array{bar?: null}', $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType("array{bar: 1}|array{bar?: null}", $a); + } + + public function maybeCertainNonNull(): void + { + if (rand() % 2) { + $a = ['bar' => 'world']; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + + assertType("array{bar: 'hello'}|array{bar: 'world'}", $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}|array{bar: 'world'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType("array{bar: 1}", $a); + } + + public function yesCertainNull(): void + { + $a = ['bar' => null]; + if (rand() % 2) { + $a = ['bar' => 'hello']; + } + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType("array{bar: 'hello'}|array{bar: null}", $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: null}", $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 1}|array{bar: null}", $a); + } + + public function yesCertainNonNull(): void + { + $a = ['bar' => 'world']; + if (rand() % 2) { + $a = ['bar' => 'hello']; + } + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType("array{bar: 'hello'}|array{bar: 'world'}", $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'hello'}|array{bar: 'world'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('*NEVER*', $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 1}", $a); + } +} + function doFoo():mixed { return 1; } @@ -72,6 +227,30 @@ function stdclassIsset(?\stdClass $m): void } } +function maybeNonNullableVariable(): void +{ + if (rand(0,1)) { + $a = 'hello'; + } + + if (isset($a)) { + assertType("'hello'", $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType("*ERROR*", $a); + } +} + +function nonNullableVariable(string $a): void +{ + if (isset($a)) { + assertType("string", $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType("*ERROR*", $a); + } +} + function nullableVariable(?string $a): void { if (isset($a)) { diff --git a/tests/PHPStan/Analyser/data/has-offset-type-bug.php b/tests/PHPStan/Analyser/data/has-offset-type-bug.php index 8b470ba316..22f23cdf55 100644 --- a/tests/PHPStan/Analyser/data/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/data/has-offset-type-bug.php @@ -65,12 +65,12 @@ 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("array{max?: bool|float|int|string|null, min?: bool|float|int|string|null}&non-empty-array", $range); } else { - assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + assertType("array{min?: null, max?: null}", $range); } - assertType("array{}|array{min?: bool|float|int|string|null, max?: bool|float|int|string|null}", $range); + assertType("array{max?: bool|float|int|string|null, min?: bool|float|int|string|null}", $range); } } diff --git a/tests/PHPStan/Analyser/data/tagged-unions.php b/tests/PHPStan/Analyser/data/tagged-unions.php index 9926467123..45e7e6e98b 100644 --- a/tests/PHPStan/Analyser/data/tagged-unions.php +++ b/tests/PHPStan/Analyser/data/tagged-unions.php @@ -55,7 +55,7 @@ public function doFoo4(array $foo) if (isset($foo['C'])) { assertType("array{A: string, C: 1}", $foo); } else { - assertType("array{A: int, B: 1}|array{A: string, C: 1}", $foo); // could be array{A: int, B: 1} + assertType("array{A: int, B: 1}", $foo); } assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index e629152005..627b81e44f 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -732,4 +732,24 @@ public function testBug9991(): void ]); } + public function testBug8724(): void + { + $this->analyse([__DIR__ . '/data/bug-8724.php'], []); + } + + public function testBug8724b(): void + { + $this->analyse([__DIR__ . '/data/bug-8724b.php'], []); + } + + public function testBug5128(): void + { + $this->analyse([__DIR__ . '/data/bug-5128.php'], []); + } + + public function testBug5128b(): void + { + $this->analyse([__DIR__ . '/data/bug-5128b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5128.php b/tests/PHPStan/Rules/Arrays/data/bug-5128.php new file mode 100644 index 0000000000..48af9efa91 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5128.php @@ -0,0 +1,14 @@ + + */ + private function convertNumberFilters(array $filter): array + { + if (isset($filter['condition1'], $filter['condition2'])) { + $conditions = [ + $this->convertNumberFilter($filter['condition1']), + $this->convertNumberFilter($filter['condition2']), + ]; + } else { + $conditions = [ + $this->convertNumberFilter($filter), + ]; + } + + return $conditions; + } + + /** + * @param NumberFilter $filter + * @return array + */ + public function convertNumberFilter(array $filter): array + { + return []; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8724.php b/tests/PHPStan/Rules/Arrays/data/bug-8724.php new file mode 100644 index 0000000000..d863791534 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8724.php @@ -0,0 +1,25 @@ +getLabel(); + + $label2 = $data['label'] ?? (isset($data['input']) ? $data['input']->getLabel() : ''); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 35fbe17d18..899a1f46b1 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3076,9 +3076,20 @@ public function testTypedClassConstants(): void $this->checkNullables = true; $this->checkUnionTypes = true; $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/return-type-class-constant.php'], []); } + public function testBug9908b(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9908b.php'], []); + } + public function testNamedParametersForMultiVariantFunctions(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Methods/data/bug-9908b.php b/tests/PHPStan/Rules/Methods/data/bug-9908b.php new file mode 100644 index 0000000000..0fbd23f429 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9908b.php @@ -0,0 +1,28 @@ + 'string']; + } + + if (isset($a['bar'])) { + $a['bar'] = 1; + } + + $this->sayHello($a); + } + + /** + * @param array{bar?: int} $foo + */ + public function sayHello(array $foo): void + { + echo 'Hello' . print_r($foo, true); + } +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index cf056f3983..43ac9ea5b5 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1008,6 +1008,7 @@ public function testIsStringNarrowsCertainty(): void $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/isstring-certainty.php'], [ [ 'Variable $a might not be defined.', @@ -1026,7 +1027,18 @@ public function testDiscussion10252(): void $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); } + + public function testBug9426(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-9426.php'], []); + } } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9426.php b/tests/PHPStan/Rules/Variables/data/bug-9426.php new file mode 100644 index 0000000000..5a00ab31dd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9426.php @@ -0,0 +1,20 @@ + Date: Thu, 30 Nov 2023 14:33:38 +0100 Subject: [PATCH 02/16] impl --- src/Analyser/TypeSpecifier.php | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 057eaf4e3c..09d06c94d1 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -38,6 +38,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ConditionalTypeForParameter; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -665,6 +666,97 @@ public function specifyTypesInCondition( ); } + if ( + $issetExpr instanceof ArrayDimFetch + && $issetExpr->dim !== null + ) { + $type = $scope->getType($issetExpr->var); + if ($type instanceof MixedType) { + return new SpecifiedTypes(); + } + + $dimType = $scope->getType($issetExpr->dim); + if (!($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType)) { + return new SpecifiedTypes(); + } + + $hasOffsetType = $type->hasOffsetValueType($dimType); + if ($hasOffsetType->no()) { + return new SpecifiedTypes(); + } + + $isset = $hasOffsetType->yes(); + $offsetType = $type->getOffsetValueType($dimType); + $isNullable = !$offsetType->isNull()->no(); + + $setOptionalDim = function (Type $outerType, Type $dimType): Type { + return TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof ConstantArrayType) { + $builder = ConstantArrayTypeBuilder::createFromConstantArray( + $type->unsetOffset($dimType), + ); + $builder->setOffsetValueType( + $dimType, + new NullType(), + true, + ); + $buildType = $builder->getArray(); + return $buildType; + } + + return $type; + }); + }; + + $exprType = $this->create( + $issetExpr, + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + ); + if ($isset === true) { + if ($isNullable) { + return $exprType; + } + + // array-key cannot exist in !isset() + return $exprType->unionWith($this->create( + $issetExpr, + new HasOffsetType($dimType), + $context, + false, + $scope, + $rootExpr, + )); + } + + if ($isNullable) { + return $this->create( + $issetExpr->var, + $setOptionalDim($type, $dimType), + $context->negate(), + false, + $scope, + $rootExpr, + ); + } + + return $this->create( + $issetExpr->var, + $type->unsetOffset($dimType), + $context->negate(), + false, + $scope, + $rootExpr, + ); + } + return new SpecifiedTypes(); } From f0673e8f44cff97fa79231a567b3dd5c162cc5e9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 14:49:14 +0100 Subject: [PATCH 03/16] refine --- src/Analyser/TypeSpecifier.php | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 09d06c94d1..6a07fad3e9 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -712,28 +712,33 @@ public function specifyTypesInCondition( }); }; - $exprType = $this->create( - $issetExpr, - new NullType(), - $context->negate(), - false, - $scope, - $rootExpr, - ); if ($isset === true) { if ($isNullable) { - return $exprType; + return $this->create( + $issetExpr->var, + $setOptionalDim($type, $dimType), + $context->negate(), + true, + $scope, + $rootExpr, + )->unionWith($this->create( + new IssetExpr($issetExpr->var), + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + )); } - // array-key cannot exist in !isset() - return $exprType->unionWith($this->create( - $issetExpr, - new HasOffsetType($dimType), + return $this->create( + new IssetExpr($issetExpr->var), + new NullType(), $context, false, $scope, $rootExpr, - )); + ); } if ($isNullable) { From d8b781f3d14fee6597acb2895232423d64bef212 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 14:59:35 +0100 Subject: [PATCH 04/16] more precise results --- tests/PHPStan/Analyser/data/bug-4708.php | 4 ++-- tests/PHPStan/Analyser/data/has-offset-type-bug.php | 4 ++-- tests/PHPStan/Rules/Comparison/data/bug-4708.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/data/bug-4708.php b/tests/PHPStan/Analyser/data/bug-4708.php index d6bae28afc..ea68902056 100644 --- a/tests/PHPStan/Analyser/data/bug-4708.php +++ b/tests/PHPStan/Analyser/data/bug-4708.php @@ -51,7 +51,7 @@ function GetASCConfig() assertType('array', $result); if (!isset($result['bsw'])) { - assertType('array', $result); + assertType("array", $result); $result['bsw'] = 1; assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result); } @@ -66,7 +66,7 @@ function GetASCConfig() if (!isset($result['bew'])) { - assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); $result['bew'] = 5; assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); } diff --git a/tests/PHPStan/Analyser/data/has-offset-type-bug.php b/tests/PHPStan/Analyser/data/has-offset-type-bug.php index 22f23cdf55..c90d43cabe 100644 --- a/tests/PHPStan/Analyser/data/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/data/has-offset-type-bug.php @@ -67,10 +67,10 @@ public function testIsset($range): void if (isset($range['min']) || isset($range['max'])) { assertType("array{max?: bool|float|int|string|null, min?: bool|float|int|string|null}&non-empty-array", $range); } else { - assertType("array{min?: null, max?: null}", $range); + assertType("array{}|array{min?: null, max?: null}", $range); } - assertType("array{max?: bool|float|int|string|null, min?: bool|float|int|string|null}", $range); + assertType("array{}|array{max?: bool|float|int|string|null, min?: bool|float|int|string|null}", $range); } } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4708.php b/tests/PHPStan/Rules/Comparison/data/bug-4708.php index 5c60d3dec1..7c3efd324b 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-4708.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-4708.php @@ -66,7 +66,7 @@ function GetASCConfig() if (!isset($result['bew'])) { - assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); + assertType("non-empty-array&hasOffsetValue('bsw', int)", $result); $result['bew'] = 5; assertType("non-empty-array&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result); } From 3b9643549464aac8a0efc0969b46b549556de527 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 15:03:42 +0100 Subject: [PATCH 05/16] keep maybe certainty --- src/Analyser/TypeSpecifier.php | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6a07fad3e9..dbd30ca151 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -685,7 +685,7 @@ public function specifyTypesInCondition( return new SpecifiedTypes(); } - $isset = $hasOffsetType->yes(); + $hasOffset = $hasOffsetType->yes(); $offsetType = $type->getOffsetValueType($dimType); $isNullable = !$offsetType->isNull()->no(); @@ -712,23 +712,30 @@ public function specifyTypesInCondition( }); }; - if ($isset === true) { + if ($hasOffset === true) { if ($isNullable) { - return $this->create( + $specifiedType = $this->create( $issetExpr->var, $setOptionalDim($type, $dimType), $context->negate(), true, $scope, $rootExpr, - )->unionWith($this->create( - new IssetExpr($issetExpr->var), - new NullType(), - $context->negate(), - false, - $scope, - $rootExpr, - )); + ); + + // keep maybe certainty + if ($scope->hasExpressionType($issetExpr->var)->maybe()) { + return $specifiedType->unionWith($this->create( + new IssetExpr($issetExpr->var), + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + )); + } + + return $specifiedType; } return $this->create( From b661b6810b61f07520a6fca059a73928a1e32ff9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 15:11:44 +0100 Subject: [PATCH 06/16] fix sometime optional --- src/Analyser/TypeSpecifier.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index dbd30ca151..d583090021 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -689,8 +689,8 @@ public function specifyTypesInCondition( $offsetType = $type->getOffsetValueType($dimType); $isNullable = !$offsetType->isNull()->no(); - $setOptionalDim = function (Type $outerType, Type $dimType): Type { - return TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType): Type { + $setOffset = function (Type $outerType, Type $dimType, bool $optional): Type { + return TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType, $optional): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -702,7 +702,7 @@ public function specifyTypesInCondition( $builder->setOffsetValueType( $dimType, new NullType(), - true, + $optional, ); $buildType = $builder->getArray(); return $buildType; @@ -716,14 +716,14 @@ public function specifyTypesInCondition( if ($isNullable) { $specifiedType = $this->create( $issetExpr->var, - $setOptionalDim($type, $dimType), + $setOffset($type, $dimType, !$scope->hasExpressionType($issetExpr->var)->yes()), $context->negate(), true, $scope, $rootExpr, ); - // keep maybe certainty + // keep variable maybe certainty if ($scope->hasExpressionType($issetExpr->var)->maybe()) { return $specifiedType->unionWith($this->create( new IssetExpr($issetExpr->var), @@ -751,7 +751,7 @@ public function specifyTypesInCondition( if ($isNullable) { return $this->create( $issetExpr->var, - $setOptionalDim($type, $dimType), + $setOffset($type, $dimType, true), $context->negate(), false, $scope, From 0a8786e7b6aa05660dcecf385390b24ba42d9e6b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 15:17:04 +0100 Subject: [PATCH 07/16] fix stan warning --- src/Analyser/TypeSpecifier.php | 7 ++++++- tests/PHPStan/Analyser/data/falsy-isset.php | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index d583090021..b6f0d49efd 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -696,8 +696,13 @@ public function specifyTypesInCondition( } if ($type instanceof ConstantArrayType) { + $typeWithoutOffset = $type->unsetOffset($dimType); + if (!$typeWithoutOffset instanceof ConstantArrayType) { + throw new ShouldNotHappenException(); + } + $builder = ConstantArrayTypeBuilder::createFromConstantArray( - $type->unsetOffset($dimType), + $typeWithoutOffset, ); $builder->setOffsetValueType( $dimType, diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index 7dfb0f7212..c12b8ebb80 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -152,8 +152,8 @@ public function yesCertainNonNull(): void $a['bar'] = 1; assertType("array{bar: 1}", $a); } else { - assertVariableCertainty(TrinaryLogic::createYes(), $a); - assertType('*NEVER*', $a); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); } assertVariableCertainty(TrinaryLogic::createYes(), $a); From 94247c71408348abfe3c8c6f0e01df90b7c59bcc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Nov 2023 15:22:49 +0100 Subject: [PATCH 08/16] fix build --- src/Analyser/TypeSpecifier.php | 11 ++++------- tests/PHPStan/Analyser/data/tagged-unions.php | 2 +- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index b6f0d49efd..13f6b4f48f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -689,8 +689,7 @@ public function specifyTypesInCondition( $offsetType = $type->getOffsetValueType($dimType); $isNullable = !$offsetType->isNull()->no(); - $setOffset = function (Type $outerType, Type $dimType, bool $optional): Type { - return TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType, $optional): Type { + $setOffset = static fn (Type $outerType, Type $dimType, bool $optional): Type => TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType, $optional): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -709,17 +708,15 @@ public function specifyTypesInCondition( new NullType(), $optional, ); - $buildType = $builder->getArray(); - return $buildType; + return $builder->getArray(); } return $type; - }); - }; + }); if ($hasOffset === true) { if ($isNullable) { - $specifiedType = $this->create( + $specifiedType = $this->create( $issetExpr->var, $setOffset($type, $dimType, !$scope->hasExpressionType($issetExpr->var)->yes()), $context->negate(), diff --git a/tests/PHPStan/Analyser/data/tagged-unions.php b/tests/PHPStan/Analyser/data/tagged-unions.php index 45e7e6e98b..9926467123 100644 --- a/tests/PHPStan/Analyser/data/tagged-unions.php +++ b/tests/PHPStan/Analyser/data/tagged-unions.php @@ -55,7 +55,7 @@ public function doFoo4(array $foo) if (isset($foo['C'])) { assertType("array{A: string, C: 1}", $foo); } else { - assertType("array{A: int, B: 1}", $foo); + assertType("array{A: int, B: 1}|array{A: string, C: 1}", $foo); // could be array{A: int, B: 1} } assertType('array{A: int, B: 1}|array{A: string, C: 1}', $foo); diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 960dad4d31..30ef691f72 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -65,7 +65,7 @@ public function testRule(): void 67, ], [ - 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null-offset: array{a: true|null}, dim-empty: array{}, dim-null: 1|null} in isset() does not exist.', 73, ], [ @@ -153,7 +153,7 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void 67, ], [ - 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null-offset: array{a: true|null}, dim-empty: array{}, dim-null: 1|null} in isset() does not exist.', 73, ], [ From 9ade6700aa1f3d0f51c0af22afad6175e4d7b550 Mon Sep 17 00:00:00 2001 From: Markus Staab <47448731+clxmstaab@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:56:13 +0100 Subject: [PATCH 09/16] Discard changes to tests/PHPStan/Rules/Arrays/data/bug-8724b.php --- tests/PHPStan/Rules/Arrays/data/bug-8724b.php | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 tests/PHPStan/Rules/Arrays/data/bug-8724b.php diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8724b.php b/tests/PHPStan/Rules/Arrays/data/bug-8724b.php deleted file mode 100644 index d623200f07..0000000000 --- a/tests/PHPStan/Rules/Arrays/data/bug-8724b.php +++ /dev/null @@ -1,20 +0,0 @@ -getLabel(); - - $label2 = $data['label'] ?? (isset($data['input']) ? $data['input']->getLabel() : ''); -} From 4fdc324ed0fd9852e48676755a37f177b9b98716 Mon Sep 17 00:00:00 2001 From: Markus Staab <47448731+clxmstaab@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:56:43 +0100 Subject: [PATCH 10/16] Update NonexistentOffsetInArrayDimFetchRuleTest.php --- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 627b81e44f..1da5de2985 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -737,11 +737,6 @@ public function testBug8724(): void $this->analyse([__DIR__ . '/data/bug-8724.php'], []); } - public function testBug8724b(): void - { - $this->analyse([__DIR__ . '/data/bug-8724b.php'], []); - } - public function testBug5128(): void { $this->analyse([__DIR__ . '/data/bug-5128.php'], []); From 2af27ccc20fd2559634536266546d1e98fa62a22 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Dec 2023 07:30:54 +0100 Subject: [PATCH 11/16] more tests --- tests/PHPStan/Analyser/data/falsy-isset.php | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index c12b8ebb80..7273bdc539 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -159,6 +159,70 @@ public function yesCertainNonNull(): void assertVariableCertainty(TrinaryLogic::createYes(), $a); assertType("array{bar: 1}", $a); } + + public function nestedFetch(): void + { + $a = ['bar' => null]; + if (rand() % 2) { + $a = ['bar' => ['foo' => 'hello']]; + } + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType("array{bar: array{foo: 'hello'}}|array{bar: null}", $a); + if (isset($a['bar']['foo'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: 'hello'}}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: null}", $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: 'hello'}}|array{bar: null}", $a); + } + + public function nestedNullableFetch(?string $nullableString): void + { + $a = ['bar' => null]; + if (rand() % 2) { + $a = ['bar' => ['foo' => $nullableString]]; + } + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType("array{bar: array{foo: string|null}}|array{bar: null}", $a); + if (isset($a['bar']['foo'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: string}}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: null}}|array{bar: null}", $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: null}}|array{bar: array{foo: string}}|array{bar: null}", $a); + } + + public function nestedOptionalNullableFetch(?string $nullableString): void + { + $a = []; + if (rand() % 2) { + $a = ['bar' => ['foo' => $nullableString]]; + } + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType("array{}|array{bar: array{foo: string|null}}", $a); + if (isset($a['bar']['foo'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: array{foo: string}}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{}|array{bar: array{foo: null}}", $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{}|array{bar: array{foo: string|null}}", $a); + } + } function doFoo():mixed { From 93555fd47840f9e81009ac7d82932a5db007c40b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 2 Dec 2023 07:51:57 +0100 Subject: [PATCH 12/16] non-optional offset cannot get optional --- src/Analyser/TypeSpecifier.php | 9 ++++--- test2.php | 24 +++++++++++++++++++ tests/PHPStan/Analyser/data/falsy-isset.php | 4 ++-- .../Variables/DefinedVariableRuleTest.php | 2 +- 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 test2.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 13f6b4f48f..f2c2d69c96 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -689,7 +689,9 @@ public function specifyTypesInCondition( $offsetType = $type->getOffsetValueType($dimType); $isNullable = !$offsetType->isNull()->no(); - $setOffset = static fn (Type $outerType, Type $dimType, bool $optional): Type => TypeTraverser::map($outerType, static function (Type $type, callable $traverse) use ($dimType, $optional): Type { + $setOffset = static fn (Type $outerType, Type $dimType, bool $optional): Type => TypeTraverser::map( + $outerType, + static function (Type $type, callable $traverse) use ($dimType, $optional): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -712,13 +714,14 @@ public function specifyTypesInCondition( } return $type; - }); + }, + ); if ($hasOffset === true) { if ($isNullable) { $specifiedType = $this->create( $issetExpr->var, - $setOffset($type, $dimType, !$scope->hasExpressionType($issetExpr->var)->yes()), + $setOffset($type, $dimType, false), $context->negate(), true, $scope, diff --git a/test2.php b/test2.php new file mode 100644 index 0000000000..f05484205f --- /dev/null +++ b/test2.php @@ -0,0 +1,24 @@ +analyse([__DIR__ . '/data/discussion-10252.php'], []); } - + public function testBug9426(): void { $this->cliArgumentsVariablesRegistered = true; From 2b207f1804cd73abbbf679e7a36d2d7c4f921efd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 10 Dec 2023 11:33:56 +0100 Subject: [PATCH 13/16] adjust expectation per discussion https://github.com/phpstan/phpstan/issues/10273 --- tests/PHPStan/Analyser/data/falsy-isset.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index 2ee91f8ec9..b03f31f998 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -216,11 +216,11 @@ public function nestedOptionalNullableFetch(?string $nullableString): void assertType("array{bar: array{foo: string}}", $a); } else { assertVariableCertainty(TrinaryLogic::createYes(), $a); - assertType("array{}|array{bar: array{foo: null}}", $a); + assertType("array{bar: array{foo: null}}", $a); } assertVariableCertainty(TrinaryLogic::createYes(), $a); - assertType("array{}|array{bar: array{foo: string|null}}", $a); + assertType("array{bar: array{foo: null}}|array{bar: array{foo: string}}", $a); } } From 105ecdae5387778e81dabf53a6ac6c75bd723903 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 10 Dec 2023 12:42:03 +0100 Subject: [PATCH 14/16] Add new failling test-case --- src/Analyser/TypeSpecifier.php | 1 + tests/PHPStan/Analyser/data/falsy-isset.php | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index f2c2d69c96..11f0c482df 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -697,6 +697,7 @@ static function (Type $type, callable $traverse) use ($dimType, $optional): Type } if ($type instanceof ConstantArrayType) { + // unset the offset and set a new value, since we don't want to narrow the existing one $typeWithoutOffset = $type->unsetOffset($dimType); if (!$typeWithoutOffset instanceof ConstantArrayType) { throw new ShouldNotHappenException(); diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index b03f31f998..14d63fb51f 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -340,3 +340,24 @@ function render(?int $noteListLimit, int $count): void assertType('int', $noteListLimit); } } + +/** + * @param mixed[] $requestAttributes + */ +function getParameters(string $legacyLink, array $requestAttributes): void +{ + $legacyParameters = []; + + assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); + if (isset($requestAttributes['_legacy_link'])) { + $linkParts = explode(':', $legacyLink); + if (!isset($legacyParameters['controller'])) { + $legacyParameters['controller'] = $linkParts[0]; + } + + assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); + if (isset($legacyParameters['controller'], $legacyParameters['action'])) { + } + assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); + } +} From 59e87d2df07f007ee8a75c8d8a0315322f095a3f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 10 Dec 2023 12:56:43 +0100 Subject: [PATCH 15/16] added a multi-offset shape test --- tests/PHPStan/Analyser/data/falsy-isset.php | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index 14d63fb51f..501cc9d153 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -114,6 +114,31 @@ public function maybeCertainNonNull(): void assertType("array{bar: 1}", $a); } + public function maybeCertainNonNullMultiOffsetShape(): void + { + if (rand() % 2) { + $a = [ + 'bar' => 'world', + 'foo' => 'hello' + ]; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + + assertType("array{bar: 'world', foo: 'hello'}", $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType("array{bar: 'world', foo: 'hello'}", $a); + $a['bar'] = 1; + assertType("array{bar: 1, foo: 'hello'}", $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType("*ERROR*", $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType("array{bar: 1, foo: 'hello'}", $a); + } + public function yesCertainNull(): void { $a = ['bar' => null]; From 43b8649ed4d504b1a18a9787033008255aebd83d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 10 Dec 2023 15:31:09 +0100 Subject: [PATCH 16/16] fix --- src/Analyser/TypeSpecifier.php | 26 ++++++++++++++------- tests/PHPStan/Analyser/data/falsy-isset.php | 26 +++++++-------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 11f0c482df..fb6658b6a2 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -744,14 +744,24 @@ static function (Type $type, callable $traverse) use ($dimType, $optional): Type return $specifiedType; } - return $this->create( - new IssetExpr($issetExpr->var), - new NullType(), - $context, - false, - $scope, - $rootExpr, - ); + $typeWithoutOffset = $type->unsetOffset($dimType); + $arraySize = $typeWithoutOffset->getArraySize(); + if ( + !$arraySize instanceof NeverType + && (new ConstantIntegerType(0))->isSuperTypeOf($arraySize)->yes() + ) { + // variable cannot exist + return $this->create( + new IssetExpr($issetExpr->var), + new NullType(), + $context, + false, + $scope, + $rootExpr, + ); + } + + return new SpecifiedTypes(); } if ($isNullable) { diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php index 501cc9d153..cc7dd40eba 100644 --- a/tests/PHPStan/Analyser/data/falsy-isset.php +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -131,12 +131,12 @@ public function maybeCertainNonNullMultiOffsetShape(): void $a['bar'] = 1; assertType("array{bar: 1, foo: 'hello'}", $a); } else { - assertVariableCertainty(TrinaryLogic::createNo(), $a); - assertType("*ERROR*", $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType("array{bar: 'world', foo: 'hello'}", $a); } assertVariableCertainty(TrinaryLogic::createMaybe(), $a); - assertType("array{bar: 1, foo: 'hello'}", $a); + assertType("array{bar: 1|'world', foo: 'hello'}", $a); } public function yesCertainNull(): void @@ -366,23 +366,15 @@ function render(?int $noteListLimit, int $count): void } } -/** - * @param mixed[] $requestAttributes - */ -function getParameters(string $legacyLink, array $requestAttributes): void +function getParameters(): void { + /** @var array{controller: string} */ $legacyParameters = []; assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); - if (isset($requestAttributes['_legacy_link'])) { - $linkParts = explode(':', $legacyLink); - if (!isset($legacyParameters['controller'])) { - $legacyParameters['controller'] = $linkParts[0]; - } - - assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); - if (isset($legacyParameters['controller'], $legacyParameters['action'])) { - } - assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); + if (isset($legacyParameters['controller'], $legacyParameters['action'])) { + assertType('*NEVER*', $legacyParameters); } + assertType('array{controller: string}', $legacyParameters); + assertVariableCertainty(\PHPStan\TrinaryLogic::createYes(), $legacyParameters); }