Skip to content
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ parameters:
internalTag: true
newStaticInAbstractClassStaticMethod: true
checkExtensionsForComparisonOperators: true
reportTooWideBool: true
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ parameters:
internalTag: false
newStaticInAbstractClassStaticMethod: false
checkExtensionsForComparisonOperators: false
reportTooWideBool: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ parametersSchema:
internalTag: bool()
newStaticInAbstractClassStaticMethod: bool()
checkExtensionsForComparisonOperators: bool()
reportTooWideBool: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ public function stream_eof(): bool
return $this->readFromFile;
}

/**
* @return true
*/
public function stream_flush(): bool
{
return true;
Expand Down Expand Up @@ -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
{
Expand Down
7 changes: 6 additions & 1 deletion src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\ClassPropertiesNode;
use PHPStan\Reflection\PropertyReflection;
Expand All @@ -26,6 +27,8 @@ public function __construct(
private ReadWritePropertiesExtensionProvider $extensionProvider,
private PropertyReflectionFinder $propertyReflectionFinder,
private TooWideTypeCheck $check,
#[AutowiredParameter(ref: '%featureToggles.reportTooWideBool%')]
private bool $reportTooWideBool,
)
{
}
Expand Down Expand Up @@ -58,7 +61,9 @@ public function processNode(Node $node, Scope $scope): array
$propertyReflection = $classReflection->getNativeProperty($propertyName);
$propertyType = $propertyReflection->getWritableType();
if (!$propertyType instanceof UnionType) {
continue;
if (!$propertyType->isBoolean()->yes() || !$this->reportTooWideBool) {
continue;
}
}
foreach ($this->extensionProvider->getExtensions() as $extension) {
if ($extension->isAlwaysRead($propertyReflection, $propertyName)) {
Expand Down
21 changes: 17 additions & 4 deletions src/Rules/TooWideTypehints/TooWideTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\TooWideTypehints;

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Node\FunctionReturnStatementsNode;
Expand All @@ -21,20 +22,28 @@
final class TooWideTypeCheck
{

public function __construct(
#[AutowiredParameter(ref: '%featureToggles.reportTooWideBool%')]
private bool $reportTooWideBool,
)
{
}

/**
* @return list<IdentifierRuleError>
*/
public function checkProperty(
ClassPropertyNode $property,
UnionType $propertyType,
Type $propertyType,
string $propertyDescription,
Type $assignedType,
): array
{
$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;
}
Expand Down Expand Up @@ -68,8 +77,11 @@ public function checkFunction(
): array
{
$functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType);

if (!$functionReturnType instanceof UnionType) {
return [];
if (!$functionReturnType->isBoolean()->yes() || !$this->reportTooWideBool) {
return [];
}
}
$statementResult = $node->getStatementResult();
if ($statementResult->hasYield()) {
Expand Down Expand Up @@ -114,7 +126,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;
}
Expand Down
36 changes: 23 additions & 13 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,11 +440,13 @@ public function testBug4715(): void
public function testBug4734(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php');
$this->assertCount(3, $errors);
$this->assertCount(5, $errors); // could be 3
Copy link
Contributor Author

@staabm staabm Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a topic for a followup: NodeScopeResolver's ClassStatementsGatherer works on a per class level, which means access to private properties from a closure-scope (from within a different class) are not seen when analyzing the class which declares the property


$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 it can be removed from the property type.', $errors[0]->getMessage()); // should not error
$this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so it can be removed from the property type.', $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
Expand Down Expand Up @@ -1096,15 +1098,23 @@ public function testBug8376(): void
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 it can be removed from the return type.', $errors[0]->getMessage());
$this->assertSame(17, $errors[0]->getLine());
$this->assertSame('Function AssertDocblock\validateStringArrayIfFalse() never returns true so it can be removed from the return type.', $errors[1]->getMessage());
$this->assertSame(25, $errors[1]->getLine());
$this->assertSame('Function AssertDocblock\validateStringOrIntArray() never returns true so it can be removed from the return type.', $errors[2]->getMessage());
$this->assertSame(34, $errors[2]->getLine());
$this->assertSame('Function AssertDocblock\validateStringOrNonEmptyIntArray() never returns true so it can be removed from the return type.', $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')]
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
class LogicalXorConstantConditionRuleTest extends RuleTestCase
{

private bool $reportAlwaysTrueInLastCondition = false;

protected function getRule(): TRule
{
return new LogicalXorConstantConditionRule(
Expand All @@ -26,7 +24,7 @@ protected function getRule(): TRule
$this->shouldTreatPhpDocTypesAsCertain(),
),
$this->shouldTreatPhpDocTypesAsCertain(),
$this->reportAlwaysTrueInLastCondition,
false,
true,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase
protected function getRule(): Rule
{
return new TooWideArrowFunctionReturnTypehintRule(
new TooWideTypeCheck(),
new TooWideTypeCheck(true),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase
protected function getRule(): Rule
{
return new TooWideClosureReturnTypehintRule(
new TooWideTypeCheck(),
new TooWideTypeCheck(true),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,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
Expand Down Expand Up @@ -66,4 +68,24 @@ public function testBug10312a(): void
$this->analyse([__DIR__ . '/data/bug-10312a.php'], []);
}

public function testBug13384c(): void
{
$this->reportTooWideBool = true;
$this->analyse([__DIR__ . '/data/bug-13384c.php'], [
[
'Function Bug13384c\doFoo() never returns true so it can be removed from the return type.',
5,
],
[
'Function Bug13384c\doFoo2() never returns false so it can be removed from the return type.',
9,
],
]);
}

public function testBug13384cOff(): void
{
$this->analyse([__DIR__ . '/data/bug-13384c.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -218,4 +220,32 @@ public function testBug10312d(): void
$this->analyse([__DIR__ . '/data/bug-10312d.php'], []);
}

public function testBug13384c(): void
{
$this->reportTooWideBool = true;
$this->analyse([__DIR__ . '/data/bug-13384c.php'], [
[
'Method Bug13384c\Bug13384c::doBar() never returns true so it can be removed from the return type.',
33,
],
[
'Method Bug13384c\Bug13384c::doBar2() never returns false so it can be removed from the return type.',
37,
],
[
'Method Bug13384c\Bug13384Static::doBar() never returns true so it can be removed from the return type.',
50,
],
[
'Method Bug13384c\Bug13384Static::doBar2() never returns false so it can be removed from the return type.',
54,
],
]);
}

public function testBug13384cOff(): void
{
$this->analyse([__DIR__ . '/data/bug-13384c.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
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),
$this->reportTooWideBool,
);
}

Expand Down Expand Up @@ -59,4 +62,30 @@ public function testBug11667(): void
$this->analyse([__DIR__ . '/data/bug-11667.php'], []);
}

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 it can be removed from the property type.',
9,
],
[
'Static property Bug13384\ShutdownHandlerTrueDefault::$registered (bool) is never assigned false so it can be removed from the property type.',
34,
],
]);
}

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'], []);
}

}
Loading
Loading