Skip to content

Fix phpstan/phpstan#9181: Incorrect assertion of value of object across function call boundary#5106

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-aw8jftu
Open

Fix phpstan/phpstan#9181: Incorrect assertion of value of object across function call boundary#5106
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-aw8jftu

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a stdClass object created via (object)[] cast is passed to a function/method with side effects, PHPStan incorrectly preserved the specific property types from the cast. This led to false positives like "Strict comparison using !== between null and null will always evaluate to false" because PHPStan thought $data->search was still null even after the object was passed to a function that could have modified it.

Changes

  • Added widenUniversalObjectCrateProperties() method to src/Analyser/NodeScopeResolver.php that widens stdClass types back to their base ObjectType('stdClass') after argument invalidation
  • Called this method after invalidateExpression() in the argument processing code of processArgs()
  • Added regression test in tests/PHPStan/Analyser/nsrt/bug-9181.php

Root cause

When (object)['search' => null] is evaluated, PHPStan creates an IntersectionType of ObjectShapeType{search: null} and ObjectType(stdClass). The property type information (null for search) is embedded in the variable's type via ObjectShapeType, not stored as a separate expression type entry in the scope.

When the object is passed to a function with side effects, invalidateExpression($arg, requireMoreCharacters: true) only removes expression type entries that reference the variable (like narrowed $data->search entries). Since the property type comes from the ObjectShapeType within the variable's own type definition, there's nothing in expressionTypes to invalidate.

The fix detects when a stdClass object has additional type information (from ObjectShapeType) and replaces it with plain ObjectType('stdClass') after argument invalidation. This way, subsequent property accesses resolve through stdClass's universal object crate mechanism, returning mixed instead of the stale specific type.

Test

The regression test verifies that after passing a (object)['search' => null] object to a void method:

  • $data->search is inferred as mixed (not null)
  • The !== null comparison correctly narrows the type to mixed~null in the true branch

Fixes phpstan/phpstan#9181

…ll boundary

- When an object created via (object)[] cast is passed to a function with side effects, its property types (embedded in ObjectShapeType) were not being widened
- Added widenUniversalObjectCrateProperties() to NodeScopeResolver that resets stdClass to its base type after argument invalidation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-9181.php
- The root cause was that ObjectShapeType property types are part of the variable's type rather than stored as separate expression types, so invalidateExpression() could not reach them

Closes phpstan/phpstan#9181
Comment on lines +5960 to +5964
$stdClassType = new ObjectType('stdClass');

if (!in_array('stdClass', $argType->getObjectClassNames(), true)) {
return $scope;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should work for any ObjectShapeType and not only stdClass.

Copy link
Contributor

@staabm staabm Mar 1, 2026

Choose a reason for hiding this comment

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

should the logic be part of MutatingScope->invalidateExpression() or MutatingScope->shouldInvalidateExpression()?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe, but I think that's already not the right logic.

@staabm staabm changed the title Fix #9181: Incorrect assertion of value of object across function call boundary Fix phpstan/phpstan#9181: Incorrect assertion of value of object across function call boundary Mar 1, 2026
Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

This doesn't work with

/**
	 * @param object{search: null} $data
	 */
	public function sayHello2(object $data): void
	{
		assertType('null', $data->search);

		$this->possiblyModifyObject($data);

		assertType('mixed', $data->search);

		if (($search = $data->search) !== null) {
			assertType('mixed~null', $search);
		}
	}

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