Skip to content

Commit 83f5ead

Browse files
committed
Merge pull request #7 from thebigb/master
Fixes issue where aliases in traits aren't detected
2 parents 21dce5e + a44b242 commit 83f5ead

File tree

8 files changed

+216
-57
lines changed

8 files changed

+216
-57
lines changed

src/PhpDocReader/PhpDocReader.php

+97-57
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace PhpDocReader;
44

55
use PhpDocReader\PhpParser\UseStatementParser;
6+
use ReflectionClass;
7+
use ReflectionMethod;
68
use ReflectionParameter;
79
use ReflectionProperty;
10+
use Reflector;
811

912
/**
1013
* PhpDoc reader
@@ -95,35 +98,10 @@ public function getPropertyClass(ReflectionProperty $property)
9598

9699
// If the class name is not fully qualified (i.e. doesn't start with a \)
97100
if ($type[0] !== '\\') {
98-
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
99-
$loweredAlias = strtolower($alias);
100-
101-
// Retrieve "use" statements
102-
$uses = $this->parser->parseUseStatements($property->getDeclaringClass());
103-
104-
$found = false;
105-
106-
if (isset($uses[$loweredAlias])) {
107-
// Imported classes
108-
if (false !== $pos) {
109-
$type = $uses[$loweredAlias] . substr($type, $pos);
110-
} else {
111-
$type = $uses[$loweredAlias];
112-
}
113-
$found = true;
114-
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
115-
$type = $class->getNamespaceName() . '\\' . $type;
116-
$found = true;
117-
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
118-
// Class namespace
119-
$type = $uses['__NAMESPACE__'] . '\\' . $type;
120-
$found = true;
121-
} elseif ($this->classExists($type)) {
122-
// No namespace
123-
$found = true;
124-
}
101+
// Try to resolve the FQN using the class context
102+
$resolvedType = $this->tryResolveFqn($type, $class, $property);
125103

126-
if (!$found && !$this->ignorePhpDocErrors) {
104+
if (!$resolvedType && !$this->ignorePhpDocErrors) {
127105
throw new AnnotationException(sprintf(
128106
'The @var annotation on %s::%s contains a non existent class "%s". '
129107
. 'Did you maybe forget to add a "use" statement for this annotation?',
@@ -132,6 +110,8 @@ public function getPropertyClass(ReflectionProperty $property)
132110
$type
133111
));
134112
}
113+
114+
$type = $resolvedType;
135115
}
136116

137117
if (!$this->classExists($type) && !$this->ignorePhpDocErrors) {
@@ -203,35 +183,10 @@ public function getParameterClass(ReflectionParameter $parameter)
203183

204184
// If the class name is not fully qualified (i.e. doesn't start with a \)
205185
if ($type[0] !== '\\') {
206-
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
207-
$loweredAlias = strtolower($alias);
208-
209-
// Retrieve "use" statements
210-
$uses = $this->parser->parseUseStatements($class);
211-
212-
$found = false;
213-
214-
if (isset($uses[$loweredAlias])) {
215-
// Imported classes
216-
if (false !== $pos) {
217-
$type = $uses[$loweredAlias] . substr($type, $pos);
218-
} else {
219-
$type = $uses[$loweredAlias];
220-
}
221-
$found = true;
222-
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
223-
$type = $class->getNamespaceName() . '\\' . $type;
224-
$found = true;
225-
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
226-
// Class namespace
227-
$type = $uses['__NAMESPACE__'] . '\\' . $type;
228-
$found = true;
229-
} elseif ($this->classExists($type)) {
230-
// No namespace
231-
$found = true;
232-
}
233-
234-
if (!$found && !$this->ignorePhpDocErrors) {
186+
// Try to resolve the FQN using the class context
187+
$resolvedType = $this->tryResolveFqn($type, $class, $parameter);
188+
189+
if (!$resolvedType && !$this->ignorePhpDocErrors) {
235190
throw new AnnotationException(sprintf(
236191
'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
237192
. 'Did you maybe forget to add a "use" statement for this annotation?',
@@ -241,6 +196,8 @@ public function getParameterClass(ReflectionParameter $parameter)
241196
$type
242197
));
243198
}
199+
200+
$type = $resolvedType;
244201
}
245202

246203
if (!$this->classExists($type) && !$this->ignorePhpDocErrors) {
@@ -259,6 +216,89 @@ public function getParameterClass(ReflectionParameter $parameter)
259216
return $type;
260217
}
261218

219+
/**
220+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context.
221+
*
222+
* @param string $type
223+
* @param ReflectionClass $class
224+
* @param Reflector $member
225+
*
226+
* @return string|null Fully qualified name of the type, or null if it could not be resolved
227+
*/
228+
private function tryResolveFqn($type, ReflectionClass $class, Reflector $member)
229+
{
230+
$alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
231+
$loweredAlias = strtolower($alias);
232+
233+
// Retrieve "use" statements
234+
$uses = $this->parser->parseUseStatements($class);
235+
236+
if (isset($uses[$loweredAlias])) {
237+
// Imported classes
238+
if ($pos !== false) {
239+
return $uses[$loweredAlias] . substr($type, $pos);
240+
} else {
241+
return $uses[$loweredAlias];
242+
}
243+
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
244+
return $class->getNamespaceName() . '\\' . $type;
245+
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
246+
// Class namespace
247+
return $uses['__NAMESPACE__'] . '\\' . $type;
248+
} elseif ($this->classExists($type)) {
249+
// No namespace
250+
return $type;
251+
}
252+
253+
if (version_compare(phpversion(), '5.4.0', '<')) {
254+
return null;
255+
} else {
256+
// If all fail, try resolving through related traits
257+
return $this->tryResolveFqnInTraits($type, $class, $member);
258+
}
259+
}
260+
261+
/**
262+
* Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
263+
* through the traits that are used by the provided $class.
264+
*
265+
* @param string $type
266+
* @param ReflectionClass $class
267+
* @param Reflector $member
268+
*
269+
* @return string|null Fully qualified name of the type, or null if it could not be resolved
270+
*/
271+
private function tryResolveFqnInTraits($type, ReflectionClass $class, Reflector $member)
272+
{
273+
/** @var ReflectionClass[] $traits */
274+
$traits = array();
275+
276+
// Get traits for the class and its parents
277+
while ($class) {
278+
$traits = array_merge($traits, $class->getTraits());
279+
$class = $class->getParentClass();
280+
}
281+
282+
foreach ($traits as $trait) {
283+
// Eliminate traits that don't have the property/method/parameter
284+
if ($member instanceof ReflectionProperty && !$trait->hasProperty($member->name)) {
285+
continue;
286+
} elseif ($member instanceof ReflectionMethod && !$trait->hasMethod($member->name)) {
287+
continue;
288+
} elseif ($member instanceof ReflectionParameter && !$trait->hasMethod($member->getDeclaringFunction()->name)) {
289+
continue;
290+
}
291+
292+
// Run the resolver again with the ReflectionClass instance for the trait
293+
$resolvedType = $this->tryResolveFqn($type, $trait, $member);
294+
295+
if ($resolvedType) {
296+
return $resolvedType;
297+
}
298+
}
299+
return null;
300+
}
301+
262302
/**
263303
* @param string $class
264304
* @return bool

tests/FixturesIssue335/Class1.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class1
6+
{
7+
use Trait1;
8+
}

tests/FixturesIssue335/Class2.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class2 extends Class1
6+
{
7+
8+
}

tests/FixturesIssue335/Class3.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class Class3 extends Class2
6+
{
7+
use Trait2;
8+
}

tests/FixturesIssue335/ClassX.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
class ClassX
6+
{
7+
8+
}

tests/FixturesIssue335/Trait1.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Foo;
6+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodFoo;
7+
8+
trait Trait1
9+
{
10+
/**
11+
* @var Foo $propTrait1
12+
*/
13+
protected $propTrait1;
14+
15+
/**
16+
* @param MethodFoo $parameter
17+
*/
18+
public function methodTrait1($parameter)
19+
{
20+
21+
}
22+
}

tests/FixturesIssue335/Trait2.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader\FixturesIssue335;
4+
5+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Bar;
6+
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodBar;
7+
8+
trait Trait2
9+
{
10+
/**
11+
* @var Bar $propTrait2
12+
*/
13+
protected $propTrait2;
14+
15+
/**
16+
* @param MethodBar $parameter
17+
*/
18+
public function methodTrait2($parameter)
19+
{
20+
21+
}
22+
}

tests/Issue335Test.php

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace UnitTest\PhpDocReader;
4+
5+
use PhpDocReader\PhpDocReader;
6+
use PHPUnit_Framework_TestCase;
7+
use ReflectionClass;
8+
use UnitTest\PhpDocReader\FixturesIssue335\Class3;
9+
10+
/**
11+
* @see https://github.com/PHP-DI/PHP-DI/issues/335
12+
*/
13+
class Issue335Test extends PHPUnit_Framework_TestCase
14+
{
15+
const CLASS_X = 'UnitTest\PhpDocReader\FixturesIssue335\ClassX';
16+
17+
/**
18+
* This test ensures that namespaces are properly resolved for aliases that are defined in traits.
19+
* @see https://github.com/PHP-DI/PHP-DI/issues/335
20+
*/
21+
public function testNamespaceResolutionForTraits()
22+
{
23+
if (version_compare(phpversion(), '5.4.0', '<')) {
24+
$this->markTestSkipped('Traits were introduced in PHP 5.4');
25+
return;
26+
}
27+
28+
$parser = new PhpDocReader();
29+
30+
$target = new Class3();
31+
32+
$class = new ReflectionClass($target);
33+
34+
$this->assertEquals(self::CLASS_X, $parser->getPropertyClass($class->getProperty("propTrait1")));
35+
$this->assertEquals(self::CLASS_X, $parser->getPropertyClass($class->getProperty("propTrait2")));
36+
37+
$params = $class->getMethod("methodTrait1")->getParameters();
38+
$this->assertEquals(self::CLASS_X, $parser->getParameterClass($params[0]));
39+
40+
$params = $class->getMethod("methodTrait2")->getParameters();
41+
$this->assertEquals(self::CLASS_X, $parser->getParameterClass($params[0]));
42+
}
43+
}

0 commit comments

Comments
 (0)