Skip to content

Fix forgetting static property access after impure call #3950

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

Open
wants to merge 6 commits into
base: 2.1.x
Choose a base branch
from

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Apr 19, 2025

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

This pull request has been marked as ready for review.

Comment on lines +5 to +24
class HelloWorld
{
public static ?HelloWorld $instance = null;

public function save(): void {
self::$instance = new HelloWorld();

$callback = static function(): void {
self::$instance = null;
};

$callback();

var_dump(self::$instance);

self::$instance?->save();
}
}

(new HelloWorld())->save();
Copy link
Contributor Author

@staabm staabm Apr 19, 2025

Choose a reason for hiding this comment

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

there is a similar case where we still fail:

<?php declare(strict_types = 1);

class HelloWorld
{
	public static ?HelloWorld $instance = null;
	
    public function save(): void {
        HelloWorld::$instance = new HelloWorld();

        $callback = static function(): void {
			HelloWorld::$instance = null;
		};
		
		$callback();
			
		var_dump(HelloWorld::$instance);
		
        HelloWorld::$instance?->save();
    }
}

(new HelloWorld())->save();

note: instead of self in this case the FQCN is used.

maybe we should normalize FQCN globally in AST to self in such cases, so we don't need to handle this case all over the place?

@@ -2597,6 +2597,13 @@ static function (): void {
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
}

if (
$parametersAcceptor instanceof ClosureType && $parametersAcceptor->getReturnType()->isVoid()->yes()
Copy link
Contributor Author

@staabm staabm Apr 19, 2025

Choose a reason for hiding this comment

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

to be consistent with MethodCall, StaticCall we could add

$functionReflection !== null && $functionReflection->hasSideEffects()->yes()

here, but this would regress some types:

There were 3 failures:

1) PHPStan\Analyser\LegacyNodeScopeResolverTest::testForeachLoopVariables with data set #19 ('array<string, 1|2|3>', '$this->property', ''afterLoop'')
$this->property at 'afterLoop'
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'array<string, 1|2|3>'
+'array<int>'

/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9696
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9590
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9597
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:7092

2) PHPStan\Analyser\LegacyNodeScopeResolverTest::testForeachLoopVariables with data set #17 ('array<string, 1|2|3>', '$this->property', ''begin'')
$this->property at 'begin'
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'array<string, 1|2|3>'
+'array<int>'

/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9696
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9590
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9597
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:7092

3) PHPStan\Analyser\LegacyNodeScopeResolverTest::testForeachLoopVariables with data set #18 ('non-empty-array<string, 1|2|3>', '$this->property', ''end'')
$this->property at 'end'
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'non-empty-array<string, 1|2|3>'
+'non-empty-array<int>'

/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9696
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9590
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:9597
/Users/staabm/workspace/phpstan-src/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php:7092

FAILURES!                                                  
Tests: 12743, Assertions: 85340, Failures: 3, Skipped: 139.

wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

For closures we have already analysed the body, there's ClosureType::getInvalidateExpressions() so we know exactly what to invalidate.

Copy link
Contributor Author

@staabm staabm Apr 19, 2025

Choose a reason for hiding this comment

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

for the example in tests/PHPStan/Rules/Methods/data/bug-8523b.php ClosureType::getInvalidateExpressions() returns an empty array.

when searching for new InvalidateExprNode in the whole codebase it looks like only MethodCall and Arg->value is considered a invalidate-expression. Should I add PropertyFetch and StaticPropertyFetch and StaticCall?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I just mixed invalidating expressions with impure points. Please check ClosureType::getImpurePoints() to see whether closure is pure or not.

@@ -2597,6 +2597,13 @@ static function (): void {
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
}

if (
$parametersAcceptor instanceof ClosureType && $parametersAcceptor->getReturnType()->isVoid()->yes()
Copy link
Member

Choose a reason for hiding this comment

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

For closures we have already analysed the body, there's ClosureType::getInvalidateExpressions() so we know exactly what to invalidate.

if (
$exprStringToInvalidate === '$this'
&& $node instanceof Name
&& in_array($node->toLowerString(), ['self', 'static', 'parent'], true)
Copy link
Member

Choose a reason for hiding this comment

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

I just realized the self here should be improved. We should use Scope::resolveName or resolveTypeByName and check that the class name matches the current scope class name. Because self is just an alias to the current class name, so whether we're using self:: or Foo:: shouldn't matter.

@@ -2597,6 +2597,13 @@ static function (): void {
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
}

if (
$parametersAcceptor instanceof ClosureType && $parametersAcceptor->getReturnType()->isVoid()->yes()
Copy link
Member

Choose a reason for hiding this comment

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

Oh, I just mixed invalidating expressions with impure points. Please check ClosureType::getImpurePoints() to see whether closure is pure or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants