Skip to content

Check the subtracted type when a subtracted mixed accepts a value - #6167

Open
zonuexe wants to merge 3 commits into
phpstan:2.2.xfrom
zonuexe:non-empty-mixed-accepts
Open

Check the subtracted type when a subtracted mixed accepts a value#6167
zonuexe wants to merge 3 commits into
phpstan:2.2.xfrom
zonuexe:non-empty-mixed-accepts

Conversation

@zonuexe

@zonuexe zonuexe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes phpstan/phpstan#15033

MixedType::accepts (and StrictMixedType, reached once RuleLevelHelper rewrites an explicit MixedType for checkExplicitMixed) accepted every value unconditionally, so a subtracted mixed like non-empty-mixed rejected nothing at argument or return boundaries, even though the same subtraction already powered narrowing and reachability (identical.alwaysFalse, if.alwaysTrue, etc.).

Acceptance now turns to No only on a definite hit (subtractedType->isSuperTypeOf($given)->yes()), so partial overlaps (e.g. a general string into non-empty-mixed, which may or may not be '') stay accepted — this preserves mixed's usual looseness and is why the fix isn't isSuperTypeOf(...)->toAcceptsResult(). NeverType is exempted, mirroring MixedType::isSuperTypeOf.

RuleLevelHelper::transformCommonType now carries the subtraction through when it converts an explicit MixedType into StrictMixedType, instead of discarding it — otherwise level max stayed silent even with MixedType fixed. VerbosityLevel::getRecommendedLevelByType escalates to precise() when a subtracted (Strict)MixedType is involved, so messages render the subtraction instead of a bare, uninformative mixed.

@staabm
staabm force-pushed the non-empty-mixed-accepts branch from e8428d1 to a5e98b4 Compare August 3, 2026 10:46
Comment thread src/Rules/RuleLevelHelper.php
@zonuexe
zonuexe marked this pull request as draft August 3, 2026 13:39
@zonuexe
zonuexe marked this pull request as ready for review August 3, 2026 14:18
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

Comment thread src/Type/VerbosityLevel.php Outdated
Comment on lines +131 to +153
// A subtracted mixed only makes sense in an error message when the subtraction
// is spelled out. Template bounds are skipped - the subtraction there belongs
// to the bound, not to the type being described.
$hasSubtractedMixed = false;
TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$hasSubtractedMixed): Type {
if ($hasSubtractedMixed || $type instanceof TemplateType) {
return $type;
}

if (
($type instanceof MixedType || $type instanceof StrictMixedType)
&& $type->getSubtractedType() !== null
) {
$hasSubtractedMixed = true;
return $type;
}

return $traverse($type);
});

if ($hasSubtractedMixed) {
return self::precise();
}

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.

couldn't this additional type-traversal be prevented by merging this logic into the pre-existing $moreVerboseCallback below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merged in 14d40d3. A depth counter or boolean flag for the template-bound exclusion fails make phpstan: the self-analysis reports identical.alwaysTrue because it doesn't model the callback re-entering through $traverse. So the existing branch block moved into $flagsCallback; the outer callback maps template subtrees with it, escalates on subtracted mixed, and delegates the rest. Single traversal, no extra pass. Side effect: the accepted-type traversal in the invariant-template path now detects subtracted mixed too, so those messages also render the subtraction.

@zonuexe
zonuexe force-pushed the non-empty-mixed-accepts branch from 6877410 to 14d40d3 Compare August 5, 2026 07:18
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.

non-empty-mixed is used for narrowing but not enforced at call/return boundaries

3 participants