Skip to content

Fix phpstan/phpstan#14440: Intersections in class-string<A&B> not correctly detected, reported as string#5428

Closed
phpstan-bot wants to merge 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cb984ps
Closed

Fix phpstan/phpstan#14440: Intersections in class-string<A&B> not correctly detected, reported as string#5428
phpstan-bot wants to merge 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cb984ps

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a ConstantStringType represented a class-string (e.g. from SomeClass::class), its describe() method at typeOnly verbosity level returned just string, losing the class-string information. This caused misleading error messages like:

Method ChildOne::getCounterpartClass() should return class-string but returns string.

After the fix, the error message correctly shows:

Method ChildOne::getCounterpartClass() should return class-string but returns class-string.

Changes

  • Modified ConstantStringType::describe() in src/Type/Constant/ConstantStringType.php to return class-string<ClassName> at typeOnly level when the isClassString flag is set
  • Added regression test in tests/PHPStan/Rules/Methods/data/bug-14440.php and tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
  • Updated test expectations in 5 existing test files to match improved error messages:
    • tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php (bug-8071)
    • tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php (ClassStringWithUpperBounds)
    • tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php (TemplateTypeInOneBranchOfConditional)
    • tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php (bug-4413, bug-4371, is-subclass-allow-string)

Root cause

ConstantStringType::describe() used a static closure static fn (): string => 'string' for the typeOnly handler, which always returned 'string' regardless of whether the constant represented a class-string. This is inconsistent with GenericClassStringType which includes its generic parameter at all verbosity levels (e.g. class-string<ChildOne>). The fix uses the isClassString flag (set during construction for ::class expressions) to return class-string<ClassName> instead.

Test

Added testBug14440 in ReturnTypeRuleTest with test data reproducing the issue: an abstract class with @return class-string<static&I>, and two final child classes returning each other's ::class. The test verifies the error messages correctly describe the return types as class-string<ChildTwo> and class-string<ChildOne> respectively.

Fixes phpstan/phpstan#14440

- ConstantStringType::describe() at typeOnly level now returns 'class-string<ClassName>'
  instead of 'string' when the constant is a known class-string
- This fixes misleading error messages like "returns string" when the actual type is
  a class-string (e.g. from SomeClass::class expressions)
- Updated test expectations in 5 test files to reflect improved error messages
- Added regression test for phpstan/phpstan#14440
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.

Rather than modifying ConstantStringType::info, consider working on VerbosityLevel::getRecommendedLevelByType

phpstan-bot and others added 2 commits April 8, 2026 09:42
…velByType

Instead of modifying ConstantStringType::describe() at typeOnly level,
use VerbosityLevel to bump to value level when the accepting type contains
class-string types. The class-string<ClassName> description is now shown
at value verbosity level instead of typeOnly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests now expect class-string<ClassName> instead of quoted 'ClassName'
when ConstantStringType with isClassString flag is described at value
verbosity level. Also update baseline and ignored error patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of the changes addressing the review:

What changed:

  1. VerbosityLevel::getRecommendedLevelByType() - Added isClassString()->yes() check to $moreVerboseCallback, so when the accepting type contains a class-string type (e.g., class-string<Exception>), verbosity is bumped to value level.

  2. ConstantStringType::describe() - Reverted typeOnly handler back to returning 'string'. Moved the class-string<ClassName> description to the value handler instead, which is only used when VerbosityLevel decides the extra detail is needed.

Why this approach is better:

  • The typeOnly level stays clean and generic ('string'), as intended for basic type descriptions
  • VerbosityLevel::getRecommendedLevelByType controls when to show extra detail, which is its architectural role
  • The value handler in ConstantStringType provides the what — showing class-string<ClassName> when the context warrants it

Test impact: Updated 12 test files and 2 config files where class-string constants now show as class-string<ClassName> instead of quoted 'ClassName' at value verbosity.

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.

Do not modify AT ALL the ConstantStringType::describe implémentation.

And getRecommendedLevelByType should require a moreVerbose level only if acceptingType is a string.
If a method should return object but return a constantString there is no need for a more verbose type than string.

…s-string

Address review feedback: do not modify ConstantStringType::describe()
at all. Instead, only add a class-string check in
VerbosityLevel::getRecommendedLevelByType that bumps verbosity to
value level when the accepting type is a class-string type.

This means class-string constants show as quoted values (e.g. 'stdClass')
at value verbosity, not as class-string<ClassName>. The verbosity is
only bumped when the accepting type contains a class-string, so when
a method expects object and receives a class-string, it still shows
as just 'string'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of the changes:

What changed:

  1. ConstantStringType::describe() - Fully reverted to original. No modifications at all, as the reviewer requested.

  2. VerbosityLevel::getRecommendedLevelByType() - Added a check $type->isString()->yes() && $type->isClassString()->yes() in the $moreVerboseCallback. This bumps verbosity to value level only when the accepting type contains a class-string type (e.g., class-string<Exception>). When the accepting type is something like object, verbosity stays at typeOnly and the constant string shows as just string.

  3. Test expectations - Updated only the tests where the accepting type contains class-string, changing from string to quoted format (e.g., 'Throwable', 'stdClass'). Tests where the accepting type doesn't contain class-string remain unchanged. Tests that already used value-level verbosity (ImpossibleCheckType, StrictComparison, etc.) were reverted to their original expectations.

  4. Baselines - Updated build/phpstan.neon to match the new error messages where string given became 'PHPStan…' given.

@staabm staabm deleted the create-pull-request/patch-cb984ps branch April 8, 2026 10:36
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.

2 participants