Skip to content

Fix phpstan/phpstan#6934: Nullsafe operator does not narrow down type#5322

Open
phpstan-bot wants to merge 8 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-y24thcm
Open

Fix phpstan/phpstan#6934: Nullsafe operator does not narrow down type#5322
phpstan-bot wants to merge 8 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-y24thcm

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When using chained nullsafe operators like $node?->parentNode?->removeChild($node), PHPStan did not narrow $node to non-null inside the method call arguments, even though the nullsafe chain guarantees $node cannot be null if removeChild() is reached.

Changes

  • Added narrowNullsafeVarChain() method to src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php that walks a nullsafe chain (NullsafePropertyFetch/NullsafeMethodCall) and narrows each intermediate var to non-null
  • Modified src/Analyser/ExprHandler/MethodCallHandler.php to detect virtual nullsafe method calls (via the virtualNullsafeMethodCall attribute) and apply nullsafe chain narrowing before processing arguments, then revert the narrowing afterward
  • Added regression test in tests/PHPStan/Analyser/nsrt/bug-6934.php

Root cause

The NullsafeMethodCallHandler converts a NullsafeMethodCall to a regular MethodCall and narrows the direct var expression to non-null. When the MethodCallHandler processes this converted call, it first processes the var (which may contain inner nullsafe operators that temporarily narrow their vars and then revert), then processes args with the reverted scope. The inner nullsafe narrowing was lost before arguments were processed.

The fix applies the nullsafe chain narrowing at the right point in the MethodCallHandler — after the var is fully processed (so nullsafe rules fire correctly with the original scope) but before args are processed (so arguments see non-null types).

Test

Added tests/PHPStan/Analyser/nsrt/bug-6934.php with assertType() calls verifying that variables used in nullsafe method call chains are narrowed to non-null inside method arguments.

Fixes phpstan/phpstan#6934

@VincentLanglet VincentLanglet requested a review from staabm March 29, 2026 21:40
@staabm staabm requested a review from VincentLanglet March 30, 2026 06:59
]);
}

#[RequiresPhp('>= 8.0')]
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.

why does this test fail on php 8.0?

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. Removed the #[RequiresPhp('>= 8.0')] attribute from testBug6934() since it's unnecessary — RuleTestCase uses php-parser which can parse nullsafe ?-> syntax on any PHP version. Other nullsafe tests in the same file (e.g. testNullSafe()) already run without this attribute.

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.

thats not true. we need #[RequiresPhp('>= 8.0')]. tell me why the test errors on PHP 8.0

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.

Type inference tests also passed. All checks are green — the commit with #[RequiresPhp('>= 8.0')] restored has been pushed.

@clxmstaab clxmstaab force-pushed the create-pull-request/patch-y24thcm branch from 4b10170 to fd7af45 Compare March 30, 2026 09:57
github-actions bot and others added 6 commits March 30, 2026 11:58
…method args

- In nullsafe method call chains like $a?->b?->method($a), the variable $a
  was not narrowed to non-null when processing method arguments
- Added narrowNullsafeVarChain() to NonNullabilityHelper to walk a nullsafe
  chain and narrow each intermediate var
- Modified MethodCallHandler to apply nullsafe chain narrowing before
  processing arguments of virtual nullsafe method calls, then revert after
- New regression test in tests/PHPStan/Analyser/nsrt/bug-6934.php
…6934

- Add CallMethodsRule test to verify no parameter type error on
  $node?->parentNode?->removeChild($node)
- Assert $node remains DOMNode|null after the nullsafe chain
- Assert $foo remains Foo|null after the nullsafe chain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RuleTestCase uses php-parser which can parse nullsafe syntax on any
PHP version, so #[RequiresPhp('>= 8.0')] is not needed. Other nullsafe
tests in this file (e.g. testNullSafe) run without this attribute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test data file uses DOMNode which has version-dependent property
types via #[LanguageLevelTypeAware] in phpstorm-stubs. On PHP < 8.0,
the analysis may produce different errors since DOMNode::$parentNode
lacks a native type declaration. The // lint >= 8.0 comment in the
test data file is only processed by TypeInferenceTestCase, not by
RuleTestCase::analyse(), so #[RequiresPhp] is needed to skip the
test on older PHP versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-y24thcm branch from fd7af45 to 5ebd295 Compare March 30, 2026 09:58
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-y24thcm branch from a43dab4 to 8903c78 Compare March 30, 2026 22:04
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