Repository navigation
Taint the result of a call on an interface or base class with the returns of its overrides - #12262
Merged
Merged
Conversation
…urns of its overrides
| // unlike static::, a class it names (parent::, self::, A::) has the call run that method, and no override | ||
| if ($method_storage | ||
| && (!$stmt->class instanceof PhpParser\Node\Name || $stmt->class->toLowerString() === 'static') | ||
| ) { |
There was a problem hiding this comment.
Dynamic static calls misuse dispatch
Medium Severity
Return dispatch is wired for static calls whose class operand is not a PhpParser\Node\Name, not only for static::. PHP resolves $class::method() and $this::method() to that exact class, without late static binding, so sibling override returns should not feed the call result through the shared dispatch node.
Reviewed by Cursor Bugbot for commit 8bf85e7. Configure here.
…a node of its own
Collaborator
Author
|
bugbot review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3bb338f. Configure here.
This was referenced Oct 29, 2026
alies-dev
added a commit
to alies-dev/psalm
that referenced
this pull request
Oct 31, 2026
vimeo#12262 links a virtual call to the returns of every override, so the backward search from a generator enters every implementation of an interface it passes through, once per stack of entered calls. On Nextcloud one send reached 22.6M states and ran out of memory.
sarumaj
pushed a commit
to sarumaj/nextcloud-talk-ai
that referenced
this pull request
Nov 10, 2026
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [vimeo/psalm](https://github.com/vimeo/psalm) | `^6` → `^7` |  |  | --- ### Release Notes <details> <summary>vimeo/psalm</summary> ### [`v7.0.5`](https://github.com/vimeo/psalm/releases/tag/7.0.5) [Compare Source](vimeo/psalm@7.0.4...7.0.5) <!-- Release notes generated using configuration in .github/release.yml at e3586fb9542ad66e37122f7c7ddf6b9a9ea7eaec --> #### What's Changed ##### Fixes - feat(taint): json_encode() without JSON_PRETTY_PRINT removes the header taint by [@​jirkace](https://github.com/jirkace) in [#​12799](vimeo/psalm#12799) - Feature/callmap signature return types by [@​orklah](https://github.com/orklah) in [#​13167](vimeo/psalm#13167) - Exclude extension polyfills from PHP-versioned callmaps by [@​orklah](https://github.com/orklah) in [#​13345](vimeo/psalm#13345) - fix(methods): static return of an inherited method is the class that inherits it by [@​jirkace](https://github.com/jirkace) in [#​12994](vimeo/psalm#12994) - fix(callmap): PDO::sqliteCreateFunction() takes flags since PHP 7.1.4 by [@​jirkace](https://github.com/jirkace) in [#​12865](vimeo/psalm#12865) - fix(callmap): define() accepts any value since PHP 8.1 by [@​jirkace](https://github.com/jirkace) in [#​12992](vimeo/psalm#12992) ##### Internal changes - Add zstd cache compression with a dictionary for the consolidated cache by [@​alies-dev](https://github.com/alies-dev) in [#​13309](vimeo/psalm#13309) **Full Changelog**: <vimeo/psalm@7.0.4...7.0.5> ### [`v7.0.4`](https://github.com/vimeo/psalm/releases/tag/7.0.4) [Compare Source](vimeo/psalm@7.0.3...7.0.4) <!-- Release notes generated using configuration in .github/release.yml at fa5b1f6e665cb743838ffd865b8fc96207f9409f --> #### What's Changed ##### Fixes - fix(baseline): merge duplicated files and issue types when reading a baseline by [@​jirkace](https://github.com/jirkace) in [#​13098](vimeo/psalm#13098) - fix(visibility): drop the empty context from the inaccessible method message by [@​jirkace](https://github.com/jirkace) in [#​13094](vimeo/psalm#13094) - fix(psalter): replace the whole union return type by [@​jirkace](https://github.com/jirkace) in [#​13116](vimeo/psalm#13116) - fix(arithmetic): don't fold a shift by a negative number or zero to a negative power by [@​jirkace](https://github.com/jirkace) in [#​13103](vimeo/psalm#13103) - fix(int-range): an int range without bounds is int by [@​jirkace](https://github.com/jirkace) in [#​13091](vimeo/psalm#13091) - fix(types): a negated instanceof narrows the bound of a template param by [@​jirkace](https://github.com/jirkace) in [#​12906](vimeo/psalm#12906) - fix(types): keep named objects and other shapes next to an object with properties by [@​jirkace](https://github.com/jirkace) in [#​13101](vimeo/psalm#13101) - Perf: skip recording code use edges that nothing reads (2–32% less memory when unused code detection is off) by [@​alies-dev](https://github.com/alies-dev) in [#​13250](vimeo/psalm#13250) - Evaluate the keys of array items passed by reference once by [@​alies-dev](https://github.com/alies-dev) in [#​13297](vimeo/psalm#13297) - Fix: constants defined in Psalm's process turn into mixed on warm runs by [@​alies-dev](https://github.com/alies-dev) in [#​13302](vimeo/psalm#13302) - fix(int-range): a negative int to the power of 0 is 1 by [@​jirkace](https://github.com/jirkace) in [#​13186](vimeo/psalm#13186) - fix(stubs): SplDoublyLinkedList::offsetSet accepts null to append by [@​jirkace](https://github.com/jirkace) in [#​13207](vimeo/psalm#13207) - fix(parser): as, of and is are keys of a shape where a colon follows by [@​jirkace](https://github.com/jirkace) in [#​13075](vimeo/psalm#13075) - fix(types): $\_COOKIE can have int keys by [@​jirkace](https://github.com/jirkace) in [#​12868](vimeo/psalm#12868) - fix(config): count referencedFunction issue handler suppressions by [@​jirkace](https://github.com/jirkace) in [#​12854](vimeo/psalm#12854) - fix(scanner): an and/or condition of unknown parts defines both branches by [@​jirkace](https://github.com/jirkace) in [#​12814](vimeo/psalm#12814) - Keep the type of the elements array_splice() returns by [@​jirkace](https://github.com/jirkace) in [#​12615](vimeo/psalm#12615) - fix(param-out): do not check the [@​param-out](https://github.com/param-out) of a method without a body by [@​jirkace](https://github.com/jirkace) in [#​12681](vimeo/psalm#12681) - Ask whether the package of an unknown plugin class is installed by [@​danog](https://github.com/danog) in [#​13326](vimeo/psalm#13326) ##### Internal changes - Perf: speed up warm runs and the consolidated cache with one file per cache entry by [@​alies-dev](https://github.com/alies-dev) in [#​13249](vimeo/psalm#13249) **Full Changelog**: <vimeo/psalm@7.0.3...7.0.4> ### [`v7.0.3`](https://github.com/vimeo/psalm/releases/tag/7.0.3) [Compare Source](vimeo/psalm@7.0.2...7.0.3) <!-- Release notes generated using configuration in .github/release.yml at 8f2ac04d0ab5ed1d3cbe6e252b0f7232bc031aaf --> #### What's Changed ##### Fixes - Fix: issues found while scanning are lost with a warm cache by [@​alies-dev](https://github.com/alies-dev) in [#​13156](vimeo/psalm#13156) - fix(types): an array key that may be mixed is array-key by [@​jirkace](https://github.com/jirkace) in [#​13176](vimeo/psalm#13176) - Fix implicit native \_\_toString overrides and safeguard callmap generation by [@​orklah](https://github.com/orklah) in [#​13235](vimeo/psalm#13235) - Fix LSP lookup of builtin functions in files not analyzed in-process by [@​M393](https://github.com/M393) in [#​13237](vimeo/psalm#13237) - \[6.x] Infer return types for `array_first` and `array_last` (PHP 8.5) by [@​alies-dev](https://github.com/alies-dev) in [#​12157](vimeo/psalm#12157) - \[6.x] Infer the key parameter of `array_find`/`array_any`/`array_all` callbacks by [@​alies-dev](https://github.com/alies-dev) in [#​13245](vimeo/psalm#13245) - \[6.x] Infer the exact element removed by `array_pop`/`array_shift` by [@​alies-dev](https://github.com/alies-dev) in [#​13246](vimeo/psalm#13246) - fix(algebra): keep every orred clause of a variable when reconciling by [@​jirkace](https://github.com/jirkace) in [#​13114](vimeo/psalm#13114) - fix(types): an inherited docblock identical to the native return type keeps static of the called class by [@​jirkace](https://github.com/jirkace) in [#​12968](vimeo/psalm#12968) - fix(templates): check a method call against every type of a union template bound by [@​jirkace](https://github.com/jirkace) in [#​13106](vimeo/psalm#13106) ##### Docs - Document the Psalm GitHub actions by [@​danog](https://github.com/danog) in [#​13051](vimeo/psalm#13051) - Document --consolidate-cache by [@​alies-dev](https://github.com/alies-dev) in [#​13256](vimeo/psalm#13256) ##### Internal changes - Add guzzle/promises to the perf corpus by [@​alies-dev](https://github.com/alies-dev) in [#​12984](vimeo/psalm#12984) - Push the docker image to ghcr.io/psalm/psalm by [@​danog](https://github.com/danog) in [#​13044](vimeo/psalm#13044) - \[6.x] Push the files composer build regenerates instead of failing by [@​danog](https://github.com/danog) in [#​13147](vimeo/psalm#13147) - Perf: stop parsing the same vendor files again and again (10–55% less CPU on projects extending vendor classes) by [@​alies-dev](https://github.com/alies-dev) in [#​13234](vimeo/psalm#13234) - Social medial: announce on X and Mastodon only the releases worth a post by [@​alies-dev](https://github.com/alies-dev) in [#​13241](vimeo/psalm#13241) - Merge 6.x into master by [@​alies-dev](https://github.com/alies-dev) in [#​13243](vimeo/psalm#13243) - Merge 6.x into master by [@​alies-dev](https://github.com/alies-dev) in [#​13247](vimeo/psalm#13247) **Full Changelog**: <vimeo/psalm@7.0.2...7.0.3> ### [`v7.0.2`](https://github.com/vimeo/psalm/releases/tag/7.0.2) [Compare Source](vimeo/psalm@7.0.1...7.0.2) <!-- Release notes generated using configuration in .github/release.yml at 5cd5e2ffff7497643dd51da31ae85bb5bc593090 --> #### What's Changed ##### Fixes - Fix the caller of [@​internal](https://github.com/internal) checks in namespaced functions by [@​roxblnfk](https://github.com/roxblnfk) in [#​13136](vimeo/psalm#13136) - Perf: speed up analysis with a warm cache by [@​alies-dev](https://github.com/alies-dev) in [#​13153](vimeo/psalm#13153) - fix(unused): don't track a static property in a destructuring as a variable by [@​jirkace](https://github.com/jirkace) in [#​13100](vimeo/psalm#13100) - Fix: warm-cache crash when a file is reached under two spellings of its path by [@​alies-dev](https://github.com/alies-dev) in [#​13155](vimeo/psalm#13155) - fix(psalter): a mixed argument makes the inferred parameter type mixed by [@​jirkace](https://github.com/jirkace) in [#​13110](vimeo/psalm#13110) - Fix: cache reads crash while another worker rewrites the entry by [@​alies-dev](https://github.com/alies-dev) in [#​13154](vimeo/psalm#13154) ##### Internal changes - Only push docs changes from composer build that add issue links by [@​danog](https://github.com/danog) in [#​13148](vimeo/psalm#13148) - Fix release announcements failing on every platform by [@​danog](https://github.com/danog) in [#​13159](vimeo/psalm#13159) - Let release announcements be retried on some platforms only by [@​danog](https://github.com/danog) in [#​13160](vimeo/psalm#13160) **Full Changelog**: <vimeo/psalm@7.0.1...7.0.2> ### [`v7.0.1`](https://github.com/vimeo/psalm/releases/tag/7.0.1) [Compare Source](vimeo/psalm@7.0.0...7.0.1) <!-- Release notes generated using configuration in .github/release.yml at c3909231356b5fce30739ff0cf62bf50e6b5e7c7 --> #### What's Changed ##### Fixes - Report each property write in a restricted purity context once by [@​danog](https://github.com/danog) in [#​13067](vimeo/psalm#13067) - \[7.x] Cut peak memory 22 to 25% (4.6 to 3.6 GB on Laravel) by stopping workers before unpacking their results by [@​jarstelfox](https://github.com/jarstelfox) in [#​12573](vimeo/psalm#12573) - Fix: code used only from `__destruct` and other magic methods is reported unused by [@​alies-dev](https://github.com/alies-dev) in [#​13069](vimeo/psalm#13069) - Don't report strtolower() of a Stringable object as redundant by [@​jirkace](https://github.com/jirkace) in [#​12598](vimeo/psalm#12598) - Fix: `__callStatic` and the code it uses reported unused by [@​alies-dev](https://github.com/alies-dev) in [#​13073](vimeo/psalm#13073) - Don't crash on a constant dividing by a zero constant of another class ([#​12066](vimeo/psalm#12066)) by [@​jirkace](https://github.com/jirkace) in [#​12761](vimeo/psalm#12761) - fix(readonly): promoted properties of a readonly class are readonly by [@​jirkace](https://github.com/jirkace) in [#​12909](vimeo/psalm#12909) - fix(var-docblock): a [@​var](https://github.com/var) line without a name does not take the name of the previous line by [@​jirkace](https://github.com/jirkace) in [#​12670](vimeo/psalm#12670) - fix(messages): say that the (unset) cast was removed in PHP 8.0 by [@​jirkace](https://github.com/jirkace) in [#​12824](vimeo/psalm#12824) - fix(messages): explain impossible negated checks and calls without effect by [@​jirkace](https://github.com/jirkace) in [#​13006](vimeo/psalm#13006) - fix(types): array intersected with iterable is an array of the iterable's types by [@​jirkace](https://github.com/jirkace) in [#​13004](vimeo/psalm#13004) - fix(messages): do not tell to install a polyfill that may not ship the symbol by [@​jirkace](https://github.com/jirkace) in [#​12813](vimeo/psalm#12813) ##### Docs - Document that taint analysis runs by default by [@​roxblnfk](https://github.com/roxblnfk) in [#​13072](vimeo/psalm#13072) ##### Internal changes - Add regression tests for 92 issues already fixed in Psalm 7 by [@​alies-dev](https://github.com/alies-dev) in [#​13063](vimeo/psalm#13063) - Let a Psalm process of the end-to-end tests run for 300 seconds by [@​jirkace](https://github.com/jirkace) in [#​12613](vimeo/psalm#12613) - Push the files composer build regenerates instead of failing by [@​danog](https://github.com/danog) in [#​13138](vimeo/psalm#13138) #### New Contributors - [@​roxblnfk](https://github.com/roxblnfk) made their first contribution in [#​13072](vimeo/psalm#13072) **Full Changelog**: <vimeo/psalm@7.0.0...7.0.1> ### [`v7.0.0`](https://github.com/vimeo/psalm/releases/tag/7.0.0): Psalm 7 [Compare Source](vimeo/psalm@6.20.10...7.0.0) ### 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](https://github.com/alies-dev) has joined the Psalm maintainer team, contributing many new useful features and bugfixes! - **An official [Discord community!](https://discord.gg/RwH8rEpxt9)** - **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](#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](vimeo/psalm#11384)) runs type checking, security analysis and dead code detection together in one run, so **taint analysis is now on by default** ([#​11399](vimeo/psalm#11399)). Taint issues are reported with a `SECURITY` severity, and with a rank in SARIF reports ([#​12068](vimeo/psalm#12068), [#​11383](vimeo/psalm#11383)). The results no longer change between runs or with the thread count ([#​11946](vimeo/psalm#11946), [#​11952](vimeo/psalm#11952), [#​11953](vimeo/psalm#11953)). #### Deeper taint tracking ```php <?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 »](https://psalm.dev/r/7631291cc5)** - **New taint kinds:** [`TaintedNosql`](https://psalm.dev/docs/running_psalm/issues/TaintedNosql/) (MongoDB sinks), [`TaintedLlmPrompt`](https://psalm.dev/docs/running_psalm/issues/TaintedLlmPrompt/) (prompt injection), and [`TaintedUrlComponent`](https://psalm.dev/docs/running_psalm/issues/TaintedUrlComponent/) / [`TaintedUrlPath`](https://psalm.dev/docs/running_psalm/issues/TaintedUrlPath/) (query and path injection). curl options and `CURLFile` are reported by what they do: SSRF, header injection, file access, sleep or callables. [Every taint type is now documented](https://psalm.dev/docs/security_analysis/) ([#​11944](vimeo/psalm#11944), [#​11746](vimeo/psalm#11746), [#​12097](vimeo/psalm#12097), [#​12099](vimeo/psalm#12099), [#​12468](vimeo/psalm#12468)). - **New sources:** `php://input`, `STDIN`, the client-sent entries of `$_SERVER` and `$_FILES`, `getopt()`, `$argv`, DNS answers, exception messages, and the files, paths and URLs builtins read ([#​11945](vimeo/psalm#11945), [#​12086](vimeo/psalm#12086), [#​12246](vimeo/psalm#12246), [#​12245](vimeo/psalm#12245), [#​12135](vimeo/psalm#12135), [#​12139](vimeo/psalm#12139)). - **Taint now flows through** closures and callable parameters, generators and iterators, by-reference parameters, `foreach` by reference, global and static variables, trait method bodies, promoted constructor properties, `__toString()`, inherited methods and their overrides, exceptions, and builtin functions and objects ([#​12147](vimeo/psalm#12147), [#​12096](vimeo/psalm#12096), [#​12095](vimeo/psalm#12095), [#​12121](vimeo/psalm#12121), [#​12243](vimeo/psalm#12243), [#​12255](vimeo/psalm#12255), [#​12272](vimeo/psalm#12272), [#​12091](vimeo/psalm#12091), [#​12127](vimeo/psalm#12127), [#​12262](vimeo/psalm#12262), [#​12084](vimeo/psalm#12084), [#​12102](vimeo/psalm#12102)). - **Fewer false positives:** taints a type can't carry are removed (an `int` has no `html` taint). Array keys are tracked much more precisely. Inferred-pure functions are specialized per call, and `@psalm-cli-only` marks code whose output isn't an HTML response ([#​12079](vimeo/psalm#12079), [#​12348](vimeo/psalm#12348), [#​12150](vimeo/psalm#12150), [#​12375](vimeo/psalm#12375), [#​12048](vimeo/psalm#12048), [#​12459](vimeo/psalm#12459)). - **Library stubs:** Psalm ships taint and purity stubs for Guzzle, PSR-7, PSR-18, psr/log, Monolog, mongodb/mongodb, amphp (http-client, byte-stream, file, process), phpClickHouse, PhpSpreadsheet, Archive_Tar, php-vips, webklex/php-imap and webmozart/assert. A stub is loaded only when your code uses the library ([`loadLibraryStubs`](https://psalm.dev/docs/running_psalm/configuration/#loadlibrarystubs), on by default) ([#​12460](https://github.com/vimeo/psalm/issues/12460)–[#​12467](https://github.com/vimeo/psalm/issues/12467), [#​12481](https://github.com/vimeo/psalm/issues/12481)–[#​12484](https://github.com/vimeo/psalm/issues/12484)). - **More expressive annotations** ([docs](https://psalm.dev/docs/security_analysis/annotations/)): - conditional sinks: `@psalm-taint-sink ($mode is 0 ? null : 'html') $content` ([#​12458](vimeo/psalm#12458)); - sinks on array values only: `@psalm-taint-sink sql $params[*]` ([#​12171](vimeo/psalm#12171)); - sources under one key of the return value: `@psalm-taint-source input return['key']` ([#​12455](vimeo/psalm#12455)); - taint complements: `~(html|has_quotes)` ([#​12300](vimeo/psalm#12300)); - `@psalm-flow` through callables (`($compute()) -> return`), the receiver (`($this) -> return`), `$this` properties, and keyed stores (`($value) -> Store::$data[$key]`) ([#​12299](vimeo/psalm#12299), [#​12302](vimeo/psalm#12302), [#​12456](vimeo/psalm#12456), [#​12312](vimeo/psalm#12312), [#​12327](vimeo/psalm#12327)); - `@psalm-taint-property-bag` ([#​12486](vimeo/psalm#12486)); - custom taints declared in `psalm.xml` ([#​12330](vimeo/psalm#12330)). - **Plugin API for taints:** `Codebase::getTaintNode()`, `getTaintPropertyNode()`, `addTaintPath()`, `getReceiverTaintPath()`, and the taint nodes of a call's return value, arguments and receiver ([#​12454](vimeo/psalm#12454), [#​12414](vimeo/psalm#12414), [#​12489](vimeo/psalm#12489), [#​12535](vimeo/psalm#12535)). #### 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](https://docs.hhvm.com/hack/contexts-and-capabilities/introduction). 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](vimeo/psalm#11984)): ```php <?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 »](https://psalm.dev/r/be7eb39420)** - 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](https://psalm.dev/docs/annotating_code/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](vimeo/psalm#11984)). With `Closure[_]`, a function takes its purity from the closure it is given: ```php <?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 <?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 <?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](vimeo/psalm#12116), [#​12179](vimeo/psalm#12179), [#​12190](vimeo/psalm#12190)). - Purity templates are checked across **overrides, traits and closures** nested in a templated scope ([#​12015](vimeo/psalm#12015), [#​12126](vimeo/psalm#12126), [#​12027](vimeo/psalm#12027)). - **Inferred too:** Psalm infers the purity of calls to templated functions, and `--alter` adds `Closure[_]` to the closure parameters a function calls ([#​12077](vimeo/psalm#12077), [#​12019](vimeo/psalm#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](vimeo/psalm#12122)). See [Purity templates](https://psalm.dev/docs/annotating_code/purity_model/#purity-templates) in the Purity Model article. #### Graph-based dead code detection Unused code is now found on a graph of references ([#​11939](vimeo/psalm#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](vimeo/psalm#12487)): ```php <?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 »](https://psalm.dev/r/31c62c069b)** `--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](https://psalm.dev/docs/annotating_code/templated_annotations/#template-defaults). ```php <?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 »](https://psalm.dev/r/4e21eb262e)** - 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`](https://psalm.dev/docs/running_psalm/issues/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 <?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 »](https://psalm.dev/r/3c08a570e0)** #### 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](https://packagist.org/packages/psalm/psalm-plugin-api) metapackage tracks them instead ([#​11739](vimeo/psalm#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`](https://psalm.dev/docs/running_psalm/configuration/#loadlibraryplugins), on by default) ([#​12568](vimeo/psalm#12568), [#​12570](vimeo/psalm#12570), [#​12571](vimeo/psalm#12571)). - Plugin issues can have custom documentation URLs, and respect `ERROR_LEVEL` ([#​11735](vimeo/psalm#11735), [#​11736](vimeo/psalm#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](https://github.com/psalm/psalm-github-actions) and the [Psalm Security Scan](https://github.com/psalm/psalm-github-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: ```bash composer require --dev vimeo/psalm:^7.0 -W ``` Everything else is in the [upgrade guide](https://psalm.dev/docs/running_psalm/upgrading_to_psalm_7/). 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.10`](https://github.com/vimeo/psalm/releases/tag/6.20.10) [Compare Source](vimeo/psalm@6.20.9...6.20.10) <!-- Release notes generated using configuration in .github/release.yml at 7fc81490f861c801061836a9010603d2393bf8da --> #### What's Changed ##### Fixes - ✨ \[[#​11509](vimeo/psalm#11509)] Fix regression about iterables, list and conditional types by [@​andrew-demb](https://github.com/andrew-demb) in [#​11558](vimeo/psalm#11558) ##### Internal changes - Fix 6.x CI: retry transient stub build failures, update array_first test by [@​danog](https://github.com/danog) in [#​13407](vimeo/psalm#13407) **Full Changelog**: <vimeo/psalm@6.20.9...6.20.10> </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Berlin) - Branch creation - Between 12:00 AM and 05:59 AM, only on Monday (`* 0-5 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xNzguNSIsInVwZGF0ZWRJblZlciI6IjQ0LjIyMi41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119--> Reviewed-on: https://git.ciepiela.org/dawid/nextcloud-talk-ai/pulls/4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
Found while enabling taint analysis on a large codebase: a source returned by an implementation wasn't reported when the method was called through its interface.
The same happened for a call on a base class whose method a subclass overrides, for an abstract method, and for
static::calls.Cause
A call on an interface or base class sends its arguments to the parameters of the implementations and overrides, so a sink in their bodies is found. The other way isn't done: the result of the call only gets the return node of the method it names (
Named::name), never those of the methods that may actually run.Fix
dispatch of Named::name. That node is linked from the return nodes of the methods the dependent classes run instead, including one a class inherits from a class that isn't a dependent.@psalm-taint-specialize, or speculatively when the method can't be overridden) can't take a path of its own. Resolution only leaves it through its specializations when it has no outgoing edge. So the dispatch is linked as one more specialization of it, a call site keyed by the method's location. A flow entering that method through a specialized call still leaves it only through that call.parent::,self::andA::calls run the method they name and nothing else, so they don't take the dispatch node. A call on a final method, a private method or a constructor doesn't either.Tests
taintTheResultOfACallOnAnInterfaceWithTheReturnOfAnImplementation,taintTheResultOfACallOnAParentClassWithTheReturnOfAnOverride,taintTheResultOfACallOfAnAbstractMethodWithTheReturnOfAnImplementationandtaintTheResultOfAStaticCallOnStaticWithTheReturnOfAnOverridefail without this change.taintTheResultOfACallOnAnImplementationAlsoCalledOnItsInterfacepasses on master. It guards the specialized case: linking the dispatch straight from the implementation's return node made the call on the implementation lose its taint.dontTaintTheResultOfACallOnAClassInheritingTheMethodWithTheReturnOfASiblingOverride,dontTaintTheResultOfAParentCallWithTheReturnOfASiblingOverrideanddontTaintTheResultOfACallOnAnInterfaceWithWhatASpecializedCallOfTheImplementationReturnscheck that the dispatch doesn't reach calls that can't run the override, and that specialized calls don't leak through it.This is independent of #12100, which only dispatches arguments. The two PRs add the same
TaintFlowGraph::hasNode().The real-projects PHPUnit run now reports
print $result->output()inApplication::run()(an exception message returned byMigrateConfigurationCommand::execute()throughCommand::execute()): baselined in psalm/endtoend-test-phpunit#13.