psalm Documentation

A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications

RAW Doc

Technical Documentation: vimeo/psalm

ℹ️ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference

Overview (`docs/README.md`)

About Psalm

Psalm is a static analysis tool that attempts to dig into your program and find as many type-related bugs as possible.

It has a few features that go further than other similar tools:

  • Mixed type warnings
    If Psalm cannot infer a type for an expression then it uses a mixed placeholder type. mixed types can sometimes mask bugs, so keeping track of them helps you avoid a number of common pitfalls.

  • Intelligent logic checks
    Psalm keeps track of logical assertions made about your code, so if ($a && $a) {} and if ($a && !$a) {} are both treated as issues. Psalm also keeps track of logical assertions made in prior code paths, preventing issues like if ($a) {} elseif ($a) {}.

  • Property initialisation checks
    Psalm checks that all properties of a given object have values after the constructor is called.

  • Taint analysis
    Psalm can detect security vulnerabilities in your code.

  • Language Server
    Psalm has a Language Server that’s compatible with a range of different IDEs.

  • Automatic fixes
    Psalm can fix many of the issues it finds automatically.

  • Automatic refactoring
    Psalm can also perform simple refactors from the command line.

Example output

Given a file implode_strings.php:

php
<?php
$a = ['foo', 'bar'];
echo implode($a, ' ');
bash
> ./vendor/bin/psalm implode_strings.php
ERROR: InvalidArgument - somefile.php:3:14 - Argument 1 of implode expects `string`, `array` provided (see https://psalm.dev/004)

Inspirations

There are two main inspirations for Psalm:

  • Etsy's Phan, which uses nikic's php-ast extension to create an abstract syntax tree
  • Facebook's Hack, a PHP-like language that supports many advanced typing features natively, so docblocks aren't necessary.

Index


Contributing Index (`docs/contributing/index.md`)

Contributing to Psalm

Psalm is made possible through the contributions of hundreds of developers.

Hopefully you can be one of them?

Getting started

Here’s a rough guide to the codebase.

Here's the philosophy underpinning the Psalm’s development.

I've also put together a list of Psalm’s complexities.

Are you looking for low-hanging fruit? Here are some GitHub issues that shouldn't be too difficult to resolve.

Don’t be afraid!

One great thing about working on Psalm is that it’s very hard to introduce any sort of type error in Psalm’s codebase. There are almost 5,000 PHPUnit tests, so the risk of you messing up (without the CI system noticing) is very small.

Why static analysis is cool

Day-to-day PHP programming involves solving concrete problems, but they're rarely very complex. Psalm, on the other hand, attempts to solve a pretty hard collection of problems, which then allows it to detect a ton of bugs in PHP code without actually executing that code.

There's a lot of interesting theory behind the things Psalm does, too. If you want you can go very deep, though you don't need to know really any theory to improve Psalm.

Lastly, working to improve static analysis tools will also make you a better PHP developer – it'll help you think more about how values flow through your program.

Guides

Pull Requests

Before you send a pull request, make sure you follow these guidelines:

Run integration checks locally: composer tests

If you're adding new features or fixing bugs, don’t forget to add tests!


Security Analysis Index (`docs/security_analysis/index.md`)

Security Analysis in Psalm

Psalm can attempt to find connections between user-controlled input (like $_GET['name']) and places that we don’t want unescaped user-controlled input to end up (like echo "<h1>$name</h1>" by looking at the ways that data flows through your application (via assignments, function/method calls and array/property access).

You can enable this mode with the --taint-analysis command line flag.

Tainted input is anything that can be controlled, wholly or in part, by a user of your application. In taint analysis, tainted input is called a taint source.

Example sources:

  • $_GET[β€˜id’]
  • $_POST['email']
  • $_COOKIE['token']

Taint analysis tracks how data flows from taint sources into taint sinks. Taint sinks are places you really don’t want untrusted data to end up.

Example sinks:

  • ">
  • $pdo->exec("select * from users where name='" . $name . "'")

Taint Types

Psalm recognises a number of taint types by default, defined in the Psalm\Type\TaintKind class:

  • sql - used for strings that could contain SQL
  • ldap - used for strings that could contain a ldap DN or filter
  • html - used for strings that could contain angle brackets or unquoted strings
  • has_quotes - used for strings that could contain unquoted strings
  • shell - used for strings that could contain shell commands
  • callable - used for callable strings that could be user-controlled
  • unserialize - used for strings that could contain a serialized string
  • include - used for strings that could contain a path being included
  • eval - used for strings that could contain code
  • ssrf - used for strings that could contain text passed to Curl or similar
  • url_component - used for strings that could contain URL syntax (/, ?, &, #...) placed in the path, query or fragment of a URL
  • url_path - used for strings that could be a . or .. segment of the path of a URL, even URL-encoded
  • file - used for strings that could contain a path
  • cookie - used for strings that could contain a http cookie
  • header - used for strings that could contain a http header
  • user_secret - used for strings that could contain user-supplied secrets
  • system_secret - used for strings that could contain system secrets

You're also free to define your own taint types when defining custom taint sources – they're just strings.

Taint Sources

Psalm defines the following default taint sources:

  • the $_GET, $_POST, $_COOKIE and $_REQUEST server variables;
  • the entries of $_SERVER the client sends: the request headers (HTTP_*), the URI and what is taken from it (REQUEST_URI, QUERY_STRING, PATH_INFO, PHP_SELF, argv, ...), REQUEST_METHOD, CONTENT_TYPE and the HTTP authentication credentials, the same entries of a redirected request (REDIRECT_*), and any entry read with a key that is not a literal;
  • the names, types and full paths of the uploaded files in $_FILES;
  • reading from the input stream: fopen(), file_get_contents() and file() called with a literal php://input or php://stdin path, and the stream reading functions (fgets(), fread(), stream_get_contents(), ...) applied to such a handle;
  • the predefined STDIN constant (the php://stdin stream).
  • reading from the network: socket_read(), the data socket_recv(), socket_recvfrom() and socket_recvmsg() write to their by-reference parameter, stream_socket_recvfrom(), curl_exec() and curl_multi_getcontent(), and the streams opened with fsockopen(), pfsockopen(), stream_socket_client(), stream_socket_accept() and socket_export_stream() (so reading from them with fgets(), fread(), ... is a source too).
  • DNS answers, which whoever runs the name servers of a domain chooses: dns_get_record(), the hosts getmxrr() writes to its by-reference parameter, and gethostbyaddr().
  • the command-line options getopt() returns, which whoever runs the script chooses.
  • exceptions, since the analysis does not follow them from where they are thrown to where they are caught: their message (getMessage()), their trace (getTraceAsString()) and their string form, and the variable of a catch block, so dumping a caught exception (print_r(), var_export()) is a source too.

You can also define your own taint sources.

Taint Sinks

Psalm currently defines a number of different sinks for builtin functions and methods, including echo, include, header.

What is written to a stream opened on a literal php://output path, which is the response, or php://stdout, which is the response under the CGI SAPI, is a sink as what echo outputs is: file_put_contents() to such a path, and fwrite(), fputs(), fputcsv(), fprintf(), vfprintf(), stream_copy_to_stream() and the fwrite() and fputcsv() methods of SplFileObject on a stream opened with fopen() or new SplFileObject() on such a path in the same function, and held by its variables.

You can also define your own taint sinks.

Avoiding False-Positives

Nobody likes to wade through a ton of false-positives – here’s a guide to avoiding them.

Limitations

Taint Analysis relies on not making any mistakes when escaping values, e.g.

php
$sql = 'SELECT * FROM users WHERE id = ' . $mysqli->real_escape_string((string) $_GET['id']);

$html = "
  <img src=" . htmlentities((string) $_GET['img']) . " alt='' />
  <a href='" . htmlentities((string) $_GET['a1']) . "'>Link 1</a>
  <a href='" . htmlentities((string) $_GET['a2']) . "'>Line 2</a>";

// Details:
//    $id  = 'id'                   - Missing quotes
//    $img = '/ onerror=alert(1)'   - Missing quotes
//    $a1  = 'javascript:alert(1)'  - Normal inline JavaScript
//    $a2  = '/' onerror='alert(1)' - Pre PHP 8.1, single quotes are not escaped by default
// Test:
//    /?id=id&img=%2F+onerror%3Dalert%281%29&a1=javascript%3Aalert%281%29&a2=%2F%27+onerror%3D%27alert%281%29

To avoid these issues, use Parameterised Queries for SQL and Commands (e.g. exec); and a context-aware templating engine for HTML. Then use the literal-string type to ensure sensitive strings are defined in your application (i.e. have been written by a developer).

Security Issues

The issues of security analysis (TaintedHtml, TaintedSql, ...) flag potential vulnerabilities rather than type errors or code-quality problems. To tell them apart, Psalm's human-readable reports (console, compact, table, by-issue-level and phpstorm) show them under a SECURITY header instead of ERROR:

text
SECURITY: TaintedHtml - src/index.php:2:1 - Detected tainted HTML

A security issue configured with errorLevel="info" is still shown as INFO. Machine-readable reports (JSON, checkstyle, SARIF, ...) keep their usual severities; the JSON and XML reports also include an is_security field.

Plugins can mark their own issues the same way by implementing the Psalm\Issue\SecurityIssue interface.

Using Baseline With Taint Analysis

Since taint analysis is performed separately from other static code analysis, it makes sense to use a separate baseline for it.

You can use --use-baseline=PATH option to set a different baseline for taint analysis.

Viewing Results in a User Interface

Psalm supports the SARIF standard for exchanging static analysis results. This enables you to view the results in any SARIF compatible software, including the taint flow.

GitHub Code Scanning

GitHub code scanning can be set up by using the Psalm GitHub Action.

Alternatively, the generated SARIF file can be manually uploaded as described in the GitHub documentation.

The results will then be available in the "Security" tab of your repository.

Other SARIF compatible software

To generate a SARIF report run Psalm with the --report flag and a .sarif extension. For example:

bash
psalm --report=results.sarif

Debugging the taint graph

Psalm can output the taint graph using the DOT language. This is useful when expected taints are not detected. To generate a DOT graph run Psalm with the --dump-taint-graph flag. For example:

bash
psalm --taint-analysis --dump-taint-graph=taints.dot
dot -Tsvg -o taints.svg taints.dot

Annotating Code Adding Assertions (`docs/annotating_code/adding_assertions.md`)

Adding assertions

Psalm has five 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/@psalm-this-out (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
<?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
<?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
<?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
<?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
<?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
<?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 use @psalm-this-out to change the template arguments of a method after a method call, to reflect changes to the object's internal state.
You can also make assertions on the object's template arguments using @psalm-if-this-is.

php
<?php

/**
 * @template T
 */
class a {
    /**
     * @var list<T>
     */
    private array $data;
    /**
     * @param T $data
     */
    public function __construct($data) {
        $this->data = [$data];
    }
    /**
     * @template NewT
     * 
     * @param NewT $data
     * 
     * @psalm-this-out self<T|NewT>
     * 
     * @return void
     */
    public function addData($data) {
        /** @var self<T|NewT> $this */
        $this->data []= $data;
    }
    /**
     * @template NewT
     * 
     * @param NewT $data
     * 
     * @psalm-this-out self<NewT>
     * 
     * @return void
     */
    public function setData($data) {
        /** @var self<NewT> $this */
        $this->data = [$data];
    }
    /**
     * @psalm-if-this-is a<int>
     */
    public function test(): void {
    }
}

$i = new a(123);
// OK - $i is a<123>
$i->test();

$i->addData(321);
// OK - $i is a<123|321>
$i->test();

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

@psalm-this-out (and its aliases @psalm-self-out, @phpstan-self-out and @phpstan-this-out) also accepts a conditional type keyed off one of the method's parameters, e.g. @phpstan-this-out ($key is null ? static<TKey|int, TValue> : $this). The branch is resolved from the argument passed at each call site, so a call that omits $key (or passes null) infers a different resulting type than one that passes a concrete value. @psalm-if-this-is does not support this form, since its condition must be known before the call's arguments are analyzed.


Annotating Code Assertion Syntax (`docs/annotating_code/assertion_syntax.md`)

Assertion syntax

Psalm’s assertion annotation supports a number of different assertions.

Psalm assertions are of the form

@psalm-assert(-if-true|-if-false)? (Assertion) (Variable or Property)

Assertion here can have many forms:

Regular assertions

is_xxx assertions

Most is_xxx PHP functions have companion assertions e.g. int for is_int. Here's the full list:

  • int
  • float
  • string
  • bool
  • scalar
  • callable
  • countable
  • array
  • iterable
  • numeric
  • resource
  • object
  • null

So a custom version is_int could be annotated in Psalm as

php
<?php
/** @psalm-assert-if-true int $x */
function custom_is_int($x) {
  return is_int($x);
}

Object type assertions

Any class can be used as an assertion e.g.

@psalm-assert SomeObjectType $foo

Generic assertions

Generic type parameters can also now be asserted e.g.

@psalm-assert array $foo

Negated assertions

Any assertion above can be negated:

This asserts that $foo is not an int:

php
<?php
/** @psalm-assert !int $foo */

This asserts that $bar is not an object of type SomeObjectType:

php
<?php
/** @psalm-assert !SomeObjectType $bar  */

Bool assertions

This asserts that $bar is true:

php
<?php
/** @psalm-assert true $bar  */

This asserts that $bar is not false:

php
<?php
/** @psalm-assert !false $bar  */

Equality assertions

Psalm also supports the equivalent of assert($some_int === $other_int) in the form

php
<?php
/** @psalm-assert =int $some_int */

There are two differences between the above assertion and

php
<?php
/** @psalm-assert int $some_int */

Firstly, the negation of =int has no meaning:

php
<?php
/** @psalm-assert-if-true =int $x */
function equalsFive($x) {
  return is_int($x) && $x === 5;
}

function foo($y) : void {
  if (equalsFive($y)) {
    // $y is definitely an int
  } else {
    // $y might be an int, but it might not
  }
}

function bar($y) : void {
  if (is_int($y)) {
    // $y is definitely an int
  } else {
    // $y is definitely not an int
  }
}

Secondly, calling equalsFive($some_int) is not a RedundantCondition in Psalm, whereas calling is_int($some_int) is.


Annotating Code Purity Model (`docs/annotating_code/purity_model.md`)

The Purity Model

Psalm describes what a function, method or closure may do as a set of capabilities:
permissions to perform particular side effects, such as writing a property or printing. The
model comes from Hack's contexts and capabilities.
It replaces the three fixed levels of Psalm 6 (@psalm-pure, @psalm-mutation-free and
@psalm-external-mutation-free), which are now three points in a larger space.

This page explains how the model works. The annotations are listed in
Supported Annotations. The examples only
mark the issues they are about: Psalm also suggests annotations for the unannotated code in them
(see Inferring annotations).

Capabilities

Capability Allows
read-props reading properties of mutable objects, including $this (immutable objects never need it, nor objects the caller created with new from a class whose contract needs at most read-props|write-this-props|write-refs, or whose constructor can't keep a reference to them, see Method calls)
write-this-props writing or unsetting properties of $this
write-props writing or unsetting properties of any other object
read-globals reading static properties and superglobals, and binding global variables
write-globals writing them, including through a bound global variable, and using static variables
write-refs writing through by-reference parameters and other references into another scope
io echo, print, exit with a message, and builtins with side effects (random_int, file_put_contents, …)
time reading the clock (time, microtime, hrtime, date and getdate without a timestamp, …)

Builtins touching process-wide state (mt_rand, ini_set, spl_autoload_register, …) need
write-globals instead of io.

Two names stand for the extremes: pure is the empty set, a
pure function whose result depends only on its
arguments, and impure is every capability. A set is written with | or commas:
read-props|write-this-props.

A function-like may only do something, or call something, when its own set contains every
capability that needs. Code without an annotation is impure: it may do anything, and nothing
inside it is checked.

Each name means that one capability only. Writing does not include reading, so $this->n++
needs read-props|write-this-props, and code that writes both $this and other objects needs
write-this-props|write-props. write-props does not include write-this-props, and
write-globals does not include read-globals. A plain assignment only needs the write
capability: $this->n = 0 needs write-this-props, Counter::$count = 0 needs write-globals.
Using $this at all needs read-props or write-this-props, so a setter returning $this may be
write-this-props.

php
<?php
final class Counter {
    public static int $count = 0;
    private int $n = 0;

    /** @psalm-capabilities read-props|write-this-props */
    public function next(): int {
        return ++$this->n;
    }

    /** @psalm-capabilities write-this-props */
    public function nextWrong(): int {
        return ++$this->n; // ImpurePropertyFetch: The context is write-this-props but accessing a property on a mutable object requires read-props
    }
}

/** @psalm-capabilities read-globals */
function total(): int {
    return Counter::$count;
}

/** @psalm-capabilities read-globals */
function resetTotal(): int {
    Counter::$count = 0; // ImpureStaticProperty: The context is read-globals but writing a static property requires write-globals
    return 0;
}

Global state

Values reached from global state stay bound to it. An object read from a static property, a
superglobal or a global variable, returned by a function that may read globals, or fetched from
such an object, can only have its properties written, its mutating methods called, or be passed
to a function that may write properties, by code that has write-globals. This makes
read-globals a read-only view of global state, like Hack's readonly values:

php
<?php
final class Config {
    public string $env = "prod";
    public static ?Config $instance = null;
}

/** @psalm-capabilities read-globals */
function config(): ?Config {
    return Config::$instance;
}

/** @psalm-capabilities read-globals|write-props */
function tamper(): void {
    $config = config();
    if ($config !== null) {
        $config->env = "dev"; // ImpurePropertyAssignment: The context is write-props|read-globals but property assignment to Config::$env on an object reached from global state requires write-props|write-globals
    }
}

A new object the caller created (see Method calls) is not global state itself,
even when its constructor read global state, or when it is a clone of an object reached from
global state: writing its own properties is free. What it holds may be global state, so writing
another object through it, or calling a method that does, costs write-globals too.

Annotations

Every purity annotation is a name for a capability set:

Annotation Used on Capabilities
@psalm-pure function, method, class pure; on a class, properties can't be used at all
@psalm-capabilities <set> function, method, closure, class <set>
@psalm-impure function, method impure, written out
@psalm-immutable class read-props for every method, and every property is readonly
@psalm-mutable class impure, written out
@psalm-mutation-free function, method, class read-props (legacy)
@psalm-external-mutation-free function, method, class read-props|write-this-props|write-refs (legacy)
none impure

@pure, @phpstan-pure, #[Psalm\Pure] and #[JetBrains\PhpStorm\Pure] mean the same as
@psalm-pure; #[Psalm\Immutable] and #[JetBrains\PhpStorm\Immutable] the same as
@psalm-immutable; #[Psalm\ExternalMutationFree] the same as @psalm-external-mutation-free.

Capability aliases

A capability set can be named once with a type alias and used in @psalm-capabilities, in
closure types and as a purity argument, like Hack's context constants. Aliases of other classes
are imported with @psalm-import-type:

php
<?php
/** @psalm-type Storage = write-props|io */
final class Repo {
    /** @psalm-capabilities Storage */
    public function save(): void { echo "saved"; }
}

/** @psalm-import-type Storage from Repo */
final class Service {
    /**
     * @psalm-capabilities Storage
     * @param Closure[Storage](): void $after
     */
    public function run(Repo $repo, Closure $after): void {
        $repo->save();
        $after();
    }
}

Conditional capabilities

What a call requires may depend on its arguments, like a conditional return type. Each call
requires the branch its arguments select. Where the arguments may go either way, or there is no
call (a callable taken from the function), it requires both. The body may do what either branch
allows.

php
<?php
/** @psalm-capabilities ($seed is null ? io : pure) */
function makeId(?int $seed = null): int {
    return $seed ?? random_int(0, PHP_INT_MAX);
}

/** @psalm-pure */
function fromSeed(int $seed): int {
    return makeId($seed); // fine: $seed can't be null
}

/** @psalm-pure */
function fresh(): int {
    return makeId(); // ImpureFunctionCall: The context is pure but function call on makeid requires io
}

The constructor of MongoDB\BSON\ObjectId is stubbed this way: without an id it generates one
from the clock and a counter of the process, while given an id it only validates it.

Closures

Closures don't need an annotation. Psalm infers their capabilities from their bodies and keeps
them in their type, for example Closure[io](int): void (see
callable types). Creating a closure is not an
effect: a pure function may build and return an impure closure. Calling it, or passing it to a
parameter that expects fewer capabilities, is checked.

A closure that captures a variable by reference (use (&$x)) shares it with the enclosing
scope: reading it needs read-props and writing it write-this-props, as if it were a property
of the closure, so such a closure is not pure. The enclosing scope may still call it freely,
since the variable is its own, and only needs a capability when the variable is shared further:
write-refs for a by-reference parameter, write-globals for a global.

php
<?php
/**
 * @psalm-pure
 * @param list<int> $numbers
 */
function sum(array $numbers): int {
    $total = 0;
    $add = function (int $value) use (&$total): void { $total += $value; };
    foreach ($numbers as $number) {
        $add($number); // fine: $total belongs to sum()
    }
    return $total;
}

/**
 * @psalm-pure
 * @param Closure[pure](int): void $callback
 */
function callWithOne(Closure $callback): int {
    $callback(1);
    return 1;
}

/** @psalm-pure */
function leak(): int {
    $total = 0;
    return callWithOne(function (int $value) use (&$total): void { $total += $value; }); // ArgumentTypeCoercion: Argument 1 of callWithOne expects Closure[pure](int):void, but parent type Closure[read-props|write-this-props](int):void provided
}

Likewise, an object the enclosing scope created and the closure captures, by value or by
reference, is state of the closure, as an object a class instance creates is its own. So are the
objects in a captured array the enclosing scope created, and the state of a closure that a closure
created in it captures. Writing that state, or calling a method that writes it, needs
write-this-props, and the enclosing scope may call the closure freely. An object the enclosing
scope did not create, like a parameter, belongs to someone else: writing it needs write-props.

Class-level contracts

@psalm-capabilities on a class or interface is a contract for the class and for everything that
extends or implements it. Every method of the class gets those capabilities, and may declare fewer
but not more:

php
<?php
/** @psalm-capabilities io */
final class Logger {
    public static int $level = 0;

    public function log(string $message): void {
        echo $message; // fine: the class allows io
    }

    /** @psalm-capabilities io|write-globals */
    public function setLevel(int $level): void { // ImpureFunctionCall: setLevel is marked @psalm-capabilities write-globals|io but its containing class allows fewer capabilities, @psalm-capabilities io
        self::$level = $level;
    }
}

A class extending or implementing one with a contract must have a contract that fits inside it;
otherwise Psalm reports ImmutableDependency.
A marker interface, declaring no method or property and inheriting none, is pure without an
annotation, as it does nothing; unlike an interface marked @psalm-pure, it caps none of its
implementers.
Abstract methods, and the methods of interfaces, have no body to infer anything from, so each of
them needs its own annotation, even inside a class with a contract
(MissingAbstractPureAnnotation); use
@psalm-impure for one that may do anything.

How calls are charged

A caller normally needs every capability of the function-likes it calls. Two kinds of calls are
charged more precisely.

Method calls

A method that needs write-this-props writes its own $this, which may or may not be the
caller's. The call is charged according to the receiver:

  • the caller's own $this, also when a call on it of a method returning its receiver gives it
    back (see Fluent calls): write-this-props;
  • an object the caller created with new, from a class whose contract needs at most
    read-props|write-this-props|write-refs, or whose constructor can't keep a reference to it:
    nothing, as nobody else can see it change;
  • any other object, including a parameter, a property of $this, or an object returned by
    another call: write-props.
php
<?php
/** @psalm-capabilities read-props|write-this-props */
final class Counter {
    private int $n = 0;

    public function inc(): void {
        $this->n++;
    }

    public function incTwice(): void {
        $this->inc(); // fine: costs write-this-props
        $this->inc();
    }

    public function get(): int {
        return $this->n;
    }
}

/** @psalm-pure */
function countTwice(): int {
    $counter = new Counter();
    $counter->inc(); // fine: $counter was created here
    $counter->inc();
    return $counter->get();
}

/** @psalm-capabilities read-props */
function bump(Counter $counter): int {
    $counter->inc(); // ImpureMethodCall: The context is read-props but method Counter::inc requires write-props
    return $counter->get();
}

Reading or unsetting a property gets the same waiver: it is free on an object the caller created
with new, from a class whose contract needs at most read-props|write-this-props|write-refs, as
nobody else can change it. Reading a property of any other mutable object, including $this, a
parameter or an object held in a property, costs read-props, whatever the class's contract:

php
<?php
/** @psalm-capabilities read-props|write-this-props */
final class Tally {
    public int $n = 0;

    public function inc(): void {
        $this->n++;
    }
}

/** @psalm-pure */
function tallyTwice(): int {
    $tally = new Tally();
    $tally->inc();
    $tally->inc();
    return $tally->n; // fine: $tally was created here
}

/** @psalm-pure */
function currentTally(Tally $tally): int {
    return $tally->n; // ImpurePropertyFetch: The context is pure but accessing a property on a mutable object requires read-props
}

Without a class-level contract, calling a method on an object the caller created with new, and
reading or writing its properties, is still free, as long as the constructor can't keep a reference
to the new object anywhere. Only
three things can hand $this to someone else: writing another object (write-props), global state
(write-globals) or a by-reference parameter (write-refs). So a constructor that needs none of
these qualifies, and so does a class without a constructor:

php
<?php
final class Greeting {
    /** @var list<string> */
    private array $words = [];

    /** @psalm-capabilities read-props */
    public function __construct(string $first) {
        $this->words[] = $first;
    }

    /** @psalm-capabilities read-props|write-this-props */
    public function add(string $word): void {
        $this->words[] = $word;
    }

    /** @psalm-capabilities read-props */
    public function text(): string {
        return implode(" ", $this->words);
    }
}

/** @psalm-pure */
function hello(): string {
    $greeting = new Greeting("hello");
    $greeting->add("world"); // fine: the constructor can't hand $greeting to anyone
    return $greeting->text();
}

A constructor without a purity annotation qualifies too when it is trivial: it does nothing, or only
assigns its parameters (or constant values) to properties its class declares, promoted ones included.
Such a constructor needs write-this-props without being annotated (nothing at all if it is empty), so
neither new nor its class needs an annotation for the waiver. Any other statement makes it non-trivial:
a call (parent::__construct() included), a by-reference parameter, a default value that creates an
object or calls something, a property with hooks or one the class doesn't declare before the
constructor, or $this stored anywhere. A non-trivial constructor without an annotation may do
anything, as any unannotated method. So may a trivial constructor run for a class that redeclares one
of the properties it assigns with a hook (PHP 8.4), or by new static() / new $class() where the
class may be extended: the hook may do anything.

php
<?php
final class Point {
    public int $z = 0;

    public function __construct(public int $x, public int $y) {}
}

/** @psalm-pure */
function lift(int $x, int $y): Point {
    $point = new Point($x, $y);
    $point->z = 1; // fine: Point's constructor is trivial
    return $point;
}

A constructor that may store $this somewhere loses the waiver. Here the constructor registers
every new object in a static property, so calling a method on the new object costs what it would
on any other one. The object is also reached from global state now, which adds write-globals:

php
<?php
final class Registered {
    /** @var list<Registered> */
    public static array $all = [];

    public int $uses = 0;

    /** @psalm-capabilities read-globals|write-globals */
    public function __construct() {
        self::$all[] = $this;
    }

    /** @psalm-capabilities read-props|write-this-props */
    public function use(): void {
        $this->uses++;
    }
}

/** @psalm-capabilities read-globals|write-globals */
function register(): Registered {
    $registered = new Registered();
    $registered->use(); // ImpureMethodCall: The context is read-globals|write-globals but method Registered::use requires write-props|write-globals
    return $registered;
}

A constructor that writes another object, such as linking the new object to a parent passed to
it, or that writes a by-reference parameter, loses it the same way.

A clone is a new object too, by the same rule: the copy is free to mutate unless the class's
__clone needs write-props, write-globals or write-refs, and so may hand the copy to someone
else. A class-level contract keeps the waiver, for a clone as for a new object:

php
<?php
final class Tracked {
    /** @var list<Tracked> */
    public static array $copies = [];

    public int $uses = 0;

    /** @psalm-capabilities read-props|read-globals|write-globals */
    public function __clone() {
        self::$copies[] = $this;
    }

    /** @psalm-capabilities read-props|write-this-props */
    public function use(): void {
        $this->uses++;
    }
}

/** @psalm-capabilities read-props|read-globals|write-globals */
function copyTracked(Tracked $tracked): Tracked {
    $copy = clone $tracked;
    $copy->use(); // ImpureMethodCall: The context is read-props|read-globals|write-globals but method Tracked::use requires write-props
    return $copy;
}

When the class isn't known exactly, there is no waiver: new static() in a class that may be
extended, and new $class(), may run the constructor of a subclass that keeps a reference:

php
<?php
/** @psalm-consistent-constructor */
class Item {
    public int $count = 0;

    /** @psalm-capabilities read-props|write-this-props */
    public function bump(): void {
        $this->count++;
    }
}

/** @psalm-capabilities read-props|write-this-props */
function makeItem(): Item {
    $item = new Item();
    $item->bump(); // fine: an Item, whose constructor can't keep $item
    return $item;
}

/**
 * @param class-string<Item> $class
 * @psalm-capabilities read-props|write-this-props
 */
function makeAny(string $class): Item {
    $item = new $class();
    $item->bump(); // ImpureMethodCall: The context is read-props|write-this-props but method Item::bump requires write-props
    return $item;
}

This waiver covers calling methods, and writing and reading properties: reading a property of
such an object directly is free too, as calling a read-props method on it is. What the property
holds is another matter: reading a property of an object stored in it costs read-props, as that
object may be shared.

The write-this-props that a class purity template of the receiver is bound to
(Doer[write-this-props], see Purity templates) is charged the same way,
except that it is never waived for a new object. A receiver reached from
global state also costs write-globals.

Fluent calls

A method returns its receiver if it declares @return $this, or if no override can replace it
(it is private, final or in a final class) and each of its returns is return $this;. A call on
$this of such a method gives back $this, so a fluent chain on $this only mutates $this,
and so does writing a property of what the chain returns. @return static is not enough: the
method may return another instance of the class.

@return $this is a promise, checked like a return type: the method and every override of it may
only return $this, or a call on it of another method returning its receiver; any other return is
an InvalidReturnStatement.

php
<?php
abstract class Query {
    /** @var list<string> */
    private array $conditions = [];

    /**
     * @return $this
     * @psalm-capabilities read-props|write-this-props
     */
    public function where(string $condition): static {
        $this->conditions[] = $condition;
        return $this;
    }

    /** @psalm-capabilities read-props|write-this-props */
    public function whereBetween(string $from, string $to): static {
        return $this->where($from)->where($to); // fine: costs write-this-props
    }

    /** @psalm-capabilities read-props */
    public function copy(): static {
        return clone $this;
    }

    /** @psalm-capabilities read-props|write-this-props */
    public function copyWhere(string $condition): static {
        return $this->copy()->where($condition); // ImpureMethodCall: The context is read-props|write-this-props but method Query::where requires write-props
    }
}

By-reference arguments

A call to a function-like with write-refs does not cost write-refs itself. Each argument
passed by reference costs what writing it directly would cost: nothing for a local variable,
write-refs for one of the caller's own by-reference parameters, write-this-props for a property
of $this, write-props for another object's property, and write-globals for global state.
The callee itself still only has write-refs. This includes builtins such as sort(), so a pure
function may sort a local array.

php
<?php
final class Stats {
    public static int $n = 0;
}

/** @psalm-capabilities write-refs */
function inc(int &$i): void {
    $i++;
}

/** @psalm-capabilities write-refs */
function update(int &$ref): void {
    $local = 0;
    inc($local);    // fine: $local belongs to update()
    inc($ref);      // fine: like writing $ref directly, costs write-refs
    // both reported on the next line:
    // ImpureStaticProperty: The context is write-refs but reading a static property requires read-globals
    // ImpureFunctionCall: The context is write-refs but function call on inc requires write-globals
    inc(Stats::$n);
}

Implicit calls

Everything a function-like does is checked, including what happens implicitly: clone calls
__clone, string interpolation and casts call __toString, $object() calls __invoke, array
access on objects calls the ArrayAccess methods, throw new and new $className call the
constructor (an unknown class may do anything), and foreach over an object calls its iteration
methods (see Iterators and generators). A callable string or array
whose target is not known may do anything, so a pure function may not call one.

Destructors

Destroying an object calls its destructor, where the object dies. So a pure function may not
hold, in a local variable, an object it created with new whose __destruct has effects, unless
the object leaves the function (returned, stored, passed on, captured). Nor may it unset() a
variable, property or array element holding such an object, overwrite one by assigning it, or
discard one (new Guard();).

Only the declared destructors of the known classes of the dropped value count: a mixed,
object or template value is not charged. A property is assumed to hold nothing yet in its
class's constructor (and to be kept by the original in __clone), and a variable first assigned
in a loop body to hold a value like the one assigned when it is assigned again, the previous
iteration's.

Parameter default values

Parameter default values are evaluated like in Hack: with no capabilities at all, or with
read-globals|write-globals when the function-like has write-globals, whatever else it may do.
Only function-likes without a purity annotation may use anything in a default value, so new in
the defaults of unannotated code stays free.

Purity templates

Sometimes what a function does depends on what it is given: array_map() is pure when its
callback is pure. A purity template is a template parameter whose value is a capability set
instead of a type. It is declared with
@psalm-purity-template.

Binding a purity template

Purity arguments are written in square brackets, before any type arguments, as in Hack:

  • Closure[P](int): int, callable[pure](): void: the purity of a callable;
  • Handler[pure], Box[io]: the purity argument of a class with a purity template;
  • @extends Handler[pure], @implements Iterator[pure]: a subclass binding it.

The purity templates of a class come after its type templates, whatever the order of the
@template and @psalm-purity-template tags. A class with @template T and
@psalm-purity-template P is used as Box[pure], as Box for its default purity, or
as Box[pure] for its default type arguments. Several purity arguments are separated by commas
(Pair[pure, io]), and each is a capability set (write-props|io) or a purity template.
An empty set is written pure; Handler[] is an array of Handler.

Purity templates are covariant: a Handler[pure] can be used where a Handler[io] is expected.

The bounds of type templates may use purity templates, which are then inferred with them: a
class with @template TIterator as Traversable[P]<K, V> and @psalm-purity-template P,
constructed with a Generator[pure], is a
Foo[pure]>.

`@psalm-purity-from-template`

@psalm-purity-from-template P says that
each call needs the function-like's own capabilities plus whatever P is bound to at that call.
Inside the body, calling a closure whose purity is P is not an effect: each caller pays for it,
like with Hack's [ctx $f] contexts. It works for functions, methods, static methods and constructors. P may be a purity template of
the function-like or of its class, or a type template bound to a closure or callable type.

With a class-level template, subclasses decide the purity:

php
<?php
/** @psalm-purity-template P */
abstract class Handler {
    /**
     * @psalm-pure
     * @psalm-purity-from-template P
     */
    abstract public function handle(string $input): string;
}

/** @extends Handler[pure] */
final class Upper extends Handler {
    /** @psalm-pure */
    public function handle(string $input): string {
        return strtoupper($input);
    }
}

/** @extends Handler[io] */
final class Printer extends Handler {
    /** @psalm-capabilities io */
    public function handle(string $input): string {
        echo $input;
        return $input;
    }
}

/** @psalm-pure */
function viaUpper(Upper $handler): string {
    return $handler->handle('a'); // fine: Upper binds P to pure
}

/**
 * @psalm-pure
 * @param Handler[pure] $handler
 */
function viaPureHandler(Handler $handler): string {
    return $handler->handle('a'); // fine
}

/**
 * @psalm-pure
 * @psalm-purity-template Q
 * @param Handler[Q] $handler
 * @psalm-purity-from-template Q
 */
function viaSomeHandler(Handler $handler): string {
    return $handler->handle('a'); // fine: pure when given an Upper, io when given a Printer
}

/** @psalm-pure */
function viaAnyHandler(Handler $handler): string {
    return $handler->handle('a'); // ImpureMethodCall: The context is pure but method Handler::handle requires impure
}

With a function-level template, the closures passed in decide the purity:

php
<?php
/**
 * @psalm-pure
 * @psalm-purity-template P
 * @param Closure[P](int): int $callback
 * @psalm-purity-from-template P
 */
function apply(Closure $callback): int {
    return $callback(1);
}

/** @psalm-pure */
function usePure(): int {
    return apply(fn(int $x): int => $x + 1); // fine: the closure is pure
}

/** @psalm-capabilities io */
function useIo(): int {
    return apply(function (int $x): int { echo $x; return $x; }); // fine: the call costs io
}

/** @psalm-pure */
function usePrinting(): int {
    return apply(function (int $x): int { echo $x; return $x; }); // ImpureFunctionCall: The context is pure but function call on apply requires io
}

A type template bound to a closure type carries the closure's purity, so it works with
@psalm-purity-from-template too:

php
<?php
/**
 * @template T of callable(): void
 */
final class Deferred {
    /** @var T */
    private $callback;

    /**
     * @param T $callback
     * @psalm-capabilities write-this-props
     */
    public function __construct($callback) {
        $this->callback = $callback;
    }

    /**
     * @psalm-capabilities read-props
     * @psalm-purity-from-template T
     */
    public function run(): void {
        ($this->callback)();
    }
}

/**
 * @psalm-capabilities read-props
 * @param Deferred<pure-Closure(): void> $deferred
 */
function runPure(Deferred $deferred): void {
    $deferred->run(); // fine: T is a pure closure
}

The `_` shorthand

The common case, a function whose purity follows one closure parameter, needs no named template:
Closure[_](...) or callable[_](...) in a @param type declares one, and the function takes
its purity from that parameter, like Hack's (function()[_]: T) $f. This is the same apply() as
above:

php
<?php
/**
 * @psalm-pure
 * @param Closure[_](int): int $callback
 */
function apply(Closure $callback): int {
    return $callback(1);
}

_ may appear anywhere in a parameter's type (list<Closure[_](int): int>,
Traversable[_]). Every _ in one parameter's type stands for the same template,
which has no name of its own, so it never clashes with a template declared in the docblock. _
can only be used in @param types: in @return, @param-out, @psalm-assert, @var,
@property or @template bounds it has no parameter to stand for.

On a promoted constructor parameter, _ makes new take the purity of the closure passed for
it, as for any other parameter. The property, though, outlives that call, so its type uses the
template's bound: here $f is an impure-Closure(): int, and calling it needs every capability,
whatever closure was passed.

php
<?php
final class Holder {
    /**
     * @psalm-pure
     * @param Closure[_](): int $f
     */
    public function __construct(public Closure $f) {}
}

To keep the closure's purity in the property, use a class purity template instead
(@psalm-purity-template P on the class, @param Closure[P](): int $f on the constructor): then
new Holder(fn(): int => 1) is a Holder[pure], whose $f is a pure closure.

The parameters of an @method may use _ too. A call to such a method needs what __call (or
__callStatic) needs, plus the capabilities of the closures passed for its _ parameters:

php
<?php
/**
 * @method int run(Closure[_](): int $callback)
 */
final class Runner {
    /** @psalm-pure */
    public function __call(string $name, array $args): int { return 1; }
}

/** @psalm-pure */
function usePure(Runner $runner): int {
    return $runner->run(fn(): int => 1); // fine: the closure is pure
}

Defaults and bounds

The bounds of a purity template are written as a chain, lower <= Name(default) <= upper, where
every part but the name is optional:

  • the upper bound is the most a value may need (impure when left out): P <= write-props|io;
  • on a class, the lower bound is what every value needs, which the methods depending on the
    template may then use unconditionally: write-this-props <= C;
  • the default, in parentheses, applies to subclasses that don't bind the template: C(pure).

Several templates can be declared in one tag, separated by commas (P <= io, Q). In a chain with
a single bound, the side that is a capability is the bound: io <= C is a lower bound and
C <= io an upper bound. A type alias used as a lower bound needs the upper bound written out
as well (Alias <= C <= impure). In Hack these are the bounds and default of a context
constant, with the keywords reversed as contexts are types:
abstract const ctx C super [write_props, io] as [write_this_props] = [write_this_props].

A binding must include the lower bound itself: Doer[io] is rejected, write Doer[write-this-props|io].

php
<?php
/** @psalm-purity-template write-this-props <= C(write-this-props) <= write-this-props|write-props|io */
abstract class Doer {
    public int $runs = 0;

    /**
     * @psalm-capabilities read-props
     * @psalm-purity-from-template C
     */
    public function run(): int {
        $this->runs++; // fine: every C includes write-this-props
        return $this->runs;
    }
}

/** @extends Doer[write-this-props|io] */
final class LoudDoer extends Doer {} // fine: between the bounds

/** @extends Doer[write-globals] */
final class GlobalDoer extends Doer {} // InvalidTemplateParam: Extended template param C expects type write-this-props|write-props|io, type write-globals given

/** @extends Doer[pure] */
final class PureDoer extends Doer {} // InvalidTemplateParam: Extended template param C must include at least write-this-props, pure given

final class DefaultDoer extends Doer {} // C is write-this-props

Constructors

A constructor may take its purity from a class purity template. new Sub() then costs what Sub
binds the template to, or its default or upper bound when Sub doesn't bind it, and
new static() costs the template itself, as it may instantiate any class extending the class.

new can also bind a class purity template from what the constructor is given. A parameter
typed Closure[P] binds it from the purity of a closure. To bind it from the kind of argument
instead, give the constructor a @return: the type of the new expression, which is the class
itself with its template and purity arguments. Like any return type, it may be conditional.
ArrayObject and ArrayIterator are declared this way: their mutators only write the object
itself when it stores an array, but also write the object it wraps when given one. A class like
them can say so:

php
<?php
/**
 * @template TValue
 * @psalm-purity-template TStorage(write-props) <= write-props
 * @psalm-capabilities read-props|write-this-props
 */
final class Store {
    private int $writes = 0;

    /**
     * @param array<TValue>|object $storage
     * @return ($storage is array ? Store[pure]<TValue> : Store[write-props]<TValue>)
     * @psalm-pure
     */
    public function __construct(array|object $storage = []) {}

    /**
     * @param TValue $value
     * @psalm-purity-from-template TStorage
     */
    public function add($value): void {
        $this->writes++;
    }
}

/** @psalm-pure */
function fromArray(): int {
    $store = new Store([1]); // Store[pure]<int>
    $store->add(2); // fine: TStorage is pure, and $store was created here
    return 1;
}

/** @psalm-pure */
function fromObject(stdClass $object): int {
    $store = new Store($object); // Store[write-props]<mixed>
    $store->add(1); // ImpureMethodCall: The context is pure but method Store::add requires write-props
    return 1;
}

An argument that may be either an array or an object gives the union of both types, so calling
add() on the result costs write-props. A @return without a condition binds type templates
the same way: with @return Box on its constructor, new Box() is a Box.

A condition can also be given to each purity argument, inside the brackets, and a branch may be a
purity template of the constructor, bound from the argument. With
@param array<K, V>|Container[PP]<K, V> $param and @return self[$param is array ? pure : PP]<K, V>,
new Wrapper($container) takes the purity argument of $container, io for a
Container[io]<string, int>. Such conditional purity arguments are resolved per call in the
@return of any function or method, also nested (list>), and in
@psalm-self-out.

The @return of a constructor may only name its own class (or self or static); anything else
is an InvalidDocblock. Since it names the class itself, a subclass inheriting the constructor,
and new static() in a class that isn't final, get the type new would give without it.

Iterators and generators

Traversable, Iterator, IteratorAggregate and Generator have a purity template besides
their key and value templates, TPurity: what iterating over them may do.
Iterator[pure] can be iterated by a pure function, and
Generator[io] prints when consumed. It defaults to impure, so
Iterator still means an iterator about which nothing is known.

Iterating over a value known only by one of these types costs its TPurity and nothing else: a
pure function may consume any Iterator[pure] or
Generator[pure], including one it was given (where a generator is
paused is internal engine state). Iterating over an object of an iterator class calls that
class's own methods (rewind, valid, current, key, next, or getIterator() and then the
methods of the iterator it returns, which counts as new), so it costs what they need like any
other method call.

iterable has a purity too: iterable[pure] accepts arrays (iterating over an array
does nothing) and Traversable[pure], and a pure function may iterate over it. A
plain iterable is iterable[impure], since it may be any
Traversable. The purity of an iterable parameter may be a purity template
(iterable[P]), bound by each call to what iterating over the argument does.

A generator function-like with a purity annotation binds the TPurity of the Generator (or
Iterator, Traversable) it returns, or the purity of the iterable it returns, to its own
capabilities and purity templates, which every call resolves: consuming the generator costs what
running its body costs.

php
<?php
/**
 * @psalm-pure
 * @param Closure[_](): int $f
 * @return Generator<int, int>
 */
function map(Closure $f): Generator { yield $f(); return 0; }

/** @psalm-pure */
function sum(): int {
    $total = 0;
    foreach (map(fn(): int => 1) as $x) { // fine: a Generator[pure]<int, int, mixed, mixed>
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @param Generator[io]<int, int> $generator for example map(function (): int { echo "x"; return 1; })
 */
function printAll(Generator $generator): int {
    foreach ($generator as $x) {} // ImpureMethodCall: The context is pure but iterating over Generator[io]<int, int, mixed, mixed> requires io
    return 0;
}

A class implementing Iterator or IteratorAggregate may bind TPurity in its @implements
(@implements Iterator[pure]); its iteration methods (or its getIterator() and
what that returns) must then fit the binding. A class that doesn't bind it gets it from those
methods, as whoever iterates over it sees them: a method writing the iterator's own properties
makes it write-this-props|write-props. So MyIterator is accepted where
Iterator[pure] is expected exactly when its iteration methods are pure.

The SPL iterators wrapping another one (IteratorIterator, FilterIterator,
CallbackFilterIterator, LimitIterator, NoRewindIterator, InfiniteIterator) have a
TPurity too, bound at construction to what iterating over the inner iterator (and calling the
callback of a CallbackFilterIterator) does: new CallbackFilterIterator(gen(), fn(int $v): bool => $v > 0)
is a CallbackFilterIterator[pure]<...> when gen() returns a Generator[pure]<...>. A subclass of
FilterIterator that doesn't bind TPurity iterates impurely, whatever its accept() does.

Builtins taking callbacks

The builtins that call the closures they are given work the same way: array_map, usort,
preg_replace_callback, ArrayObject::uasort, Ds\Vector::map, … need what their callbacks
need and nothing else, and iterator_to_array and iterator_count what iterating over their
argument does. Fiber has a purity template for what its callback does, which starting or
resuming the fiber costs. Closure::bind() and bindTo() keep the type of the closure they
rebind, and $closure->call($newThis, ...) is a call of the closure that writes $newThis
where the closure writes $this.

Overrides

An override may need fewer capabilities than the method it overrides, never more. This holds for
every capability, and for interface methods as well as parent class methods:

php
<?php
/** @psalm-immutable */
interface Shape {
    /** @psalm-capabilities read-props */
    public function area(): float;
}

/** @psalm-immutable */
final class Square implements Shape {
    public function __construct(private float $side) {}

    public function area(): float { // fine: read-props, like Shape::area
        return $this->side * $this->side;
    }
}

/** @psalm-pure */
final class Unit implements Shape {
    public function area(): float { // fine: pure needs less than read-props
        return 1.0;
    }
}

// both reported on the class name:
// ImmutableDependency: Shape is marked with @psalm-immutable, but LoggedUnit is not
// ImmutableDependency: Shape::area is read-props, but LoggedUnit::area additionally requires io
/** @psalm-capabilities io */
final class LoggedUnit implements Shape {
    public function area(): float {
        echo 'area';
        return 1.0;
    }
}

ImmutableDependency is a level 1 issue, so it is
reported as info at the default level 2.

A subclass may bind a class purity template of its parent to one of its own
(@psalm-purity-template P with @extends Filter[P]), which each new then binds, possibly to
pure. So an override of a method with @psalm-purity-from-template may use the template only as
the parent does: what it needs unconditionally must fit what the parent's method needs
unconditionally, plus the template's lower bound. This is like overrides in Hack, which may only
use an abstract context constant through this::C.

Property refinements after calls

By default Psalm remembers what it knows about object properties across calls. With
rememberPropertyAssignmentsAfterCall="false",
it forgets refinements after a call, but only those the call could invalidate:

The callee may use Psalm forgets refinements of
write-this-props or write-props properties of mutable objects
write-globals static properties and superglobals
none of these nothing

This applies to function calls, method calls, static calls and new. With
rememberPropertyAssignmentsAfterCall="false", this code reports one issue:

php
<?php
final class Order {
    public ?int $total = null;
    public static ?string $currency = null;
}

/** @psalm-capabilities read-globals */
function currencyIsSet(): bool {
    return Order::$currency !== null;
}

/** @psalm-capabilities write-props */
function clear(Order $order): void {
    $order->total = null;
}

function kept(Order $order): int {
    if ($order->total === null) {
        return 0;
    }
    currencyIsSet();
    return $order->total; // fine: currencyIsSet() can't write properties
}

function forgotten(Order $order, Order $other): int {
    if ($order->total === null) {
        return 0;
    }
    clear($other);
    return $order->total; // NullableReturnStatement: clear() may have written $order->total
}

Inferring annotations

Psalm infers the capabilities of unannotated code from its body and the callees it uses. It does
so as a fixpoint over the call graph, after the whole codebase has been analysed: a function-like
that only calls pure function-likes, annotated or inferred, is pure itself, whatever the order in
which they are declared, and mutual recursion, recursive closures and closures assigned to a
variable are handled. It reports code that could be annotated:

The first two can be fixed automatically:

bash
vendor/bin/psalm --alter --issues=MissingPureAnnotation,MissingImmutableAnnotation

Given this code:

php
<?php
function slug(string $title): string { // MissingPureAnnotation: slug must be marked @psalm-pure to aid security analysis, run with --alter --issues=MissingPureAnnotation to fix this
    return strtolower(trim($title));
}

final class Cart { // MissingImmutableAnnotation: Cart must be marked @psalm-capabilities read-props|write-this-props|write-refs to aid security analysis, run with --alter --issues=MissingImmutableAnnotation to fix this
    private int $items = 0;

    public function addItem(): void { // MissingPureAnnotation: addItem must be marked @psalm-capabilities read-props|write-this-props|write-refs to aid security analysis, run with --alter --issues=MissingPureAnnotation to fix this
        $this->items++;
    }

    public function count(): int {
        return $this->items;
    }
}

--alter adds @psalm-pure to slug(), and @psalm-capabilities read-props|write-this-props|write-refs
to Cart and addItem(). Suggestions use the four named levels only: pure, read-props,
read-props|write-this-props|write-refs and impure (@psalm-immutable for a class at
read-props). Write a narrower set by hand if you want one.

A class-level contract binds every subclass too, so the one suggested
for a class is the smallest of these levels that allows:

  • what the class's own methods need;
  • the contract of its parent class;
  • what each known subclass needs, or what its contract allows if it has one;
  • for a class but not an interface, the upper bounds of its purity templates, up to which
    subclasses may bind them.

So an abstract base class whose subclasses do more, or a class with a purity template whose upper
bound is left as impure, gets no class-level suggestion. A subclass without an annotation is
suggested a contract on the next run, once its parent has one.

php
<?php
abstract class Shape { // fine: no suggestion, as only impure covers Circle
    /** @psalm-pure */
    public function name(): string {
        return 'shape';
    }
}

final class Circle extends Shape {
    public function draw(): void {
        echo 'o';
    }
}

class Base { // MissingImmutableAnnotation: Base must be marked @psalm-capabilities read-props|write-this-props|write-refs to aid security analysis, run with --alter --issues=MissingImmutableAnnotation to fix this
    /** @psalm-pure */
    public function name(): string {
        return 'base';
    }
}

final class Child extends Base {
    private int $count = 0;

    /** @psalm-capabilities read-props|write-this-props */
    public function increment(): void {
        $this->count++;
    }
}

Psalm doesn't suggest an annotation for an unannotated method that is overridden somewhere, nor
for the code calling it, since the annotation would restrict its overrides. Annotate it yourself
when you want a contract. It doesn't suggest one either when the function-like's
parameter default values need capabilities the annotation wouldn't
give them. A function-like marked @psalm-impure is never reported.

These issues are always reported as errors. To turn them off, or to adopt them gradually, use
issueHandlers:

xml
<issueHandlers>
    <MissingPureAnnotation errorLevel="suppress" />
    <MissingImmutableAnnotation errorLevel="suppress" />
    <MissingAbstractPureAnnotation errorLevel="suppress" />
    <MissingInterfaceImmutableAnnotation errorLevel="suppress" />
</issueHandlers>

Migrating from Psalm 6

Existing annotations keep their meaning:

  • @psalm-pure and @psalm-immutable are unchanged.
  • @psalm-mutation-free means @psalm-capabilities read-props.
  • @psalm-external-mutation-free means @psalm-capabilities read-props|write-this-props|write-refs.
    It no longer allows static properties or static variables: use read-globals or
    write-globals for those.

@psalm-mutation-free and @psalm-external-mutation-free are still accepted but no longer
documented or suggested. mutation-free and external-mutation-free are not capability names,
so @psalm-capabilities mutation-free is an InvalidDocblock.

What changes for your code:

  • Calls on objects of the same class are no longer free. In Psalm 6, a method could call a
    mutating method on any object of its own class, and a @psalm-mutation-free method could call
    one on $this. Now a call on $this costs write-this-props and a call on any other object
    costs write-props (see Method calls).
  • Overrides are checked at every level. Psalm 6 only reported impure overrides of
    @psalm-external-mutation-free methods. Now any override that needs more capabilities is
    reported, as ImmutableDependency.
  • MissingImmutableAnnotation has a new meaning. In Psalm 6 it reported a mutable class
    inheriting from an immutable one; that is now ImmutableDependency. MissingImmutableAnnotation
    now reports a class that could be annotated. Review existing suppressions of it.
  • New issues are reported by default: MissingPureAnnotation, MissingImmutableAnnotation,
    MissingAbstractPureAnnotation and MissingInterfaceImmutableAnnotation. Run --alter once,
    or suppress them (see Inferring annotations).
  • More is checked. Implicit calls (__toString, __clone, __invoke, ArrayAccess,
    destructors), throw new, new $className and call_user_func are now checked for purity.
    Builtins with side effects need specific capabilities: io for I/O and random numbers,
    time for reading the clock, write-globals for process-wide state such as mt_rand() or ini_set().
  • Iteration has a purity. foreach over an object costs the iterator's methods, or the
    TPurity of Traversable, Iterator, Generator and iterable, instead of always being
    impure (see Iterators and generators).
  • Callable types: pure-callable and pure-Closure still work, and any capability set can
    now be written, as in Closure[io](int): void. The capability names (pure, impure,
    read-props, …) are now reserved type names.

The full list of changes is in
UPGRADING.md.


Annotating Code Supported Annotations (`docs/annotating_code/supported_annotations.md`)

Supported docblock annotations

Psalm supports a wide range of docblock annotations.

PHPDoc tags

Psalm uses the following PHPDoc tags to understand your code:

  • @var
    Used for specifying the types of properties and variables
  • @return
    Used for specifying the return types of functions, methods and closures. On a constructor, it
    gives the type of the object new creates (see Constructors)
  • @param
    Used for specifying types of parameters passed to functions, methods and closures
  • @property
    Used to specify what properties can be accessed on an object that uses __get and __set
  • @property-read
    Used to specify what properties can be read on object that uses __get
  • @property-write
    Used to specify what properties can be written on object that uses __set
  • @method
    Used to specify which magic methods are available on object that uses __call.
  • @deprecated
    Used to mark functions, methods, classes and interfaces as being deprecated
  • @internal
    Used to mark classes, functions and properties that are internal to an application or library.
  • @mixin
    Used to tell Psalm that the current class proxies the methods and properties of the referenced class.

Off-label usage of the `@var` tag

The @var tag is supposed to only be used for properties. Psalm, taking a lead from PHPStorm and other static analysis tools, allows its use inline in the form @var Type [VariableReference].

If VariableReference is provided, it should be of the form $variable or $variable->property. If used above an assignment, Psalm checks whether the VariableReference matches the variable being assigned. If they differ, Psalm will assign the Type to VariableReference and use it in the expression below.

If no VariableReference is given, the annotation tells Psalm that the right-hand side of the expression, whether an assignment or a return, is of type Type.

php
<?php
/** @var string */
$a = $_GET['foo'];

/** @var string $b */
$b = $_GET['bar'];

function bat(): string {
    /** @var string */
    return $_GET['bat'];
}

@mixins

Adding @mixin to a classes docblock tells Psalm that the class proxies will proxy the methods and properties of the referenced class.

php
class A
{
    public string $a = 'A';
 
    public function doA(): void
    {
    }
}

/**
 * @mixin A
 */
class B
{
    public string $b = 'B';

    public function doB(): void
    {
    }

    public function __call($name, $arguments)
    {
        (new A())->$name(...$arguments);
    }
    
    public function __get($name)
    {
        (new A())->$name;
    }
}

$b = new B();
$b->doB();
$b->doA(); // works
echo $b->b;
echo $b->a; // works

Mixins are followed transitively: if B mixes in A and C mixes in B, members of A are also available on C.
Chains are only followed through non-generic mixins; a generic hop such as @mixin Collection<T> is not traversed.

Psalm-specific tags

There are a number of custom tags that determine how Psalm treats your code.

`@psalm-consistent-constructor`

See UnsafeInstantiation

`@psalm-consistent-templates`

See UnsafeGenericInstantiation

`@param-out`, `@psalm-param-out`

This is used to specify that a by-ref type is different from the one that entered. In the function below the first param can be null, but once the function has executed the by-ref value is not null.

php
<?php
/**
 * @param-out string $s
 */
function addFoo(?string &$s) : void {
    if ($s === null) {
        $s = "hello";
    }
    $s .= "foo";
}

`@param-closure-this`, `@psalm-param-closure-this`, `@phpstan-param-closure-this`

This binds $this to a specific class type inside a Closure or arrow-function expression passed directly as an argument. Use it when the receiving function or method runs the callback with Closure::bind / Closure::call so that $this resolves to a different object than the caller's $this. The bound type may be a class name (including a generic type such as Box), $this, static, self, parent, or a class-level template parameter.

php
<?php
class Macroable {
    /**
     * @param-closure-this static $macro
     */
    public static function macro(string $name, Closure $macro): void {
        // store $macro and later run $macro->call($instance)
    }
}

class Builder extends Macroable {
    public int $value = 42;
}

Builder::macro('grab', function (): int {
    // $this is Builder here, not the outer scope
    return $this->value;
});

Inside the callback, class scope follows the bound type: self:: refers to that class and parent:: to its parent. Property types retain the bound class's generic arguments rather than using the caller's property types.

The tag is ignored when it cannot name one known class to bind to (a union such as A|B, or a class Psalm has not seen), and when the argument is a static closure, since PHP cannot rebind one.

`@psalm-var`, `@psalm-param`, `@psalm-return`, `@psalm-property`, `@psalm-property-read`, `@psalm-property-write`, `@psalm-method`

When specifying types in a format not supported by phpDocumentor (but supported by Psalm) you may wish to prepend @psalm- to the PHPDoc tag, so as to avoid confusing your IDE. If a @psalm-prefixed tag is given, Psalm will use it in place of its non-prefixed counterpart.

`@psalm-ignore-var`

This annotation is used to ignore the @var annotation written in the same docblock. Some IDEs don't fully understand complex types like generics. To take advantage of such IDE's auto-completion, you may sometimes want to use explicit @var annotations even when psalm can infer the type just fine. This weakens the effectiveness of type checking in many cases since the explicit @var annotation overrides the types inferred by psalm. As psalm ignores the @var annotation which is co-located with @psalm-ignore-var, IDEs can use the type specified by the @var for auto-completion, while psalm can still use its own inferred type for type checking.

php
<?php
/** @return iterable<array-key,\DateTime> $f */
function getTimes(int $n): iterable {
    while ($n--) {
        yield new \DateTime();
    }
};
/**
 * @var \Datetime[] $times
 * @psalm-ignore-var
 */
$times = getTimes(3);
// this trace shows "iterable<array-key, DateTime>" instead of "array<array-key, Datetime>"
/** @psalm-trace $times */
foreach ($times as $time) {
    echo $time->format('Y-m-d H:i:s.u') . PHP_EOL;
}

`@psalm-suppress SomeIssueName`

This annotation is used to suppress issues. It can be used in function docblocks, class docblocks and also inline, applying to the following statement.

Function docblock example:

php
<?php
/**
 * @psalm-suppress PossiblyNullOperand
 */
function addString(?string $s) {
    echo "hello " . $s;
}

Inline example:

php
<?php
function addString(?string $s) {
    /** @psalm-suppress PossiblyNullOperand */
    echo "hello " . $s;
}

@psalm-suppress all can be used to suppress all issues instead of listing them individually.

Suppressing an unused-code issue (such as UnusedClass or PossiblyUnusedMethod) only silences the report for that symbol β€” it does not mark the symbol as used, so code that is only referenced from it is still reported as unused. Redundant @psalm-suppress annotations, in any docblock (including on classes, interfaces, traits and enums), are reported by --find-unused-psalm-suppress.

`@psalm-assert`, `@psalm-assert-if-true`, `@psalm-assert-if-false`, `@psalm-if-this-is` and `@psalm-this-out`

See Adding assertions.

`@psalm-ignore-nullable-return`

This can be used to tell Psalm not to worry if a function/method returns null. It’s a bit of a hack, but occasionally useful for scenarios where you either have a very high confidence of a non-null value, or some other function guarantees a non-null value for that particular code path.

php
<?php
class Foo {}
function takesFoo(Foo $f): void {}

/** @psalm-ignore-nullable-return */
function getFoo(): ?Foo {
  return rand(0, 10000) > 1 ? new Foo() : null;
}

takesFoo(getFoo());

`@psalm-ignore-falsable-return`

This provides the same, but for false. Psalm uses this internally for functions like preg_replace, which can return false if the given input has encoding errors, but where 99.9% of the time the function operates as expected.

`@psalm-seal-properties`, `@psalm-no-seal-properties`, `@seal-properties`, `@no-seal-properties`

If you have a magic property getter/setter, you can use @psalm-seal-properties to instruct Psalm to disallow getting and setting any properties not contained in a list of @property (or @property-read/@property-write) annotations.
This is automatically enabled with the configuration option sealAllProperties and can be disabled for a class with @psalm-no-seal-properties

php
<?php
/**
 * @property string $foo
 * @seal-properties
 */
class A {
     public function __get(string $name): ?string {
          if ($name === "foo") {
               return "hello";
          }
     }

     public function __set(string $name, $value): void {}
}

$a = new A();
$a->bar = 5; // this call fails

`@psalm-seal-methods`, `@psalm-no-seal-methods`, `@seal-methods`, `@no-seal-methods`

If you have a magic method caller, you can use @psalm-seal-methods to instruct Psalm to disallow calling any methods not contained in a list of @method annotations.
This is automatically enabled with the configuration option sealAllMethods and can be disabled for a class with @psalm-no-seal-methods

php
<?php
/**
 * @method foo(): string
 * @seal-methods
 */
class A {
     public function __call(string $name, array $args) {
          if ($name === "foo") {
               return "hello";
          }
     }
 }

$a = new A();
$b = $a->bar(); // this call fails

`@psalm-internal`

Used to mark a class, property or function as internal to a given namespace or class or even method.
Psalm treats this slightly differently to the PHPDoc @internal tag. For @internal,
an issue is raised if the calling code is in a namespace completely unrelated to the namespace of the calling code,
i.e. not sharing the first element of the namespace.

In contrast for @psalm-internal, the docblock line must specify a namespace. An issue is raised if the calling code
is not within the given namespace.

php
<?php
namespace A\B {
    /**
     * @internal
     * @psalm-internal A\B
     */
    class Foo { }
}

namespace A\B\C {
    class Bat {
        public function batBat(): void {
            $a = new \A\B\Foo(); // this is fine
        }
    }
}

namespace A {
    class B {
        public function batBat(): void {
            $a = new \A\B\Foo(); // this is fine
        }
    }
}

namespace A\C {
    class Bat {
        public function batBat(): void {
            $a = new \A\B\Foo(); // error
        }
    }
}

namespace X {
    class Foo {        
        /**
         * @psalm-internal Y\Bat::batBat
         */
        public static function barBar(): void {
        }
    }
}

namespace Y {
    class Bat {
        public function batBat() : void {
            \X\Foo::barBar(); // this is fine
        }
        public function fooFoo(): void {
            \X\Foo::barBar(); // error
        }
    }
}

`@psalm-readonly` and `@readonly`

Used to annotate a property that can only be written to in its defining class's constructor.

php
<?php
class B {
  /** @readonly */
  public string $s;

  public function __construct(string $s) {
    $this->s = $s;
  }
}

$b = new B("hello");
echo $b->s;
$b->s = "boo"; // disallowed

Purity and capabilities

Psalm tracks the side effects a function, method or closure may have as a set of capabilities:
read-props, write-this-props, write-props, read-globals, write-globals, write-refs, io
and time, with pure for none of them and impure for all of them. The Purity Model
explains what each capability allows, how calls are charged and how purity templates work. The
annotations are listed below.

`@psalm-pure` and `@psalm-impure`

@psalm-pure on a function or method gives it no capabilities: its result depends only on its
arguments. On a class, it applies to every method and bans the use of properties. @psalm-impure
on a function or method gives it every capability, the default for unannotated code, written out.
See Annotations.

`@psalm-capabilities`

@psalm-capabilities <set> on a function, method or closure gives the capabilities it may use,
separated by | or commas, as in @psalm-capabilities read-props|write-this-props. The set may be
a type alias. On a class or interface, it is a
contract for every method of the class and of the classes
extending or implementing it. See Capabilities.

`@psalm-immutable`

Used to annotate a class where every property is treated by consumers as @psalm-readonly and every instance method is treated as @psalm-capabilities read-props (see Annotations).

php
<?php
/**
 * @psalm-immutable
 */
abstract class Foo
{
    public string $baz;

    abstract public function bar(): int;
}

/**
 * @psalm-immutable
 */
final class ChildClass extends Foo
{
    public function __construct(string $baz)
    {
        $this->baz = $baz;
    }

    public function bar(): int
    {
        return 0;
    }
}

$anonymous = new /** @psalm-immutable */ class extends Foo
{
    public string $baz = "B";

    public function bar(): int
    {
        return 1;
    }
};

`@psalm-mutable`

Used to annotate a class where at least one property is mutable: this is the default behavior, but it can be explicitly marked for clarity.

`@psalm-purity-template`

@psalm-purity-template P on a class, function or method declares a purity template: a template
parameter whose values are capability sets rather than types. It is used as the purity of a
callable (Closure[P](int): int) or as the purity argument of a class (Box[P],
@extends Box[pure]). Bounds and a default are written as a chain,
@psalm-purity-template lower <= P(default) <= upper, and Closure[_](...) in a @param type
declares an unnamed one. See Purity templates.

Iterators and generators

Traversable, Iterator, IteratorAggregate, Generator and iterable have a purity template,
TPurity: what iterating over them may do, as in Iterator[pure] or
iterable[pure] (impure by default). See
Iterators and generators.

`@psalm-purity-from-template`

@psalm-purity-from-template P on a function or method: each call needs the function-like's own
capabilities plus whatever P, a purity template or a type template bound to a closure type, is
bound to at that call. See
@psalm-purity-from-template.

`@psalm-allow-private-mutation`

Used to annotate readonly properties that can be mutated in a private context. With this, public properties can be read from another class but only be mutated within a method of its own class.

php
<?php
class Counter {
  /**
   * @readonly
   * @psalm-allow-private-mutation
   */
  public int $count = 0;

  public function increment() : void {
    $this->count++;
  }
}

$counter = new Counter();
echo $counter->count; // outputs 0
$counter->increment(); // Method can mutate property
echo $counter->count; // outputs 1
$counter->count = 5; // This will fail, as it's mutating a property directly

`@psalm-readonly-allow-private-mutation`

This is a shorthand for the property annotations @readonly and @psalm-allow-private-mutation.

php
<?php
class Counter {
  /**
   * @psalm-readonly-allow-private-mutation
   */
  public int $count = 0;

  public function increment() : void {
    $this->count++;
  }
}

$counter = new Counter();
echo $counter->count; // outputs 0
$counter->increment(); // Method can mutate property
echo $counter->count; // outputs 1
$counter->count = 5; // This will fail, as it's mutating a property directly

`@psalm-trace`

You can use this annotation to trace inferred type (applied to the next statement).

php
<?php

/** @psalm-trace $username */
$username = $_GET['username']; // prints something like "test.php:4 $username: mixed"

Note: it throws special low-level issue.
To see it, you can set the global errorLevel to 1, or invoke Psalm with
--show-info=true, but both these solutions will probably result in a lot of
output. Another solution is to selectively bump the error level of the issue,
so that you only get one more error:

xml
<issueHandlers>
  <Trace errorLevel="error"/>
</issueHandlers>

`@psalm-check-type`

You can use this annotation to ensure the inferred type matches what you expect.

php
<?php

/** @psalm-check-type $foo = int */
$foo = 1; // No issue

/** @psalm-check-type $bar = int */
$bar = "not-an-int"; // Checked variable $bar = int does not match $bar = 'not-an-int'

`@psalm-check-type-exact`

Like @psalm-check-type, but checks the exact type of the variable without allowing subtypes.

php
<?php

/** @psalm-check-type-exact $foo = int */
$foo = 1; // Checked variable $foo = int does not match $foo = 1

`@psalm-taint-*`

See Security Analysis annotations.

`@psalm-type`

This allows you to define an alias for another type.

php
<?php
/**
 * @psalm-type PhoneType = array{phone: string}
 */
class Phone {
    /**
     * @psalm-return PhoneType
     */
    public function toArray(): array {
        return ["phone" => "Nokia"];
    }
}

`@psalm-import-type`

You can use this annotation to import a type defined with @psalm-type if it was defined somewhere else.

php
<?php
/**
 * @psalm-import-type PhoneType from Phone
 */
class User {
    /**
     * @psalm-return PhoneType
     */
    public function toArray(): array {
        return array_merge([], (new Phone())->toArray());
    }
}

You can also alias a type when you import it:

php
<?php
/**
 * @psalm-import-type PhoneType from Phone as MyPhoneTypeAlias
 */
class User {
    /**
     * @psalm-return MyPhoneTypeAlias
     */
    public function toArray(): array {
        return array_merge([], (new Phone())->toArray());
    }
}

`@psalm-require-extends`

The @psalm-require-extends annotation allows you to define the requirements that a trait imposes on the using class.

Psalm also reads the @phpstan-require-extends spelling. When a docblock has both, @psalm-require-extends is used.

php
<?php
abstract class DatabaseModel {
  // methods, properties, etc.
}

/**
 * @psalm-require-extends DatabaseModel
 */
trait SoftDeletingTrait {
  // useful but scoped functionality, that depends on methods/properties from DatabaseModel
}


class MyModel extends DatabaseModel {
  // valid
  use SoftDeletingTrait;
}

class NormalClass {
  // triggers an error
  use SoftDeletingTrait;
}

`@psalm-require-implements`

Behaves the same way as @psalm-require-extends, but for interfaces.

Psalm also reads the @phpstan-require-implements spelling. When a docblock has both, only the @psalm-require-implements tags are used.

`@no-named-arguments`

This will prevent access to the function or method tagged with named parameters (by emitting a NamedArgumentNotAllowed issue).

Incidentally, it will change the inferred type for the following code:

php
<?php
    function a(int ...$a){
        var_dump($a);
    }

The type of $a is array<array-key, int> without @no-named-arguments but becomes list with it, because it excludes the case where the offset would be a string with the name of the parameter

`@psalm-ignore-variable-property` and `@psalm-ignore-variable-method`

Instructs Psalm to ignore variable property fetch / variable method call when looking for dead code.

php
class Foo
{
    // this property can be deleted by Psalter,
    // as potential reference in get() is ignored
    public string $bar = 'bar';

    public function get(string $name): mixed
    {
        /** @psalm-ignore-variable-property */
        return $this->{$name};
    }
}

When Psalm encounters variable property, it treats all properties in given class as potentially referenced.
With @psalm-ignore-variable-property annotation, this reference is ignored.

While PossiblyUnusedProperty would be emitted in both cases, using @psalm-ignore-variable-property
would allow Psalter to delete Foo::$bar.

@psalm-ignore-variable-method behaves the same way, but for variable method calls.

`@psalm-yield`

Used to specify the type of value which will be sent back to a generator when an annotated object instance is yielded.

php
<?php
/**
 * @template-covariant TValue
 * @psalm-yield TValue
 */
interface Promise {}

/**
 * @template-covariant TValue
 * @template-implements Promise<TValue>
 */
class Success implements Promise {
    /**
     * @psalm-param TValue $value
     */
    public function __construct($value) {}
}

/**
 * @return Promise<string>
 */
function fetch(): Promise {
    return new Success('{"data":[]}');
}

function (): Generator {
    $data = yield fetch();
    
    // this is fine, Psalm knows that $data is a string
    return json_decode($data);
};

This annotation supports only generic types, meaning that e.g. @psalm-yield string would be ignored.

`@api`, `@psalm-api`

Used to tell Psalm that a class or method is used, even if no references to it can be
found. Unused issues will be suppressed.

For example, in frameworks, controllers are often invoked "magically" without
any explicit references to them in your code. You should mark these classes with
@psalm-api.

php
/**
 * @psalm-api
 */
class UnreferencedClass {}

When an Unused* issue is reported, there are only
two correct fix paths:

  • The symbol is genuinely public surface β€” consumed from outside the
    analysed codebase, e.g. a library entry point, or a class instantiated only
    by a framework, container or plugin loader. Mark it with @api/@psalm-api.
    Use this only for real public surface.
  • Otherwise the symbol is dead code β€” remove it, along with anything that
    was only reachable through it.

For a reported method or property, @api may be placed either on that member or
on the whole class, but marking the whole class is almost always the right
choice: an externally-consumed member belongs to an externally-consumed class,
and one class-level annotation then covers its entire public API. Conversely, if
a member is reported as unused inside a class that is not marked @api, it is
almost always genuinely dead code β€” remove it rather than annotating the lone
member. Annotating an individual member is only correct in the uncommon case
where the class is not public API but that one member genuinely is.

A class that implements an interface (or extends a class) defined outside the
project is treated as public surface automatically, since it can be instantiated
and invoked through that external type: its overriding methods are considered
used without an explicit @api.

Do not reach for @psalm-suppress UnusedClass (or another Unused*
suppression) in place of @api: as noted above, a suppression only silences the
report, it does not mark the symbol as used, so symbols it references may still
be reported as unused.

`@psalm-inheritors`

Used to tell Psalm that a class can only be extended by a certain subset of classes.

For example,

php
<?php
/**
 * @psalm-inheritors FooClass|BarClass
 */
class BaseClass {}
class FooClass extends BaseClass {}
class BarClass extends BaseClass {}
class BazClass extends BaseClass {} // this is an error

Type Syntax

Psalm supports PHPDoc’s type syntax, and also the proposed PHPDoc PSR type syntax.

A detailed write-up is found in Typing in Psalm


Annotating Code Templated Annotations (`docs/annotating_code/templated_annotations.md`)

Templating

Docblocks allow you to tell Psalm some simple information about how your code works. For example @return int in a function return type tells Psalm that a function should return an int and @return MyContainer tells Psalm that a function should return an instance of a user-defined class MyContainer. In either case, Psalm can check that the function actually returns those types and that anything calling that function uses its returned value properly.

Templated types allow you to tell Psalm even more information about how your code works.

Let's look at a simple class MyContainer:

php
<?php
class MyContainer {
  private $value;

  public function __construct($value) {
    $this->value = $value;
  }

  public function getValue() {
    return $this->value;
  }
}

When Psalm handles the return type of $my_container->getValue() it doesn't know what it's getting out, because the value can be arbitrary.

Templated annotations provide us with a workaround - we can define a generic/templated param T that is a placeholder for the value inside MyContainer:

php
<?php
/**
 * @template T
 */
class MyContainer {
  /** @var T */
  private $value;

  /** @param T $value */
  public function __construct($value) {
    $this->value = $value;
  }

  /** @return T */
  public function getValue() {
    return $this->value;
  }
}

Now we can substitute values for that templated param when we reference MyContainer in docblocks e.g. @return MyContainer. This tells Psalm to substitute T for int when evaluating that return type, effectively treating it as a class that looks like

php
<?php
class One_off_instance_of_MyContainer {
  /** @var int */
  private $value;

  /** @param int $value */
  public function __construct($value) {
    $this->value = $value;
  }

  /** @return int */
  public function getValue() {
    return $this->value;
  }
}

This pattern can be used in a large number of different situations like mocking, collections, iterators and loading arbitrary objects. Psalm has a large number of annotations to make it easy to use templated types in your codebase.

`@template`, `@psalm-template`

The @template/@psalm-template tag allows classes and functions to declare a generic type parameter.

As a very simple example, this function returns whatever is passed in:

php
<?php
/**
 * @template T
 * @psalm-param T $t
 * @return T
 */
function mirror($t) {
    return $t;
}

$a = 5;
$b = mirror($a); // Psalm knows the result is an int

$c = "foo";
$d = mirror($c); // Psalm knows the result is string

Psalm also uses @template annotations in its stubbed versions of PHP array functions e.g.

php
<?php
/**
 * Takes one array with keys and another with values and combines them
 *
 * @template TKey
 * @template TValue
 *
 * @param array<mixed, TKey> $arr
 * @param array<mixed, TValue> $arr2
 * @return array<TKey, TValue>
 */
function array_combine(array $arr, array $arr2) {}

Notes

  • @template tag order matters for class docblocks, as they dictate the order in which those generic parameters are referenced in docblocks.
  • The names of your templated types (e.g. TKey, TValue) don't matter outside the scope of the class or function in which they're declared.

@param class-string<T>

Psalm also allows you to parameterize class types

php
<?php
/**
 * @template T of Foo
 * @psalm-param class-string<T> $class
 * @return T
 */
function instantiator(string $class) {
    return new $class();
}

class Foo {
    public final function __construct() {}
}

class FooChild extends Foo {}

$r = instantiator(FooChild::class);
// Psalm knows $r is an object of type FooChild

Template inheritance

Psalm allows you to extend templated classes with @extends/@template-extends:

php
<?php
/**
 * @template T
 */
class ParentClass {}

/**
 * @extends ParentClass<int>
 */
class ChildClass extends ParentClass {}

similarly you can implement interfaces with @implements/@template-implements

php
<?php
/**
 * @template T
 */
interface IFoo {}

/**
 * @implements IFoo<int>
 */
class Foo implements IFoo {}

and import traits with @use/@template-use

php
<?php
/**
 * @template T
 */
trait MyTrait {}

class Foo {
    /**
     * @use MyTrait<int>
     */
    use MyTrait;
}

You can also extend one templated class with another, e.g.

php
<?php
/**
 * @template T1
 */
class ParentClass {}

/**
 * @template T2
 * @extends ParentClass<T2>
 */
class ChildClass extends ParentClass {}

Template constraints

You can use @template of <type> to restrict input. For example, to restrict to a given class you can use

php
<?php
class Foo {}
class FooChild extends Foo {}

/**
 * @template T of Foo
 * @psalm-param T $t
 * @return array<int, T>
 */
function makeArray($t) {
    return [$t];
}
$a = makeArray(new Foo()); // typed as array<int, Foo>
$b = makeArray(new FooChild()); // typed as array<int, FooChild>
$c = makeArray(new stdClass()); // type error

Templated types aren't limited to key-value pairs, and you can re-use templates across multiple arguments of a template-supporting type:

php
<?php
/**
 * @template T0 of array-key
 *
 * @template-implements IteratorAggregate<T0, int>
 */
abstract class Foo implements IteratorAggregate {
  /**
   * @var int
   */
  protected $rand_min;

  /**
   * @var int
   */
  protected $rand_max;

  public function __construct(int $rand_min, int $rand_max) {
    $this->rand_min = $rand_min;
    $this->rand_max = $rand_max;
  }

  /**
   * @return Generator<T0, int, mixed, T0>
   */
  public function getIterator() : Generator {
    $j = random_int($this->rand_min, $this->rand_max);
    for($i = $this->rand_min; $i <= $j; $i += 1) {
      yield $this->getFuzzyType($i) => $i ** $i;
    }

    return $this->getFuzzyType($j);
  }

  /**
   * @return T0
   */
  abstract protected function getFuzzyType(int $i);
}

/**
 * @template-extends Foo<int>
 */
class Bar extends Foo {
  protected function getFuzzyType(int $i) : int {
    return $i;
  }
}

/**
 * @template-extends Foo<string>
 */
class Baz extends Foo {
  protected function getFuzzyType(int $i) : string {
    return static::class . '[' . $i . ']';
  }
}

Template covariance

Imagine you have code like this:

php
<?php
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

/**
 * @template T
 */
class Collection {
    /**
     * @var array<int, T>
     */
    public array $list;

    /**
     * @param array<int, T> $list
     */
    public function __construct(array $list) {
        $this->list = $list;
    }

    /**
     * @param T $t
     */
    public function add($t) : void {
        $this->list[] = $t;
    }
}

/**
 * @param Collection<Animal> $collection
 */
function addAnimal(Collection $collection) : void {
    $collection->add(new Cat());
}

/**
 * @param Collection<Dog> $dog_collection
 */
function takesDogList(Collection $dog_collection) : void {
    addAnimal($dog_collection);
}

That last call addAnimal($dog_collection) breaks the type of the collection – suddenly a collection of dogs becomes a collection of dogs or cats. That is bad.

To prevent this, Psalm emits an error when calling addAnimal($dog_collection) saying "addAnimal expects a Collection<Animal>, but Collection<Dog> was passed". If you haven't encountered this rule before it's probably confusing to you – any function that accepted an Animal would be happy to accept a subtype thereof. But as we see in the example above, doing so can lead to problems.

But there are also times where it's perfectly safe to pass template param subtypes:

php
<?php
abstract class Animal {
    abstract public function getNoise() : string;
}
class Dog extends Animal {
    public function getNoise() : string { return "woof"; }
}
class Cat extends Animal {
    public function getNoise() : string { return "miaow"; }
}

/**
 * @template T
 */
class Collection {
    /** @var array<int, T> */
    public array $list = [];
}

/**
 * @param Collection<Animal> $collection
 */
function getNoises(Collection $collection) : void {
    foreach ($collection->list as $animal) {
        echo $animal->getNoise();
    }
}

/**
 * @param Collection<Dog> $dog_collection
 */
function takesDogList(Collection $dog_collection) : void {
    getNoises($dog_collection);
}

Here we're not doing anything bad – we're just iterating over an array of objects. But Psalm still gives that same basic error – "getNoises expects a Collection<Animal>, but Collection<Dog> was passed".

We can tell Psalm that it's safe to pass subtypes for the templated param T by using the annotation @template-covariant T (or @psalm-template-covariant T):

php
<?php
/**
 * @template-covariant T
 */
class Collection {
    /** @var array<int, T> */
    public array $list = [];
}

Doing this for the above example produces no errors: https://psalm.dev/r/5254af7a8b

But @template-covariant doesn't get rid of all errors – if you add it to the first example, you get a new error – https://psalm.dev/r/0fcd699231 – complaining that you're attempting to use a covariant template parameter for function input. That’s no good, as it means you're likely altering the collection somehow (which is, again, a violation).

But what about immutability?

Psalm has comprehensive support for declaring functional immutability.

If we make sure that the class is immutable, we can declare a class with an add method that still takes a covariant param as input, but which does not modify the collection at all, instead returning a new one:

php
<?php
/**
 * @template-covariant T
 * @psalm-immutable
 */
class Collection {
    /**
     * @var array<int, T>
     */
    public array $list = [];

    /**
     * @param array<int, T> $list
     */
    public function __construct(array $list) {
        $this->list = $list;
    }

    /**
     * @param T $t
     * @return Collection<T>
     */
    public function add($t) : Collection {
        return new Collection(array_merge($this->list, [$t]));
    }
}

This is perfectly valid, and Psalm won't complain.

Builtin templated classes and interfaces

Psalm has support for a number of builtin classes and interfaces that you can extend/implement in your own code.

  • interface Traversable<TKey, TValue>

  • interface ArrayAccess<TKey, TValue>

  • interface IteratorAggregate<TKey, TValue> extends Traversable<TKey, TValue>

  • interface Iterator<TKey, TValue> extends Traversable<TKey, TValue>

  • interface SeekableIterator<TKey, TValue> extends Iterator<TKey, TValue>

  • class Generator<TKey, TValue, TSend, TReturn> extends Traversable<TKey, TValue>

  • class ArrayObject<TKey, TValue> implements IteratorAggregate<TKey, TValue>, ArrayAccess<TKey, TValue>

  • class ArrayIterator<TKey of array-key, TValue> implements SeekableIterator<TKey, TValue>, ArrayAccess<TKey, TValue>

  • class DOMNodeList<TNode of DOMNode> implements Traversable

  • class SplDoublyLinkedList<TValue> implements Iterator<TKey, TValue>, ArrayAccess<TKey, TValue>

  • class SplQueue<TValue> extends SplDoublyLinkedList<TValue>

  • abstract class FilterIterator<TKey, TValue, TIterator>


Annotating Code Type Variables (`docs/annotating_code/type_variables.md`)

Type Variables

A type variable is a placeholder type β€” rendered internally as `_0, `_1, … β€”
that stands in for a class template parameter whose concrete type is not yet known. The
idea comes from the Hack typechecker's local inference: in Hack you can write
new Vector<_>() and let the checker solve for the type argument from how the object is
used. PHP has no constructor type arguments at all, so under this model every
templated construction site is effectively new Foo<_>(...).

How it works

When Psalm analyzes new Foo(...) and Foo has a template T, it mints a fresh type
variable and makes it the object's type parameter (Foo<_0>`). The variable starts life
with:

  • lower bounds β€” whatever the constructor arguments already implied
    (new Box(1) seeds `_0 >: 1); empty if nothing bound it,
  • upper bounds β€” the template's declared constraint (@template T of int|string
    seeds `_0 <: int|string), and, when the constructor names the type exactly
    through a class-string<T>/T::class parameter, the named type as a pin
    (new ReflectionClass(Foo::class) pins `_0 = Foo).

From then on the variable flows through the function body, and every use records a
constraint instead of passing judgment
: passing a value into a T-typed parameter adds
a lower bound; returning the object against a declared Foo<string> adds an equality
upper bound; arithmetic adds an int|float upper bound; concatenation and echo add
array-key; scalar assertions like is_int($v) add lower bounds. The comparator treats
a type variable as compatible with anything during these checks β€” the verdict is
deferred.

When the surrounding function-like (or the file's global scope) finishes analysis, all
accumulated bounds for each variable are reconciled with each other; any lower bound
that cannot satisfy an upper bound produces
IncompatibleTypeParameters,
reported at the position where the offending bound was recorded. The constraints
T <: int|string, T >: int can hold together; T = string, T >: 1 cannot β€” that pair
is what makes this code report "Type 1 should be a subtype of string" at the new:

php
<?php

/**
 * @template T of int|string
 */
class Box {
    /** @param T $t */
    public function __construct(public $t) {}
    /** @param T $item */
    public function set($item): void {
        $this->t = $item;
    }
}

/** @return Box<string> */
function bad(): Box {
    $box = new Box(1); // IncompatibleTypeParameters: Type 1 should be a subtype of string
    $box->set("two");
    return $box;
}

How this differs from Psalm's previous approach

Psalm's previous model was eager pinning. At the construction site the template was
resolved once and frozen: to the most specific type the constructor arguments implied
(new Box(1) β‡’ Box<1>), or to its declared constraint when nothing bound it
(new IntBox() with T of int β‡’ IntBox). Every later use was then checked
against that frozen type, argument by argument.

The practical differences:

  1. Inference direction. Eager pinning means the constructor is the only place T
    can be learned; everything after is checking. With type variables, inference runs
    through the whole function body β€” a later $box->add(5) on an unbound Box
    genuinely informs what T is, and $box->get() resolves through those accumulated
    bounds instead of falling back to the constraint.

  2. Widening is legal. Under eager pinning, new Box(1) pins T = 1, so
    $box->set('two') was InvalidArgument ("expects 1, but 'two' provided") even
    though 'two' satisfies T of int|string. Under type variables the set call just
    widens T toward 1|'two', which the constraint accepts. T is only ever
    required to be something by its declared constraint, a class-string pin, or a
    context that names it (a declared return/param/property type) β€” not by whichever
    value happened to reach the constructor first.

  3. Where and how errors are reported. Eager pinning reported a separate per-call
    mismatch at each offending argument, phrased against the frozen type. Reconciliation
    reports the actual contradiction between bounds, once, at the bound that cannot hold β€”
    so a Box<string> return conflicting with the constructor argument points at
    $box = new Box(1);, which is where the wrong value actually entered. The issue is
    its own type (IncompatibleTypeParameters, level 1), shown as INFO at normal error
    levels rather than a hard error.

  4. Comparisons become provisional. Because a type variable "matches anything while
    recording a bound," any code that used a successful containment check to make a
    decision (negated instanceof narrowing, rewriting hinted closure params from an
    expected callable) treats variable-involving containments as non-definitive β€” they
    record evidence for reconciliation but prove nothing on their own.

What stays the same

  • Templates with a mixed constraint and templates with no public mutation channel
    (named only in the constructor) are never turned into type variables β€” there is
    nothing to check, and nothing later could constrain them β€” so plain @template T
    classes behave exactly as before.
  • SplObjectStorage keeps its never special case for unbound templates.
  • A type variable renders through its bounds, so inferred types still display as
    Box<1> or Foo<A> rather than `_0.
  • Callables validate against the construction-site inference with ordinary
    InvalidArgument messages, since a structural signature mismatch is clearer reported
    in full than deferred to reconciliation.

Annotating Code Typing In Psalm (`docs/annotating_code/typing_in_psalm.md`)

Typing in Psalm

Psalm is able to interpret all PHPDoc type annotations, and use them to further understand the codebase.

Types are used to describe acceptable values for properties, variables, function parameters and return $x.

Docblock Type Syntax

Psalm allows you to express a lot of complicated type information in docblocks.

All docblock types are either atomic types, union types or intersection types.

Additionally, Psalm supports PHPDoc’s type syntax, and also the proposed PHPDoc PSR type syntax.

Property declaration types vs Assignment typehints

You can use the /** @var Type */ docblock to annotate both property declarations and to help Psalm understand variable assignment.

Property declaration types

You can specify a particular type for a class property declaration in Psalm by using the @var declaration:

php
<?php
/** @var string|null */
public $foo;

When checking $this->foo = $some_variable;, Psalm will check to see whether $some_variable is either string or null and, if neither, emit an issue.

If you leave off the property type docblock, Psalm will emit a MissingPropertyType issue.

Assignment typehints

Consider the following code:

php
<?php
namespace YourCode {
  function bar() : int {
    $a = \ThirdParty\foo();
    return $a;
  }
}
namespace ThirdParty {
  function foo() {
    return mt_rand(0, 100);
  }
}

Psalm does not know what the third-party function ThirdParty\foo returns, because the author has not added any return types. If you know that the function returns a given value you can use an assignment typehint like so:

php
<?php
namespace YourCode {
  function bar() : int {
    /** @var int */
    $a = \ThirdParty\foo();
    return $a;
  }
}
namespace ThirdParty {
  function foo() {
    return mt_rand(0, 100);
  }
}

This tells Psalm that int is a possible type for $a, and allows it to infer that return $a; produces an integer.

Unlike property types, however, assignment typehints are not binding – they can be overridden by a new assignment without Psalm emitting an issue e.g.

php
<?php
/** @var string|null */
$a = foo();
$a = 6; // $a is now typed as an int

You can also use typehints on specific variables e.g.

php
<?php
/** @var string $a */
echo strpos($a, 'hello');

This tells Psalm to assume that $a is a string (though it will still throw an error if $a is undefined).

Specifying string/int options (aka enums)

Psalm allows you to specify a specific set of allowed string/int values for a given function or method.

Whereas this would cause Psalm to complain that not all paths return a value:

php
<?php
function foo(string $s) : string {
  switch ($s) {
    case 'a':
      return 'hello';

    case 'b':
      return 'goodbye';
  }
}

If you specify the param type of $s as 'a'|'b' Psalm will know that all paths return a value:

php
<?php
/**
 * @param 'a'|'b' $s
 */
function foo(string $s) : string {
  switch ($s) {
    case 'a':
      return 'hello';

    case 'b':
      return 'goodbye';
  }
}

If the values are in class constants, you can use those too:

php
<?php
class A {
  const FOO = 'foo';
  const BAR = 'bar';
}

/**
 * @param A::FOO | A::BAR $s
 */
function foo(string $s) : string {
  switch ($s) {
    case A::FOO:
      return 'hello';

    case A::BAR:
      return 'goodbye';
  }
}

If the class constants share a common prefix, you can specify them all using a wildcard:

php
<?php
class A {
  const STATUS_FOO = 'foo';
  const STATUS_BAR = 'bar';
}

/**
 * @param A::STATUS_* $s
 */
function foo(string $s) : string {
  switch ($s) {
    case A::STATUS_FOO:
      return 'hello';

    default:
      // any other status
      return 'goodbye';
  }
}

Contributing Adding Issues (`docs/contributing/adding_issues.md`)

Adding a new issue type

To add a new issue type there are a number of required steps, listed below.

Generating a new shortcode

Run bin/max_used_shortcode.php and note the value it printed ($max_shortcode)

Create issue class

Create a class in Psalm\Issue namespace like this:

php
<?php

namespace Psalm\Issue;

final class MyNewIssue extends CodeIssue 
{
    public const ERROR_LEVEL = 2;
    public const SHORTCODE = 123;
}

For SHORTCODE value use $max_shortcode + 1. To choose appropriate error level see Error levels.

There a number of abstract classes you can extend:

  • CodeIssue - non-specific, default issue. It's a base class for all issues.
  • ClassIssue - issue related to a specific class (also interface, trait, enum). These issues can be suppressed for specific classes in psalm.xml by using referencedClass attribute
  • PropertyIssue - issue related to a specific property. Can be targeted by using referencedProperty in psalm.xml
  • FunctionIssue - issue related to a specific function. Can be suppressed with referencedFunction attribute.
  • ArgumentIssue - issue related to a specific argument. Can be targeted with referencedFunction attribute.
  • MethodIssue - issue related to a specific method. Can be targeted with referencedMethod attribute.
  • ClassConstantIssue - issue related to a specific class constant. Can be targeted with referencedConstant.
  • VariableIssue - issue for a specific variable. Targeted with referencedVariable

Add a `config.xsd` entry

All issue types needs to be listed in config.xsd, which is used to validate psalm.xml. Choose appropriate type attribute. E.g. for issues extending PropertyIssue use type="PropertyIssueHandlerType".

Add a doc page for your new issue

Every issue needs to be documented. Create a markdown file in docs/running_psalm/issues folder. Make sure to include a snippet of code illustrating your issue. Important: snippets must use fenced php code block and must include opening PHP tag (<?php). The snippet must actually produce the issue you're documenting. It's checked by our test suite.

Add links to the doc page you created to docs/running_psalm/error_levels.md and docs/running_psalm/issues.md

Run documentation tests

text
$ vendor/bin/phpunit tests/DocumentationTest.php

It will check whether you did all (or at least most) of the steps above.

Use your new issue type in Psalm core

php
IssueBuffer::maybeAdd(new MyNewIssue(...))

Contributing Editing Callmaps (`docs/contributing/editing_callmaps.md`)

Altering callmaps

Intro

One of the first things most contributors start with is proposing changes to
callmaps.

Callmap is a data file (formatted as a PHP file returning an array) that tells
Psalm what arguments function/method takes and what it returns.

There is a partial callmap file in dictionaries/override/CallMap.php folder, and
a number of delta files that provide information on
how signatures changed in various PHP versions in dictionaries/override/CallMap_*_delta.php.

To alter info for a function, edit the signatures in dictionaries/override/CallMap* (adding the function if missing, copying it from dictionaries/autogen/CallMap_$latest.php).

When editing info for a function, don't forget to alter all revisions of the same function in dictionaries/override/CallMap_*_delta.php.

The callmap files in the override subfolder are then used, in conjunction with the
automatically-generated reflection callmaps in dictionaries/autogen, to generate the
final callmaps in dictionaries/CallMap_*, which are the actual callmaps in use by Psalm.

After editing an override callmap, run bin/gen_callmap.php to regenerate the final callmap file.

To also regenerate the base callmaps, run bin/gen_callmap.sh: it will use the dockerfiles in bin/
to extract types from PHP and a set of extensions.
To add types from an extension to the callmap, edit all versions of the Dockerfiles.

Full callmap format

The full callmap (CallMap.php) has function/method
names as keys and an array representing the corresponding signature as a value.

First element of that value is a return type (it also doesn't have a key), and
subsequent elements represent function/method parameters. Parameter name for an
optional parameter is postfixed with =, references with &/&r_/&w_/&rw_
(depending on the read/write meaning of the reference param) and
variadic args are prefixed with ....

Callmaps also support function aliases: aliases are very useful to specify that
a certain function behaves differently according to the parameter types.

For example, the following aliases are currently in use in the callmap to specify
that the version_compare function can be called with an operator parameter,
in which case it will return a boolean; otherwise it will return an integer.

php
[
    'version_compare' => [
        0 => 'bool',
        'version1' => 'string',
        'version2' => 'string',
        'operator' => '\'!=\'|\'<\'|\'<=\'|\'<>\'|\'=\'|\'==\'|\'>\'|\'>=\'|\'eq\'|\'ge\'|\'gt\'|\'le\'|\'lt\'|\'ne\'|null',
    ],
    'version_compare\'1' => [
        0 => 'int',
        'version1' => 'string',
        'version2' => 'string',
    ],
]

Note: the above example doesn't provide almost any useful information for type inference;
in fact, the real logic for return type inference is contained in the VersionCompareReturnTypeProvider.

The callmap is mainly useful when treating functions and methods as callable values, for example:

php
<?php
function naive_version_compare(string $a, string $b, ?string $operator = null): int|bool {
    return 0;
}

$a = ["1.0", "2.0"];

// OK
usort($a, "version_compare");

// InvalidArgument: Argument 2 of usort expects callable(string, string):int, but impure-callable(string, string, null|string=):(bool|int) provided
usort($a, "naive_version_compare");

The first usort succeeds, because psalm chooses the correct alias to use between the two provided in the callmap.
The second usort fails (equivalent to the non-split return type of version_compare inferred by reflection), because the return type is a union of the two possible signatures of version_compare.

When you have multifaceted functions like these, it's a very good idea to at least define a templated stub in stubs/ for them, or a custom return type provider for even more complex logic, not representable with templates/conditional types/etc in a stub.

Also note that bin/gen_callmap.php has some validation logic which will re-add back removed parameters in overridden aliased callmaps: to avoid this, explicitly whitelist aliased functions by editing assertParameter in bin/gen_callmap_utils.php, and eventually bin/gen_callmap.php as needed.

Delta file format

Delta files (named CallMap__delta.php)
list changes that happened in the corresponding PHP version. There are
three section with self-explanatory names: added (for functions/methods that
were added in that PHP version), removed (for those that were removed) and
changed.

Entry format for removed and added section matches that of a full callmap,
while changed entries list old and new signatures.

How Psalm uses delta files

When the current PHP version is set to something other than the latest PHP
version supported by Psalm, it needs to process delta files to arrive at a
version of callmap matching the one that is used during analysis. Psalm uses
the following process to do that:

  1. Read CallMap.php (Note: it's the one having the latest signatures).
  2. If it matches configured PHP version, use it.
  3. If the callmap delta for previous PHP version exists, read that.
  4. Take previous callmap delta and apply it in reverse order. That is, entries
    in removed section are added, those in added section are removed and
    changed.new signatures in the current callamp are replaced with
    changed.old.
  5. Goto 2

Consistent histories

To make sure there are no mismatches in deltas and the callmap, CI validates
that all function/method entries have consistent histories. E.g. that the
signature in changed.new matches the one in CallMap.php, the removed
entries are actually absent from CallMap.php and so on.

Typical changes

To put that into practical perspective, let's see how a couple of typical
callmap changes may look like.

Adding a new function

Say, there's a function added in PHP 8.1, e.g. array_is_list(). Add it to the
CallMap_81_delta.php (as it was introduced in PHP 8.1), and CallMap.php (as
it exists in the latest PHP version). Here's the PR that does it.

Correcting the function signature

Assume you found an incorrect signature, the one that was always different to what
we currently have in Psalm. This will need a change to CallMap.php (as the signature is
still valid). Here's the PR that does it.

If function signature is correct for an older version but has changed since you
will need to edit the delta for PHP version where signature changed and
CallMap.php (as this new signature is still valid). Here's
the PR that does it (makes timestamp nullable).


Contributing How Psalm Works (`docs/contributing/how_psalm_works.md`)

How Psalm works

The entry point for all analysis is ProjectAnalyzer

ProjectAnalyzer is in charge of two things: Scanning and Analysis

Scanning

For any file or set of files, Psalm needs to determine all the possible dependencies and get their function signatures and constants, so that the analysis phase can be done multi-threaded.

Scanning happens in Psalm\Internal\Codebase\Scanner.

The first task is to convert a file into a set of PHP Parser statements. PHP Parser converts PHP code into an abstract syntax tree that Psalm uses for all its analysis.

Deep scanning vs shallow scanning

Psalm then uses a custom PHP Parser NodeVisitor called ReflectorVisitor which has two modes when scanning a given file: a shallow scan, where it just gets function signatures, return types, constants, and inheritance, or a deep scan, where it drills into every function statement to get those dependencies too (e.g. the class names instantiated by a function). It only does a deep scan on files that it knows will be analysed later (so the vast majority of the vendor directory, for example, just gets a shallow scan).

So, when analysing the src directory, Psalm will deep scan the following file:

src/A.php

php
<?php
use Vendor\VendorClass;
use Vendor\OtherVendorClass;

class A extends VendorClass
{
    public function foo(OtherVendorClass $c): void {}
}

And will also deep scan the file belonging to Vendor\VendorClass, because it may have to check instantiations of properties at some point.

It will do a shallow scan of Vendor\OtherVendorClass (and any dependents) because all it cares about are the method signatures and return types of the variable $c.

Finding files from class names

To figure out the ClassName => src/FileName.php mapping it uses reflection for project files and the Composer classmap for vendor files.

Storing data from scanning

For each file that ReflectorVisitor visits, Psalm creates a FileStorage instance, along with ClassLikeStorage and FunctionLikeStorage instances depending on the file contents.

Once we have a set of all files and their classes and function signatures, we calculate inheritance for everything in the Populator class and then move onto analysis.

At the end of the scanning step we have populated all the necessary information in ClassLikes, Functions and Methods classes, and created a complete list of FileStorage and ClassLikeStorage objects (in FileStorageProvider and ClassLikeStorageProvider respectively) for all classes and files used in our project.

Analysis

We analyse files in FileAnalyzer

The FileAnalyzer takes a given file and looks for a set of top-level components: classes, traits, interfaces, functions. It can look inside namespaces and extract the classes, interfaces, traits and functions in them as well.

It delegates the analysis of those components to ClassAnalyzer, InterfaceAnalyzer and FunctionAnalyzer.

Because it’s the most basic use case for the line-by-line analysis (no class inheritance to worry about), let’s drill down into FunctionAnalyzer.

Function Analysis

FunctionAnalyzer::analyze is defined in FunctionLikeAnalyzer. That method first gets the FunctionLikeStorage object for the given function that we created in our scanning step. That FunctionLikeStorage object has information about function parameters, which we then feed into a Context object. The Context contains all the type information we know about variables & properties (stored in Context::$vars_in_scope) and also a whole bunch of other information that can change depending on assignments and assertions.

Somewhere in FunctionLikeAnalyzer::analyze we create a new StatementsAnalyzer and then call its analyze() method, passing in a set of PhpParser nodes. StatementAnalyzer::analyze passes off to a number of different checkers (IfAnalyzer, ForeachAnalyzer, ExpressionAnalyzer etc.) for more thorough analysis.

At each line the Context object may or may not be manipulated. At branching points (if statements, loops, ternary etc) the Context object is cloned and then, at the end of the branch, Psalm figures out how to resolve the changes and update the uncloned Context object.

The NodeDataProvider stores a type for each PhpParser node.

After all the statements have been analysed we gather up all the return types and compare them to the given return type.

Type Reconciliation

While some updates to the Context object are straightforward, others are not. Updating the Context object in the light of new type information happens in Reconciler, which takes an array assertions e.g. [β€œ$a” => β€œ!null”] and a list of existing type information e.g. $a => string|null and return a set of updated information e.g. $a => string


Contributing Philosophy (`docs/contributing/philosophy.md`)

Philosophy

This is about why Psalm is the way it is. This is a living document!

Psalm is a tool for PHP written in PHP

Advantages

  • PHP is fast enough for most use-cases
  • Writing the tool in PHP guarantees that PHP community members can contribute to it without too much difficulty

Drawbacks

  • Psalm is slow in very large monorepos (over 5 million lines of checked-in code).
  • Psalm’s language server is more limited in what it can provide than comparable compiled-language tools. For example, it's infeasible to find/change all occurrences of a symbol, in all files that use it, as you type.

Comparison to other languages & tools

Many languages have typecheckers/compilers written in the same language. Popular examples include Go, Rust, and TypeScript. Python is a special case where the semi-official typechecker MyPy (written in Python) can also compile to a C Python extension, which runs 4x faster than the interpreted equivalent.

Some interpreted languages have unofficial open-source typecheckers written in faster compiled languages. In all cases a single mid-to-large company is behind each effort, with a small number of contributors not employed by that company:

  • PHP
    • NoVerify – written in Go. Runs much faster than Psalm (but does not support many modern PHP features)
  • Ruby
  • Python
    • Pyre - written in OCaml
  • Hack - the typechecker is written in OCaml and Rust

Psalm's primary purpose is finding bugs

Psalm does a lot, but people mainly use it to find potential bugs in their code.

All other functionality – the language server, security analysis, manipulating/fixing code is a secondary concern.

It's designed to be run on syntactically-correct code

Psalm is almost always run on PHP code that parses a lint check (php -l <filename>) – i.e. syntactically-correct code. Psalm is not a replacement for that syntax check.

Given Psalm is almost always used on syntactically-correct code it should use a parser built for that purpose, and nikic/php-parser is the gold-standard.

Where Psalm needs to run on syntactically-incorrect code (e.g. in language server mode) Psalm should still use the same parser (and work around any issues that it produces).

Docblock annotations are better than type-providing plugins

Psalm offers a plugin API that allows you to tell it what about your program's property types, return types and parameter types.

Psalm aims to operate in a space with other static analysis tools. The more users rely on plugins, the less chance those other tools have to understand the user's intent.

Psalm should encourage developers to use docblock annotations rather than type-providing plugins. This was a driving force in the adoption of Conditional Types which allowed Psalm to replace some of its own internal type-providing plugins.

The other benefit to docblock annotations is verifiability – for the most part Psalm is able to verify that docblock annotations are correct, but it cannot provide many assurances when plugins are used.

This doesn’t mean that plugins as a whole are bad, or that they can’t provide useful types. A great adjacent use of plugins is to provide stubs with Psalm type annotations for libraries that don’t have them. This helps the PHP ecosystem because those stubs may eventually make their way into the project currently being stubbed.

Docblock annotations that can be verified are better than those that cannot

Psalm currently supports a number of function/class docblock annotations that it's unable to verify:

  • @psalm-assert, @psalm-assert-if-true, @psalm-assert-if-false
  • @property, @method, @mixin

Whenever new docblock annotations are added, effort should be made to allow Psalm to verify their correctness.

In certain circumstances docblock annotations are better than PHP 8 attributes

For information that's just designed to be consumed by static analysis tools, docblocks are a better home than PHP 8 attributes.

A rationale is provided in this article.


Contributing What Makes Psalm Complicated (`docs/contributing/what_makes_psalm_complicated.md`)

Things that make developing Psalm complicated

This is a somewhat informal list that might aid others.

Type inference

Type inference is one of the big things Psalm does. It tries to figure out what different PHP elements (function calls, if/for/foreach statements etc.) mean for the data in your code.

Within type inference there are a number of tricky areas:

Loops

Loops are hard to reason about - break and continue are a pain. This analysis mainly takes place in LoopAnalyzer

Combining types

There are lots of edge-cases when combining types together, given the many types Psalm supports. Type combining occurs in TypeCombiner.

Logical assertions

What effect do different PHP elements have on user-asserted logic in if conditionals, ternarys etc. This logic is spread between a number of different classes.

Generics & Templated code

Figuring out how templated code should work (@template tags) and how much it should work like it does in other languages (Hack, TypeScript etc.) is tricky. Psalm also supports things like nested templates (@template T1 of T2) which makes things trickier

Detecting dead code

Detecting unused variables requires some fun data-flow analysis.

Detecting unused classes and methods between different runs requires maintaining references to those classes in cache (see below).

Supporting the community

  • Supporting formal PHPDoc annotations
  • Supporting informal PHPDoc annotations
    e.g. ArrayIterator|string[] to denote an ArrayIterator over strings
  • non-Composer projects
    e.g. WordPress

Making Psalm fast

Parser-based reflection

Requires scanning everything necessary for analysis

Forking processes** (non-windows)

Mostly handled by code borrowed from Phan, but can introduce subtle issues, also requires to think about how to make work happen in processes

Caching thing

see below

Cache invalidation

Invalidating analysis results

Requires tracking what methods/properties are used in what other files, and invalidating those results when linked methods change

Partial parsing

Reparsing bits of files that have changed, which is hard

Language Server Support

Handling temporary file changes

When files change Psalm figures out what's changed within them to avoid re-analysing things unnecessarily

Dealing with malformed PHP code

When people write code, it's not always pretty as they write it. A language server needs to deal with that bad code somehow

Fixing code with Psalter

Adding/replacing code

Figuring out what changed, making edits that could have been made by a human

Minimal diffs

hard to change more than you need


Manipulating Code Fixing (`docs/manipulating_code/fixing.md`)

Fixing Code

Psalm is good at finding potential issues in large codebases, but once found, it can be something of a gargantuan task to fix all the issues.

It comes with a tool, called Psalter, that helps you fix code.

You can either run it via its binary

text
vendor/bin/psalter [args]

or via Psalm's binary:

text
vendor/bin/psalm --alter [args]

Safety features

Updating code is inherently risky, doing so automatically is even more so. I've added a few features to make it a little more reassuring:

  • To see what changes Psalter will make ahead of time, you can run it with --dry-run.
  • You can target particular versions of PHP via --php-version, so that (for example) you don't add nullable typehints to PHP 7.0 code, or any typehints at all to PHP 5.6 code. --php-version defaults to your current version.
  • it has a --safe-types mode that will only update PHP 7 return typehints with information Psalm has gathered from non-docblock sources of type information (e.g. typehinted params, instanceof checks, other return typehints etc.)
  • using --allow-backwards-incompatible-changes=false you can make sure to not create backwards incompatible changes

Plugins

You can pass in your own manipulation plugins e.g.

bash
vendor/bin/psalter --plugin=vendor/vimeo/psalm/examples/plugins/ClassUnqualifier.php --dry-run

The above example plugin converts all unnecessarily qualified classnames in your code to shorter aliased versions.

Supported fixes

This initial release provides support for the following alterations, corresponding to the names of issues Psalm finds.
To fix all of these at once, run vendor/bin/psalter --issues=all

MissingReturnType

Running vendor/bin/psalter --issues=MissingReturnType --php-version=7.0 on

php
<?php
function foo() {
  return "hello";
}

gives

php
<?php
function foo() : string {
  return "hello";
}

and running vendor/bin/psalter --issues=MissingReturnType --php-version=5.6 on

php
<?php
function foo() {
  return "hello";
}

gives

php
<?php
/**
 * @return string
 */
function foo() {
  return "hello";
}

MissingClosureReturnType

As above, except for closures

InvalidReturnType

Running vendor/bin/psalter --issues=InvalidReturnType on

php
<?php
/**
 * @return int
 */
function foo() {
  return "hello";
}

gives

php
<?php
/**
 * @return string
 */
function foo() {
  return "hello";
}

There's also support for return typehints, so running vendor/bin/psalter --issues=InvalidReturnType on

php
<?php
function foo() : int {
  return "hello";
}

gives

php
<?php
function foo() : string {
  return "hello";
}

InvalidNullableReturnType

Running vendor/bin/psalter --issues=InvalidNullableReturnType --php-version=7.1 on

php
<?php
function foo() : string {
  return rand(0, 1) ? "hello" : null;
}

gives

php
<?php
function foo() : ?string {
  return rand(0, 1) ? "hello" : null;
}

and running vendor/bin/psalter --issues=InvalidNullableReturnType --php-version=7.0 on

php
<?php
function foo() : string {
  return rand(0, 1) ? "hello" : null;
}

gives

php
<?php
/**
 * @return string|null
 */
function foo() {
  return rand(0, 1) ? "hello" : null;
}

InvalidFalsableReturnType

Running vendor/bin/psalter --issues=InvalidFalsableReturnType on

php
<?php
function foo() : string {
  return rand(0, 1) ? "hello" : false;
}

gives

php
<?php
/**
 * @return string|false
 */
function foo() {
  return rand(0, 1) ? "hello" : false;
}

MissingParamType

Running vendor/bin/psalter --issues=MissingParamType on

php
<?php
class C {
  public static function foo($s) : void {
    echo $s;
  }
}
C::foo("hello");

gives

php
<?php
class C {
  /**
   * @param string $s
   */
  public static function foo($s) : void {
    echo $s;
  }
}
C::foo("hello");

MissingPropertyType

Running vendor/bin/psalter --issues=MissingPropertyType on

php
<?php
class A {
    public $foo;
    public $bar;
    public $baz;

    public function __construct()
    {
        if (rand(0, 1)) {
            $this->foo = 5;
        } else {
            $this->foo = "hello";
        }

        $this->bar = "baz";
    }

    public function setBaz() {
        $this->baz = [1, 2, 3];
    }
}

gives

php
<?php
class A {
    /**
     * @var string|int
     */
    public $foo;

    public string $bar;

    /**
     * @var array<int, int>|null
     * @psalm-var non-empty-list<int>|null
     */
    public $baz;

    public function __construct()
    {
        if (rand(0, 1)) {
            $this->foo = 5;
        } else {
            $this->foo = "hello";
        }

        $this->bar = "baz";
    }

    public function setBaz() {
        $this->baz = [1, 2, 3];
    }
}

MismatchingDocblockParamType

Given

php
<?php
class A {}
class B extends A {}
class C extends A {}
class D {}

running vendor/bin/psalter --issues=MismatchingDocblockParamType on

php
<?php
/**
 * @param B|C $first
 * @param D $second
 */
function foo(A $first, A $second) : void {}

gives

php
<?php
/**
 * @param B|C $first
 * @param A $second
 */
function foo(A $first, A $second) : void {}

MismatchingDocblockReturnType

Running vendor/bin/psalter --issues=MismatchingDocblockReturnType on

php
<?php
/**
 * @return int
 */
function foo() : string {
  return "hello";
}

gives

php
<?php
/**
 * @return string
 */
function foo() : string {
  return "hello";
}

LessSpecificReturnType

Running vendor/bin/psalter --issues=LessSpecificReturnType on

php
<?php
function foo() : ?string {
  return "hello";
}

gives

php
<?php
function foo() : string {
  return "hello";
}

PossiblyUndefinedVariable

Running vendor/bin/psalter --issues=PossiblyUndefinedVariable on

php
<?php
function foo()
{
    if (rand(0, 1)) {
      $a = 5;
    }
    echo $a;
}

gives

php
<?php
function foo()
{
    $a = null;
    if (rand(0, 1)) {
      $a = 5;
    }
    echo $a;
}

PossiblyUndefinedGlobalVariable

Running vendor/bin/psalter --issues=PossiblyUndefinedGlobalVariable on

php
<?php
if (rand(0, 1)) {
  $a = 5;
}
echo $a;

gives

php
<?php
$a = null;
if (rand(0, 1)) {
  $a = 5;
}
echo $a;

UnusedMethod

This removes private unused methods.

Running vendor/bin/psalter --issues=UnusedMethod on

php
<?php
class A {
    private function foo() : void {}
}

new A();

gives

php
<?php
class A {

}

new A();

PossiblyUnusedMethod

This removes protected/public unused methods.

Running vendor/bin/psalter --issues=PossiblyUnusedMethod on

php
<?php
class A {
    protected function foo() : void {}
    public function bar() : void {}
}

new A();

gives

php
<?php
class A {

}

new A();

UnusedProperty

This removes private unused properties.

Running vendor/bin/psalter --issues=UnusedProperty on

php
<?php
class A {
    /** @var string */
    private $foo;
}

new A();

gives

php
<?php
class A {

}

new A();

PossiblyUnusedProperty

This removes protected/public unused properties.

Running vendor/bin/psalter --issues=PossiblyUnusedProperty on

php
<?php
class A {
    /** @var string */
    public $foo;

    /** @var string */
    protected $bar;
}

new A();

gives

php
<?php
class A {

}

new A();

UnusedVariable

This removes unused variables.

Running vendor/bin/psalter --issues=UnusedVariable on

php
<?php
function foo(string $s) : void {
    $a = 5;
    $b = 6;
    $c = $b += $a -= intval($s);
    echo "foo";
}

gives

php
<?php
function foo(string $s) : void {
    echo "foo";
}

UnnecessaryVarAnnotation

This removes unused @var annotations

Running vendor/bin/psalter --issues=UnnecessaryVarAnnotation on

php
<?php
function foo() : string {
    return "hello";
}

/** @var string */
$a = foo();

gives

php
<?php
function foo() : string {
    return "hello";
}

$a = foo();

ParamNameMismatch

This aligns child class param names with their parent.

Running vendor/bin/psalter --issues=ParamNameMismatch on

php
<?php

class A {
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $string, bool $b = false) : void {
        echo $string;
    }
}

gives

php
<?php

class A {
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $str, bool $b = false) : void {
        echo $str;
    }
}

Manipulating Code Refactoring (`docs/manipulating_code/refactoring.md`)

Refactoring Code

Sometimes you want to make big changes to your codebase like moving methods or classes.

Psalm has a refactoring tool you can access with either vendor/bin/psalm-refactor or vendor/bin/psalm --refactor, followed by appropriate commands.

Moving all classes from one namespace to another

text
vendor/bin/psalm-refactor --move "Ns1\*" --into "Some\Other\Namespace"

This moves all classes in Ns1 (e.g. Ns1\Foo, Ns1\Baz) into the specified namespace. Files are moved as necessary.

Moving classes between namespaces

text
vendor/bin/psalm-refactor --move "Ns1\Foo" --into "Ns2"

This tells Psalm to move class Ns1\Foo into the namespace Ns2, so any reference to Ns1\Foo becomes Ns2\Foo). Files are moved as necessary.

Moving and renaming classes

text
vendor/bin/psalm-refactor --rename "Ns1\Foo" --to "Ns2\Bar\Baz"

This tells Psalm to move class Ns1\Foo into the namespace Ns2\Bar and rename it to Baz, so any reference to Ns1\Foo becomes Ns2\Bar\Baz). Files are moved as necessary.

Moving methods between classes

text
vendor/bin/psalm-refactor --move "Ns1\Foo::bar" --into "Ns2\Baz"

This tells Psalm to move a method named bar inside Ns1\Foo into the class Ns2\Baz, so any reference to Ns1\Foo::bar becomes Ns2\Baz::bar). Psalm currently allows you to move static methods between arbitrary classes, and instance methods into child classes of that instance.

Moving and renaming methods

text
vendor/bin/psalm-refactor --rename "Ns1\Foo::bar" --to "Ns2\Baz::bat"

This tells Psalm to move method Ns1\Foo::bar into the class Ns2\Baz and rename it to bat, so any reference to Ns1\Foo::bar becomes Ns2\Baz::bat).


Running Psalm Checking Non Php Files (`docs/running_psalm/checking_non_php_files.md`)

Checking non-PHP files

Psalm supports the ability to check various PHPish files by extending the FileChecker class. For example, if you have a template where the variables are set elsewhere, Psalm can scrape those variables and check the template with those variables pre-populated.

An example TemplateChecker is provided here.

Using `psalm.xml`

To ensure your custom FileChecker is used, you must update the Psalm fileExtensions config in psalm.xml:

xml
<fileExtensions>
    <extension name=".php" />
    <extension name=".phpt" checker="path/to/TemplateChecker.php" />
</fileExtensions>

Using custom plugin

Plugins can register their own custom scanner and analyzer implementations for particular file extensions.

php
<?php
namespace Psalm\Example;

use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\PluginFileExtensionsInterface;
use Psalm\Plugin\FileExtensionsInterface;
use Psalm\Plugin\RegistrationInterface;

class CustomPlugin implements PluginEntryPointInterface, PluginFileExtensionsInterface
{
    public function __invoke(RegistrationInterface $registration, ?\SimpleXMLElement $config = null): void
    {
        // ... regular plugin processes, stub registration, hook registration
    }

    public function processFileExtensions(FileExtensionsInterface $fileExtensions, ?SimpleXMLElement $config = null): void
    {
        $fileExtensions->addFileTypeScanner('phpt', TemplateScanner::class);
        $fileExtensions->addFileTypeAnalyzer('phpt', TemplateAnalyzer::class);
    }    
}

Running Psalm Command Line Usage (`docs/running_psalm/command_line_usage.md`)

Running Psalm

Once you've set up your config file, you can run Psalm from your project's root directory with

bash
./vendor/bin/psalm

and Psalm will scan all files in the project referenced by ``.

If you want to run on specific files, use

bash
./vendor/bin/psalm file1.php [file2.php...]

Command-line options

Run with --help to see a list of options that Psalm supports.

Exit status

Psalm exits with status 0 when it successfully completed and found no issues,
1 when there was a problem running Psalm and 2 when it completed
successfully but found some issues. Any exit status apart from those indicate
some internal problem.

Shepherd

Psalm currently offers some GitHub integration with public projects.

Add --shepherd to send information about your build to https://shepherd.dev.

Currently, Shepherd tracks type coverage (the percentage of types Psalm can infer) on master branches.

Running Psalm faster

To run Psalm up to 50% faster, use the official docker image.

Psalm also has a couple of command-line options that will result in faster builds:

  • --threads=[n] to run Psalm’s analysis in a number of threads
  • --diff which only checks files you’ve updated since the last run (and their dependents).

In Psalm 4 --diff is turned on by default (you can disable it with --no-diff).

Data from the last run is stored in the cache directory, which may be set in configuration.
If you are running Psalm on a build server, you may want to configure the server to ensure that the cache directory
is preserved between runs.

Running them together (e.g. --threads=8 --diff) will result in the fastest possible Psalm run.

Reviewing issues in your IDE of choice

Psalm now offers a psalm-review tool which allows you to manually review issues one by one in your favorite IDE.

bash
./vendor/bin/psalm-review report.json [code|phpstorm|code-server] [ inv|rev|[~-]IssueType1 ] [ [~-]IssueType2 ] ...

The tool may also be run using the main psalm entry point, useful for example when working with the phar:

bash
./vendor/bin/psalm.phar --review report.json [code|phpstorm|code-server] [ inv|rev|[~-]IssueType1 ] [ [~-]IssueType2 ] ...

psalm-review parses the Psalm JSON report in report.json (generated using vendor/bin/psalm --report=report.json) and opens the specified IDE at the line and column of the issue, one by one for all issues; press enter to go to the next issue, q to quit.

The IDE argument is optional when running inside a supported IDE's integrated terminal β€” PhpStorm, VS Code, and code-server are auto-detected via environment variables. When running from an external terminal, the IDE must be specified explicitly.

The extra arguments may be used to filter only for issues of the specified types, or for all issues except the specified types (with the ~ or - inversion).

The rev or inv keywords may be used to start from the end of the report instead of at the beginning.


Running Psalm Configuration (`docs/running_psalm/configuration.md`)

Configuration

Psalm uses an XML config file (by default, psalm.xml). A barebones example looks like this:

xml
<?xml version="1.0"?>
<psalm>
    <projectFiles>
        <directory name="src" />
    </projectFiles>
</psalm>

Configuration file may be split into several files using XInclude tags (c.f. previous example):

psalm.xml

xml
<?xml version="1.0"?>
<psalm
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
    xmlns:xi="http://www.w3.org/2001/XInclude"
>
    <xi:include href="files.xml"/>
</psalm>

files.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<projectFiles xmlns="https://getpsalm.org/schema/config">
    <file name="Bar.php" />
    <file name="Bat.php" />
</projectFiles>

Different configuration file

You can also create a different configuration file, then run Psalm with:

bash
vendor/bin/psalm --config=other-psalm-config.xml

Optional <psalm /> attributes

Coding style

errorLevel

xml
<psalm
  errorLevel="[int]"
/>

This corresponds to Psalmβ€˜s error-detection level.

reportMixedIssues

xml
<psalm
  reportMixedIssues="[bool]"
/>

Setting this to "false" hides all issues with Mixed types in Psalm’s output. If not given, this defaults to "false" when errorLevel is 3 or higher, and "true" when the error level is 1 or 2.

totallyTyped

(Deprecated) This setting has been replaced by reportMixedIssues which is automatically enabled when errorLevel is 1.

resolveFromConfigFile

xml
<psalm
  resolveFromConfigFile="[bool]"
/>

If this is enabled, relative directories mentioned in the config file will be resolved relative to the location
of the config file. If it is disabled, or absent they will be resolved relative to the working directory of the Psalm process.

New versions of Psalm enable this option when generating config files. Older versions did not include it.

useDocblockTypes

xml
<psalm
  useDocblockTypes="[bool]"
>

Whether or not to use types as defined in docblocks. Defaults to true.

useDocblockPropertyTypes

xml
<psalm
  useDocblockPropertyTypes="[bool]"
>

If not using all docblock types, you can still use docblock property types. Defaults to false (though only relevant if useDocblockTypes is false).

docblockPropertyTypesSealProperties

xml
<psalm
  docblockPropertyTypesSealProperties="[bool]"
>

Whether using @property in class docblocks should imply @psalm-seal-properties. Defaults to true.

usePhpDocMethodsWithoutMagicCall

xml
<psalm
  usePhpDocMethodsWithoutMagicCall="[bool]"
>

The PHPDoc @method annotation normally only applies to classes with a __call method. Setting this to true allows you to use the @method annotation to override inherited method return types. Defaults to false.

usePhpDocPropertiesWithoutMagicCall

xml
<psalm
  usePhpDocPropertiesWithoutMagicCall="[bool]"
>

The PHPDoc @property, @property-read and @property-write annotations normally only apply to classes with __get/__set methods. Setting this to true allows you to use the @property, @property-read and @property-write annotations to override property existence checks and resulting property types. Defaults to false.

disableVarParsing

xml
<psalm
  disableVarParsing="[bool]"
/>

Disables parsing of @var PHPDocs everywhere except for properties. Setting this to true can remove many false positives due to outdated @var annotations, used before integrations of Psalm generics and proper typing, enforcing Single Source Of Truth principles. Defaults to false.

strictBinaryOperands

xml
<psalm
  strictBinaryOperands="[bool]"
>

If true we force strict typing on numerical and string operations (see https://github.com/vimeo/psalm/issues/24). Defaults to true.

allowBoolToLiteralBoolComparison

xml
<psalm
  allowBoolToLiteralBoolComparison="[bool]"
>

If false, disallows comparing a boolean to a literal boolean (mostly a codestyle rule, not a bug detection rule; see RedundantIdentityWithTrue). Defaults to true.

rememberPropertyAssignmentsAfterCall

xml
<psalm
  rememberPropertyAssignmentsAfterCall="[bool]"
>

Setting this to false means that function calls will cause Psalm to forget what it knew about object properties within the scope of the function it's currently analysing, unless the callee can't write them (see property refinements after calls). This duplicates functionality that Hack has. Defaults to true.

allowStringToStandInForClass

xml
<psalm
  allowStringToStandInForClass="[bool]"
>

When true, strings can be used as classes, meaning $some_string::someMethod() is allowed. If false, only class constant strings (of the form Foo\Bar::class) can stand in for classes, otherwise an InvalidStringClass issue is emitted. Defaults to false.

disableSuppressAll

xml
<psalm
  disableSuppressAll="[bool]"
>

When true, disables wildcard suppression of all issues with @psalm-suppress all. Defaults to true.

memoizeMethodCallResults

xml
<psalm
  memoizeMethodCallResults="[bool]"
>

When true, the results of method calls without arguments passed are remembered between repeated calls of that method on a given object. Defaults to false.

hoistConstants

xml
<psalm
  hoistConstants="[bool]"
>

When true, constants defined in a function in a file are assumed to be available when requiring that file, and not just when calling that function. Defaults to false (i.e. constants defined in functions will only be available for use when that function is called)

addParamDefaultToDocblockType

xml
<psalm
  addParamDefaultToDocblockType="[bool]"
>

Occasionally a param default will not match up with the docblock type. By default, Psalm emits an issue. Setting this flag to true causes it to expand the param type to include the param default. Defaults to false.

checkForThrowsDocblock

xml
<psalm
  checkForThrowsDocblock="[bool]"
>

When true, Psalm will check that the developer has supplied @throws docblocks for every exception thrown in a given function or method. Defaults to false.

checkForThrowsInGlobalScope

xml
<psalm
  checkForThrowsInGlobalScope="[bool]"
>

When true, Psalm will check that the developer has caught every exception in global scope. Defaults to false.

ignoreInternalFunctionFalseReturn

xml
<psalm
  ignoreInternalFunctionFalseReturn="[bool]"
>

When true, Psalm ignores possibly-false issues stemming from return values of internal functions (like preg_split) that may return false, but do so rarely. Defaults to false.

ignoreInternalFunctionNullReturn

xml
<psalm
  ignoreInternalFunctionNullReturn="[bool]"
>

When true, Psalm ignores possibly-null issues stemming from return values of internal array functions (like current) that may return null, but do so rarely. Defaults to false.

inferPropertyTypesFromConstructor

xml
<psalm
  inferPropertyTypesFromConstructor="[bool]"
>

When true, Psalm infers property types from assignments seen in straightforward constructors. Defaults to true.

findUnusedVariablesAndParams

xml
<psalm
  findUnusedVariablesAndParams="[bool]"
>

When true, Psalm will attempt to find all unused variables, the equivalent of running with --find-unused-variables. Defaults to false.

findUnusedCode

xml
<psalm
  findUnusedCode="[bool]"
>

When true, Psalm will attempt to find all unused code (including unused variables), the equivalent of running with --find-unused-code. Defaults to true.

Unused-code detection is based on a graph of references between code elements, resolved by reachability from the program's entry points (the public API, top-level code, free functions, and code outside of the project directories). As a result, a class, method, property or constant that is referenced only by other unused code β€” including cycles of otherwise unreferenced code β€” is reported as unused. Suppressing an unused-code issue (e.g. @psalm-suppress UnusedClass) only silences the report for that symbol; it does not turn the symbol into an entry point, so code that only it references is still reported.

This is enabled by default since Psalm 6, so unused code issues are reported even without passing --find-unused-code (or --find-dead-code). Set findUnusedCode="false" to turn it off.

forceJit

xml
<psalm
  forceJit="[bool]"
>

When true, Psalm will enable JIT acceleration and exit immediately if it cannot be enabled, the equivalent of running with --force-jit. When false (default), Psalm runs without JIT.

noCache

xml
<psalm
  noCache="[bool]"
>

When true, Psalm will disable usage of the cache, the equivalent of running with --no-cache. Defaults to false.

arrayCache

xml
<psalm
  arrayCache="[bool]"
>

When false, Psalm will disable usage of the array cache.

The array cache is used together with the file cache to avoid re-loading cache entries when re-scanning the same file, which offers a nice performance improvement.

Defaults to true.

disallowLiteralKeysOnUnshapedArrays

xml
<psalm
  disallowLiteralKeysOnUnshapedArrays="[bool]"
>

When true, Psalm will emit issues when using literal keys on unshaped arrays (useful to enforce usage of shaped arrays). Defaults to false.

allFunctionsGlobal

xml
<psalm
  allFunctionsGlobal="[bool]"
>

When true, Psalm will treat all functions declared in scanned files as available to all files, even if they aren't loaded by the Composer autoloader (useful for legacy codebases which make heavy use of require/include, instead of Composer's autoloader). Defaults to false.

allConstantsGlobal

xml
<psalm
  allConstantsGlobal="[bool]"
>

When true, Psalm will treat all constants declared in scanned files as available to all files, even if they aren't loaded by the Composer autoloader (useful for legacy codebases which make heavy use of require/include, instead of Composer's autoloader). Defaults to false.

findUnusedPsalmSuppress

xml
<psalm
  findUnusedPsalmSuppress="[bool]"
>

When true, Psalm will report all @psalm-suppress annotations that aren't used, the equivalent of running with --find-unused-psalm-suppress. Defaults to false.

ensureArrayStringOffsetsExist

xml
<psalm
  ensureArrayStringOffsetsExist="[bool]"
>

When true, Psalm will complain when referencing an explicit string offset on an array e.g. $arr['foo'] without a user first asserting that it exists (either via an isset check or via an object-like array). Defaults to false.

ensureArrayIntOffsetsExist

xml
<psalm
  ensureArrayIntOffsetsExist="[bool]"
>

When true, Psalm will complain when referencing an explicit integer offset on an array e.g. $arr[7] without a user first asserting that it exists (either via an isset check or via an object-like array). Defaults to false.

ensureOverrideAttribute

xml
<psalm
  ensureOverrideAttribute="[bool]"
>

When true, Psalm will report class and interface methods that override a method on a parent, but do not have an Override attribute. Defaults to true.

phpVersion

xml
<psalm
  phpVersion="[string]"
>

Set the php version Psalm should assume when checking and/or fixing the project. If this attribute is not set, Psalm uses the declaration in composer.json if one is present. It will check against the earliest version of PHP that satisfies the declared php dependency

This can be overridden on the command-line using the --php-version= flag which takes the highest precedence over both the phpVersion setting and the version derived from composer.json.

skipChecksOnUnresolvableIncludes

xml
<psalm
  skipChecksOnUnresolvableIncludes="[bool]"
>

When true, Psalm will skip checking classes, variables and functions after it comes across an include or require it cannot resolve. This allows code to reference functions and classes unknown to Psalm.

This defaults to false.

sealAllMethods

xml
<psalm
  sealAllMethods="[bool]"
>

When true, Psalm will treat all classes as if they had sealed methods, meaning that if you implement the magic method __call, you also have to add @method for each magic method. Defaults to true.

sealAllProperties

xml
<psalm
  sealAllProperties="[bool]"
>

When true, Psalm will treat all classes as if they had sealed properties, meaning that Psalm will disallow getting and setting any properties not contained in a list of @property (or @property-read/@property-write) annotations and not explicitly defined as a property. Defaults to true.

runTaintAnalysis

xml
<psalm
  runTaintAnalysis="[bool]"
>

When true (the default), Psalm will run Taint Analysis on your codebase. This config is the same as if you were running Psalm with --taint-analysis.

reportInfo

xml
<psalm
  reportInfo="[bool]"
>

When false, Psalm will not consider issue at lower level than errorLevel as info (they will be suppressed instead). This can be a big improvement in analysis time for big projects. However, this config will prevent Psalm to count or suggest fixes for suppressed issue

allowNamedArgumentCalls

xml
<psalm
  allowNamedArgumentCalls="[bool]"
>

When false, Psalm will not report ParamNameMismatch issues in your code anymore. This does not replace the use of individual @no-named-arguments to prevent external access to a library's method or to reduce the type to a list when using variadics

triggerErrorExits

xml
<psalm
   triggerErrorExits="[string]"
>

Describe the behavior of trigger_error. always means it always exits, never means it never exits, default means it exits only for E_USER_ERROR. Default is default

Running Psalm

autoloader

xml
<psalm
  autoloader="[string]"
>

If your application registers one or more custom autoloaders, and/or declares universal constants/functions, this autoloader script will be executed by Psalm before scanning starts. Psalm always registers composer's autoloader by default.

throwExceptionOnError

xml
<psalm
  throwExceptionOnError="[bool]"
>

Useful in testing, this makes Psalm throw a regular-old exception when it encounters an error. Defaults to false.

hideExternalErrors

xml
<psalm
  hideExternalErrors="[bool]"
>

Whether or not to show issues in files that are used by your project files, but which are not included in ``. Defaults to false.

hideAllErrorsExceptPassedFiles

xml
<psalm
  hideAllErrorsExceptPassedFiles="[bool]"
>

Whether or not to report issues only for files that were passed explicitly as arguments in CLI. This means any files that are loaded with require/include will not report either, if not set in CLI. Useful if you want to only check errors in a single or selected files. Defaults to false.

cacheDirectory

xml
<psalm
  cacheDirectory="[string]"
>

The directory used to store Psalm's cache data - if you specify one (and it does not already exist), its parent directory must already exist, otherwise Psalm will throw an error.

Defaults to $XDG_CACHE_HOME/psalm. If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache/psalm is used or sys_get_temp_dir() . '/psalm' when not defined.

allowFileIncludes

xml
<psalm
  allowFileIncludes="[bool]"
>

Whether or not to allow require/include calls in your PHP. Defaults to true.

ignoreIncludeSideEffects

xml
<psalm
  ignoreIncludeSideEffects="[bool]"
>

Whether or not to ignore side effects of includes.

Ignoring include side effects can significantly speed up scans on legacy codebases, but changes to variables or variables defined inside of included files will not be visible to Psalm (functions, constants and classes will still be visible anyway, especially if the hoistConstants, allConstantsGlobal, allFunctionsGlobal configuration parameters are set to true).

Defaults to false.

respectIncludeOnce

xml
<psalm
  respectIncludeOnce="[bool]"
>

Whether or not to process files after the first require_once() or include_once() call.

By default, Psalm treats require_once() and include_once() calls as if they were require() or include() calls in an attempt to consider all possible permutations of file include order. Respecting require_once() and include_once() behavior can significantly speed up scans on codebases with circular require_once() or include_once() calls. While potentially less comprehensive, this setting aims to more closely mimic PHP's runtime behavior.

Defaults to false.

serializer

xml
<psalm
  serializer="['igbinary'|'default']"
>

Allows you to hard-code a serializer for Psalm to use when caching data. By default, Psalm uses ext-igbinary if the version is greater than or equal to 2.0.5, otherwise it defaults to PHP's built-in serializer.

compressor

xml
<psalm
  compressor="['lz4'|'deflate'|'off']"
>

Allows you to hard-code a compressor for Psalm's cache. By default, Psalm uses ext-zlib deflate, if it's enabled.

threads

xml
<psalm
        threads="[int]"
>

Allows you to hard-code the number of threads Psalm will use (similar to --threads on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using --threads or --debug (which sets threads to 1) on the command line

scanThreads

xml
<psalm
        scanThreads="[int]"
>

Allows you to hard-code the number of threads Psalm will use during the scan phase (as opposed to the analysis phase, controlled by the threads field) (similar to --scan-threads on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using --scan-threads, --threads or --debug (which sets threads to 1) on the command line.

maxStringLength

xml
<psalm
  maxStringLength="1000"
>

This setting controls the maximum length of literal strings that will be transformed into a literal string type during Psalm analysis.
Strings longer than this value (by default 1000 bytes) will be transformed in a generic non-empty-string type, instead.

Please note that changing this setting might introduce unwanted side effects and those side effects won't be considered as bugs.

maxShapedArraySize

xml
<psalm
  maxShapedArraySize="100"
>

This setting controls the maximum size of shaped arrays that will be transformed into a shaped array{key1: "value", key2: T} type during Psalm analysis.
Arrays bigger than this value (100 by default) will be transformed in a generic non-empty-array type, instead.

Please note that changing this setting might introduce unwanted side effects and those side effects won't be considered as bugs.

longScanWarning

xml
<psalm
  longScanWarning="10.0"
>

Specifies the maximum scan and analysis duration for individual files; files which take longer than the specified amount of seconds (as a floating point, so values smaller than one second are also accepted) will print out a warning to CLI (the scan will not be interrupted).

Only used in multithreaded mode, defaults to 10 seconds.

restrictReturnTypes

xml
<psalm
  restrictReturnTypes="true"
>

Emits LessSpecificReturnType when the declared return type is not as tight as
the inferred return type.

This code:

php
function getOne(): int // declared type: int
{
    return 1; // inferred type: 1 (int literal)
}

Will give this error: LessSpecificReturnType - The inferred return type '1' for getOne is more specific than the declared return type 'int'

To fix the error, you should specify the more specific type in the doc-block:

php
/**
 * @return 1
 */
function getOne(): int
{
    return 1;
}

Warning: Forcing a tighter type is not always the best course of action and
may cause unexpected results. The following code is invalid with
restrictReturnTypes="true":

php
class StandardCar {
    /**
     * @return list{'motor', 'brakes', 'wheels'}
     */
    public function getSystems(): array {
        return ['motor', 'brakes', 'wheels'];
    }
}

class PremiumCar extends StandardCar {
    /**
     * @return list{'motor', 'brakes', 'wheels', 'rear parking sensor'}
     */
    public function getSystems(): array {
        return ['motor', 'brakes', 'wheels', 'rear parking sensor'];
    }
}

ImplementedReturnTypeMismatch - The inherited return type 'list{'motor', 'brakes', 'wheels'}' for StandardCar::getSystems is different to the implemented return type for PremiumCar::getsystems 'list{'motor', 'brakes', 'wheels', 'rear parking sensor'}'

findUnusedBaselineEntry

Emits UnusedBaselineEntry when a baseline entry
is not being used to suppress an issue. Defaults to true.

findUnusedIssueHandlerSuppression

Emits UnusedIssueHandlerSuppression when a suppressed issue handler
is not being used to suppress an issue. Defaults to true.

Project settings

<projectFiles>

Contains a list of all the directories that Psalm should inspect. You can also specify a set of files and folders to ignore with the `` directive. By default, ignored files/folders are required to exist. An allowMissingFiles attribute can be added for ignored files/folders than may or may not exist.

xml
<projectFiles>
  <directory name="src" />
  <ignoreFiles>
    <directory name="src/Stubs" />
  </ignoreFiles>
  <ignoreFiles allowMissingFiles="true">
    <directory name="path-that-may-not-exist" />
  </ignoreFiles>
</projectFiles>

<extraFiles>

Optional. Same format as ``. Directories Psalm should load but not inspect.

<fileExtensions>

Optional. A list of extensions to search over. See Checking non-PHP files to understand how to extend this.

<enableExtensions>

Optional. A list of extensions to enable. By default, only extensions required by your composer.json will be enabled.

xml
<enableExtensions>
  <extension name="decimal"/>
  <extension name="pdo"/>
</enableExtensions>

<disableExtensions>

Optional. A list of extensions to disable. By default, only extensions required by your composer.json will be enabled.

xml
<disableExtensions>
  <extension name="gmp"/>
</disableExtensions>

<plugins>

Optional. A list of `` entries. See the Plugins section for more information.

<issueHandlers>

Optional. If you don't want Psalm to complain about every single issue it finds, the issueHandler tag allows you to configure that. Dealing with code issues tells you more.

<unsuppressibleIssues>

Optional. A list of issues that can't be suppressed: Psalm reports them as errors whatever @psalm-suppress, ``, the error level, reportMixedIssues and the baseline say.

xml
<unsuppressibleIssues>
  
  <issue name="TaintedInput" />
  <issue name="PossiblyNullReference" />
  
  <issue name="Vendor\Plugin\Issue\SomeIssue" />
</unsuppressibleIssues>

An entry names an issue, or a class or interface several issues extend, which makes all of them unsuppressible: TaintedInput for every taint issue, MixedIssue for every Mixed* issue, ClassIssue, MethodIssue, PropertyIssue, ArgumentIssue, FunctionIssue, ClassConstantIssue, VariableIssue, PluginIssue for every issue of a plugin, or CodeIssue for every issue. An issue of a plugin is named by the fully qualified name of its class.

Taint issues can only be suppressed in docblocks all at once, with @psalm-suppress TaintedInput, which stops tracking taints in the function or the statement it annotates: if any taint issue is unsuppressible, it doesn't, and every taint issue found there is reported.

The issues are still only reported in the files of `` that aren't ignored. A @psalm-suppress of an unsuppressible issue is never used, so findUnusedPsalmSuppress reports it, and a baseline entry for one is never used either: --set-baseline and --update-baseline leave them out, and findUnusedBaselineEntry reports those of an existing baseline.

<mockClasses>

Optional. Do you use mock classes in your tests? If you want Psalm to ignore them when checking files, include a fully-qualified path to the class with <class name="Your\Namespace\ClassName" />

<universalObjectCrates>

Optional. Do you have objects with properties that cannot be determined statically? If you want Psalm to treat all properties on a given classlike as mixed, include a fully-qualified path to the class with <class name="Your\Namespace\ClassName" />. By default, stdClass and SimpleXMLElement are configured to be universal object crates.

<stubs>

Optional. If your codebase uses classes and functions that are not visible to Psalm via reflection
(e.g. if there are internal packages that your codebase relies on that are not available on the machine running Psalm),
you can use stub files. Used by PhpStorm (a popular IDE) and others, stubs provide a description of classes and
functions without the implementations.

You can find a list of stubs for common classes here.
List out each file with <file name="path/to/file.php" />. In case classes to be tested use parent classes
or interfaces defined in a stub file, this stub should be configured with attribute preloadClasses="true".

xml
<stubs>
  <file name="path/to/file.php" />
  <file name="path/to/abstract-class.php" preloadClasses="true" />
</stubs>

Stubs aren't analyzed, but the issues of their docblocks (e.g. InvalidDocblock) are reported, also for the stubs
plugins add, so that an annotation a stub gets wrong isn't silently dropped.

Note: some extension stubs are already built into Psalm, and can be enabled by requiring the extension in your composer.json or by using the enableExtensions config key.

<ignoreExceptions>

Optional. A list of exceptions to not report for checkForThrowsDocblock or checkForThrowsInGlobalScope. The class tag will make Psalm ignore only instances of the specified class, while classAndDescendants will make Psalm also ignore subclasses. If an exception has onlyGlobalScope set to true, only checkForThrowsInGlobalScope is ignored for that exception, e.g.

xml
<ignoreExceptions>
  <class name="fully\qualified\path\Exc" onlyGlobalScope="true" />
  <classAndDescendants name="fully\qualified\path\OtherExc" />
</ignoreExceptions>

<globals>

Optional. If your codebase uses global variables that are accessed with the global keyword, you can declare their type. e.g.

xml
<globals>
  <var name="globalVariableName" type="type" />
</globals>

Some frameworks and libraries expose functionalities through e.g. $GLOBALS[DB]->query($query).
The following configuration declares custom types for super-globals ($GLOBALS, $_GET, ...).

xml
<globals>
  <var name="GLOBALS" type="array{DB: MyVendor\DatabaseConnection, VIEW: MyVendor\TemplateView}" />
  <var name="_GET" type="array{data: array<string, string>}" />
</globals>

The example above declares global variables as shown below

  • $GLOBALS
    • DB of type MyVendor\DatabaseConnection
    • VIEW of type MyVendor\TemplateView
  • $_GET
    • data e.g. like ["id" => "123", "title" => "Nice"]

<forbiddenFunctions>

Optional. Allows you to specify a list of functions that should emit the ForbiddenCode issue type.

xml
<forbiddenFunctions>
  <function name="var_dump" />
  <function name="dd" />
</forbiddenFunctions>

<forbiddenConstants>

Optional. Allows you to specify a list of constants that should emit the ForbiddenCode issue type.

xml
<forbiddenConstants>
  <constant name="FILTER_VALIDATE_URL" />
</forbiddenConstants>

<taintAnalysis>

Optional. Settings of the security analysis:

  • ``: the files and directories whose taint paths are not reported (see
    Avoiding files in taint paths).
  • <taints>: custom taints, which the annotations can then name without a plugin registering them. Each can be added
    to aliases of built-in taints (input, tainted, input_except_sleep) with <alias>: a taint added to input is
    user input, which the built-in sources give (see
    Custom taints declared in the configuration).
xml
<taintAnalysis>
  <taints>
    <taint name="file_upload">
      <alias name="input" />
    </taint>
    <taint name="session_id" />
  </taints>
</taintAnalysis>

Accessing Psalm configuration in plugins

Plugins can access or modify the global configuration in plugins using
singleton Psalm\Config.

php
$config = \Psalm\Config::getInstance();
if (!isset($config->globals['$GLOBALS'])) {
    $config->globals['$GLOBALS'] = 'array{data: array<string, string>}';
}

Running Psalm Dealing With Code Issues (`docs/running_psalm/dealing_with_code_issues.md`)

Dealing with code issues

Psalm has a large number of code issues. Each project can specify its own reporting level for a given issue.

Code issue levels in Psalm fall into three categories:

  • error
    This will cause Psalm to print a message, and to ultimately terminate with a non-zero exit status

  • info
    This will cause Psalm to print a message

  • suppress
    This will cause Psalm to ignore the code issue entirely

The third category, suppress, is the one you will probably be most interested in, especially when introducing Psalm to a large codebase.

Suppressing issues

There are two ways to suppress an issue – via the Psalm config or via a function docblock.

Config suppression

You can use the `` tag in the config file to influence how issues are treated.

Some issue types allow the use of referencedMethod, referencedClass or referencedVariable to isolate known trouble spots.

xml
<issueHandlers>
  <MissingPropertyType errorLevel="suppress" /> 

  <InvalidReturnType>
    <errorLevel type="suppress">
      <directory name="some_bad_directory" /> 
      <file name="some_bad_file.php" />  
    </errorLevel>
  </InvalidReturnType>
  <UndefinedMethod>
    <errorLevel type="suppress">
      <referencedMethod name="Bar\Bat::bar" /> 
      <file name="some_bad_file.php" />
    </errorLevel>
  </UndefinedMethod>
  <UndefinedClass>
    <errorLevel type="suppress">
      <referencedClass name="Bar\Bat\Baz" />
    </errorLevel>
  </UndefinedClass>
  <PropertyNotSetInConstructor>
    <errorLevel type="suppress">
        <referencedProperty name="Symfony\Component\Validator\ConstraintValidator::$context" />
    </errorLevel>
  </PropertyNotSetInConstructor>
  <UndefinedGlobalVariable>
    <errorLevel type="suppress">
      <referencedVariable name="$fooBar" /> 
    </errorLevel>
  </UndefinedGlobalVariable>
  <PluginIssue name="IssueNameEmittedByPlugin" errorLevel="info" /> 
</issueHandlers>

Docblock suppression

You can also use @psalm-suppress IssueName on a function's docblock to suppress Psalm issues e.g.

php
<?php
/**
 * @psalm-suppress InvalidReturnType
 */
function (int $a) : string {
  return $a;
}

You can also suppress issues at the line level e.g.

php
<?php
/**
 * @psalm-suppress InvalidReturnType
 */
function (int $a) : string {
  /**
   * @psalm-suppress InvalidReturnStatement
   */
  return $a;
}

If you wish to suppress all issues, you can use @psalm-suppress all instead of multiple annotations.

The issues listed in `` can't be suppressed, neither this way nor with `` or a baseline.

Using a baseline file

If you have a bunch of errors and you don't want to fix them all at once, Psalm can grandfather-in errors in existing code, while ensuring that new code doesn't have those same sorts of errors.

text
vendor/bin/psalm --set-baseline

will generate a file psalm-baseline.xml containing the current errors. Alternatively, you can specify the name of your baseline file.

text
vendor/bin/psalm --set-baseline=your-baseline.xml

You should commit that generated file so that Psalm can use it when running in other places (e.g. CI). It won't complain about those errors either.

You have two options to use the generated baseline when running psalm:

text
vendor/bin/psalm --use-baseline=your-baseline.xml

or using the configuration:

xml
<?xml version="1.0"?>
<psalm
       ...
       errorBaseline="./path/to/your-baseline.xml"
>
   ...
</psalm>

To update that baseline file, use

text
vendor/bin/psalm --update-baseline

This will remove fixed issues, but will not add new issues. To add new issues, use --set-baseline=....

In case you want to run psalm without the baseline, run

text
vendor/bin/psalm --ignore-baseline

Baseline files are a great way to gradually improve a codebase.

Using a plugin

If you want something more custom, like suppressing a certain type of error on classes that implement a particular interface, you can use a plugin that implements AfterClassLikeVisitInterface

php
<?php
namespace Foo\Bar;

use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use ReflectionClass;

/**
 * Suppress issues dynamically based on interface implementation
 */
class DynamicallySuppressClassIssueBasedOnInterface implements AfterClassLikeVisitInterface
{
    public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
    {
        $storage = $event->getStorage();
        if ($storage->user_defined
            && !$storage->is_interface
            && \class_exists($storage->name)
            && (new ReflectionClass($storage->name))->implementsInterface(\Your\Interface::class)
        ) {
            $storage->suppressed_issues[-1] = 'PropertyNotSetInConstructor';
        }
    }
}

Running Psalm Error Levels (`docs/running_psalm/error_levels.md`)

Error levels

Psalm can run at error levels 1 (strictest) through 8 (most lenient). The default is level 2.

A stricter level (lower number) reports more issues as errors. A more lenient level (higher number) demotes issues to info (non-blocking).

Quick-reference summary

Level Strictness What changes compared to the level above
1 (strictest) Everything is an error Adds Mixed* issues, LessSpecificReturnType as errors
2 (default) Most issues Mixed* demoted to info
3 Moderate Deprecated*, MissingParamType, PropertyNotSetInConstructor demoted to info
4 Lenient Possibly* issues demoted to info
5 More lenient InvalidScalarArgument, RedundantCondition, TooManyArguments demoted to info
6 Very lenient FalsableReturnStatement, InvalidNullableReturnType demoted to info
7 Extremely lenient InvalidArgument, InvalidMethodCall, UndefinedMethod demoted to info
8 (most lenient) Almost nothing MethodSignatureMismatch, UninitializedProperty demoted to info

Special categories

  • Always errors: Issues with very low false-positive rates (e.g., UndefinedClass, UndefinedVariable). These cannot be suppressed by changing the error level.
  • Feature-specific errors: Issues only reported when their feature is enabled (e.g., --taint-analysis for Tainted*, --find-unused-code for Unused*). When enabled, they are always treated as errors.

Always treated as errors

These issues have very low false-positive rates and indicate definite problems. They cannot be suppressed by changing the error level.

Errors that only appear at level 1

At the default level (2), these are reported as info. Set errorLevel="1" to treat them as errors.

Errors at level 2 and below

These issues become info (non-blocking) at level 3 and higher.

Errors at level 3 and below

These issues become info (non-blocking) at level 4 and higher.

Errors at level 4 and below

These issues become info (non-blocking) at level 5 and higher.

Errors at level 5 and below

These issues become info (non-blocking) at level 6 and higher.

Errors at level 6 and below

These issues become info (non-blocking) at level 7 and higher.

Errors at level 7 and below

These issues become info (non-blocking) at level 8.

Feature-specific errors

These issues are only reported when their corresponding feature is enabled (e.g., --taint-analysis for Tainted* issues, --find-unused-code for Unused* issues). When enabled, they are always treated as errors.


Running Psalm Installation (`docs/running_psalm/installation.md`)

Installation

The latest version of Psalm requires PHP >= 8.2 and Composer.

bash
composer require --dev vimeo/psalm

Generate a config file:

bash
./vendor/bin/psalm --init

Psalm will scan your project and figure out an appropriate error level for your codebase.

Then run Psalm:

bash
./vendor/bin/psalm --no-cache

Psalm will probably find a number of issues - find out how to deal with them in Dealing with code issues.

Docker image

It is recommended to run Psalm in the official docker image: it uses a custom build of PHP built from scratch, running Psalm +30% faster on average than normal PHP (+50% faster if comparing to PHP without opcache installed).

bash
docker run -v $PWD:/app --rm -it ghcr.io/danog/psalm:latest /composer/vendor/bin/psalm --no-cache

More specific tags are also available (i.e. ghcr.io/danog/psalm:6.9.1, as well as branch tags).

Issues due to missing extensions can be fixed by enabling them in psalm.xml and/or requiring them in composer.json see here » for more info.

Extensions not stubbed by Psalm itself (and thus not available as a psalm config option) may be stubbed using traditional PHP stubs.

Installing plugins

While Psalm can figure out the types used by various libraries based on
their source code and docblocks, it works even better with custom-tailored types
provided by Psalm plugins.

Check out the list of existing plugins on Packagist.
Install them with composer require --dev && vendor/bin/psalm-plugin enable

Read more about plugins in Using Plugins chapter.

Using the Phar

Sometimes your project can conflict with one or more of Psalm’s dependencies. In
that case you may find the Phar (a self-contained PHP executable) useful.

The Phar can be downloaded from GitHub:

bash
wget https://github.com/vimeo/psalm/releases/latest/download/psalm.phar
chmod +x psalm.phar
./psalm.phar --version

Alternatively, you can use Composer to install the Phar:

bash
composer require --dev psalm/phar

Running Psalm Issues (`docs/running_psalm/issues.md`)

Issue types


Running Psalm Language Server (`docs/running_psalm/language_server.md`)

Using Psalm’s Language Server

Psalm now has built-in Language Server Compatibility support so you can run it in your favourite IDE.

It currently supports diagnostics (i.e. finding errors and warnings), go-to-definition and hover, with limited support for autocompletion (PRs are welcome!).

It works well in a variety of editors (listed alphabetically):

Client configuration

Emacs

I got it working with eglot

This is the config I used:

text
(when (file-exists-p "vendor/bin/psalm-language-server")
  (progn
    (require 'php-mode)
    (require 'eglot)
    (add-to-list 'eglot-server-programs '(php-mode . ("php" "vendor/bin/psalm-language-server")))
    (add-hook 'php-mode-hook 'eglot-ensure)
    (advice-add 'eglot-eldoc-function :around
                (lambda (oldfun)
                  (let ((help (help-at-pt-kbd-string)))
                    (if help (message "%s" help) (funcall oldfun)))))
    )
  )

PhpStorm

Native Support

As of PhpStorm 2020.3 support for psalm is supported and on by default, you can read more about that here

With LSP

Alternatively, psalm works with gtache/intellij-lsp plugin (Jetbrains-approved version, latest version).

Setup is done via a GUI.

When you install the plugin, you should see a "Language Server Protocol" section under the "Languages & Frameworks" tab.

In the "Server definitions" tab you should add a definition for Psalm:

  • Select Executable
  • Extension: php
  • Path: `` e.g. /usr/local/bin/php or C:\php\php.exe
    • this should be an absolute path, not just php
  • Args: vendor/bin/psalm-language-server (on Windows use vendor/vimeo/psalm/psalm-language-server, or for a 'global' install '%APPDATA%' + \Composer\vendor\vimeo\psalm\psalm-language-server, where the '%APPDATA%' environment variable is probably something like C:\Users\<homedir>\AppData\Roaming\)

In the "Timeouts" tab you can adjust the initialization timeout. This is important if you have a large project. You should set the "Init" value to the number of milliseconds you allow Psalm to scan your entire project and your project's dependencies. For opening a couple of projects that use large PHP frameworks, on a high-end business laptop, try 240000 milliseconds for Init.

Sublime Text

I use the excellent Sublime LSP plugin with the following config(Package Settings > LSP > Settings):

json
"clients": {
        "psalm": {
            "command": ["php", "vendor/bin/psalm-language-server"],
            "selector": "source.php | embedding.php",
            "enabled": true
        }
    }

Vim & Neovim

ALE

ALE has support for Psalm (since v2.3.0).

vim
let g:ale_linters = { 'php': ['php', 'psalm'] }

vim-lsp

I also got it working with vim-lsp

This is the config I used (for Vim):

vim
au User lsp_setup call lsp#register_server({
     \ 'name': 'psalm-language-server',
     \ 'cmd': {server_info->[expand('vendor/bin/psalm-language-server')]},
     \ 'allowlist': ['php'],
     \ })

coc.nvim

It also works with coc.nvim.

Add settings to coc-settings.json:

jsonc
"languageserver": {
    "psalmls": {
      "command": "vendor/bin/psalm-language-server",
      "filetypes": ["php"],
      "rootPatterns": ["psalm.xml", "psalm.xml.dist"],
      "requireRootPattern": true
    }
  }

VS Code

Get the Psalm plugin here (Requires VS Code 1.26+):

Running the server in a docker container

Make sure you use --map-folder option. Using it without argument will map the server's CWD to the host's project root folder. You can also specify a custom mapping. For example:

bash
docker-compose exec php /usr/share/php/psalm/psalm-language-server \
   -r=/var/www/html \
   --map-folder=/var/www/html:$PWD

Security Analysis Annotations (`docs/security_analysis/annotations.md`)

Security analysis annotations

`@psalm-taint-source `

See Custom taint sources.

`@psalm-taint-sink `

See Custom taint sinks.

`@psalm-taint-escape `

See Escaping tainted output.

`@psalm-taint-unescape `

See Unescaping statements.

Taint type complements

Wherever these annotations take a <taint-type>, ~(<taint-type>|<taint-type>) (or ~<taint-type> for one) stands
for every taint but the ones listed: the built-in ones (user_secret and system_secret too), the custom ones,
including those other annotations only register later. The ones listed may be aliases, like input. Write it without
spaces.

php
<?php // --taint-analysis

/**
 * Taken from the request line, which can't hold a line break: any input but a header injection
 *
 * @psalm-taint-source ~(header|user_secret|system_secret)
 */
function getRequestPath(): string {}

The taints the native types of a value can't hold are still removed from it: a string returned by
getRequestPath() above can't be a NoSQL query.

`@psalm-taint-specialize`

See Specializing taints in functions and Specializing taints in classes.

`@psalm-flow [proxy ] ( , [ , ] ) [ -> return ]`

See Taint Flow.

Each <arg> is a parameter, like $value. With -> return, an <arg> can also be:

  • $this: the object the method is called on
  • a parameter followed by (), like $compute(): what the callables passed to that parameter return

`@psalm-flow ( , [ , ] ) -> ::$[]` and `@psalm-flow ::$[] -> return`

See Keyed stores


Security Analysis Avoiding False Negatives (`docs/security_analysis/avoiding_false_negatives.md`)

Avoiding false-negatives

Unescaping statements

Post-processing previously escaped/encoded statements can cause insecure scenarios.
@psalm-taint-unescape <taint-type> allows to declare those components insecure explicitly.

php
<?php

/**
 * @psalm-taint-unescape html
 */
function decode(string $str): string
{
    return str_replace(
        ['&lt;', '&gt;', '&quot;', '&apos;'],
        ['<', '>', '"', '"'],
        $str
    );
}

$safe = htmlspecialchars($_GET['text']);
echo decode($safe);

Security Analysis Avoiding False Positives (`docs/security_analysis/avoiding_false_positives.md`)

Avoiding false-positives

When you run Psalm's taint analysis for the first time you may see a bunch of false-positives.

Nobody likes false-positives!

There are a number of ways you can prevent them:

Escaping tainted input

Some operations remove taints from data – for example, wrapping $_GET['name'] in an htmlentities call prevents cross-site-scripting attacks in that $_GET call.

Psalm allows you to remove taints via a @psalm-taint-escape <taint-type> annotation:

php
<?php

function echoVar(string $str) : void {
    /**
     * @psalm-taint-escape html
     */
    $str = str_replace(['<', '>'], '', $str);
    echo $str;
}

echoVar($_GET["text"]);

Types

A value can only carry the taints its type can hold: a number or a boolean can't hold HTML, SQL or a path, a string can't be a NoSQL query document (but what json_decode(), unserialize(), parse_str() and the other decoders build out of it can), and a literal string (a value the code wrote, or one Psalm knows is among such values) can't hold anything the client sent. An array holds what its keys and values can: list holds no HTML, while the string keys of array<string, int> can hold anything. A union holds what any of its types can hold, and null holds nothing.

Psalm removes the taints a value can't carry where it is cast, passed to a function, returned, or assigned to a variable or a property, using the type it infers for the value, and where PHP guarantees a type:

  • casts: (int) $_GET['id'] is only left with the taints a number can carry;
  • the native type of a parameter, inside the function;
  • the native return type of a function or method, for what it returns, taint sources included: a method annotated @psalm-taint-source input with a string return type never returns a NoSQL query.
php
<?php

/** @psalm-taint-source input */
function getParam(string $name): string {
    return (string) ($_GET[$name] ?? '');
}

function getId(string $name): int {
    return (int) getParam($name);
}

$collection->find(['name' => getParam('name')]); // a string can't inject query operators
echo '<b>' . getId('id') . '</b>'; // an int can't carry HTML

$direction = getParam('direction');
if ($direction === 'asc' || $direction === 'desc') {
    echo $direction; // 'asc'|'desc' is a literal
}

A function that validates a value against data the code trusts can tell Psalm about it with @psalm-assert-if-true literal-string: once validated, the value can't hold anything the client chose.

php
<?php

/** @psalm-assert-if-true literal-string $city */
function isKnownCity(string $city): bool {
    return in_array($city, getCityKeysFromDatabase(), true);
}

$city = getParam('city');
if (isKnownCity($city)) {
    $organization->city = $city; // stored without any taint
}

Conditionally escaping tainted input

A slightly modified version of the previous example is using a condition to determine whether the return value
is considered secure. Only in case function argument $escape is true, the corresponding annotation
@psalm-taint-escape is applied for taint types html and has_quotes.

php
<?php
/**
 * @param string $str
 * @param bool $escape
 * @psalm-taint-escape ($escape is true ? 'html'|'has_quotes' : null)
 */
function processVar(string $str, bool $escape = true) : string {
    if ($escape) {
      $str = htmlspecialchars($str, ENT_QUOTES);
    }
    return $str;
}

echo processVar($_GET['text'], false); // detects tainted HTML
echo processVar($_GET['text'], true); // considered secure

Sanitizing HTML user input

Whenever possible, applications should be designed to accept & store user input as discrete text fields, rather than blocks of HTML. This allows user input to be fully escaped via htmlspecialchars or htmlentities. In cases where HTML user input is required (e.g. rich text editors like TinyMCE), a library designed specifically to filter out risky HTML is highly recommended. For example, HTML Purifier could be used as follows:

php
<?php

/**
 * @psalm-taint-escape html
 * @psalm-taint-escape has_quotes
 */
function sanitizeHTML($html){
    $purifier = new HTMLPurifier();
    return $purifier->purify($html);
}

Specializing taints in functions

For functions, methods and classes you can use the @psalm-taint-specialize annotation.

php
<?php

function takesInput(string $s) : string {
    error_log("Got input");
    return $s;
}

echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // Psalm detects tainted HTML here

Adding a @psalm-taint-specialize annotation solves the problem, by telling Psalm that each invocation of the function should be treated separately.

php
<?php

/**
 * @psalm-taint-specialize
 */
function takesInput(string $s) : string {
    error_log("Got input");
    return $s;
}

echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // No error

A specialized function or method will still track tainted input:

php
<?php

/**
 * @psalm-taint-specialize
 */
function takesInput(string $s) : string {
    return $s;
}

echo takesInput($_GET["name"]); // Psalm detects tainted input
echo takesInput("hello"); // No error

Here we’re telling Psalm that a function’s taintedness is wholly dependent on the input to the function.

If you're familiar with immutability in Psalm then this general idea should be familiar, since a pure function is one where the output is wholly dependent on its input. Unsurprisingly, all functions marked @psalm-pure also specialize the taintedness of their output based on input:

php
<?php

/**
 * @psalm-pure
 */
function takesInput(string $s) : string {
    return $s;
}

echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // No error

Since each invocation is treated separately, a sink the input reaches through a specialized function – in its body or in a function it calls – is reported for every call that passes tainted input to it:

php
<?php

/**
 * @psalm-taint-specialize
 */
function run(string $command) : void {
    exec($command); // Reported twice: once for each call below
}

run($_GET["first"]);
run($_GET["second"]);

Psalm also infers the purity of functions without a purity annotation, so an unannotated function is specialized too when it turns out to be pure, as would be the first takesInput above without the error_log call. Since a method could be overridden by an impure one, this only applies to methods that cannot be overridden: private or final methods, and methods of final classes.

Specializing taints in classes

Just as taints can be specialized in function calls, tainted properties can also be specialized to a given class.

php
<?php

class User {
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

/**
 * @psalm-taint-specialize
 */
function echoUserName(User $user) {
    echo $user->name; // Error, detected tainted input
}

$user1 = new User("Keith");
$user2 = new User($_GET["name"]);

echoUserName($user1);

Adding @psalm-taint-specialize to the class fixes the issue.

php
<?php

/**
 * @psalm-taint-specialize
 */
class User {
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

/**
 * @psalm-taint-specialize
 */
function echoUserName(User $user) {
    echo $user->name; // No error
}

$user1 = new User("Keith");
$user2 = new User($_GET["name"]);

echoUserName($user1);

Each instance then holds its own taints, which only follow the variables the instance is assigned to. So Psalm makes sure the instance doesn't change once constructed, as the change wouldn't reach the other variables holding the same instance: its properties are readonly, and its methods can't write them. Nor can its methods write other objects, static properties or global variables, from which another instance could read back what this one wrote. To get a changed instance, create a new one, for instance by cloning it:

php
<?php

/**
 * @psalm-taint-specialize
 */
class User {
    public function __construct(public string $name) {}

    public function withName(string $name): static {
        $new = clone $this;
        $new->name = $name;
        return $new;
    }
}

The classes that extend a class with @psalm-taint-specialize are specialized too, and held to the same rules: Psalm reports an ImmutableDependency issue for those that don't have the annotation themselves.

And, because it’s form of purity enforcement, @psalm-immutable can also be used:

php
<?php

/**
 * @psalm-immutable
 */
class User {
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

/**
 * @psalm-taint-specialize
 */
function echoUserName(User $user) {
    echo $user->name; // No error
}

$user1 = new User("Keith");
$user2 = new User($_GET["name"]);

echoUserName($user1);

Avoiding files in taint paths

You can also tell Psalm that you’re not interested in any taint paths that flow through certain files or directories by specifying them in your Psalm config:

xml
<taintAnalysis>
        <ignoreFiles>
            <directory name="tests"/>
        </ignoreFiles>
    </taintAnalysis>

Security Analysis Custom Taint Sinks (`docs/security_analysis/custom_taint_sinks.md`)

Custom Taint Sinks

The @psalm-taint-sink <taint-type> annotation allows you to define a taint sink.

Any tainted value matching the given taint type will be reported as an error by Psalm.

Example

Here the PDOWrapper class has an exec method that should not receive tainted SQL, so we can prevent its insertion:

php
<?php

class PDOWrapper {
    /**
     * @psalm-taint-sink sql $sql
     */
    public function exec(string $sql) : void {}
}

Values of an array

Append [*] to the parameter name to make only the values of the array given to the parameter a sink, not its keys:

php
<?php

final class TemplateLoader {
    /**
     * @param array<string, string> $templates template names => template sources
     * @psalm-taint-sink eval $templates[*]
     */
    public function __construct(private array $templates) {}
}

Here a tainted template name is not reported, a tainted template source is.


Security Analysis Custom Taint Sources (`docs/security_analysis/custom_taint_sources.md`)

Custom Taint Sources

You can define your own taint sources with an annotation or a plugin.

Taint source annotation

You can use the annotation @psalm-taint-source <taint-type> to indicate a function or method that provides user input.

In the below example the input taint type is specified as a standin for input taints as defined in Psalm\Type\TaintKind.

php
<?php
/**
 * @psalm-taint-source input
 */
function getQueryParam(string $name) : string {}

Custom taints declared in the configuration

Custom taints can be declared in psalm.xml, so that the annotations can name them without a plugin registering
them:

xml
<taintAnalysis>
  <taints>
    <taint name="file_upload">
      <alias name="input" />
    </taint>
    <taint name="session_id" />
  </taints>
</taintAnalysis>

A taint added to the alias input with <alias name="input" /> is a kind of user input like the built-in ones:
$_GET and the other built-in sources of user input give it, @psalm-taint-source input, @psalm-taint-sink input
and @psalm-taint-escape input name it, and a value of a type that can't hold user input (a literal string, a number,
a boolean, null) can't carry it. Plugins get these taints with Codebase::getInputTaints(). Each of the other aliases,
tainted and input_except_sleep, holds the custom taints added to it with its own <alias>, and only those. A
taint without an alias, like session_id above, only comes from the annotations naming it
(@psalm-taint-source session_id), and only reaches the sinks naming it.

A taint's name is a word (letters, digits, _ and -) other than those of the built-in taints.

Custom taint plugin

For example this plugin treats all variables named $bad_data as taint sources.

text
/* Detailed source-code truncated for AI context efficiency. */

Security Analysis Taint Flow (`docs/security_analysis/taint_flow.md`)

Taint Flow

Optimized Taint Flow

When dealing with frameworks, keeping track of the data flow might involve different layers
and even other 3rd party components. Using the @psalm-flow annotation allows PsalmPHP to
take a shortcut and to make a tainted data flow more explicit.

Proxy hint

php
<?php // --taint-analysis
/**
 * @psalm-flow proxy exec($value)
 */
function process(string $value): void {}

process($_GET['malicious'] ?? '');

The example above states, that the function process($value) is a proxy of the native PHP
function exec($value) - which is potentially vulnerable to code execution (TaintedShell).

Examples

  • @psalm-flow proxy exec($value) referencing the global/scoped function exec
  • @psalm-flow proxy MyClass::mySinkMethod($value) referencing a function/method of the class MyClass

Return value hint

php
<?php // --taint-analysis
/**
 * @psalm-flow ($value, $items) -> return
 */
function inputOutputHandler(string $value, string ...$items): string
{
    // lots of complicated magic
}

echo inputOutputHandler('first', 'second', $_GET['malicious'] ?? '');

The example above states, that the function parameters $value and $items are reflected
again in the return value. Thus, in case any of the input parameters to the function
inputOutputHandler is tainted, then the resulting return value is as well. In this
example TaintedHtml would be detected due to using echo.

Object to return value hint

php
<?php // --taint-analysis
interface Message
{
    /**
     * @psalm-taint-specialize
     * @psalm-flow ($this) -> return
     */
    public function getBody(): string;
}

/**
 * @psalm-taint-source input
 */
function fetch(): Message {
    // a request to another server
}

echo fetch()->getBody();

$this in a return value hint states that what a method returns holds what the object it is called on holds: here,
the bodies of the messages fetch() returns are tainted, while those of the messages the code builds itself only hold
what they were given. This is useful for methods whose bodies Psalm doesn't analyze, such as those of interfaces,
stubs and vendor code. With @psalm-taint-specialize, each call returns what its own object holds; without it, what
any object the method is called on holds flows into what every call returns.

Callable result to return value hint

php
<?php // --taint-analysis
interface Cache
{
    /**
     * @template T
     * @param callable(): T $compute
     * @return T
     * @psalm-taint-specialize
     * @psalm-flow ($compute()) -> return
     */
    public function get(string $key, callable $compute): mixed;
}

function show(Cache $cache): void
{
    echo $cache->get('key', fn(): string => $_GET['malicious'] ?? '');
}

$compute() in a return value hint states that what the function returns holds what the callables given to $compute
return: the closure's body, or the function named. Here TaintedHtml is detected. As for $this, each call returns
what its own callables return only with @psalm-taint-specialize.

Keyed stores

Caches, key-value stores and similar services keep what they are given out of the analyzed code, under a key. Their
methods can say so, and what is stored under a key then flows into what the reads of that key return, wherever the
code reading it is:

php
<?php // --taint-analysis
final class Cache
{
    /**
     * @psalm-flow ($value) -> Cache::$data[$key]
     */
    public static function set(string $key, string $value): void {}

    /**
     * @psalm-flow Cache::$data[$key] -> return
     */
    public static function get(string $key): string {}
}

function remember(string $id): void {
    Cache::set('comment_' . $id, $_GET['comment'] ?? '');
}

echo Cache::get('comment_1'); // TaintedHtml
echo Cache::get('title_1');   // no issue
  • ($value, $other) -> Store::$data[$key] stores the arguments given to $value and $other under the key given to
    $key, and Store::$data[$key] -> return makes a call return what is stored under its key. ($compute()) stores
    what the callables given to $compute return, as in a return value hint:
    ($compute()) -> Cache::$data[$key] for a cache computing what it stores when the key is missing.
  • A store is named like a static property, Class::$name. The class and the property need not exist; self and
    static name the class declaring the method. The methods of other classes (a wrapper, an extension) share a store
    by naming the same one.
  • [*] is any key: ($values) -> Store::$data[*] for a method storing an array of keys and values, and
    Store::$data[*] -> return for one returning what is stored under any key. [$keys[*]] is each key of a list, as
    for mGet(array $keys). In nested stores, like Store::$data[$namespace][$key], only the first key tells the values
    apart.
  • Keys are told apart by what the analysis knows of them: the exact keys of literal strings and class names, else how
    the key starts: the start of a concatenation, of an interpolated string, of a sprintf() format up to its first
    conversion other than %s, of an implode() of a list with a literal separator, __CLASS__, or what the variable
    (also one a closure uses), the property of $this, the method of the class or the function giving the key starts
    with. A property set only by its default and its constructor starts like what the constructor is given by the new
    of the classes analyzed. The keys of a list start like all its items: those of a literal list, those assigned to it
    ($keys[] = 'lock_' . $id;), and those of a list a method or a function builds this way and returns. A key the
    analysis knows nothing of can be any key.
  • Psalm's stubs declare the keyed store of phpredis, Redis::$data, and that of APCu, APCu::$data. Plugins can
    connect their own nodes to a store with Codebase::addTaintStoreWrite() (paths from a node into the store under
    the names) and Codebase::addTaintStoreRead() (paths from the store to a node), with the names
    Codebase::getTaintStoreNames() tells for an expression, for stores whose keys an annotation can't express (a
    table named in an SQL query).

Combined proxy & return value hint

php
<?php // --taint-analysis
/**
 * @psalm-flow proxy exec($value)
 * @psalm-flow ($value, $items) -> return
 */
function handleInput(string $value, string ...$items): string
{
    // lots of complicated magic
}

echo handleInput($_GET['malicious'] ?? '');

The example above combines both previous examples and shows, that the @psalm-flow annotation
can be used multiple times. Here, it would lead to detecting both TaintedHtml and TaintedShell.


Annotating Code Type Syntax Array Types (`docs/annotating_code/type_syntax/array_types.md`)

Array types

In PHP, the array type is commonly used to represent three different data structures:

List:

php
<?php
$a = [1, 2, 3, 4, 5];

Associative array:

php
<?php
$a = [0 => 'hello', 5 => 'goodbye'];
$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'];

Makeshift Structs:

php
<?php
$a = ['name' => 'Psalm', 'type' => 'tool'];

PHP treats all these arrays the same, essentially (though there are some optimisations under the hood for the first case).

Psalm has a few different ways to represent arrays in its type system:

Generic arrays

Psalm uses a syntax borrowed from Java that allows you to denote the types of both keys and values:

php
/** @return array<TKey, TValue> */

You can also specify that an array is non-empty with the special type non-empty-array<TKey, TValue>.

PHPDoc syntax

PHPDoc allows you to specify the type of values a generic array holds with the annotation:

php
/** @return ValueType[] */

In Psalm this annotation is equivalent to @psalm-return array<array-key, ValueType>.

Generic arrays encompass both associative arrays and lists.

Lists

(Psalm 3.6+)

Psalm supports a list type that represents continuous, integer-indexed arrays like ["red", "yellow", "blue"].

A frequent way to create a list is with the $arr[] = notation.

These arrays will return true to array_is_list($arr)(PHP 8.1+) and represent a large percentage of all array usage in PHP applications.

A list type is of the form list<SomeType>, where SomeType is any permitted union type supported by Psalm.

  • list is a subtype of array
  • list<Foo> is a subtype of array.

List types show their value in a few ways:

php
<?php
/**
 * @param array<int, string> $arr
 */
function takesArray(array $arr) : void {
  if ($arr) {
     // this index may not be set
    echo $arr[0];
  }
}

/**
 * @psalm-param list<string> $arr
 */
function takesList(array $arr) : void {
  if ($arr) {
    // list indexes always start from zero,
    // so a non-empty list will have an element here
    echo $arr[0];
  }
}

takesArray(["hello"]); // this is fine
takesArray([1 => "hello"]); // would trigger bug, without warning

takesList(["hello"]); // this is fine
takesList([1 => "hello"]); // triggers warning in Psalm

Array shapes

Psalm supports a special format for arrays where the key offsets are known: array shapes, also known as "object-like arrays".

Given an array

php
<?php
["hello", "world", "foo" => new stdClass, 28 => false];

Psalm will type it internally as:

text
array{0: string, 1: string, foo: stdClass, 28: false}

You can specify types in that format yourself, e.g.

php
/** @return array{foo: string, bar: int} */

Optional keys can be denoted by a trailing ?, e.g.:

php
/** @return array{optional?: string, bar: int} */

You can use "one-line" comments (similar to PHP), e.g:

php
/** @return array { // Array with comments.
 *     // Comments can be placed on their own line. 
 *     foo: string, // An array key description.
 *     bar: array {, // Another array key description.
 *         'foo//bar': string, // Array key with "//" in it's name.
 *     },
 * }
 */

Tip: if you find yourself copying the same complex array shape over and over again to avoid InvalidArgument issues, try using type aliases, instead.

Validating array shapes

Use Valinor in strict mode to easily assert array shapes at runtime using Psalm array shape syntax (instead of manually asserting keys with isset):

php
try {
  $array = (new \CuyZ\Valinor\MapperBuilder())
      ->mapper()
      ->map(
          'array{a: string, b: int}',
          json_decode(file_get_contents('https://.../'), true)
      );

  /** @psalm-trace $array */; // array{a: string, b: int}

  echo $array['a'];
  echo $array['b'];
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
  // Do something…
}

Valinor provides both runtime and static Psalm assertions with full Psalm syntax support and many other features, check out the Valinor documentation for more info!

List shapes

Starting in Psalm 5, Psalm also supports a special format for list arrays where the key offsets are known.

Given a list array

php
<?php
["hello", "world", new stdClass, false];

Psalm will type it internally as:

text
list{string, string, stdClass, false}

You can specify types in that format yourself, e.g.

php
/** @return list{string, int} */
/** @return list{0: string, 1: int} */

Optional keys can be denoted by a specifying keys for all elements and specifying a trailing ? for optional keys, e.g.:

php
/** @return list{0: string, 1?: int} */

List shapes are essentially n-tuples from a type theory perspective.

Unsealed array and list shapes

Starting from Psalm v5, array shapes and list shapes can be marked as open by adding ... as their last element.

Here we have a function handleOptions that takes an array of options. The type tells us it has a single known key with type string, and potentially many other keys of unknown types.

php
/** @param array{verbose: string, ...} $options */
function handleOptions(array $options): float {
    if ($options['verbose']) {
        var_dump($options);
    }
}

$options = get_opt(/* some code */);
$options['verbose'] = isset($options['verbose']);
handleOptions($options);

... is a shorthand for ...<array-key, mixed> you can use other array generic types to provide more information about the open shape.

php
// This is an open array
/** @param array{someKey: string, ...} */ 
// Which is the same as
/** @param array{someKey: string, ...<array-key, mixed>} */ 
// But it can be further locked down with a shape ...<TKey, TValue>
/** @return array{someKey: string, ...<int, bool>} */

Callable arrays

An array holding a callable, like PHP's native call_user_func() and friends supports it:

php
<?php

$callable = ['myClass', 'aMethod'];
$callable = [$object, 'aMethod'];

non-empty-array

An array which is not allowed to be empty.
Generic syntax is also supported: non-empty-array<string, int>.


Annotating Code Type Syntax Atomic Types (`docs/annotating_code/type_syntax/atomic_types.md`)

Atomic types

Atomic types are the basic building block of all type information used in Psalm. Multiple atomic types can be combined, either with union types or intersection types. Psalm allows many different sorts of atomic types to be expressed in docblock syntax:

Note: you can view detailed documentation and usage examples for all atomic types by clicking on each type in the following list.


Annotating Code Type Syntax Callable Types (`docs/annotating_code/type_syntax/callable_types.md`)

Callable types

Psalm supports a special format for callables of the form. It can also be used for annotating Closure.

text
callable(Type1, OptionalType2=, SpreadType3...):ReturnType

Adding = after the type implies it is optional, and suffixing with ... implies the use of the spread operator.

Using this annotation you can specify that a given function return a Closure e.g.

php
<?php
/**
 * @return Closure(bool):int
 */
function delayedAdd(int $x, int $y) : Closure {
  return function(bool $debug) use ($x, $y) {
    if ($debug) echo "got here" . PHP_EOL;
    return $x + $y;
  };
}

$adder = delayedAdd(3, 4);
echo $adder(true);

Pure callables

A callable type carries the capabilities (see the purity model)
the callable may use, given in square brackets after the keyword:

  • Closure[pure](int): int / callable[pure](int): int - a pure callable, also written pure-Closure(int): int / pure-callable(int): int
  • Closure[write-props|io](): void - any combination of capabilities
  • Closure[P](): void - a purity template declared with @psalm-purity-template
  • Closure[_](): void - in a parameter's type only, at any depth: the function inherits its purity from that parameter (see purity templates)
  • Closure(): void / callable(): void - an impure callable (the default), also written impure-Closure(): void / impure-callable(): void

The parameter list may be left out: Closure[pure] is any pure closure.

A callable needing fewer capabilities fits where more are allowed: a pure-Closure can be passed
for a Closure[io] parameter, but not the other way round. A closure's capabilities are inferred
from its body, so its type carries what it actually does.

This can be useful when the callable is used in a function with few capabilities (see @psalm-capabilities), for example:

php
<?php
/** @psalm-immutable */
class intList {
    /** @param list<int> $items */
    public function __construct(private array $items) {}
    
    /**
     * @param pure-callable(int, int): int $callback
     * @psalm-capabilities read-props
     */
    public function walk(callable $callback): int {
        return array_reduce($this->items, $callback, 0);
    }
}

$list = new intList([1,2,3]);

// This is ok, as the callable is pure
echo $list->walk(fn (int $c, int $v): int => $c + $v);

// This will cause an InvalidArgument error, as the closure calls an impure function
echo $list->walk(fn (int $c, int $v): int => $c + random_int(1, $v));

Annotating Code Type Syntax Conditional Types (`docs/annotating_code/type_syntax/conditional_types.md`)

Conditional types

Psalm supports the equivalent of TypeScript’s conditional types.

Conditional types have the form:

(<template param> is ? : )

All conditional types must be wrapped inside brackets e.g. (...)

Conditional types are dependent on template parameters, so you can only use them in a function where template parameters are defined.

Example application

Let's suppose we want to make a userland implementation of PHP's numeric addition (but please never do this). You could type this with a conditional return type:

php
<?php

/**
 * @template T of int|float
 * @param T $a
 * @param T $b
 * @return int|float
 * @psalm-return (T is int ? int : float)
 */
function add($a, $b) {
    return $a + $b;
}

When figuring out the result of add($x, $y) Psalm tries to infer the value T for that particular call. When calling add(1, 2), T can be trivially inferred as an int. Then Psalm takes the provided conditional return type

(T is int ? int : float)

and substitutes in the known value of T, int, so that expression becomes

(int is int ? int : float)

which simplifies to (true ? int : float), which simplifies to int.

Calling add(1, 2.1) means T would instead be inferred as int|float, which means the expression (T is int ? int : float) would instead have the substitution

(int|float is int ? int : float)

The union int|float is clearly not an int, so the expression is simplified to (false ? int : float), which simplifies to float.

Nested conditionals

You can also nest conditionals just as you could ternary expressions:

php
<?php

class A {
    const TYPE_STRING = 0;
    const TYPE_INT = 1;

    /**
     * @template T of int
     * @param T $i
     * @psalm-return (
     *     T is self::TYPE_STRING
     *     ? string
     *     : (T is self::TYPE_INT ? int : bool)
     * )
     */
    public static function getDifferentType(int $i) {
        if ($i === self::TYPE_STRING) {
            return "hello";
        }

        if ($i === self::TYPE_INT) {
            return 5;
        }

        return true;
    }
}

Annotating Code Type Syntax Intersection Types (`docs/annotating_code/type_syntax/intersection_types.md`)

Intersection types

An annotation of the form Type1&Type2&Type3 is an Intersection Type. Any value must satisfy Type1, Type2 and Type3 simultaneously. Type1, Type2 and Type3 are all atomic types.

For example, after this statement in a PHPUnit test:

php
<?php

$hare = $this->createMock(Hare::class);

$hare will be an instance of a class that extends Hare, and implements \PHPUnit\Framework\MockObject\MockObject. So
$hare is typed as Hare&\PHPUnit\Framework\MockObject\MockObject. You can use this syntax whenever a value is
required to implement multiple interfaces.

Another use case is being able to merge object-like arrays:

php
/**
 * @psalm-type A=array{a: int}
 * @psalm-type B=array{b: int}
 *
 * @param A $a
 * @param B $b
 *
 * @return A&B
 */
function foo($a, $b) {
    return $a + $b;
}

The returned type will contain the properties of both A and B. In other words, it will be {a: int, b: int}.

Intersections are only valid for lists of only object types and lists of only object-like arrays.

You can also intersect a class with an object shape to describe properties the class doesn't declare. This helps with
classes that read properties through the magic __get method, where Psalm can't know which properties exist:

php
<?php

class Row
{
    /** @param array<string, mixed> $columns */
    public function __construct(private array $columns) {}

    public function __get(string $name): mixed
    {
        return $this->columns[$name] ?? null;
    }
}

/** @param Row&object{email: string} $user */
function sendWelcomeEmail(Row $user): void
{
    mail($user->email, 'Welcome!', 'Thanks for signing up.');
}

With a plain Row, $user->email would be mixed. With Row&object{email: string} it is a string.

Psalm takes a property from the shape only when the class doesn't declare it. The shape takes priority over a property
that comes from a @mixin. A final class can only have undeclared properties through __get, so for a final class
without __get (its own or inherited) Psalm still reports UndefinedPropertyFetch.


Annotating Code Type Syntax Object Types (`docs/annotating_code/type_syntax/object_types.md`)

Object types

Unnamed objects

object are examples of unnamed object types. This type is also a valid type in PHP.

Named objects

stdClass, Foo, Bar\Baz etc. are examples of named object types. These types are also valid types in PHP.

Object properties

Psalm supports specifying the properties of an object and their expected types, e.g.:

php
/** @param object{foo: string} $obj */
function takesObject(object $obj) : string {
    return $obj->foo;
}

takesObject((object) ["foo" => "hello"]);

Optional properties can be denoted by a trailing ?, e.g.:

php
/** @param object{optional?: string} */

Generic object types

Psalm supports using generic object types like ArrayObject. Any generic object should be typehinted with appropriate @template tags.

Generators

Generator types support up to four parameters, e.g. Generator:

  1. TKey, the type of the yield key - default: mixed
  2. TValue, the type of the yield value - default: mixed
  3. TSend, the type of the send() method's parameter - default: mixed
  4. TReturn, the return type of the getReturn() method - default: mixed

Generator is a shorthand for Generator<mixed, int, mixed, mixed>.

Generator is a shorthand for Generator.


Annotating Code Type Syntax Other Types (`docs/annotating_code/type_syntax/other_types.md`)

Other types

  • iterable - represents the iterable pseudo-type. Like arrays, iterables can have type parameters e.g. iterable<string, Foo>, and a purity in brackets, what iterating over them may do: iterable[pure]<string, Foo> accepts arrays and Traversable[pure]<string, Foo> (see Iterators and generators).
  • void - can be used in a return type when a function does not return a value.
  • resource represents a PHP resource.
  • closed-resource represents a PHP resource that was closed (using fclose or another closing function).

Annotating Code Type Syntax Scalar Types (`docs/annotating_code/type_syntax/scalar_types.md`)

Scalar types

scalar

int, bool, float, string are examples of scalar types. Scalar types represent scalar values in PHP. These types are also valid types in PHP 7.
The type scalar is the supertype of all scalar types.

int-range

Integer ranges indicate an integer within a range, specified using generic syntax: int<x, y>.
x and y must be integer numbers.
x can also be min to indicate PHP_INT_MIN, and y can be max to indicate PHP_INT_MAX.

Examples:

  • int<-1, 3>
  • int<min, 0>
  • int<1, max> (equivalent to positive-int)
  • int<0, max> (equivalent to non-negative-int)
  • int<min, -1> (equivalent to negative-int)
  • int<min, 0> (equivalent to non-positive-int)
  • int<min, max> (equivalent to int)

int-mask<1, 2, 4>

Represents the type that is the result of a bitmask combination of its parameters.
int-mask<1, 2, 4> corresponds to 0|1|2|3|4|5|6|7.

int-mask-of<MyClass::CLASS_CONSTANT_*>

Represents the type that is the result of a bitmask combination of its parameters.
This is the same concept as int-mask but this type is used with a reference to constants in code: int-mask-of<MyClass::CLASS_CONSTANT_*> will correspond to 0|1|2|3|4|5|6|7 if there are three constants called CLASS_CONSTANT_{A,B,C} with values 1, 2 and 4.

Comparing masks

A value of an int-mask or int-mask-of type is a set of bits, and Psalm remembers it through &, | and ^: $mask & B is a narrowed mask of the bits of B. A mask may only be compared by value with 0 (no bit set) or with all of its bits: any other comparison only matches one exact set of bits, where testing some bits with ($mask & B) === B or ($mask & B) !== 0 is usually meant, and is reported as IntMaskComparison.

array-key

array-key is the supertype (but not a union) of int and string.

numeric

numeric is a supertype of int or float or numeric-string.

class-string, interface-string

Psalm supports a special meta-type for MyClass::class constants, class-string, which can be used everywhere string can.

For example, given a function with a string parameter $class_name, you can use the annotation @param class-string $class_name to tell Psalm make sure that the function is always called with a ::class constant in that position:

php
<?php
class A {}

/**
 * @param class-string $s
 */
function takesClassName(string $s) : void {}

takesClassName("A"); would trigger a TypeCoercion issue, whereas takesClassName(A::class) is fine.

You can also parameterize class-string with an object name e.g. class-string<Foo>. This tells Psalm that any matching type must either be a class string of Foo or one of its descendants.

trait-string

Psalm also supports a trait-string annotation denoting a trait that exists.

enum-string

Psalm also supports a enum-string annotation denote an enum that exists.

callable-string

callable-string denotes a string value that has passed an is_callable check.

numeric-string

numeric-string denotes a string value that has passed an is_numeric check.

literal-string

literal-string denotes a string value that is entirely composed of strings in your application.

Examples:

  • "hello " . "world"
  • "hello " . Person::DEFAULT_NAME
  • implode(', ', ["one", "two"])
  • implode(', ', [1, 2, 3])
  • "hello " . <another literal-string>

Strings that don't pass this type check:

  • file_get_contents("foo.txt")
  • $_GET["foo"]
  • "hello " . $_GET["foo"]

literal-int

literal-int denotes an int value that is entirely composed of literal integers in your application.

Examples:

  • 12
  • 12+42

Integers that don't pass this type check:

  • (int) file_get_contents("foo.txt")
  • (int) $_GET["foo"]
  • ((int)$_GET["foo"]) + 2

lowercase-string, non-empty-string, non-empty-lowercase-string

A non empty string, lowercased or both at once.

empty here is defined as all strings except the empty string ''. Another type non-falsy-string is effectively a subtype of non-empty-string, and also precludes the string value '0'.


Annotating Code Type Syntax Top Bottom Types (`docs/annotating_code/type_syntax/top_bottom_types.md`)

Top types, bottom types

`mixed`

This is the top type in PHP's type system, and represents a lack of type information. Psalm warns about mixed types when the reportMixedIssues flag is turned on, or when you're on level 1.

`never`

It can be aliased to no-return or never-return in docblocks. Note: it replaced the old empty type that used to exist in Psalm

This is the bottom type in PHP's type system. It's used to describe a type that has no possible value. It can happen in multiple cases:

  • the actual never type from PHP 8.1 (can be used in docblocks for older versions). This type can be used as a return type for functions that will never return, either because they always throw exceptions or always exit()
  • a union type that has been stripped of all its possible types. (For example, if a variable is string|int and we perform an is_bool() check in a condition, the type of the variable in the condition will be never as the condition will never be entered)
  • it can represent a placeholder for types yet to come β€” a good example is the type of the empty array [], which Psalm types as array<never, never>, the content of the array is void so it can accept any content
  • it can also happen in the same context as the line above for templates that have yet to be defined

Annotating Code Type Syntax Union Types (`docs/annotating_code/type_syntax/union_types.md`)

Union Types

An annotation of the form Type1|Type2|Type3 is a Union Type. Type1, Type2 and Type3 are all acceptable possible types of that union type.

Type1, Type2 and Type3 are each atomic types.

Union types can be generated in a number of different ways, for example in ternary expressions:

php
<?php
$rabbit = rand(0, 10) === 4 ? 'rabbit' : ['rabbit'];

$rabbit will be either a string or an array. We can represent that idea with Union Types – so $rabbit is typed as string|array. Union types represent all the possible types a given variable can have.

PHP builtin functions also have union-type returns - strpos can return false in some situations, int in others. We represent that union type with int|false.


Annotating Code Type Syntax Utility Types (`docs/annotating_code/type_syntax/utility_types.md`)

Utility types

Psalm supports some magical utility types that brings superpower to the PHP type system.

key-of<T>

(Psalm 5.0+)

The key-of utility returns the offset-type for any array type.

Some examples:

  • key-of<Foo\Bar::ARRAY_CONST> evaluates to offset-type of ARRAY_CONST (Psalm 3.3+)
  • key-of<list<mixed>> evaluates to int
  • key-of<array{a: mixed, b: mixed}|array{c: mixed}> evaluates to 'a'|'b'|'c'
  • key-of<string[]> evaluates to array-key
  • key-of<T> evaluates to the template param's offset-type (ensure @template T of array)

Notes on template usage

If you use key-of with a template param, you can fulfill the type check only with these allowed methods:

  • array_keys($t)
  • array_key_first($t)
  • array_key_last($t)

Currently array_key_exists($key, $t) does not infer that $key is of key-of<T>.

php
/**
 * @template T of array
 * @param T $array
 * @return list<key-of<T>>
 */
function getKeys($array) {
    return array_keys($array);
}

value-of<T>

(Psalm 5.0+)

The value-of utility returns the value-type for any array type.

Some examples:

  • value-of<Foo\Bar::ARRAY_CONST> evaluates to value-type of ARRAY_CONST (Psalm 3.3+)
  • value-of<list<float>> evaluates to float
  • value-of<array{a: bool, b: int}|array{c: string}> evaluates to bool|int|string
  • value-of<string[]> evaluates to string
  • value-of<T> evaluates to the template param's value-type (ensure @template T of array)

Use with enumerations

In addition to array-types, value-of can also be used to specify an int or string that contains one of the possible values of a BackedEnum:

  • value-of<Suit> evaluates to 'H'|'D'|'C'|'S' (see Backed enumerations)
  • value-of evaluates to 0|1

Notes on template usage

If you use value-of with a template param, you can fulfill the type check only with these allowed methods:

  • array_values
php
/**
 * @template T of array
 * @param T $array
 * @return value-of<T>[]
 */
function getValues($array) {
    return array_values($array);
}

Currently in_array($value, $t) does not infer that $value is of value-of<T>.

properties-of<T>

(Psalm 5.0+)

This collection of utility types construct a keyed-array type, with the names of non-static properties of a class as
keys, and their respective types as values. This can be useful if you need to convert objects into arrays.

php
class A {
  public string $foo = 'foo!';
  public int $bar = 42;

  /**
   * @return properties-of<self>
   */
  public function asArray(): array {
    return [
      'foo' => $this->foo,
      'bar' => $this->bar,
    ];
  }

  /**
   * @return list<key-of<properties-of<self>>>
   */
  public function attributeNames(): array {
    return ['foo', 'bar']
  }
}

Variants

Note that properties-of<T> will return all non-static properties. There are the following subtypes to pick only
properties with a certain visibility:

  • public-properties-of<T>
  • protected-properties-of<T>
  • private-properties-of<T>

Sealed array support

Use final classes if you want to properties-of and get_object_vars to return sealed arrays:

php
/**
 * @template T
 * @param T $object
 * @return properties-of<T>
 */
function asArray($object): array {
  return get_object_vars($object);
}


class A {
  public string $foo = 'foo!';
  public int $bar = 42;
}

final class B extends A {
  public float $baz = 2.1;
}

$a = asArray(new A);
/** @psalm-trace $a */; // array{foo: string, bar: int, ...}

$b = asArray(new B);
/** @psalm-trace $b */; // array{foo: string, bar: int, baz: float}

class-string-map<T of Foo, T>

Used to indicate an array where each value is equal an instance of the class string contained in the key:

php
<?php

/**
 * @psalm-consistent-constructor
 */
class Foo {}

/**
 * @psalm-consistent-constructor
 */
class Bar extends Foo {}

class A {
  /** @var class-string-map<T of Foo, T> */
  private static array $map = [];

  /**
   * @template U of Foo
   * @param class-string<U> $class
   * @return U
   */
  public static function get(string $class) : Foo {
    if (isset(self::$map[$class])) {
      return self::$map[$class];
    }

    self::$map[$class] = new $class();
    return self::$map[$class];
  }
}

$foo = A::get(Foo::class);
$bar = A::get(Bar::class);

/** @psalm-trace $foo */; // Foo
/** @psalm-trace $bar */; // Bar

If we had used an array<class-string<Foo>, Foo> instead of a class-string-map<T of Foo, T> in the above example, we would've gotten some false positive InvalidReturnStatement issues, caused by the lack of a type assertion inside the isset.
On the other hand, when using class-string-map, Psalm assumes that the value obtained by using a key class-string<T> is always equal to T.

Unbounded templates can also be used for unrelated classes:

php
<?php

/**
 * @psalm-consistent-constructor
 */
class Foo {}

/**
 * @psalm-consistent-constructor
 */
class Bar {}

/**
 * @psalm-consistent-constructor
 */
class Baz {}

class A {
  /** @var class-string-map<T, T> */
  private static array $map = [];

  /**
   * @template U
   * @param class-string<U> $class
   * @return U
   */
  public static function get(string $class) : object {
    if (isset(self::$map[$class])) {
      return self::$map[$class];
    }

    self::$map[$class] = new $class();
    return self::$map[$class];
  }
}

$foo = A::get(Foo::class);
$bar = A::get(Bar::class);
$baz = A::get(Baz::class);

/** @psalm-trace $foo */; // Foo
/** @psalm-trace $bar */; // Bar
/** @psalm-trace $baz */; // Baz

`T[K]`

Used to get the value corresponding to the specified key:

php
<?php

/**
 * @template T of array
 * @template TKey of string
 * @param T $arr
 * @param TKey $k
 * @return T[TKey]
 */
function a(array $arr, string $k): mixed {
  assert(isset($arr[$k]));
  return $arr[$k];
}

$a = a(['test' => 123], 'test');
/** @psalm-trace $a */; // 123

Type aliases

Psalm allows defining type aliases for complex types (like array shapes) which must be reused often:

php
/**
 * @psalm-type PhoneType = array{phone: string}
 */
class Phone {
    /**
     * @psalm-return PhoneType
     */
    public function toArray(): array {
        return ["phone" => "Nokia"];
    }
}

You can use the @psalm-import-type annotation to import a type defined with @psalm-type if it was defined somewhere else.

php
<?php
/**
 * @psalm-import-type PhoneType from Phone
 */
class User {
    /**
     * @psalm-return PhoneType
     */
    public function toArray(): array {
        return array_merge([], (new Phone())->toArray());
    }
}

You can also alias a type when you import it:

php
<?php
/**
 * @psalm-import-type PhoneType from Phone as MyPhoneTypeAlias
 */
class User {
    /**
     * @psalm-return MyPhoneTypeAlias
     */
    public function toArray(): array {
        return array_merge([], (new Phone())->toArray());
    }
}

Variable templates

Variable templates allow directly using variables instead of template types, for example instead of the following verbose example:

php
<?php

/**
 * @template TA of string
 * @template TB of string
 * @template TChoose of bool
 * @param TA $a
 * @param TB $b
 * @param TChoose $choose
 * @return (TChoose is true ? TA : TB)
 */
function pick(string $a, string $b, bool $choose): string {
  return $choose ? $a : $b;
}

$a = pick('a', 'b', true);
/** @psalm-trace $a */; // 'a'

$a = pick('a', 'b', false);
/** @psalm-trace $a */; // 'b'

We can instead use variable templates like so:

php
<?php

/**
 * @return ($choose is true ? $a : $b)
 */
function pick(string $a, string $b, bool $choose): string {
  return $choose ? $a : $b;
}

$a = pick('a', 'b', true);
/** @psalm-trace $a */; // 'a'

$a = pick('a', 'b', false);
/** @psalm-trace $a */; // 'b'

Annotating Code Type Syntax Value Types (`docs/annotating_code/type_syntax/value_types.md`)

Value types

Psalm also allows you to specify values in types.

null

This is the null value, destroyer of worlds. Use it sparingly. Psalm supports you writing ?Foo to mean null|Foo.

true, false

Use of true and false is also PHPDoc-compatible

"some_string", 4, 3.14

Psalm also allows you specify literal values in types, e.g. @return "good"|"bad"

Regular class constants

Psalm allows you to include class constants in types, e.g. @return Foo::GOOD|Foo::BAD. You can also specify explicit class strings e.g. Foo::class|Bar::class

If you want to specify that a parameter should only take class strings that are, or extend, a given class, you can use the annotation @param class-string<Foo> $foo_class. If you only want the param to accept that exact class string, you can use the annotation Foo::class:

php
<?php
class A {}
class AChild extends A {}
class B {}
class BChild extends B {}

/**
 * @param class-string<A>|class-string<B> $s
 */
function foo(string $s) : void {}

/**
 * @param A::class|B::class $s
 */
function bar(string $s) : void {}

foo(A::class); // works
foo(AChild::class); // works
foo(B::class); // works
foo(BChild::class); // works
bar(A::class); // works
bar(AChild::class); // fails
bar(B::class); // works
bar(BChild::class); // fails

Running Psalm Issues AbstractInstantiation (`docs/running_psalm/issues/AbstractInstantiation.md`)

AbstractInstantiation

Emitted when an attempt is made to instantiate an abstract class:

php
<?php

abstract class A {}
new A();

Running Psalm Issues AbstractMethodCall (`docs/running_psalm/issues/AbstractMethodCall.md`)

AbstractMethodCall

Emitted when an attempt is made to call an abstract static method directly

php
<?php

abstract class Base {
    abstract static function bar() : void;
}

Base::bar();

Why this is bad

It's not allowed by PHP.


Running Psalm Issues AmbiguousConstantInheritance (`docs/running_psalm/issues/AmbiguousConstantInheritance.md`)

AmbiguousConstantInheritance

Emitted when a constant is inherited from multiple sources.

php
<?php

interface Foo
{
    /** @var non-empty-string */
    public const CONSTANT='foo';
}

interface Bar
{
    /**
     * @var non-empty-string
     */
    public const CONSTANT='bar';
}

interface Baz extends Foo, Bar {}
php
<?php

interface Foo
{
    /** @var non-empty-string */
    public const CONSTANT='foo';
}

class Bar
{
    /** @var non-empty-string */
    public const CONSTANT='bar';
}

class Baz extends Bar implements Foo {}

Running Psalm Issues ArgumentTypeCoercion (`docs/running_psalm/issues/ArgumentTypeCoercion.md`)

ArgumentTypeCoercion

Emitted when calling a function with an argument which has a less specific type than the function expects

php
<?php

class A {}
class B extends A {}

function takesA(A $a) : void {
    takesB($a);
}
function takesB(B $b) : void {}

How to fix

You could add a typecheck before the call to takesB:

php
<?php

function takesA(A $a) : void {
    if ($a instanceof B) {
        takesB($a);
    }
}

Or, if you have control over the function signature of takesA you can change it to expect B:

php
<?php

function takesA(B $a) : void {
    takesB($a);
}

Running Psalm Issues AssignmentToVoid (`docs/running_psalm/issues/AssignmentToVoid.md`)

AssignmentToVoid

Emitted when assigning from a function that returns void:

php
<?php

function foo() : void {}
$a = foo();

Why this is bad

Though void-returning functions are treated by PHP as returning null (so this on its own does not lead to runtime errors), void is a concept more broadly in programming languages which is not designed for assignment purposes.

How to fix

You should just be able to remove the assignment entirely:

php
<?php

function foo() : void {}
foo();

Running Psalm Issues CheckType (`docs/running_psalm/issues/CheckType.md`)

CheckType

Checks if a variable matches a specific type.
Similar to Trace, but only shows if the type does not match the expected type.

php
<?php

/** @psalm-check-type $x = 1 */
$x = 2; // Checked variable $x = 1 does not match $x = 2
php
<?php

/** @psalm-check-type-exact $x = int */
$x = 2; // Checked variable $x = int does not match $x = 2

Running Psalm Issues CircularReference (`docs/running_psalm/issues/CircularReference.md`)

CircularReference

Emitted when a class references itself as one of its parents

php
<?php

class A extends B {}
class B extends A {}

Why this is bad

The code above will not compile


Running Psalm Issues ClassMustBeFinal (`docs/running_psalm/issues/ClassMustBeFinal.md`)

ClassMustBeFinal

Emitted when a non-final, non-abstract class with no child classes is found.

php
<?php

class A {}

Why this is bad

Non-final classes are bad for multiple reasons:

  • They allow overriding non-final methods and properties, which can lead to unexpected behavior and bugs if implementation details are changed during inheritance of classes not explicitly part of the public API (marked using the @api attribute).
  • A corollary of the above is that non-final classes increase the amount of code that must be covered by any backwards-compatibility promise.
    • In final classes, only public functions/properties/constants must be covered by the backwards compatibility promise.
    • In non-final classes, all private, protected and public functions/properties/constants must be covered by the backwards compatibility promise (private methods too, because changes to their code may not be compatible with overridden protected/public methods).
  • They are not optimized by Opcache and PHP itself, and thus are more expensive to use at runtime.
  • Psalm type inference is more complex and not as exact for non-final classes.

In general, the number of non-final classes in the codebase should be reduced as much as possible, both to speed up code execution and avoid unexpected bugs.

How to fix

Recommended, make the class final:

php
<?php

final class A {}

The above can also be automated using vendor/bin/psalm --alter --issues=ClassMustBeFinal.

If inheritance should still be allowed, reduce the surface covered by the backwards compatibility promise by making the class abstract (containing only the logic that should be overridable), and move any non-overridable logic to a new A class:

php
<?php

abstract class A {}

final class NewA extends A {}

Note: if non-final classes are needed for mocking in unit tests, simply use dg/bypass-finals in your unit tests to allow mocking final classes.

An alternative, not recommended for the above reasons, is to make the class part of the public API of your library with @api.


Running Psalm Issues ComplexFunction (`docs/running_psalm/issues/ComplexFunction.md`)

ComplexFunction

Emitted when a function is too complicated. Complicated functions should be split up.


Running Psalm Issues ComplexMethod (`docs/running_psalm/issues/ComplexMethod.md`)

ComplexMethod

Emitted when a method is too complicated. Complicated methods should be split up.


Running Psalm Issues ConfigIssue (`docs/running_psalm/issues/ConfigIssue.md`)

ConfigIssue

Emitted for non-fatal configuration issues, e.g. deprecated configuration switches.

Why this is bad

Your config file may be incompatible with future Psalm versions.


Running Psalm Issues ConflictingReferenceConstraint (`docs/running_psalm/issues/ConflictingReferenceConstraint.md`)

ConflictingReferenceConstraint

Emitted when a by-ref variable is set in two different branches of an if to different types.

php
<?php

 class A {
    /** @var int */
    private $foo;

    public function __construct(int &$foo) {
        $this->foo = &$foo;
    }
}

class B {
    /** @var string */
    private $bar;

    public function __construct(string &$bar) {
        $this->bar = &$bar;
    }
}

if (rand(0, 1)) {
    $v = 5;
    $c = (new A($v)); // $v is constrained to an int
} else {
    $v = "hello";
    $c = (new B($v)); // $v is constrained to a string
}

$v = 8;

Why this is bad

Psalm doesn't understand what the type of $c should be


Running Psalm Issues ConstantDeclarationInTrait (`docs/running_psalm/issues/ConstantDeclarationInTrait.md`)

ConstantDeclarationInTrait

Emitted when a trait declares a constant in PHP <8.2.0

php
<?php

trait A {
    const B = 0;
}

Why this is bad

A fatal error will be thrown.


Running Psalm Issues ConstructorSignatureMismatch (`docs/running_psalm/issues/ConstructorSignatureMismatch.md`)

ConstructorSignatureMismatch

Emitted when a constructor parameter differs from a parent constructor parameter, or if there are fewer parameters than the parent constructor AND where the parent class has a @psalm-consistent-constructor annotation.

php
<?php

/**
 * @psalm-consistent-constructor
 */
class A {
    public function __construct(int $s) {}
}
class B extends A {
    public function __construct(string $s) {}
}

Running Psalm Issues ContinueOutsideLoop (`docs/running_psalm/issues/ContinueOutsideLoop.md`)

ContinueOutsideLoop

Emitted when encountering a continue statement outside a loop context.

php
<?php

$a = 5;
continue;

Why this is bad

The code won't compile in PHP 5.6 and above.


Running Psalm Issues DeprecatedClass (`docs/running_psalm/issues/DeprecatedClass.md`)

DeprecatedClass

Emitted when referring to a deprecated class:

php
<?php

/** @deprecated */
class A {}
new A();

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated class.


Running Psalm Issues DeprecatedConstant (`docs/running_psalm/issues/DeprecatedConstant.md`)

DeprecatedConstant

Emitted when referring to a deprecated constant or enum case:

php
<?php

class A {
    /** @deprecated */
    const FOO = 'foo';
}

echo A::FOO;

enum B {
    /** @deprecated */
    case B;
}

echo B::B;

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated constant or enum case


Running Psalm Issues DeprecatedFunction (`docs/running_psalm/issues/DeprecatedFunction.md`)

DeprecatedFunction

Emitted when calling a deprecated function:

php
<?php

/** @deprecated */
function foo() : void {}
foo();

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated function.


Running Psalm Issues DeprecatedInterface (`docs/running_psalm/issues/DeprecatedInterface.md`)

DeprecatedInterface

Emitted when referring to a deprecated interface

php
<?php

/** @deprecated */
interface I {}

class A implements I {}

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated interface.


Running Psalm Issues DeprecatedMethod (`docs/running_psalm/issues/DeprecatedMethod.md`)

DeprecatedMethod

Emitted when calling a deprecated method on a given class:

php
<?php

class A {
    /** @deprecated */
    public function foo() : void {}
}
(new A())->foo();

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated method.


Running Psalm Issues DeprecatedProperty (`docs/running_psalm/issues/DeprecatedProperty.md`)

DeprecatedProperty

Emitted when getting/setting a deprecated property of a given class

php
<?php

class A {
    /**
     * @deprecated
     * @var ?string
     */
    public $foo;
}
(new A())->foo = 5;

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated property.


Running Psalm Issues DeprecatedTrait (`docs/running_psalm/issues/DeprecatedTrait.md`)

DeprecatedTrait

Emitted when referring to a deprecated trait:

php
<?php

/** @deprecated */
trait T {}
class A {
    use T;
}

Why this is bad

The @deprecated tag is normally indicative of code that will stop working in the near future.

How to fix

Don’t use the deprecated trait.


Running Psalm Issues DirectConstructorCall (`docs/running_psalm/issues/DirectConstructorCall.md`)

DirectConstructorCall

Emitted when __construct() is called directly as a method. Constructors are supposed to be called implicitly, as a result of new ClassName statement.

php
<?php
class A {
    public function __construct() {}
}
$a = new A;
$a->__construct(); // wrong

Running Psalm Issues DocblockTypeContradiction (`docs/running_psalm/issues/DocblockTypeContradiction.md`)

DocblockTypeContradiction

Emitted when conditional doesn't make sense given the docblock types supplied.

php
<?php

/**
 * @param string $s
 *
 * @return void
 */
function foo($s) {
    if ($s === null) {
        throw new \Exception('Bad input');
    }

    echo $s;
}

Why this is bad

This can sometimes point to a flaw in either your docblock types, or some unnecessary runtime checks in an environment where all types can be checked by Psalm, without the need for additional runtime checks.

How to fix

A lot of old PHP code is set up to prevent unexpected errors with checks like the one above.

As you migrate your code to newer versions of PHP you can also use stricter type-hints:

php
<?php

function foo(string $s) : void {
    echo $s;
}

Since it's impossible for $s to be anything but a string inside the function, the null check can be removed.


Running Psalm Issues DuplicateArrayKey (`docs/running_psalm/issues/DuplicateArrayKey.md`)

DuplicateArrayKey

Emitted when an array has a key more than once

php
<?php

$arr = [
    'a' => 'one',
    'b' => 'two',
    'c' => 'this text will be overwritten by the next line',
    'c' => 'three',
];

This can be caused by variadic arguments if @no-named-arguments is not specified:

php
<?php
function foo($bar, ...$baz): array
{
    return [$bar, ...$baz]; // $baz is array<array-key, mixed> since it can have named arguments
}

How to fix

Remove the offending duplicates:

php
<?php

$arr = [
    'a' => 'one',
    'b' => 'two',
    'c' => 'three',
];

The first matching 'c' key was removed to prevent a change in behaviour (any new duplicate keys overwrite the values of previous ones).


Running Psalm Issues DuplicateClass (`docs/running_psalm/issues/DuplicateClass.md`)

DuplicateClass

Emitted when a class is defined twice

php
<?php

class A {}
class A {}

Why this is bad

The above code won’t compile.

PHP does allow you to define a class conditionally:

php
<?php

if (rand(0, 1)) {
    class A {
        public function __construct(string $s) {}
    }
} else {
    class A {
        public function __construct(object $o) {}
    }
}

But Psalm really doesn't want you to use this pattern – it's impossible for Psalm to know (without using reflection) which class is getting used.


Running Psalm Issues DuplicateConstant (`docs/running_psalm/issues/DuplicateConstant.md`)

DuplicateConstant

Emitted when a constant is defined twice in a single class or when there's a
clash between a constant and an enum case.

php
<?php
class C {
    public const A = 1;
    public const A = 2;
}

Why this is bad

The above code won’t compile.


Running Psalm Issues DuplicateEnumCase (`docs/running_psalm/issues/DuplicateEnumCase.md`)

DuplicateEnumCase

Emitted when enum has duplicate cases.

php
<?php

enum Status 
{
    case Open;
    case Open;
}

How to fix

Remove or rename the offending duplicates.

php
<?php

enum Status 
{
    case Open;
    case Closed;
}

Running Psalm Issues DuplicateEnumCaseValue (`docs/running_psalm/issues/DuplicateEnumCaseValue.md`)

DuplicateEnumCaseValue

Emitted when a backed enum has duplicate case values.

php
<?php

enum Status: string 
{
    case Open = "open";
    case Closed = "open";
}

How to fix

Change case values so that there are no duplicates.

php
<?php

enum Status: string 
{
    case Open = "open";
    case Closed = "closed";
}

Running Psalm Issues DuplicateFunction (`docs/running_psalm/issues/DuplicateFunction.md`)

DuplicateFunction

Emitted when a function is defined twice

php
<?php

function foo() : void {}
function bar() : void {}
function foo() : void {}

Running Psalm Issues DuplicateMethod (`docs/running_psalm/issues/DuplicateMethod.md`)

DuplicateMethod

Emitted when a method is defined twice

php
<?php

class A {
    public function foo() {}
    public function foo() {}
}

Running Psalm Issues DuplicateParam (`docs/running_psalm/issues/DuplicateParam.md`)

DuplicateParam

Emitted when a function has a param defined twice

php
<?php

function foo(int $b, string $b) {}

Why this is bad

The above code produces a fatal error in PHP.


Running Psalm Issues DuplicateProperty (`docs/running_psalm/issues/DuplicateProperty.md`)

DuplicateProperty

Emitted when a class property is defined twice

php
<?php

class Foo
{
    public int $foo;
    public string $foo;
}

class Bar
{
    public int $bar;
    public static string $bar;
}

Running Psalm Issues EmptyArrayAccess (`docs/running_psalm/issues/EmptyArrayAccess.md`)

EmptyArrayAccess

Emitted when attempting to access a value on an empty array

php
<?php

$a = [];
$b = $a[0];

Running Psalm Issues ExtensionRequirementViolation (`docs/running_psalm/issues/ExtensionRequirementViolation.md`)

ExtensionRequirementViolation

Emitted when a using class of a trait does not extend the class specified using @psalm-require-extends.

php
<?php

class A { }

/**
 * @psalm-require-extends A
 */
trait T { }

class B {
  // ExtensionRequirementViolation is emitted, as T requires
  // the using class B to extend A, which is not the case
  use T; 
}

Running Psalm Issues FalsableReturnStatement (`docs/running_psalm/issues/FalsableReturnStatement.md`)

FalsableReturnStatement

Emitted if a return statement contains a false value, but the function return type does not allow false

php
<?php

function getCommaPosition(string $a) : int {
    return strpos($a, ',');
}

How to fix

You can add a specific check for false:

php
<?php

function getCommaPosition(string $a) : int {
    $pos = strpos($a, ',');

    if ($pos === false) {
        return -1;
    }

    return $pos;
}

Alternatively you may choose to throw an exception:

php
<?php

function getCommaPosition(string $a) : int {
    $pos = strpos($a, ',');

    if ($pos === false) {
        throw new Exception('This is unexpected');
    }

    return $pos;
}

Running Psalm Issues FalseOperand (`docs/running_psalm/issues/FalseOperand.md`)

FalseOperand

Emitted when using false as part of an operation (e.g. +, ., ^ etc.)

php
<?php

echo 5 . false;

Why this is bad

false does not make sense in these operations


Running Psalm Issues ForbiddenCode (`docs/running_psalm/issues/ForbiddenCode.md`)

ForbiddenCode

Emitted when Psalm encounters a var_dump, exec or similar expression that may make your code more vulnerable

php
<?php

var_dump("bah");

This functions list can be extended by configuring forbiddenFunctions or forbiddenConstants in psalm.xml

xml
<?xml version="1.0"?>
<psalm>
    

    <forbiddenFunctions>
        <function name="dd"/>
        <function name="var_dump"/>
    </forbiddenFunctions>
    
    <forbiddenConstants>
        <constant name="FILTER_VALIDATE_URL" />
    </forbiddenConstants>
</psalm>

Running Psalm Issues IfThisIsMismatch (`docs/running_psalm/issues/IfThisIsMismatch.md`)

IfThisIsMismatch

Emitted when the type in @psalm-if-this-is annotation cannot be contained by the object's actual type.

php
<?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();

Running Psalm Issues ImmutableDependency (`docs/running_psalm/issues/ImmutableDependency.md`)

ImmutableDependency

Emitted when an mutable class inherits from an immutable class, trait or interface.

php
<?php

/** @psalm-immutable */
class ImmutableParent {
    public int $i = 0;

    public function getI(): int {
        return $this->i;
    }
}

final class MutableChild extends ImmutableParent {
    public function setI(int $i): void {
        $this->i = 123;
    }
}

// This is bad because when passing around an ImmutableParent instance,
// we might actually be passing around a MutableChild.

Will also be emitted for classes marked @psalm-pure or @psalm-capabilities, and for a method override that needs more capabilities than the method it overrides:

php
<?php

abstract class Parent_ {
    /** @psalm-pure */
    abstract public function get(): int;
}

final class Child extends Parent_ {
    /** @psalm-capabilities write-this-props */
    public function get(): int { return 1; } // needs more than the pure method it overrides
}

Will also be emitted for a class extending a class with @psalm-taint-specialize without having it: the class is specialized, and held to the rules of specialized classes, all the same.

php
<?php

/** @psalm-taint-specialize */
class User {
    public function __construct(public string $name) {}
}

final class Admin extends User {}

To fix, make the child need the same (or fewer) capabilities than the parent, or vice versa.

See class-level contracts and overrides in the purity model.


Running Psalm Issues ImplementationRequirementViolation (`docs/running_psalm/issues/ImplementationRequirementViolation.md`)

ImplementationRequirementViolation

Emitted when a using class of a trait does not implement all interfaces specified using @psalm-require-implements.

php
<?php

interface A { }
interface B { }

/**
 * @psalm-require-implements A
 * @psalm-require-implements B
 */
trait T { }

class C {
  // ImplementationRequirementViolation is emitted, as T requires
  // the using class C to implement A and B, which is not the case
  use T; 
}

Running Psalm Issues ImplementedParamTypeMismatch (`docs/running_psalm/issues/ImplementedParamTypeMismatch.md`)

ImplementedParamTypeMismatch

Emitted when a class that inherits another, or implements an interface, has a docblock param type that's entirely different to the parent.

php
<?php

class D {
    /** @param string $a */
    public function foo($a): void {}
}

class E extends D {
    /** @param int $a */
    public function foo($a): void {}
}

How to fix

Make sure to respect the Liskov substitution principle – any method that overrides a parent method must accept all the same arguments as its parent method.

php
<?php

class D {
    /** @param string $a */
    public function foo($a): void {}
}

class E extends D {
    /** @param string|int $a */
    public function foo($a): void {}
}

Running Psalm Issues ImplementedReturnTypeMismatch (`docs/running_psalm/issues/ImplementedReturnTypeMismatch.md`)

ImplementedReturnTypeMismatch

Emitted when a class that inherits another, or implements an interface, has a docblock return type that's entirely different to the parent.

php
<?php

class A {
    /** @return bool */
    public function foo() {
        return true;
    }
}
class B extends A {
    /** @return string */
    public function foo()  {
        return "hello";
    }
}

How to fix

Make sure to respect the Liskov substitution principle – any method that overrides a parent method must return a subtype of the parent method.

In the above case, that means adding the child return type to the parent one.

php
<?php

class A {
    /** @return bool|string */
    public function foo() {
        return true;
    }
}
class B extends A {
    /** @return string */
    public function foo()  {
        return "hello";
    }
}

Running Psalm Issues ImplicitToStringCast (`docs/running_psalm/issues/ImplicitToStringCast.md`)

ImplicitToStringCast

Emitted when implicitly converting an object with a __toString method to a string

php
<?php

class A {
    public function __toString() {
        return "foo";
    }
}

function takesString(string $s) : void {}

takesString(new A);

How to fix

You can add an explicit string cast:

php
<?php

...

takesString((string) new A);

Running Psalm Issues ImpureByReferenceAssignment (`docs/running_psalm/issues/ImpureByReferenceAssignment.md`)

ImpureByReferenceAssignment

Emitted when assigning a passed-by-reference variable inside a function or method without the write-refs capability.

php
<?php

/**
 * @psalm-pure
 */
function foo(string &$a): string {
    $a = "B";
    return $a;
}

How to fix

Just remove the mutating assignment:

php
<?php

/**
 * @psalm-pure
 */
function foo(string &$a): string {
    return $a;
}

See by-reference arguments in the purity model.


Running Psalm Issues ImpureFunctionCall (`docs/running_psalm/issues/ImpureFunctionCall.md`)

ImpureFunctionCall

Emitted when calling an impure function from a function or method marked as pure.

php
<?php

function impure(array $a) : array {
    /** @var int */
    static $i = 0;

    ++$i;

    $a[$i] = 1;

    return $a;
}

/** @psalm-pure */
function filterOdd(array $a) : void {
    impure($a);
}

To make a function's purity depend on the closures it is given, see purity templates in the purity model.


Running Psalm Issues ImpureGlobalVariable (`docs/running_psalm/issues/ImpureGlobalVariable.md`)

ImpureGlobalVariable

Emitted when attempting to use a global or superglobal variable from a function or method marked as pure

php
<?php

/** @psalm-pure */
function getFromSuperglobal() : int {
    return (int) $_GET['v'];
}

/** @psalm-pure */
function addCumulativeGlobal(int $left) : int {
    /** @var int */
    global $i;
    $i ??= 0;
    $i += $left;
    return $left;
}

/** @psalm-pure */
function addCumulativeGlobals(int $left) : int {
    $GLOBALS['i'] ??= 0;
    $GLOBALS['i'] += $left;
    return $left;
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues ImpureMethodCall (`docs/running_psalm/issues/ImpureMethodCall.md`)

ImpureMethodCall

Emitted when calling an impure method from a function or method marked as pure.

php
<?php

class A {
    public int $a = 5;

    public function foo() : void {
        $this->a++;
    }
}

/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
    $a->foo();

    if ($i % 2 === 0 || $a->a === 2) {
        return $i;
    }

    return null;
}

What a method call costs depends on its receiver: see how calls are charged in the purity model.


Running Psalm Issues ImpurePropertyAssignment (`docs/running_psalm/issues/ImpurePropertyAssignment.md`)

ImpurePropertyAssignment

Emitted when updating a property value from a function or method marked as pure.

php
<?php

class A {
    public int $a = 5;
}

/** @psalm-pure */
function foo(int $i, A $a) : int {
    $a->a = $i;

    return $i;
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues ImpurePropertyFetch (`docs/running_psalm/issues/ImpurePropertyFetch.md`)

ImpurePropertyFetch

Emitted when fetching a property value inside a function or method marked as pure.

php
<?php

class A {
    public int $a = 5;
}

/** @psalm-pure */
function foo(int $i, A $a) : int {
    return $i + $a->a;
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues ImpureStaticProperty (`docs/running_psalm/issues/ImpureStaticProperty.md`)

ImpureStaticProperty

Emitted when attempting to use a static property from a function or method marked as pure

php
<?php

class ValueHolder {
    public static ?string $value = null;

    /**
     * @psalm-pure
     */
    public static function get(): ?string {
        return self::$value;
    }
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues ImpureStaticVariable (`docs/running_psalm/issues/ImpureStaticVariable.md`)

ImpureStaticVariable

Emitted when attempting to use a static variable from a function or method marked as pure

php
<?php

/** @psalm-pure */
function addCumulative(int $left) : int {
    /** @var int */
    static $i = 0;
    $i += $left;
    return $left;
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues ImpureVariable (`docs/running_psalm/issues/ImpureVariable.md`)

ImpureVariable

Emitted when referencing an impure or possibly-impure variable from a pure context.

php
<?php

class A {
    public int $a = 5;

    /**
     * @psalm-pure
     */
    public function foo() : self {
        return $this;
    }
}

See capabilities in the purity model for what each capability allows.


Running Psalm Issues InaccessibleClassConstant (`docs/running_psalm/issues/InaccessibleClassConstant.md`)

InaccessibleClassConstant

Emitted when a public/private class constant is not accessible from the calling context

php
<?php

class A {
    protected const FOO = 'FOO';
}
echo A::FOO;

Running Psalm Issues InaccessibleMethod (`docs/running_psalm/issues/InaccessibleMethod.md`)

InaccessibleMethod

Emitted when attempting to access a protected/private method from outside its available scope

php
<?php

class A {
    protected function foo() : void {}
}
echo (new A)->foo();

Running Psalm Issues InaccessibleProperty (`docs/running_psalm/issues/InaccessibleProperty.md`)

InaccessibleProperty

Emitted when attempting to access a protected/private property from outside its available scope

php
<?php

class A {
    /** @return string */
    protected $foo;
}
echo (new A)->foo;

It is also emitted when writing to a property whose asymmetric set visibility (PHP 8.4) is not accessible
from the current context:

php
<?php

class A {
    public private(set) string $foo = 'bar';
}

$a = new A();
$a->foo = 'baz';

Running Psalm Issues IncompatibleTypeParameters (`docs/running_psalm/issues/IncompatibleTypeParameters.md`)

IncompatibleTypeParameters

Emitted when the constraints recorded for a class template parameter that could not
be inferred at the construction site turn out to be incompatible with each other.

When a templated class is constructed, Psalm tracks each of its templates as a
type variable: the constructor arguments
and each later use add constraints, which are reconciled with each other once the
surrounding function has been analyzed.

php
<?php

/** @template T of int */
class IntBox {
    public function __construct() {}

    /** @param T $item */
    public function add($item): void {}
}

function probe(): void {
    $box = new IntBox();
    $box->add("nope");
}

Here nothing binds T when IntBox is constructed, so T must be inferred from the
later call to add() β€” but 'nope' cannot satisfy the template's of int constraint.

Why it’s bad

There is no type the template parameter could take that satisfies all of its
recorded constraints, so the object is being used in contradiction with its
own declaration.

How to fix

Pass values that satisfy the template's declared constraint:

php
<?php

/** @template T of int */
class IntBox {
    public function __construct() {}

    /** @param T $item */
    public function add($item): void {}
}

function probe(): void {
    $box = new IntBox();
    $box->add(42);
}

Running Psalm Issues InheritorViolation (`docs/running_psalm/issues/InheritorViolation.md`)

InheritorViolation

Emitted when a class/interface using @psalm-inheritors is extended/implemented
by a class that does not fulfil it's requirements.

php
<?php

/**
 * @psalm-inheritors FooClass|BarClass
 */
class BaseClass {}
class BazClass extends BaseClass {}
// InheritorViolation is emitted, as BaseClass can only be extended
// by FooClass|BarClass, which is not the case
$a = new BazClass();

Running Psalm Issues IntMaskComparison (`docs/running_psalm/issues/IntMaskComparison.md`)

IntMaskComparison

Emitted when a bit set, i.e. a value typed as int-mask<…> or int-mask-of<…>, is compared by value with something other than 0 or the set of all its bits.

php
<?php

final class Permission
{
    public const READ = 1;
    public const WRITE = 2;
    public const DELETE = 4;
}

/** @param int-mask-of<Permission::*> $permissions */
function canRead(int $permissions): bool
{
    return $permissions === Permission::READ;
}

Why this is bad

$permissions === Permission::READ is only true when READ is the only bit set: canRead(Permission::READ | Permission::WRITE) returns false. Comparing a bit set by value is almost always a mistake for a test of some of its bits.

How to fix

Test the bits you need with &:

php
<?php

final class Permission
{
    public const READ = 1;
    public const WRITE = 2;
    public const DELETE = 4;
}

/** @param int-mask-of<Permission::*> $permissions */
function canRead(int $permissions): bool
{
    return ($permissions & Permission::READ) === Permission::READ;
}
  • ($mask & B) === B: every bit of B is set;
  • ($mask & B) !== 0: at least one bit of B is set;
  • ($mask & ~B) === 0: no bit outside of B is set, i.e. the mask is a subset of B;
  • $mask === 0: no bit is set;
  • $mask === ALL, where ALL has every bit of the mask's type: every bit is set.

$mask & B is itself a bit set, of the bits of B, so it may only be compared with 0 or B.

$a & $b may also be compared with $a or $b, as in ($available & $required) === $required.

The check covers ===, !==, ==, !=, <, <=, >, >= and <=> (ordering comparisons only allow 0), match arms, switch cases, and the search value of in_array(), array_search() and array_keys().

When a comparison of the exact set of bits is intended, for instance to tell whether a fixed point was reached, pass the masks to a function taking a plain int that documents that intent.


Running Psalm Issues InterfaceInstantiation (`docs/running_psalm/issues/InterfaceInstantiation.md`)

InterfaceInstantiation

Emitted when an attempt is made to instantiate an interface:

php
<?php

interface I {}
new I();

Running Psalm Issues InternalClass (`docs/running_psalm/issues/InternalClass.md`)

InternalClass

Emitted when attempting to access a class marked as internal an unrelated namespace or class, or attempting
to access a class marked as psalm-internal to a different namespace.

php
<?php

namespace A {
    /**
     * @internal
     */
    class Foo { }
}

namespace B {
    class Bat {
        public function batBat(): void {
            $a = new \A\Foo();
        }
    }
}

Running Psalm Issues InternalMethod (`docs/running_psalm/issues/InternalMethod.md`)

InternalMethod

Emitted when attempting to access a method marked as internal an unrelated namespace or class, or attempting
to access a method marked as psalm-internal to a different namespace.

php
<?php

namespace A {
    class Foo {
        /**
         * @internal
         */
        public static function barBar(): void {
        }
    }
}
namespace B {
    class Bat {
        public function batBat(): void {
            \A\Foo::barBar();
        }
    }
}

Running Psalm Issues InternalProperty (`docs/running_psalm/issues/InternalProperty.md`)

InternalProperty

Emitted when attempting to access a property marked as internal from an unrelated namespace or class, or attempting
to access a property marked as psalm-internal to a different namespace.

php
<?php

namespace A {
    class Foo {
        /**
         * @internal
         * @var ?int
         */
        public $foo;
    }
}

namespace B {
    class Bat {
        public function batBat() : void {
            echo (new \A\Foo)->foo;
        }
    }
}

Running Psalm Issues InvalidArgument (`docs/running_psalm/issues/InvalidArgument.md`)

InvalidArgument

Emitted when a supplied function/method argument is incompatible with the method signature or docblock one.

php
<?php

class A {}

function foo(A $a) : void {}

/**
 * @param string $s
 */
function callFoo($s) : void {
    foo($s);
}

Why it’s bad

Calling functions with incorrect values will cause a fatal error at runtime.

How to fix

Sometimes this message can just be the result of an incorrect docblock.

You can fix by correcting the docblock, or converting to a function signature:

php
<?php

class A {}

function foo(A $a) : void {}

function callFoo(A $a) : void {
    foo($a);
}

Running Psalm Issues InvalidArrayAccess (`docs/running_psalm/issues/InvalidArrayAccess.md`)

InvalidArrayAccess

Emitted when attempting to access an array offset on a value that does not permit it

php
<?php

$arr = 5;
echo $arr[0];

Running Psalm Issues InvalidArrayAssignment (`docs/running_psalm/issues/InvalidArrayAssignment.md`)

InvalidArrayAssignment

Emitted when attempting to assign a value on a non-array

php
<?php

$arr = 5;
$arr[0] = 3;

Running Psalm Issues InvalidArrayOffset (`docs/running_psalm/issues/InvalidArrayOffset.md`)

InvalidArrayOffset

Emitted when attempting to access an array using a value that's not a valid offset for that array

php
<?php

$a = [5, 20, 18];
echo $a["hello"];

Running Psalm Issues InvalidAttribute (`docs/running_psalm/issues/InvalidAttribute.md`)

InvalidAttribute

Emitted when using an attribute on an element that doesn't match the attribute's target

php
<?php
namespace Foo;

#[\Attribute(\Attribute::TARGET_CLASS)]
class Table {
    public function __construct(public string $name) {}
}

#[Table("videos")]
function foo() : void {}

Running Psalm Issues InvalidCast (`docs/running_psalm/issues/InvalidCast.md`)

InvalidCast

Emitted when attempting to cast a value that's not castable

php
<?php

class A {}
$a = new A();
$b = (string)$a;

Running Psalm Issues InvalidCatch (`docs/running_psalm/issues/InvalidCatch.md`)

InvalidCatch

Emitted when trying to catch a class/interface that doesn't extend Exception or implement Throwable

php
<?php

class A {}
try {
    $worked = true;
}
catch (A $e) {}

Running Psalm Issues InvalidClass (`docs/running_psalm/issues/InvalidClass.md`)

InvalidClass

Emitted when referencing a class with the wrong casing

php
<?php

class Foo {}
(new foo());

Could also be an issue in the namespace even if the class has the correct casing

php
<?php

namespace OneTwo {
    class Three {}
}

namespace {
    use Onetwo\Three;
    //     ^ ("t" instead of "T")

    $three = new Three();
}

Running Psalm Issues InvalidClassConstantType (`docs/running_psalm/issues/InvalidClassConstantType.md`)

InvalidClassConstantType

Emitted when a constant type in a child does not satisfy the type in the parent.

php
<?php

class Foo
{
    /** @var int<1,max> */
    public const CONSTANT = 3;

    public static function bar(): array
    {
        return str_split("foobar", static::CONSTANT);
    }
}

class Bar extends Foo
{
    /** @var int<min,-1> */
    public const CONSTANT = -1;
}

Bar::bar(); // Error: str_split argument 2 must be greater than 0

This issue will always show up when overriding a constant that doesn't have a docblock type. Psalm will infer the most specific type for the constant that it can, you have to add a type annotation to tell it what type constraint you wish to be applied. Otherwise Psalm has no way of telling if you mean for the constant to be a literal 1, int<1, max>, int, numeric, etc.


Running Psalm Issues InvalidClone (`docs/running_psalm/issues/InvalidClone.md`)

InvalidClone

Emitted when trying to clone a value that's not cloneable

php
<?php

$a = "hello";
$b = clone $a;

Running Psalm Issues InvalidConstantAssignmentValue (`docs/running_psalm/issues/InvalidConstantAssignmentValue.md`)

InvalidConstantAssignmentValue

Emitted when attempting to assign a value to a class constant that cannot contain that type.

php
<?php

class Foo {
    /** @var int */
    public const BAR = "bar";
}

Running Psalm Issues InvalidDocblock (`docs/running_psalm/issues/InvalidDocblock.md`)

InvalidDocblock

Emitted when there's an error in a docblock type

php
<?php

/** @var array() */
$a = [];

Running Psalm Issues InvalidDocblockParamName (`docs/running_psalm/issues/InvalidDocblockParamName.md`)

InvalidDocblockParamName

Emitted when a docblock param name does not match up with a named param in the function, if the param does not have a type or its type is array.

php
<?php

/**
 * @param string[] $bar
 */
function foo(array $barb): void {
    //
}

Running Psalm Issues InvalidEnumBackingType (`docs/running_psalm/issues/InvalidEnumBackingType.md`)

InvalidEnumBackingType

Enums can only be backed by int or string. Emitted when an enum is backed
by something else.

php
<?php

enum Status: array 
{
   case None = [];
}

Running Psalm Issues InvalidEnumCaseValue (`docs/running_psalm/issues/InvalidEnumCaseValue.md`)

InvalidEnumCaseValue

Emitted when case value is invalid (see below).

Case with a value on a pure enum

php
<?php

enum Status 
{
    case Open = "open";
}

How to fix

Either remove the value or alter the enum to be backed.

php
<?php

enum Status: string 
{
    case Open = "open";
}

Case without a value on a backed enum

php
<?php

enum Status: string 
{
    case Open;    
}

How to fix

Either alter the enum to be pure, or add a value.

php
<?php

enum Status 
{
    case Open;
}

Case type mismatch

Case type should match the backing type of the enum.

php
<?php

enum Status: string
{
    case Open = 1;
}

How to fix

Change the types so that they match

php
<?php

enum Status: string 
{
    case Open = "open";
}

Case with a type that cannot back an enum

Case type should be either int or string.

php
<?php

enum Status: int {
    case Open = [];
}

How to fix

Change the case value so that it's one of the allowed types (and matches the backing type)

php
<?php

enum Status: int
{
    case Open = 1;
}

Running Psalm Issues InvalidEnumMethod (`docs/running_psalm/issues/InvalidEnumMethod.md`)

InvalidEnumMethod

Enums may not define most of the magic methods like __get, __toString, etc.

php
<?php
enum Status: string {
    case Open = 'open';
    case Closed = 'closed';

    public function __toString(): string {
        return "SomeStatus";
    }
}

Running Psalm Issues InvalidExtendClass (`docs/running_psalm/issues/InvalidExtendClass.md`)

InvalidExtendClass

Emitted when attempting to extend a final class, a class annotated with @final or a class using @psalm-inheritors and not in the inheritor list

php
<?php

final class A {}

class B extends A {}

/**
 * @final
 */
class DoctrineA {}

class DoctrineB extends DoctrineA {}

/**
 * @psalm-inheritors A|B
 */
class C {}

class D extends C {}

Running Psalm Issues InvalidFalsableReturnType (`docs/running_psalm/issues/InvalidFalsableReturnType.md`)

InvalidFalsableReturnType

Emitted when a function can return a nullable value, but its given return type says otherwise

php
<?php

function foo() : string {
    if (rand(0, 1)) {
        return "foo";
    }

    return false;
}

Running Psalm Issues InvalidFunctionCall (`docs/running_psalm/issues/InvalidFunctionCall.md`)

InvalidFunctionCall

Emitted when calling a function on a non-callable variable

php
<?php

$a = 5;
$b = $a();

Running Psalm Issues InvalidGlobal (`docs/running_psalm/issues/InvalidGlobal.md`)

InvalidGlobal

Emitted when there's a reference to the global keyword where it's not expected.

php
<?php

global $e;

If the file is included from a non-global scope this issue will have to be suppressed. See
Config suppression for how to suppress this at the file or directory
level.


Running Psalm Issues InvalidInterfaceImplementation (`docs/running_psalm/issues/InvalidInterfaceImplementation.md`)

InvalidInterfaceImplementation

Emitted when trying to implement interface that cannot be implemented (e.g. Throwable, UnitEnum, BackedEnum).

php
<?php

class E implements UnitEnum 
{
    public static function cases(): array 
    {
        return []; 
    }
}

Running Psalm Issues InvalidIterator (`docs/running_psalm/issues/InvalidIterator.md`)

InvalidIterator

Emitted when trying to iterate over a value that's not iterable

php
<?php

$a = 5;
foreach ($a as $b) {}

Running Psalm Issues InvalidLiteralArgument (`docs/running_psalm/issues/InvalidLiteralArgument.md`)

InvalidLiteralArgument

Emitted when a literal argument is passed where a variable is expected, such as the first argument of strpos, where an explicit $haystack is almost always unintended.

php
<?php

function foo(string $s) : void {
    echo strpos(".", $s);
}

Running Psalm Issues InvalidMethodCall (`docs/running_psalm/issues/InvalidMethodCall.md`)

InvalidMethodCall

Emitted when attempting to call a method on a non-object

php
<?php

$a = 5;
$a->foo();

Running Psalm Issues InvalidNamedArgument (`docs/running_psalm/issues/InvalidNamedArgument.md`)

InvalidNamedArgument

Emitted when a supplied function/method argument name is incompatible with the function/method signature.

php
<?php

function takesArguments(string $name, int $age) : void {}

takesArguments(name: "hello", ag: 5);

Running Psalm Issues InvalidNullableReturnType (`docs/running_psalm/issues/InvalidNullableReturnType.md`)

InvalidNullableReturnType

Emitted when a function can return a nullable value, but its given return type says otherwise

php
<?php

function foo() : string {
    if (rand(0, 1)) {
        return "foo";
    }

    return null;
}

Running Psalm Issues InvalidOperand (`docs/running_psalm/issues/InvalidOperand.md`)

InvalidOperand

Emitted when using something as an operand that is unexpected

php
<?php

class A {}
echo (new A) . ' ';

Running Psalm Issues InvalidOverride (`docs/running_psalm/issues/InvalidOverride.md`)

InvalidOverride

Emitted when an Override attribute was added to a method that does not override a method from a parent class or implemented interface.

php
<?php

class A {
    function receive(): void
    {
    }
}

class B extends A {
    #[Override]
    function obtain(): void
    {
    }
}

Why this is bad

A fatal error will be thrown.


Running Psalm Issues InvalidParamDefault (`docs/running_psalm/issues/InvalidParamDefault.md`)

InvalidParamDefault

Emitted when a function parameter default clashes with the type Psalm expects the param to be

php
<?php

function foo(int $i = false) : void {}

Running Psalm Issues InvalidParent (`docs/running_psalm/issues/InvalidParent.md`)

InvalidParent

Emitted when a function return type is parent, but there's no parent class

php
<?php

class Foo {
    public function f(): parent {}
}

Running Psalm Issues InvalidPassByReference (`docs/running_psalm/issues/InvalidPassByReference.md`)

InvalidPassByReference

Emitted when passing a non-variable to a function that expects a by-ref variable

php
<?php

function foo(array &$arr) : void {}
foo([0, 1, 2]);

Running Psalm Issues InvalidPropertyAssignment (`docs/running_psalm/issues/InvalidPropertyAssignment.md`)

InvalidPropertyAssignment

Emitted when attempting to assign a property to a non-object

php
<?php

$a = "foo";
$a->bar = "bar";

Running Psalm Issues InvalidPropertyAssignmentValue (`docs/running_psalm/issues/InvalidPropertyAssignmentValue.md`)

InvalidPropertyAssignmentValue

Emitted when attempting to assign a value to a property that cannot contain that type.

php
<?php

class A {
    /** @var string|null */
    public $foo;
}
$a = new A();
$a->foo = new stdClass();

Running Psalm Issues InvalidPropertyFetch (`docs/running_psalm/issues/InvalidPropertyFetch.md`)

InvalidPropertyFetch

Emitted when attempting to get a property from a non-object

php
<?php

$a = "foo";
echo $a->bar;

Running Psalm Issues InvalidReturnStatement (`docs/running_psalm/issues/InvalidReturnStatement.md`)

InvalidReturnStatement

Emitted when a function return statement is incorrect

php
<?php

function foo() : string {
    return 5; // emitted here
}

Running Psalm Issues InvalidReturnType (`docs/running_psalm/issues/InvalidReturnType.md`)

InvalidReturnType

Emitted when a function’s signature return type is incorrect (often emitted with InvalidReturnStatement)

php
<?php

function foo() : int {
    if (rand(0, 1)) {
        return "hello";
    }

    return 5;
}

Running Psalm Issues InvalidScalarArgument (`docs/running_psalm/issues/InvalidScalarArgument.md`)

InvalidScalarArgument

Emitted when a scalar value is passed to a method that expected another scalar type.

This is only emitted in situations where Psalm can be sure that PHP tries to coerce one scalar type to another.

In all other cases InvalidArgument is emitted.

php
<?php

function foo(int $i) : void {}
function bar(string $s) : void {
    if (is_numeric($s)) {
        foo($s);
    }
}

Running Psalm Issues InvalidScope (`docs/running_psalm/issues/InvalidScope.md`)

InvalidScope

Emitted when referring to $this outside a class

php
<?php

echo $this;

Running Psalm Issues InvalidStaticInvocation (`docs/running_psalm/issues/InvalidStaticInvocation.md`)

InvalidStaticInvocation

Emitted when trying to call an instance function statically

php
<?php

class A {
    /** @var ?string */
    public $foo;

    public function bar() : void {
        echo $this->foo;
    }
}

A::bar();

Running Psalm Issues InvalidStringClass (`docs/running_psalm/issues/InvalidStringClass.md`)

InvalidStringClass

Emitted when you have allowStringToStandInForClass="false" in your config and you’re passing a string instead of calling a class directly

php
<?php

class Foo {}
$a = "Foo";
new $a();

Running Psalm Issues InvalidTemplateParam (`docs/running_psalm/issues/InvalidTemplateParam.md`)

InvalidTemplateParam

Emitted when using the @extends/@implements annotation to extend a class that has a template type constraint, where that extended value does not satisfy the parent class/interface's constraints.

php
<?php

/**
 * @template T of object
 */
class Base {}

/** @template-extends Base<int> */
class SpecializedByInheritance extends Base {}

Running Psalm Issues InvalidThrow (`docs/running_psalm/issues/InvalidThrow.md`)

InvalidThrow

Emitted when trying to throw a class that doesn't extend Exception or implement Throwable

php
<?php

class A {}
throw new A();

Running Psalm Issues InvalidToString (`docs/running_psalm/issues/InvalidToString.md`)

InvalidToString

Emitted when a __toString method does not always return a string

php
<?php

class A {
    public function __toString() {
        /** @psalm-suppress InvalidReturnStatement */
        return true;
    }
}

Running Psalm Issues InvalidTraversableImplementation (`docs/running_psalm/issues/InvalidTraversableImplementation.md`)

InvalidTraversableImplementation

Emitted when class incorrectly implements Traversable. Traversable needs to be
implemented by implementing either IteratorAggregate or Iterator

php
<?php

/**
 * @implements Traversable<mixed, mixed>
 */
final class C implements Traversable {} // will cause fatal error

Running Psalm Issues InvalidTypeImport (`docs/running_psalm/issues/InvalidTypeImport.md`)

InvalidTypeImport

Emitted when a type imported with @psalm-import-type is invalid

php
<?php

namespace A;

class Types {}

namespace B;
use A\Types;
/** @psalm-import-type UnknownType from Types */
class C {}

Running Psalm Issues LessSpecificClassConstantType (`docs/running_psalm/issues/LessSpecificClassConstantType.md`)

LessSpecificClassConstantType

Emitted when a constant type in a child is less specific than the type in the parent.

php
<?php

class Foo
{
    /** @var int<1,max> */
    public const CONSTANT = 3;

    public static function bar(): array
    {
        return str_split("foobar", static::CONSTANT);
    }
}

class Bar extends Foo
{
    /** @var int */
    public const CONSTANT = -1;
}

Bar::bar(); // Error: str_split argument 2 must be greater than 0

This issue will always show up when overriding a constant that doesn't have a docblock type. Psalm will infer the most specific type for the constant that it can, you have to add a type annotation to tell it what type constraint you wish to be applied. Otherwise Psalm has no way of telling if you mean for the constant to be a literal 1, int<1, max>, int, numeric, etc.


Running Psalm Issues LessSpecificImplementedReturnType (`docs/running_psalm/issues/LessSpecificImplementedReturnType.md`)

LessSpecificImplementedReturnType

Emitted when a class implements an interface method but its return type is less specific than the interface method return type

php
<?php

class A {}
class B extends A {}
interface I {
    /** @return B[] */
    public function foo();
}
class D implements I {
    /** @return A[] */
    public function foo() {
        return [new A, new A];
    }
}

Running Psalm Issues LessSpecificReturnStatement (`docs/running_psalm/issues/LessSpecificReturnStatement.md`)

LessSpecificReturnStatement

Emitted when a return statement is more general than the return type given for the function

php
<?php

class A {}
class B extends A {}

function foo() : B {
    return new A(); // emitted here
}

Running Psalm Issues LessSpecificReturnType (`docs/running_psalm/issues/LessSpecificReturnType.md`)

LessSpecificReturnType

Emitted when a return type covers more possibilities than the function itself

php
<?php

function foo() : ?int {
    return 5;
}

Running Psalm Issues LiteralKeyUnshapedArray (`docs/running_psalm/issues/LiteralKeyUnshapedArray.md`)

LiteralKeyUnshapedArray

Emitted when a literal key is used on an unshaped array, enabled by the disallowLiteralKeysOnUnshapedArrays config parameter.

Useful to enforce usage of shaped arrays instead of generic arrays.

php
<?php

/**
 * @param array<string, bool> $arr
 */
function takesGenericArr(array $arr): void {
    // Error: LiteralKeyUnshapedArray
    $flagA = $arr['flagA'];
    // Error: LiteralKeyUnshapedArray
    $flagB = $arr['flagB'];
}

/**
 * @param array{flagA: bool, flagB: bool} $arr
 */
function takesShapedArr(array $arr): void {
    // OK
    $flagA = $arr['flagA'];
    // OK
    $flagB = $arr['flagB'];
}

Running Psalm Issues LoopInvalidation (`docs/running_psalm/issues/LoopInvalidation.md`)

LoopInvalidation

Emitted when logic inside a loop invalidates one of the conditionals of the loop

php
<?php

for ($i = 0; $i < 10; $i++) {
    $i = 5;
}

Running Psalm Issues MethodSignatureMismatch (`docs/running_psalm/issues/MethodSignatureMismatch.md`)

MethodSignatureMismatch

Emitted when a method parameter differs from a parent method parameter, or if there are fewer parameters than the parent method

php
<?php

class A {
    public function foo(int $i) : void {}
}
class B extends A {
    public function foo(string $i) : void {}
}

Running Psalm Issues MethodSignatureMustOmitReturnType (`docs/running_psalm/issues/MethodSignatureMustOmitReturnType.md`)

MethodSignatureMustOmitReturnType

Emitted when a __clone, __construct, or __destruct method is defined with a return type.

php
<?php

class A {
    public function __clone() : void {}
}

Running Psalm Issues MethodSignatureMustProvideReturnType (`docs/running_psalm/issues/MethodSignatureMustProvideReturnType.md`)

MethodSignatureMustProvideReturnType

In PHP 8.1+, most non-final internal methods now require overriding methods to declare a compatible return type, otherwise a deprecated notice is emitted during inheritance validation.

This issue is emitted when a method overriding a native method is defined without a return type.

Only if the return type cannot be declared to keep support for PHP 7, a #[ReturnTypeWillChange] attribute can be added to silence the PHP deprecation notice and Psalm issue.

php
<?php
class A implements JsonSerializable {
    public function jsonSerialize() {
        return ['type' => 'A'];
    }
}

Running Psalm Issues MismatchingDocblockParamType (`docs/running_psalm/issues/MismatchingDocblockParamType.md`)

MismatchingDocblockParamType

Emitted when an @param entry in a function’s docblock does not match the param typehint

php
<?php

/**
 * @param int $b
 */
function foo(string $b) : void {}

Running Psalm Issues MismatchingDocblockPropertyType (`docs/running_psalm/issues/MismatchingDocblockPropertyType.md`)

MismatchingDocblockPropertyType

Emitted when an @var entry in a property’s docblock does not match the property's type.

php
<?php
class A {
    /** @var array */
    public string $s = [];
}

Running Psalm Issues MismatchingDocblockReturnType (`docs/running_psalm/issues/MismatchingDocblockReturnType.md`)

MismatchingDocblockReturnType

Emitted when an @return entry in a function’s docblock does not match the function return typehint

php
<?php

class A {}
class B {}
/**
 * @return B // emitted here
 */
function foo() : A {
    return new A();
}

This, however, is fine:

php
<?php

class A {}
class B extends A {}
/**
 * @return B // emitted here
 */
function foo() : A {
    return new B();
}

Running Psalm Issues MissingAbstractPureAnnotation (`docs/running_psalm/issues/MissingAbstractPureAnnotation.md`)

MissingAbstractPureAnnotation

Emitted when an abstract method does not have a @psalm-pure, @psalm-impure or @psalm-capabilities
annotation, which says what every implementation may do, for example:

  • @psalm-pure - no side effects and no property accesses
  • @psalm-capabilities read-props - may read properties, but not write them
  • @psalm-capabilities read-props|write-this-props - may also write the properties of $this
  • @psalm-impure - may do anything (not recommended)

This issue is emitted to aid security analysis, which works best when all explicitly pure functions and methods are marked as pure.

php
<?php

interface a {
    public function someMethod(): void;
}

See class-level contracts in the purity model.


Running Psalm Issues MissingClassConstType (`docs/running_psalm/issues/MissingClassConstType.md`)

MissingClassConstType

Emitted when a class constant doesn't have a declared type.

php
<?php

class A {
    public const B = 0;
}

Correct with:

php
<?php

class A {
    public const int B = 0;
}

Running Psalm Issues MissingClosureParamType (`docs/running_psalm/issues/MissingClosureParamType.md`)

MissingClosureParamType

Emitted when a closure parameter has no type information associated with it

php
<?php

$a = function($a): string {
    return "foo";
};

Running Psalm Issues MissingClosureReturnType (`docs/running_psalm/issues/MissingClosureReturnType.md`)

MissingClosureReturnType

Emitted when a closure lacks a return type

php
<?php

$a = function() {
    return "foo";
};

Running Psalm Issues MissingConstructor (`docs/running_psalm/issues/MissingConstructor.md`)

MissingConstructor

Uninitialized properties are statically hard to analyze. To prevent mistakes, Psalm will enforce that all properties should be initialized.

It does that through PropertyNotSetInConstructor and this issue.

Psalm will then assume every property in the codebase is initialized.

Doing that allows it to report missing initializations as well as RedundantPropertyInitializationCheck

This issue is emitted when non-null properties without default values are defined in a class without a __construct method

If your project relies on having uninitialized properties, it is advised to suppress this issue, as well as PropertyNotSetInConstructor and RedundantPropertyInitializationCheck.

php
<?php

class A {
    /** @var string */
    public $foo;
}

Running Psalm Issues MissingDependency (`docs/running_psalm/issues/MissingDependency.md`)

MissingDependency

Emitted when referencing a class that does not exist

php
<?php

/**
 * @psalm-suppress UndefinedClass
 */
class A extends B {}

$a = new A();

Running Psalm Issues MissingDocblockType (`docs/running_psalm/issues/MissingDocblockType.md`)

MissingDocblockType

Emitted when a docblock is present, but the type is missing or badly formatted

php
<?php

/** @var $a */
$a = [];

Running Psalm Issues MissingFile (`docs/running_psalm/issues/MissingFile.md`)

MissingFile

Emitted when using include or require on a file that does not exist

php
<?php

require("nonexistent.php");

Running Psalm Issues MissingImmutableAnnotation (`docs/running_psalm/issues/MissingImmutableAnnotation.md`)

MissingImmutableAnnotation

Emitted when a potentially immutable interface or class does not have a @psalm-pure, @psalm-immutable or @psalm-capabilities declaration.

To automatically add immutable annotations where needed, run Psalm with --alter --issues=MissingImmutableAnnotation.

This issue is emitted to aid security analysis, which works best when all explicitly immutable interfaces and classes are marked as immutable.

php
<?php

/** @api */
final class CouldBeExternallyMutationFree {
    private int $counter = 0;

    /** @psalm-capabilities read-props|write-this-props */
    public function someInteger() : int {
        return ++$this->counter;
    }
}

/** @api */
final class CouldBeImmutable {
}

See inferring annotations in the purity model.


Running Psalm Issues MissingInterfaceImmutableAnnotation (`docs/running_psalm/issues/MissingInterfaceImmutableAnnotation.md`)

MissingInterfaceImmutableAnnotation

Emitted when an interface is not annotated with @psalm-pure, @psalm-immutable, @psalm-capabilities or @psalm-mutable, and declares a method without a purity annotation of its own (or a @psalm-purity-from-template): to fix, mark the interface with one of them, which then applies to all properties and methods of implementing classes. An interface whose methods all say what they may do needs no class-level annotation. Nor does a marker interface, which declares no method or property and inherits none: it does nothing, so it is pure, without capping its implementers.

This issue is emitted to aid security analysis, which works best when all explicitly immutable interfaces and classes are marked as immutable.

php
<?php

/** @api */
interface SomethingPotentiallyImmutable {
    public function someInteger() : int;
}

final class A implements SomethingPotentiallyImmutable {
    public function someInteger() : int {
        return 0;
    }
}

See class-level contracts in the purity model.


Running Psalm Issues MissingOverrideAttribute (`docs/running_psalm/issues/MissingOverrideAttribute.md`)

MissingOverrideAttribute

Emitted when the config flag ensureOverrideAttribute is set to true and a method on a child class or interface overrides a method on a parent, but no Override attribute is present.

php
<?php

class A {
    function receive(): void
    {
    }
}

class B extends A {
    function receive(): void
    {
    }
}

Why this is bad

Having an Override attribute on overridden methods makes intentions clear. Read the PHP RFC for more details.

How to fix

Declare the #[\Override] attribute on all indicated methods, or run vendor/bin/psalter --issues=MissingOverrideAttribute to let Psalm do it for you.

Note that the #[\Override] attribute is compatible with all PHP versions, even PHP 4.

On PHP 8.0-8.2, require symfony/polyfill-php83 to polyfill the missing Override attribute.


Running Psalm Issues MissingParamType (`docs/running_psalm/issues/MissingParamType.md`)

MissingParamType

Emitted when a function parameter has no type information associated with it

php
<?php

function foo($a) : void {}

Running Psalm Issues MissingPropertyType (`docs/running_psalm/issues/MissingPropertyType.md`)

MissingPropertyType

Emitted when a property is defined on a class without a type

php
<?php

class A {
    public $foo = 5;
}

This feature is available in PHP since version 7.4

For more information see:


Running Psalm Issues MissingPureAnnotation (`docs/running_psalm/issues/MissingPureAnnotation.md`)

MissingPureAnnotation

Emitted when a potentially pure function or method does not have a @psalm-pure or @psalm-capabilities declaration: the issue suggests the capabilities it was inferred to need.

A function-like explicitly marked @psalm-impure is left alone: for example a hook that overriding methods may implement freely.

No annotation is suggested for an unannotated method that is overridden, nor for the code calling it: the overrides may do more than its own body does, and the annotation would restrict them.

No annotation is suggested for a function-like whose parameter default values need capabilities the annotation would not give them (see parameter default values).

To automatically add pure annotations where needed, run Psalm with --alter --issues=MissingPureAnnotation.

This issue is emitted to aid security analysis, which works best when all explicitly pure functions and methods are marked as pure.

php
<?php

function couldBePure(int $a): int {
    return $a+1;
}

A function or method (not a closure) is inferred without what the _ purity of its parameters (Closure[_], Traversable[_], see @psalm-purity-template) would charge to its callers: calling the closures and callables found in a parameter with the default purity, directly ($f()), in its elements ($fs[0](), foreach ($fs as $f) { $f(); }) or in what they return ($f()()); iterating over an iterable parameter; and calling a method whose purity depends on a purity argument of a parameter (Doer[_] for a Doer with a @psalm-purity-template). The issue then names the parameters, and --alter adds the _ where it stands, along with the purity annotation.

php
<?php

/**
 * @param list<Closure(int): int> $fs
 */
function applyAll(array $fs, int $x): int {
    foreach ($fs as $f) {
        $x = $f($x); // with --alter, $fs becomes list<Closure[_](int): int> and applyAll @psalm-pure
    }
    return $x;
}

Only what the function-like's own body does counts, not what the closures it creates do: those may be returned or stored rather than called. The parameter must not be assigned in the body, and the _ must be writable where the parameter's type is written (not behind a @psalm-type alias, and not where another purity is written).

See inferring annotations in the purity model for how purity is inferred.


Running Psalm Issues MissingReturnType (`docs/running_psalm/issues/MissingReturnType.md`)

MissingReturnType

Emitted when a function doesn't have a return type defined

php
<?php

function foo() {
    return "foo";
}

Correct with:

php
<?php

function foo() : string {
    return "foo";
}

Running Psalm Issues MissingTemplateParam (`docs/running_psalm/issues/MissingTemplateParam.md`)

MissingTemplateParam

Emitted when using the @extends/@implements annotation to extend a class without
extending all its template params.

php
<?php

/**
 * @template-implements ArrayAccess<int>
 */
class SomeIterator implements ArrayAccess
{
    public function offsetSet($offset, $value) {
    }

    public function offsetExists($offset) {
        return false;
    }

    public function offsetUnset($offset) {
    }

    public function offsetGet($offset) {
        return null;
    }
}

Running Psalm Issues MissingThrowsDocblock (`docs/running_psalm/issues/MissingThrowsDocblock.md`)

MissingThrowsDocblock

Enabled when the checkForThrowsDocblock configuration option is enabled.

Emitted when a function throws (or fails to handle) an exception and does not have a @throws annotation.

php
<?php

function foo(int $x, int $y) : int {
    if ($y === 0) {
        throw new \InvalidArgumentException('Cannot divide by zero');
    }

    return intdiv($x, $y);
}

Running Psalm Issues MixedArgument (`docs/running_psalm/issues/MixedArgument.md`)

MixedArgument

Emitted when Psalm cannot determine the type of an argument

php
<?php

function takesInt(int $i) : void {}
takesInt($GLOBALS['foo']);

Running Psalm Issues MixedArgumentTypeCoercion (`docs/running_psalm/issues/MixedArgumentTypeCoercion.md`)

MixedArgumentTypeCoercion

Emitted when Psalm cannot be sure that part of an array/iterable argument's type constraints can be fulfilled

php
<?php

function foo(array $a) : void {
    takesStringArray($a);
}

/** @param string[] $a */
function takesStringArray(array $a) : void {}

This can happen with variadic arguments when @no-named-arguments is not specified:

php
<?php

/** @param list<int> $args */
function foo(int ...$args): array {
    return $args; // $args is array<array-key, int> since it can have named arguments
}

Running Psalm Issues MixedArrayAccess (`docs/running_psalm/issues/MixedArrayAccess.md`)

MixedArrayAccess

Emitted when trying to access an array offset on a value whose type Psalm cannot determine

php
<?php

echo $GLOBALS['foo'][0];

Running Psalm Issues MixedArrayAssignment (`docs/running_psalm/issues/MixedArrayAssignment.md`)

MixedArrayAssignment

Emitted when trying to assign a value to an array offset on a value whose type Psalm cannot determine

php
<?php

$GLOBALS['foo'][0] = "5";

Running Psalm Issues MixedArrayOffset (`docs/running_psalm/issues/MixedArrayOffset.md`)

MixedArrayOffset

Emitted when attempting to access an array offset where Psalm cannot determine the offset type

php
<?php

echo [1, 2, 3][$GLOBALS['foo']];

Running Psalm Issues MixedArrayTypeCoercion (`docs/running_psalm/issues/MixedArrayTypeCoercion.md`)

MixedArrayTypeCoercion

Emitted when trying to access an array with a less specific offset than is expected

php
<?php

/**
 * @param array<array-key, int> $a
 * @param array<int, string> $b
 */
function foo(array $a, array $b) : void {
    foreach ($a as $j => $k) {
        echo $b[$j];
    }
}

Running Psalm Issues MixedAssignment (`docs/running_psalm/issues/MixedAssignment.md`)

MixedAssignment

Emitted when assigning an unannotated variable to a value for which Psalm
cannot infer a type more specific than mixed.

php
<?php

$a = $GLOBALS['foo'];

How to fix

The above example can be fixed in a few ways – by adding an assert call:

php
<?php

$a = $GLOBALS['foo'];
assert(is_string($a));

or by adding an explicit cast:

php
<?php

$a = (string) $GLOBALS['foo'];

or by adding a docblock

php
<?php

/** @var string */
$a = $GLOBALS['foo'];

Running Psalm Issues MixedClone (`docs/running_psalm/issues/MixedClone.md`)

MixedClone

Emitted when trying to clone a value whose type is not known

php
<?php

$a = clone $GLOBALS["a"];

Running Psalm Issues MixedFunctionCall (`docs/running_psalm/issues/MixedFunctionCall.md`)

MixedFunctionCall

Emitted when calling a function on a value whose type Psalm cannot infer.

php
<?php

/** @var mixed */
$a = $GLOBALS['foo'];
$a();

Running Psalm Issues MixedMethodCall (`docs/running_psalm/issues/MixedMethodCall.md`)

MixedMethodCall

Emitted when calling a method on a value that Psalm cannot infer a type for

php
<?php

class A {
    public function foo() : void {}
}

function callFoo(array $arr) : void {
    array_pop($arr)->foo(); // MixedMethodCall emitted here
}

callFoo(
    [new A()]
);

Why this is bad

If Psalm does not know what array_pop($arr) is, it can't verify whether array_pop($arr)->foo() will work or not.

How to fix

Make sure that to provide as much type information as possible to Psalm so that it can perform inference. For example, you could add a docblock to the callFoo function:

php
<?php

class A {
    public function foo() : void {}
}

/**
 * @param  array<A> $arr
 */
function callFoo(array $arr) : void {
    array_pop($arr)->foo(); // MixedMethodCall emitted here
}

callFoo(
    [new A()]
);

Alternatively you could add a runtime check:

php
<?php

class A {
    public function foo() : void {}
}

function callFoo(array $arr) : void {
    $a = array_pop($arr);
    assert($a instanceof A);
    $a->foo(); // MixedMethodCall emitted here
}

callFoo(
    [new A()]
);

Running Psalm Issues MixedOperand (`docs/running_psalm/issues/MixedOperand.md`)

MixedOperand

Emitted when Psalm cannot infer a type for an operand in any calculated expression

php
<?php

echo $GLOBALS['foo'] + "hello";

Why it’s bad

Mixed operands can have fatal consequences, e.g. here:

php
<?php

function foo(mixed $m) {
    echo $m . 'bar';
}

class A {}

foo(new A()); // triggers fatal error

Running Psalm Issues MixedPropertyAssignment (`docs/running_psalm/issues/MixedPropertyAssignment.md`)

MixedPropertyAssignment

Emitted when assigning a property to a value for which Psalm cannot infer a type

php
<?php

/** @param mixed $a */
function foo($a) : void {
    $a->foo = "bar";
}

Running Psalm Issues MixedPropertyFetch (`docs/running_psalm/issues/MixedPropertyFetch.md`)

MixedPropertyFetch

Emitted when retrieving a property on a value for which Psalm cannot infer a type

php
<?php

/** @param mixed $a */
function foo($a) : void {
    echo $a->foo;
}

Running Psalm Issues MixedPropertyTypeCoercion (`docs/running_psalm/issues/MixedPropertyTypeCoercion.md`)

MixedPropertyTypeCoercion

Emitted when Psalm cannot be sure that part of an array/iterable argument's type constraints can be fulfilled

php
<?php

class A {
    /** @var string[] */
    public $takesStringArray = [];
}

function foo(A $a, array $arr) : void {
    $a->takesStringArray = $arr;
}

Running Psalm Issues MixedReturnStatement (`docs/running_psalm/issues/MixedReturnStatement.md`)

MixedReturnStatement

Emitted when Psalm cannot determine the type of a given return statement

php
<?php

function foo() : int {
    return $GLOBALS['foo']; // emitted here
}

Running Psalm Issues MixedReturnTypeCoercion (`docs/running_psalm/issues/MixedReturnTypeCoercion.md`)

MixedReturnTypeCoercion

Emitted when Psalm cannot be sure that part of an array/iterable return type's constraints can be fulfilled

php
<?php

/**
 * @return string[]
 */
function foo(array $a) : array {
    return $a;
}

This can happen with variadic arguments when @no-named-arguments is not specified:

php
<?php

/** @return list<int> */
function foo(int ...$args): array {
    return $args; // $args is array<array-key, int> since it can have named arguments
}

Running Psalm Issues MixedStringOffsetAssignment (`docs/running_psalm/issues/MixedStringOffsetAssignment.md`)

MixedStringOffsetAssignment

Emitted when assigning a value on a string using a value for which Psalm cannot infer a type

php
<?php

"hello"[0] = $GLOBALS['foo'];

Running Psalm Issues MoreSpecificImplementedParamType (`docs/running_psalm/issues/MoreSpecificImplementedParamType.md`)

MoreSpecificImplementedParamType

Emitted when a class implements an interface method but a param type is more specific than the interface method param type

php
<?php

class A {}
class B extends A {
    public function bar(): void {}
}
class C extends A {
    public function bar(): void {}
}

class D {
    public function foo(A $a): void {}
}

class E extends D {
    /** @param B|C $a */
    public function foo(A $a): void {
        $a->bar();
    }
}

Running Psalm Issues MoreSpecificReturnType (`docs/running_psalm/issues/MoreSpecificReturnType.md`)

MoreSpecificReturnType

Emitted when the declared return type for a method is more specific than the inferred one (emitted in the same methods that LessSpecificReturnStatement is)

php
<?php

class A {}
class B extends A {}
function foo() : B {
    /** @psalm-suppress LessSpecificReturnStatement */
    return new A();
}

Running Psalm Issues MutableDependency (`docs/running_psalm/issues/MutableDependency.md`)

MutableDependency

Emitted when an immutable class inherits from a class or trait not marked @psalm-immutable

php
<?php

class MutableParent {
    public int $i = 0;

    public function increment() : void {
        $this->i++;
    }
}

/**
 * @psalm-immutable
 */
final class NotReallyImmutableClass extends MutableParent {}

Running Psalm Issues NamedArgumentNotAllowed (`docs/running_psalm/issues/NamedArgumentNotAllowed.md`)

NamedArgumentNotAllowed

Emitted when a named argument is used when calling a function with @no-named-arguments.

php
<?php

/** @no-named-arguments */
function foo(int $a, int $b): int {
	return $a + $b;
}

foo(a: 0, b: 1);

Why this is bad

The @no-named-arguments annotation indicates that argument names may be changed in the future, and an update may break backwards compatibility with function calls using named arguments.

How to fix

Avoid using named arguments for functions annotated with @no-named-arguments.

php
<?php

/** @no-named-arguments */
function foo(int $a, int $b): int {
	return $a + $b;
}

foo(0, 1);

Running Psalm Issues NoEnumProperties (`docs/running_psalm/issues/NoEnumProperties.md`)

NoEnumProperties

Emitted when there a property defined on an enum, as PHP
does not allow user-defined properties on enums.

php
<?php

enum Status {
    public $prop;
}

Running Psalm Issues NoInterfaceProperties (`docs/running_psalm/issues/NoInterfaceProperties.md`)

NoInterfaceProperties

Emitted when trying to fetch a property on an interface as interfaces, by definition, do not have definitions for properties.

php
<?php

interface I {}
class A implements I {
    /** @var ?string */
    public $foo;
}
function bar(I $i) : void {
    if ($i->foo) {}
}

Running Psalm Issues NoValue (`docs/running_psalm/issues/NoValue.md`)

NoValue

Emitted when Psalm invalidated all possible types for a given expression. It is often an indication that Psalm found dead code or that documented types were not exhaustive

php
<?php

/**
 * @return never
 */
function foo() : void {
    exit();
}

$a = foo(); // Psalm knows $a will never contain any type because foo() won't return
php
<?php

function foo() : void {
    return throw new Exception(''); //Psalm detected the return expression is never used
}
php
<?php
function shutdown(): never {die('Application closed unexpectedly');}
function foo(string $_a): void{}

foo(shutdown()); // foo() will never be called
php
<?php
$a = [];
/** @psalm-suppress TypeDoesNotContainType */
assert(!empty($a));

count($a); // Assert above always fail. There is no possible type that $a can have here

Running Psalm Issues NonInvariantDocblockPropertyType (`docs/running_psalm/issues/NonInvariantDocblockPropertyType.md`)

NonInvariantDocblockPropertyType

Emitted when a public or protected class property has a different docblock type to the matching property in the parent class.

php
<?php

class A {
    /** @var null|string */
    public $foo = 'hello';
}

class AChild extends A {
    /** @var string */
    public $foo;
}

Why this is bad

For non-typed properties, it can cause a type system violation when code written against the parent class reads or writes a value on an object of the child class.

If the child class widens the type then reading the value may return unexpected value that client code cannot deal with. If the child class narrows the type then code setting the value may set it to an invalid value:

php
<?php

function takesA(A $a) {
    $a->foo = null; // this is valid for A
}

$child = new AChild();
takesA($child);
echo strlen($child->foo); // this is valid for AChild

Workarounds

You can either broaden the type or you could, in certain situations, use templates instead:

php
<?php

/**
 * @template T of string|null
 */
abstract class A {
    /** @var T */
    public $foo = 'hello';
}

/**
 * @extends A<string>
 */
class AChild extends A {
    /** @var string */
    public $foo;
}

Running Psalm Issues NonInvariantPropertyType (`docs/running_psalm/issues/NonInvariantPropertyType.md`)

NonInvariantPropertyType

Emitted when a public or protected class property has a different type to the matching property in the parent class.

php
<?php

class A {
    public string $foo = 'hello';
}

class B extends A {
    public ?string $foo;
}

Why this is bad

For typed properties, this can cause a compile error.


Running Psalm Issues NonStaticSelfCall (`docs/running_psalm/issues/NonStaticSelfCall.md`)

NonStaticSelfCall

Emitted when calling a non-static function statically

php
<?php

class A {
    public function foo(): void {}

    public static function bar(): void {
        self::foo();
    }
}

Running Psalm Issues NonVariableReferenceReturn (`docs/running_psalm/issues/NonVariableReferenceReturn.md`)

NonVariableReferenceReturn

Emitted when a function returns by reference expression that is not a variable

php
<?php

function &getByRef(): int {
    return 5;
}

Running Psalm Issues NullArgument (`docs/running_psalm/issues/NullArgument.md`)

NullArgument

Emitted when calling a function with a null value argument when the function does not expect it

php
<?php

function foo(string $s) : void {}
foo(null);

Running Psalm Issues NullArrayAccess (`docs/running_psalm/issues/NullArrayAccess.md`)

NullArrayAccess

Emitted when trying to access an array value on null

php
<?php

$arr = null;
echo $arr[0];

Running Psalm Issues NullArrayOffset (`docs/running_psalm/issues/NullArrayOffset.md`)

NullArrayOffset

Emitted when trying to access an array offset with null

php
<?php

$arr = ['' => 5, 'foo' => 1];
echo $arr[null];

Running Psalm Issues NullFunctionCall (`docs/running_psalm/issues/NullFunctionCall.md`)

NullFunctionCall

Emitted when trying to use null as a callable

php
<?php

$arr = null;
echo $arr();

Running Psalm Issues NullIterator (`docs/running_psalm/issues/NullIterator.md`)

NullIterator

Emitted when iterating over null

php
<?php

foreach (null as $a) {}

Running Psalm Issues NullOperand (`docs/running_psalm/issues/NullOperand.md`)

NullOperand

Emitted when using null as part of an operation (e.g. +, ., ^ etc.)

php
<?php

echo null . 'hello';

Running Psalm Issues NullPropertyAssignment (`docs/running_psalm/issues/NullPropertyAssignment.md`)

NullPropertyAssignment

Emitted when trying to set a property on null

php
<?php

$a = null;
$a->foo = "bar";

Running Psalm Issues NullPropertyFetch (`docs/running_psalm/issues/NullPropertyFetch.md`)

NullPropertyFetch

Emitted when trying to fetch a property on a null value

php
<?php

$a = null;
echo $a->foo;

Running Psalm Issues NullReference (`docs/running_psalm/issues/NullReference.md`)

NullReference

Emitted when attempting to call a method on null

php
<?php

$a = null;
$a->foo();

Running Psalm Issues NullableReturnStatement (`docs/running_psalm/issues/NullableReturnStatement.md`)

NullableReturnStatement

Emitted if a return statement contains a null value, but the function return type is not nullable

php
<?php

function foo() : string {
    if (rand(0, 1)) {
        return "foo";
    }

    return null; // emitted here
}

Running Psalm Issues OverriddenFinalConstant (`docs/running_psalm/issues/OverriddenFinalConstant.md`)

OverriddenFinalConstant

Emitted when a constant declared as final is overridden in a child class or interface.

php
<?php

class Foo
{
    /** @var string */
    final public const BAR='baz';
}

class Bar extends Foo
{
    /** @var string */
    public const BAR='foobar';
}

Running Psalm Issues OverriddenFinalProperty (`docs/running_psalm/issues/OverriddenFinalProperty.md`)

OverriddenFinalProperty

Emitted when a property declared as final is redeclared in a child class. Properties declared with private(set)
asymmetric visibility are implicitly final.

php
<?php

class Foo
{
    public private(set) string $bar = 'baz';
}

class Bar extends Foo
{
    public string $bar = 'foobar';
}

Running Psalm Issues OverriddenInterfaceConstant (`docs/running_psalm/issues/OverriddenInterfaceConstant.md`)

OverriddenInterfaceConstant

Emitted when a constant declared on an interface is overridden by a child (illegal in PHP < 8.1).

php
<?php

interface Foo
{
    public const BAR='baz';
}

interface Bar extends Foo
{
    public const BAR='foobar';
}

Running Psalm Issues OverriddenMethodAccess (`docs/running_psalm/issues/OverriddenMethodAccess.md`)

OverriddenMethodAccess

Emitted when a method is less accessible than its parent

php
<?php

class A {
    public function foo() : void {}
}
class B extends A {
    protected function foo() : void {}
}

Running Psalm Issues OverriddenPropertyAccess (`docs/running_psalm/issues/OverriddenPropertyAccess.md`)

OverriddenPropertyAccess

Emitted when a property is less accessible than the same-named property in its parent class

php
<?php

class A {
    /** @var string|null */
    public $foo;
}
class B extends A {
    /** @var string|null */
    protected $foo;
}

Running Psalm Issues ParadoxicalCondition (`docs/running_psalm/issues/ParadoxicalCondition.md`)

ParadoxicalCondition

Emitted when a paradox is encountered in your programs logic that could not be caught by RedundantCondition

php
<?php

function foo(string $input) : string {
    return $input === "a" ? "bar" : ($input === "a" ? "foo" : "b");
}

Running Psalm Issues ParamNameMismatch (`docs/running_psalm/issues/ParamNameMismatch.md`)

ParamNameMismatch

Emitted when method overrides a parent method but renames a param.

php
<?php

class A {
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $string, bool $b = false) : void {}
}

Why is this bad?

PHP 8 introduces named parameters which allow developers to call methods with explicitly-named parameters;

php
<?php

function callFoo(A $a) {
    $a->foo(str: "hello");
}

In the first example passing new AChild() to callFoo() results in a fatal error, as AChild's definition of the method foo() doesn't have a parameter named $str.

How to fix

You can change the child method param name to match:

php
<?php

class A {
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $str, bool $b = false) : void {}
}

This fix can be applied automatically by Psalter.

Workarounds

@no-named-arguments

Alternatively you can ignore this issue by adding a @no-named-arguments annotation to the parent method:

php
<?php

class A {
    /** @no-named-arguments */
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $string, bool $b = false) : void {}
}

Any method with this annotation will be prevented (by Psalm) from being called with named parameters, so the original issue does not matter.

Config allowNamedArgumentCalls="false"

This prevents any use of named params in your codebase. Ideal for self-contained projects, but less ideal for libraries.

It means the original code above will not emit any errors as long as the class A is defined in a directory that Psalm can scan.

Config allowInternalNamedArgumentCalls="false"

For library authors Psalm supports a more nuanced flag that tells Psalm to prohibit any named parameter calls on @internal classes or methods.

With that config value, this is now allowed:

php
<?php

/**
 * @internal
 */
class A {
    public function foo(string $str, bool $b = false) : void {}
}

class AChild extends A {
    public function foo(string $string, bool $b = false) : void {}
}

Running Psalm Issues ParentNotFound (`docs/running_psalm/issues/ParentNotFound.md`)

ParentNotFound

Emitted when using parent:: in a class without a parent class.

php
<?php

class A {
  public function foo() : void {
    parent::foo();
  }
}

Running Psalm Issues ParseError (`docs/running_psalm/issues/ParseError.md`)

ParseError

Emitted when the PHP Parser encounters an error.

php
<?php

class A {
  public function foo() : void {
    echo "foo"
  }
}

Running Psalm Issues PossibleRawObjectIteration (`docs/running_psalm/issues/PossibleRawObjectIteration.md`)

PossibleRawObjectIteration

Emitted when possibly iterating over an object’s properties, the comparison to RawObjectIteration.

php
<?php

class A {
    /** @var string|null */
    public $foo;

    /** @var string|null */
    public $bar;
}

function takesA(A $a) {
    if (rand(0, 1)) {
        $a = [1, 2, 3];
    }

    foreach ($a as $property) {}
}

Running Psalm Issues PossiblyFalseArgument (`docs/running_psalm/issues/PossiblyFalseArgument.md`)

PossiblyFalseArgument

Emitted when a function argument is possibly false, but the function does not expect false. This is distinct from a function argument is possibly bool, which results in PossiblyInvalidArgument.

php
<?php

function foo(string $s) : void {
    $a_pos = strpos($s, "a");
    echo substr($s, $a_pos);
}

Running Psalm Issues PossiblyFalseIterator (`docs/running_psalm/issues/PossiblyFalseIterator.md`)

PossiblyFalseIterator

Emitted when trying to iterate over a value that may be false

php
<?php

$arr = rand(0, 1) ? [1, 2, 3] : false;
foreach ($arr as $a) {}

Running Psalm Issues PossiblyFalseOperand (`docs/running_psalm/issues/PossiblyFalseOperand.md`)

PossiblyFalseOperand

Emitted when using a possibly false value as part of an operation (e.g. +, ., ^ etc).

php
<?php

function echoCommaPosition(string $str) : void {
    echo 'The comma is located at ' . strpos($str, ','); 
}

How to fix

You can detect the false value with some extra logic:

php
<?php

function echoCommaPosition(string $str) : void {
    $pos = strpos($str, ',');

    if ($pos === false) {
        echo 'There is no comma in the string';
    }

    echo 'The comma is located at ' . $pos; 
}

Alternatively you can just use a ternary to suppress this issue:

php
<?php

function echoCommaPosition(string $str) : void {
    echo 'The comma is located at ' . (strpos($str, ',') ?: ''); 
}

Running Psalm Issues PossiblyFalsePropertyAssignmentValue (`docs/running_psalm/issues/PossiblyFalsePropertyAssignmentValue.md`)

PossiblyFalsePropertyAssignmentValue

Emitted when trying to assign a value that may be false to a property that only takes non-false values.

php
<?php

class A {
    /** @var int */
    public $foo = 0;
}

function assignToA(string $s) {
    $a = new A();
    $a->foo = strpos($s, "haystack");
}

Running Psalm Issues PossiblyFalseReference (`docs/running_psalm/issues/PossiblyFalseReference.md`)

PossiblyFalseReference

Emitted when making a method call on a value that might be false

php
<?php

class A {
    public function bar() : void {}
}

/** @return A|false */
function foo() {
    return rand(0, 1) ? new A : false;
}

foo()->bar();

Running Psalm Issues PossiblyInvalidArgument (`docs/running_psalm/issues/PossiblyInvalidArgument.md`)

PossiblyInvalidArgument

Emitted when

php
<?php

/** @return int|stdClass */
function foo() {
    return rand(0, 1) ? 5 : new stdClass;
}
function bar(int $i) : void {}
bar(foo());

Running Psalm Issues PossiblyInvalidArrayAccess (`docs/running_psalm/issues/PossiblyInvalidArrayAccess.md`)

PossiblyInvalidArrayAccess

Emitted when attempting to access an array offset on a value that may not be an array

php
<?php

$arr = rand(0, 1) ? 5 : [4, 3, 2, 1];
echo $arr[0];

Running Psalm Issues PossiblyInvalidArrayAssignment (`docs/running_psalm/issues/PossiblyInvalidArrayAssignment.md`)

PossiblyInvalidArrayAssignment

Emitted when attempting to assign an array offset on a value that may not be an array

php
<?php

$arr = rand(0, 1) ? 5 : [4, 3, 2, 1];
$arr[0] = "hello";

Running Psalm Issues PossiblyInvalidArrayOffset (`docs/running_psalm/issues/PossiblyInvalidArrayOffset.md`)

PossiblyInvalidArrayOffset

Emitted when it’s possible that the array offset is not applicable to the value you’re trying to access.

php
<?php

$arr = rand(0, 1) ? ["a" => 5] : "hello";
echo $arr[0];

Running Psalm Issues PossiblyInvalidCast (`docs/running_psalm/issues/PossiblyInvalidCast.md`)

PossiblyInvalidCast

Emitted when attempting to cast a value that may not be castable

php
<?php

class A {}
class B {
    public function __toString() {
        return 'hello';
    }
}
$c = (string) (rand(0, 1) ? new A() : new B());

Running Psalm Issues PossiblyInvalidClone (`docs/running_psalm/issues/PossiblyInvalidClone.md`)

PossiblyInvalidClone

Emitted when trying to clone a value that's possibly not cloneable

php
<?php

class A {}

/**
 * @param A|string $a
 */
function foo($a) {
    return clone $a;
}

Running Psalm Issues PossiblyInvalidDocblockTag (`docs/running_psalm/issues/PossiblyInvalidDocblockTag.md`)

PossiblyInvalidDocblockTag

Emitted when Psalm detects a likely mix-up of docblock tags, e.g. @var
used on a method (where @param is likely expected).

php
<?php

/** @var int $param */
function foo($param): void {}

Running Psalm Issues PossiblyInvalidFunctionCall (`docs/running_psalm/issues/PossiblyInvalidFunctionCall.md`)

PossiblyInvalidFunctionCall

Emitted when trying to call a function on a value that may not be callable

php
<?php

$a = rand(0, 1) ? 5 : function() : int { return 5; };
$b = $a();

Running Psalm Issues PossiblyInvalidIterator (`docs/running_psalm/issues/PossiblyInvalidIterator.md`)

PossiblyInvalidIterator

Emitted when trying to iterate over a value that may be invalid

php
<?php

$arr = rand(0, 1) ? [1, 2, 3] : "hello";
foreach ($arr as $a) {}

Running Psalm Issues PossiblyInvalidMethodCall (`docs/running_psalm/issues/PossiblyInvalidMethodCall.md`)

PossiblyInvalidMethodCall

Emitted when trying to call a method on a value that may not be an object

php
<?php

class A {
    public function bar() : void {}
}

/** @return A|int */
function foo() {
    return rand(0, 1) ? new A : 5;
}

foo()->bar();

Running Psalm Issues PossiblyInvalidOperand (`docs/running_psalm/issues/PossiblyInvalidOperand.md`)

PossiblyInvalidOperand

Emitted when using a possibly invalid value as part of an operation (e.g. +, ., ^ etc.

php
<?php

function foo() : void {
    $b = rand(0, 1) ? [] : 4;
    echo $b + 5;
}

Running Psalm Issues PossiblyInvalidPropertyAssignment (`docs/running_psalm/issues/PossiblyInvalidPropertyAssignment.md`)

PossiblyInvalidPropertyAssignment

Emitted when trying to assign a property on a value that may not be an object or may be an object that does not have the desired property.

php
<?php

class A {
    /** @var ?string */
    public $bar;
}

/** @return A|int */
function foo() {
    return rand(0, 1) ? new A : 5;
}

$a = foo();
$a->bar = "5";

Running Psalm Issues PossiblyInvalidPropertyAssignmentValue (`docs/running_psalm/issues/PossiblyInvalidPropertyAssignmentValue.md`)

PossiblyInvalidPropertyAssignmentValue

Emitted when trying to assign a possibly invalid value to a typed property.

php
<?php

class A {
    /** @var int[] */
    public $bb = [];
}

class B {
    /** @var string[] */
    public $bb;
}

$c = rand(0, 1) ? new A : new B;
$c->bb = ["hello", "world"];

Running Psalm Issues PossiblyInvalidPropertyFetch (`docs/running_psalm/issues/PossiblyInvalidPropertyFetch.md`)

PossiblyInvalidPropertyFetch

Emitted when trying to fetch a property on a value that may not be an object or may be an object that does not have the desired property.

php
<?php

class A {
    /** @var ?string */
    public $bar;
}

/** @return A|int */
function foo() {
    return rand(0, 1) ? new A : 5;
}

$a = foo();
echo $a->bar;

Running Psalm Issues PossiblyNullArgument (`docs/running_psalm/issues/PossiblyNullArgument.md`)

PossiblyNullArgument

Emitted when calling a function with a value that’s possibly null when the function does not expect it

php
<?php

function foo(string $s): void {}
foo(rand(0, 1) ? "hello" : null);

Common Problem Cases

Using a Function Call inside `if`

php
<?php

if (is_string($cat->getName()) {
    foo($cat->getName());
}

This fails since it's not guaranteed that subsequent calls to $cat->getName() always give the same result.

Possible Solutions

Use a variable:

php
<?php

$catName = $cat->getName();
if (is_string($catName) {
    foo($catName);
}
unset($catName);

Or add @psalm-capabilities read-props to the declaration of the function

Calling Another Function After `if`

php
<?php

if (is_string($cat->getName()) {
    changeCat();
    foo($cat->getName());
}

This fails since psalm cannot know if changeCat() does actually modify $cat.

Possible Solutions


Running Psalm Issues PossiblyNullArrayAccess (`docs/running_psalm/issues/PossiblyNullArrayAccess.md`)

PossiblyNullArrayAccess

Emitted when trying to access an array offset on a possibly null value

php
<?php

function foo(?array $a) : void {
    echo $a[0];
}

Running Psalm Issues PossiblyNullArrayAssignment (`docs/running_psalm/issues/PossiblyNullArrayAssignment.md`)

PossiblyNullArrayAssignment

Emitted when trying to set a value on a possibly null array

php
<?php

$a = null;
$a[0][] = 1;

Running Psalm Issues PossiblyNullArrayOffset (`docs/running_psalm/issues/PossiblyNullArrayOffset.md`)

PossiblyNullArrayOffset

Emitted when trying to access a value on an array using a possibly null offset

php
<?php

function foo(?int $a) : void {
    echo [1, 2, 3, 4][$a];
}

Running Psalm Issues PossiblyNullFunctionCall (`docs/running_psalm/issues/PossiblyNullFunctionCall.md`)

PossiblyNullFunctionCall

Emitted when trying to call a function on a value that may be null

php
<?php

function foo(?callable $a) : void {
    $a();
}

Running Psalm Issues PossiblyNullIterator (`docs/running_psalm/issues/PossiblyNullIterator.md`)

PossiblyNullIterator

Emitted when trying to iterate over a value that may be null

php
<?php

function foo(?array $arr) : void {
    foreach ($arr as $a) {}
}

Running Psalm Issues PossiblyNullOperand (`docs/running_psalm/issues/PossiblyNullOperand.md`)

PossiblyNullOperand

Emitted when using a possibly null value as part of an operation (e.g. +, ., ^ etc.)

php
<?php

function foo(?int $a) : void {
    echo $a + 5;
}

Running Psalm Issues PossiblyNullPropertyAssignment (`docs/running_psalm/issues/PossiblyNullPropertyAssignment.md`)

PossiblyNullPropertyAssignment

Emitted when trying to assign a property to a possibly null object

php
<?php

class A {
    /** @var ?string */
    public $foo;
}
function foo(?A $a) : void {
    $a->foo = "bar";
}

Running Psalm Issues PossiblyNullPropertyAssignmentValue (`docs/running_psalm/issues/PossiblyNullPropertyAssignmentValue.md`)

PossiblyNullPropertyAssignmentValue

Emitted when trying to assign a value that may be null to a property that only takes non-null values.

php
<?php

class A {
    /** @var string */
    public $foo = "bar";
}

function assignToA(?string $s) {
    $a = new A();
    $a->foo = $s;
}

Running Psalm Issues PossiblyNullPropertyFetch (`docs/running_psalm/issues/PossiblyNullPropertyFetch.md`)

PossiblyNullPropertyFetch

Emitted when trying to fetch a property on a possibly null object

php
<?php

class A {
    /** @var ?string */
    public $foo;
}
function foo(?A $a) : void {
    echo $a->foo;
}

Running Psalm Issues PossiblyNullReference (`docs/running_psalm/issues/PossiblyNullReference.md`)

PossiblyNullReference

Emitted when trying to call a method on a possibly null value

php
<?php

class A {
    public function bar() : void {}
}
function foo(?A $a) : void {
    $a->bar();
}

Running Psalm Issues PossiblyUndefinedArrayOffset (`docs/running_psalm/issues/PossiblyUndefinedArrayOffset.md`)

PossiblyUndefinedArrayOffset

Emitted when trying to access a possibly undefined array offset

php
<?php

if (rand(0, 1)) {
    $arr = ["a" => 1, "b" => 2];
} else {
    $arr = ["a" => 3];
}

echo $arr["b"];

How to fix

You can use the null coalesce operator to provide a default value in the event the array offset does not exist:

php
<?php

...

echo $arr["b"] ?? 0;

Alternatively, you can ensure that the array offset always exists:

php
<?php

if (rand(0, 1)) {
    $arr = ["a" => 1, "b" => 2];
} else {
    $arr = ["a" => 3, "b" => 0];
}

echo $arr["b"];

Running Psalm Issues PossiblyUndefinedGlobalVariable (`docs/running_psalm/issues/PossiblyUndefinedGlobalVariable.md`)

PossiblyUndefinedGlobalVariable

Emitted when trying to access a variable in the global scope that may not be defined

php
<?php

if (rand(0, 1)) {
  $a = 5;
}
echo $a;

Running Psalm Issues PossiblyUndefinedIntArrayOffset (`docs/running_psalm/issues/PossiblyUndefinedIntArrayOffset.md`)

PossiblyUndefinedIntArrayOffset

Emitted when the config flag ensureArrayIntOffsetsExist is set to true and an integer-keyed offset is not checked for existence

php
<?php

/**
 * @param array<int, string> $arr
 */
function foo(array $arr) : void {
    echo $arr[0];
}

Running Psalm Issues PossiblyUndefinedMethod (`docs/running_psalm/issues/PossiblyUndefinedMethod.md`)

PossiblyUndefinedMethod

Emitted when trying to access a method that may not be defined on the object

php
<?php

class A {
    public function bar() : void {}
}
class B {}

$a = rand(0, 1) ? new A : new B;
$a->bar();

Running Psalm Issues PossiblyUndefinedStringArrayOffset (`docs/running_psalm/issues/PossiblyUndefinedStringArrayOffset.md`)

PossiblyUndefinedStringArrayOffset

Emitted when the config flag ensureArrayStringOffsetsExist is set to true and a string-keyed offset is not checked for existence

php
<?php

/**
 * @param array<string, string> $arr
 */
function foo(array $arr) : void {
    echo $arr["hello"];
}

Running Psalm Issues PossiblyUndefinedVariable (`docs/running_psalm/issues/PossiblyUndefinedVariable.md`)

PossiblyUndefinedVariable

Emitted when trying to access a variable in function scope that may not be defined

php
<?php

function foo() : void {
    if (rand(0, 1)) {
        $a = 5;
    }
    echo $a;
}

Running Psalm Issues PossiblyUnusedMethod (`docs/running_psalm/issues/PossiblyUnusedMethod.md`)

PossiblyUnusedMethod

Emitted when --find-dead-code is turned on and Psalm cannot find any calls to
a public or protected method.

If this method is genuinely part of your public API β€” used from outside the analysed codebase (for example a library entry point, or a method invoked only by a framework) β€” annotate the containing class with @api (or @psalm-api) β€” mark the whole class rather than the individual method, since a public-API method almost always belongs to a public-API class. Otherwise the method is dead code and should be removed: a method reported as unused in a class that is not already marked @api is almost always genuinely unused, so annotating the individual method with @api to silence the report is very rarely the right fix.

See @api for the full guidance; note that @psalm-suppress is not a substitute for @api, as it silences the report without marking the method as used.

php
<?php

class A {
    public function foo() : void {}
    public function bar() : void {}
}
(new A)->foo();

Running Psalm Issues PossiblyUnusedParam (`docs/running_psalm/issues/PossiblyUnusedParam.md`)

PossiblyUnusedParam

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a particular parameter in a public/protected method

php
<?php

class A {
    public function foo(int $a, int $b) : int {
        return $a + 4;
    }
}

$a = new A();
echo $a->foo(1, 2);

Can be suppressed by prefixing the parameter name with an underscore:

php
<?php
class A {
    public function foo(int $a, int $_b) : int {
        return $a + 4;
    }
}

Running Psalm Issues PossiblyUnusedProperty (`docs/running_psalm/issues/PossiblyUnusedProperty.md`)

PossiblyUnusedProperty

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a
particular public/protected property.

If this property is genuinely part of your public API β€” used from outside the analysed codebase (for example a library entry point, or a property accessed only by a framework) β€” annotate the containing class with @api (or @psalm-api) β€” mark the whole class rather than the individual property, since a public-API property almost always belongs to a public-API class. Otherwise the property is dead code and should be removed: a property reported as unused in a class that is not already marked @api is almost always genuinely unused, so annotating the individual property with @api to silence the report is very rarely the right fix.

See @api for the full guidance; note that @psalm-suppress is not a substitute for @api, as it silences the report without marking the property as used.

php
<?php

class A {
    /** @var string|null */
    public $foo;

    /** @var int|null */
    public $bar;
}

$a = new A();
echo $a->foo;

Running Psalm Issues PossiblyUnusedReturnValue (`docs/running_psalm/issues/PossiblyUnusedReturnValue.md`)

PossiblyUnusedReturnValue

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a public/protected method’s return type.

php
<?php

class A {
    public function foo() : string {
        return "hello";
    }
}
(new A)->foo();

Running Psalm Issues PrivateFinalMethod (`docs/running_psalm/issues/PrivateFinalMethod.md`)

PrivateFinalMethod

Emitted when a class defines private final method. PHP 8.0+ emits a warning when it sees private final method (except __construct where it's allowed), and allows redefinition in child classes (effectively ignoring final modifier). Before PHP 8.0, final was respected in this case.

php
<?php
class Foo {
    final private function baz(): void {}
}

Why this is bad

It causes a warning, and behavior differs between versions.


Running Psalm Issues PropertyNotSetInConstructor (`docs/running_psalm/issues/PropertyNotSetInConstructor.md`)

PropertyNotSetInConstructor

Uninitialized properties are hard to statically analyze. To prevent mistakes, Psalm will enforce that all properties should be initialized.

It does that through MissingConstructor and this issue.

Psalm will then assume every property in the codebase is initialized.

Doing that allows it to report missing initializations as well as RedundantPropertyInitializationCheck

This issue is emitted when a non-null property without a default value is declared but not set in the class’s constructor

If your project relies on having uninitialized properties, it is advised to suppress this issue, as well as MissingConstructor and RedundantPropertyInitializationCheck.

php
<?php

class A {
    /** @var string */
    public $foo;

    public function __construct() {}
}

Running Psalm Issues PropertyTypeCoercion (`docs/running_psalm/issues/PropertyTypeCoercion.md`)

PropertyTypeCoercion

Emitted when setting a property with a value which has a less specific type than the property expects

php
<?php

class A {}
class B extends A {}

function takesA(C $c, A $a) : void {
    $c->b = $a;
}

class C {
    /** @var ?B */
    public $b;
}

Running Psalm Issues RawObjectIteration (`docs/running_psalm/issues/RawObjectIteration.md`)

RawObjectIteration

Emitted when iterating over an object’s properties. This issue exists because it may be undesired behaviour (e.g. you may have meant to iterate over an array)

php
<?php

class A {
    /** @var string|null */
    public $foo;

    /** @var string|null */
    public $bar;
}

function takesA(A $a) {
    foreach ($a as $property) {}
}

Running Psalm Issues RedundantCast (`docs/running_psalm/issues/RedundantCast.md`)

RedundantCast

Emitted when a cast (string, int, float etc.) is redundant

php
<?php
function foo(string $s) : string {
    return (string) $s;
}

Running Psalm Issues RedundantCastGivenDocblockType (`docs/running_psalm/issues/RedundantCastGivenDocblockType.md`)

RedundantCastGivenDocblockType

Emitted when a cast (string, int, float etc.) is redundant given the docblock-provided type

php
<?php
/**
 * @param  string $s
 */
function foo($s) : string {
    return (string) $s;
}

Running Psalm Issues RedundantCondition (`docs/running_psalm/issues/RedundantCondition.md`)

RedundantCondition

Emitted when conditional is redundant given previous assertions

php
<?php

class A {}
function foo(A $a) : ?A {
    if ($a) return $a;
    return null;
}

Running Psalm Issues RedundantConditionGivenDocblockType (`docs/running_psalm/issues/RedundantConditionGivenDocblockType.md`)

RedundantConditionGivenDocblockType

Emitted when conditional is redundant given information supplied in one or more docblocks.

This may be desired (e.g. when checking user input) so is distinct from RedundantCondition, which only applies to non-docblock types.

php
<?php

/**
 * @param string $s
 *
 * @return void
 */
function foo($s) {
    if (is_string($s)) {};
}

Running Psalm Issues RedundantFlag (`docs/running_psalm/issues/RedundantFlag.md`)

RedundantFlag

Emitted when a flag is redundant. e.g. FILTER_NULL_ON_FAILURE won't do anything when the default option is specified

php
<?php
$x = filter_input(INPUT_GET, 'hello', FILTER_VALIDATE_DOMAIN, array('options' => array('default' => 'world.com'), 'flags' => FILTER_NULL_ON_FAILURE));

Running Psalm Issues RedundantFunctionCall (`docs/running_psalm/issues/RedundantFunctionCall.md`)

RedundantFunctionCall

Emitted when a function call (array_values, strtolower, ksort etc.) is redundant.

php
<?php

$a = ['already', 'a', 'list'];

$redundant = array_values($a);
$alreadyLower = strtolower($redundant[0]);

Running Psalm Issues RedundantFunctionCallGivenDocblockType (`docs/running_psalm/issues/RedundantFunctionCallGivenDocblockType.md`)

RedundantFunctionCallGivenDocblockType

Emitted when function call is redundant given information supplied in one or more docblocks.

This may be desired (e.g. when checking user input) so is distinct from RedundantFunctionCall, which only applies to non-docblock types.

php
<?php

/**
 * @param array{0: lowercase-string, 1: non-empty-list<lowercase-string>} $s
 *
 * @return lowercase-string
 */
function foo($s): string {
    $redundantList = array_values($s);
    $redundantSubList = array_values($s[1]);
    $redundantLowercase = strtolower($redundantSubList[0]);
    return $redundantLowercase;
}

Running Psalm Issues RedundantIdentityWithTrue (`docs/running_psalm/issues/RedundantIdentityWithTrue.md`)

RedundantIdentityWithTrue

This is a codestyle rule, it does not indicate a possible bug.

Emitted when comparing a known boolean with with a literal boolean and the allowBoolToLiteralBoolComparison flag is set to false.

php
<?php

function returnsABool(): bool {
    return rand(1, 2) === 1;
}

if (returnsABool() === true) {
    echo "hi!";
}

if (returnsABool() !== false) {
    echo "hi!";
}

To fix:

php
<?php

function returnsABool(): bool {
    return rand(1, 2) === 1;
}

if (returnsABool()) {
    echo "hi!";
}

if (returnsABool()) {
    echo "hi!";
}

Running Psalm Issues RedundantPropertyInitializationCheck (`docs/running_psalm/issues/RedundantPropertyInitializationCheck.md`)

RedundantPropertyInitializationCheck

Uninitialized properties are hard to statically analyze. To prevent mistakes, Psalm will enforce that all properties should be initialized.

It does that through PropertyNotSetInConstructor and MissingConstructor.

Psalm will then assume every property in the codebase is initialized.

Doing that allows it to report missing initializations as well as this issue.

This issue is emitted when checking isset() on a non-nullable property. Because every property is assumed to be initialized, this check is redundant

If your project relies on having uninitialized properties, it is advised to suppress this issue, as well as PropertyNotSetInConstructor and MissingConstructor.

php
<?php
    class A {
        public string $bar;
        public function getBar() : string {
            if (isset($this->bar)) {
                return $this->bar;
            }
            return "hello";
        }
    }

Running Psalm Issues ReferenceConstraintViolation (`docs/running_psalm/issues/ReferenceConstraintViolation.md`)

ReferenceConstraintViolation

Emitted when changing the type of a pass-by-reference variable

php
<?php

function foo(string &$a) {
    $a = 5;
}

Running Psalm Issues ReferenceReusedFromConfusingScope (`docs/running_psalm/issues/ReferenceReusedFromConfusingScope.md`)

ReferenceReusedFromConfusingScope

Emitted when a reference assigned in a potentially confusing scope is reused later.
Since PHP doesn't scope loops and ifs the same way most other languages do, a common issue is the reuse of a variable
declared in such a scope. Usually it doesn't matter, because a reassignment to an already-defined variable will just
reassign it, but if the already-defined variable is a reference it will change the value of the referred to variable.

php
<?php

$arr = [1, 2, 3];
foreach ($arr as &$i) {
    ++$i;
}

// ...more code, after which the usage of $i as a reference has been forgotten by the programmer

for ($i = 0; $i < 10; ++$i) {
    echo $i;
}
// $arr is now [2, 3, 10] instead of the expected [2, 3, 4]

To fix the issue, unset the reference after using it in such a scope:

php
<?php

$arr = [1, 2, 3];
foreach ($arr as &$i) {
    ++$i;
}
unset($i);

for ($i = 0; $i < 10; ++$i) {
    echo $i;
}
// $arr is correctly [2, 3, 4]

Running Psalm Issues ReservedWord (`docs/running_psalm/issues/ReservedWord.md`)

ReservedWord

Emitted when using a reserved word as a class name

php
<?php

function foo(resource $res) : void {}

Running Psalm Issues RiskyCast (`docs/running_psalm/issues/RiskyCast.md`)

RiskyCast

Emitted when attempting to cast an array to int or float

php
<?php

$foo = (int) array( 'hello' );

Why this is bad

The value resulting from the cast depends on if the array is empty or not and can easily lead to off-by-one errors

How to fix

Don't cast arrays to int or float.


Running Psalm Issues RiskyTruthyFalsyComparison (`docs/running_psalm/issues/RiskyTruthyFalsyComparison.md`)

RiskyTruthyFalsyComparison

Emitted when comparing a value with multiple types, where at least one type can be only truthy or falsy and other types can contain both truthy and falsy values.

php
<?php

/**
 * @param array|null $arg
 * @return void
 */
function foo($arg) {
    if ($arg) {
        // this is risky, bc the empty array and null case are handled together
    }
    
    if (!$arg) {
        // this is risky, bc the empty array and null case are handled together  
    }
}

Why this is bad

The truthy/falsy type of a variable is often forgotten and not handled explicitly causing hard to track down errors.

How to fix

Explicitly validate the variable with strict comparison.


Running Psalm Issues StringIncrement (`docs/running_psalm/issues/StringIncrement.md`)

StringIncrement

Emitted when incrementing a non-numeric string ($a = "a"; $a++; gives "b"). This works in PHP,
but is unexpected behaviour for most people, and it is deprecated as of PHP 8.5: use
str_increment() (available since
PHP 8.3) instead. The issue is emitted whatever the PHP version the code is analysed for.

php
<?php

$a = "hello";
$a++;

How to fix

php
<?php

$a = "hello";
$a = str_increment($a);

Running Psalm Issues TaintedCallable (`docs/running_psalm/issues/TaintedCallable.md`)

TaintedCallable

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when tainted text is used in an arbitrary function call.

This can lead to dangerous situations, like running arbitrary functions.

php
<?php

$name = $_GET["name"];

evalCode($name);

function evalCode(string $name) {
    if (is_callable($name)) {
        $name();
    }
}

Running Psalm Issues TaintedCookie (`docs/running_psalm/issues/TaintedCookie.md`)

TaintedCookie

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Potential cookie injection. This rule is emitted when user-controlled input can be passed into a cookie.

Risk

The risk of setting arbitrary cookies depends on further application configuration.

Examples of potential issues:

  • Session Fixation: If the authentication cookie doesn't change after a successful login an attacker could fixate the session cookie. If a victim logs in with a fixated cookie, the attacker can now take over the session of the user.
  • Cross-Site-Scripting (XSS): Some application code could read cookies and print it out unsanitized to the user.

Example

php
<?php

setcookie('authtoken', $_GET['value'], time() + (86400 * 30), '/');

Mitigations

If this is required functionality, limit the cookie setting to values and not the name. (e.g. authtoken in the example)

Make sure to change session tokens after authentication attempts.

Further resources


Running Psalm Issues TaintedCustom (`docs/running_psalm/issues/TaintedCustom.md`)

TaintedCustom

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when tainted input detection is turned on and custom-tainted data is detected.


Running Psalm Issues TaintedEval (`docs/running_psalm/issues/TaintedEval.md`)

TaintedEval

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into an eval call.

Passing untrusted user input to eval calls is dangerous, as it allows arbitrary data to be executed on your server.

php
<?php

$name = $_GET["name"];

evalCode($name);

function evalCode(string $name) {
    eval($name);
}

Running Psalm Issues TaintedExtract (`docs/running_psalm/issues/TaintedExtract.md`)

TaintedExtract

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled array can be passed into an extract call.

php
<?php

$array = $_GET;
extract($array);

Running Psalm Issues TaintedFile (`docs/running_psalm/issues/TaintedFile.md`)

TaintedFile

This is a security issue, reported by security analysis: it flags a potential vulnerability.

This rule is emitted when user-controlled input can be passed into a sensitive file operation.

Risk

The risk here depends on the actual operation that contains user-controlled input, and how it is later on processed.

It could range from:

  • Creating files
    • Example: file_put_contents
    • Risk: Depending on the server configuration this may result in remote code execution. (e.g. writing a file in the web root)
  • Modifying files
    • Example: file_put_contents
    • Risk: Depending on the server configuration this may result in remote code execution. (e.g. modifying a PHP file)
  • Reading files
    • Example: file_get_contents
    • Risk: Sensitive data could be exposed from the filesystem. (e.g. config values, source code, user-submitted files)
  • Deleting files
    • Example: unlink
    • Risk: Denial of Service or potentially RCE. (e.g. deleting application code, removing a .htaccess file)

curl opens the files some of its options name too. CURLOPT_COOKIEJAR writes the cookies the server sends into the file it names, so a server can put PHP code in a file of the attacker's choosing. A CURLFile uploads the file it names to the server.

Example

php
<?php

unlink($_GET['path']);

Mitigations

Use an allowlist approach where possible to verify names on file operations.

Sanitize user-controlled filenames by stripping .., \ and /.

Further resources


Running Psalm Issues TaintedHeader (`docs/running_psalm/issues/TaintedHeader.md`)

TaintedHeader

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Potential header injection. This rule is emitted when user-controlled input can be passed into an HTTP header.

Risk

The risk of a header injection depends hugely on your environment.

If your webserver supports something like XSendFile / X-Accel, an attacker could potentially access arbitrary files on the systems.

If your system does not do that, there may be other concerns, such as:

  • Cookie Injection
  • Open Redirects
  • Proxy Cache Poisoning

The headers of a request the application makes itself are sinks too. Unlike header(), curl writes the value of an option such as CURLOPT_HTTPHEADER, CURLOPT_USERAGENT, CURLOPT_COOKIE or CURLOPT_CUSTOMREQUEST into the request as is. A line break in it adds headers to the request, for example another Host or Authorization header. Given to CURLOPT_CUSTOMREQUEST or CURLOPT_REQUEST_TARGET, it adds a whole other request to the connection. CURLOPT_QUOTE sends the FTP or SFTP commands it's given as they are, and CURLOPT_COOKIELIST adds cookies to the requests.

Example

php
<?php

header($_GET['header']);
php
<?php

$ch = curl_init('https://api.example.com/');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Locale: ' . $_GET['locale']]);
curl_exec($ch);

Mitigations

Make sure only the value and not the key can be set by an attacker. (e.g. header('Location: ' . $_GET['target']);)

Verify the set values are sensible. Consider using an allow list. (e.g. for redirections)

For the headers of a request made with curl, reject values containing \r or \n, and annotate the function doing so with @psalm-taint-escape header.

Further resources


Running Psalm Issues TaintedHtml (`docs/running_psalm/issues/TaintedHtml.md`)

TaintedHtml

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input that can contain HTML can be passed into an echo statement.

Risk

This could lead to a potential Cross Site Scripting (XSS) vulnerability. Using an XSS vulnerability, an attacker could inject malicious JavaScript and execute any action JavaScript could do. Examples include:

  • Stealing authentication material (e.g. cookies, JWT tokens)
  • Exfiltrate sensitive information by reading the DOM
  • Keylog entries on the website (e.g. fake login form)

Whether this is exploitable or not depends on a few conditions:

  • Is an executable mimetype set? (e.g. text/html)
  • Is the content served inline or as attachment? (Content-Disposition)
  • Is the output properly sanitized? (e.g. stripping all HTML tags or having an allowlist of allowed characters)

Example

php
<?php

$name = $_GET["name"];

printName($name);

function printName(string $name) {
    echo $name;
}

Mitigations

  • Sanitize user-input by using functions such as htmlentities or use an allowlist.
  • Set all cookies to HTTPOnly.
  • Consider using Content Security Policy (CSP), to limit the risk of XSS vulnerabilities.
  • If user input itself is HTML, see Sanitizing HTML User Input

Further resources


Running Psalm Issues TaintedInclude (`docs/running_psalm/issues/TaintedInclude.md`)

TaintedInclude

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into an include or require expression.

Passing untrusted user input to include calls is dangerous, as it can allow an attacker to execute arbitrary scripts on your server.

php
<?php

$name = $_GET["name"];

includeCode($name);

function includeCode(string $name) : void {
    include($name . '.php');
}

Running Psalm Issues TaintedInput (`docs/running_psalm/issues/TaintedInput.md`)

TaintedInput

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when tainted input detection is turned on


Running Psalm Issues TaintedLdap (`docs/running_psalm/issues/TaintedLdap.md`)

TaintedLdap

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Potential LDAP injection. This rule is emitted when user-controlled input can be passed into an LDAP request.

Risk

Passing untrusted user input to LDAP requests could be dangerous.

If LDAP requests like these are used for login purposes, it could result in an authentication bypass. An attacker could write a filter that would evaluate to true for any user, and thus bruteforce credentials easily.

Example

php
<?php

$ds = ldap_connect('example.com');
$dn = 'o=Psalm, c=US';
$filter = $_GET['filter'];
ldap_search($ds, $dn, $filter, []);

Mitigations

Use ldap_escape to escape user input to the LDAP filter and DN.

Further resources


Running Psalm Issues TaintedLlmPrompt (`docs/running_psalm/issues/TaintedLlmPrompt.md`)

TaintedLlmPrompt

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into an LLM prompt, risking prompt injection.

php
<?php

class LlmAgent {
    /** @psalm-taint-sink llm_prompt $prompt */
    public function prompt(string $prompt): string {
        return "";
    }
}

$agent = new LlmAgent();
$agent->prompt((string) $_GET["question"]);

Running Psalm Issues TaintedNosql (`docs/running_psalm/issues/TaintedNosql.md`)

TaintedNosql

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into a NoSQL query (e.g. a MongoDB filter or command).

Unlike SQL injection, NoSQL injection does not depend on string concatenation. It happens
when an attacker controls the structure of a query document β€” typically by supplying an
array/object instead of a scalar. For example, a request like ?username[$ne]= causes
$_GET['username'] to be the array ['$ne' => ''], which turns an equality match into a
"not equal" match and can bypass authentication.

Because of this, only values that can hold an array (or object) can carry the nosql taint β€”
a scalar can never be a NoSQL query, so casting user input to string, int, float or
bool removes the taint.

A string decoded back into an array or an object can be a query again, though: what
json_decode(), unserialize(), parse_str(), parse_ini_string() and the other decoders
build out of user input carries the nosql taint, whatever the type of the string was.

php
<?php

function getUser(MongoDB\Driver\Manager $manager): array {
    // $_GET["username"] may be an array such as ["$ne" => null]
    $filter = ["username" => $_GET["username"]];
    $query = new MongoDB\Driver\Query($filter);

    return $manager->executeQuery("db.users", $query)->toArray();
}
php
<?php

function findUsers(string $json): MongoDB\Driver\Query {
    // {"username": {"$ne": null}} decodes to a query operator
    return new MongoDB\Driver\Query(json_decode($json, true));
}

findUsers((string) $_POST["filter"]);

Safe alternatives

Cast user input to a scalar so it can only ever be a literal value, never a query operator:

php
<?php

function getUser(): MongoDB\Driver\Query {
    // (string) forces a literal match; the nosql taint is removed.
    // Casting to (int)/(float)/(bool) works the same way for numeric/boolean fields.
    return new MongoDB\Driver\Query(["username" => (string) $_GET["username"]]);
}

Or route the filter through a sanitizer annotated with @psalm-taint-escape nosql:

php
<?php

/**
 * Forces every filter value to a scalar so an attacker cannot inject query
 * operators such as ["$ne" => null] through array-valued input.
 *
 * @param array<string, mixed> $filter
 * @return array<string, string>
 * @psalm-taint-escape nosql
 */
function sanitize_mongo_filter(array $filter): array {
    $safe = [];
    foreach ($filter as $field => $value) {
        if (is_array($value)) {
            throw new InvalidArgumentException("Filter values must be scalar");
        }
        $safe[$field] = (string) $value;
    }
    return $safe;
}

function getUser(): MongoDB\Driver\Query {
    $filter = sanitize_mongo_filter(["username" => $_GET["username"]]);
    return new MongoDB\Driver\Query($filter);
}

Running Psalm Issues TaintedSSRF (`docs/running_psalm/issues/TaintedSSRF.md`)

TaintedSSRF

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Potential Server-Side Request Forgery vulnerability. This rule is emitted when user-controlled input can be passed into a network request.

Risk

Passing untrusted user input to network requests could be dangerous.

If an attacker can fully control an HTTP request they could connect to internal services. Depending on the nature of these, this can pose a security risk. (e.g. backend services, admin interfaces, AWS metadata, ...)

Besides the URL, the curl options choosing where a request goes or the protocol it uses are reported: a proxy (CURLOPT_PROXY), or the hosts not to use it for (CURLOPT_NOPROXY), the addresses a name resolves to (CURLOPT_RESOLVE), the protocol of a URL without a scheme (CURLOPT_DEFAULT_PROTOCOL, which can make it read a local file), the protocols a redirect may use (CURLOPT_REDIR_PROTOCOLS_STR), ...

Example

php
<?php
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $_GET['url']);

curl_exec($ch);

Mitigations

Mitigating SSRF vulnerabilities can be tricky. Disallowing IPs would likely not work as an attacker could create a malicious domain that points to an internal DNS name.

Consider:

  1. Having an allow list of domains that can be connected to.
  2. Pointing cURL to a proxy that has no access to internal resources.

Further resources


Running Psalm Issues TaintedShell (`docs/running_psalm/issues/TaintedShell.md`)

TaintedShell

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into an exec call or similar.

php
<?php

$command = $_GET["command"];

runCode($command);

function runCode(string $command) {
    exec($command);
}

Running Psalm Issues TaintedSleep (`docs/running_psalm/issues/TaintedSleep.md`)

TaintedSleep

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into a sleep call or similar.

php
<?php

sleep($_GET["seconds"]);

The options making curl throttle a transfer (CURLOPT_MAX_RECV_SPEED_LARGE, ...) or wait longer for it (CURLOPT_TIMEOUT, ...) are reported too: a client choosing them can keep the request busy as long as it wants.


Running Psalm Issues TaintedSql (`docs/running_psalm/issues/TaintedSql.md`)

TaintedSql

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into a SQL command.

php
<?php

class A {
    public function deleteUser(PDO $pdo) : void {
        $userId = self::getUserId();
        $pdo->exec("delete from users where user_id = " . $userId);
    }

    public static function getUserId() : string {
        return (string) $_GET["user_id"];
    }
}

Running Psalm Issues TaintedSystemSecret (`docs/running_psalm/issues/TaintedSystemSecret.md`)

TaintedSystemSecret

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when data marked as a system secret is detected somewhere it shouldn’t be.

php
<?php

/**
 * @psalm-taint-source system_secret
 */
function getConfigValue(string $data) {
    return "$omePa$$word";
}

echo getConfigValue("secret");

Running Psalm Issues TaintedTextWithQuotes (`docs/running_psalm/issues/TaintedTextWithQuotes.md`)

TaintedTextWithQuotes

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input that can contain quotation marks can be passed into an echo statement.

Risk

This could lead to a potential Cross Site Scripting (XSS) vulnerability. Using an XSS vulnerability, an attacker could inject malicious JavaScript and execute any action JavaScript could do. Examples include:

  • Stealing authentication material (e.g. cookies, JWT tokens)
  • Exfiltrate sensitive information by reading the DOM
  • Keylog entries on the website (e.g. fake login form)

Whether this is exploitable or not depends on a few conditions:

  • Is an executable mimetype set? (e.g. text/html)
  • Is the content served inline or as attachment? (Content-Disposition)
  • Is the output properly sanitized? (e.g. stripping all HTML tags or having an allowlist of allowed characters)

Example

php
<?php
$param = strip_tags($_GET['param']);
?>

<script>
    console.log('<?=$param?>')
</script>

Passing ');alert('injection');// as a GET param here would cause the alert to trigger.

Mitigations

  • Sanitize user input by using functions such as htmlentities with the ENT_QUOTES flag or use an allowlist.
  • Set all cookies to HTTPOnly.
  • Consider using Content Security Policy (CSP), to limit the risk of XSS vulnerabilities.
  • If user input itself is HTML, see Sanitizing HTML User Input

Further resources


Running Psalm Issues TaintedUnserialize (`docs/running_psalm/issues/TaintedUnserialize.md`)

TaintedUnserialize

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Tainted input detected to an unserialize call.

Passing untrusted user input to unserialize calls is dangerous – from the PHP documentation on unserialize:

Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. Use a safe, standard data interchange format such as JSON (via json_decode() and json_encode()) if you need to pass serialized data to the user.

php
<?php

$command = $_GET["data"];

getObject($command);

function getObject(string $data) : object {
    return unserialize($data);
}

Running Psalm Issues TaintedUrlComponent (`docs/running_psalm/issues/TaintedUrlComponent.md`)

TaintedUrlComponent

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input is placed, without being URL-encoded, in the path, the query or the fragment of a URL whose server is fixed, and the URL is then requested.

The input can't choose the server the request is sent to (that would be a TaintedSSRF), but it can still change the request in ways it shouldn't, with URL syntax: ../ to reach another path of the server, & to add query parameters or override earlier ones, # to cut off the rest of the URL.

Example

php
<?php
$query = (string) $_GET['query'];

// with $query = "a&admin=1", this sends ?q=a&admin=1
file_get_contents('https://api.example.com/search?q=' . $query);

Mitigations

Encode the input with rawurlencode() or urlencode(), or build the query string with http_build_query():

php
<?php
$query = (string) $_GET['query'];

file_get_contents('https://api.example.com/search?q=' . rawurlencode($query));
file_get_contents('https://api.example.com/search?' . http_build_query(['q' => $query]));

In the path of the URL, an encoded value can still be a .. segment: see TaintedUrlPath.

Further resources


Running Psalm Issues TaintedUrlPath (`docs/running_psalm/issues/TaintedUrlPath.md`)

TaintedUrlPath

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input is placed in the path of a URL whose server is fixed, and the URL is then requested, even once URL-encoded: URL encoding leaves . as it is, so the input can still be a .. segment of the path.

HTTP clients such as cURL resolve these segments before sending the request, so the input can reach another path of the server than the one intended (client-side path traversal).

Example

php
<?php
$id = rawurlencode((string) $_GET['id']);

// with $id = "..", this requests https://api.example.com/profile
file_get_contents('https://api.example.com/users/' . $id . '/profile');

Mitigations

Only accept the values the path segment can have, e.g. cast an identifier to int or format it with sprintf('%d'), or validate it in a function annotated with @psalm-taint-escape url_path.

Values placed in the query or the fragment of the URL (after a ? or a #) can't change its path, and aren't reported.

Further resources


Running Psalm Issues TaintedUserSecret (`docs/running_psalm/issues/TaintedUserSecret.md`)

TaintedUserSecret

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when tainted input detection is turned on and data marked as a user secret is detected somewhere it shouldn’t be.

php
<?php

class User {
    /**
     * @psalm-taint-source user_secret
     */
    public function getPassword() : string {
        return "$omePa$$word";
    }
}

function showUserPassword(User $user) {
    echo $user->getPassword();
}

Running Psalm Issues TaintedXpath (`docs/running_psalm/issues/TaintedXpath.md`)

TaintedXpath

This is a security issue, reported by security analysis: it flags a potential vulnerability.

Emitted when user-controlled input can be passed into a xpath query.

php
<?php

function queryExpression(SimpleXMLElement $xml) : array|false|null {
    $expression = $_GET["expression"];
    return $xml->xpath($expression);
}

Running Psalm Issues TooFewArguments (`docs/running_psalm/issues/TooFewArguments.md`)

TooFewArguments

Emitted when calling a function with fewer arguments than the function has parameters

php
<?php

function foo(string $a) : void {}
foo();

Running Psalm Issues TooManyArguments (`docs/running_psalm/issues/TooManyArguments.md`)

TooManyArguments

Emitted when calling a function with more arguments than the function has parameters

php
<?php

function foo(string $a) : void {}
foo("hello", 4);

Running Psalm Issues TooManyTemplateParams (`docs/running_psalm/issues/TooManyTemplateParams.md`)

TooManyTemplateParams

Emitted when using the @extends/@implements annotation to extend a class and adds too
many types.

php
<?php

/**
 * @template-implements IteratorAggregate<int, string, int>
 */
class SomeIterator implements IteratorAggregate
{
    public function getIterator() {
        yield 5;
    }
}

Running Psalm Issues Trace (`docs/running_psalm/issues/Trace.md`)

Trace

Not really an issue. Just reports the type of a variable when using
@psalm-trace.

php
<?php

/** @psalm-trace $x */
$x = getmypid();

How to fix

Use it for debugging purposes, not for production


Running Psalm Issues TraitMethodSignatureMismatch (`docs/running_psalm/issues/TraitMethodSignatureMismatch.md`)

TraitMethodSignatureMismatch

Emitted when a method's signature or return type differs from corresponding trait-defined method

php
<?php

trait T {
    abstract public function foo(int $i);
}

class A {
    use T;

    public function foo(string $s) : void {}
}

Running Psalm Issues TypeDoesNotContainNull (`docs/running_psalm/issues/TypeDoesNotContainNull.md`)

TypeDoesNotContainNull

Emitted when checking a non-nullable type for null

php
<?php

$a = "hello";
if ($a === null) {}

Running Psalm Issues TypeDoesNotContainType (`docs/running_psalm/issues/TypeDoesNotContainType.md`)

TypeDoesNotContainType

Emitted checking whether one value has a type or value that is impossible given its currently-known type

php
<?php

$a = "hello";
if ($a === 5) {}

Running Psalm Issues UncaughtThrowInGlobalScope (`docs/running_psalm/issues/UncaughtThrowInGlobalScope.md`)

UncaughtThrowInGlobalScope

Emitted when a possible exception isn't caught in global scope

php
<?php

/**
 * @throws \Exception
 */
function foo() : int {
    return random_int(0, 1);
}
foo();

Running Psalm Issues UndefinedAttributeClass (`docs/running_psalm/issues/UndefinedAttributeClass.md`)

UndefinedAttributeClass

Emitted when referencing an attribute class that does not exist

php
<?php
use Foo\Bar\Pure;

#[Pure]
class Video {}

Running Psalm Issues UndefinedClass (`docs/running_psalm/issues/UndefinedClass.md`)

UndefinedClass

Emitted when referencing a class that does not exist

php
<?php

$a = new A();

Is also emitted when using extension classes not added to the composer.json requirements or to the enableExtensions config key.


Running Psalm Issues UndefinedConstant (`docs/running_psalm/issues/UndefinedConstant.md`)

UndefinedConstant

Emitted when referencing a constant that does not exist

php
<?php

echo FOO_BAR;

Is also emitted when using extension constants not added to the composer.json requirements or to the enableExtensions config key.


Running Psalm Issues UndefinedDocblockClass (`docs/running_psalm/issues/UndefinedDocblockClass.md`)

UndefinedDocblockClass

Emitted when referencing a class that does not exist from a docblock

php
<?php

/**
 * @param DoesNotExist $a
 */
function foo($a) : void {}

Running Psalm Issues UndefinedFunction (`docs/running_psalm/issues/UndefinedFunction.md`)

UndefinedFunction

Emitted when referencing a function that doesn't exist

php
<?php

foo();

Is also emitted when using extension functions not added to the composer.json requirements or to the enableExtensions config key.


Running Psalm Issues UndefinedGlobalVariable (`docs/running_psalm/issues/UndefinedGlobalVariable.md`)

UndefinedGlobalVariable

Emitted when referencing a variable that doesn't exist

php
<?php

echo $a;

Running Psalm Issues UndefinedInterface (`docs/running_psalm/issues/UndefinedInterface.md`)

UndefinedInterface

Emitted when referencing an interface that does not exist but does have an identically-named class.

php
<?php

class C {}

interface I extends C {}

Running Psalm Issues UndefinedInterfaceMethod (`docs/running_psalm/issues/UndefinedInterfaceMethod.md`)

UndefinedInterfaceMethod

Emitted when calling a method that does not exist on an interface

php
<?php

interface I {}

function foo(I $i) {
    $i->bar();
}

Running Psalm Issues UndefinedMagicMethod (`docs/running_psalm/issues/UndefinedMagicMethod.md`)

UndefinedMagicMethod

Emitted when calling a magic method that does not exist

php
<?php

/**
 * @method bar():string
 */
class A {
    public function __call(string $name, array $args) {
        return "cool";
    }
}
(new A)->foo();

To fix, add all used magic methods as @method annotations:

php
<?php

/**
 * @method bar():string
 * @method foo():string
 */
class A {
    public function __call(string $name, array $args) {
        return "cool";
    }
}
(new A)->foo();

Or, only if dealing with generic wrapper objects (like FFI classes), use @psalm-no-seal-methods.
Try to avoid using @psalm-no-seal-methods, as it worsens type coverage, and is not needed in the vast majority of cases.


Running Psalm Issues UndefinedMagicPropertyAssignment (`docs/running_psalm/issues/UndefinedMagicPropertyAssignment.md`)

UndefinedMagicPropertyAssignment

Emitted when assigning a property on an object that does not have that magic property defined

php
<?php

/**
 * @property string $bar
 */
class A {
    /** @param mixed $value */
    public function __set(string $name, $value) {}
}
$a = new A();
$a->foo = "bar";

To fix, add all used magic properties as @property annotations:

php
<?php

/**
 * @property string $bar
 * @property string $foo
 */
class A {
    /** @param mixed $value */
    public function __set(string $name, $value) {}
}
$a = new A();
$a->foo = "bar";

Or, only if dealing with generic container objects (like ArrayObject), use @psalm-no-seal-properties.

php
<?php

/** @psalm-no-seal-properties */
class ArrayBag {
    public function __construct(private array $arr = []) {}
    public function __set(string $k, mixed $v) {
        $this->arr[$k] = $v;
    }
    public function __get(string $k): mixed {
        return $this->arr[$k];
    }
}
$a = new ArrayBag(['bar' => 'foo']);
$a->foo = "bar";

Running Psalm Issues UndefinedMagicPropertyFetch (`docs/running_psalm/issues/UndefinedMagicPropertyFetch.md`)

UndefinedMagicPropertyFetch

Emitted when getting a property on an object that does not have that magic property defined

php
<?php

/**
 * @property string $bar
 */
class A {
    public function __get(string $name) {
        return "cool";
    }
}
$a = new A();
echo $a->foo;

To fix, add all used magic properties as @property annotations:

php
<?php

/**
 * @property string $bar
 * @property string $foo
 */
class A {
    /** @param mixed $value */
    public function __set(string $name, $value) {}
}
$a = new A();
echo $a->foo;

Or, only if dealing with generic container objects (like ArrayObject), use @psalm-no-seal-properties.

php
<?php

/** @psalm-no-seal-properties */
class ArrayBag {
    public function __construct(private array $arr = []) {}
    public function __set(string $k, mixed $v) {
        $this->arr[$k] = $v;
    }
    public function __get(string $k): mixed {
        return $this->arr[$k];
    }
}
$a = new ArrayBag(['bar' => 'foo']);
echo $a->bar;

Running Psalm Issues UndefinedMethod (`docs/running_psalm/issues/UndefinedMethod.md`)

UndefinedMethod

Emitted when calling a method that does not exist

php
<?php

class A {}
A::foo();

Running Psalm Issues UndefinedPropertyAssignment (`docs/running_psalm/issues/UndefinedPropertyAssignment.md`)

UndefinedPropertyAssignment

Emitted when assigning a property on an object that does not have that property defined

php
<?php

class A {}
$a = new A();
$a->foo = "bar";

Running Psalm Issues UndefinedPropertyFetch (`docs/running_psalm/issues/UndefinedPropertyFetch.md`)

UndefinedPropertyFetch

Emitted when getting a property on an object that does not have that property defined

php
<?php

class A {}
$a = new A();
echo $a->foo;

Running Psalm Issues UndefinedThisPropertyAssignment (`docs/running_psalm/issues/UndefinedThisPropertyAssignment.md`)

UndefinedThisPropertyAssignment

Emitted when assigning a property on an object in one of that object’s methods when no such property exists

php
<?php

class A {
    function foo() {
        $this->foo = "bar";
    }
}

Running Psalm Issues UndefinedThisPropertyFetch (`docs/running_psalm/issues/UndefinedThisPropertyFetch.md`)

UndefinedThisPropertyFetch

Emitted when getting a property for an object in one of that object’s methods when no such property exists

php
<?php

class A {
    function foo() {
        echo $this->foo;
    }
}

Running Psalm Issues UndefinedTrace (`docs/running_psalm/issues/UndefinedTrace.md`)

UndefinedTrace

Attempt to trace an undefined variable

php
<?php

/** @psalm-trace $x */
echo 'Hello World!';

How to fix

Provide existing variable or remove it


Running Psalm Issues UndefinedTrait (`docs/running_psalm/issues/UndefinedTrait.md`)

UndefinedTrait

Emitted when referencing a trait that does not exist

php
<?php

class A {
    use T;
}

Running Psalm Issues UndefinedVariable (`docs/running_psalm/issues/UndefinedVariable.md`)

UndefinedVariable

Emitted when referencing a variable that doesn't exist in a given function’s scope

php
<?php

function foo() {
    echo $a;
}

Running Psalm Issues UnevaluatedCode (`docs/running_psalm/issues/UnevaluatedCode.md`)

UnevaluatedCode

Emitted when --find-dead-code is turned on and Psalm encounters code that will not be evaluated

php
<?php

function foo() : void {
    return;
    $a = "foo";
}

Running Psalm Issues UnhandledMatchCondition (`docs/running_psalm/issues/UnhandledMatchCondition.md`)

UnhandledMatchCondition

Emitted when a match expression does not handle one or more options.

php
<?php

function matchOne(): string {
    $foo = rand(0, 1) ? "foo" : "bar";

    return match ($foo) {
        'foo' => 'foo',
    };
}

Why this is bad

The above code will fail 50% of the time with an UnhandledMatchError error.


Running Psalm Issues UnimplementedAbstractMethod (`docs/running_psalm/issues/UnimplementedAbstractMethod.md`)

UnimplementedAbstractMethod

Emitted when a class extends another, but does not implement all of its abstract methods

php
<?php

abstract class A {
    abstract public function foo() : void;
}
class B extends A {}

Running Psalm Issues UnimplementedInterfaceMethod (`docs/running_psalm/issues/UnimplementedInterfaceMethod.md`)

UnimplementedInterfaceMethod

Emitted when a class implements an interface but does not implement all of its methods

php
<?php

interface I {
    public function foo() : void;
}
class A implements I {}

Running Psalm Issues UninitializedProperty (`docs/running_psalm/issues/UninitializedProperty.md`)

UninitializedProperty

Emitted when a property is used in a constructor before it is initialized

php
<?php

class A {
    /** @var string */
    public $foo;

    public function __construct() {
        echo strlen($this->foo);
        $this->foo = "foo";
    }
}

Running Psalm Issues UnnecessaryVarAnnotation (`docs/running_psalm/issues/UnnecessaryVarAnnotation.md`)

UnnecessaryVarAnnotation

Emitted when --find-dead-code is turned on and you're using a @var annotation on an assignment that Psalm has already identified a type for.

php
<?php

function foo() : string {
    return "hello";
}

/** @var string */
$a = foo();

Running Psalm Issues UnrecognizedExpression (`docs/running_psalm/issues/UnrecognizedExpression.md`)

UnrecognizedExpression

Emitted when Psalm encounters an expression that it doesn't know how to handle. This should never happen.


Running Psalm Issues UnrecognizedStatement (`docs/running_psalm/issues/UnrecognizedStatement.md`)

UnrecognizedStatement

Emitted when Psalm encounters a code construct that it doesn't know how to handle. This should never happen.


Running Psalm Issues UnresolvableConstant (`docs/running_psalm/issues/UnresolvableConstant.md`)

UnresolvableConstant

Emitted when Psalm cannot resolve a constant used in key-of or value-of. Note that static::CONST is considered
unresolvable for key-of and value-of, since the literal keys and values can't be resolved due to the possibility
of being overridden by child classes.

php
<?php

class Foo
{
    public const BAR = ['bar'];

    /**
     * @return value-of<self::BAT>
     */
    public function bar(): string
    {
        return self::BAR[0];
    }
}

Running Psalm Issues UnresolvableInclude (`docs/running_psalm/issues/UnresolvableInclude.md`)

UnresolvableInclude

Emitted when Psalm cannot figure out what specific file is being included/required by PHP.

php
<?php

function requireFile(string $s) : void {
    require_once $s;
}

Running Psalm Issues UnsafeGenericInstantiation (`docs/running_psalm/issues/UnsafeGenericInstantiation.md`)

UnsafeGenericInstantiation

Emitted when an attempt is made to instantiate a class using new static without a constructor that's final:

php
<?php

/**
 * @template T
 * @psalm-consistent-constructor
 */
class Container {
    /**
     * @var T
     */
    public $t;

    /**
     * @param T $t
     */
    public function __construct($t) {
        $this->t = $t;
    }

    /**
     * @template U
     * @param U $u
     * @return static<U>
     */
    public function getInstance($u) : static
    {
        return new static($u);
    }
}

What’s wrong here?

The problem comes when extending the class:

php
<?php

/**
 * @template T
 * @psalm-consistent-constructor
 */
class Container {
    /**
     * @var T
     */
    public $t;

    /**
     * @param T $t
     */
    public function __construct($t) {
        $this->t = $t;
    }

    /**
     * @template U
     * @param U $u
     * @return static<U>
     */
    public function getInstance($u) : static
    {
        return new static($u);
    }
}

/**
 * @extends Container<string>
 */
class StringContainer extends Container {}

$c = StringContainer::getInstance(new stdClass());
// creates StringContainer<stdClass>, clearly invalid

How to fix

Either use new self instead of new static:

php
<?php

/**
 * @template T
 * @psalm-consistent-constructor
 */
class Container {
    /**
     * @var T
     */
    public $t;

    /**
     * @param T $t
     */
    public function __construct($t) {
        $this->t = $t;
    }

    /**
     * @template U
     * @param U $u
     * @return self<U>
     */
    public function getInstance($u) : self
    {
        return new self($u);
    }
}

Or you can add a @psalm-consistent-templates annotation which ensures that any child class has the same generic params:

php
<?php

/**
 * @template T
 * @psalm-consistent-constructor
 * @psalm-consistent-templates
 */
class Container {
    /**
     * @var T
     */
    public $t;

    /**
     * @param T $t
     */
    public function __construct($t) {
        $this->t = $t;
    }

    /**
     * @template U
     * @param U $u
     * @return static<U>
     */
    public function getInstance($u) : static
    {
        return new static($u);
    }
}

/**
 * @template T
 * @psalm-extends Container<T>
 */
class LazyLoadingContainer extends Container {}

A purity template (@psalm-purity-template) only makes new static() unsafe when the constructor's
parameters take it (Closure[C]): the arguments cannot infer the others, only the child classes bind them.


Running Psalm Issues UnsafeInstantiation (`docs/running_psalm/issues/UnsafeInstantiation.md`)

UnsafeInstantiation

Emitted when an attempt is made to instantiate a class using new static or similar without a constructor that's final:

php
<?php

class A {
    public function getInstance() : self
    {
        return new static();
    }
}

What’s wrong here?

The problem comes when extending the class:

php
<?php

class A {
    public function getInstance() : self
    {
        return new static();
    }
}

class AChild extends A {
    public function __construct(string $some_required_param) {}
}

AChild::getInstance(); // fatal error

How to fix

You have two options – you can make the constructor final:

php
<?php

class A {
    final public function __construct() {}

    public function getInstance() : self
    {
        return new static();
    }
}

Or you can add a @psalm-consistent-constructor annotation which ensures that any constructor in a child class has the same signature as the parent constructor:

php
<?php

/**
 * @psalm-consistent-constructor
 */
class A {
    public function getInstance() : self
    {
        return new static();
    }
}

class AChild extends A {
    public function __construct() {
        // this is fine
    }
}

class BadAChild extends A {
    public function __construct(string $s) {
        // this is reported as a violation
    }
}

Running Psalm Issues UnsupportedPropertyReferenceUsage (`docs/running_psalm/issues/UnsupportedPropertyReferenceUsage.md`)

UnsupportedPropertyReferenceUsage

Psalm cannot guarantee the soundness of code that uses references to properties.

Examples of Uncaught Errors

  • Instance property assigned wrong type:
php
<?php
class A {
    public int $b = 0;
}
$a = new A();
$b = &$a->b;
$b = ''; // Fatal error
  • Static property assigned wrong type:
php
<?php
class A {
    public static int $b = 0;
}
$b = &A::$b;
$b = ''; // Fatal error
  • Readonly property reassigned:
php
<?php
class A {
    public function __construct(
        public readonly int $b,
    ) {
    }
}
$a = new A(0);
$b = &$a->b;
$b = 1; // Fatal error

Running Psalm Issues UnsupportedReferenceUsage (`docs/running_psalm/issues/UnsupportedReferenceUsage.md`)

UnsupportedReferenceUsage

Emitted when Psalm encounters a reference that it is not currently able to track (for instance a reference to an array
offset of an array offset: $foo = &$bar[$baz[0]]). When an unsupported reference is encountered, Psalm will issue this
warning and treat the variable as though it wasn't actually a reference.

How to fix

This can sometimes be fixed by using a temporary variable:

php
<?php

/** @var non-empty-list<int> */
$bar = [1, 2, 3];
/** @var non-empty-list<int> */
$baz = [1, 2, 3];

$foo = &$bar[$baz[0]];

can be turned into

php
<?php

/** @var non-empty-list<int> */
$bar = [1, 2, 3];
/** @var non-empty-list<int> */
$baz = [1, 2, 3];

$offset = $baz[0];
$foo = &$bar[$offset];

Running Psalm Issues UnusedBaselineEntry (`docs/running_psalm/issues/UnusedBaselineEntry.md`)

UnusedBaselineEntry

Emitted when a baseline entry is not being used to suppress an issue.

Enabled by findUnusedBaselineEntry

php
<?php
$a = 'Hello, World!';
echo $a;
xml
<?xml version="1.0" encoding="UTF-8"?>
<files>
    <file src="example.php">
        <UnusedVariable>
            
            <code>$a</code>
        </UnusedVariable>
    </file>
</files>

Running Psalm Issues UnusedClass (`docs/running_psalm/issues/UnusedClass.md`)

UnusedClass

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a
given class.

If this class is genuinely part of your public API β€” used from outside the analysed codebase (for example a library entry point, or a class instantiated only by a framework or plugin loader) β€” annotate it with @api (or @psalm-api); use this only for real public surface. Otherwise the class is dead code and should be removed.

See @api for the full guidance; note that @psalm-suppress is not a substitute for @api, as it silences the report without marking the class as used.

php
<?php

final class A {}
final class B {}
$a = new A();

Running Psalm Issues UnusedClosureParam (`docs/running_psalm/issues/UnusedClosureParam.md`)

UnusedClosureParam

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a particular parameter in a closure.

php
<?php

$a = function (int $a, int $b) : int {
    return $a + 4;
};

/**
 * @param callable(int,int):int $c
 */
function foo(callable $c) : int {
    return $c(2, 4);
}

Can be suppressed by prefixing the parameter name with an underscore:

php
$f = function (int $_a, int $b) : int {
    return $b + 4;
};

Running Psalm Issues UnusedConstructor (`docs/running_psalm/issues/UnusedConstructor.md`)

UnusedConstructor

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a given private constructor or function

php
<?php

class A {
    private function __construct() {}

    public static function createInstance() : void {}
}
A::createInstance();

Running Psalm Issues UnusedDocblockParam (`docs/running_psalm/issues/UnusedDocblockParam.md`)

UnusedDocblockParam

Emitted when --find-dead-code is turned on and a parameter specified in docblock does not have a corresponding parameter in function / method signature.

php
<?php

/**
 * @param string $legacy_param was renamed to $newParam
 */
function f(string $newParam): string {
    return strtolower($newParam);
}

Running Psalm Issues UnusedForeachValue (`docs/running_psalm/issues/UnusedForeachValue.md`)

UnusedForeachValue

Emitted when --find-dead-code is turned on and Psalm cannot find any
references to the foreach value

php
<?php

/** @param array<string, int> $a */
function foo(array $a) : void {
    foreach ($a as $key => $value) { // $value is unused
        echo $key;
    }
}

Can be suppressed by prefixing the variable name with an underscore or naming
it $_:

php
<?php

foreach ([1, 2, 3] as $key => $_val) {}

foreach ([1, 2, 3] as $key => $_) {}

Running Psalm Issues UnusedFunctionCall (`docs/running_psalm/issues/UnusedFunctionCall.md`)

UnusedFunctionCall

Emitted when --find-dead-code is turned on and Psalm finds a function call whose return value is not used anywhere

php
<?php

$a = strlen("hello");
strlen("goodbye"); // unused
echo $a;

It is also emitted, regardless of --find-dead-code, when the return value of a function annotated with PHP 8.5's #[\NoDiscard] attribute is discarded. Casting the call to (void) intentionally discards the value without triggering the issue.


Running Psalm Issues UnusedIssueHandlerSuppression (`docs/running_psalm/issues/UnusedIssueHandlerSuppression.md`)

UnusedIssueHandlerSuppression

Emitted when an issue type suppression in the configuration file is not being used to suppress an issue.

Enabled by findUnusedIssueHandlerSuppression

php
<?php
$a = 'Hello, World!';
echo $a;
xml
<?xml version="1.0" encoding="UTF-8"?>
<issueHandlers>
    <PossiblyNullOperand errorLevel="suppress"/>
</issueHandlers>

Running Psalm Issues UnusedMethod (`docs/running_psalm/issues/UnusedMethod.md`)

UnusedMethod

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a
given private method or function.

If this method is genuinely part of your public API β€” used from outside the analysed codebase (for example a library entry point, or a method invoked only by a framework) β€” annotate the containing class with @api (or @psalm-api) β€” mark the whole class rather than the individual method, since a public-API method almost always belongs to a public-API class. Otherwise the method is dead code and should be removed: a method reported as unused in a class that is not already marked @api is almost always genuinely unused, so annotating the individual method with @api to silence the report is very rarely the right fix.

See @api for the full guidance; note that @psalm-suppress is not a substitute for @api, as it silences the report without marking the method as used.

php
<?php

class A {
    public function __construct() {
        $this->foo();
    }
    private function foo() : void {}
    private function bar() : void {}
}
$a = new A();

Running Psalm Issues UnusedMethodCall (`docs/running_psalm/issues/UnusedMethodCall.md`)

UnusedMethodCall

Emitted when --find-dead-code is turned on and Psalm finds a method call whose return value is not used anywhere

php
<?php

final class A {
    private string $foo;

    public function __construct(string $foo) {
        $this->foo = $foo;
    }

    public function getFoo() : string {
        return $this->foo;
    }
}

$a = new A("hello");
$a->getFoo();

It is also emitted, regardless of --find-dead-code, when the return value of a method annotated with PHP 8.5's #[\NoDiscard] attribute is discarded. Casting the call to (void) intentionally discards the value without triggering the issue.


Running Psalm Issues UnusedParam (`docs/running_psalm/issues/UnusedParam.md`)

UnusedParam

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a particular parameter in a private method or function:

php
<?php

function foo(int $a, int $b) : int {
    return $a + 4;
}

Can be suppressed by prefixing the unused parameter's name with an underscore:

php
function foo(int $a, int $_b) : int {
    return $a + 4;
}

Running Psalm Issues UnusedProperty (`docs/running_psalm/issues/UnusedProperty.md`)

UnusedProperty

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a
private property.

Properties used in constructor only are considered unused. Use normal variables instead.

If this property is genuinely part of your public API β€” used from outside the analysed codebase (for example a library entry point, or a property read only by a framework) β€” annotate the containing class with @api (or @psalm-api) β€” mark the whole class rather than the individual property, since a public-API property almost always belongs to a public-API class. Otherwise the property is dead code and should be removed: a property reported as unused in a class that is not already marked @api is almost always genuinely unused, so annotating the individual property with @api to silence the report is very rarely the right fix.

See @api for the full guidance; note that @psalm-suppress is not a substitute for @api, as it silences the report without marking the property as used.

php
<?php

class A {
    /** @var string|null */
    private $foo;

    /** @var int|null */
    private $bar;

    public function getFoo(): ?string {
        return $this->foo;
    }
}

$a = new A();
echo $a->getFoo();

Running Psalm Issues UnusedPsalmSuppress (`docs/running_psalm/issues/UnusedPsalmSuppress.md`)

UnusedPsalmSuppress

Emitted when --find-unused-psalm-suppress is turned on and Psalm cannot find any uses of a given @psalm-suppress annotation

php
<?php

/** @psalm-suppress InvalidArgument */
echo strlen("hello");

This is checked for @psalm-suppress annotations in any docblock, including those on classes, interfaces, traits and enums. @psalm-suppress of Tainted* issues is only checked when running with --taint-analysis, since those issues are not computed otherwise.


Running Psalm Issues UnusedReturnValue (`docs/running_psalm/issues/UnusedReturnValue.md`)

UnusedReturnValue

Emitted when --find-dead-code is turned on and Psalm cannot find any uses of a private method’s return value.

php
<?php

class A {
    public function __construct() {
        $this->foo();
    }
    private function foo() : string {
        return "hello";
    }
}

new A();

Running Psalm Issues UnusedVariable (`docs/running_psalm/issues/UnusedVariable.md`)

UnusedVariable

Emitted when --find-dead-code is turned on and Psalm cannot find any references to a variable, once instantiated

php
<?php

function foo() : void {
    $a = 5;
    $b = 4;
    echo $b;
}

Can be suppressed by prefixing the variable name with an underscore:

php
<?php

$_a = 22;

If the variable contains an object with an impure destructor, the issue is not emitted, to allow for patterns used for example in timers or mutexes:

php
<?php

class Timer {
    private readonly float $start;
    public function __construct(private readonly string $op) {
        $this->start = microtime(true);
    }
    /**
     * Must be explicitly marked as @psalm-impure.
     * 
     * @psalm-impure
     */
    public function __destruct() {
        $took = microtime(true) - $this->start;
        // Send to influx, or similar
        echo "{$this->op} took $took seconds!\n";
    }
}

function foo() : void {
    // OK!
    $timer = new Timer('file_get_contents');

    echo file_get_contents('https://httpbin.io/delay/3');
}

foo();

$var = [new Timer('bad')];

// UnusedVariable: The issue will still be emitted even if the destructor is impure for variables used as foreach variables.
foreach ($var as $timer) {

}

Running Psalm Plugins Authoring Plugins (`docs/running_psalm/plugins/authoring_plugins.md`)

Authoring Plugins

Quick start

Using a template repository

Head over to plugin template repository on GitHub, login and click Use this template button.

Using skeleton project

Run composer create-project psalm/psalm-plugin-skeleton:dev-master your-plugin-name to quickly bootstrap a new plugin project in your-plugin-name folder. Make sure you adjust namespaces in composer.json, Plugin.php and tests folder.

Psalm plugin API

Backwards compatibility for the plugin API is now covered by a separate metapackage, psalm/psalm-plugin-api.

This is a separate, empty metapackage: its major version will be bumped every time a breaking change occurs within Psalm's plugin API; minors will also be bumped when adding new features to Psalm's plugin API.

Plugins, starting from the first stable release of v7, need to explicitly require a caret range (i.e. ^1, NOT 1.0.0) that package in order to avoid breaking changes during Psalm upgrades.

Plugins that support v7 betas can already require 0.x versions of the package.

All plugin API versions with a list of contained breaking changes will be listed in the readme of psalm/psalm-plugin-api.

Stub files

Stub files provide a way to override third-party type information when you cannot add Psalm's extended docblocks to the upstream source files directly.
By convention, stub files have .phpstub extension to avoid IDEs treating them as actual php code.

Generating stubs

Dev-require the library you want to tweak types for, e.g.

text
composer require --dev cakephp/chronos

Then generate the stubs

text
vendor/bin/psalm --generate-stubs=stubs/chronos.phpstub

Open the generated file and remove everything not related to the library you're stubbing. Tweak the docblocks to provide more accurate types.

Registering stub files

Skeleton/template project includes the code to register all .phpstub files from the stubs directory.

To register a stub file manually use Psalm\Plugin\RegistrationInterface::addStubFile().

Registering custom scanners and analyzers

In addition to XML configuration node <fileExtensions> plugins can register their own custom scanner
and analyzer implementations for particular file extensions, e.g.

  • Psalm\Plugin\FileExtensionsInterface::addFileTypeScanner('html', CustomFileScanner::class)
  • Psalm\Plugin\FileExtensionsInterface::addFileTypeAnalyzer('html', CustomFileAnalyzer::class)

Publishing your plugin on Packagist

Follow instructions on packagist.org under 'Publishing Packages' section.

Advanced topics

Starting from scratch

Composer-based plugin is a composer package which conforms to these requirements:

  1. Its type field is set to psalm-plugin
  2. It has extra.psalm.pluginClass subkey in its composer.json that reference an entry-point class that will be invoked to register the plugin into Psalm runtime.
  3. Entry-point class implements Psalm\Plugin\PluginEntryPointInterface

Psalm API

Plugins may implement one of (or more than one of) Psalm\Plugin\EventHandler\* interface(s).

php
<?php
class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface
{
}

Psalm\Plugin\EventHandler\* offers the following interfaces that you can implement:

  • AfterAnalysisInterface - called after Psalm has completed its analysis. Use this hook if you want to do something with the analysis results.
  • AfterClassLikeAnalysisInterface - called after Psalm has completed its analysis of a given class.
  • AfterClassLikeExistenceCheckInterface - called after Psalm analyzes a reference to a class, interface or trait.
  • AfterClassLikeVisitInterface - called after Psalm crawls the parsed Abstract Syntax Tree for a class-like (class, interface, trait). Due to caching the AST is crawled the first time Psalm sees the file, and is only re-crawled if the file changes, the cache is cleared, or you're disabling cache with --no-cache/--no-reflection-cache. Use this if you want to collect or modify information about a class before Psalm begins its analysis.
  • AfterCodebasePopulatedInterface - called after Psalm has scanned necessary files and populated codebase data.
  • AfterEveryFunctionCallAnalysisInterface - called after Psalm evaluates any function call. Cannot influence the call further.
  • AfterExpressionAnalysisInterface - called after Psalm evaluates an expression.
  • AfterFileAnalysisInterface - called after Psalm analyzes a file.
  • AfterFunctionCallAnalysisInterface - called after Psalm evaluates a function call to any function defined within the project itself. Can alter the return type or perform modifications of the call.
  • AfterFunctionLikeAnalysisInterface - called after Psalm has completed its analysis of a given function-like.
  • AfterMethodCallAnalysisInterface - called after Psalm analyzes a method call.
  • BeforeStatementAnalysisInterface - called before Psalm evaluates a statement.
  • AfterStatementAnalysisInterface - called after Psalm evaluates a statement.
  • BeforeAddIssueInterface - called before Psalm adds an item to it's internal IssueBuffer, allows handling code issues individually
  • BeforeFileAnalysisInterface - called before Psalm analyzes a file.
  • ClassFilePathProviderInterface.php - can be used to determine the file path a of a class not autoloadable using composer, but autoloadable through a custom autoloader, to avoid actually invoking the autoloader and executing the class
  • FunctionExistenceProviderInterface - can be used to override Psalm's builtin function existence checks for one or more functions.
  • FunctionParamsProviderInterface.php - can be used to override Psalm's builtin function parameter lookup for one or more functions.
  • FunctionReturnTypeProviderInterface - can be used to override Psalm's builtin function return type lookup for one or more functions.
  • MethodCapabilitiesProviderInterface - can be used to give the capabilities (purity) a call of a method of one or more classes requires, e.g. from its arguments.
  • MethodExistenceProviderInterface - can be used to override Psalm's builtin method existence checks for one or more classes.
  • MethodParamsProviderInterface - can be used to override Psalm's builtin method parameter lookup for one or more classes.
  • MethodReturnTypeProviderInterface - can be used to override Psalm's builtin method return type lookup for one or more classes.
  • MethodVisibilityProviderInterface - can be used to override Psalm's builtin method visibility checks for one or more classes.
  • PropertyExistenceProviderInterface - can be used to override Psalm's builtin property existence checks for one or more classes.
  • PropertyTypeProviderInterface - can be used to override Psalm's builtin property type lookup for one or more classes.
  • PropertyVisibilityProviderInterface - can be used to override Psalm's builtin property visibility checks for one or more classes.
  • DynamicFunctionStorageProviderInterface - can be used to override signature for one or more functions. It means you can define variadic param list. Infer return type by input args. Define function templates. Also check out Psalm\Plugin\DynamicFunctionStorage to find out what api it brings for you to change function's definition.

Here are a couple of example plugins:

To ensure your plugin runs when Psalm does, add it to your config (not needed for composer-based plugins):

xml
<plugins>
        <plugin filename="src/plugins/SomePlugin.php" />
    </plugins>

You can also specify an absolute path to your plugin:

xml
<plugins>
        <plugin filename="/path/to/SomePlugin.php" />
    </plugins>

Using Xdebug

As Psalm disables Xdebug at runtime, if you need to debug your code step-by-step when authoring a plugin, you can allow the extension by running Psalm as following:

console
$ PSALM_ALLOW_XDEBUG=1 path/to/psalm

Type system

Understand how Psalm handles types by reading this guide.

Handling custom plugin issues

Plugins may sometimes need to emit their own issues (i.e. not emit one of the existing issues). If this is the case, they can emit an issue that extends Psalm\Issue\PluginIssue.

To suppress a custom plugin issue in docblocks you can just use its issue name (e.g. /** @psalm-suppress NoFloatAssignment */, but to suppress it in Psalm’s config you must use the pattern:

xml
<PluginIssue name="NoFloatAssignment" errorLevel="suppress" />

You can also use more complex rules in the `` element, as you can with any other issue type e.g.

xml
<PluginIssue name="NoFloatAssignment">
    <errorLevel type="suppress">
        <directory name="tests" />
    </errorLevel>
</PluginIssue>

Upgrading file-based plugin to composer-based version

Create new plugin project using skeleton, then pass the class name of you file-based plugin to registerHooksFromClass()
method of the Psalm\Plugin\RegistrationInterface instance that was passed into your plugin entry point's __invoke()
method. See the conversion example.


Running Psalm Plugins Plugins Type System (`docs/running_psalm/plugins/plugins_type_system.md`)

Plugins: type system internals

Psalm's type system represents the types of variables within a program using different classes. Plugins both receive, and can update this type information.

Union types

All type information you are likely to use will be wrapped in a Union Type.

The Union class constructor takes an array of Atomic types, and can represent one or more of these types at a time. They correspond to a vertical bar in a doc comment.

php
new Union([new TNamedObject('Foo\\Bar\\SomeClass')]); // equivalent to Foo\Bar\SomeClass in docblock
new Union([new TString(), new TInt()]); // equivalent to string|int in docblock

Atomic types

Primitive types like floats, integers and strings, plus arrays, and classes. You can find all of these in src/Psalm/Types/Atomic.

Note that all non-abstract classes in this folder are valid types. They are all prefixed by 'T'.

The classes are as follows:

Misc

TVoid - denotes the void type, normally just used to annotate a function/method that returns nothing

TNull - denotes the null type

TNever - denotes the no-return/never-return type for functions that never return, either throwing an exception or terminating (like the builtin exit()). Also used for union types that can have no possible types (impossible intersections for example). Empty arrays [] have the type array<never, never>.

TMixed - denotes the mixed type, used when you don’t know the type of an expression.

TNonEmptyMixed - as above, but not empty. Generated for $x inside the if statement if ($x) {...} when $x is mixed outside.

TEmptyMixed - as above, but empty. Generated for $x inside the if statement if (!$x) {...} when $x is mixed outside.

TIterable - denotes the iterable type (which can also result from an is_iterable check).

TResource - denotes the resource type (e.g. a file handle).

TClosedResource - denotes the resource type that has been closed (e.g. a file handle through fclose()).

TAssertionFalsy - Represents any value reduced to false when computed in boolean context. This is used for assertions

TConditional - Internal representation of a conditional return type in phpdoc. For example ($param1 is int ? int : string)

TIntMask - Represents the type that is the result of a bitmask combination of its parameters. int-mask<1, 2, 4> corresponds to 1|2|3|4|5|6|7

TIntMaskOf - as above, but used with a reference to constants in code int-mask-of<MyClass::CLASS_CONSTANT_*> will corresponds to 1|2|3|4|5|6|7 if there are three constant 1, 2 and 4

TKeyOf - Represents an offset of an array (e.g. key-of<MyClass::CLASS_CONSTANT>).

TValueOf - Represents a value of an array or enum (e.g. value-of<MyClass::CLASS_CONSTANT>).

TTemplateIndexedAccess - To be documented

TTemplateKeyOf - Represents the type used when using TKeyOf when the type of the array is a template

TTemplateValueOf - Represents the type used when using TValueOf when the type of the array or enum is a template

TPropertiesOf - Represents properties and their types of a class as a keyed array (e.g. properties-of<MyClass>)

TTemplatePropertiesOf - Represents the type used when using TPropertiesOf when type of the class is a template

TTypeAlias - To be documented

Scalar supertype

TScalar - denotes the scalar super type (which can also result from an is_scalar check). This type encompasses float, int, bool and string.

TEmptyScalar - denotes a scalar type that is also empty.

TNonEmptyScalar - denotes a scalar type that is also non-empty.

Numeric supertype

TNumeric - denotes the numeric type (which can also result from an is_numeric check).

TEmptyNumeric - denotes the numeric type that's also empty (which can also result from an is_numeric and empty check).

Scalar types

All scalar types have literal versions e.g. int vs int(5).

Ints

TInt - denotes the int type, where the exact value is unknown.

TLiteralInt - is used to represent an integer value where the exact numeric value is known.

TIntRange - allows to describe an int with bounded values (ie. int<1, 5>).

Floats

TFloat - denotes the float type, where the exact value is unknown.

TLiteralFloat - is used to represent a floating point value where the exact numeric value is known.

Bools

TBool, TFalse, TTrue

TBool - denotes the bool type where the exact value is unknown.

TFalse - denotes the false value type

TTrue - denotes the true value type

php
/** @return string|false    false when string is empty, first char of the parameter otherwise */
function firstChar(string $s) { return empty($s) ? false : $s[0]; }

Here, the function may never return true, but if you had to replace false with bool, Psalm would have to consider true as a possible return value. With narrower type it's able to report meaningless code like this (https://psalm.dev/r/037291351d):

php
$first = firstChar("sdf");
if (true === $first) {
  echo "This is actually dead code";
}

Strings

TString - denotes the string type, where the exact value is unknown.

TNonEmptyString - denotes a string, that is also non-empty

TNumericString - denotes a string that's also a numeric value e.g. "5". It can result from is_string($s) && is_numeric($s).

TLiteralString - is used to represent a string whose value is known.

TClassString - denotes the class-string type, used to describe a string representing a valid PHP class. The parent type from which the classes descend may or may not be specified in the constructor.

TLiteralClassString - denotes a specific class string, generated by expressions like A::class.

TTraitString - denotes the trait-string type, used to describe a string representing a valid PHP trait.

TDependentGetClass - Represents a string whose value is a fully-qualified class found by get_class($var)

TDependentGetDebugType - Represents a string whose value is that of a type found by get_debug_type($var)

TDependentGetType - Represents a string whose value is that of a type found by gettype($var)

TCallableString - denotes the callable-string type, used to represent an unknown string that is also callable.

TSqlSelectString - this is a special type, specifically for consumption by plugins.

TLowercaseString - denotes a string where every character is lowercased. (which can also result from a strtolower call).

TNonEmptyLowercaseString - denotes a non-empty-string where every character is lowercased. (which can also result from a strtolower call).

TSingleLetter - denotes a string that has a length of 1

Scalar class constants

TScalarClassConstant - denotes a class constant whose value might not yet be known.

Array key supertype

TArrayKey - denotes the array-key type, used for something that could be the offset of an array.

Arrays

TArray - denotes a simple array of the form array<TKey, TValue>. It expects an array with two elements, both union types.

TNonEmptyArray - as above, but denotes an array known to be non-empty.

TKeyedArray represents an 'object-like array' - an array with known keys.

php
$x = ["a" => 1, "b" => 2]; // is TKeyedArray, array{a: int, b: int}
$y = rand(0, 1) ? ["a" => null] : ["a" => 1, "b" => "b"]; // is TKeyedArray with optional keys/values, array{a: ?int, b?: string}

This type is also used to represent lists (instead of the now-deprecated TList type).

Note that not all associative arrays are considered object-like. If the keys are not known, the array is treated as a mapping between two types.

php
$a = [];
foreach (range(1,1) as $_) $a[(string)rand(0,1)] = rand(0,1); // array<string,int>

TCallableKeyedArray - denotes an object-like array that is also callable.

TClassStringMap - Represents an array where the type of each value is a function of its string key value

Callables & closures

TCallable - denotes the callable type. Can result from an is_callable check.
TClosure - denotes a `Clo