Skip to content

Update dependency vimeo/psalm to v7 - #28

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/vimeo-psalm-7.x
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/vimeo-psalm-7.x

Conversation

@renovate

@renovate renovate Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
vimeo/psalm ^6.13 → ^7.0 age adoption passing confidence

Release Notes

vimeo/psalm

v7.0.0: Psalm 7

Compare Source

Psalm 7

Announcing the release of Psalm 7!

Featuring a simply enormous number of new features and bug fixes, such as:

  • security analysis on by default: fully refactored for 10x+ performance and much greater coverage, in the same run as type checking and dead code detection;
  • much deeper security analysis taint tracking: NoSQL, LLM prompt and URL injection, new sources like php://input and uploaded file names, flows through closures, generators, traits and by-reference parameters, and ready-made stubs for Guzzle, PSR-7, Monolog, MongoDB and more.
  • a new purity model based on Hack's capabilities: declare exactly which side effects a function may have (io, time, write-props...), with purity templates for classes, closures and automatically inferred annotations;
  • graph-based dead code detection: If nothing reachable uses a class, function, method, constant, variable, etc, Psalm finds it, now properly handling unused cycles as well.
  • Template defaults (@template TError of Throwable = RuntimeException) for classes, functions and methods: Result<int> just works, and libraries can add templates without breaking their users.
  • PHP 8.5 and 8.6 support: Full support for Partial Function Application, the pipe operator, and much more!
  • Daily releases. Features and fixes reach you the day they land, no more waiting on dev-master.
  • Framework plugins in the box. The PHPUnit, Symfony, Laravel, Mockery and sabre/event plugins ship with Psalm and switch on by themselves. Zero setup.
  • a bigger Psalm maintainer team: @​alies-dev has joined the Psalm maintainer team, contributing many new useful features and bugfixes!
  • An official Discord community!
  • And so, so many other new features and bugfixes!

Psalm 7 runs on 64-bit PHP 8.2 to 8.6. Upgrading is simple for most projects: see Upgrading below.

Security analysis: faster, always on, in a single run

Taints are now stored as a bitmask, and the taint graph is resolved over compact summaries of each function. This makes security analysis up to 10x faster.

Combined analysis (#​11384) runs type checking, security analysis and dead code detection together in one run, so taint analysis is now on by default (#​11399).

Taint issues are reported with a SECURITY severity, and with a rank in SARIF reports (#​12068, #​11383). The results no longer change between runs or with the thread count (#​11946, #​11952, #​11953).

Deeper taint tracking

<?php
$run = function (string $c): void {
    shell_exec($c);
};
$run((string) $_GET['cmd']); // TaintedShell, through the closure

$mongo = new MongoDB\Driver\Manager('mongodb://localhost');
$mongo->executeQuery('db.users', new MongoDB\Driver\Query(['name' => $_GET['name']])); // TaintedNosql

/** @return mixed */
function body() {
    return json_decode((string) file_get_contents('php://input'), true);
}
eval((string) body()); // TaintedEval: php://input is a source

Try it on psalm.dev »

A new purity model: capabilities

@psalm-pure, @psalm-mutation-free and @psalm-external-mutation-free are replaced by capabilities, modelled on Hack's contexts and capabilities. A function-like declares exactly which side effects it may have: read-props, write-this-props, write-props, read-globals, write-globals, write-refs, io and time, with pure and impure at the two ends (#​11984):

<?php
/** @psalm-capabilities time */
function today(): string {
    return date('Y-m-d'); // no issue
}

/** @psalm-pure */
function todayWrong(): string {
    return date('Y-m-d'); // ImpureFunctionCall: ... requires time
}

Try it on psalm.dev »

  • Psalm infers purity across the call graph. Where an annotation could be added, it can suggest one: set <psalm suggestPurityAnnotations="true"> to get MissingPureAnnotation and MissingImmutableAnnotation (and their abstract and interface variants), and run --alter --issues=MissingPureAnnotation,MissingImmutableAnnotation to add the annotations. The --alter command works without the attribute. The suggestions are opt-in.
  • Pure and read-only functions are taint-specialized automatically, so annotating your code also removes false positives from security analysis.
  • Builtins, SPL, the Random, phpredis and MongoDB extensions and more declare what they do.
  • clone, __toString(), __invoke(), ArrayAccess, destructors, new $class and parameter defaults are now checked too.
  • @psalm-pure can be used on classes, and plugins can declare the capabilities of the methods they handle.

The new Purity Model article explains the model in full.

Purity templates

Many functions are only as pure as what they are given: array_map() is pure when its callback is. Purity templates are template parameters whose values are capability sets instead of types, like Hack's [ctx $f] contexts. A function can now be both pure and higher-order, and each call costs only what its arguments do (#​11984).

With Closure[_], a function takes its purity from the closure it is given:

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

/** @psalm-pure */
function double(int $x): int {
    return apply(fn(int $y): int => $y * 2, $x); // no issue: the closure is pure
}

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

/** @psalm-pure */
function lengths(array $words): array {
    return array_map(fn(string $w): int => strlen($w), $words); // no issue: builtins taking callbacks work the same way
}

On a class, a purity template lets each subclass decide what its methods may do:

<?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 */
    #[Override]
    public function handle(string $input): string {
        return strtoupper($input);
    }
}

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

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

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

Traversable, Iterator, IteratorAggregate and Generator have a purity template, for what iterating over them does. iterable has one too. A generator with a purity annotation binds it to what its body does:

<?php
/**
 * @psalm-pure
 * @param Traversable[pure]<int, int> $xs
 */
function sum(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // no issue
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @param Traversable<int, int> $xs
 */
function sumAny(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // ImpureMethodCall: The context is pure but iterating over Traversable<int, int> requires impure
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @return Generator<int, int>
 */
function numbers(): Generator {
    yield 1;
    yield 2;
}

/** @psalm-pure */
function total(): int {
    return sum(numbers()); // no issue: numbers() returns a Generator[pure]
}
  • Syntax: purity arguments go in square brackets before the type arguments, as in Hack: Closure[io](int): void, Handler[pure], Iterator[pure]<int, string>, Box[io]<int>. Named templates are declared with @psalm-purity-template P and used with @psalm-purity-from-template P.
  • Bounds and defaults are written as a chain: @psalm-purity-template write-this-props <= C(write-this-props) <= write-this-props|write-props|io. A method may use the lower bound unconditionally. Subclasses must stay within the bounds, and get the default when they don't bind the template.
  • Constructors can bind a class template from their arguments, with a Closure[P] parameter or a conditional @return on __construct. For example, new ArrayObject([...]) is an ArrayObject[pure], so a pure function may fill an ArrayObject it created (#​12116, #​12179, #​12190).
  • Purity templates are checked across overrides, traits and closures nested in a templated scope (#​12015, #​12126, #​12027).
  • Inferred too: Psalm infers the purity of calls to templated functions, and --alter adds Closure[_] to the closure parameters a function calls (#​12077, #​12019).
  • Builtins and SPL taking callbacks or iterators are declared with purity templates: array_map, usort, preg_replace_callback, iterator_to_array, CallbackFilterIterator, Fiber and more (#​12122).

See Purity templates in the Purity Model article.

Graph-based dead code detection

Unused code is now found on a graph of references (#​11939). Code is used only if it can be reached from an entry point, so classes and methods referenced only by other unused code, cycles included, are reported. Exceptions that are created but never thrown are reported too (#​12487):

<?php
final class Service {
    public function run(int $id): void {
        if ($id < 0) {
            new InvalidArgumentException('negative id'); // UnusedMethodCall: ... never thrown or used
        }
        echo $id;
    }
}

final class Ping {                                     // UnusedClass
    public static function ping(): void { Pong::pong(); }
}

final class Pong {                                     // UnusedClass
    public static function pong(): void { Ping::ping(); }
}

(new Service())->run(1);

Try it on psalm.dev »

--find-unused-psalm-suppress now also checks suppressions on classes and on Tainted* issues.

Type system

  • Template defaults: a template can declare a default type, @template T = Default or @template T of Bound = Default, on classes, interfaces, traits, functions and methods. Psalm uses it when the type argument is left out: in @extends/@implements/@use, in a generic type with only its first arguments (Result<int>), in new, and in calls that don't infer the template. Libraries can add a template without breaking every Foo<A> their users wrote, and those users no longer get MissingTemplateParam. See Template defaults.

    <?php
    /**
     * @template TValue
     * @template TError of Throwable = RuntimeException
     */
    final class Result {
        /** @var TError|null */
        public ?Throwable $error = null;
    }
    
    /** @param Result<int> $result */
    function handle(Result $result): void {
        $error = $result->error; // RuntimeException|null (Psalm 6: MissingTemplateParam, Throwable|null)
    }
    
    /** @template T */
    interface Option {
        /**
         * @template TResult = T
         * @param (callable(T): TResult)|null $map
         * @return Option<TResult>
         */
        public function map(?callable $map = null): Option;
    }
    
    /** @param Option<int> $option */
    function check(Option $option): void {
        $same = $option->map(); // Option<int> (Psalm 6: Option<never>)
    }

    Try it on psalm.dev »

  • New types and syntax: decimal-int-string and non-decimal-int-string, global constant wildcards, conditional types on types that depend on a template, is not in conditional types, and exact intersections of scalar and conditional types.

  • Constructor @return types.

  • New IntMaskComparison issue for int-masks compared by value.

PHP 8.5 and 8.6

  • Psalm runs on PHP 8.6 and has a PHP 8.6 call map.

  • The PHP 8.5 pipe operator and PHP 8.6 partial function application are fully typed: Psalm infers the type of every pipeline stage and the signature of every partially applied closure, so a stage that doesn't fit the previous one is reported :

    <?php
    function slug(string $title): string {
        return $title
            |> trim(...)
            |> strtolower(...)
            |> str_replace(' ', '-', ?); // no issue: each stage is typed
    }
    
    function lowercaseWords(string $title): string {
        return $title |> explode(' ', ?) |> strtolower(...); // InvalidArgument: Argument 1 of strtolower expects string, but non-empty-list<string> provided
    }
    
    function wrongLength(string $title): string {
        return $title |> trim(...) |> strlen(...); // InvalidReturnStatement: 'int<0, max>' does not match 'string'
    }

    Try it on psalm.dev »

Plugins

  • Versioned plugin API: Psalm's version number no longer tracks the plugin API. Breaking changes to the plugin API may ship in any Psalm release, including minor and patch releases. The psalm/psalm-plugin-api metapackage tracks them instead (#​11739). Its major version is bumped for every breaking change, and its readme lists the changes. Plugins should require it with a caret range (^0.4): Composer will then only install the plugin with a Psalm release whose plugin API it was written for.
  • Bundled plugins: Psalm requires psalm/plugin-phpunit, plugin-mockery, plugin-symfony, plugin-laravel and plugin-sabre-event, and enables each one, plus the webmozart/assert plugin, when your composer.json uses the matching library (loadLibraryPlugins, on by default) (#​12568, #​12570, #​12571).
  • Plugin issues can have custom documentation URLs, and respect ERROR_LEVEL (#​11735, #​11736).

CLI and configuration

  • --init-ci generates a GitHub Actions workflow that runs the official Docker image, pinned to the Psalm version in your composer.lock. The docs now cover the Psalm GitHub Action and the Psalm Security Scan action, which sends security analysis results to GitHub code scanning.
  • The official Docker image is now ghcr.io/psalm/psalm.
  • The old compact output format is renamed table, and a new, truly compact compact format is added.
  • <unsuppressibleIssues> makes the issues it lists impossible to suppress.
  • Faster startup: less scanning, and single-file scans no longer build the full project file list. Composer's autoloader is no longer deep-scanned.
  • More detailed progress output, including for taint graph resolution, and short, actionable CLI messages.

Upgrading

For most projects, upgrading is as simple as updating the Composer dependency:

composer require --dev vimeo/psalm:^7.0 -W

Everything else is in the upgrade guide.

Most projects will notice:

  • Taint analysis runs by default. Set runTaintAnalysis="false" to opt out.
  • If enabled via <psalm suggestPurityAnnotations="true">, new MissingPureAnnotation/MissingImmutableAnnotation issues appear for code that can be annotated. Fix them with --alter --issues=MissingPureAnnotation,MissingImmutableAnnotation. The annotations Psalm adds improve both security analysis and type inference:
    • Fewer security false positives: Psalm specializes the taints of calls to pure and read-only functions and methods per call site. Tainted input passed at one call then no longer taints the result of every other call. Without annotations, Psalm can only infer this for functions and methods that can't be overridden.
    • Better type inference: Psalm remembers the results of methods of immutable classes between calls, so $a->get() !== null ? $a->get() : '' is a string. Without @psalm-immutable, the second call is still ?string.
    • Checked contracts: pure code can call the annotated functions, and every override and implementation is checked not to do more than the annotation allows.

Thanks

Thanks to everyone who contributed to Psalm 7, and to everyone who tested the betas and release candidates and reported issues!

Full Changelog: vimeo/psalm@6.20.3...7.0.0

v6.20.7

Compare Source

What's Changed

Docs

Full Changelog: vimeo/psalm@6.20.6...6.20.7

v6.20.6

Compare Source

What's Changed

Internal changes

Full Changelog: vimeo/psalm@6.20.5...6.20.6


Configuration

📅 Schedule: (in timezone UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

Read more information about the use of Renovate Bot within Laminas.

| datasource | package     | from   | to    |
| ---------- | ----------- | ------ | ----- |
| packagist  | vimeo/psalm | 6.20.5 | 7.0.0 |


Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@renovate renovate Bot added the renovate label Nov 2, 2026
@renovate

renovate Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: composer.lock
Loading composer repositories with package information
Dependency psalm/plugin-phpunit is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies (-W) to include root dependencies.
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires vimeo/psalm ^7.0, found vimeo/psalm[7.0.0] but these were not loaded, likely because it conflicts with another require.
  Problem 2
    - psalm/plugin-phpunit is locked to version 0.19.7 and an update of this package was not requested.
    - psalm/plugin-phpunit 0.19.7 requires vimeo/psalm dev-master || ^6.10.0 -> found vimeo/psalm[6.10.0, ..., 6.20.6] but it conflicts with your root composer.json require (^7.0).

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants