Skip to content

Commit 64ec72e

Browse files
phpstan-botondrejmirtes
authored andcommitted
Prevent infinite recursion in AttributeReflectionFactory when an attribute references itself on its constructor
- Add `$currentlyResolving` tracking array to `AttributeReflectionFactory` to detect when an attribute class is already being resolved - When recursion is detected, return the attribute with empty argument types instead of recursing infinitely - Covers self-referencing attribute on constructor method, mutual recursion between two attribute classes, and self-referencing attribute on constructor parameter
1 parent efdf31f commit 64ec72e

3 files changed

Lines changed: 79 additions & 27 deletions

File tree

src/Reflection/AttributeReflectionFactory.php

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

21+
/** @var array<string, true> */
22+
private array $currentlyResolving = [];
23+
2124
public function __construct(
2225
private InitializerExprTypeResolver $initializerExprTypeResolver,
2326
private ReflectionProviderProvider $reflectionProviderProvider,
@@ -98,40 +101,51 @@ private function fromNameAndArgumentExpressions(string $name, array $arguments,
98101
return null;
99102
}
100103

101-
$constructor = $classReflection->getConstructor();
102-
$parameters = $constructor->getOnlyVariant()->getParameters();
103-
$namedArgTypes = [];
104-
foreach ($arguments as $i => $argExpr) {
105-
if (is_int($i)) {
106-
if (isset($parameters[$i])) {
107-
$namedArgTypes[$parameters[$i]->getName()] = $this->initializerExprTypeResolver->getType($argExpr, $context);
108-
continue;
109-
}
110-
if (count($parameters) > 0) {
111-
$lastParameter = array_last($parameters);
112-
if ($lastParameter->isVariadic()) {
113-
$parameterName = $lastParameter->getName();
114-
if (array_key_exists($parameterName, $namedArgTypes)) {
115-
$namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $this->initializerExprTypeResolver->getType($argExpr, $context));
116-
continue;
104+
$className = $classReflection->getName();
105+
if (isset($this->currentlyResolving[$className])) {
106+
return new AttributeReflection($className, []);
107+
}
108+
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);
117130
}
118-
$namedArgTypes[$parameterName] = $this->initializerExprTypeResolver->getType($argExpr, $context);
119131
}
120-
}
121-
continue;
122-
}
123-
124-
foreach ($parameters as $parameter) {
125-
if ($parameter->getName() !== $i) {
126132
continue;
127133
}
128134

129-
$namedArgTypes[$i] = $this->initializerExprTypeResolver->getType($argExpr, $context);
130-
break;
135+
foreach ($parameters as $parameter) {
136+
if ($parameter->getName() !== $i) {
137+
continue;
138+
}
139+
140+
$namedArgTypes[$i] = $this->initializerExprTypeResolver->getType($argExpr, $context);
141+
break;
142+
}
131143
}
132-
}
133144

134-
return new AttributeReflection($classReflection->getName(), $namedArgTypes);
145+
return new AttributeReflection($className, $namedArgTypes);
146+
} finally {
147+
unset($this->currentlyResolving[$className]);
148+
}
135149
}
136150

137151
}

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,12 @@ public function testBug9172(): void
15841584
$this->assertNotEmpty($errors);
15851585
}
15861586

1587+
public function testBug14707(): void
1588+
{
1589+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14707.php');
1590+
$this->assertNoErrors($errors);
1591+
}
1592+
15871593
/**
15881594
* @param string[]|null $allAnalysedFiles
15891595
* @return list<Error>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14707;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_METHOD)]
8+
class AttributeUsingItself
9+
{
10+
#[AttributeUsingItself('hi')]
11+
public function __construct(string $param) {}
12+
}
13+
14+
#[Attribute(Attribute::TARGET_ALL)]
15+
class AttrA
16+
{
17+
#[AttrB('world')]
18+
public function __construct(string $param) {}
19+
}
20+
21+
#[Attribute(Attribute::TARGET_ALL)]
22+
class AttrB
23+
{
24+
#[AttrA('hello')]
25+
public function __construct(string $param) {}
26+
}
27+
28+
#[Attribute(Attribute::TARGET_ALL)]
29+
class SelfRefOnParam
30+
{
31+
public function __construct(#[SelfRefOnParam('hello')] string $param) {}
32+
}

0 commit comments

Comments
 (0)