From 934174eb86c59750ec7c1552212fd8667f565808 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 20 Jun 2022 10:05:08 +0200 Subject: [PATCH 01/15] Fix resolving mixed + array --- .../InitializerExprTypeResolver.php | 21 +++++++++++++ .../Operators/InvalidBinaryOperationRule.php | 31 ++++++++++++------- .../Analyser/LegacyNodeScopeResolverTest.php | 10 +++++- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/binary.php | 4 +++ tests/PHPStan/Analyser/data/bug-7492.php | 14 +++++++++ .../InvalidBinaryOperationRuleTest.php | 8 +++++ .../Rules/Operators/data/invalid-binary.php | 15 +++++++++ 8 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-7492.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index ebe3e8b50c..9e113571d0 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -955,6 +955,27 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): ]); } + $mixedArray = new ArrayType(new MixedType(), new MixedType()); + if ( + ($leftType->isArray()->yes() && $rightType->isSuperTypeOf($mixedArray)->no()) + || ($rightType->isArray()->yes() && $leftType->isSuperTypeOf($mixedArray)->no()) + ) { + return new ErrorType(); + } + + if ( + ($leftType->isArray()->yes() && $rightType->isSuperTypeOf($mixedArray)->yes()) + || ($rightType->isArray()->yes() && $leftType->isSuperTypeOf($mixedArray)->yes()) + ) { + $arrayType = new ArrayType(new MixedType(), new MixedType()); + + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + return $arrayType; + } + return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType); } diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 5acfb28dc7..723f2dc7fc 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -11,6 +11,7 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -71,22 +72,28 @@ public function processNode(Node $node, Scope $scope): array $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; } - $leftType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $left, - '', - $callback, - )->getType(); + $leftType = $scope->getType($left); + if (!$leftType instanceof MixedType) { + $leftType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $left, + '', + $callback, + )->getType(); + } if ($leftType instanceof ErrorType) { return []; } - $rightType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $right, - '', - $callback, - )->getType(); + $rightType = $scope->getType($right); + if (!$rightType instanceof MixedType) { + $rightType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $right, + '', + $callback, + )->getType(); + } if ($rightType instanceof ErrorType) { return []; } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 03b1ba4810..996af0c923 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2696,9 +2696,17 @@ public function dataBinaryOperations(): array '$mixed - $mixed', ], [ - '*ERROR*', + 'array', '$mixed + []', ], + [ + '*ERROR*', + '$mixedNoArray + []', + ], + [ + '*ERROR*', + '$integer + []', + ], [ '124', '1 + "123"', diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a892709cb0..2e005ede1c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1000,6 +1000,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-array-bug.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7492.php'); } /** diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index fc19fca14e..61676df638 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -190,6 +190,10 @@ public function doFoo(array $generalArray) $severalSumWithStaticConst2 = 1 + static::INT_CONST + 1; $severalSumWithStaticConst3 = 1 + 1 + static::INT_CONST; + if (!is_array($mixed)) { + $mixedNoArray = $mixed; + } + die; } diff --git a/tests/PHPStan/Analyser/data/bug-7492.php b/tests/PHPStan/Analyser/data/bug-7492.php new file mode 100644 index 0000000000..67fb2909d9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7492.php @@ -0,0 +1,14 @@ + '', 'login' => '', 'password' => '', 'name' => '']; + assertType('non-empty-array', $x); + } +} diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 1c8d3b2fc8..c0df6eceba 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -242,6 +242,14 @@ public function testRule(): void 'Binary operation "/" between 10 and literal-string results in an error.', 222, ], + [ + 'Binary operation "+" between int and array{} results in an error.', + 259, + ], + [ + 'Binary operation "+" between mixed and array results in an error.', + 266, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 3069c82ce8..d11f93316c 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -254,3 +254,18 @@ function benevolentPlus(array $a, int $i): void { echo $k + $i; } }; + +function (int $int) { + $int + []; +}; + +function ($mixed, array $arr) { + if (is_array($mixed)) { + return; + } + $mixed + $arr; // mixed~array + array +}; + +function noErrorOnMixedPlusArray($mixed, array $arr) { + $mixed + $arr; +}; From 1dafe1e6392c4c25f44221e6774dfd753177f1a2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 3 Aug 2022 10:20:27 +0200 Subject: [PATCH 02/15] refined logic based on maybe() --- phpstan-baseline.neon | 10 -------- .../InitializerExprTypeResolver.php | 23 +++++++++++-------- .../Analyser/LegacyNodeScopeResolverTest.php | 4 ++++ tests/PHPStan/Analyser/data/binary.php | 3 +++ .../Rules/Operators/data/invalid-binary.php | 4 ---- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ce45068281..7b7d712ce8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -48,11 +48,6 @@ parameters: count: 1 path: src/Collectors/Collector.php - - - message: "#^Binary operation \"\\+\" between array\\{class\\-string\\\\} and array\\\\|false results in an error\\.$#" - count: 1 - path: src/Collectors/Registry.php - - message: "#^Method PHPStan\\\\Collectors\\\\Registry\\:\\:__construct\\(\\) has parameter \\$collectors with generic interface PHPStan\\\\Collectors\\\\Collector but does not specify its types\\: TNodeType, TValue$#" count: 1 @@ -307,11 +302,6 @@ parameters: count: 1 path: src/Reflection/SignatureMap/Php8SignatureMapProvider.php - - - message: "#^Binary operation \"\\+\" between array\\{class\\-string\\\\} and array\\\\|false results in an error\\.$#" - count: 1 - path: src/Rules/Registry.php - - message: "#^Method PHPStan\\\\Rules\\\\Registry\\:\\:__construct\\(\\) has parameter \\$rules with generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" count: 1 diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 9e113571d0..8e8bfa0414 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -955,25 +955,30 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): ]); } - $mixedArray = new ArrayType(new MixedType(), new MixedType()); if ( - ($leftType->isArray()->yes() && $rightType->isSuperTypeOf($mixedArray)->no()) - || ($rightType->isArray()->yes() && $leftType->isSuperTypeOf($mixedArray)->no()) + ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->no()) + || ($arrayType->isSuperTypeOf($rightType)->yes() && $arrayType->isSuperTypeOf($leftType)->no()) ) { return new ErrorType(); } if ( - ($leftType->isArray()->yes() && $rightType->isSuperTypeOf($mixedArray)->yes()) - || ($rightType->isArray()->yes() && $leftType->isSuperTypeOf($mixedArray)->yes()) + ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->maybe()) + || ($arrayType->isSuperTypeOf($rightType)->yes() && $arrayType->isSuperTypeOf($leftType)->maybe()) + || ($arrayType->isSuperTypeOf($leftType)->maybe() && $arrayType->isSuperTypeOf($rightType)->maybe()) + || ($arrayType->isSuperTypeOf($rightType)->maybe() && $arrayType->isSuperTypeOf($leftType)->maybe()) ) { - $arrayType = new ArrayType(new MixedType(), new MixedType()); + if ($rightType->isArray()->yes() || $leftType->isArray()->yes()) { + $resultType = new ArrayType(new MixedType(), new MixedType()); - if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + return $resultType; } - return $arrayType; + return TypeCombinator::union($leftType, $rightType); } return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 996af0c923..747760f096 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2699,6 +2699,10 @@ public function dataBinaryOperations(): array 'array', '$mixed + []', ], + [ + 'array|int', + '$intOrArray + $intOrArray', + ], [ '*ERROR*', '$mixedNoArray + []', diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 61676df638..dcf616b7f4 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -194,6 +194,9 @@ public function doFoo(array $generalArray) $mixedNoArray = $mixed; } + /** @var int|array $intOrArray */ + $intOrArray = doFoo(); + die; } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index d11f93316c..4bfe17ed74 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -265,7 +265,3 @@ function ($mixed, array $arr) { } $mixed + $arr; // mixed~array + array }; - -function noErrorOnMixedPlusArray($mixed, array $arr) { - $mixed + $arr; -}; From a48cce2beca993bed07e0aa3d339d2eab608f59e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 3 Aug 2022 11:41:58 +0200 Subject: [PATCH 03/15] added mixed~float testcase --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 4 ++++ tests/PHPStan/Analyser/data/binary.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 747760f096..451e68c174 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2703,6 +2703,10 @@ public function dataBinaryOperations(): array 'array|int', '$intOrArray + $intOrArray', ], + [ + 'array', + '$mixedNoFloat + []', + ], [ '*ERROR*', '$mixedNoArray + []', diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index dcf616b7f4..637140015c 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -193,6 +193,9 @@ public function doFoo(array $generalArray) if (!is_array($mixed)) { $mixedNoArray = $mixed; } + if (!is_float($mixed)) { + $mixedNoFloat = $mixed; + } /** @var int|array $intOrArray */ $intOrArray = doFoo(); From ed97587c1d12885a5f9660730e2df1dd76213dd8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 3 Aug 2022 12:02:51 +0200 Subject: [PATCH 04/15] more substractable tests --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 8 ++++++++ tests/PHPStan/Analyser/data/binary.php | 3 +++ 2 files changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 451e68c174..d977e678c7 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2707,6 +2707,14 @@ public function dataBinaryOperations(): array 'array', '$mixedNoFloat + []', ], + [ + '(float|int)', + '$mixedNoFloat + 5', + ], + [ + '(float|int)', + '$mixedNoInt + 5', + ], [ '*ERROR*', '$mixedNoArray + []', diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 637140015c..49972c9184 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -193,6 +193,9 @@ public function doFoo(array $generalArray) if (!is_array($mixed)) { $mixedNoArray = $mixed; } + if (!is_int($mixed)) { + $mixedNoInt = $mixed; + } if (!is_float($mixed)) { $mixedNoFloat = $mixed; } From 94a5d2d93f0912ac717d9dd339dbda460f5c0d4c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 25 Aug 2022 10:20:43 +0200 Subject: [PATCH 05/15] handle NeverType in getPlusType() --- src/Reflection/InitializerExprTypeResolver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 8e8bfa0414..3d398344d1 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -859,6 +859,10 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return new NeverType(); + } + $leftTypes = TypeUtils::getConstantScalars($leftType); $rightTypes = TypeUtils::getConstantScalars($rightType); $leftTypesCount = count($leftTypes); From 3c17ff898efb4658960a5dccd16c2f0151ddd011 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 25 Aug 2022 17:28:09 +0200 Subject: [PATCH 06/15] refactor --- src/Reflection/InitializerExprTypeResolver.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 3d398344d1..0a72c2da49 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -925,7 +925,9 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): return TypeCombinator::union(...$resultTypes); } - if ($leftType->isArray()->yes() && $rightType->isArray()->yes()) { + $leftIsArray = $leftType->isArray(); + $rightIsArray = $rightType->isArray(); + if ($leftIsArray->yes() && $rightIsArray->yes()) { if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) { // to preserve BenevolentUnionType $keyType = $leftType->getIterableKeyType(); @@ -960,17 +962,17 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): } if ( - ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->no()) - || ($arrayType->isSuperTypeOf($rightType)->yes() && $arrayType->isSuperTypeOf($leftType)->no()) + ($leftIsArray->yes() && $rightIsArray->no()) + || ($rightIsArray->yes() && $leftIsArray->no()) ) { return new ErrorType(); } if ( - ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->maybe()) - || ($arrayType->isSuperTypeOf($rightType)->yes() && $arrayType->isSuperTypeOf($leftType)->maybe()) - || ($arrayType->isSuperTypeOf($leftType)->maybe() && $arrayType->isSuperTypeOf($rightType)->maybe()) - || ($arrayType->isSuperTypeOf($rightType)->maybe() && $arrayType->isSuperTypeOf($leftType)->maybe()) + ($leftIsArray->yes() && $rightIsArray->maybe()) + || ($rightIsArray->yes() && $leftIsArray->maybe()) + || ($leftIsArray->maybe() && $rightIsArray->maybe()) + || ($rightIsArray->maybe() && $leftIsArray->maybe()) ) { if ($rightType->isArray()->yes() || $leftType->isArray()->yes()) { $resultType = new ArrayType(new MixedType(), new MixedType()); From 93db2ef12f75dfcbf5dc66922450df6bcd1f97a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 25 Aug 2022 17:52:53 +0200 Subject: [PATCH 07/15] refactor --- .../Operators/InvalidBinaryOperationRule.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 723f2dc7fc..5acfb28dc7 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; -use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -72,28 +71,22 @@ public function processNode(Node $node, Scope $scope): array $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; } - $leftType = $scope->getType($left); - if (!$leftType instanceof MixedType) { - $leftType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $left, - '', - $callback, - )->getType(); - } + $leftType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $left, + '', + $callback, + )->getType(); if ($leftType instanceof ErrorType) { return []; } - $rightType = $scope->getType($right); - if (!$rightType instanceof MixedType) { - $rightType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $right, - '', - $callback, - )->getType(); - } + $rightType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $right, + '', + $callback, + )->getType(); if ($rightType instanceof ErrorType) { return []; } From 4efb222e746ab780d1da1871ad9c9618e63590b0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 25 Aug 2022 20:38:13 +0200 Subject: [PATCH 08/15] fix --- src/Reflection/InitializerExprTypeResolver.php | 2 +- src/Rules/RuleLevelHelper.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 0a72c2da49..087ccac3e4 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -974,7 +974,7 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): || ($leftIsArray->maybe() && $rightIsArray->maybe()) || ($rightIsArray->maybe() && $leftIsArray->maybe()) ) { - if ($rightType->isArray()->yes() || $leftType->isArray()->yes()) { + if ($rightIsArray->yes() || $leftIsArray->yes()) { $resultType = new ArrayType(new MixedType(), new MixedType()); if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 43f57d7223..b6e84c68b0 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -166,7 +166,10 @@ public function findTypeToCheck( return new FoundTypeResult(new StrictMixedType(), [], [], null); } - if ($type instanceof MixedType || $type instanceof NeverType) { + if ( + ($type instanceof MixedType && $type->getSubtractedType() === null) + || $type instanceof NeverType + ) { return new FoundTypeResult(new ErrorType(), [], [], null); } if ($type instanceof StaticType) { From 31e763c2b52826eb5beac59ec084072766c8ef2b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 11:25:39 +0200 Subject: [PATCH 09/15] inference improvements --- .../InitializerExprTypeResolver.php | 50 +++++++++++++++---- .../Analyser/LegacyNodeScopeResolverTest.php | 12 +++++ tests/PHPStan/Analyser/data/binary.php | 9 ++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 087ccac3e4..0810175f2a 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -53,6 +53,7 @@ use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; +use PHPStan\Type\SubtractableType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -969,22 +970,51 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): } if ( - ($leftIsArray->yes() && $rightIsArray->maybe()) - || ($rightIsArray->yes() && $leftIsArray->maybe()) - || ($leftIsArray->maybe() && $rightIsArray->maybe()) - || ($rightIsArray->maybe() && $leftIsArray->maybe()) + $leftIsArray->yes() && $rightIsArray->maybe() ) { - if ($rightIsArray->yes() || $leftIsArray->yes()) { - $resultType = new ArrayType(new MixedType(), new MixedType()); + if ($rightType instanceof SubtractableType && $rightType->getSubtractedType() !== null) { + if ($rightType->getSubtractedType()->isArray()->yes()) { + return new ErrorType(); + } + } + + $resultType = new ArrayType(new MixedType(), new MixedType()); + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } - if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + return $resultType; + } + + if ( + $leftIsArray->maybe() && $rightIsArray->yes() + ) { + if ($leftType instanceof SubtractableType && $leftType->getSubtractedType() !== null) { + if ($leftType->getSubtractedType()->isArray()->yes()) { + return new ErrorType(); } + } - return $resultType; + $resultType = new ArrayType(new MixedType(), new MixedType()); + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); } - return TypeCombinator::union($leftType, $rightType); + return $resultType; + } + + if ($leftIsArray->maybe() && $rightIsArray->maybe()) { + $plusable = new UnionType([ + new StringType(), + new FloatType(), + new IntegerType(), + new ArrayType(new MixedType(), new MixedType()), + new BooleanType(), + ]); + + if ($plusable->isSuperTypeOf($leftType)->yes() && $plusable->isSuperTypeOf($rightType)->yes()) { + return TypeCombinator::union($leftType, $rightType); + } } return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index d977e678c7..f34cd3c2d6 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2703,6 +2703,18 @@ public function dataBinaryOperations(): array 'array|int', '$intOrArray + $intOrArray', ], + [ + 'float|int', + '$intOrFloat + $intOrFloat', + ], + [ + 'array|float', + '$floatOrArray + $floatOrArray', + ], + [ + 'array|bool|float|int|string', + '$plusable + $plusable', + ], [ 'array', '$mixedNoFloat + []', diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 49972c9184..7ae007ee74 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -203,6 +203,15 @@ public function doFoo(array $generalArray) /** @var int|array $intOrArray */ $intOrArray = doFoo(); + /** @var array|float $floatOrArray */ + $floatOrArray = doFoo(); + + /** @var int|float $intOrFloat */ + $intOrFloat = doFoo(); + + /** @var array|float|int|string|bool $plusable */ + $plusable = doFoo(); + die; } From f849be49bf7d46357ebb6cf0f956c5b291b72418 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 11:29:44 +0200 Subject: [PATCH 10/15] fix tests --- src/Rules/RuleLevelHelper.php | 3 +-- .../Rules/Operators/InvalidBinaryOperationRuleTest.php | 4 ---- tests/PHPStan/Rules/Operators/data/invalid-binary.php | 3 ++- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index b6e84c68b0..3b1245e208 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -167,8 +167,7 @@ public function findTypeToCheck( } if ( - ($type instanceof MixedType && $type->getSubtractedType() === null) - || $type instanceof NeverType + $type instanceof MixedType || $type instanceof NeverType ) { return new FoundTypeResult(new ErrorType(), [], [], null); } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index c0df6eceba..06c7503109 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -246,10 +246,6 @@ public function testRule(): void 'Binary operation "+" between int and array{} results in an error.', 259, ], - [ - 'Binary operation "+" between mixed and array results in an error.', - 266, - ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 4bfe17ed74..6ad7f235c2 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -263,5 +263,6 @@ function ($mixed, array $arr) { if (is_array($mixed)) { return; } - $mixed + $arr; // mixed~array + array + // mixed~array + array + $mixed + $arr; // should report: Binary operation "+" between mixed and array results in an error. }; From f2e3a9abd53d621eef14c8c44d18301c5d294dfb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 11:32:22 +0200 Subject: [PATCH 11/15] remove not yet implemented test --- tests/PHPStan/Rules/Operators/data/invalid-binary.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 6ad7f235c2..ef5f6e4c58 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -258,11 +258,3 @@ function benevolentPlus(array $a, int $i): void { function (int $int) { $int + []; }; - -function ($mixed, array $arr) { - if (is_array($mixed)) { - return; - } - // mixed~array + array - $mixed + $arr; // should report: Binary operation "+" between mixed and array results in an error. -}; From e60d8a40c3bb5b2d1c81217658d7ba66de5ee58c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 11:38:22 +0200 Subject: [PATCH 12/15] revert leftovers --- src/Rules/RuleLevelHelper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 3b1245e208..43f57d7223 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -166,9 +166,7 @@ public function findTypeToCheck( return new FoundTypeResult(new StrictMixedType(), [], [], null); } - if ( - $type instanceof MixedType || $type instanceof NeverType - ) { + if ($type instanceof MixedType || $type instanceof NeverType) { return new FoundTypeResult(new ErrorType(), [], [], null); } if ($type instanceof StaticType) { From d76b37b31a93088c0cb4d616b109d98979a28d91 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 11:47:33 +0200 Subject: [PATCH 13/15] fixed mixed~array|float + array --- src/Reflection/InitializerExprTypeResolver.php | 4 ++-- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 4 ++++ tests/PHPStan/Analyser/data/binary.php | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 0810175f2a..a284566dc9 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -973,7 +973,7 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): $leftIsArray->yes() && $rightIsArray->maybe() ) { if ($rightType instanceof SubtractableType && $rightType->getSubtractedType() !== null) { - if ($rightType->getSubtractedType()->isArray()->yes()) { + if (count(TypeUtils::getAnyArrays($rightType->getSubtractedType())) > 0) { return new ErrorType(); } } @@ -990,7 +990,7 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): $leftIsArray->maybe() && $rightIsArray->yes() ) { if ($leftType instanceof SubtractableType && $leftType->getSubtractedType() !== null) { - if ($leftType->getSubtractedType()->isArray()->yes()) { + if (count(TypeUtils::getAnyArrays($leftType->getSubtractedType())) > 0) { return new ErrorType(); } } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index f34cd3c2d6..cdc6492de9 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2731,6 +2731,10 @@ public function dataBinaryOperations(): array '*ERROR*', '$mixedNoArray + []', ], + [ + '*ERROR*', + '$mixedNoArrayOrInt + []', + ], [ '*ERROR*', '$integer + []', diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 7ae007ee74..352ff65141 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -199,6 +199,11 @@ public function doFoo(array $generalArray) if (!is_float($mixed)) { $mixedNoFloat = $mixed; } + if (!is_array($mixed)) { + if (!is_int($mixed)) { + $mixedNoArrayOrInt = $mixed; + } + } /** @var int|array $intOrArray */ $intOrArray = doFoo(); From a78132e4745508127325c28a317273abb7ae35de Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Aug 2022 12:21:52 +0200 Subject: [PATCH 14/15] de-duplicate --- .../InitializerExprTypeResolver.php | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index a284566dc9..4f90fa1400 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -964,33 +964,23 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): if ( ($leftIsArray->yes() && $rightIsArray->no()) - || ($rightIsArray->yes() && $leftIsArray->no()) + || ($leftIsArray->no() && $rightIsArray->yes()) ) { return new ErrorType(); } if ( - $leftIsArray->yes() && $rightIsArray->maybe() + ($leftIsArray->yes() && $rightIsArray->maybe()) + || ($leftIsArray->maybe() && $rightIsArray->yes()) ) { - if ($rightType instanceof SubtractableType && $rightType->getSubtractedType() !== null) { - if (count(TypeUtils::getAnyArrays($rightType->getSubtractedType())) > 0) { + if ($leftType instanceof SubtractableType && $leftType->getSubtractedType() !== null) { + if (count(TypeUtils::getAnyArrays($leftType->getSubtractedType())) > 0) { return new ErrorType(); } } - $resultType = new ArrayType(new MixedType(), new MixedType()); - if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::intersect($resultType, new NonEmptyArrayType()); - } - - return $resultType; - } - - if ( - $leftIsArray->maybe() && $rightIsArray->yes() - ) { - if ($leftType instanceof SubtractableType && $leftType->getSubtractedType() !== null) { - if (count(TypeUtils::getAnyArrays($leftType->getSubtractedType())) > 0) { + if ($rightType instanceof SubtractableType && $rightType->getSubtractedType() !== null) { + if (count(TypeUtils::getAnyArrays($rightType->getSubtractedType())) > 0) { return new ErrorType(); } } From 20f620b5442da67feec1be67b99166bb38c9203a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Aug 2022 14:14:57 +0200 Subject: [PATCH 15/15] fix after rebase --- src/Reflection/InitializerExprTypeResolver.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 4f90fa1400..caa37fe936 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -53,7 +53,6 @@ use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; -use PHPStan\Type\SubtractableType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -973,18 +972,6 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): ($leftIsArray->yes() && $rightIsArray->maybe()) || ($leftIsArray->maybe() && $rightIsArray->yes()) ) { - if ($leftType instanceof SubtractableType && $leftType->getSubtractedType() !== null) { - if (count(TypeUtils::getAnyArrays($leftType->getSubtractedType())) > 0) { - return new ErrorType(); - } - } - - if ($rightType instanceof SubtractableType && $rightType->getSubtractedType() !== null) { - if (count(TypeUtils::getAnyArrays($rightType->getSubtractedType())) > 0) { - return new ErrorType(); - } - } - $resultType = new ArrayType(new MixedType(), new MixedType()); if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { return TypeCombinator::intersect($resultType, new NonEmptyArrayType());