diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 8cccad6c7e..b5f69aee86 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -10,3 +10,4 @@ parameters: internalTag: true newStaticInAbstractClassStaticMethod: true checkExtensionsForComparisonOperators: true + reportTooWideBool: true diff --git a/conf/config.neon b/conf/config.neon index 9d7c9e061b..d2bd278a3c 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -34,6 +34,7 @@ parameters: internalTag: false newStaticInAbstractClassStaticMethod: false checkExtensionsForComparisonOperators: false + reportTooWideBool: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 73e7f56b0f..eb7b172709 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -37,6 +37,7 @@ parametersSchema: internalTag: bool() newStaticInAbstractClassStaticMethod: bool() checkExtensionsForComparisonOperators: bool() + reportTooWideBool: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 96bf233209..cc2ea94c98 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -43,4 +43,9 @@ public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsTrueAndFalseStandaloneType(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php index 4a35d07ca2..047a6808e2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -212,6 +212,9 @@ public function stream_eof(): bool return $this->readFromFile; } + /** + * @return true + */ public function stream_flush(): bool { return true; @@ -254,6 +257,8 @@ public function stream_seek($offset, $whence): bool * @param int $option * @param int $arg1 * @param int $arg2 + * + * @return false */ public function stream_set_option($option, $arg1, $arg2): bool { diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 738203a6bc..d52d2e1238 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -31,15 +31,16 @@ public function processNode(Node $node, Scope $scope): array { $function = $node->getFunctionReflection(); - $functionReturnType = $function->getReturnType(); return $this->check->checkFunction( $node, - $functionReturnType, + $function->getReturnType(), + $function->getPhpDocReturnType(), sprintf( 'Function %s()', $function->getName(), ), false, + $scope, ); } diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 6b2c8f81b1..b14b7dcb81 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -49,16 +49,17 @@ public function processNode(Node $node, Scope $scope): array } } - $methodReturnType = $method->getReturnType(); return $this->check->checkFunction( $node, - $methodReturnType, + $method->getReturnType(), + $method->getPhpDocReturnType(), sprintf( 'Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName(), ), !$isFirstDeclaration && !$method->isPrivate(), + $scope, ); } diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index 4be9a14c9f..9c6fcee1bf 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; use function count; use function sprintf; @@ -57,9 +56,13 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection = $classReflection->getNativeProperty($propertyName); $propertyType = $propertyReflection->getWritableType(); - if (!$propertyType instanceof UnionType) { + $phpdocType = $propertyReflection->getPhpDocType(); + + $propertyType = $this->check->findTypeToCheck($propertyType, $phpdocType, $scope); + if ($propertyType === null) { continue; } + foreach ($this->extensionProvider->getExtensions() as $extension) { if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { continue 2; diff --git a/src/Rules/TooWideTypehints/TooWideTypeCheck.php b/src/Rules/TooWideTypehints/TooWideTypeCheck.php index 8ea9a0cea4..cd07f72145 100644 --- a/src/Rules/TooWideTypehints/TooWideTypeCheck.php +++ b/src/Rules/TooWideTypehints/TooWideTypeCheck.php @@ -2,14 +2,18 @@ namespace PHPStan\Rules\TooWideTypehints; +use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -21,12 +25,19 @@ final class TooWideTypeCheck { + public function __construct( + #[AutowiredParameter(ref: '%featureToggles.reportTooWideBool%')] + private bool $reportTooWideBool, + ) + { + } + /** * @return list */ public function checkProperty( ClassPropertyNode $property, - UnionType $propertyType, + Type $propertyType, string $propertyDescription, Type $assignedType, ): array @@ -34,7 +45,8 @@ public function checkProperty( $errors = []; $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType); - foreach ($propertyType->getTypes() as $type) { + $propertyTypes = $propertyType instanceof UnionType ? $propertyType->getTypes() : $propertyType->getFiniteTypes(); + foreach ($propertyTypes as $type) { if (!$type->isSuperTypeOf($assignedType)->no()) { continue; } @@ -43,6 +55,23 @@ public function checkProperty( continue; } + if ($propertyType->isBoolean()->yes()) { + $suggestedType = $type->isTrue()->yes() ? new ConstantBooleanType(false) : new ConstantBooleanType(true); + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s (%s) is never assigned %s so the property type can be changed to %s.', + $propertyDescription, + $propertyType->describe($verbosityLevel), + $type->describe($verbosityLevel), + $suggestedType->describe($verbosityLevel), + )) + ->identifier('property.tooWideBool') + ->line($property->getStartLine()) + ->build(); + + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( '%s (%s) is never assigned %s so it can be removed from the property type.', $propertyDescription, @@ -62,15 +91,18 @@ public function checkProperty( */ public function checkFunction( MethodReturnStatementsNode|FunctionReturnStatementsNode $node, - Type $functionReturnType, + Type $nativeFunctionReturnType, + Type $phpdocFunctionReturnType, string $functionDescription, bool $checkDescendantClass, + Scope $scope, ): array { - $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); - if (!$functionReturnType instanceof UnionType) { + $functionReturnType = $this->findTypeToCheck($nativeFunctionReturnType, $phpdocFunctionReturnType, $scope); + if ($functionReturnType === null) { return []; } + $statementResult = $node->getStatementResult(); if ($statementResult->hasYield()) { return []; @@ -114,7 +146,8 @@ public function checkFunction( } $messages = []; - foreach ($functionReturnType->getTypes() as $type) { + $functionReturnTypes = $functionReturnType instanceof UnionType ? $functionReturnType->getTypes() : $functionReturnType->getFiniteTypes(); + foreach ($functionReturnTypes as $type) { if (!$type->isSuperTypeOf($returnType)->no()) { continue; } @@ -129,6 +162,19 @@ public function checkFunction( } } + if ($functionReturnType->isBoolean()->yes()) { + $suggestedType = $type->isTrue()->yes() ? new ConstantBooleanType(false) : new ConstantBooleanType(true); + + $messages[] = RuleErrorBuilder::message(sprintf( + '%s never returns %s so the return type can be changed to %s.', + $functionDescription, + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + $suggestedType->describe(VerbosityLevel::getRecommendedLevelByType($suggestedType)), + ))->identifier('return.tooWideBool')->build(); + + continue; + } + $messages[] = RuleErrorBuilder::message(sprintf( '%s never returns %s so it can be removed from the return type.', $functionDescription, @@ -165,4 +211,46 @@ public function checkAnonymousFunction( return $messages; } + /** + * Returns null when type should not be checked, e.g. because it would be too annoying. + */ + public function findTypeToCheck( + Type $nativeType, + Type $phpdocType, + Scope $scope, + ): ?Type + { + $combinedType = TypeUtils::resolveLateResolvableTypes(TypehintHelper::decideType($nativeType, $phpdocType)); + if ($combinedType instanceof UnionType) { + return $combinedType; + } + + if (!$this->reportTooWideBool) { + return null; + } + + if ( + $phpdocType->isBoolean()->yes() + ) { + if ( + !$phpdocType->isTrue()->yes() + && !$phpdocType->isFalse()->yes() + ) { + return $combinedType; + } + } elseif ( + $scope->getPhpVersion()->supportsTrueAndFalseStandaloneType()->yes() + && $nativeType->isBoolean()->yes() + ) { + if ( + !$nativeType->isTrue()->yes() + && !$nativeType->isFalse()->yes() + ) { + return $combinedType; + } + } + + return null; + } + } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index f6d7dceff2..8876db5af4 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -437,14 +437,17 @@ public function testBug4715(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.2')] public function testBug4734(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); - $this->assertCount(3, $errors); + $this->assertCount(5, $errors); // could be 3 - $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[0]->getMessage()); - $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[1]->getMessage()); - $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[2]->getMessage()); + $this->assertSame('Static property Bug4734\Foo::$httpMethodParameterOverride (bool) is never assigned false so the property type can be changed to true.', $errors[0]->getMessage()); // should not error + $this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so the property type can be changed to true.', $errors[1]->getMessage()); // should not error + $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[2]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[3]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[4]->getMessage()); } public function testBug5231(): void @@ -1092,19 +1095,27 @@ public function testBug8376(): void $this->assertNoErrors($errors); } - #[RequiresPhp('>= 8.0')] + #[RequiresPhp('>= 8.2')] public function testAssertDocblock(): void { $errors = $this->runAnalyse(__DIR__ . '/nsrt/assert-docblock.php'); - $this->assertCount(4, $errors); - $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[0]->getMessage()); - $this->assertSame(218, $errors[0]->getLine()); - $this->assertSame('Call to method AssertDocblock\A::testNotInt() with string will always evaluate to true.', $errors[1]->getMessage()); - $this->assertSame(224, $errors[1]->getLine()); - $this->assertSame('Call to method AssertDocblock\A::testInt() with int will always evaluate to true.', $errors[2]->getMessage()); - $this->assertSame(232, $errors[2]->getLine()); - $this->assertSame('Call to method AssertDocblock\A::testNotInt() with int will always evaluate to false.', $errors[3]->getMessage()); - $this->assertSame(238, $errors[3]->getLine()); + $this->assertCount(8, $errors); + $this->assertSame('Function AssertDocblock\validateStringArrayIfTrue() never returns false so the return type can be changed to true.', $errors[0]->getMessage()); + $this->assertSame(17, $errors[0]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringArrayIfFalse() never returns true so the return type can be changed to false.', $errors[1]->getMessage()); + $this->assertSame(25, $errors[1]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringOrIntArray() never returns true so the return type can be changed to false.', $errors[2]->getMessage()); + $this->assertSame(34, $errors[2]->getLine()); + $this->assertSame('Function AssertDocblock\validateStringOrNonEmptyIntArray() never returns true so the return type can be changed to false.', $errors[3]->getMessage()); + $this->assertSame(44, $errors[3]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[4]->getMessage()); + $this->assertSame(218, $errors[4]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with string will always evaluate to true.', $errors[5]->getMessage()); + $this->assertSame(224, $errors[5]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with int will always evaluate to true.', $errors[6]->getMessage()); + $this->assertSame(232, $errors[6]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with int will always evaluate to false.', $errors[7]->getMessage()); + $this->assertSame(238, $errors[7]->getLine()); } #[RequiresPhp('>= 8.0')] diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index eee1500835..2bf3017098 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -182,4 +182,16 @@ public function testBug8926(): void $this->analyse([__DIR__ . '/data/bug-8926.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug13384b(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../TooWideTypehints/data/bug-13384b.php'], [ + [ + 'If condition is always false.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index 99e45cf088..7f9146b058 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -11,8 +11,6 @@ class LogicalXorConstantConditionRuleTest extends RuleTestCase { - private bool $reportAlwaysTrueInLastCondition = false; - protected function getRule(): TRule { return new LogicalXorConstantConditionRule( @@ -26,7 +24,7 @@ protected function getRule(): TRule $this->shouldTreatPhpDocTypesAsCertain(), ), $this->shouldTreatPhpDocTypesAsCertain(), - $this->reportAlwaysTrueInLastCondition, + false, true, ); } diff --git a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php index 8de4ae7bed..58aea82f3a 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideFunctionThrowTypeRuleTest.php @@ -11,11 +11,9 @@ class TooWideFunctionThrowTypeRuleTest extends RuleTestCase { - private bool $implicitThrows = true; - protected function getRule(): Rule { - return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck($this->implicitThrows)); + return new TooWideFunctionThrowTypeRule(new TooWideThrowTypeCheck(true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php index da40cc6d80..2db813da37 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWidePropertyHookThrowTypeRuleTest.php @@ -13,11 +13,9 @@ class TooWidePropertyHookThrowTypeRuleTest extends RuleTestCase { - private bool $implicitThrows = true; - protected function getRule(): Rule { - return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck($this->implicitThrows)); + return new TooWidePropertyHookThrowTypeRule(self::getContainer()->getByType(FileTypeMapper::class), new TooWideThrowTypeCheck(true)); } #[RequiresPhp('>= 8.4')] diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index daaac7cb2a..e41af50e31 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -14,7 +14,7 @@ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { return new TooWideArrowFunctionReturnTypehintRule( - new TooWideTypeCheck(), + new TooWideTypeCheck(true), ); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index e7dfbc6d94..33700aacc3 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -14,7 +14,7 @@ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { return new TooWideClosureReturnTypehintRule( - new TooWideTypeCheck(), + new TooWideTypeCheck(true), ); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index cce5fd6e0e..231eecf289 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -11,9 +12,11 @@ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase { + private bool $reportTooWideBool = false; + protected function getRule(): Rule { - return new TooWideFunctionReturnTypehintRule(new TooWideTypeCheck()); + return new TooWideFunctionReturnTypehintRule(new TooWideTypeCheck($this->reportTooWideBool)); } public function testRule(): void @@ -66,4 +69,49 @@ public function testBug10312a(): void $this->analyse([__DIR__ . '/data/bug-10312a.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug13384cPhp82(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Function Bug13384c\doFoo() never returns true so the return type can be changed to false.', + 5, + ], + [ + 'Function Bug13384c\doFoo2() never returns false so the return type can be changed to true.', + 9, + ], + [ + 'Function Bug13384c\doFooPhpdoc() never returns false so the return type can be changed to true.', + 93, + ], + [ + 'Function Bug13384c\doFooPhpdoc2() never returns true so the return type can be changed to false.', + 100, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384cPrePhp82(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Function Bug13384c\doFooPhpdoc() never returns false so the return type can be changed to true.', + 93, + ], + [ + 'Function Bug13384c\doFooPhpdoc2() never returns true so the return type can be changed to false.', + 100, + ], + ]); + } + + public function testBug13384cOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384c.php'], []); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 677e4570be..bd74b550ed 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -15,9 +15,11 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase private bool $checkProtectedAndPublicMethods = true; + private bool $reportTooWideBool = false; + protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideTypeCheck()); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideTypeCheck($this->reportTooWideBool)); } public function testPrivate(): void @@ -218,4 +220,57 @@ public function testBug10312d(): void $this->analyse([__DIR__ . '/data/bug-10312d.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug13384c(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Method Bug13384c\Bug13384c::doBar() never returns true so the return type can be changed to false.', + 33, + ], + [ + 'Method Bug13384c\Bug13384c::doBar2() never returns false so the return type can be changed to true.', + 37, + ], + [ + 'Method Bug13384c\Bug13384c::doBarPhpdoc() never returns false so the return type can be changed to true.', + 55, + ], + [ + 'Method Bug13384c\Bug13384Static::doBar() never returns true so the return type can be changed to false.', + 62, + ], + [ + 'Method Bug13384c\Bug13384Static::doBar2() never returns false so the return type can be changed to true.', + 66, + ], + [ + 'Method Bug13384c\Bug13384Static::doBarPhpdoc() never returns false so the return type can be changed to true.', + 84, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384cPrePhp82(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384c.php'], [ + [ + 'Method Bug13384c\Bug13384c::doBarPhpdoc() never returns false so the return type can be changed to true.', + 55, + ], + [ + 'Method Bug13384c\Bug13384Static::doBarPhpdoc() never returns false so the return type can be changed to true.', + 84, + ], + ]); + } + + public function testBug13384cOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384c.php'], []); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php index e590aea5eb..ea7c4464c5 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -14,12 +14,14 @@ class TooWidePropertyTypeRuleTest extends RuleTestCase { + private bool $reportTooWideBool = false; + protected function getRule(): Rule { return new TooWidePropertyTypeRule( new DirectReadWritePropertiesExtensionProvider([]), new PropertyReflectionFinder(), - new TooWideTypeCheck(), + new TooWideTypeCheck($this->reportTooWideBool), ); } @@ -59,4 +61,49 @@ public function testBug11667(): void $this->analyse([__DIR__ . '/data/bug-11667.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug13384(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384.php'], [ + [ + 'Static property Bug13384\ShutdownHandlerFalseDefault::$registered (bool) is never assigned true so the property type can be changed to false.', + 9, + ], + [ + 'Static property Bug13384\ShutdownHandlerTrueDefault::$registered (bool) is never assigned false so the property type can be changed to true.', + 34, + ], + ]); + } + + #[RequiresPhp('< 8.2')] + public function testBug13384PrePhp82(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384.php'], []); + } + + public function testBug13384Phpdoc(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384-phpdoc.php'], [ + [ + 'Static property Bug13384Phpdoc\ShutdownHandlerPhpdocTypes::$registered (bool) is never assigned true so the property type can be changed to false.', + 12, + ], + ]); + } + + public function testBug13384b(): void + { + $this->reportTooWideBool = true; + $this->analyse([__DIR__ . '/data/bug-13384b.php'], []); + } + + public function testBug13384bOff(): void + { + $this->analyse([__DIR__ . '/data/bug-13384b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384-phpdoc.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384-phpdoc.php new file mode 100644 index 0000000000..49a4287931 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384-phpdoc.php @@ -0,0 +1,33 @@ += 8.2 + +declare(strict_types=1); + +namespace Bug13384b; + +use function register_shutdown_function; + +final class ShutdownHandlerFooBar +{ + private static false $registered = false; + private static string $message = ''; + + public static function setMessage(string $message): void + { + self::register(); + + self::$message = $message; + } + + private static function register(): void + { + if (self::$registered) { + return; + } + + register_shutdown_function(static function (): void + { + print self::$message; + }); + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php new file mode 100644 index 0000000000..0d0ad4efbf --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-13384c.php @@ -0,0 +1,106 @@ +checkMaybeUndefinedVariables); + return new CompactVariablesRule(true); } public function testCompactVariables(): void { - $this->checkMaybeUndefinedVariables = true; $this->analyse([__DIR__ . '/data/compact-variables.php'], [ [ 'Call to function compact() contains undefined variable $bar.',