Skip to content

Don't remember non-nullable properties as nullable #3943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Apr 17, 2025

as it stands right now we only narrow property types from assigments when declare(strict-types=1) because doing so in non-strict mode is very complicated.

I think we can still handle the very common case of non-nullable vs. nullable in non-strict mode as this should work independent from strict-types.

I am not interessted in going down the rabbit hole for all possible types, but I think supporting null brings us already a big step forward

similar to phpstan/phpstan#12393
inspired by phpstan/phpstan#12889 (comment)

Comment on lines 8 to 27
class KeepsPropertyNonNullable {
private readonly int $i;

public function __construct()
{
$this->i = getIntOrNull();
}

public function doFoo(): void {
assertType('int', $this->i);
assertNativeType('int', $this->i);
}
}

function getIntOrNull(): ?int {
if (rand(0, 1) === 0) {
return null;
}
return 1;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

main motivation was this case here, as with types remembered from constructors we otherwise get a lot of followup errors in instance methods

https://phpstan.org/r/40dfb9a5-a8e9-49c5-80e8-36affa12bda6

@staabm staabm marked this pull request as ready for review April 17, 2025 12:45
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.


$scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType));
if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) {
Copy link
Member

Choose a reason for hiding this comment

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

Please restructure this code. I don't really like two following if statements to repeat the first part of the condition.

					if ($propertyReflection->hasNativeType() && $propertyNativeType->isNull()->no()) {
						$assignedExprType = TypeCombinator::removeNull($assignedExprType);
						$assignedNativeType = TypeCombinator::removeNull($assignedNativeType);
					}

						$scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType));
					if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) {

should become:

if ($propertyReflection->hasNativeType()) {
    if ($scope->isDeclareStrictTypes()) {
        ....
    } elseif ($propertyNativeType->isNull()->no()) {
        ....
    }
}

I'm still not sure whether this is a good idea. I first need to have this code restructured so I can think about it clearly. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

restructured, but its not

if ($propertyReflection->hasNativeType()) {
    if ($scope->isDeclareStrictTypes()) {
        ....
    } elseif ($propertyNativeType->isNull()->no()) {
        ....
    }
}

because we want $propertyNativeType->isNull()->no() also be considered when strict-types=1

Copy link
Member

Choose a reason for hiding this comment

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

Please submit a separate PR with test + fix what this code currently achieves with declare(strict_types = 1). I'm trying to understand it. I don't think there's a present bug about that right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just realized you are right. I can re-structure like you suggested, as there is no bug with strict-types=1 and my fix is only useful for strict-types=0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found a bug unrelated to property nullability and created a reported: phpstan/phpstan#12902

@@ -398,7 +398,7 @@ private static function isFileLintSkipped(string $file): bool

@fclose($f);

if (preg_match('~<?php\\s*\\/\\/\s*lint\s*([^\d\s]+)\s*([^\s]+)\s*~i', $firstLine, $m) === 1) {
if (preg_match('~<?php\\s*(?:declare\\s*\([^)]+\)\\s*;\\s*)?\\/\\/\s*lint\s*([^\d\s]+)\s*([^\s]+)\s*~i', $firstLine, $m) === 1) {
Copy link
Contributor Author

@staabm staabm Apr 18, 2025

Choose a reason for hiding this comment

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

added support for <?php declare(…); // lint >= 8.1-format in gatherAssertTypesFromDirectory

@staabm
Copy link
Contributor Author

staabm commented Apr 19, 2025

looks like we have a better alternative implementation in #3945

@staabm staabm closed this Apr 19, 2025
@staabm staabm deleted the test-prop branch April 19, 2025 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants