Repository: barryvdh/laravel-ide-helper (php)

Issue:
Macro docblock regression: empty docblock
### Versions:
- ide-helper Version: 2.8.1
- Laravel Version: 8.15.x <!-- remove if not applicable -->
- PHP Version: 7.4.x

### Description:

The #1006 added support for macros but it contains regression: 

![image](https://user-images.githubusercontent.com/1329824/100515455-cc281880-3195-11eb-9552-5590519e8c27.png)

The problem is `Closure`s also can have docblock and in the previous version it parsed and saved correctly. After  #1006 ide-helper always loses it :(

My suggestion is: if docblock present ide-helper should use it (`$this->phpdoc = new DocBlock($method);`); if not - add it.

@domkrm

Pull request:
`Macro::initPhpDoc()` will save original docblock if present.
## Summary

The #1006 added support for macros but lost the content of the original docblock that was useful to provide an actual description, params, and return types. This PR fixes it:

- If the closure has a docblock it will be saved
- `@param`s will be added only when the original docblock doesn't have them
- `@return` will be added only when the original docblock doesn't have it

Fixes #1099

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

### Checklist
- [x] Existing tests have been adapted and/or new tests have been added



Patch:
diff --git a/src/Macro.php b/src/Macro.php
index 0a4b630..069770d 100644
--- a/src/Macro.php
+++ b/src/Macro.php
@@ -33,23 +33,25 @@ class Macro extends Method
      */
     protected function initPhpDoc($method)
     {
-        $this->phpdoc = new DocBlock('/** */');
+        $this->phpdoc = new DocBlock($method);
 
         $this->addLocationToPhpDoc();
 
-        // Add macro parameters
-        foreach ($method->getParameters() as $parameter) {
-            $type = $parameter->hasType() ? $parameter->getType()->getName() : 'mixed';
-            $type .= $parameter->hasType() && $parameter->getType()->allowsNull() ? '|null' : '';
+        // Add macro parameters if they are missed in original docblock
+        if (!$this->phpdoc->hasTag('param')) {
+            foreach ($method->getParameters() as $parameter) {
+                $type = $parameter->hasType() ? $parameter->getType()->getName() : 'mixed';
+                $type .= $parameter->hasType() && $parameter->getType()->allowsNull() ? '|null' : '';
 
-            $name = $parameter->isVariadic() ? '...' : '';
-            $name .= '$' . $parameter->getName();
+                $name = $parameter->isVariadic() ? '...' : '';
+                $name .= '$' . $parameter->getName();
 
-            $this->phpdoc->appendTag(Tag::createInstance("@param {$type} {$name}"));
+                $this->phpdoc->appendTag(Tag::createInstance("@param {$type} {$name}"));
+            }
         }
 
-        // Add macro return type
-        if ($method->hasReturnType()) {
+        // Add macro return type if it missed in original docblock
+        if ($method->hasReturnType() && !$this->phpdoc->hasTag('return')) {
             $builder = EloquentBuilder::class;
             $return = $method->getReturnType();
 


Test patch:
diff --git a/tests/MacroTest.php b/tests/MacroTest.php
index 6c9a486..ea9dd0b 100644
--- a/tests/MacroTest.php
+++ b/tests/MacroTest.php
@@ -46,6 +46,147 @@ class MacroTest extends TestCase
         );
     }
 
+    /**
+     * @covers ::initPhpDoc
+     * @throws \ReflectionException
+     */
+    public function testInitPhpDocClosureWithoutDocBlock(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                function (int $a = null): int {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertEmpty($phpdoc->getText());
+        $this->assertEquals('@param int|null $a', $this->tagsToString($phpdoc, 'param'));
+        $this->assertEquals('@return int', $this->tagsToString($phpdoc, 'return'));
+        $this->assertTrue($phpdoc->hasTag('see'));
+    }
+
+    /**
+     * @covers ::initPhpDoc
+     * @throws \ReflectionException
+     */
+    public function testInitPhpDocClosureWithArgsAndReturnType(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                /**
+                 * Test docblock.
+                 */
+                function (int $a = null): int {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertStringContainsString('Test docblock', $phpdoc->getText());
+        $this->assertEquals('@param int|null $a', $this->tagsToString($phpdoc, 'param'));
+        $this->assertEquals('@return int', $this->tagsToString($phpdoc, 'return'));
+        $this->assertTrue($phpdoc->hasTag('see'));
+    }
+
+    /**
+     * @covers ::initPhpDoc
+     * @throws \ReflectionException
+     */
+    public function testInitPhpDocClosureWithArgs(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                /**
+                 * Test docblock.
+                 */
+                function (int $a = null) {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertStringContainsString('Test docblock', $phpdoc->getText());
+        $this->assertEquals('@param int|null $a', $this->tagsToString($phpdoc, 'param'));
+        $this->assertFalse($phpdoc->hasTag('return'));
+        $this->assertTrue($phpdoc->hasTag('see'));
+    }
+
+    /**
+     * @covers ::initPhpDoc
+     * @throws \ReflectionException
+     */
+    public function testInitPhpDocClosureWithReturnType(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                /**
+                 * Test docblock.
+                 */
+                function (): int {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertStringContainsString('Test docblock', $phpdoc->getText());
+        $this->assertFalse($phpdoc->hasTag('param'));
+        $this->assertEquals('@return int', $this->tagsToString($phpdoc, 'return'));
+        $this->assertTrue($phpdoc->hasTag('see'));
+    }
+
+    /**
+     * @covers ::initPhpDoc
+     */
+    public function testInitPhpDocParamsAddedOnlyNotPresent(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                /**
+                 * Test docblock.
+                 *
+                 * @param \stdClass|null $a aaaaa
+                 */
+                function ($a = null): int {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertStringContainsString('Test docblock', $phpdoc->getText());
+        $this->assertEquals('@param \stdClass|null $a aaaaa', $this->tagsToString($phpdoc, 'param'));
+        $this->assertEquals('@return int', $this->tagsToString($phpdoc, 'return'));
+    }
+
+    /**
+     * @covers ::initPhpDoc
+     */
+    public function testInitPhpDocReturnAddedOnlyNotPresent(): void
+    {
+        $phpdoc = (new MacroMock())->getPhpDoc(
+            new ReflectionFunction(
+                /**
+                 * Test docblock.
+                 *
+                 * @return \stdClass|null rrrrrrr
+                 */
+                function ($a = null): int {
+                    return 0;
+                }
+            )
+        );
+
+        $this->assertNotNull($phpdoc);
+        $this->assertStringContainsString('Test docblock', $phpdoc->getText());
+        $this->assertEquals('@param mixed $a', $this->tagsToString($phpdoc, 'param'));
+        $this->assertEquals('@return \stdClass|null rrrrrrr', $this->tagsToString($phpdoc, 'return'));
+    }
+
     protected function tagsToString(DocBlock $docBlock, string $name)
     {
         $tags = $docBlock->getTagsByName($name);