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 amixedplaceholder type.mixedtypes 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, soif ($a && $a) {}andif ($a && !$a) {}are both treated as issues. Psalm also keeps track of logical assertions made in prior code paths, preventing issues likeif ($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
$a = ['foo', 'bar'];
echo implode($a, ' ');> ./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
- Running Psalm:
- Security analysis:
- Annotating code:
- Manipulating code:
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 SQLldap- used for strings that could contain a ldap DN or filterhtml- used for strings that could contain angle brackets or unquoted stringshas_quotes- used for strings that could contain unquoted stringsshell- used for strings that could contain shell commandscallable- used for callable strings that could be user-controlledunserialize- used for strings that could contain a serialized stringinclude- used for strings that could contain a path being includedeval- used for strings that could contain codessrf- used for strings that could contain text passed to Curl or similarurl_component- used for strings that could contain URL syntax (/,?,&,#...) placed in the path, query or fragment of a URLurl_path- used for strings that could be a.or..segment of the path of a URL, even URL-encodedfile- used for strings that could contain a pathcookie- used for strings that could contain a http cookieheader- used for strings that could contain a http headeruser_secret- used for strings that could contain user-supplied secretssystem_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,$_COOKIEand$_REQUESTserver variables; - the entries of
$_SERVERthe 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_TYPEand 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()andfile()called with a literalphp://inputorphp://stdinpath, and the stream reading functions (fgets(),fread(),stream_get_contents(), ...) applied to such a handle; - the predefined
STDINconstant (thephp://stdinstream). - reading from the network:
socket_read(), the datasocket_recv(),socket_recvfrom()andsocket_recvmsg()write to their by-reference parameter,stream_socket_recvfrom(),curl_exec()andcurl_multi_getcontent(), and the streams opened withfsockopen(),pfsockopen(),stream_socket_client(),stream_socket_accept()andsocket_export_stream()(so reading from them withfgets(),fread(), ... is a source too). - DNS answers, which whoever runs the name servers of a domain chooses:
dns_get_record(), the hostsgetmxrr()writes to its by-reference parameter, andgethostbyaddr(). - 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 acatchblock, 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.
$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%29To 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:
SECURITY: TaintedHtml - src/index.php:2:1 - Detected tainted HTMLA 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:
psalm --report=results.sarifDebugging 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:
psalm --taint-analysis --dump-taint-graph=taints.dot
dot -Tsvg -o taints.svg taints.dotAnnotating 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 abool)@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
/** @psalm-assert string[] $arr */
function validateStringArray(array $arr) : void {
foreach ($arr as $s) {
if (!is_string($s)) {
throw new UnexpectedValueException('Invalid value ' . gettype($s));
}
}
}This enables you to call the validateStringArray function on some data and have Psalm understand that the given data must be an array of strings:
<?php
function takesString(string $s) : void {}
function takesInt(int $s) : void {}
function takesArray(array $arr) : void {
takesInt($arr[0]); // this is fine
validateStringArray($arr);
takesInt($arr[0]); // this is an error
foreach ($arr as $a) {
takesString($a); // this is fine
}
}Similarly, @psalm-assert-if-true and @psalm-assert-if-false will filter input if the function/method returns true and false respectively:
<?php
class A {
public function isValid() : bool {
return (bool) rand(0, 1);
}
}
class B extends A {
public function bar() : void {}
}
/**
* @psalm-assert-if-true B $a
*/
function isValidB(A $a) : bool {
return $a instanceof B && $a->isValid();
}
/**
* @psalm-assert-if-false B $a
*/
function isInvalidB(A $a) : bool {
return !$a instanceof B || !$a->isValid();
}
function takesA(A $a) : void {
if (isValidB($a)) {
$a->bar();
}
if (isInvalidB($a)) {
// do something
} else {
$a->bar();
}
$a->bar(); //error
}As well as getting Psalm to understand that the given data must be a certain type, you can also show that a variable must be not null:
<?php
/**
* @psalm-assert !null $value
*/
function assertNotNull($value): void {
// Some check that will mean the method will only complete if $value is not null.
}And you can check on null values:
<?php
/**
* @psalm-assert-if-true null $value
*/
function isNull($value): bool {
return ($value === null);
}Asserting return values of methods
You can also use the @psalm-assert-if-true and @psalm-assert-if-false annotations to assert return values of
methods inside classes. As you can see, Psalm even allows you to specify multiple annotations in the same DocBlock:
<?php
class Result {
/**
* @var ?Exception
*/
private $exception;
/**
* @psalm-assert-if-true Exception $this->exception
* @psalm-assert-if-true Exception $this->getException()
*/
public function hasException(): bool {
return $this->exception !== null;
}
public function getException(): ?Exception {
return $this->exception;
}
public function foo(): void {
if( $this->hasException() ) {
// Psalm now knows that $this->exception is an instance of Exception
echo $this->exception->getMessage();
}
}
}
$result = new Result;
if( $result->hasException() ) {
// Psalm now knows that $result->getException() will return an instance of Exception
echo $result->getException()->getMessage();
}Please note that the example above only works if you enable method call memoization
in the config file or annotate the class as immutable.
You can 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
/**
* @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:
intfloatstringboolscalarcallablecountablearrayiterablenumericresourceobjectnull
So a custom version is_int could be annotated in Psalm as
<?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
/** @psalm-assert !int $foo */This asserts that $bar is not an object of type SomeObjectType:
<?php
/** @psalm-assert !SomeObjectType $bar */Bool assertions
This asserts that $bar is true:
<?php
/** @psalm-assert true $bar */This asserts that $bar is not false:
<?php
/** @psalm-assert !false $bar */Equality assertions
Psalm also supports the equivalent of assert($some_int === $other_int) in the form
<?php
/** @psalm-assert =int $some_int */There are two differences between the above assertion and
<?php
/** @psalm-assert int $some_int */Firstly, the negation of =int has no meaning:
<?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, β¦) needwrite-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 needswrite-this-props|write-props. write-props does not include write-this-props, andwrite-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 bewrite-this-props.
<?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 makesread-globals a read-only view of global state, like Hack's readonly values:
<?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
/** @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
/** @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
/**
* @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, needswrite-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
/** @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 mostread-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
/** @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
/** @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
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
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
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
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
/** @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
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
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 withread-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 aFoo[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
/** @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
/**
* @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
/**
* @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
/**
* @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
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): thennew 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
/**
* @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 (
impurewhen 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 andC <= 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
/** @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-propsConstructors
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, andnew 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
/**
* @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 callingadd() 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 aContainer[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, andGenerator[io] prints when consumed. It defaults to impure, soIterator 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] orGenerator[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 anyTraversable. 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 (orIterator, 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
/**
* @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 whereIterator[pure] is expected exactly when its iteration methods are pure.
The SPL iterators wrapping another one (IteratorIterator, FilterIterator,CallbackFilterIterator, LimitIterator, NoRewindIterator, InfiniteIterator) have aTPurity 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 ofFilterIterator 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
/** @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 topure. 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. WithrememberPropertyAssignmentsAfterCall="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. WithrememberPropertyAssignmentsAfterCall="false", this code reports one issue:
<?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:
- MissingPureAnnotation: a function or method
that could have@psalm-pureor@psalm-capabilities; - MissingImmutableAnnotation: a class
that could have@psalm-pure,@psalm-immutableor@psalm-capabilities; - MissingAbstractPureAnnotation: an
abstract method with no annotation; - MissingInterfaceImmutableAnnotation:
an interface with no annotation.
The first two can be fixed automatically:
vendor/bin/psalm --alter --issues=MissingPureAnnotation,MissingImmutableAnnotationGiven this code:
<?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 atread-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
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, useissueHandlers:
<issueHandlers>
<MissingPureAnnotation errorLevel="suppress" />
<MissingImmutableAnnotation errorLevel="suppress" />
<MissingAbstractPureAnnotation errorLevel="suppress" />
<MissingInterfaceImmutableAnnotation errorLevel="suppress" />
</issueHandlers>Migrating from Psalm 6
Existing annotations keep their meaning:
@psalm-pureand@psalm-immutableare unchanged.@psalm-mutation-freemeans@psalm-capabilities read-props.@psalm-external-mutation-freemeans@psalm-capabilities read-props|write-this-props|write-refs.
It no longer allows static properties orstaticvariables: useread-globalsorwrite-globalsfor 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-freemethod could call
one on$this. Now a call on$thiscostswrite-this-propsand a call on any other object
costswrite-props(see Method calls). - Overrides are checked at every level. Psalm 6 only reported impure overrides of
@psalm-external-mutation-freemethods. Now any override that needs more capabilities is
reported, asImmutableDependency. MissingImmutableAnnotationhas a new meaning. In Psalm 6 it reported a mutable class
inheriting from an immutable one; that is nowImmutableDependency.MissingImmutableAnnotation
now reports a class that could be annotated. Review existing suppressions of it.- New issues are reported by default:
MissingPureAnnotation,MissingImmutableAnnotation,MissingAbstractPureAnnotationandMissingInterfaceImmutableAnnotation. Run--alteronce,
or suppress them (see Inferring annotations). - More is checked. Implicit calls (
__toString,__clone,__invoke,ArrayAccess,
destructors),throw new,new $classNameandcall_user_funcare now checked for purity.
Builtins with side effects need specific capabilities:iofor I/O and random numbers,timefor reading the clock,write-globalsfor process-wide state such asmt_rand()orini_set(). - Iteration has a purity.
foreachover an object costs the iterator's methods, or theTPurityofTraversable,Iterator,Generatoranditerable, instead of always being
impure (see Iterators and generators). - Callable types:
pure-callableandpure-Closurestill work, and any capability set can
now be written, as inClosure[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 objectnewcreates (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__getand__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
/** @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.
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; // worksMixins 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`
`@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
/**
* @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
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
/** @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
/**
* @psalm-suppress PossiblyNullOperand
*/
function addString(?string $s) {
echo "hello " . $s;
}Inline example:
<?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
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
/**
* @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
/**
* @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
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
class B {
/** @readonly */
public string $s;
public function __construct(string $s) {
$this->s = $s;
}
}
$b = new B("hello");
echo $b->s;
$b->s = "boo"; // disallowedPurity 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
/**
* @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] oriterable[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
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
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
/** @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:
<issueHandlers>
<Trace errorLevel="error"/>
</issueHandlers>`@psalm-check-type`
You can use this annotation to ensure the inferred type matches what you expect.
<?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
/** @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
/**
* @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
/**
* @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
/**
* @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
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
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.
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
/**
* @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.
/**
* @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
/**
* @psalm-inheritors FooClass|BarClass
*/
class BaseClass {}
class FooClass extends BaseClass {}
class BarClass extends BaseClass {}
class BazClass extends BaseClass {} // this is an errorType 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
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
/**
* @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
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
/**
* @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 stringPsalm also uses @template annotations in its stubbed versions of PHP array functions e.g.
<?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
@templatetag 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
/**
* @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 FooChildTemplate inheritance
Psalm allows you to extend templated classes with @extends/@template-extends:
<?php
/**
* @template T
*/
class ParentClass {}
/**
* @extends ParentClass<int>
*/
class ChildClass extends ParentClass {}similarly you can implement interfaces with @implements/@template-implements
<?php
/**
* @template T
*/
interface IFoo {}
/**
* @implements IFoo<int>
*/
class Foo implements IFoo {}and import traits with @use/@template-use
<?php
/**
* @template T
*/
trait MyTrait {}
class Foo {
/**
* @use MyTrait<int>
*/
use MyTrait;
}You can also extend one templated class with another, e.g.
<?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
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 errorTemplated types aren't limited to key-value pairs, and you can re-use templates across multiple arguments of a template-supporting type:
<?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
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
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
/**
* @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
/**
* @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 Traversableclass 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 writenew 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 aclass-string<T>/T::classparameter, 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 addarray-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 constraintsT <: 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
/**
* @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:
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 unboundBox
genuinely informs whatTis, and$box->get()resolves through those accumulated
bounds instead of falling back to the constraint.Widening is legal. Under eager pinning,
new Box(1)pinsT = 1, so$box->set('two')wasInvalidArgument("expects 1, but 'two' provided") even
though'two'satisfiesT of int|string. Under type variables the set call just
widensTtoward1|'two', which the constraint accepts.Tis 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.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 aBox<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.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 (negatedinstanceofnarrowing, 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
mixedconstraint 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. SplObjectStoragekeeps itsneverspecial case for unbound templates.- A type variable renders through its bounds, so inferred types still display as
Box<1>orFoo<A>rather than`_0. - Callables validate against the construction-site inference with ordinary
InvalidArgumentmessages, 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
/** @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
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
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
/** @var string|null */
$a = foo();
$a = 6; // $a is now typed as an intYou can also use typehints on specific variables e.g.
<?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
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
/**
* @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
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
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
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 inpsalm.xmlby usingreferencedClassattributePropertyIssue- issue related to a specific property. Can be targeted by usingreferencedPropertyinpsalm.xmlFunctionIssue- issue related to a specific function. Can be suppressed withreferencedFunctionattribute.ArgumentIssue- issue related to a specific argument. Can be targeted withreferencedFunctionattribute.MethodIssue- issue related to a specific method. Can be targeted withreferencedMethodattribute.ClassConstantIssue- issue related to a specific class constant. Can be targeted withreferencedConstant.VariableIssue- issue for a specific variable. Targeted withreferencedVariable
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
Add links to the doc page you created to docs/running_psalm/error_levels.md and docs/running_psalm/issues.md
Run documentation tests
$ vendor/bin/phpunit tests/DocumentationTest.phpIt will check whether you did all (or at least most) of the steps above.
Use your new issue type in Psalm core
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.
[
'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
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) andchanged.
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:
- Read
CallMap.php(Note: it's the one having the latest signatures). - If it matches configured PHP version, use it.
- If the callmap delta for previous PHP version exists, read that.
- Take previous callmap delta and apply it in reverse order. That is, entries
inremovedsection are added, those inaddedsection are removed andchanged.newsignatures in the current callamp are replaced withchanged.old. - 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 theCallMap_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 andCallMap.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
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
- Sorbet - written in C
- 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 anArrayIteratorover 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
vendor/bin/psalter [args]or via Psalm's binary:
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-versiondefaults to your current version. - it has a
--safe-typesmode that will only update PHP 7 return typehints with information Psalm has gathered from non-docblock sources of type information (e.g. typehinted params,instanceofchecks, other return typehints etc.) - using
--allow-backwards-incompatible-changes=falseyou can make sure to not create backwards incompatible changes
Plugins
You can pass in your own manipulation plugins e.g.
vendor/bin/psalter --plugin=vendor/vimeo/psalm/examples/plugins/ClassUnqualifier.php --dry-runThe 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
function foo() {
return "hello";
}gives
<?php
function foo() : string {
return "hello";
}and running vendor/bin/psalter --issues=MissingReturnType --php-version=5.6 on
<?php
function foo() {
return "hello";
}gives
<?php
/**
* @return string
*/
function foo() {
return "hello";
}MissingClosureReturnType
As above, except for closures
InvalidReturnType
Running vendor/bin/psalter --issues=InvalidReturnType on
<?php
/**
* @return int
*/
function foo() {
return "hello";
}gives
<?php
/**
* @return string
*/
function foo() {
return "hello";
}There's also support for return typehints, so running vendor/bin/psalter --issues=InvalidReturnType on
<?php
function foo() : int {
return "hello";
}gives
<?php
function foo() : string {
return "hello";
}InvalidNullableReturnType
Running vendor/bin/psalter --issues=InvalidNullableReturnType --php-version=7.1 on
<?php
function foo() : string {
return rand(0, 1) ? "hello" : null;
}gives
<?php
function foo() : ?string {
return rand(0, 1) ? "hello" : null;
}and running vendor/bin/psalter --issues=InvalidNullableReturnType --php-version=7.0 on
<?php
function foo() : string {
return rand(0, 1) ? "hello" : null;
}gives
<?php
/**
* @return string|null
*/
function foo() {
return rand(0, 1) ? "hello" : null;
}InvalidFalsableReturnType
Running vendor/bin/psalter --issues=InvalidFalsableReturnType on
<?php
function foo() : string {
return rand(0, 1) ? "hello" : false;
}gives
<?php
/**
* @return string|false
*/
function foo() {
return rand(0, 1) ? "hello" : false;
}MissingParamType
Running vendor/bin/psalter --issues=MissingParamType on
<?php
class C {
public static function foo($s) : void {
echo $s;
}
}
C::foo("hello");gives
<?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
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
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
class A {}
class B extends A {}
class C extends A {}
class D {}running vendor/bin/psalter --issues=MismatchingDocblockParamType on
<?php
/**
* @param B|C $first
* @param D $second
*/
function foo(A $first, A $second) : void {}gives
<?php
/**
* @param B|C $first
* @param A $second
*/
function foo(A $first, A $second) : void {}MismatchingDocblockReturnType
Running vendor/bin/psalter --issues=MismatchingDocblockReturnType on
<?php
/**
* @return int
*/
function foo() : string {
return "hello";
}gives
<?php
/**
* @return string
*/
function foo() : string {
return "hello";
}LessSpecificReturnType
Running vendor/bin/psalter --issues=LessSpecificReturnType on
<?php
function foo() : ?string {
return "hello";
}gives
<?php
function foo() : string {
return "hello";
}PossiblyUndefinedVariable
Running vendor/bin/psalter --issues=PossiblyUndefinedVariable on
<?php
function foo()
{
if (rand(0, 1)) {
$a = 5;
}
echo $a;
}gives
<?php
function foo()
{
$a = null;
if (rand(0, 1)) {
$a = 5;
}
echo $a;
}PossiblyUndefinedGlobalVariable
Running vendor/bin/psalter --issues=PossiblyUndefinedGlobalVariable on
<?php
if (rand(0, 1)) {
$a = 5;
}
echo $a;gives
<?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
class A {
private function foo() : void {}
}
new A();gives
<?php
class A {
}
new A();PossiblyUnusedMethod
This removes protected/public unused methods.
Running vendor/bin/psalter --issues=PossiblyUnusedMethod on
<?php
class A {
protected function foo() : void {}
public function bar() : void {}
}
new A();gives
<?php
class A {
}
new A();UnusedProperty
This removes private unused properties.
Running vendor/bin/psalter --issues=UnusedProperty on
<?php
class A {
/** @var string */
private $foo;
}
new A();gives
<?php
class A {
}
new A();PossiblyUnusedProperty
This removes protected/public unused properties.
Running vendor/bin/psalter --issues=PossiblyUnusedProperty on
<?php
class A {
/** @var string */
public $foo;
/** @var string */
protected $bar;
}
new A();gives
<?php
class A {
}
new A();UnusedVariable
This removes unused variables.
Running vendor/bin/psalter --issues=UnusedVariable on
<?php
function foo(string $s) : void {
$a = 5;
$b = 6;
$c = $b += $a -= intval($s);
echo "foo";
}gives
<?php
function foo(string $s) : void {
echo "foo";
}UnnecessaryVarAnnotation
This removes unused @var annotations
Running vendor/bin/psalter --issues=UnnecessaryVarAnnotation on
<?php
function foo() : string {
return "hello";
}
/** @var string */
$a = foo();gives
<?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
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
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
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
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
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
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
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:
<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
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
./vendor/bin/psalmand Psalm will scan all files in the project referenced by ``.
If you want to run on specific files, use
./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--diffwhich 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.
./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:
./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 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 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 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:
vendor/bin/psalm --config=other-psalm-config.xmlOptional <psalm /> attributes
Coding style
errorLevel
<psalm
errorLevel="[int]"
/>This corresponds to Psalmβs error-detection level.
reportMixedIssues
<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
<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
<psalm
useDocblockTypes="[bool]"
>Whether or not to use types as defined in docblocks. Defaults to true.
useDocblockPropertyTypes
<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
<psalm
docblockPropertyTypesSealProperties="[bool]"
>Whether using @property in class docblocks should imply @psalm-seal-properties. Defaults to true.
usePhpDocMethodsWithoutMagicCall
<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
<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
<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
<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
<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
<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
<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
<psalm
disableSuppressAll="[bool]"
>When true, disables wildcard suppression of all issues with @psalm-suppress all. Defaults to true.
memoizeMethodCallResults
<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
<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
<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
<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
<psalm
checkForThrowsInGlobalScope="[bool]"
>When true, Psalm will check that the developer has caught every exception in global scope. Defaults to false.
ignoreInternalFunctionFalseReturn
<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
<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
<psalm
inferPropertyTypesFromConstructor="[bool]"
>When true, Psalm infers property types from assignments seen in straightforward constructors. Defaults to true.
findUnusedVariablesAndParams
<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
<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
<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
<psalm
noCache="[bool]"
>When true, Psalm will disable usage of the cache, the equivalent of running with --no-cache. Defaults to false.
arrayCache
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<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
<psalm
throwExceptionOnError="[bool]"
>Useful in testing, this makes Psalm throw a regular-old exception when it encounters an error. Defaults to false.
hideExternalErrors
<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
<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
<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
<psalm
allowFileIncludes="[bool]"
>Whether or not to allow require/include calls in your PHP. Defaults to true.
ignoreIncludeSideEffects
<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
<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
<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
<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
<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
<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
<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
<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
<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
<psalm
restrictReturnTypes="true"
>Emits LessSpecificReturnType when the declared return type is not as tight as
the inferred return type.
This code:
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:
/**
* @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 withrestrictReturnTypes="true":
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.
<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.
<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.
<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.
<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".
<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.
<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.
<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, ...).
<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
$GLOBALSDBof typeMyVendor\DatabaseConnectionVIEWof typeMyVendor\TemplateView
$_GETdatae.g. like["id" => "123", "title" => "Nice"]
<forbiddenFunctions>
Optional. Allows you to specify a list of functions that should emit the ForbiddenCode issue type.
<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.
<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 toinputis
user input, which the built-in sources give (see
Custom taints declared in the configuration).
<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.
$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 statusinfo
This will cause Psalm to print a messagesuppress
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.
<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
/**
* @psalm-suppress InvalidReturnType
*/
function (int $a) : string {
return $a;
}You can also suppress issues at the line level e.g.
<?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.
vendor/bin/psalm --set-baselinewill generate a file psalm-baseline.xml containing the current errors. Alternatively, you can specify the name of your baseline file.
vendor/bin/psalm --set-baseline=your-baseline.xmlYou 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:
vendor/bin/psalm --use-baseline=your-baseline.xmlor using the configuration:
<?xml version="1.0"?>
<psalm
...
errorBaseline="./path/to/your-baseline.xml"
>
...
</psalm>To update that baseline file, use
vendor/bin/psalm --update-baselineThis 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
vendor/bin/psalm --ignore-baselineBaseline 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
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-analysisforTainted*,--find-unused-codeforUnused*). 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.
- AbstractMethodCall
- ComplexFunction
- ComplexMethod
- ConfigIssue
- ConstantDeclarationInTrait
- DuplicateArrayKey
- DuplicateClass
- DuplicateConstant
- DuplicateEnumCase
- DuplicateEnumCaseValue
- DuplicateFunction
- DuplicateMethod
- DuplicateParam
- DuplicateProperty
- EmptyArrayAccess
- ExtensionRequirementViolation
- ImplementationRequirementViolation
- ImpureByReferenceAssignment
- ImpureFunctionCall
- ImpureGlobalVariable
- ImpureMethodCall
- ImpurePropertyAssignment
- ImpurePropertyFetch
- ImpureStaticProperty
- ImpureStaticVariable
- ImpureVariable
- InaccessibleClassConstant
- InaccessibleMethod
- InaccessibleProperty
- InterfaceInstantiation
- InvalidAttribute
- InvalidEnumBackingType
- InvalidEnumCaseValue
- InvalidEnumMethod
- InvalidExtendClass
- InvalidGlobal
- InvalidInterfaceImplementation
- InvalidParamDefault
- InvalidParent
- InvalidPassByReference
- InvalidScope
- InvalidStaticInvocation
- InvalidThrow
- LoopInvalidation
- MethodSignatureMustOmitReturnType
- MethodSignatureMustProvideReturnType
- MissingAbstractPureAnnotation
- MissingDependency
- MissingFile
- MissingImmutableAnnotation
- MissingInterfaceImmutableAnnotation
- MissingPureAnnotation
- MissingTemplateParam
- MissingThrowsDocblock
- NoEnumProperties
- NoValue
- NonInvariantPropertyType
- NonStaticSelfCall
- NullArrayAccess
- NullFunctionCall
- NullIterator
- NullPropertyAssignment
- NullPropertyFetch
- NullReference
- OverriddenPropertyAccess
- ParadoxicalCondition
- ParentNotFound
- TooFewArguments
- UndefinedAttributeClass
- UndefinedClass
- UndefinedConstant
- UndefinedDocblockClass
- UndefinedFunction
- UndefinedGlobalVariable
- UndefinedInterface
- UndefinedTrait
- UndefinedVariable
- UnimplementedAbstractMethod
- UnimplementedInterfaceMethod
- UnrecognizedExpression
- UnrecognizedStatement
- UnresolvableConstant
- UnsupportedPropertyReferenceUsage
- UnusedBaselineEntry
- UnusedFunctionCall
- UnusedIssueHandlerSuppression
- UnusedMethodCall
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.
- ImmutableDependency
- IncompatibleTypeParameters
- IntMaskComparison
- InvalidClassConstantType
- LessSpecificClassConstantType
- LessSpecificReturnType
- MixedArgument
- MixedArgumentTypeCoercion
- MixedArrayAccess
- MixedArrayAssignment
- MixedArrayOffset
- MixedArrayTypeCoercion
- MixedAssignment
- MixedClone
- MixedFunctionCall
- MixedMethodCall
- MixedOperand
- MixedPropertyAssignment
- MixedPropertyFetch
- MixedPropertyTypeCoercion
- MixedReturnStatement
- MixedReturnTypeCoercion
- MixedStringOffsetAssignment
- MutableDependency
- PossiblyNullOperand
- RedundantFlag
- RedundantIdentityWithTrue
- Trace
Errors at level 2 and below
These issues become info (non-blocking) at level 3 and higher.
- ClassMustBeFinal
- DeprecatedClass
- DeprecatedConstant
- DeprecatedFunction
- DeprecatedInterface
- DeprecatedMethod
- DeprecatedProperty
- DeprecatedTrait
- DirectConstructorCall
- DocblockTypeContradiction
- InvalidDocblockParamName
- InvalidFalsableReturnType
- InvalidStringClass
- MismatchingDocblockPropertyType
- MissingClassConstType
- MissingClosureParamType
- MissingClosureReturnType
- MissingConstructor
- MissingParamType
- MissingPropertyType
- MissingReturnType
- NullOperand
- PrivateFinalMethod
- PropertyNotSetInConstructor
- RawObjectIteration
- RedundantCastGivenDocblockType
- RedundantConditionGivenDocblockType
- RedundantFunctionCallGivenDocblockType
- ReferenceConstraintViolation
- RiskyTruthyFalsyComparison
- UndefinedTrace
- UnresolvableInclude
- UnsafeGenericInstantiation
- UnsafeInstantiation
- UnsupportedReferenceUsage
Errors at level 3 and below
These issues become info (non-blocking) at level 4 and higher.
- ArgumentTypeCoercion
- LessSpecificReturnStatement
- MoreSpecificReturnType
- NonInvariantDocblockPropertyType
- PossiblyFalseArgument
- PossiblyFalseIterator
- PossiblyFalseOperand
- PossiblyFalsePropertyAssignmentValue
- PossiblyFalseReference
- PossiblyInvalidArgument
- PossiblyInvalidArrayAccess
- PossiblyInvalidArrayAssignment
- PossiblyInvalidArrayOffset
- PossiblyInvalidCast
- PossiblyInvalidClone
- PossiblyInvalidFunctionCall
- PossiblyInvalidIterator
- PossiblyInvalidMethodCall
- PossiblyInvalidOperand
- PossiblyInvalidPropertyAssignment
- PossiblyInvalidPropertyAssignmentValue
- PossiblyInvalidPropertyFetch
- PossiblyNullArgument
- PossiblyNullArrayAccess
- PossiblyNullArrayAssignment
- PossiblyNullArrayOffset
- PossiblyNullFunctionCall
- PossiblyNullIterator
- PossiblyNullPropertyAssignment
- PossiblyNullPropertyAssignmentValue
- PossiblyNullPropertyFetch
- PossiblyNullReference
- PossiblyUndefinedArrayOffset
- PossiblyUndefinedGlobalVariable
- PossiblyUndefinedMethod
- PossiblyUndefinedVariable
- PropertyTypeCoercion
- RiskyCast
Errors at level 4 and below
These issues become info (non-blocking) at level 5 and higher.
- FalseOperand
- ForbiddenCode
- IfThisIsMismatch
- ImplementedParamTypeMismatch
- ImplementedReturnTypeMismatch
- ImplicitToStringCast
- InheritorViolation
- InternalClass
- InternalMethod
- InternalProperty
- InvalidDocblock
- InvalidLiteralArgument
- InvalidOperand
- InvalidScalarArgument
- InvalidToString
- MismatchingDocblockParamType
- MismatchingDocblockReturnType
- MissingDocblockType
- NoInterfaceProperties
- PossibleRawObjectIteration
- PossiblyInvalidDocblockTag
- RedundantCast
- RedundantCondition
- RedundantFunctionCall
- RedundantPropertyInitializationCheck
- ReferenceReusedFromConfusingScope
- StringIncrement
- TooManyArguments
- TypeDoesNotContainNull
- TypeDoesNotContainType
- UndefinedMagicMethod
- UndefinedMagicPropertyAssignment
- UndefinedMagicPropertyFetch
Errors at level 5 and below
These issues become info (non-blocking) at level 6 and higher.
- ConstructorSignatureMismatch
- FalsableReturnStatement
- InvalidNullableReturnType
- LessSpecificImplementedReturnType
- MoreSpecificImplementedParamType
- NullableReturnStatement
- UndefinedInterfaceMethod
- UndefinedThisPropertyAssignment
Errors at level 6 and below
These issues become info (non-blocking) at level 7 and higher.
- AmbiguousConstantInheritance
- InvalidArgument
- InvalidArrayAccess
- InvalidArrayAssignment
- InvalidArrayOffset
- InvalidCast
- InvalidCatch
- InvalidClass
- InvalidClone
- InvalidConstantAssignmentValue
- InvalidFunctionCall
- InvalidIterator
- InvalidMethodCall
- InvalidNamedArgument
- InvalidPropertyAssignment
- InvalidPropertyAssignmentValue
- InvalidPropertyFetch
- InvalidReturnStatement
- InvalidReturnType
- InvalidTemplateParam
- NullArgument
- NullArrayOffset
- OverriddenFinalConstant
- OverriddenFinalProperty
- OverriddenInterfaceConstant
- TooManyTemplateParams
- TraitMethodSignatureMismatch
- UndefinedMethod
- UndefinedPropertyAssignment
- UndefinedPropertyFetch
- UndefinedThisPropertyFetch
Errors at level 7 and below
These issues become info (non-blocking) at level 8.
- AbstractInstantiation
- AssignmentToVoid
- CircularReference
- ConflictingReferenceConstraint
- ContinueOutsideLoop
- InvalidOverride
- InvalidTraversableImplementation
- InvalidTypeImport
- MethodSignatureMismatch
- NamedArgumentNotAllowed
- NonVariableReferenceReturn
- OverriddenMethodAccess
- ParamNameMismatch
- ReservedWord
- UnhandledMatchCondition
- UninitializedProperty
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.
- LiteralKeyUnshapedArray
- MissingOverrideAttribute
- PossiblyUndefinedIntArrayOffset
- PossiblyUndefinedStringArrayOffset
- PossiblyUnusedMethod
- PossiblyUnusedParam
- PossiblyUnusedProperty
- PossiblyUnusedReturnValue
- TaintedCallable
- TaintedCookie
- TaintedCustom
- TaintedEval
- TaintedExtract
- TaintedFile
- TaintedHeader
- TaintedHtml
- TaintedInclude
- TaintedInput
- TaintedLdap
- TaintedLlmPrompt
- TaintedNosql
- TaintedSSRF
- TaintedShell
- TaintedSleep
- TaintedSql
- TaintedSystemSecret
- TaintedTextWithQuotes
- TaintedUnserialize
- TaintedUrlComponent
- TaintedUrlPath
- TaintedUserSecret
- TaintedXpath
- UncaughtThrowInGlobalScope
- UnevaluatedCode
- UnnecessaryVarAnnotation
- UnusedClass
- UnusedClosureParam
- UnusedConstructor
- UnusedDocblockParam
- UnusedForeachValue
- UnusedMethod
- UnusedParam
- UnusedProperty
- UnusedPsalmSuppress
- UnusedReturnValue
- UnusedVariable
Running Psalm Installation (`docs/running_psalm/installation.md`)
Installation
The latest version of Psalm requires PHP >= 8.2 and Composer.
composer require --dev vimeo/psalmGenerate a config file:
./vendor/bin/psalm --initPsalm will scan your project and figure out an appropriate error level for your codebase.
Then run Psalm:
./vendor/bin/psalm --no-cachePsalm 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).
docker run -v $PWD:/app --rm -it ghcr.io/danog/psalm:latest /composer/vendor/bin/psalm --no-cacheMore 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:
wget https://github.com/vimeo/psalm/releases/latest/download/psalm.phar
chmod +x psalm.phar
./psalm.phar --versionAlternatively, you can use Composer to install the Phar:
composer require --dev psalm/pharRunning Psalm Issues (`docs/running_psalm/issues.md`)
Issue types
- AbstractInstantiation
- AbstractMethodCall
- AmbiguousConstantInheritance
- ArgumentTypeCoercion
- AssignmentToVoid
- CheckType
- CircularReference
- ClassMustBeFinal
- ComplexFunction
- ComplexMethod
- ConfigIssue
- ConflictingReferenceConstraint
- ConstantDeclarationInTrait
- ConstructorSignatureMismatch
- ContinueOutsideLoop
- DeprecatedClass
- DeprecatedConstant
- DeprecatedFunction
- DeprecatedInterface
- DeprecatedMethod
- DeprecatedProperty
- DeprecatedTrait
- DirectConstructorCall
- DocblockTypeContradiction
- DuplicateArrayKey
- DuplicateClass
- DuplicateConstant
- DuplicateEnumCase
- DuplicateEnumCaseValue
- DuplicateFunction
- DuplicateMethod
- DuplicateParam
- DuplicateProperty
- EmptyArrayAccess
- ExtensionRequirementViolation
- FalsableReturnStatement
- FalseOperand
- ForbiddenCode
- IfThisIsMismatch
- ImmutableDependency
- ImplementationRequirementViolation
- ImplementedParamTypeMismatch
- ImplementedReturnTypeMismatch
- ImplicitToStringCast
- ImpureByReferenceAssignment
- ImpureFunctionCall
- ImpureGlobalVariable
- ImpureMethodCall
- ImpurePropertyAssignment
- ImpurePropertyFetch
- ImpureStaticProperty
- ImpureStaticVariable
- ImpureVariable
- InaccessibleClassConstant
- InaccessibleMethod
- InaccessibleProperty
- IncompatibleTypeParameters
- InheritorViolation
- InterfaceInstantiation
- InternalClass
- InternalMethod
- InternalProperty
- IntMaskComparison
- InvalidArgument
- InvalidArrayAccess
- InvalidArrayAssignment
- InvalidArrayOffset
- InvalidAttribute
- InvalidCast
- InvalidCatch
- InvalidClass
- InvalidClassConstantType
- InvalidClone
- InvalidConstantAssignmentValue
- InvalidDocblock
- InvalidDocblockParamName
- InvalidEnumBackingType
- InvalidEnumCaseValue
- InvalidEnumMethod
- InvalidExtendClass
- InvalidFalsableReturnType
- InvalidFunctionCall
- InvalidGlobal
- InvalidInterfaceImplementation
- InvalidIterator
- InvalidLiteralArgument
- InvalidMethodCall
- InvalidNamedArgument
- InvalidNullableReturnType
- InvalidOperand
- InvalidOverride
- InvalidParamDefault
- InvalidParent
- InvalidPassByReference
- InvalidPropertyAssignment
- InvalidPropertyAssignmentValue
- InvalidPropertyFetch
- InvalidReturnStatement
- InvalidReturnType
- InvalidScalarArgument
- InvalidScope
- InvalidStaticInvocation
- InvalidStringClass
- InvalidTemplateParam
- InvalidThrow
- InvalidToString
- InvalidTraversableImplementation
- InvalidTypeImport
- LessSpecificClassConstantType
- LessSpecificImplementedReturnType
- LessSpecificReturnStatement
- LessSpecificReturnType
- LiteralKeyUnshapedArray
- LoopInvalidation
- MethodSignatureMismatch
- MethodSignatureMustOmitReturnType
- MethodSignatureMustProvideReturnType
- MismatchingDocblockParamType
- MismatchingDocblockPropertyType
- MismatchingDocblockReturnType
- MissingAbstractPureAnnotation
- MissingClassConstType
- MissingClosureParamType
- MissingClosureReturnType
- MissingConstructor
- MissingDependency
- MissingDocblockType
- MissingFile
- MissingImmutableAnnotation
- MissingInterfaceImmutableAnnotation
- MissingOverrideAttribute
- MissingParamType
- MissingPropertyType
- MissingPureAnnotation
- MissingReturnType
- MissingTemplateParam
- MissingThrowsDocblock
- MixedArgument
- MixedArgumentTypeCoercion
- MixedArrayAccess
- MixedArrayAssignment
- MixedArrayOffset
- MixedArrayTypeCoercion
- MixedAssignment
- MixedClone
- MixedFunctionCall
- MixedMethodCall
- MixedOperand
- MixedPropertyAssignment
- MixedPropertyFetch
- MixedPropertyTypeCoercion
- MixedReturnStatement
- MixedReturnTypeCoercion
- MixedStringOffsetAssignment
- MoreSpecificImplementedParamType
- MoreSpecificReturnType
- MutableDependency
- NamedArgumentNotAllowed
- NoEnumProperties
- NoInterfaceProperties
- NonInvariantDocblockPropertyType
- NonInvariantPropertyType
- NonStaticSelfCall
- NonVariableReferenceReturn
- NoValue
- NullableReturnStatement
- NullArgument
- NullArrayAccess
- NullArrayOffset
- NullFunctionCall
- NullIterator
- NullOperand
- NullPropertyAssignment
- NullPropertyFetch
- NullReference
- OverriddenFinalConstant
- OverriddenFinalProperty
- OverriddenInterfaceConstant
- OverriddenMethodAccess
- OverriddenPropertyAccess
- ParadoxicalCondition
- ParamNameMismatch
- ParentNotFound
- ParseError
- PluginIssue
- PossibleRawObjectIteration
- PossiblyFalseArgument
- PossiblyFalseIterator
- PossiblyFalseOperand
- PossiblyFalsePropertyAssignmentValue
- PossiblyFalseReference
- PossiblyInvalidArgument
- PossiblyInvalidArrayAccess
- PossiblyInvalidArrayAssignment
- PossiblyInvalidArrayOffset
- PossiblyInvalidCast
- PossiblyInvalidClone
- PossiblyInvalidDocblockTag
- PossiblyInvalidFunctionCall
- PossiblyInvalidIterator
- PossiblyInvalidMethodCall
- PossiblyInvalidOperand
- PossiblyInvalidPropertyAssignment
- PossiblyInvalidPropertyAssignmentValue
- PossiblyInvalidPropertyFetch
- PossiblyNullArgument
- PossiblyNullArrayAccess
- PossiblyNullArrayAssignment
- PossiblyNullArrayOffset
- PossiblyNullFunctionCall
- PossiblyNullIterator
- PossiblyNullOperand
- PossiblyNullPropertyAssignment
- PossiblyNullPropertyAssignmentValue
- PossiblyNullPropertyFetch
- PossiblyNullReference
- PossiblyUndefinedArrayOffset
- PossiblyUndefinedGlobalVariable
- PossiblyUndefinedIntArrayOffset
- PossiblyUndefinedMethod
- PossiblyUndefinedStringArrayOffset
- PossiblyUndefinedVariable
- PossiblyUnusedMethod
- PossiblyUnusedParam
- PossiblyUnusedProperty
- PossiblyUnusedReturnValue
- PrivateFinalMethod
- PropertyNotSetInConstructor
- PropertyTypeCoercion
- RawObjectIteration
- RedundantCast
- RedundantCastGivenDocblockType
- RedundantCondition
- RedundantConditionGivenDocblockType
- RedundantFlag
- RedundantFunctionCall
- RedundantFunctionCallGivenDocblockType
- RedundantIdentityWithTrue
- RedundantPropertyInitializationCheck
- ReferenceConstraintViolation
- ReferenceReusedFromConfusingScope
- ReservedWord
- RiskyCast
- RiskyTruthyFalsyComparison
- StringIncrement
- TaintedCallable
- TaintedCookie
- TaintedCustom
- TaintedEval
- TaintedExtract
- TaintedFile
- TaintedHeader
- TaintedHtml
- TaintedInclude
- TaintedInput
- TaintedLdap
- TaintedLlmPrompt
- TaintedNosql
- TaintedShell
- TaintedSleep
- TaintedSql
- TaintedSSRF
- TaintedSystemSecret
- TaintedTextWithQuotes
- TaintedUnserialize
- TaintedUrlComponent
- TaintedUrlPath
- TaintedUserSecret
- TaintedXpath
- TooFewArguments
- TooManyArguments
- TooManyTemplateParams
- Trace
- TraitMethodSignatureMismatch
- TypeDoesNotContainNull
- TypeDoesNotContainType
- UncaughtThrowInGlobalScope
- UndefinedAttributeClass
- UndefinedClass
- UndefinedConstant
- UndefinedDocblockClass
- UndefinedFunction
- UndefinedGlobalVariable
- UndefinedInterface
- UndefinedInterfaceMethod
- UndefinedMagicMethod
- UndefinedMagicPropertyAssignment
- UndefinedMagicPropertyFetch
- UndefinedMethod
- UndefinedPropertyAssignment
- UndefinedPropertyFetch
- UndefinedThisPropertyAssignment
- UndefinedThisPropertyFetch
- UndefinedTrace
- UndefinedTrait
- UndefinedVariable
- UnevaluatedCode
- UnhandledMatchCondition
- UnimplementedAbstractMethod
- UnimplementedInterfaceMethod
- UninitializedProperty
- UnnecessaryVarAnnotation
- UnrecognizedExpression
- UnrecognizedStatement
- UnresolvableConstant
- UnresolvableInclude
- UnsafeGenericInstantiation
- UnsafeInstantiation
- UnsupportedPropertyReferenceUsage
- UnsupportedReferenceUsage
- UnusedBaselineEntry
- UnusedClass
- UnusedClosureParam
- UnusedConstructor
- UnusedDocblockParam
- UnusedForeachValue
- UnusedFunctionCall
- UnusedIssueHandlerSuppression
- UnusedMethod
- UnusedMethodCall
- UnusedParam
- UnusedProperty
- UnusedPsalmSuppress
- UnusedReturnValue
- UnusedVariable
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:
(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/phporC:\php\php.exe- this should be an absolute path, not just
php
- this should be an absolute path, not just
- Args:
vendor/bin/psalm-language-server(on Windows usevendor/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 likeC:\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):
"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).
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):
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:
"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:
docker-compose exec php /usr/share/php/psalm/psalm-language-server \
-r=/var/www/html \
--map-folder=/var/www/html:$PWDSecurity 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 `
`@psalm-taint-unescape `
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 // --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 bygetRequestPath() 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
/**
* @psalm-taint-unescape html
*/
function decode(string $str): string
{
return str_replace(
['<', '>', '"', '''],
['<', '>', '"', '"'],
$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
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 inputwith astringreturn type never returns a NoSQL query.
<?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
/** @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
/**
* @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 secureSanitizing 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
/**
* @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
function takesInput(string $s) : string {
error_log("Got input");
return $s;
}
echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // Psalm detects tainted HTML hereAdding a @psalm-taint-specialize annotation solves the problem, by telling Psalm that each invocation of the function should be treated separately.
<?php
/**
* @psalm-taint-specialize
*/
function takesInput(string $s) : string {
error_log("Got input");
return $s;
}
echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // No errorA specialized function or method will still track tainted input:
<?php
/**
* @psalm-taint-specialize
*/
function takesInput(string $s) : string {
return $s;
}
echo takesInput($_GET["name"]); // Psalm detects tainted input
echo takesInput("hello"); // No errorHere 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
/**
* @psalm-pure
*/
function takesInput(string $s) : string {
return $s;
}
echo htmlentities(takesInput($_GET["name"]));
echo takesInput("hello"); // No errorSince 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
/**
* @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
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
/**
* @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
/**
* @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
/**
* @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:
<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
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
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
/**
* @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:
<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.
/* 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 // --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 functionexec@psalm-flow proxy MyClass::mySinkMethod($value)referencing a function/method of the classMyClass
Return value hint
<?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 functioninputOutputHandler 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 // --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 // --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 // --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$valueand$otherunder the key given to$key, andStore::$data[$key] -> returnmakes a call return what is stored under its key.($compute())stores
what the callables given to$computereturn, 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;selfandstaticname 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, andStore::$data[*] -> returnfor one returning what is stored under any key.[$keys[*]]is each key of a list, as
formGet(array $keys). In nested stores, likeStore::$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 asprintf()format up to its first
conversion other than%s, of animplode()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 thenew
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 withCodebase::addTaintStoreWrite()(paths from a node into the store under
the names) andCodebase::addTaintStoreRead()(paths from the store to a node), with the namesCodebase::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 // --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
$a = [1, 2, 3, 4, 5];<?php
$a = [0 => 'hello', 5 => 'goodbye'];
$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'];Makeshift Structs:
<?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:
/** @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:
/** @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.
listis a subtype ofarraylist<Foo>is a subtype ofarray.
List types show their value in a few ways:
<?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 PsalmArray 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
["hello", "world", "foo" => new stdClass, 28 => false];Psalm will type it internally as:
array{0: string, 1: string, foo: stdClass, 28: false}You can specify types in that format yourself, e.g.
/** @return array{foo: string, bar: int} */Optional keys can be denoted by a trailing ?, e.g.:
/** @return array{optional?: string, bar: int} */You can use "one-line" comments (similar to PHP), e.g:
/** @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):
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
["hello", "world", new stdClass, false];Psalm will type it internally as:
list{string, string, stdClass, false}You can specify types in that format yourself, e.g.
/** @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.:
/** @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.
/** @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.
// 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
$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.
- Scalar types
- Object types
- Array types
- Callable types
- Value types
- Utility types
- Other types
- Top and bottom types
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.
callable(Type1, OptionalType2=, SpreadType3...):ReturnTypeAdding = 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
/**
* @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 writtenpure-Closure(int): int/pure-callable(int): intClosure[write-props|io](): void- any combination of capabilitiesClosure[P](): void- a purity template declared with@psalm-purity-templateClosure[_](): 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 writtenimpure-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
/** @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
/**
* @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
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
$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:
/**
* @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
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.:
/** @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.:
/** @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:
TKey, the type of theyieldkey - default:mixedTValue, the type of theyieldvalue - default:mixedTSend, the type of thesend()method's parameter - default:mixedTReturn, the return type of thegetReturn()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 andTraversable[pure]<string, Foo>(see Iterators and generators).void- can be used in a return type when a function does not return a value.resourcerepresents a PHP resource.closed-resourcerepresents a PHP resource that was closed (usingfcloseor 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 topositive-int)int<0, max>(equivalent tonon-negative-int)int<min, -1>(equivalent tonegative-int)int<min, 0>(equivalent tonon-positive-int)int<min, max>(equivalent toint)
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
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_NAMEimplode(', ', ["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:
1212+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
nevertype 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|intand we perform an is_bool() check in a condition, the type of the variable in the condition will beneveras 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 asarray<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
$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 ofARRAY_CONST(Psalm 3.3+)key-of<list<mixed>>evaluates tointkey-of<array{a: mixed, b: mixed}|array{c: mixed}>evaluates to'a'|'b'|'c'key-of<string[]>evaluates toarray-keykey-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>.
/**
* @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 ofARRAY_CONST(Psalm 3.3+)value-of<list<float>>evaluates tofloatvalue-of<array{a: bool, b: int}|array{c: string}>evaluates tobool|int|stringvalue-of<string[]>evaluates tostringvalue-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-ofevaluates to0|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
/**
* @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.
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:
/**
* @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
/**
* @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 */; // BarIf 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
/**
* @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
/**
* @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 */; // 123Type aliases
Psalm allows defining type aliases for complex types (like array shapes) which must be reused often:
/**
* @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
/**
* @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
/**
* @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
/**
* @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
/**
* @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
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); // failsRunning Psalm Issues AbstractInstantiation (`docs/running_psalm/issues/AbstractInstantiation.md`)
AbstractInstantiation
Emitted when an attempt is made to instantiate an abstract class:
<?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
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
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
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
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
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
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
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
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
/** @psalm-check-type $x = 1 */
$x = 2; // Checked variable $x = 1 does not match $x = 2<?php
/** @psalm-check-type-exact $x = int */
$x = 2; // Checked variable $x = int does not match $x = 2Running Psalm Issues CircularReference (`docs/running_psalm/issues/CircularReference.md`)
CircularReference
Emitted when a class references itself as one of its parents
<?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
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
@apiattribute). - 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-
finalclasses.
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
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
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
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
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
/**
* @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
$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
/** @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
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
/** @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
/** @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
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
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
/** @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
class A {
public function __construct() {}
}
$a = new A;
$a->__construct(); // wrongRunning Psalm Issues DocblockTypeContradiction (`docs/running_psalm/issues/DocblockTypeContradiction.md`)
DocblockTypeContradiction
Emitted when conditional doesn't make sense given the docblock types supplied.
<?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
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
$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
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
$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
class A {}
class A {}Why this is bad
The above code wonβt compile.
PHP does allow you to define a class conditionally:
<?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
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
enum Status
{
case Open;
case Open;
}How to fix
Remove or rename the offending duplicates.
<?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
enum Status: string
{
case Open = "open";
case Closed = "open";
}How to fix
Change case values so that there are no duplicates.
<?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
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
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
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
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
$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
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
function getCommaPosition(string $a) : int {
return strpos($a, ',');
}How to fix
You can add a specific check for false:
<?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
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
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
var_dump("bah");This functions list can be extended by configuring forbiddenFunctions or forbiddenConstants in psalm.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
/**
* @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
/** @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
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
/** @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
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
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
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
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
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
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
...
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
/**
* @psalm-pure
*/
function foo(string &$a): string {
$a = "B";
return $a;
}How to fix
Just remove the mutating assignment:
<?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
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
/** @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
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
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
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
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
/** @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
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
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
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
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
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
/** @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
/** @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
/**
* @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
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
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 ofBis set;($mask & B) !== 0: at least one bit ofBis set;($mask & ~B) === 0: no bit outside ofBis set, i.e. the mask is a subset ofB;$mask === 0: no bit is set;$mask === ALL, whereALLhas 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
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
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
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
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
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
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
$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
$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
$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
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
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
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
class Foo {}
(new foo());Could also be an issue in the namespace even if the class has the correct casing
<?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
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 0This 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
$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
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
/** @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
/**
* @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
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
enum Status
{
case Open = "open";
}How to fix
Either remove the value or alter the enum to be backed.
<?php
enum Status: string
{
case Open = "open";
}Case without a value on a backed enum
<?php
enum Status: string
{
case Open;
}How to fix
Either alter the enum to be pure, or add a value.
<?php
enum Status
{
case Open;
}Case type mismatch
Case type should match the backing type of the enum.
<?php
enum Status: string
{
case Open = 1;
}How to fix
Change the types so that they match
<?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
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
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
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
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
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
$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
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
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
$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
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
$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
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
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
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
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
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
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
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
$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
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
$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
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
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
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
echo $this;Running Psalm Issues InvalidStaticInvocation (`docs/running_psalm/issues/InvalidStaticInvocation.md`)
InvalidStaticInvocation
Emitted when trying to call an instance function statically
<?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
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
/**
* @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
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
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
/**
* @implements Traversable<mixed, mixed>
*/
final class C implements Traversable {} // will cause fatal errorRunning Psalm Issues InvalidTypeImport (`docs/running_psalm/issues/InvalidTypeImport.md`)
InvalidTypeImport
Emitted when a type imported with @psalm-import-type is invalid
<?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
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 0This 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
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
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
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
/**
* @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
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
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
class A {
public function __clone() : void {}
}Running Psalm Issues MethodSignatureMustProvideReturnType (`docs/running_psalm/issues/MethodSignatureMustProvideReturnType.md`)
MethodSignatureMustProvideReturnType
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
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
/**
* @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
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
class A {}
class B {}
/**
* @return B // emitted here
*/
function foo() : A {
return new A();
}This, however, is fine:
<?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
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
class A {
public const B = 0;
}Correct with:
<?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
$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
$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
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
/**
* @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
/** @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
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
/** @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
/** @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
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
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
class A {
public $foo = 5;
}This feature is available in PHP since version 7.4
For more information see:
- Properties in the PHP manual.
- PHP RFC: Typed Properties 2.0
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
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
/**
* @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
function foo() {
return "foo";
}Correct with:
<?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
/**
* @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
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
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
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
/** @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
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
$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
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
/**
* @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
$a = $GLOBALS['foo'];How to fix
The above example can be fixed in a few ways β by adding an assert call:
<?php
$a = $GLOBALS['foo'];
assert(is_string($a));or by adding an explicit cast:
<?php
$a = (string) $GLOBALS['foo'];or by adding a docblock
<?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
$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
/** @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
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
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
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
echo $GLOBALS['foo'] + "hello";Why itβs bad
Mixed operands can have fatal consequences, e.g. here:
<?php
function foo(mixed $m) {
echo $m . 'bar';
}
class A {}
foo(new A()); // triggers fatal errorRunning 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
/** @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
/** @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
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
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
/**
* @return string[]
*/
function foo(array $a) : array {
return $a;
}This can happen with variadic arguments when @no-named-arguments is not specified:
<?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
"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
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
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
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
/** @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
/** @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
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
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
/**
* @return never
*/
function foo() : void {
exit();
}
$a = foo(); // Psalm knows $a will never contain any type because foo() won't return<?php
function foo() : void {
return throw new Exception(''); //Psalm detected the return expression is never used
}<?php
function shutdown(): never {die('Application closed unexpectedly');}
function foo(string $_a): void{}
foo(shutdown()); // foo() will never be called<?php
$a = [];
/** @psalm-suppress TypeDoesNotContainType */
assert(!empty($a));
count($a); // Assert above always fail. There is no possible type that $a can have hereRunning 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
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
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 AChildWorkarounds
You can either broaden the type or you could, in certain situations, use templates instead:
<?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
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
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
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
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
$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
$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
$arr = null;
echo $arr();Running Psalm Issues NullIterator (`docs/running_psalm/issues/NullIterator.md`)
NullIterator
Emitted when iterating over null
<?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
echo null . 'hello';Running Psalm Issues NullPropertyAssignment (`docs/running_psalm/issues/NullPropertyAssignment.md`)
NullPropertyAssignment
Emitted when trying to set a property on null
<?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
$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
$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
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
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
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
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
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
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
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
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
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
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
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
/**
* @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
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
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
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
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
$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
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
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
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
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
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
/** @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
$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
$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
$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
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
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
/** @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
$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
$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
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
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
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
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
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
function foo(string $s): void {}
foo(rand(0, 1) ? "hello" : null);Common Problem Cases
Using a Function Call inside `if`
<?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
$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
if (is_string($cat->getName()) {
changeCat();
foo($cat->getName());
}This fails since psalm cannot know if changeCat() does actually modify $cat.
Possible Solutions
- Add
@psalm-capabilities read-propsto the declaration of the other function (here:changeCat()) too - Use a variable: See above
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
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
$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
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
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
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
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
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
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
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
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
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
...
echo $arr["b"] ?? 0;Alternatively, you can ensure that the array offset always exists:
<?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
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
/**
* @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
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
/**
* @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
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
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
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
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
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
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
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
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
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
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
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
/**
* @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
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
/**
* @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
$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
$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
/**
* @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
function returnsABool(): bool {
return rand(1, 2) === 1;
}
if (returnsABool() === true) {
echo "hi!";
}
if (returnsABool() !== false) {
echo "hi!";
}To fix:
<?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
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
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
$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
$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
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
$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
/**
* @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: usestr_increment() (available since
PHP 8.3) instead. The issue is emitted whatever the PHP version the code is analysed for.
<?php
$a = "hello";
$a++;How to fix
<?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
$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
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
$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
$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)
- Example:
- 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)
- Example:
- Reading files
- Example:
file_get_contents - Risk: Sensitive data could be exposed from the filesystem. (e.g. config values, source code, user-submitted files)
- Example:
- Deleting files
- Example:
unlink - Risk: Denial of Service or potentially RCE. (e.g. deleting application code, removing a .htaccess file)
- Example:
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
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
header($_GET['header']);<?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
$name = $_GET["name"];
printName($name);
function printName(string $name) {
echo $name;
}Mitigations
- Sanitize user-input by using functions such as
htmlentitiesor 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
$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
$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
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 orbool removes the taint.
A string decoded back into an array or an object can be a query again, though: whatjson_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
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
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
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
/**
* 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
$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:
- Having an allow list of domains that can be connected to.
- 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
$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
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
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
/**
* @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
$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
htmlentitieswith theENT_QUOTESflag 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
$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
$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
$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
$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
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
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
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
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
/**
* @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
/** @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
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
$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
$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
/**
* @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
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
$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
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
/**
* @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
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
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
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
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
/**
* @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
/**
* @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
/**
* @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
/**
* @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
/** @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
/**
* @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
/**
* @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
/** @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
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
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
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
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
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
/** @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
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
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
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
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
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
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
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
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
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
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
/**
* @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
/**
* @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 invalidHow to fix
Either use new self instead of new static:
<?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
/**
* @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
class A {
public function getInstance() : self
{
return new static();
}
}Whatβs wrong here?
The problem comes when extending the class:
<?php
class A {
public function getInstance() : self
{
return new static();
}
}
class AChild extends A {
public function __construct(string $some_required_param) {}
}
AChild::getInstance(); // fatal errorHow to fix
You have two options β you can make the constructor final:
<?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
/**
* @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
class A {
public int $b = 0;
}
$a = new A();
$b = &$a->b;
$b = ''; // Fatal error- Static property assigned wrong type:
<?php
class A {
public static int $b = 0;
}
$b = &A::$b;
$b = ''; // Fatal error- Readonly property reassigned:
<?php
class A {
public function __construct(
public readonly int $b,
) {
}
}
$a = new A(0);
$b = &$a->b;
$b = 1; // Fatal errorRunning 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
/** @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
/** @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
$a = 'Hello, World!';
echo $a;<?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
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
$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:
$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
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
/**
* @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
/** @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
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
$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
$a = 'Hello, World!';
echo $a;<?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
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
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
function foo(int $a, int $b) : int {
return $a + 4;
}Can be suppressed by prefixing the unused parameter's name with an underscore:
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
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
/** @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
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
function foo() : void {
$a = 5;
$b = 4;
echo $b;
}Can be suppressed by prefixing the variable name with an underscore:
<?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
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.
composer require --dev cakephp/chronosThen generate the stubs
vendor/bin/psalm --generate-stubs=stubs/chronos.phpstubOpen 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:
- Its
typefield is set topsalm-plugin - It has
extra.psalm.pluginClasssubkey in itscomposer.jsonthat reference an entry-point class that will be invoked to register the plugin into Psalm runtime. - 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
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 internalIssueBuffer, allows handling code issues individuallyBeforeFileAnalysisInterface- 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 classFunctionExistenceProviderInterface- 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 outPsalm\Plugin\DynamicFunctionStorageto find out what api it brings for you to change function's definition.
Here are a couple of example plugins:
- StringChecker - checks class references in strings
- PreventFloatAssignmentChecker - prevents assignment to floats
- FunctionCasingChecker - checks that your functions and methods are correctly-cased
To ensure your plugin runs when Psalm does, add it to your config (not needed for composer-based plugins):
<plugins>
<plugin filename="src/plugins/SomePlugin.php" />
</plugins>You can also specify an absolute path to your plugin:
<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:
$ PSALM_ALLOW_XDEBUG=1 path/to/psalmType 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:
<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.
<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.
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 docblockAtomic 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
/** @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):
$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.
$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.
$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