Skip to content

Fix GH-16643: Improve incompatible trait property error message - #25047

Open
glbessa wants to merge 1 commit into
php:masterfrom
glbessa:gh16643-trait-property-error
Open

glbessa wants to merge 1 commit into
php:masterfrom
glbessa:gh16643-trait-property-error

Conversation

@glbessa

@glbessa glbessa commented Oct 30, 2026

Copy link
Copy Markdown

Closes GH-16643

When a class and a trait (or two traits) define the same property incompatibly, the error currently only says that "the definition differs and is considered incompatible", which is confusing in cases like the one from the issue (a constructor promoted property has no default value, while the trait's property does).

This splits the single compatibility check in zend_do_traits_property_binding() and appends the reason to the existing message:

Foo and FooTrait define the same property ($options) in the composition of Foo. However, the definition differs and is considered incompatible: only one of them has a default value. Class was composed in %s on line %d

Possible reasons, checked in this order:

  • the visibility differs
  • only one of them is static
  • only one of them is readonly
  • the types differ
  • only one of them has a default value
  • the default values differ

The start of the message is unchanged, so only the expected output of existing tests had to be updated; each reason is covered by at least one of them, plus a new test for the case from the issue.

Trait constants have a similar generic message (emit_incompatible_trait_constant_error()); I left them out to keep this focused, but I'm happy to follow up if desired. I also didn't add a NEWS/UPGRADING entry since this is only an error message change; let me know if one is wanted.

🤖 Generated with Claude Code

When a class and a trait (or two traits) define the same property in an
incompatible way, the error message now states why the definitions are
considered incompatible: differing visibility, static or readonly
modifiers, types, or default values, including the case where only one
of them has a default value (e.g. a constructor promoted property).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anun333

anun333 commented Oct 30, 2026

Copy link
Copy Markdown

Quoted below: written with Claude, an LLM, as CONTRIBUTING.md asks us to mark. The builds and test runs behind it are on my machine; the commands to repeat them are under the fold.

Tested on today's master (1a72e5f2542) with this PR cherry-picked, debug builds. The reason given, trait property first, class property second (master gives the message without a reason for every incompatible row):

Trait / class this PR
public $p = 1 / protected $p = 1 the visibility differs
public static $p = 1 / public $p = 1 only one of them is static
public readonly int $p / public int $p only one of them is readonly
public int $p = 1 / public ?int $p = 1 the types differ
public int|string $p = 1 / public string|int $p = 1 composes (as on master)
private array $o = [] / promoted private array $o = [] (the issue) only one of them has a default value
public $p = 1 / public $p = 2 the default values differ
public $p / public $p = null composes (as on master)
public $p = 1 / private $p = 2 the visibility differs (the first check that fails)
two traits, readonly and not only one of them is readonly

All right. Zend/tests passes with the PR (5,676, its eleven updated expectations included), and the new test fails on master without it.

One adjacent case the reasons don't reach, unchanged by this PR and the same on PHP-8.4: properties that differ only in set visibility or final compose without an error, and which definition wins depends on order:

Trait / class (or second trait) resulting $p
public private(set) int $p = 0 / class public int $p = 0 public set: code outside the class can write it
public int $p = 0 / class public private(set) int $p = 0 private(set)
traits private(set), then public private(set)
traits public, then private(set) public set
final public int $p = 0 / class public int $p = 0 not final

The old mask (ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC | ZEND_ACC_READONLY), which this PR splits into the separate checks, never included the set-visibility or final flags. That may want its own issue rather than this PR.

How this was tested

Linux x86_64, gcc 13.3, debug builds of origin/master (and origin/PHP-8.4 for the last table) and of master with this PR's commit cherry-picked:

./buildconf --force
./configure --enable-debug --disable-cgi --disable-phpdbg --disable-fileinfo --enable-pcntl --enable-zend-test --enable-fpm --with-curl --enable-mbstring --disable-mbregex --with-mysqli --with-pdo-mysql --with-openssl --with-bz2
make -j4

Each row is one sapi/cli/php -n -r '<trait and class>; echo "composed\n";'. The resulting property is read with ReflectionProperty::isPrivateSet(), isProtectedSet() and isFinal(), and the write with $a = new A; $a->p = 5; from outside the class. Suites: TEST_PHP_EXECUTABLE=$PWD/sapi/cli/php sapi/cli/php run-tests.php -n -q -j4 Zend/tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve incompatible trait property error message

2 participants