Skip to content

Fix phpstan/phpstan#14188: template error: It breaks the contract for some argument types, typically subtypes.#5301

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-v64so8b
Closed

Fix phpstan/phpstan#14188: template error: It breaks the contract for some argument types, typically subtypes.#5301
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-v64so8b

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a method has a class-string<T> parameter and T template return type, narrowing the parameter via $class === A::class and then doing return new $class() inside that branch produced a false positive: "Method should return T but returns A. Type A is not always the same as T. It breaks the contract for some argument types, typically subtypes."

The fix preserves the template type relationship when resolving new $class() after the class-string parameter has been narrowed.

Changes

  • Modified src/Analyser/ExprHandler/NewHandler.php: In resolveType(), when evaluating new $variable() where the variable is a function parameter with a declared class-string<T> type, intersect the template type T with the concrete object type. This produces T & A instead of just A, which is correctly accepted as a subtype of T.
  • Added rule test tests/PHPStan/Rules/Methods/data/bug-14188.php and test method in ReturnTypeRuleTest.php
  • Added NSRT test tests/PHPStan/Analyser/nsrt/bug-14188.php

Root cause

When $class has type class-string<T of MyInterface> and is narrowed by $class === A::class, the scope narrows $class to ConstantStringType('A'), losing the template type information. Then new $class() resolves to ObjectType('A') via getObjectTypeOrClassStringObjectType(). The return type check compares ObjectType('A') against TemplateType T, and TemplateTypeArgumentStrategy::accepts() returns maybe with the "breaks the contract" message because A is not always the same as T in general.

The fix addresses this by looking up the function parameter's declared type when resolving new $variable(). If the parameter was declared as class-string<T> and the concrete object type satisfies T's bound, the result is T & A (an intersection), which is a proper subtype of T and passes the return type check.

Test

  • Rule test: Reproduces the exact scenario from the issue — a factory method with @template T of MyInterface, @param class-string<T> $class, @return T, where $class === A::class branch returns new $class(). Expects no errors.
  • NSRT test: Verifies the inferred type of new $class() inside the narrowed branch includes both the template type and the concrete type.

Fixes phpstan/phpstan#14188

…s-string parameter

- Modified NewHandler::resolveType() to intersect the template type from the
  parameter's declared class-string<T> type with the concrete object type when
  evaluating `new $class()` after the class-string has been narrowed
- Added rule regression test in tests/PHPStan/Rules/Methods/data/bug-14188.php
- Added NSRT test in tests/PHPStan/Analyser/nsrt/bug-14188.php
- Root cause: when class-string<T> is narrowed via === to a constant class string,
  `new $class()` lost the template type T and returned a plain ObjectType, causing
  the return type check to report a false positive "breaks the contract" error
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-v64so8b branch March 26, 2026 08:34
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