diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a769db8499..d979a0f24a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5628,24 +5628,25 @@ static function (): void { $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { - $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $assignedTypeIsCompatible = false; - foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { - if ($type->isSuperTypeOf($assignedNativeType)->yes()) { - $assignedTypeIsCompatible = true; - break; + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } } } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); - } elseif ($scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { $scope = $scope->assignExpression( $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { @@ -5716,24 +5717,25 @@ static function (): void { $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { - $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); + $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes(); - $assignedTypeIsCompatible = false; - foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { - if ($type->isSuperTypeOf($assignedNativeType)->yes()) { - $assignedTypeIsCompatible = true; - break; + if (!$assignedTypeIsCompatible) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedExprType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } } } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); - } elseif ($scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + } else { $scope = $scope->assignExpression( $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 9af225a3c1..8bcc663327 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -29,6 +29,7 @@ use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -215,6 +216,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 5ea351a924..1e3b55b0ee 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -29,6 +29,7 @@ use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -212,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 961e2cbd95..f9fce63d94 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -213,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 90e7c1f64d..6600512da1 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -215,6 +215,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 447bf76ecc..72f81cabdb 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -215,6 +215,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index 18ee7399bf..3fee19deb3 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -29,6 +29,7 @@ use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -212,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 679c0b9824..a703decac4 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -113,6 +113,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + return $this; } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 72784cf114..568c847711 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -24,6 +24,7 @@ use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -331,7 +332,12 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { - return TypeCombinator::union($this, new StringType(), new ArrayType(new MixedType(true), new MixedType(true)), new ObjectType(Closure::class)); + return TypeCombinator::union( + $this, + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + new ArrayType(new MixedType(true), new MixedType(true)), + new ObjectType(Closure::class), + ); } public function isOffsetAccessLegal(): TrinaryLogic diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index ea1c4b09ef..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -14,6 +14,7 @@ use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; /** @api */ @@ -109,6 +110,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); + } + return $this; } diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 52f29d37a2..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -14,6 +14,7 @@ use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use function abs; use function sprintf; @@ -92,6 +93,15 @@ public function toArrayKey(): Type return $this; } + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); + } + + return TypeCombinator::union($this, $this->toFloat()); + } + public function generalize(GeneralizePrecision $precision): Type { return new IntegerType(); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 253af75e4e..e38e5be35a 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -145,6 +145,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this, $this->toString(), $this->toBoolean()); + } + return $this; } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index e974888bc7..fcb6fcd893 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -100,6 +100,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); + } + return TypeCombinator::union($this, $this->toFloat()); } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index e5b2540d7b..cc834e2950 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -706,6 +706,18 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + $classReflection = $this->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->hasNativeMethod('__toString') + ) { + return $this; + } + + return TypeCombinator::union($this, $this->toString()); + } + return $this; } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index c0114d8462..0f1778aa21 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -183,6 +183,13 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + if ($this->isNumericString()->no()) { + return TypeCombinator::union($this, $this->toBoolean()); + } + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + return $this; } diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index fe2a3f6ee6..c600f2d74a 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -21,6 +21,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; trait ObjectTypeTrait { @@ -275,6 +276,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php new file mode 100644 index 0000000000..b73906fdfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php @@ -0,0 +1,23 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12393Php84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + + +class StringableFoo { + private string $foo; + + // https://3v4l.org/2SPPj#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php index 9445d8632b..4edd2300c1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -143,3 +143,42 @@ public function getMixed() } } + +// https://3v4l.org/LK6Rh +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; // PHPStorm wrongly reports an error on this line + assertType('callable-string|non-empty-string', $this->foo); + } +} + +// https://3v4l.org/WJ8NW +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + +class StringableFoo { + private string $foo; + + // https://3v4l.org/DQSgA#v8.4.6 + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php new file mode 100644 index 0000000000..ae1946cdb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php @@ -0,0 +1,22 @@ += 8.4 + +declare(strict_types = 0); + +namespace Bug12393bPhp84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class StringableFoo { + private string $foo; + + // https://3v4l.org/nelJF#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('non-empty-string&numeric-string', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php new file mode 100644 index 0000000000..7ec8f3012b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -0,0 +1,709 @@ += 8.0 + +declare(strict_types = 0); + +namespace Bug12393b; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + private string $name; + + /** @var string */ + private $untypedName; + + private float $float; + + /** @var float */ + private $untypedFloat; + + private array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + $this->name = $plugin["name"]; + assertType('string', $this->name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + $this->untypedName = $plugin["name"]; + assertType('mixed', $this->untypedName); + } + + public function doBar(int $i){ + $this->float = $i; + assertType('float', $this->float); + } + + public function doBaz(int $i){ + $this->untypedFloat = $i; + assertType('int', $this->untypedFloat); + } + + public function doLorem(): void + { + $this->a = ['a' => 1]; + assertType('array{a: 1}', $this->a); + } + + public function doFloatTricky(){ + $this->float = 1; + assertType('1.0', $this->float); + } +} + +class HelloWorldStatic +{ + private static string $name; + + /** @var string */ + private static $untypedName; + + private static float $float; + + /** @var float */ + private static $untypedFloat; + + private static array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + self::$name = $plugin["name"]; + assertType('string', self::$name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + self::$untypedName = $plugin["name"]; + assertType('mixed', self::$untypedName); + } + + public function doBar(int $i){ + self::$float = $i; + assertType('float', self::$float); + } + + public function doBaz(int $i){ + self::$untypedFloat = $i; + assertType('int', self::$untypedFloat); + } + + public function doLorem(): void + { + self::$a = ['a' => 1]; + assertType('array{a: 1}', self::$a); + } +} + +class EntryPointLookup +{ + + /** @var array|null */ + private ?array $entriesData = null; + + /** + * @return array + */ + public function doFoo(): void + { + if ($this->entriesData !== null) { + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + + $data = $this->getMixed(); + if ($data !== null) { + $this->entriesData = $data; + assertType('array', $this->entriesData); + assertNativeType('array', $this->entriesData); + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + } + + /** + * @return mixed + */ + public function getMixed() + { + + } + +} + +class FooStringInt +{ + + public int $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('int', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('int', $this->foo); + $this->foo = $nonFalsy; + assertType('int|int<1, max>', $this->foo); + $this->foo = $numeric; + assertType('int', $this->foo); + $this->foo = $literal; + assertType('int', $this->foo); + $this->foo = $lower; + assertType('int', $this->foo); + $this->foo = $upper; + assertType('int', $this->foo); + } +} + +class FooStringFloat +{ + + public float $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('float', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123.0', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('float', $this->foo); + $this->foo = $nonFalsy; + assertType('float', $this->foo); + $this->foo = $numeric; + assertType('float', $this->foo); + $this->foo = $literal; + assertType('float', $this->foo); + $this->foo = $lower; + assertType('float', $this->foo); + $this->foo = $upper; + assertType('float', $this->foo); + } +} + +class FooStringBool +{ + + public bool $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('bool', $this->foo); + } + + public function doBar(): void + { + $this->foo = '0'; + assertType('false', $this->foo); + $this->foo = 'foo'; + assertType('true', $this->foo); + $this->foo = '123'; + assertType('true', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('bool', $this->foo); + $this->foo = $nonFalsy; + assertType('true', $this->foo); + $this->foo = $numeric; + assertType('bool', $this->foo); + $this->foo = $literal; + assertType('bool', $this->foo); + $this->foo = $lower; + assertType('bool', $this->foo); + $this->foo = $upper; + assertType('bool', $this->foo); + } +} + +class FooBoolInt +{ + + public int $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType('0|1', $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType('1', $this->foo); + $this->foo = false; + assertType('0', $this->foo); + } +} + +class FooVoidInt { + private ?int $foo; + private int $fooNonNull; + + public function doFoo(): void { + $this->foo = $this->returnVoid(); + assertType('null', $this->foo); + + $this->fooNonNull = $this->returnVoid(); + assertType('int|null', $this->foo); // should be *NEVER* + } + + public function returnVoid(): void { + return; + } +} + + +class FooBoolString +{ + + public string $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType("''|'1'", $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType("'1'", $this->foo); + $this->foo = false; + assertType("''", $this->foo); + } +} + +class FooIntString +{ + + public string $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = -1; + assertType("'-1'", $this->foo); + $this->foo = 1; + assertType("'1'", $this->foo); + $this->foo = 0; + assertType("'0'", $this->foo); + } +} + +class FooIntBool +{ + + public bool $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('bool', $this->foo); + + if ($b !== 0) { + $this->foo = $b; + assertType('true', $this->foo); + } + if ($b !== 1) { + $this->foo = $b; + assertType('bool', $this->foo); + } + } + + public function doBar(): void + { + $this->foo = -1; + assertType("true", $this->foo); + $this->foo = 1; + assertType("true", $this->foo); + $this->foo = 0; + assertType("false", $this->foo); + } +} + +class FooIntRangeString +{ + + public string $foo; + + /** + * @param int<5, 10> $b + */ + public function doFoo(int $b): void + { + $this->foo = $b; + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); + } + + public function doBar(): void + { + $i = rand(5, 10); + $this->foo = $i; + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); + } +} + +class FooNullableIntString +{ + + public string $foo; + + public function doFoo(?int $b): void + { + $this->foo = $b; + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = null; + assertType('*NEVER*', $this->foo); // null cannot be coerced to string, see https://3v4l.org/5k1Dl + } +} + +class FooFloatString +{ + + public string $foo; + + public function doFoo(float $b): void + { + $this->foo = $b; + assertType('numeric-string&uppercase-string', $this->foo); + } + + public function doBar(): void + { + $this->foo = 1.0; + assertType("'1'", $this->foo); + } +} + +class FooStringToUnion +{ + + public int|float $foo; + + public function doFoo(string $b): void + { + $this->foo = $b; + assertType('float|int', $this->foo); + } + + public function doBar(): void + { + $this->foo = "1.0"; + assertType('1|1.0', $this->foo); + } +} + +class FooNumericToString +{ + + public string $foo; + + public function doFoo(float|int $b): void + { + $this->foo = $b; + assertType('numeric-string&uppercase-string', $this->foo); + } + +} + +class FooMixedToInt +{ + + public int $foo; + + public function doFoo(mixed $b): void + { + $this->foo = $b; + assertType('int', $this->foo); + } + +} + + +class FooArrayToInt +{ + public int $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToFloat +{ + public float $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToString +{ + public string $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArray +{ + public array $foo; + + /** + * @param non-empty-array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('non-empty-array', $this->foo); + + if (array_key_exists('foo', $arr)) { + $this->foo = $arr; + assertType("non-empty-array&hasOffset('foo')", $this->foo); + } + + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + $this->foo = $arr; + assertType("non-empty-array&hasOffsetValue('foo', 'bar')", $this->foo); + } + } +} + +class FooTypedArray +{ + /** + * @var array + */ + public array $foo; + + /** + * @param array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } + + /** + * @param array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } +} + +class FooList +{ + public array $foo; + + /** + * @param non-empty-list $list + */ + public function doFoo(array $list): void + { + $this->foo = $list; + assertType('non-empty-list', $this->foo); + + if (array_key_exists(3, $list)) { + $this->foo = $list; + assertType("non-empty-list&hasOffset(3)", $this->foo); + } + + if (array_key_exists(3, $list) && is_string($list[3])) { + $this->foo = $list; + assertType("non-empty-list&hasOffsetValue(3, string)", $this->foo); + } + } + +} + +// https://3v4l.org/LJiRB +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('callable-string|non-empty-string', $this->foo); + } +} + +// https://3v4l.org/VvUsp +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + +class StringableFoo { + private string $foo; + + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('string', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} + +final class NotStringable {} + +class ObjectWithToStringMethod { + private string $foo; + + public function doFoo(object $foo): void { + if (method_exists($foo, '__toString')) { + $this->foo = $foo; + assertType('string', $this->foo); + } + } + public function __toString(): string { + return 'Foo'; + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php new file mode 100644 index 0000000000..9618bc818f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +interface ObjectDataMapper +{ + /** + * @template OutType of object + * + * @param literal-string&class-string $class + * @param mixed $data + * + * @return OutType + * + * @throws \Exception + */ + public function map(string $class, $data): object; +} + +final class ApiProductController +{ + + protected ?SearchProductsVM $searchProductsVM = null; + + protected static ?SearchProductsVM $searchProductsVMStatic = null; + + public function search(ObjectDataMapper $dataMapper): void + { + $this->searchProductsVM = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', $this->searchProductsVM); + } + + public function searchStatic(ObjectDataMapper $dataMapper): void + { + self::$searchProductsVMStatic = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', self::$searchProductsVMStatic); + } +} + +class SearchProductsVM {} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 68cd2cc059..4c27bfd80f 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1016,4 +1016,9 @@ public function testBug11019(): void $this->analyse([__DIR__ . '/data/bug-11019.php'], []); } + public function testBug12946(): void + { + $this->analyse([__DIR__ . '/data/bug-12946.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12946.php b/tests/PHPStan/Rules/Comparison/data/bug-12946.php new file mode 100644 index 0000000000..895b0573bf --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12946.php @@ -0,0 +1,37 @@ += 8.1 + +namespace Bug12946; + +interface UserInterface {} +class User implements UserInterface{} + +class UserMapper { + function getFromId(int $id) : ?UserInterface { + return $id === 10 ? new User : null; + } +} + +class GetUserCommand { + + private ?UserInterface $currentUser = null; + + public function __construct( + private readonly UserMapper $userMapper, + private readonly int $id, + ) { + } + + public function __invoke() : UserInterface { + if( $this->currentUser ) { + return $this->currentUser; + } + + $this->currentUser = $this->userMapper->getFromId($this->id); + if( $this->currentUser === null ) { + throw new \Exception; + } + + return $this->currentUser; + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index bc177e9d0b..decb237d34 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3616,4 +3616,14 @@ public function testBug12880(): void $this->analyse([__DIR__ . '/data/bug-12880.php'], []); } + public function testBug12940(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12940.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12940.php b/tests/PHPStan/Rules/Methods/data/bug-12940.php new file mode 100644 index 0000000000..ad00e11c1b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12940.php @@ -0,0 +1,43 @@ + $className + * @return T + */ + public static function makeInstance(string $className, mixed ...$args): object + { + return new $className(...$args); + } +} + +class PageRenderer +{ + public function setTemplateFile(string $path): void + { + } + + public function setLanguage(string $lang): void + { + } +} + +class TypoScriptFrontendController +{ + + protected ?PageRenderer $pageRenderer = null; + + public function initializePageRenderer(): void + { + if ($this->pageRenderer !== null) { + return; + } + $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); + $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html'); + $this->pageRenderer->setLanguage('DE'); + } +}