← All results

@psalm-seal-properties / @no-seal-properties

Group: Advanced PHPDoc types · Category: ecosystem · File: phpdoc_advanced_psalm_seal_properties.php

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)

Analyzer results

AnalyzerVersionResultDiagnostics
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.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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-rules
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]
  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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]
  • Recognition: spelling resolved
  • Enforcement: 1/1 of the expected violations reported
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]
  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
mir 0.70.1mir 0.70.1 Recognized (no probes)

No diagnostics reported.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
phpantom 0.9.0phpantom_lsp 0.9.0 Recognized (no probes)

No diagnostics reported.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
intelephense 1.18.5intelephense 1.18.5 Recognized (no probes)

No diagnostics reported.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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.
  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
qodana 262.8665.325Qodana 262.8665.325 Recognized (no probes)

No diagnostics reported.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
steins 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) Recognized (no probes)

No diagnostics reported.

  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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]
  • Recognition: spelling resolved
  • Enforcement: not measured — this test carries no rejection probes
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]
  • Recognition: spelling resolved
  • Enforcement: 1/1 of the expected violations reported

Source

<?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]