diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a6c7090e34..d33fabf595 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -15,6 +15,14 @@ parameters: count: 1 path: src/Analyser/LazyInternalScopeFactory.php + - + message: """ + #^Call to deprecated method getAnyArrays\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: + Use PHPStan\\\\Type\\\\Type\\:\\:getArrays\\(\\) instead\\.$# + """ + count: 2 + path: src/Analyser/MutatingScope.php + - message: """ #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 79178a0cb2..8dcdda47f8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4078,7 +4078,7 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType); + $variableType = self::generalizeType($variableType, $prevVariableType, 0); } } @@ -4200,7 +4200,7 @@ private function generalizeVariableTypeHolders( $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder( $variableTypeHolder->getExpr(), - self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType()), + self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), $variableTypeHolder->getCertainty(), ); } @@ -4208,7 +4208,7 @@ private function generalizeVariableTypeHolders( return $variableTypeHolders; } - private static function generalizeType(Type $a, Type $b): Type + private static function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -4301,6 +4301,7 @@ private static function generalizeType(Type $a, Type $b): Type self::generalizeType( $constantArraysA->getOffsetValueType($keyType), $constantArraysB->getOffsetValueType($keyType), + $depth + 1, ), !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(), ); @@ -4309,8 +4310,8 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes[] = $resultArrayBuilder->getArray(); } else { $resultType = new ArrayType( - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())), + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)), ); if ($constantArraysA->isIterableAtLeastOnce()->yes() && $constantArraysB->isIterableAtLeastOnce()->yes()) { $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); @@ -4334,16 +4335,14 @@ private static function generalizeType(Type $a, Type $b): Type $aValueType = $generalArraysA->getIterableValueType(); $bValueType = $generalArraysB->getIterableValueType(); - $aArrays = $aValueType->getArrays(); - $bArrays = $bValueType->getArrays(); if ( - count($aArrays) === 1 - && $aArrays[0]->isConstantArray()->no() - && count($bArrays) === 1 - && $bArrays[0]->isConstantArray()->no() + $aValueType->isArray()->yes() + && $aValueType->isConstantArray()->no() + && $bValueType->isArray()->yes() + && $bValueType->isConstantArray()->no() ) { - $aDepth = self::getArrayDepth($aArrays[0]); - $bDepth = self::getArrayDepth($bArrays[0]); + $aDepth = self::getArrayDepth($aValueType) + $depth; + $bDepth = self::getArrayDepth($bValueType) + $depth; if ( ($aDepth > 2 || $bDepth > 2) && abs($aDepth - $bDepth) > 0 @@ -4354,8 +4353,8 @@ private static function generalizeType(Type $a, Type $b): Type } $resultType = new ArrayType( - TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType)), + TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union(self::generalizeType($aValueType, $bValueType, $depth + 1)), ); if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) { $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); @@ -4514,17 +4513,14 @@ private static function generalizeType(Type $a, Type $b): Type ); } - private static function getArrayDepth(ArrayType $type): int + private static function getArrayDepth(Type $type): int { $depth = 0; - while ($type instanceof ArrayType) { + $arrays = TypeUtils::getAnyArrays($type); + while (count($arrays) > 0) { $temp = $type->getIterableValueType(); - $arrays = $temp->getArrays(); - if (count($arrays) === 1) { - $type = $arrays[0]; - } else { - $type = $temp; - } + $type = $temp; + $arrays = TypeUtils::getAnyArrays($type); $depth++; } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 913e5a455e..1aeaf6eb51 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -93,27 +93,52 @@ public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap return $types; } - /** - * @return string[] - */ public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->types); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; } public function getArrays(): array { - return UnionTypeHelper::getArrays($this->getTypes()); + $arrays = []; + foreach ($this->types as $type) { + foreach ($type->getArrays() as $array) { + $arrays[] = $array; + } + } + + return $arrays; } public function getConstantArrays(): array { - return UnionTypeHelper::getConstantArrays($this->getTypes()); + $constantArrays = []; + foreach ($this->types as $type) { + foreach ($type->getConstantArrays() as $constantArray) { + $constantArrays[] = $constantArray; + } + } + + return $constantArrays; } public function getConstantStrings(): array { - return UnionTypeHelper::getConstantStrings($this->getTypes()); + $strings = []; + foreach ($this->types as $type) { + foreach ($type->getConstantStrings() as $string) { + $strings[] = $string; + } + } + + return $strings; } public function accepts(Type $otherType, bool $strictTypes): TrinaryLogic diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index a51e2fb51b..7c83f34d48 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -218,7 +218,9 @@ private static function map( if ($type instanceof UnionType) { $matchingTypes = []; foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof $typeClass) { + $matchingInner = self::map($typeClass, $innerType, $inspectIntersections, $stopOnUnmatched); + + if ($matchingInner === []) { if ($stopOnUnmatched) { return []; } @@ -226,7 +228,9 @@ private static function map( continue; } - $matchingTypes[] = $innerType; + foreach ($matchingInner as $innerMapped) { + $matchingTypes[] = $innerMapped; + } } return $matchingTypes; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 414de9b311..9630c6bbc3 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -102,22 +102,67 @@ private function getSortedTypes(): array */ public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->getTypes()); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; } public function getArrays(): array { - return UnionTypeHelper::getArrays($this->getTypes()); + $arrays = []; + foreach ($this->types as $type) { + $innerTypeArrays = $type->getArrays(); + if ($innerTypeArrays === []) { + return []; + } + + foreach ($innerTypeArrays as $array) { + $arrays[] = $array; + } + } + + return $arrays; } public function getConstantArrays(): array { - return UnionTypeHelper::getConstantArrays($this->getTypes()); + $constantArrays = []; + foreach ($this->types as $type) { + $typeAsConstantArrays = $type->getConstantArrays(); + + if ($typeAsConstantArrays === []) { + return []; + } + + foreach ($typeAsConstantArrays as $constantArray) { + $constantArrays[] = $constantArray; + } + } + + return $constantArrays; } public function getConstantStrings(): array { - return UnionTypeHelper::getConstantStrings($this->getTypes()); + $strings = []; + foreach ($this->types as $type) { + $constantStrings = $type->getConstantStrings(); + + if ($constantStrings === []) { + return []; + } + + foreach ($constantStrings as $string) { + $strings[] = $string; + } + } + + return $strings; } public function accepts(Type $type, bool $strictTypes): TrinaryLogic diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index 7f8936899e..db153a6a01 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -3,12 +3,10 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use function array_merge; use function count; use function strcasecmp; use function usort; @@ -17,62 +15,6 @@ class UnionTypeHelper { - /** - * @param Type[] $types - * @return string[] - */ - public static function getReferencedClasses(array $types): array - { - $referencedClasses = []; - foreach ($types as $type) { - $referencedClasses[] = $type->getReferencedClasses(); - } - - return array_merge(...$referencedClasses); - } - - /** - * @param Type[] $types - * @return list - */ - public static function getArrays(array $types): array - { - $arrays = []; - foreach ($types as $type) { - $arrays[] = $type->getArrays(); - } - - return array_merge(...$arrays); - } - - /** - * @param Type[] $types - * @return list - */ - public static function getConstantArrays(array $types): array - { - $constantArrays = []; - foreach ($types as $type) { - $constantArrays[] = $type->getConstantArrays(); - } - - return array_merge(...$constantArrays); - } - - /** - * @param Type[] $types - * @return list - */ - public static function getConstantStrings(array $types): array - { - $strings = []; - foreach ($types as $type) { - $strings[] = $type->getConstantStrings(); - } - - return array_merge(...$strings); - } - /** * @param Type[] $types * @return Type[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 9c7243a005..eb53bed1ef 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1154,6 +1154,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo-php8.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8568.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-8568.php b/tests/PHPStan/Analyser/data/bug-8568.php new file mode 100644 index 0000000000..71db7a6c98 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8568.php @@ -0,0 +1,26 @@ +get()); + } + + public function get(): ?int + { + return rand() ? 5 : null; + } + + /** + * @param numeric-string $numericS + */ + public function intersections($numericS): void { + assertType('non-falsy-string', 'a'. $numericS); + assertType('numeric-string', (string) $numericS); + } +} diff --git a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php index 431a37cbe0..986af00a85 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php @@ -122,4 +122,10 @@ public function testBug8076(): void $this->analyse([__DIR__ . '/data/bug-8076.php'], []); } + public function testBug8562(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8562.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8562.php b/tests/PHPStan/Rules/Comparison/data/bug-8562.php new file mode 100644 index 0000000000..9deeaa6a0d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8562.php @@ -0,0 +1,18 @@ + $a + */ +function a(array $a): void { + $l = (string) array_key_last($a); + $s = substr($l, 0, 2); + if ($s === '') { + ; + } else { + var_dump($s); + } +} + + diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index b2c88c3a41..61d4768534 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; @@ -25,6 +26,7 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; use stdClass; use function array_merge; use function array_reverse; @@ -1280,4 +1282,205 @@ public function testSorting(): void ); } + /** + * @dataProvider dataGetConstantArrays + * @param Type[] $types + * @param list $expectedDescriptions + */ + public function testGetConstantArrays( + array $types, + array $expectedDescriptions, + ): void + { + $unionType = TypeCombinator::union(...$types); + $constantArrays = $unionType->getConstantArrays(); + + $actualDescriptions = []; + foreach ($constantArrays as $constantArray) { + $actualDescriptions[] = $constantArray->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantArrays(): iterable + { + yield from [ + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ], + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + ), + new IntegerType(), + ], + [], + ], + ]; + } + + /** + * @dataProvider dataGetConstantStrings + * @param list $expectedDescriptions + */ + public function testGetConstantStrings( + Type $unionType, + array $expectedDescriptions, + ): void + { + $constantStrings = $unionType->getConstantStrings(); + + $actualDescriptions = []; + foreach ($constantStrings as $constantString) { + $actualDescriptions[] = $constantString->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantStrings(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [ + "'hello'", + "'world'", + ], + ], + [ + TypeCombinator::union( + new ConstantStringType(''), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + ), + [], + ], + [ + new UnionType([ + new IntersectionType( + [ + new ConstantStringType('foo'), + new AccessoryLiteralStringType(), + ], + ), + new IntersectionType( + [ + new ConstantStringType('bar'), + new AccessoryLiteralStringType(), + ], + ), + ]), + [ + "'foo'", + "'bar'", + ], + ], + ]; + } + + /** + * @dataProvider dataGetArrays + * @param list $expectedDescriptions + */ + public function testGetArrays( + Type $unionType, + array $expectedDescriptions, + ): void + { + $arrays = $unionType->getArrays(); + + $actualDescriptions = []; + foreach ($arrays as $arrayType) { + $actualDescriptions[] = $arrayType->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetArrays(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [], + ], + [ + TypeCombinator::union( + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ), + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + TypeCombinator::union( + new ArrayType(new IntegerType(), new StringType()), + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ), + [ + 'array', + ], + ], + ]; + } + }