Skip to content

Fix phpstan/phpstan#14393: Does not warn about non-nullable class-property#5327

Merged
staabm merged 14 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-831pof2
Mar 30, 2026
Merged

Fix phpstan/phpstan#14393: Does not warn about non-nullable class-property#5327
staabm merged 14 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-831pof2

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented Mar 29, 2026

Summary

PHPStan did not warn about using ?? on non-nullable native-typed instance properties accessed on objects other than $this. For example, $o->i ?? -1 where $i is public int $i = 10 produced no warning, even though the type is int (not nullable).

Changes

  • Modified src/Rules/IssetCheck.php to only bail out of the native-typed property check for $this->prop access (where initialization tracking matters) and static properties (where lazy initialization via ??= is a valid pattern). Non-$this instance property access now falls through to the normal type check.
  • Updated tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php — the ReflectionClass::$name warning is now expected on all PHP versions (previously only PHP < 8.1).
  • Updated tests/PHPStan/Rules/Variables/IssetRuleTest.php — native-typed instance properties on external objects now all produce warnings.
  • Fixed src/Type/Php/ArrayFindFunctionReturnTypeExtension.php — removed unnecessary ?? null on PhpParser\Node\Arg::$value which is always non-null.
  • Added regression test tests/PHPStan/Rules/Variables/data/bug-14393.php.

Root cause

In IssetCheck::check(), when a property had a native type and the scope didn't explicitly track the expression type ($scope->hasExpressionType($expr)->yes() was false), the code bailed out early for ALL property accesses. This was intended to handle potentially uninitialized typed properties, but it was too conservative — it also suppressed warnings for instance property access on external objects where PHPStan resolves the type as non-nullable.

The fix narrows the bail-out to only apply to:

  1. $this->prop — where PHPStan tracks initialization state and the bail-out prevents false positives for possibly-uninitialized properties
  2. Static properties — where ??= is commonly used for lazy initialization

Test

Added tests/PHPStan/Rules/Variables/data/bug-14393.php which tests $o->i ?? -1 where $i is public int $i = 10. The expected error is: "Property Bug14393\MyClass::$i (int) on left side of ?? is not nullable."

Fixes phpstan/phpstan#14393
Closes phpstan/phpstan#4846

Copy link
Copy Markdown
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.

Add test for

public int $i;

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's what I changed to address the review:

  1. Added test for public int $i; (no default value) in bug-14393.php — a MyClassUninitialized class with an uninitialized typed property. Using $o2->i ?? -1 should NOT produce a warning since the property could be uninitialized.

  2. Fixed IssetCheck.php — for non-$this instance property fetches on native-typed properties without a default value (hasDefaultValue() === false), we now bail out instead of reporting a false positive. Properties without defaults can be uninitialized when accessed on external objects, making ?? valid.

  3. Updated test expectations — removed the ReflectionClass::$name warning (it has no default value, set in constructor) and the $canBeUninitialized isset warning (also no default value).

Copy link
Copy Markdown
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.

Add test for

$this->i ?? -1

inside both MyClass and MyClassUninitialized

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

make tests also passed. Both make phpstan and make tests completed successfully with no regressions.

The commit has already been pushed. The review request has been addressed — $this->i ?? -1 tests are now included inside both MyClass (initialized) and MyClassUninitialized (uninitialized), confirming that neither produces a false positive warning for $this->prop access.

Comment thread tests/PHPStan/Rules/Variables/data/bug-14393.php
Copy link
Copy Markdown
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.

Add test for

	/** @var int */
	public $i;

and for

	public ?int $i;

and for

	public ?int $i = 10;

and for

	public ?int $i = null;

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

make tests also confirmed complete — 11687 tests, all passing. The commit with the reviewer's requested test cases has been pushed.

Comment on lines -117 to -122
if (PHP_VERSION_ID < 80100) {
$errors[] = [
'Property ReflectionClass<object>::$name (class-string<object>) on left side of ?? is not nullable.',
136,
];
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Revert this change

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

make tests also passed — all 11687 tests green. The review feedback has been fully addressed, committed, and pushed.

Copy link
Copy Markdown
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.

Add non regression test for phpstan/phpstan#4846

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Already handled — make tests passed (11688 tests, all green). Everything has been pushed.

@VincentLanglet
Copy link
Copy Markdown
Contributor

I was not sure about the phpdoc case but it's already reported https://phpstan.org/r/190c6781-3043-4ccd-bcf6-9bf2a37b3d91.

Maybe you'll see more test to add but this seems ok to me @staabm

@VincentLanglet VincentLanglet requested a review from staabm March 29, 2026 15:14
Comment thread tests/PHPStan/Rules/Variables/data/bug-14393.php
Comment thread tests/PHPStan/Rules/Variables/data/bug-14393.php
VincentLanglet and others added 10 commits March 30, 2026 08:25
… instance properties

- Removed bail-out in IssetCheck for native-typed instance properties accessed on non-$this objects
- Static properties still bail out (valid lazy initialization pattern with ??=)
- $this->prop still respects initialization tracking as before
- Updated NullCoalesceRuleTest to always expect warning for ReflectionClass::$name
- Updated IssetRuleTest to expect warnings for all native-typed instance properties
- Removed unnecessary ?? null in ArrayFindFunctionReturnTypeExtension
- New regression test in tests/PHPStan/Rules/Variables/data/bug-14393.php
Properties declared as `public int $i;` (without a default value) can be
uninitialized when accessed on external objects, so using ?? on them is valid.
Only warn when the property has a default value (guaranteed initialized).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ized

Addresses review feedback: tests verify that $this->prop access inside
the class does not produce false positive warnings for either initialized
or uninitialized typed properties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
For $this->prop access on native-typed properties with a default value,
the property is always initialized so ?? on non-nullable types is useless.
Only skip the warning for $this->prop when the property has no default
value (could be uninitialized).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ties

Tests requested by reviewer: `/** @var int */ public $i`, `public ?int $i`,
`public ?int $i = 10`, and `public ?int $i = null` — both for $this->prop
and external object access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ReflectionClass::$name has no default value (set in constructor),
so on PHP >= 8.1 hasDefaultValue() returns false and the warning
is correctly suppressed. Restore the PHP_VERSION_ID < 80100 guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests for isset($this->i) and isset($o->i) across all property
variants (initialized, uninitialized, PHPDoc-only, nullable, etc.)
as requested by reviewer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-831pof2 branch from 8f7b91f to 8ab2b3c Compare March 30, 2026 06:25

public function doFoo(): void
{
var_dump(self::$i ?? -1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we are missing a error here, because the static property referenced by self::$i is not nullable

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The fix applies the same hasDefaultValue() check to static properties that was already in place for instance properties. Now self::$i ?? -1 and isset(self::$i) correctly report errors when the static property has a native type and a default value (like public static int $i = 10), while still allowing the bail-out for uninitialized static properties (no default value) or dynamic class references.

Static properties with a native type and a default value (e.g. `public static int $i = 10`)
should produce warnings for `self::$i ?? -1` and `isset(self::$i)` since the property
is not nullable. Previously all static property fetches were exempt from this check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm requested a review from VincentLanglet March 30, 2026 06:35
Comment thread src/Rules/IssetCheck.php
phpstan-bot and others added 2 commits March 30, 2026 08:08
…l-out

When the scope doesn't track the expression type for a native-typed
property access, check if $expr->var is undefined before falling
through to the type check. This prevents reporting "not nullable"
errors when the real issue is an undefined variable.

The check only bails out when there's an actual undefined error
(non-null return), otherwise falls through to the normal type check.
Same pattern applied to StaticPropertyFetch for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet
Copy link
Copy Markdown
Contributor

I was able to remove both checkUndefined done in this situation without breaking any test.
I didn't succeed writing a test which fail (missing an undefined test).

I'm ok merging this like this (and maybe waiting for a missing error report) but maybe you'll want to try producing an error/addind extra test.
Feel free to merge it if/when it's ok to you @staabm

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 30, 2026

love it. thank you

@staabm staabm merged commit 2f58f5f into phpstan:2.1.x Mar 30, 2026
652 of 654 checks passed
@staabm staabm deleted the create-pull-request/patch-831pof2 branch March 30, 2026 09:08
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
…perty (phpstan#5327)

Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
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