Repository navigation
[6.x] Accept string pseudo-type intersections in docblocks - #11823
Merged
Merged
Conversation
Parse-time intersections like `non-empty-string&lowercase-string` now collapse to their single-atomic equivalent (`non-empty-lowercase-string`) instead of throwing `InvalidDocblock: Intersection types must be all objects`. This mirrors the runtime narrowing in `Type::intersectAtomicTypes` and unblocks Laravel's `Illuminate\Support\Str::lower()` return type. The collapse uses a positive allowlist (`TNonEmptyString`, `TLowercaseString`, `TNonspecificLiteralString` and their subclasses) and exact-class matching on the known narrowing rules so that subclasses like `TNumericString` or `TNonFalsyString` paired with `TLowercaseString` fall through to rejection rather than silently dropping their extra constraints. Explicit handlers cover two class-hierarchy quirks: - `TNonEmptyLowercaseString` extends `TNonEmptyString` but not `TLowercaseString`. - `TNonEmptyNonspecificLiteralString` extends `TNonspecificLiteralString` but not `TNonEmptyString`. Fixes vimeo#11821.
|
I found these snippets: https://psalm.dev/r/b51da730d4
/**
* @param string $value
* @return ($value is '' ? '' : non-empty-string&lowercase-string)
*/
function lower(string $value): string
{
return strtolower($value);
}
/** @return non-empty-string&lowercase-string */
function ret(): string
{
return 'hello';
} |
alies-dev
marked this pull request as ready for review
April 22, 2026 10:00
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 0290de1. Configure here.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fixes #11821.
Parse-time intersections like
non-empty-string&lowercase-stringnow collapse to their single-atomic equivalent (non-empty-lowercase-string) instead of throwingInvalidDocblock: Intersection types must be all objects, Psalm\Type\Atomic\TNonEmptyString provided.This mirrors the runtime narrowing already present in
Type::intersectAtomicTypesand unblocks Laravel'sIlluminate\Support\Str::lower()return type:How it works
TypeParser::getTypeFromIntersectionTree()now calls a newcollapseStringPseudoTypeIntersection()helper before the existingextractKeyedIntersectionTypes(). If the helper recognizes a known string pseudo-type combination, it returns the equivalent single atomic; otherwise it returnsnulland the existing rejection path runs unchanged.Design notes
TNonEmptyString,TLowercaseString,TNonspecificLiteralString(and their subclasses) participate in the collapse. Anything else falls through to the original error path.Type::intersectAtomicTypes(non-empty-string & literal-string → non-empty-literal-string,non-empty-string & lowercase-string → non-empty-lowercase-string) use exact::classmatching rather thaninstanceof, so subclasses likeTNumericStringorTNonFalsyStringpaired withTLowercaseStringare rejected rather than silently widened.is_a()handles natural subtype relationships (e.g.numeric-string & non-empty-string → numeric-string).TNonEmptyLowercaseStringextendsTNonEmptyStringbut notTLowercaseString.TNonEmptyNonspecificLiteralStringextendsTNonspecificLiteralStringbut notTNonEmptyString.Scope
This PR addresses the specific Laravel/
Str::lower()case from #11821. It is related to but does not fully close #9628 (which covers other combinations such asnumeric-string & truthy-stringthat have no single-atomic equivalent). Those can be added incrementally on top of the same machinery.Tests
tests/TypeParseTest.php— 14 new tests covering accepted combinations, subtype resolution, class-hierarchy quirks, and rejection of unsafe/unknown pairs (to lock in that no widening occurs).tests/ReturnTypeTest.php— 3 functional tests including the Laravel-style conditional return type.Verification
composer phpunit-std(targeted): 174 / 174 TypeParseTest tests pass, including all pre-existing cases.composer psalm: no errors.composer cs: no errors.composer lint: no errors.non-empty-string&lowercase-stringrejected as "must be all objects" (used by laravel/framework Str::lower) #11821 (https://psalm.dev/r/b51da730d4) now produces no errors.Note
Medium Risk
Touches core type parsing for intersection types; while behavior is narrowly allow-listed, it can change how certain docblocks are interpreted and could affect downstream analysis results.
Overview
Docblock intersections involving specific string pseudo-types are now accepted and collapsed to a single narrower atomic during parsing (e.g.
non-empty-string&lowercase-string→non-empty-lowercase-string,non-empty-string&literal-string→non-empty-literal-string) instead of throwing an invalid intersection error.TypeParser::getTypeFromIntersectionTree()calls a new helper to safely reduce allow-listed string pseudo-type pairs (including a few hierarchy edge-cases) and otherwise falls through to the existing rejection logic unchanged, with new unit/analysis tests covering accepted, subtype, and rejected combinations (including a Laravel-style conditional return type).Reviewed by Cursor Bugbot for commit 0290de1. Bugbot is set up for automated code reviews on this repo. Configure here.