Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions phpunit/code/trait_adaptations_valid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
trait Inner
{
public function f(): void {}
}

trait Outer
{
use Inner;
}

// Aliases resolve methods arriving from NESTED traits, both unqualified and
// qualified with the directly-used trait.
class UsesNested
{
use Outer {
f as g;
}
}

class UsesNestedQualified
{
use Outer {
Outer::f as h;
}
}

trait Winner
{
public function m(): void {}
}

trait Loser
{
public function other(): void {}
}

// The overridden trait need not declare the method a precedence rule names.
class PrecedenceLoserMissing
{
use Winner, Loser {
Winner::m insteadof Loser;
}
}

function main() {}
14 changes: 14 additions & 0 deletions phpunit/code/trait_alias_missing_method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
trait A
{
public function f(): void {}
}

class C
{
use A {
missing as g;
}
}

function main() {}
14 changes: 14 additions & 0 deletions phpunit/code/trait_alias_missing_qualified.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
trait A
{
public function f(): void {}
}

class C
{
use A {
A::missing as g;
}
}

function main() {}
19 changes: 19 additions & 0 deletions phpunit/code/trait_alias_trait_not_used.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
trait A
{
public function f(): void {}
}

trait B
{
public function h(): void {}
}

class C
{
use A {
B::h as g;
}
}

function main() {}
18 changes: 18 additions & 0 deletions phpunit/code/trait_const_identity_conflict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
trait A
{
const X = 1;
}

trait B
{
const X = 1.0;
}

class C
{
use A;
use B;
}

function main() {}
18 changes: 18 additions & 0 deletions phpunit/code/trait_const_value_conflict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
trait A
{
const int X = 1;
}

trait B
{
const int X = 3;
}

class C
{
use A;
use B;
}

function main() {}
19 changes: 19 additions & 0 deletions phpunit/code/trait_insteadof_missing_method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
trait A
{
public function f(): void {}
}

trait B
{
public function g(): void {}
}

class C
{
use A, B {
B::f insteadof A;
}
}

function main() {}
24 changes: 24 additions & 0 deletions phpunit/code/trait_insteadof_trait_not_used.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
trait A
{
public function f(): void {}
}

trait B
{
public function f(): void {}
}

trait D
{
public function f(): void {}
}

class C
{
use A, B {
A::f insteadof B, D;
}
}

function main() {}
35 changes: 35 additions & 0 deletions phpunit/code/trait_member_same_value_spelling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
trait A
{
const int X = 1 + 1;
public array $p = [1, 2];
public float $f = 1;
}

trait B
{
const int X = 2;
public array $p = array(1, 2);
public float $f = 1.0;
}

class C
{
use A;
use B;
}

// The class's own members are also compared by value with trait members.
trait T
{
const int Y = 2 + 3;
}

class D
{
use T;

const int Y = 5;
}

function main() {}
18 changes: 18 additions & 0 deletions phpunit/code/trait_prop_value_conflict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
trait A
{
public array $p = [1, 2];
}

trait B
{
public array $p = [2, 1];
}

class C
{
use A;
use B;
}

function main() {}
58 changes: 58 additions & 0 deletions phpunit/src/TraitAdaptationValidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

use TypePhp\Exception\TestError;

/**
* Zend validates trait adaptations while binding traits: an alias must name a
* method that exists in a used trait, and a precedence rule must name used
* traits and an existing preferred method (the overridden trait need not
* declare it). Methods arriving from nested traits satisfy adaptations on the
* directly-used trait.
*/
class TraitAdaptationValidationTest extends BaseTest
{
public function testValidAdaptationsCompile(): void
{
$this->compile('trait_adaptations_valid.php');
}

public function testUnqualifiedAliasForMissingMethod(): void
{
$this->exec(
'An alias (`g`) was defined for method `missing()`, but this method does not exist',
'trait_alias_missing_method.php',
);
}

public function testQualifiedAliasForMissingMethod(): void
{
$this->exec(
'An alias was defined for `A::missing` but this method does not exist',
'trait_alias_missing_qualified.php',
);
}

public function testAliasReferencingUnusedTrait(): void
{
$this->exec(
"Required Trait `B` wasn't added to `C`",
'trait_alias_trait_not_used.php',
);
}

public function testPrecedenceRuleForMissingMethod(): void
{
$this->exec(
'A precedence rule was defined for `B::f` but this method does not exist',
'trait_insteadof_missing_method.php',
);
}

public function testPrecedenceRuleReferencingUnusedTrait(): void
{
$this->exec(
"Required Trait `D` wasn't added to `C`",
'trait_insteadof_trait_not_used.php',
);
}
}
32 changes: 32 additions & 0 deletions phpunit/src/TraitMemberValueConflictTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use TypePhp\Exception\TestError;

/**
* Zend compares trait data members by VALUE when flattening traits into a
* class: `1 + 1` and `2`, or `[1, 2]` and `array(1, 2)`, are the same
* definition, while genuinely different values (and identical-but-differently
* typed ones, e.g. 1 vs 1.0 on an untyped constant) conflict.
*/
class TraitMemberValueConflictTest extends BaseTest
{
public function testSameValueDifferentSpellingCompiles(): void
{
$this->compile('trait_member_same_value_spelling.php');
}

public function testDifferentConstantValuesConflict(): void
{
$this->exec('constant `x` already exists', 'trait_const_value_conflict.php');
}

public function testDifferentPropertyDefaultsConflict(): void
{
$this->exec('property `p` already exists', 'trait_prop_value_conflict.php');
}

public function testValueComparisonIsIdentityNotEquality(): void
{
$this->exec('constant `x` already exists', 'trait_const_identity_conflict.php');
}
}
12 changes: 8 additions & 4 deletions src/Entity/ClassDef.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,18 @@ class ClassDef extends ClassLikeDef
public array $traitUseConstants = [];

/**
* FullMethodName -> alias list
* @var array<string, array<int, array{newName: string, newModifier: int}>>
* FullMethodName -> alias list. `group` identifies the source adaptation
* (an unqualified alias is registered under every used trait's key),
* `method` is the aliased method as written, and `trait` the explicit
* trait qualifier or null.
* @var array<string, array<int, array{newName: string, newModifier: int, group: string, method: string, trait: ?string}>>
*/
public array $traitAliases = [];

/**
* FullMethodName -> true
* @var array<string, bool>
* FullMethodName of the ignored (overridden) method -> precedence rule
* info for existence validation. Consumers test the key with isset().
* @var array<string, array{method: string, winnerTrait: string, loserTrait: string}>
*/
public array $traitIgnored = [];
public int $flags;
Expand Down
19 changes: 17 additions & 2 deletions src/Preprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2438,7 +2438,12 @@ private function prepareInterfaceProperty(Node\Stmt\Property $property): void

protected function parseTraitUseOptions(Node\Stmt\TraitUse $traitUse, array &$aliases, array &$ignored): void
{
foreach ($traitUse->adaptations as $adaptation) {
// Adaptation identity used to verify during trait composition that
// every alias matched a real trait method (an unqualified alias is
// registered under every used trait's key, so its variants share one
// group and the group is satisfied when ANY variant matches).
$groupBase = $traitUse->getAttribute('startFilePos', $traitUse->getStartLine()) . '@';
foreach ($traitUse->adaptations as $adaptationIndex => $adaptation) {
if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
$traits = [];
if (!$adaptation->trait) {
Expand All @@ -2461,6 +2466,9 @@ protected function parseTraitUseOptions(Node\Stmt\TraitUse $traitUse, array &$al
$aliases[$this->getFullMethodName($traitName, $methodName)][] = [
'newName' => $adaptation->newName ? $adaptation->newName->toString() : $methodName,
'newModifier' => $adaptation->newModifier ?: 0,
'group' => $groupBase . $adaptationIndex,
'method' => $methodName,
'trait' => $adaptation->trait ? $traitName : null,
];
}
}
Expand All @@ -2469,14 +2477,21 @@ protected function parseTraitUseOptions(Node\Stmt\TraitUse $traitUse, array &$al
$this->fatalError($traitUse, 'Trait precedence cannot be used without a trait');
}
$methodName = $adaptation->method->toString();
$winnerTrait = $this->getNamespacedClassName($this->parseIdentifier($adaptation->trait));
/*
* For example:
* use TraitA { TraitA::method insteadof TraitB}
* This means TraitB::method() is ignored, and TraitA::method() is actually executed
*/
foreach ($adaptation->insteadof as $trait2) {
$traitName = $this->getNamespacedClassName($this->parseIdentifier($trait2));
$ignored[$this->getFullMethodName($traitName, $methodName)] = true;
// The value records the rule for existence validation
// during composition; consumers only use isset() on the key.
$ignored[$this->getFullMethodName($traitName, $methodName)] = [
'method' => $methodName,
'winnerTrait' => $winnerTrait,
'loserTrait' => $traitName,
];
}
}
}
Expand Down
Loading
Loading