Skip to content

Impossible template types not rejeced #2579

Open

Description

@muglug

Code snippet that reproduces the problem

class A {}
class B {}
class A1 extends A {
  public function foo() : void {}
}
class B1 extends B {
  public function bar() : void {}
}

/**
 * @template T1 of A
 * @template T2 of B
 * @param T1|T2 $type
 * @return T1|T2
 */
function f(object $type) {
	return new $type();
}

(f(new A1))->foo(); // should pass
(f(new B1))->bar(); // should pass

(f(new A1))->bar(); // should fail
(f(new B1))->foo(); // should fail

https://phpstan.org/r/a7b835ae-b7a1-4cda-b7ba-3b3f13713fb6

Expected output

See above

Actual output

+---------------------------------------------------------------------+
| Line | test.php                                                     |
+---------------------------------------------------------------------+
| 21   | Call to an undefined method A1|B::foo().                     |
| 21   | Unable to resolve the template type T2 in call to function f |
| 22   | Call to an undefined method A|B1::bar().                     |
| 22   | Unable to resolve the template type T1 in call to function f |
| 24   | Call to an undefined method A1|B::bar().                     |
| 24   | Unable to resolve the template type T2 in call to function f |
| 25   | Call to an undefined method A|B1::foo().                     |
| 25   | Unable to resolve the template type T1 in call to function f |
+---------------------------------------------------------------------+

Activity

  1. ondrejmirtes commented on Nov 11, 2019

    @ondrejmirtes
    Member

    I confirm - this is a bug and should be easy to fix.

  2. ondrejmirtes commented on Nov 30, 2019

    @ondrejmirtes
    Member

    On the other hand, this is a weird example. To have a successful type resolution, we need to resolve all template types in a function call. When we're passing A1 as the argument, we don't have anything to resolve as T2. So we cannot construct the return type. On line 21, the function according to its signature always returns something as B. So it's not safe to call something that's only on A.

    With a simple signature that also describes what the functon does, it behaves as expected: https://phpstan.org/r/02a16855-0d1c-42dd-aa28-8e281c48df81

  3. muglug commented on Nov 30, 2019

    @muglug
    ContributorAuthor

    on the other hand this is a weird example

    Yes, that describes a lot of the bugs people report with templating.

    But here we can reasonably say that an unmatched template should be bound to the empty type or similar - that is, a type that disappears in any union.

  4. ondrejmirtes commented on Dec 1, 2019

    @ondrejmirtes
    Member

    Unmatched template type is resolved to its bound and not to never.

    I think we'd be able to construct method body with the above signature that would break in runtime without Psalm complaining about anything...

    The question really is - does T1|T2 mean that the function expects a union of two types, or does it mean that I can pass in either of them. /cc @arnaud-lb

  5. added this to the Generics milestone on Dec 30, 2019
  6. phpstan-bot commented on Mar 9, 2021

    @phpstan-bot
    Contributor

    @ondrejmirtes PHPStan now reports different result with your code snippet:

    @@ @@
    +17: Function f() should return T1 of object but returns mixed.
     23: Call to an undefined method A1::bar().
     24: Call to an undefined method B1::foo().
    Full report
    Line Error
    17 Function f() should return T1 of object but returns mixed.
    23 Call to an undefined method A1::bar().
    24 Call to an undefined method B1::foo().
  7. phpstan-bot commented on Sep 15, 2021

    @phpstan-bot
    Contributor

    @ondrejmirtes After the latest commit in dev-master, PHPStan now reports different result with your code snippet:

    @@ @@
    +17: Function f() should return T1 of object but returns object.
     23: Call to an undefined method A1::bar().
     24: Call to an undefined method B1::foo().
    Full report
    Line Error
    17 Function f() should return T1 of object but returns object.
    23 Call to an undefined method A1::bar().
    24 Call to an undefined method B1::foo().
  8. ZebulanStanphill commented on Mar 25, 2022

    @ZebulanStanphill
    Contributor

    Looks like this is fixed now in PHPStan 1.5.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions