Skip to content

Fixes issue where aliases in traits aren't detected #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 29, 2015
Merged
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
154 changes: 97 additions & 57 deletions src/PhpDocReader/PhpDocReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace PhpDocReader;

use PhpDocReader\PhpParser\UseStatementParser;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use Reflector;

/**
* PhpDoc reader
Expand Down Expand Up @@ -95,35 +98,10 @@ public function getPropertyClass(ReflectionProperty $property)

// If the class name is not fully qualified (i.e. doesn't start with a \)
if ($type[0] !== '\\') {
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
$loweredAlias = strtolower($alias);

// Retrieve "use" statements
$uses = $this->parser->parseUseStatements($property->getDeclaringClass());

$found = false;

if (isset($uses[$loweredAlias])) {
// Imported classes
if (false !== $pos) {
$type = $uses[$loweredAlias] . substr($type, $pos);
} else {
$type = $uses[$loweredAlias];
}
$found = true;
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
$type = $class->getNamespaceName() . '\\' . $type;
$found = true;
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
// Class namespace
$type = $uses['__NAMESPACE__'] . '\\' . $type;
$found = true;
} elseif ($this->classExists($type)) {
// No namespace
$found = true;
}
// Try to resolve the FQN using the class context
$resolvedType = $this->tryResolveFqn($type, $class, $property);

if (!$found && !$this->ignorePhpDocErrors) {
if (!$resolvedType && !$this->ignorePhpDocErrors) {
throw new AnnotationException(sprintf(
'The @var annotation on %s::%s contains a non existent class "%s". '
. 'Did you maybe forget to add a "use" statement for this annotation?',
Expand All @@ -132,6 +110,8 @@ public function getPropertyClass(ReflectionProperty $property)
$type
));
}

$type = $resolvedType;
}

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

// If the class name is not fully qualified (i.e. doesn't start with a \)
if ($type[0] !== '\\') {
$alias = (false === $pos = strpos($type, '\\')) ? $type : substr($type, 0, $pos);
$loweredAlias = strtolower($alias);

// Retrieve "use" statements
$uses = $this->parser->parseUseStatements($class);

$found = false;

if (isset($uses[$loweredAlias])) {
// Imported classes
if (false !== $pos) {
$type = $uses[$loweredAlias] . substr($type, $pos);
} else {
$type = $uses[$loweredAlias];
}
$found = true;
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
$type = $class->getNamespaceName() . '\\' . $type;
$found = true;
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
// Class namespace
$type = $uses['__NAMESPACE__'] . '\\' . $type;
$found = true;
} elseif ($this->classExists($type)) {
// No namespace
$found = true;
}

if (!$found && !$this->ignorePhpDocErrors) {
// Try to resolve the FQN using the class context
$resolvedType = $this->tryResolveFqn($type, $class, $parameter);

if (!$resolvedType && !$this->ignorePhpDocErrors) {
throw new AnnotationException(sprintf(
'The @param annotation for parameter "%s" of %s::%s contains a non existent class "%s". '
. 'Did you maybe forget to add a "use" statement for this annotation?',
Expand All @@ -241,6 +196,8 @@ public function getParameterClass(ReflectionParameter $parameter)
$type
));
}

$type = $resolvedType;
}

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

/**
* Attempts to resolve the FQN of the provided $type based on the $class and $member context.
*
* @param string $type
* @param ReflectionClass $class
* @param Reflector $member
*
* @return string|null Fully qualified name of the type, or null if it could not be resolved
*/
private function tryResolveFqn($type, ReflectionClass $class, Reflector $member)
{
$alias = ($pos = strpos($type, '\\')) === false ? $type : substr($type, 0, $pos);
$loweredAlias = strtolower($alias);

// Retrieve "use" statements
$uses = $this->parser->parseUseStatements($class);

if (isset($uses[$loweredAlias])) {
// Imported classes
if ($pos !== false) {
return $uses[$loweredAlias] . substr($type, $pos);
} else {
return $uses[$loweredAlias];
}
} elseif ($this->classExists($class->getNamespaceName() . '\\' . $type)) {
return $class->getNamespaceName() . '\\' . $type;
} elseif (isset($uses['__NAMESPACE__']) && $this->classExists($uses['__NAMESPACE__'] . '\\' . $type)) {
// Class namespace
return $uses['__NAMESPACE__'] . '\\' . $type;
} elseif ($this->classExists($type)) {
// No namespace
return $type;
}

if (version_compare(phpversion(), '5.4.0', '<')) {
return null;
} else {
// If all fail, try resolving through related traits
return $this->tryResolveFqnInTraits($type, $class, $member);
}
}

/**
* Attempts to resolve the FQN of the provided $type based on the $class and $member context, specifically searching
* through the traits that are used by the provided $class.
*
* @param string $type
* @param ReflectionClass $class
* @param Reflector $member
*
* @return string|null Fully qualified name of the type, or null if it could not be resolved
*/
private function tryResolveFqnInTraits($type, ReflectionClass $class, Reflector $member)
{
/** @var ReflectionClass[] $traits */
$traits = array();

// Get traits for the class and its parents
while ($class) {
$traits = array_merge($traits, $class->getTraits());
$class = $class->getParentClass();
}

foreach ($traits as $trait) {
// Eliminate traits that don't have the property/method/parameter
if ($member instanceof ReflectionProperty && !$trait->hasProperty($member->name)) {
continue;
} elseif ($member instanceof ReflectionMethod && !$trait->hasMethod($member->name)) {
continue;
} elseif ($member instanceof ReflectionParameter && !$trait->hasMethod($member->getDeclaringFunction()->name)) {
continue;
}

// Run the resolver again with the ReflectionClass instance for the trait
$resolvedType = $this->tryResolveFqn($type, $trait, $member);

if ($resolvedType) {
return $resolvedType;
}
}
return null;
}

/**
* @param string $class
* @return bool
Expand Down
8 changes: 8 additions & 0 deletions tests/FixturesIssue335/Class1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

class Class1
{
use Trait1;
}
8 changes: 8 additions & 0 deletions tests/FixturesIssue335/Class2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

class Class2 extends Class1
{

}
8 changes: 8 additions & 0 deletions tests/FixturesIssue335/Class3.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

class Class3 extends Class2
{
use Trait2;
}
8 changes: 8 additions & 0 deletions tests/FixturesIssue335/ClassX.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

class ClassX
{

}
22 changes: 22 additions & 0 deletions tests/FixturesIssue335/Trait1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Foo;
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodFoo;

trait Trait1
{
/**
* @var Foo $propTrait1
*/
protected $propTrait1;

/**
* @param MethodFoo $parameter
*/
public function methodTrait1($parameter)
{

}
}
22 changes: 22 additions & 0 deletions tests/FixturesIssue335/Trait2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace UnitTest\PhpDocReader\FixturesIssue335;

use UnitTest\PhpDocReader\FixturesIssue335\ClassX as Bar;
use UnitTest\PhpDocReader\FixturesIssue335\ClassX as MethodBar;

trait Trait2
{
/**
* @var Bar $propTrait2
*/
protected $propTrait2;

/**
* @param MethodBar $parameter
*/
public function methodTrait2($parameter)
{

}
}
43 changes: 43 additions & 0 deletions tests/Issue335Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace UnitTest\PhpDocReader;

use PhpDocReader\PhpDocReader;
use PHPUnit_Framework_TestCase;
use ReflectionClass;
use UnitTest\PhpDocReader\FixturesIssue335\Class3;

/**
* @see https://github.com/PHP-DI/PHP-DI/issues/335
*/
class Issue335Test extends PHPUnit_Framework_TestCase
{
const CLASS_X = 'UnitTest\PhpDocReader\FixturesIssue335\ClassX';

/**
* This test ensures that namespaces are properly resolved for aliases that are defined in traits.
* @see https://github.com/PHP-DI/PHP-DI/issues/335
*/
public function testNamespaceResolutionForTraits()
{
if (version_compare(phpversion(), '5.4.0', '<')) {
$this->markTestSkipped('Traits were introduced in PHP 5.4');
return;
}

$parser = new PhpDocReader();

$target = new Class3();

$class = new ReflectionClass($target);

$this->assertEquals(self::CLASS_X, $parser->getPropertyClass($class->getProperty("propTrait1")));
$this->assertEquals(self::CLASS_X, $parser->getPropertyClass($class->getProperty("propTrait2")));

$params = $class->getMethod("methodTrait1")->getParameters();
$this->assertEquals(self::CLASS_X, $parser->getParameterClass($params[0]));

$params = $class->getMethod("methodTrait2")->getParameters();
$this->assertEquals(self::CLASS_X, $parser->getParameterClass($params[0]));
}
}