Skip to content

Commit 7ecfc04

Browse files
phpstan-botclaude
authored andcommitted
Use native reflection for constructor parameter names to avoid infinite recursion
Instead of calling getOnlyVariant()->getParameters() which creates PhpParameterReflection objects (eagerly resolving attributes and causing infinite recursion for self-referencing attributes), use native reflection to get just the parameter names needed for argument mapping. This preserves attribute argument types — the previous approach returned empty arguments when a cycle was detected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64ec72e commit 7ecfc04

4 files changed

Lines changed: 133 additions & 38 deletions

File tree

src/Reflection/AttributeReflectionFactory.php

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
final class AttributeReflectionFactory
1919
{
2020

21-
/** @var array<string, true> */
22-
private array $currentlyResolving = [];
23-
2421
public function __construct(
2522
private InitializerExprTypeResolver $initializerExprTypeResolver,
2623
private ReflectionProviderProvider $reflectionProviderProvider,
@@ -102,50 +99,45 @@ private function fromNameAndArgumentExpressions(string $name, array $arguments,
10299
}
103100

104101
$className = $classReflection->getName();
105-
if (isset($this->currentlyResolving[$className])) {
106-
return new AttributeReflection($className, []);
102+
103+
$nativeConstructor = $classReflection->getNativeReflection()->getConstructor();
104+
if ($nativeConstructor === null) {
105+
return null;
107106
}
107+
$nativeParameters = $nativeConstructor->getParameters();
108108

109-
$this->currentlyResolving[$className] = true;
110-
111-
try {
112-
$constructor = $classReflection->getConstructor();
113-
$parameters = $constructor->getOnlyVariant()->getParameters();
114-
$namedArgTypes = [];
115-
foreach ($arguments as $i => $argExpr) {
116-
if (is_int($i)) {
117-
if (isset($parameters[$i])) {
118-
$namedArgTypes[$parameters[$i]->getName()] = $this->initializerExprTypeResolver->getType($argExpr, $context);
119-
continue;
120-
}
121-
if (count($parameters) > 0) {
122-
$lastParameter = array_last($parameters);
123-
if ($lastParameter->isVariadic()) {
124-
$parameterName = $lastParameter->getName();
125-
if (array_key_exists($parameterName, $namedArgTypes)) {
126-
$namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $this->initializerExprTypeResolver->getType($argExpr, $context));
127-
continue;
128-
}
129-
$namedArgTypes[$parameterName] = $this->initializerExprTypeResolver->getType($argExpr, $context);
130-
}
131-
}
109+
$namedArgTypes = [];
110+
foreach ($arguments as $i => $argExpr) {
111+
if (is_int($i)) {
112+
if (isset($nativeParameters[$i])) {
113+
$namedArgTypes[$nativeParameters[$i]->getName()] = $this->initializerExprTypeResolver->getType($argExpr, $context);
132114
continue;
133115
}
134-
135-
foreach ($parameters as $parameter) {
136-
if ($parameter->getName() !== $i) {
137-
continue;
116+
if (count($nativeParameters) > 0) {
117+
$lastParameter = array_last($nativeParameters);
118+
if ($lastParameter->isVariadic()) {
119+
$parameterName = $lastParameter->getName();
120+
if (array_key_exists($parameterName, $namedArgTypes)) {
121+
$namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $this->initializerExprTypeResolver->getType($argExpr, $context));
122+
continue;
123+
}
124+
$namedArgTypes[$parameterName] = $this->initializerExprTypeResolver->getType($argExpr, $context);
138125
}
139-
140-
$namedArgTypes[$i] = $this->initializerExprTypeResolver->getType($argExpr, $context);
141-
break;
142126
}
127+
continue;
143128
}
144129

145-
return new AttributeReflection($className, $namedArgTypes);
146-
} finally {
147-
unset($this->currentlyResolving[$className]);
130+
foreach ($nativeParameters as $parameter) {
131+
if ($parameter->getName() !== $i) {
132+
continue;
133+
}
134+
135+
$namedArgTypes[$i] = $this->initializerExprTypeResolver->getType($argExpr, $context);
136+
break;
137+
}
148138
}
139+
140+
return new AttributeReflection($className, $namedArgTypes);
149141
}
150142

151143
}

tests/PHPStan/Reflection/AttributeReflectionFromNodeRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ public function testRule(): void
104104
'#[AttributeReflectionTest\MyAttr(one: 28, two: 29)]',
105105
59,
106106
],
107+
[
108+
"#[AttributeReflectionTest\SelfReferencingAttr(param: 'hi')]",
109+
67,
110+
],
111+
[
112+
"#[AttributeReflectionTest\MutualAttrB(param: 'world')]",
113+
74,
114+
],
115+
[
116+
"#[AttributeReflectionTest\MutualAttrA(param: 'hello')]",
117+
81,
118+
],
119+
[
120+
"\$param: #[AttributeReflectionTest\SelfRefOnParam(param: 'hello')]",
121+
88,
122+
],
107123
]);
108124
}
109125

tests/PHPStan/Reflection/AttributeReflectionTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
namespace PHPStan\Reflection;
44

55
use AttributeReflectionTest\Foo;
6+
use AttributeReflectionTest\MutualAttrA;
7+
use AttributeReflectionTest\MutualAttrB;
68
use AttributeReflectionTest\MyAttr;
9+
use AttributeReflectionTest\SelfReferencingAttr;
10+
use AttributeReflectionTest\SelfRefOnParam;
711
use PhpParser\Node\Name;
812
use PHPStan\Testing\PHPStanTestCase;
913
use PHPStan\Type\VerbosityLevel;
@@ -256,6 +260,62 @@ public static function dataAttributeReflections(): iterable
256260
],
257261
],
258262
];
263+
264+
$selfRef = $reflectionProvider->getClass(SelfReferencingAttr::class);
265+
266+
yield [
267+
$selfRef->getConstructor()->getAttributes(),
268+
[
269+
[
270+
SelfReferencingAttr::class,
271+
[
272+
'param' => "'hi'",
273+
],
274+
],
275+
],
276+
];
277+
278+
$mutualA = $reflectionProvider->getClass(MutualAttrA::class);
279+
280+
yield [
281+
$mutualA->getConstructor()->getAttributes(),
282+
[
283+
[
284+
MutualAttrB::class,
285+
[
286+
'param' => "'world'",
287+
],
288+
],
289+
],
290+
];
291+
292+
$mutualB = $reflectionProvider->getClass(MutualAttrB::class);
293+
294+
yield [
295+
$mutualB->getConstructor()->getAttributes(),
296+
[
297+
[
298+
MutualAttrA::class,
299+
[
300+
'param' => "'hello'",
301+
],
302+
],
303+
],
304+
];
305+
306+
$selfRefOnParam = $reflectionProvider->getClass(SelfRefOnParam::class);
307+
308+
yield [
309+
$selfRefOnParam->getConstructor()->getOnlyVariant()->getParameters()[0]->getAttributes(),
310+
[
311+
[
312+
SelfRefOnParam::class,
313+
[
314+
'param' => "'hello'",
315+
],
316+
],
317+
],
318+
];
259319
}
260320

261321
/**

tests/PHPStan/Reflection/data/attribute-reflection.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,30 @@ function myFunction4() {
6060
function myFunction5() {
6161

6262
}
63+
64+
#[Attribute(Attribute::TARGET_METHOD)]
65+
class SelfReferencingAttr
66+
{
67+
#[SelfReferencingAttr('hi')]
68+
public function __construct(string $param) {}
69+
}
70+
71+
#[Attribute(Attribute::TARGET_ALL)]
72+
class MutualAttrA
73+
{
74+
#[MutualAttrB('world')]
75+
public function __construct(string $param) {}
76+
}
77+
78+
#[Attribute(Attribute::TARGET_ALL)]
79+
class MutualAttrB
80+
{
81+
#[MutualAttrA('hello')]
82+
public function __construct(string $param) {}
83+
}
84+
85+
#[Attribute(Attribute::TARGET_ALL)]
86+
class SelfRefOnParam
87+
{
88+
public function __construct(#[SelfRefOnParam('hello')] string $param) {}
89+
}

0 commit comments

Comments
 (0)