1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00
psalm/docs/annotating_code/adding_assertions.md
2021-09-21 17:35:44 +02:00

4.3 KiB

Adding assertions

Psalm has four docblock annotations that allow you to specify that a function verifies facts about variables and properties:

  • @psalm-assert (used when throwing an exception)
  • @psalm-assert-if-true/@psalm-assert-if-false (used when returning a bool)
  • @psalm-if-this-is (used when calling a method)

A list of acceptable assertions can be found here.

Examples

If you have a class that verified its input is an array of strings, you can make that clear to Psalm:

<?php
/** @psalm-assert string[] $arr */
function validateStringArray(array $arr) : void {
    foreach ($arr as $s) {
        if (!is_string($s)) {
          throw new UnexpectedValueException('Invalid value ' . gettype($s));
        }
    }
}

This enables you to call the validateStringArray function on some data and have Psalm understand that the given data must be an array of strings:

<?php
function takesString(string $s) : void {}
function takesInt(int $s) : void {}

function takesArray(array $arr) : void {
    takesInt($arr[0]); // this is fine

    validateStringArray($arr);

    takesInt($arr[0]); // this is an error

    foreach ($arr as $a) {
        takesString($a); // this is fine
    }
}

Similarly, @psalm-assert-if-true and @psalm-assert-if-false will filter input if the function/method returns true and false respectively:

<?php
class A {
    public function isValid() : bool {
        return (bool) rand(0, 1);
    }
}
class B extends A {
    public function bar() : void {}
}

/**
 * @psalm-assert-if-true B $a
 */
function isValidB(A $a) : bool {
    return $a instanceof B && $a->isValid();
}

/**
 * @psalm-assert-if-false B $a
 */
function isInvalidB(A $a) : bool {
    return !$a instanceof B || !$a->isValid();
}

function takesA(A $a) : void {
    if (isValidB($a)) {
        $a->bar();
    }

    if (isInvalidB($a)) {
        // do something
    } else {
        $a->bar();
    }

    $a->bar(); //error
}

As well as getting Psalm to understand that the given data must be a certain type, you can also show that a variable must be not null:

<?php
/**
 * @psalm-assert !null $value
 */
function assertNotNull($value): void {
  // Some check that will mean the method will only complete if $value is not null.
}

And you can check on null values:

<?php
/**
 * @psalm-assert-if-true null $value
 */
function isNull($value): bool {
  return ($value === null);
}

Asserting return values of methods

You can also use the @psalm-assert-if-true and @psalm-assert-if-false annotations to assert return values of methods inside classes. As you can see, Psalm even allows you to specify multiple annotations in the same DocBlock:

<?php
class Result {
    /**
     * @var ?Exception
     */
    private $exception;

    /**
     * @psalm-assert-if-true Exception $this->exception
     * @psalm-assert-if-true Exception $this->getException()
     */
    public function hasException(): bool {
        return $this->exception !== null;
    }

    public function getException(): ?Exception {
        return $this->exception;
    }

    public function foo(): void {
        if( $this->hasException() ) {
            // Psalm now knows that $this->exception is an instance of Exception
            echo $this->exception->getMessage();
        }
    }
}

$result = new Result;

if( $result->hasException() ) {
    // Psalm now knows that $result->getException() will return an instance of Exception
    echo $result->getException()->getMessage();
}

Please note that the example above only works if you enable method call memoization in the config file or annotate the class as immutable.

You can also make sure, when calling a method, that its object has some specific template arguments:

<?php

/**
 * @template T
 */
class a {
    /**
     * @var T
     */
    private $data;
    /**
     * @param T $data
     */
    public function __construct($data) {
        $this->data = $data;
    }
    /**
     * @psalm-if-this-is a<int>
     */
    public function test(): void {
    }
}

$i = new a(123);
$i->test();

$i = new a("test");
// IfThisIsMismatch - Class is not a<int> as required by psalm-if-this-is
$i->test();