@psalm-seal-methods / @no-seal-methods@psalm-seal-methods cannot be observed on its own: Psalm seals
magic methods by default (sealAllMethods="true"), and PHPStan / Mago / phpy
likewise distrust a __call method that has no matching @method tag. So a
call to an undeclared magic method is rejected *with or without* the seal tag,
and that rejection proves nothing about whether the tag was read.
The lever that actually discriminates is the inverse tag
@psalm-no-seal-methods: it *lifts* the default seal, so an undeclared magic
call becomes allowed. Only a tool that models the seal machinery goes silent
there; a tool that ignores the tag keeps its default diagnostic. That silence
is the real signal, so it is scored with a quiet probe on the origin tools.
References:
- Psalm supported_annotations.md: @psalm-seal-methods, @psalm-no-seal-methods
- Psalm configuration.md: sealAllMethods (default true)| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 | Recognized (no probes) | No diagnostics reported.
|
| phpstan | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Recognized (no probes) reported Lv.2+ | phpdoc_advanced_psalm_seal_methods.php:72: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed::wave(). [identifier=method.notFound] [reported-from-level=2] phpdoc_advanced_psalm_seal_methods.php:81: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Unsealed::wave(). [identifier=method.notFound] [reported-from-level=2] With strict-rulesphpdoc_advanced_psalm_seal_methods.php:72: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed::wave(). [identifier=method.notFound] phpdoc_advanced_psalm_seal_methods.php:81: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Unsealed::wave(). [identifier=method.notFound]
|
| psalm | 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac next: 7.0.0-beta19 |
Enforced | phpdoc_advanced_psalm_seal_methods.php:72: Magic method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed::wave does not exist [UndefinedMagicMethod]
|
| mago | 1.46.0mago 1.46.0 | Recognized (no probes) | phpdoc_advanced_psalm_seal_methods.php:72: Ambiguous method call to `wave` on class `Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed`. [non-documented-method] phpdoc_advanced_psalm_seal_methods.php:81: Ambiguous method call to `wave` on class `Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Unsealed`. [non-documented-method]
|
| mir | 0.70.1mir 0.70.1 | Recognized (no probes) | No diagnostics reported.
|
| phpantom | 0.9.0phpantom_lsp 0.9.0 | Recognized (no probes) | No diagnostics reported.
|
| intelephense | 1.18.5intelephense 1.18.5 | Recognized (no probes) | No diagnostics reported.
|
| phpy | 0.2.0phpy 0.2.0 | Recognized (no probes) | phpdoc_advanced_psalm_seal_methods.php:72: Method Sealed::wave() is not defined. The call is passed to the magic Sealed::__call() method. phpdoc_advanced_psalm_seal_methods.php:81: Method Unsealed::wave() is not defined. The call is passed to the magic Unsealed::__call() method.
|
| qodana | 262.8665.325Qodana 262.8665.325 | Recognized (no probes) | No diagnostics reported.
|
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Recognized (no probes) | phpdoc_advanced_psalm_seal_methods.php:40: param $arguments miss matched with phpdoc type <<list<mixed>>> [funcParamTypeMissMatch] phpdoc_advanced_psalm_seal_methods.php:60: param $arguments miss matched with phpdoc type <<list<mixed>>> [funcParamTypeMissMatch]
|
| steins | 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) | Recognized (no probes) | No diagnostics reported.
|
| phpstan-strict | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Recognized (no probes) | phpdoc_advanced_psalm_seal_methods.php:72: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed::wave(). [identifier=method.notFound] phpdoc_advanced_psalm_seal_methods.php:81: Call to an undefined method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Unsealed::wave(). [identifier=method.notFound]
|
| psalm-next | 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 | Enforced | phpdoc_advanced_psalm_seal_methods.php:72: Magic method Conformance\Tests\PhpdocAdvancedPsalmSealMethods\Sealed::wave does not exist [UndefinedMagicMethod]
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\PhpdocAdvancedPsalmSealMethods;
/**
* Cross-tool handling of `@psalm-seal-methods` / `@no-seal-methods`.
*
* The positive `@psalm-seal-methods` cannot be observed on its own: Psalm seals
* magic methods by default (`sealAllMethods="true"`), and PHPStan / Mago / phpy
* likewise distrust a `__call` method that has no matching `@method` tag. So a
* call to an undeclared magic method is rejected *with or without* the seal tag,
* and that rejection proves nothing about whether the tag was read.
*
* The lever that actually discriminates is the inverse tag
* `@psalm-no-seal-methods`: it *lifts* the default seal, so an undeclared magic
* call becomes allowed. Only a tool that models the seal machinery goes silent
* there; a tool that ignores the tag keeps its default diagnostic. That silence
* is the real signal, so it is scored with a quiet probe on the origin tools.
*
* References:
* - Psalm supported_annotations.md: `@psalm-seal-methods`, `@psalm-no-seal-methods`
* - Psalm configuration.md: `sealAllMethods` (default true)
*/
/**
* Sealed (the default). An undeclared magic call is rejected here, but every
* default-sealing tool does that regardless of the tag, so the diagnostic is
* baseline noise rather than evidence the seal tag was honoured.
*
* @method string greet()
* @psalm-seal-methods
*/
final class Sealed // T: @psalm-seal-methods
{
/**
* @param list<mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed // E?[noise]: some tools flag the docblock/native array mismatch
{
return $name === 'greet' ? 'hello' : throw new \BadMethodCallException($name);
}
}
/**
* Explicitly unsealed. `@psalm-no-seal-methods` lifts the default seal, so a
* tool that honours the seal family must now *allow* an undeclared magic call.
* Silence is the discriminator; a tool that ignores the tag keeps its default
* "undefined magic method" diagnostic.
*
* @method string greet()
* @psalm-no-seal-methods
*/
final class Unsealed // T: @psalm-no-seal-methods
{
/**
* @param list<mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed // E?[noise]: some tools flag the docblock/native array mismatch
{
return $name === 'greet' ? 'hello' : throw new \BadMethodCallException($name);
}
}
$sealed = new Sealed();
$sealed->greet(); // V
// Rejected by every default-sealing tool, tag or no tag — baseline, not the
// seal tag under test. A bare statement avoids a confounding mixed-return
// diagnostic from the surrounding expression.
$sealed->wave(); // E?[noise]: undeclared magic method rejected by default sealing
$unsealed = new Unsealed();
$unsealed->greet(); // V
// Honouring the seal family means `@psalm-no-seal-methods` lifts the seal and
// this undeclared call is allowed (silence). A tool that ignores the tag still
// reports it; that report is noise, not tag enforcement. Quiet is origin-only
// (Psalm/pzoom), the only tools confirmed to model the no-seal lever.
$unsealed->wave(); // Q?<psalm> // Q?<pzoom> // E?[noise]