Psalm: Bidirectional type inference for functions and methods

Created on 1 Oct 2019  Â·  7Comments  Â·  Source: vimeo/psalm

Not sure was this discussed or not. It would be great to make Psalm less to report ...cannot be mixed or ...has no provided type.

In the following example, Psalm will report multiple info messages about missing type:
https://psalm.dev/r/01e39c763b

function foo($s) {
    return "foo" . $s;
}

$s = foo(1);
$s + 1;
INFO: MixedOperand - 4:20 - Right operand cannot be mixed
INFO: MissingParamType - 3:14 - Parameter $s has no provided type
INFO: MissingReturnType - 3:10 - Method foo does not have a return type, expecting string
INFO: MixedAssignment - 7:1 - Cannot assign $s to a mixed type
INFO: MixedOperand - 8:1 - Left operand cannot be mixed



md5-086552ee908ee4bbd31b5e985fbf1c27



ERROR: InvalidScalarArgument - 11:10 - Argument 1 of foo expects string, int(1) provided
ERROR: InvalidOperand - 12:1 - Cannot perform a numeric operation with a non-numeric type string
```

Psalm already do types inference.Probably, it might be possible to infer types for functions signatures.

In some particular cases, the type templates can be inferred which are already supported by Psalm:

  1. without types https://psalm.dev/r/cfef863ba4
  2. with types https://psalm.dev/r/aa024b4141

Dart's type system is a great example. I can guess that such type inference system might be very complex. For example, Python's mypy does not support types inference for dynamically typed functions and there might be some a reason for this.

wontfix

All 7 comments

Hey!

Psalm doesn't do this for three reasons:

  1. when writing code at scale — code that will be seen and edited by others — we generally want to be explicit about expected types (whether in docblocks or in function signatures)
  2. the analysis is somewhat slower than the current analysis that Psalm does, and cannot so easily be parallelised
  3. I'm probably not smart enough to do a good job of it, what with Psalm's existing complexity.

If you want a tool that does this, I recommend Phan which has always had it, I think? cc @TysonAndre on that.

If you have a solution that's not obviously slower (or which could be hidden behind an opt-in flag) I would welcome a PR. I'm happy to also provide pointers on how you might go about things. But I'm not going to add this myself.

It's not quite bidirectional - The parameter types aren't inferred from the method body. Instead, types are inferred from usage, with a limit on how deeply Phan would recurse. (This was easier to implement)

  • Phan recursively analyzes by default (where it makes sense to do so). That can be turned off with --quick, which makes it only analyze method bodies once, non-recursively. (similar to Psalm)
  • Instead of inferring parameters from the method body, the parameter types are inferred from the argument types if parameter types aren't specified. Literals and array shapes get normalized to non-literals and generic arrays (e.g. array{0:'string literal'} becomes array<int,string>
  • Additionally, if no return type is specified (either in the signature or as @return), the types from return statements within the method body get added to the return type of the method. (So the return type you get currently depends on the order in which uses get analyzed, which is a situation that could probably get improved on)
  • Inferring types from usage is also done for inferring types of properties, in cases when types aren't documented (or are vague). It works best if properties are assigned near the top of the file (e.g. in constructors), and used below that, because Phan analyzes statements/expressions in the order they occur.

@muglug another much simpler example of type inference of return type that would be useful:
https://psalm.dev/r/2d72825662
All current static analyzer Psalm, Phan, Phpstan reports about a not existing method:

Psalm output (using commit 19faa31):

ERROR: UndefinedMethod - 37:34 - Method ParentClassTranslation::setextrafield does not exist

Surprisingly, PHPStorm can infer the type correctly and recognize the method properly.

I found these snippets:


https://psalm.dev/r/2d72825662

<?php

class ParentClassTranslation
{

}

class ParentClass
{
    public function getTranslation() : ParentClassTranslation
    {
        return $this->createTranslation();
    }

    protected function createTranslation() : ParentClassTranslation
    {
        return new ParentClassTranslation();
    }
}

class ChildClassTranslation extends ParentClassTranslation
{
    /** @var string|null */
    private $extraField;

    public function setExtraField(string $extraField) : void
    {
        $this->extraField = $extraField;
    }
}

class ChildClass extends ParentClass
{

    public function setExtraField(string $extraField) : void
    {
        $this->getTranslation()->setExtraField($extraField);
    }

    // Starting from version 7.4 PHP supports covariant return types but it does not help as well
    protected function createTranslation() : ParentClassTranslation
    {
        return new ChildClassTranslation();
    }

}

$obj = new ChildClass();
$obj->setExtraField('some text');
Psalm output (using commit 19faa31):

ERROR: UndefinedMethod - 37:34 - Method ParentClassTranslation::setextrafield does not exist

You can achieve decent results with templates here: https://psalm.dev/r/e16fcbab0a

I found these snippets:


https://psalm.dev/r/e16fcbab0a

<?php

class ParentClassTranslation
{

}

/**
 * @template T as ParentClassTranslation
 */
abstract class ParentClass
{
    /** @return T */
    public function getTranslation() : ParentClassTranslation
    {
        return $this->createTranslation();
    }

    /** @return T */
    abstract protected function createTranslation() : ParentClassTranslation;
}

class ChildClassTranslation extends ParentClassTranslation
{
    /** @var string|null */
    private $extraField;

    public function setExtraField(string $extraField) : void
    {
        $this->extraField = $extraField;
    }
}

/**
 * @extends ParentClass<ChildClassTranslation>
 */
class ChildClass extends ParentClass
{

    public function setExtraField(string $extraField) : void
    {
        $this->getTranslation()->setExtraField($extraField);
    }

    // Starting from version 7.4 PHP supports covariant return types but it does not help as well
    protected function createTranslation() : ParentClassTranslation
    {
        return new ChildClassTranslation();
    }

}

$obj = new ChildClass();
$obj->setExtraField('some text');
Psalm output (using commit 366e2d3):

No issues!

@muglug I agree that it is not a problem to cover code with types. Unfortunately, these cases are mostly related to third-party libraries. A much simpler workaround can be done with simple assert(... instanceof ChildClassTranslation ), but I just use @psalm-suppress to avoid additional variable creation.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

muglug picture muglug  Â·  3Comments

ADmad picture ADmad  Â·  3Comments

vudaltsov picture vudaltsov  Â·  3Comments

ErikBooijCB picture ErikBooijCB  Â·  4Comments

greg0ire picture greg0ire  Â·  3Comments