Repository: tighten/tlint (php)

Issue:
Imports are marked unused when they're only used as part of a union type
Given this file tlint lints `DateTimeInterface` as an unused import:

```php
<?php

namespace App\Quotes;

use DateTimeInterface;

class Quote
{
    public function construct(string|DateTimeInterface $start, string|DateTimeInterface $end)
    {
        // ...
    }
}
```

If I remove `string|` so the type hint is just `DateTimeInterface` on its own, it works.

Using the Sublime plugin:

<img width="940" alt="Screen Shot 2021-02-03 at 2 14 06 PM" src="https://user-images.githubusercontent.com/18192441/106797215-54111f80-662a-11eb-84d2-abd30769a471.png">


Pull request:
Add support for recognizing union types in NoUnusedImports linter
Closes #218.

Patch:
diff --git a/src/Concerns/IdentifiesImports.php b/src/Concerns/IdentifiesImports.php
index 62228a2..2ec5be7 100644
--- a/src/Concerns/IdentifiesImports.php
+++ b/src/Concerns/IdentifiesImports.php
@@ -64,7 +64,8 @@ trait IdentifiesImports
                         $used[] = $name->toString();
                     }
                 }
-            } elseif (($node instanceof Node\Param || $node instanceof Node\Stmt\Property)
+            } elseif (
+                ($node instanceof Node\Param || $node instanceof Node\Stmt\Property)
                 && property_exists($node, 'type')
             ) {
                 $type = $node->type;
@@ -84,29 +85,32 @@ trait IdentifiesImports
                 foreach ($node->traits as $name) {
                     $used[] = $name->toString();
                 }
-            } elseif ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name
-            ) {
+            } elseif ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
                 $used[] = $node->name->toString();
-            } elseif ($node instanceof Node\Stmt\ClassMethod
-                && property_exists($node, 'returnType')
-            ) {
+            } elseif ($node instanceof Node\Stmt\ClassMethod && property_exists($node, 'returnType')) {
                 if ($node->returnType instanceof Node\Name) {
                     $used[] = $node->returnType->toString();
-                } elseif ($node->returnType instanceof Node\NullableType
+                } elseif (
+                    $node->returnType instanceof Node\NullableType
                     && $node->returnType->type instanceof Node\Name
                 ) {
                     $used[] = $node->returnType->type->toString();
                 }
-            } elseif ($node instanceof Node\Stmt\Function_
-                && property_exists($node, 'returnType')
-            ) {
+            } elseif ($node instanceof Node\Stmt\Function_ && property_exists($node, 'returnType')) {
                 if ($node->returnType instanceof Node\Name) {
                     $used[] = $node->returnType->toString();
-                } elseif ($node->returnType instanceof Node\NullableType
+                } elseif (
+                    $node->returnType instanceof Node\NullableType
                     && $node->returnType->type instanceof Node\Name
                 ) {
                     $used[] = $node->returnType->type->toString();
                 }
+            } elseif ($node instanceof Node\UnionType) {
+                foreach ($node->types as $type) {
+                    if ($type instanceof Node\Name) {
+                        $used[] = (string) $type;
+                    }
+                }
             }
 
             return false;


Test patch:
diff --git a/tests/Linting/Linters/NoUnusedImportsTest.php b/tests/Linting/Linters/NoUnusedImportsTest.php
index aeb390d..66a180f 100644
--- a/tests/Linting/Linters/NoUnusedImportsTest.php
+++ b/tests/Linting/Linters/NoUnusedImportsTest.php
@@ -514,4 +514,96 @@ file;
 
         $this->assertEmpty($lints);
     }
+
+    /** @test */
+    function does_not_trigger_when_import_used_in_union_type_function_parameter_typehint()
+    {
+        $file = <<<'file'
+<?php
+
+use App\Job;
+
+function test(string|Job $j)
+{
+    return $j ?? false;
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoUnusedImports($file)));
+    }
+
+    /** @test */
+    function does_not_trigger_when_import_used_in_union_type_method_parameter_typehint()
+    {
+        $file = <<<'file'
+<?php
+
+use App\AnotherJob;
+use App\Job;
+
+class Test
+{
+    function test(AnotherJob|Job $j)
+    {
+        return $j ?? false;
+    }
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoUnusedImports($file)));
+    }
+
+    /** @test */
+    function does_not_trigger_when_import_used_in_union_type_method_return_typehint()
+    {
+        $file = <<<'file'
+<?php
+
+use App\Job;
+
+class Test
+{
+    public function test(): boolean|Job
+    {
+        //
+    }
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoUnusedImports($file)));
+    }
+
+    /** @test */
+    function does_not_trigger_when_import_used_in_union_type_function_return_typehint()
+    {
+        $file = <<<'file'
+<?php
+
+use App\Job;
+
+function test(): string|Job
+{
+    //
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoUnusedImports($file)));
+    }
+
+    /** @test */
+    function does_not_trigger_when_import_used_in_union_type_class_property_typehint()
+    {
+        $file = <<<'file'
+<?php
+
+use DateTimeInterface;
+
+class Test
+{
+    public string|DateTimeInterface $start;
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoUnusedImports($file)));
+    }
 }