@psalm-seal-properties / @no-seal-properties@psalm-seal-properties cannot be observed on its own: Psalm
seals magic properties by default (sealAllProperties="true"), and PHPStan /
Mago / phpy likewise distrust a __get/__set pair with no matching
@property tag. So access to an undeclared magic property 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-properties: it *lifts* the default seal, so an undeclared
magic property 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-properties, @psalm-no-seal-properties
- Psalm configuration.md: sealAllProperties (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_properties.php:75: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag::$extra. [identifier=property.notFound] [reported-from-level=2] phpdoc_advanced_psalm_seal_properties.php:84: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\UnsealedBag::$extra. [identifier=property.notFound] [reported-from-level=2] With strict-rulesphpdoc_advanced_psalm_seal_properties.php:75: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag::$extra. [identifier=property.notFound] phpdoc_advanced_psalm_seal_properties.php:84: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\UnsealedBag::$extra. [identifier=property.notFound]
|
| psalm | 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac next: 7.0.0-beta19 |
Enforced | phpdoc_advanced_psalm_seal_properties.php:75: Magic instance property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag::$extra is not defined [UndefinedMagicPropertyAssignment]
|
| mago | 1.46.0mago 1.46.0 | Recognized (no probes) | phpdoc_advanced_psalm_seal_properties.php:75: Ambiguous property access: $extra on class `Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag`. [non-documented-property] phpdoc_advanced_psalm_seal_properties.php:84: Ambiguous property access: $extra on class `Conformance\Tests\PhpdocAdvancedPsalmSealProperties\UnsealedBag`. [non-documented-property]
|
| 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_properties.php:75: Property $extra accessed via magic method SealedBag::__set(): void. phpdoc_advanced_psalm_seal_properties.php:84: Property $extra accessed via magic method UnsealedBag::__set(): void.
|
| 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) | No diagnostics reported.
|
| 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_properties.php:75: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag::$extra. [identifier=property.notFound] phpdoc_advanced_psalm_seal_properties.php:84: Access to an undefined property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\UnsealedBag::$extra. [identifier=property.notFound]
|
| psalm-next | 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 | Enforced | phpdoc_advanced_psalm_seal_properties.php:75: Magic instance property Conformance\Tests\PhpdocAdvancedPsalmSealProperties\SealedBag::$extra is not defined [UndefinedMagicPropertyAssignment]
|
<?php
declare(strict_types=1);
namespace Conformance\Tests\PhpdocAdvancedPsalmSealProperties;
/**
* Cross-tool handling of `@psalm-seal-properties` / `@no-seal-properties`.
*
* The positive `@psalm-seal-properties` cannot be observed on its own: Psalm
* seals magic properties by default (`sealAllProperties="true"`), and PHPStan /
* Mago / phpy likewise distrust a `__get`/`__set` pair with no matching
* `@property` tag. So access to an undeclared magic property 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-properties`: it *lifts* the default seal, so an undeclared
* magic property 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-properties`, `@psalm-no-seal-properties`
* - Psalm configuration.md: `sealAllProperties` (default true)
*/
/**
* Sealed (the default). An undeclared magic property 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.
*
* @property string $name
* @psalm-seal-properties
*/
final class SealedBag // T: @psalm-seal-properties
{
public function __get(string $name): mixed
{
return $name === 'name' ? '' : null;
}
public function __set(string $name, mixed $value): void
{
}
}
/**
* Explicitly unsealed. `@psalm-no-seal-properties` lifts the default seal, so a
* tool that honours the seal family must now *allow* an undeclared magic
* property. Silence is the discriminator; a tool that ignores the tag keeps its
* default "undefined magic property" diagnostic.
*
* @property string $name
* @psalm-no-seal-properties
*/
final class UnsealedBag // T: @psalm-no-seal-properties
{
public function __get(string $name): mixed
{
return $name === 'name' ? '' : null;
}
public function __set(string $name, mixed $value): void
{
}
}
$sealed = new SealedBag();
$sealed->name = 'Ada'; // V
// Rejected by every default-sealing tool, tag or no tag — baseline, not the
// seal tag under test.
$sealed->extra = 1; // E?[noise]: undeclared magic property rejected by default sealing
$unsealed = new UnsealedBag();
$unsealed->name = 'Ada'; // V
// Honouring the seal family means `@psalm-no-seal-properties` lifts the seal and
// this undeclared assignment 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->extra = 1; // Q?<psalm> // Q?<pzoom> // E?[noise]