Skip to content

Commit dc78a9d

Browse files
ondrejmirtesclaude
andcommitted
Skip trait methods the using class overrides by the native method's declaring class
Whether a trait method is analysed in the using class depended on the kind of the class's method reflection: NativeMethodReflection was skipped, PhpMethodReflection was skipped unless it came from this trait, and anything else was analysed. The engine-provided enum methods wrapped in EnumCasesMethodReflection and BackedEnumFromMethodReflection fell through, so a trait's cases(), from() and tryFrom() - which PHP ignores in an enum - were analysed and OverridingMethodRule compared them with UnitEnum::cases() and BackedEnum::from()/tryFrom(). The trait method is now skipped when the BetterReflection method of the using class is declared by anything else than the trait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ED2DmxgSuXYuvCKNuLKDJ
1 parent a0d8033 commit dc78a9d

6 files changed

Lines changed: 179 additions & 38 deletions

File tree

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
use PHPStan\Node\MethodCallExpressionNode;
4242
use PHPStan\Node\StaticMethodCallableNode;
4343
use PHPStan\Node\StaticMethodCallExpressionNode;
44-
use PHPStan\Reflection\Native\NativeMethodReflection;
4544
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
46-
use PHPStan\Reflection\Php\PhpMethodReflection;
4745
use PHPStan\Reflection\Php\PhpPropertyReflection;
4846
use PHPStan\ShouldNotHappenException;
4947
use PHPStan\Turbo\ShadowedByTurboExtension;
@@ -416,24 +414,22 @@ public function processStmtNode(
416414

417415
if ($stmt instanceof Node\Stmt\ClassMethod) {
418416
// a trait method the using class overrides is not analysed here at all -
419-
// decided before the node callback is emitted
417+
// decided before the node callback is emitted; the override can be the
418+
// class's own method, another trait's, or an engine-provided one like
419+
// enum cases(), from() and tryFrom(), so it is told by the declaring
420+
// class of the native method, not by the kind of its method reflection
420421
if (!$scope->isInClass()) {
421422
throw new ShouldNotHappenException();
422423
}
423-
if (
424-
$scope->isInTrait()
425-
&& $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
426-
) {
427-
$methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
428-
if ($methodReflection instanceof NativeMethodReflection) {
424+
if ($scope->isInTrait()) {
425+
$nativeClassReflection = $scope->getClassReflection()->getNativeReflection();
426+
$methodName = $stmt->name->toString();
427+
if (
428+
$nativeClassReflection->hasMethod($methodName)
429+
&& $nativeClassReflection->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() !== $scope->getTraitReflection()->getName()
430+
) {
429431
return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
430432
}
431-
if ($methodReflection instanceof PhpMethodReflection) {
432-
$declaringTrait = $methodReflection->getDeclaringTrait();
433-
if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
434-
return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
435-
}
436-
}
437433
}
438434
}
439435

‎tests/PHPStan/Reflection/ClassReflectionTest.php‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,32 @@ public function testBackedEnumType(): void
355355
$this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType());
356356
}
357357

358+
public static function dataBackedEnumFromMethodFromTrait(): iterable
359+
{
360+
yield ['from'];
361+
yield ['tryFrom'];
362+
}
363+
364+
#[RequiresPhp('>= 8.1.0')]
365+
#[DataProvider('dataBackedEnumFromMethodFromTrait')]
366+
public function testBackedEnumFromMethodFromTrait(string $methodName): void
367+
{
368+
$reflectionProvider = self::createReflectionProvider();
369+
$enum = $reflectionProvider->getClass('BackedEnumFromTrait\Foo');
370+
371+
// the engine-provided method wins over the one from the used trait
372+
$nativeMethod = $enum->getNativeReflection()->getMethod($methodName);
373+
$this->assertSame('BackedEnumFromTrait\Foo', $nativeMethod->getDeclaringClass()->getName());
374+
$nativeParameter = $nativeMethod->getParameters()[0];
375+
$this->assertSame('value', $nativeParameter->getName());
376+
$this->assertSame('string|int', (string) $nativeParameter->getType());
377+
378+
$method = $enum->getNativeMethod($methodName);
379+
$this->assertSame('BackedEnumFromTrait\Foo', $method->getDeclaringClass()->getName());
380+
$this->assertSame('BackedEnum', $method->getPrototype()->getDeclaringClass()->getName());
381+
$this->assertSame('value', $method->getOnlyVariant()->getParameters()[0]->getName());
382+
}
383+
358384
public function testIs(): void
359385
{
360386
$className = static::class;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace BackedEnumFromTrait;
4+
5+
trait FooTrait
6+
{
7+
8+
public static function from(string $case): self
9+
{
10+
return self::ONE;
11+
}
12+
13+
public static function tryFrom(string $case): ?static
14+
{
15+
return null;
16+
}
17+
18+
}
19+
20+
enum Foo: int
21+
{
22+
23+
use FooTrait;
24+
25+
case ONE = 1;
26+
27+
}

‎tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,4 +905,11 @@ public function testBug14398(): void
905905
]);
906906
}
907907

908+
#[RequiresPhp('>= 8.1.0')]
909+
public function testEngineProvidedEnumMethodsWinOverTraitMethods(): void
910+
{
911+
$this->phpVersionId = PHP_VERSION_ID;
912+
$this->analyse([__DIR__ . '/data/backed-enum-from-trait.php'], []);
913+
}
914+
908915
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace BackedEnumFromTraitOverride;
6+
7+
trait IsEnumHelper
8+
{
9+
10+
public static function fromName(string $name): self
11+
{
12+
return static::tryFromName($name) ?? throw new \ValueError(sprintf('"%s" is not a valid case name for enum "%s"', $name, static::class));
13+
}
14+
15+
public static function tryFromName(string $name): ?static
16+
{
17+
$cases = array_filter(
18+
static::cases(),
19+
fn (\UnitEnum $case) => $case->name === $name,
20+
);
21+
22+
return array_shift($cases);
23+
}
24+
25+
/**
26+
* This will not override the `from()` method for Backed enums
27+
*/
28+
public static function from(string $case): self
29+
{
30+
return static::fromName($case);
31+
}
32+
33+
/**
34+
* This will not override the `tryFrom()` method for Backed enums
35+
*/
36+
public static function tryFrom(string $case): ?static
37+
{
38+
return static::tryFromName($case);
39+
}
40+
41+
}
42+
43+
enum SampleIntegerBackedEnum: int
44+
{
45+
46+
use IsEnumHelper;
47+
48+
case SUCCESS = 200;
49+
case CREATED = 201;
50+
51+
}
52+
53+
enum SampleStringBackedEnum: string
54+
{
55+
56+
use IsEnumHelper;
57+
58+
case SUCCESS = 'success';
59+
60+
}
61+
62+
enum SamplePureEnum
63+
{
64+
65+
use IsEnumHelper;
66+
67+
case SUCCESS;
68+
69+
}
70+
71+
trait CasesTrait
72+
{
73+
74+
public static function cases(): string
75+
{
76+
return '';
77+
}
78+
79+
}
80+
81+
enum CasesFromTrait
82+
{
83+
84+
use CasesTrait;
85+
86+
case ONE;
87+
88+
}

‎turbo-ext/src/NodeScopeResolver.cpp‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,36 +1838,33 @@ class NodeScopeResolver
18381838
if (UNEXPECTED(!pt_mutating_scope_is_in_trait(Z_OBJ_P(scope), inTrait))) return zv::Val();
18391839
if (!inTrait) return zv::Val::null();
18401840

1841+
/* $nativeClassReflection = $scope->getClassReflection()->getNativeReflection() */
18411842
zv::Val classReflection = pt_mutating_scope_get_class_reflection(Z_OBJ_P(scope));
1842-
if (UNEXPECTED(classReflection.isUndef() || !requireObject(classReflection, "hasNativeMethod"))) return zv::Val();
1843+
if (UNEXPECTED(classReflection.isUndef() || !requireObject(classReflection, "getNativeReflection"))) return zv::Val();
1844+
zv::Val nativeClassReflection = pt_class_reflection_get_native_reflection(Z_OBJ_P(classReflection.raw()));
1845+
if (UNEXPECTED(nativeClassReflection.isUndef())) return zv::Val();
18431846
zv::Val methodName = identifierToString(stmt);
18441847
if (UNEXPECTED(methodName.isUndef())) return zv::Val();
1845-
zv::Val hasNativeMethod = pt_type_call(Z_OBJ_P(classReflection.raw()), PT_LC("hasnativemethod"), 1, methodName.raw());
1846-
if (UNEXPECTED(hasNativeMethod.isUndef())) return zv::Val();
1847-
if (Z_TYPE_P(hasNativeMethod.raw()) != IS_TRUE) return zv::Val::null();
1848-
1849-
zv::Val classReflectionAgain = pt_mutating_scope_get_class_reflection(Z_OBJ_P(scope));
1850-
if (UNEXPECTED(classReflectionAgain.isUndef() || !requireObject(classReflectionAgain, "getNativeMethod"))) return zv::Val();
1851-
zv::Val methodNameAgain = identifierToString(stmt);
1852-
if (UNEXPECTED(methodNameAgain.isUndef())) return zv::Val();
1853-
zv::Val methodReflection = pt_type_call(Z_OBJ_P(classReflectionAgain.raw()), PT_LC("getnativemethod"), 1, methodNameAgain.raw());
1854-
if (UNEXPECTED(methodReflection.isUndef())) return zv::Val();
1855-
bool isNative;
1856-
if (UNEXPECTED(!isInstanceValue(methodReflection.raw(), PT_CLASS_NATIVE_METHOD_REFLECTION, isNative))) return zv::Val();
1857-
if (isNative) return emptyInternalStatementResult(scope, NULL);
1858-
bool isPhp;
1859-
if (UNEXPECTED(!isInstanceValue(methodReflection.raw(), PT_CLASS_PHP_METHOD_REFLECTION, isPhp))) return zv::Val();
1860-
if (!isPhp) return zv::Val::null();
1861-
zv::Val declaringTrait = pt_type_call(Z_OBJ_P(methodReflection.raw()), PT_LC("getdeclaringtrait"), 0, NULL);
1862-
if (UNEXPECTED(declaringTrait.isUndef())) return zv::Val();
1863-
if (declaringTrait.isNull()) return emptyInternalStatementResult(scope, NULL);
1864-
zv::Val declaringTraitName = requireObject(declaringTrait, "getName") ? pt_class_reflection_get_name(Z_OBJ_P(declaringTrait.raw())) : zv::Val();
1865-
if (UNEXPECTED(declaringTraitName.isUndef())) return zv::Val();
1848+
if (UNEXPECTED(!requireObject(nativeClassReflection, "hasMethod"))) return zv::Val();
1849+
zv::Val hasMethod = pt_type_call(Z_OBJ_P(nativeClassReflection.raw()), PT_LC("hasmethod"), 1, methodName.raw());
1850+
if (UNEXPECTED(hasMethod.isUndef())) return zv::Val();
1851+
if (!zend_is_true(hasMethod.raw())) return zv::Val::null();
1852+
1853+
/* $nativeClassReflection->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() */
1854+
zv::Val nativeMethod = pt_type_call(Z_OBJ_P(nativeClassReflection.raw()), PT_LC("getmethod"), 1, methodName.raw());
1855+
if (UNEXPECTED(nativeMethod.isUndef() || !requireObject(nativeMethod, "getBetterReflection"))) return zv::Val();
1856+
zv::Val betterReflectionMethod = pt_reflection_adapter_get_better_reflection(nativeMethod.raw());
1857+
if (UNEXPECTED(betterReflectionMethod.isUndef() || !requireObject(betterReflectionMethod, "getDeclaringClass"))) return zv::Val();
1858+
zv::Val declaringClass = pt_better_reflection_member_get_declaring_class(betterReflectionMethod.raw());
1859+
if (UNEXPECTED(declaringClass.isUndef() || !requireObject(declaringClass, "getName"))) return zv::Val();
1860+
zv::Val declaringClassName = pt_better_reflection_class_get_name(declaringClass.raw());
1861+
if (UNEXPECTED(declaringClassName.isUndef())) return zv::Val();
1862+
18661863
zv::Val traitReflection = pt_mutating_scope_get_trait_reflection(Z_OBJ_P(scope));
18671864
if (UNEXPECTED(traitReflection.isUndef() || !requireObject(traitReflection, "getName"))) return zv::Val();
18681865
zv::Val traitName = pt_class_reflection_get_name(Z_OBJ_P(traitReflection.raw()));
18691866
if (UNEXPECTED(traitName.isUndef())) return zv::Val();
1870-
if (!zend_is_identical(declaringTraitName.raw(), traitName.raw())) return emptyInternalStatementResult(scope, NULL);
1867+
if (!zend_is_identical(declaringClassName.raw(), traitName.raw())) return emptyInternalStatementResult(scope, NULL);
18711868
return zv::Val::null();
18721869
}
18731870

0 commit comments

Comments
 (0)