Repository: phpactor/phpactor (php)

Issue:
Generic tag reported to be incompatible when correct for child generic classes
Hello!

I'm experiencing a problem similar to #2262, but this time when making child classes generic:

Code Example: 

```php
<?php

namespace Phpactor;

abstract class Model
{
}

/** @template TModel of \Phpactor\Model */
class Factory
{
}

/**
 * @template TModel of \Phpactor\Schedule
 * @extends Factory<TModel>
 */
class ScheduleFactory extends Factory
{
}

class Schedule extends Model
{
}

/** @extends ScheduleFactory<ChildSchedule> */
class ChildScheduleFactory extends ScheduleFactory
{
}

class ChildSchedule extends Schedule
{
}
```

From [PHPStan](https://phpstan.org/blog/generics-in-php-using-phpdocs):
![image](https://github.com/phpactor/phpactor/assets/4176520/36de3faf-171a-4b05-ae82-3cb9fa57eb6f)


Thanks!

Pull request description:
Gh 2295: Fix generic extends with templated argument
fixes #2295 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea88c82f..f452120c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 Changelog
 =========
 
+## master
+
+Bug fixes:
+
+  - Fix generic extends with templated argument #2295
+
 ## 2023.06.17-1
 
 Bug fixes:
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/Docblock/ClassGenericDiagnosticHelper.php b/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/Docblock/ClassGenericDiagnosticHelper.php
index 773c0211..628fe982 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/Docblock/ClassGenericDiagnosticHelper.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/Docblock/ClassGenericDiagnosticHelper.php
@@ -11,6 +11,7 @@ use Phpactor\WorseReflection\Core\Reflection\ReflectionClass;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClassLike;
 use Phpactor\WorseReflection\Core\Reflector\ClassReflector;
 use Phpactor\WorseReflection\Core\Type;
+use Phpactor\WorseReflection\Core\Type\ClassType;
 use Phpactor\WorseReflection\Core\Type\MixedType;
 use Phpactor\WorseReflection\Core\Type\MissingType;
 use Phpactor\WorseReflection\Core\Type\GenericClassType;
@@ -64,6 +65,7 @@ final class ClassGenericDiagnosticHelper
         }
 
         $genericTypes = array_filter($genericTypes, fn (Type $extendTagType) => $parentClass->type()->accepts($extendTagType)->isTrue());
+
         $defaultGenericType = new GenericClassType(
             $reflector,
             $parentClass->name(),
@@ -85,6 +87,23 @@ final class ClassGenericDiagnosticHelper
 
 
         $extendTagType = $genericTypes[array_key_first($genericTypes)];
+
+        // if generic uses a templateed type, then replace the templated type
+        // with the type restriction if it exists (e.g. replace T with Foo if
+        // @template T of Foo)
+        if ($extendTagType instanceof GenericClassType) {
+            $classTemplateMap = $class->templateMap();
+            $extendTagType = $extendTagType->withArguments(array_map(function (Type $type) use ($classTemplateMap) {
+                if (!$type instanceof ClassType) {
+                    return $type;
+                }
+                if (!$classTemplateMap->has($type->__toString())) {
+                    return $type;
+                }
+                return $classTemplateMap->get($type->__toString());
+            }, $extendTagType->arguments()));
+        }
+
         if ($parentClass->type()->upcastToGeneric()->accepts($extendTagType)->isFalse()) {
             yield new DocblockIncorrectClassGenericDiagnostic(
                 $range,
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/DocblockMissingExtendsTagProvider.php b/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/DocblockMissingExtendsTagProvider.php
index 65a96c3d..20566037 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/DocblockMissingExtendsTagProvider.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Diagnostics/DocblockMissingExtendsTagProvider.php
@@ -306,6 +306,52 @@ class DocblockMissingExtendsTagProvider implements DiagnosticProvider
                 Assert::assertCount(0, $diagnostics);
             }
         );
+        yield new DiagnosticExample(
+            title: 'extend with typed templated argument',
+            source: <<<'PHP'
+                <?php
+
+                abstract class Model {}
+                class Schedule extends Model {}
+
+                /** @template TModel of Model */
+                class Factory {}
+
+                /**
+                 * @template TModel of Schedule
+                 * @extends Factory<TModel>
+                 */
+                class ScheduleFactory extends Factory {}
+
+                PHP,
+            valid: true,
+            assertion: function (Diagnostics $diagnostics): void {
+                Assert::assertCount(0, $diagnostics);
+            }
+        );
+        yield new DiagnosticExample(
+            title: 'extend with unconstrained argument',
+            source: <<<'PHP'
+                <?php
+
+                abstract class Model {}
+                class Schedule extends Model {}
+
+                /** @template TModel */
+                class Factory {}
+
+                /**
+                 * @template TModel
+                 * @extends Factory<TModel>
+                 */
+                class ScheduleFactory extends Factory {}
+
+                PHP,
+            valid: true,
+            assertion: function (Diagnostics $diagnostics): void {
+                Assert::assertCount(0, $diagnostics);
+            }
+        );
     }
 
     public function name(): string


Test patch:
diff --git a/lib/WorseReflection/Tests/Inference/generics/gh-2295-test.test b/lib/WorseReflection/Tests/Inference/generics/gh-2295-test.test
new file mode 100644
index 00000000..0f14555e
--- /dev/null
+++ b/lib/WorseReflection/Tests/Inference/generics/gh-2295-test.test
@@ -0,0 +1,49 @@
+<?php
+
+abstract class Model
+{
+}
+
+/** @template TModel of Model */
+class Factory
+{
+    /**
+     * @return TModel
+     */
+    public function create(): object
+    {
+    }
+}
+
+/**
+ * @template TModel of Schedule
+ * @extends Factory<TModel>
+ */
+class ScheduleFactory extends Factory
+{
+    /**
+     * @param TModel $schedule
+     */
+    public function __construct(Schedule $schedule) 
+    {
+    }
+}
+
+class Schedule extends Model
+{
+}
+
+/** @extends ScheduleFactory<ChildSchedule> */
+class ChildScheduleFactory extends ScheduleFactory
+{
+}
+
+class ChildSchedule extends Schedule
+{
+}
+
+$s = (new ScheduleFactory(new ChildSchedule()))->create();
+wrAssertType('ChildSchedule', $s);
+
+$s = (new ChildScheduleFactory())->create();
+wrAssertType('ChildSchedule', $s);