Allow @throws annotation in base classes for exceptions not actually thrown

Bug report

A method in a base class may be annotated with @throws SomeException to express that derived classes are allowed to throw such exceptions:

abstract class Base {
    /**
     * @throws \LogicException
     */
    public function f(): void {
    }
}

class Derived extends Base {
    public function f(): void {
        throw new \LogicException();
    }
}

This informs users of the base class that code calling the method has to expect the exception:

function g(Base $obj): void {
    try {
        $obj->f();
    } catch (\LogicException $e) {
        // Not a dead catch: Base::f itself doesn't throw,
        // but it may be overridden to throw
    }
}

However, PhpStan reports the @throws annotation as an error:

Method Base::f() has LogicException in PHPDoc @throws tag but it's not thrown.

Some thoughts around this:

  • A class which is not final is a slight hint that this situation may happen. 'Slight hint' refers to the fact that most code bases do not use final consistently. I think this is the most important curlpit here.
  • A class which is tagged as abstract is a stronger hint that this may happen.
  • A class tagged as final would disallow overriding the method, making the error message valid.
  • Same applies to final on the method.
  • And to private methods.

Code snippet that reproduces the problem

https://phpstan.org/r/f7f8735c-2620-4cfa-88e1-f4442f36214f

Expected output

No error reported.

Did PHPStan help you today? Did it make you happy in any way?

No response