diff --git a/src/Method.php b/src/Method.php index 234dd68d1..0ca2fb124 100644 --- a/src/Method.php +++ b/src/Method.php @@ -12,10 +12,10 @@ use Barryvdh\Reflection\DocBlock; use Barryvdh\Reflection\DocBlock\Context; +use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer; use Barryvdh\Reflection\DocBlock\Tag; -use Barryvdh\Reflection\DocBlock\Tag\ReturnTag; use Barryvdh\Reflection\DocBlock\Tag\ParamTag; -use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer; +use Barryvdh\Reflection\DocBlock\Tag\ReturnTag; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; @@ -37,6 +37,7 @@ class Method protected $real_name; protected $return = null; protected $root; + protected $uses = array(); /** * @param \ReflectionMethod|\ReflectionFunctionAbstract $method @@ -53,12 +54,16 @@ public function __construct($method, $alias, $class, $methodName = null, $interf $this->real_name = $method->isClosure() ? $this->name : $method->name; $this->initClassDefinedProperties($method, $class); + // TODO: Runtime-cache this. + $this->uses = $this->getUses($class); + //Reference the 'real' function in the declaring class $this->root = '\\' . ltrim($class->getName(), '\\'); //Create a DocBlock and serializer instance $this->initPhpDoc($method); + //Normalize the description and inherit the docs from parents/interfaces try { $this->normalizeParams($this->phpdoc); @@ -237,7 +242,8 @@ protected function normalizeParams(DocBlock $phpdoc) $tag->setContent($content); // Get the expanded type and re-set the content - $content = $tag->getType() . ' ' . $tag->getVariableName() . ' ' . $tag->getDescription(); + $type = $this->substituteMissingClasses($tag->getType()); + $content = $type . ' ' . $tag->getVariableName() . ' ' . $tag->getDescription(); $tag->setContent(trim($content)); } } @@ -263,6 +269,8 @@ protected function normalizeReturn(DocBlock $phpdoc) $returnValue = str_replace($interface, $real, $returnValue); } + $returnValue = $this->substituteMissingClasses($returnValue); + // Set the changed content $tag->setContent($returnValue . ' ' . $tag->getDescription()); $this->return = $returnValue; @@ -370,4 +378,96 @@ protected function getInheritDoc($reflectionMethod) } } } + + /** + * @param \ReflectionClass $class + * + * @return array + */ + protected function getUses(\ReflectionClass $class) + { + $start = false; + $currentString = ''; + $currentUse = ''; + $uses = []; + foreach (token_get_all(file_get_contents($class->getFileName())) as $token) { + if ($start === false) { + if (is_numeric($token[0])) { + switch (token_name($token[0])) { + case 'T_CLASS': + // Stop on reaching class definition for performance reasons. + // This leads to not catching any use statements behind the class definition. + break 2; + case 'T_USE': + $start = true; + $currentString = ''; + $currentUse = ''; + break; + } + } + } else { + if ($token[0] != ';') { + if (is_numeric($token[0])) { + switch (token_name($token[0])) { + case 'T_WHITESPACE': + // Skip Whitespaces. + break; + case 'T_AS': + $currentUse = $currentString; + $currentString = ''; + break; + default: + $currentString .= $token[1] ?? ''; + } + } else { + $currentString .= $token[1] ?? ''; + } + } else { + if ($currentUse != '') { + $uses[$currentString] = $currentUse; + } else { + $uses[$this->classBasename($currentString)] = $currentString; + } + + $start = false; + } + } + } + return $uses; + } + + /** + * @param string $types + * + * @return string + */ + protected function substituteMissingClasses(string $types) + { + $newTypes = []; + foreach (explode('|', $types) as $type) { + if (!class_exists($type)) { + $baseClassName = $this->classBasename($type); + + if ($this->uses[$baseClassName] ?? false) { + $type = $this->uses[$baseClassName]; + if (substr($type, 0, 1) != '\\') { + $type = '\\' . $type; + } + } + } + $newTypes[] = $type; + } + + return implode('|', $newTypes); + } + + /** + * @param string $className + * + * @return string + */ + protected function classBasename(string $className) + { + return basename(str_replace('\\', '/', $className)); + } } diff --git a/tests/MethodTest.php b/tests/MethodTest.php index 989ba266c..ea3827667 100644 --- a/tests/MethodTest.php +++ b/tests/MethodTest.php @@ -4,9 +4,11 @@ use Barryvdh\LaravelIdeHelper\Method; use Illuminate\Database\Eloquent\Builder; +use \Illuminate\Support\Collection; use PHPUnit\Framework\TestCase; +use Illuminate\Support\Carbon as AliasedCarbon; -class ExampleTest extends TestCase +class MethodTest extends TestCase { /** * Test that we can actually instantiate the class @@ -31,14 +33,15 @@ public function testOutput() $method = new Method($reflectionMethod, 'Example', $reflectionClass); - $output = '/** - * - * - * @param string $last - * @param string $first - * @param string $middle - * @static - */'; + $output = "/**\n"; + $output .= " * \n"; + $output .= " *\n"; + $output .= " * @param string \$last\n"; + $output .= " * @param string \$first\n"; + $output .= " * @param string \$middle\n"; + $output .= " * @static \n"; + $output .= " */"; + $this->assertSame($output, $method->getDocComment('')); $this->assertSame('setName', $method->getName()); $this->assertSame('\\'.ExampleClass::class, $method->getDeclaringClass()); @@ -59,13 +62,14 @@ public function testEloquentBuilderOutput() $method = new Method($reflectionMethod, 'Builder', $reflectionClass); - $output = '/** - * Set the relationships that should be eager loaded. - * - * @param mixed $relations - * @return \Illuminate\Database\Eloquent\Builder|static - * @static - */'; + $output = "/**\n"; + $output .= " * Set the relationships that should be eager loaded.\n"; + $output .= " *\n"; + $output .= " * @param mixed \$relations\n"; + $output .= " * @return \Illuminate\Database\Eloquent\Builder|static \n"; + $output .= " * @static \n"; + $output .= " */"; + $this->assertSame($output, $method->getDocComment('')); $this->assertSame('with', $method->getName()); $this->assertSame('\\'.Builder::class, $method->getDeclaringClass()); @@ -90,6 +94,29 @@ public function testDefaultSpecialChars() $this->assertSame('$chars = \'$\\\'\\\\\'', $method->getParamsWithDefault(true)); $this->assertSame(['$chars = \'$\\\'\\\\\''], $method->getParamsWithDefault(false)); } + + public function testRespectsImportedNamespaces() + { + $reflectionClass = new \ReflectionClass(SampleClass::class); + $reflectionMethod = $reflectionClass->getMethod('someMethod'); + + $method = new Method($reflectionMethod, 'SampleClass', $reflectionClass); + + $output = "/**\n"; + $output .= " * \n"; + $output .= " *\n"; + $output .= " * @param \Illuminate\Support\Collection \$collection\n"; + $output .= " * @param \Illuminate\Database\Eloquent\Builder \$builder\n"; + $output .= " * @param \Illuminate\Support\Carbon \$carbon\n"; + $output .= " * @param \Barryvdh\LaravelIdeHelper\Tests\UnknownClass \$unknownClass\n"; + $output .= " * @return \Illuminate\Support\Collection \n"; + $output .= " * @static \n"; + $output .= " */"; + + $this->assertSame($output, $method->getDocComment('')); + $this->assertSame('someMethod', $method->getName()); + $this->assertSame('\\'. SampleClass::class, $method->getDeclaringClass()); + } } class ExampleClass @@ -109,3 +136,17 @@ public function setSpecialChars($chars = "\$'\\") return; } } + +class SampleClass { + /** + * @param Collection $collection + * @param Builder $builder + * @param AliasedCarbon $carbon + * @param UnknownClass $unknownClass + * + * @return Collection + */ + function someMethod(Collection $collection, Builder $builder, AliasedCarbon $carbon, UnknownClass $unknownClass) { + return collect(); + } +}