Summary
Running psalm --taint-analysis over a production PHP codebase crashes the
entire run with an uncaught UnexpectedValueException raised deep inside
Psalm\Internal\Codebase\Methods::getStorage() (line 1154 on 6.16.1) instead of
reporting an UndefinedMethod-style finding.
The crash kills the whole scan with no report produced, which is fatal for
pipeline usage (we run Psalm's taint mode as one stage of a multi-tool
security scan).
Environment
- Psalm: 6.16.1 (build f1f5de5), installed via composer global
- PHP: 8.x CLI (macOS arm64)
- Command:
psalm --taint-analysis --output-format=json --report=<path> --no-progress --config=<generated psalm.xml>
Error
Uncaught Amp\Parallel\Worker\TaskFailureException: UnexpectedValueException thrown in context
with message "$storage should not be null for
App\Authenticate\Manager\LdapManagerUserInterface::setusername" and code "0"
in .../vimeo/psalm/src/Psalm/Internal/Codebase/Methods.php:1154
Stack trace in context:
#0 .../Psalm/Internal/Codebase/Methods.php(1154): Methods->getStorage(...)
#1 .../Psalm/Internal/Analyzer/MethodAnalyzer.php(73): Psalm\Internal\Codebase\Methods->getStorage(Object(Psalm\Internal\MethodIdentifier))
#2 .../Psalm/Internal/Analyzer/InterfaceAnalyzer.php(161): MethodAnalyzer->__construct(...)
#3 .../Psalm/Internal/Analyzer/FileAnalyzer.php(192): InterfaceAnalyzer->analyze()
#4 .../Psalm/Internal/Codebase/Analyzer.php(1486): FileAnalyzer->analyze()
#5 .../Psalm/Internal/Fork/AnalyzerTask.php(28): Analyzer::analysisWorker(...)
...
(Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac crashed due to an uncaught Throwable)
Context (anonymized excerpts from the affected codebase)
The lookup that crashes is for setusername — note the lowercase method
name in the message. In the analyzed code, the same name exists in different
casing across related types:
// OpsUserInterface.php — declares the method as setUserName (capital N)
interface OpsUserInterface
{
public function setUserName(string $username): self;
}
// OpsUser.php — implements it as setUsername (lowercase n), legal PHP
final class OpsUser implements OpsUserInterface
{
public function setUsername(string $username): OpsUserInterface { ... }
}
// OpsUserProvider.php — property docblocks and a fluent call chain
/**
* @var LdapManagerUserInterface
*/
private $ldapManager;
/**
* @var OpsUserInterface
*/
private $userClass;
// ...
$user = $this->userClass;
$user
->setUsername($username)
->setManagerId($managerId)
->setDn($lm->getDn())
...
LdapManagerUserInterface is a separate interface that does not declare
any setUsername variant — yet Psalm constructs MethodIdentifier for
LdapManagerUserInterface::setusername (case-normalized) during interface
analysis, and getStorage() throws because that key has no storage.
Expected behavior
A method identifier that fails to resolve against a declaring class should
surface as a normal analyzer finding (e.g. UndefinedMethod /
MixedMethodCall-style) — or at most a skipped file with a warning. It must
never escape as an uncaught Throwable that terminates the entire analysis.
The root question is also worth looking at on its own: PHP method dispatch is
case-insensitive, so the interface/implementer casing mismatch above is valid
code; the storage lookup appears to lose the case-insensitive guarantee when
the identifier is normalized against a declaring class that received the
method via a case-mismatched implementation.
Notes
- We could not reduce this below the multi-file Laravel-style project context;
the excerpts above are the closest involved types. Happy to share a fuller
reproduction project privately or test a patch against the original codebase.
- Reproduced twice with identical stacks (identical run completed normally
after the involved docblock annotations were corrected).
Summary
Running
psalm --taint-analysisover a production PHP codebase crashes theentire run with an uncaught
UnexpectedValueExceptionraised deep insidePsalm\Internal\Codebase\Methods::getStorage()(line 1154 on 6.16.1) instead ofreporting an
UndefinedMethod-style finding.The crash kills the whole scan with no report produced, which is fatal for
pipeline usage (we run Psalm's taint mode as one stage of a multi-tool
security scan).
Environment
psalm --taint-analysis --output-format=json --report=<path> --no-progress --config=<generated psalm.xml>Error
Context (anonymized excerpts from the affected codebase)
The lookup that crashes is for
setusername— note the lowercase methodname in the message. In the analyzed code, the same name exists in different
casing across related types:
LdapManagerUserInterfaceis a separate interface that does not declareany
setUsernamevariant — yet Psalm constructsMethodIdentifierforLdapManagerUserInterface::setusername(case-normalized) during interfaceanalysis, and
getStorage()throws because that key has no storage.Expected behavior
A method identifier that fails to resolve against a declaring class should
surface as a normal analyzer finding (e.g.
UndefinedMethod/MixedMethodCall-style) — or at most a skipped file with a warning. It mustnever escape as an uncaught
Throwablethat terminates the entire analysis.The root question is also worth looking at on its own: PHP method dispatch is
case-insensitive, so the interface/implementer casing mismatch above is valid
code; the storage lookup appears to lose the case-insensitive guarantee when
the identifier is normalized against a declaring class that received the
method via a case-mismatched implementation.
Notes
the excerpts above are the closest involved types. Happy to share a fuller
reproduction project privately or test a patch against the original codebase.
after the involved docblock annotations were corrected).