Repository: phpactor/phpactor (php)

Issue:
[RPC][documentation] Missing documentation for "language" parameter
It seems some commands like `goto_definition` has a language parameter which need to be provided otherwise it defaults to php.

For example this is needed for the [behat-extension](https://github.com/phpactor/behat-extension) otherwise it will do nothing.


Pull request description:
Adding documentation to the goto commands
closes #1030 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b21eaf8..4f5eaa88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Changelog
 Features:
 
   - [sf] New Symfony extension #1915
+  - [wr] Generic constructor parameters support #1920
 
 Bug fixes:
 
diff --git a/doc/reference/rpc_command.rst b/doc/reference/rpc_command.rst
index 6cd2d956..b06e3ce8 100644
--- a/doc/reference/rpc_command.rst
+++ b/doc/reference/rpc_command.rst
@@ -1036,6 +1036,9 @@ _RpcHandler_GotoDefinitionHandler
 Type: string
 
 
+Language of the current file
+
+
 **Default**: ``"php"``
 
 
@@ -1049,9 +1052,15 @@ Type: string
 Type: string
 
 
+Where should the reference be opened
+
+
 **Default**: ``"focused_window"``
 
 
+**Allowed values**: "focused_window", "vsplit", "hsplit", "new_tab"
+
+
 .. _RpcCommand_goto_definition_offset:
 
 
@@ -1059,6 +1068,12 @@ Type: string
 """"""""""
 
 
+Type: int
+
+
+Number of character into the buffer
+
+
 **Default**: ``null``
 
 
@@ -1069,6 +1084,9 @@ Type: string
 """"""""""
 
 
+Content of the current file
+
+
 **Default**: ``null``
 
 
@@ -1079,6 +1097,9 @@ Type: string
 """"""""
 
 
+Path of the current file
+
+
 **Default**: ``null``
 
 
@@ -1096,6 +1117,12 @@ _RpcHandler_GotoTypeHandler
 """"""""""""
 
 
+Type: string
+
+
+Language of the current file
+
+
 **Default**: ``"php"``
 
 
@@ -1106,9 +1133,18 @@ _RpcHandler_GotoTypeHandler
 """"""""""
 
 
+Type: string
+
+
+Where should the reference be opened
+
+
 **Default**: ``"focused_window"``
 
 
+**Allowed values**: "focused_window", "vsplit", "hsplit", "new_tab"
+
+
 .. _RpcCommand_goto_type_offset:
 
 
@@ -1116,6 +1152,12 @@ _RpcHandler_GotoTypeHandler
 """"""""""
 
 
+Type: int
+
+
+Number of character into the buffer
+
+
 **Default**: ``null``
 
 
@@ -1126,6 +1168,9 @@ _RpcHandler_GotoTypeHandler
 """"""""""
 
 
+Content of the current file
+
+
 **Default**: ``null``
 
 
@@ -1136,6 +1181,9 @@ _RpcHandler_GotoTypeHandler
 """"""""
 
 
+Path of the current file
+
+
 **Default**: ``null``
 
 
@@ -1153,6 +1201,12 @@ _RpcHandler_GotoImplementationHandler
 """"""""""""
 
 
+Type: string
+
+
+Language of the current file
+
+
 **Default**: ``"php"``
 
 
@@ -1163,9 +1217,18 @@ _RpcHandler_GotoImplementationHandler
 """"""""""
 
 
+Type: string
+
+
+Where should the reference be opened
+
+
 **Default**: ``"focused_window"``
 
 
+**Allowed values**: "focused_window", "vsplit", "hsplit", "new_tab"
+
+
 .. _RpcCommand_goto_implementation_offset:
 
 
@@ -1173,6 +1236,12 @@ _RpcHandler_GotoImplementationHandler
 """"""""""
 
 
+Type: int
+
+
+Number of character into the buffer
+
+
 **Default**: ``null``
 
 
@@ -1183,6 +1252,9 @@ _RpcHandler_GotoImplementationHandler
 """"""""""
 
 
+Content of the current file
+
+
 **Default**: ``null``
 
 
@@ -1193,6 +1265,9 @@ _RpcHandler_GotoImplementationHandler
 """"""""
 
 
+Path of the current file
+
+
 **Default**: ``null``
 
 
diff --git a/doc/reference/types.rst b/doc/reference/types.rst
index 65e0b7cd..22f37f91 100644
--- a/doc/reference/types.rst
+++ b/doc/reference/types.rst
@@ -1,9 +1,15 @@
 Types
 =====
 
-This document provides a non-exhaustive list of PHP and docblock types and
-indicates if Phpactor supports them or not. For more information about these
-types see:
+This document attempts to show all the types that Phpactor supports.
+
+.. contents::
+   :depth: 1
+   :backlinks: none
+   :local:
+
+For more information about types see:
+
 
 - `Official PHP Documentation <https://www.php.net/manual/en/language.types.declarations.php>`_
 - `PHPStan Types <https://phpstan.org/writing-php-code/phpdoc-types>`_
@@ -25,9 +31,9 @@ Basic Types
     Resource         (internal type)     ``*``      ✔
     String           ``string``          ``*``      ✔
     Self             ``self``            ``*``      ✔
-    Parent           ``parent``          ``*``      🤷 mostly
-    Callable         ``callable``        ``*``      ❓
-    Iterable         ``iterable``        ``7.1``    ❓ 
+    Parent           ``parent``          ``*``      ✔
+    Callable         ``callable``        ``*``      ✔
+    Iterable         ``iterable``        ``7.1``    ✔ 
     Nullable         ``?Foor``           ``7.1``    ✔
     Object           ``object``          ``7.2``    ✔
     Union            ``Foo|Bar``         ``8.0``    ✔
@@ -92,7 +98,7 @@ Integer Types
     ==============  =============  =========  ===========
 
 Conditional Types
-~~~~~~~~~~~~~~~~~
+-----------------
 
 Phpactor undestands conditional return types of the form:
 
@@ -112,3 +118,73 @@ Phpactor undestands conditional return types of the form:
     function array_some(array $array) {
         return array_sum($array);
     }
+
+Generic Types
+-------------
+
+Phpactor understands Generic (or templated) types. See `PHPStan <https://phpstan.org/blog/generics-in-php-using-phpdocs>`_ or
+`Psalm <https://psalm.dev/docs/annotating_code/templated_annotations/>`_
+documentation for what these are and how they work.
+
+Phpactor supports:
+
+- ``@implements`` and ``@extends`` in addition to ``@template-extends`` and
+  ``@template-implements``.
+- ``@template`` and ``@template T of Foo``
+- Injecting template variables into the constructor.
+- Method level template vars.
+- ``class-string<T>``
+
+For example:
+
+.. code-block:: php
+
+    <?php
+
+    /**
+     * @template T
+     */
+    class Foo {
+        /**
+         * @var T
+         */
+        private $a;
+
+        /** @param T $a */
+        public function __construct($a) {
+            $this->a = $a;
+        }
+
+        /**
+         * @return T
+         */
+        public function a()
+        {
+            return $this->a;
+        }
+    }
+
+    $f = new Foo(new Bar());
+    $bar = $f->a(); // Phpactor now knows that `$bar` is Bar
+
+In addition Phpactor supports `class-string<T>` which allows you to capture a
+class type from a class string (e.g. ``MyClass::class`` is interpreted as a
+`class-string`. The following extract is from the Phpactor Container.
+
+.. code-block:: php
+
+    <?php
+
+    interface Container
+    {
+        /**
+         * @template T of object
+         * @param class-string<T>|string $id
+         * @return ($id is class-string<T> ? T : mixed)
+         */
+        public function get($id);
+    }
+
+The conditional type enables the return value of ``get`` to be an object of
+class ``T`` if the ``$id`` is a ``class-string`` or ``mixed`` in any other
+case.
diff --git a/lib/Extension/ReferenceFinderRpc/Handler/GotoDefinitionHandler.php b/lib/Extension/ReferenceFinderRpc/Handler/GotoDefinitionHandler.php
index 526bcaca..61528570 100644
--- a/lib/Extension/ReferenceFinderRpc/Handler/GotoDefinitionHandler.php
+++ b/lib/Extension/ReferenceFinderRpc/Handler/GotoDefinitionHandler.php
@@ -42,10 +42,21 @@ class GotoDefinitionHandler implements Handler
             self::PARAM_SOURCE,
             self::PARAM_PATH,
         ]);
+        $resolver->setEnums([
+            self::PARAM_TARGET => OpenFileResponse::VALID_TARGETS,
+        ]);
         $resolver->setTypes([
+            self::PARAM_OFFSET => 'integer',
             self::PARAM_LANGUAGE => 'string',
             self::PARAM_TARGET => 'string',
         ]);
+        $resolver->setDescriptions([
+            self::PARAM_OFFSET => 'Number of character into the buffer',
+            self::PARAM_SOURCE => 'Content of the current file',
+            self::PARAM_PATH => 'Path of the current file',
+            self::PARAM_LANGUAGE => 'Language of the current file',
+            self::PARAM_TARGET => 'Where should the reference be opened',
+        ]);
     }
 
     public function handle(array $arguments)
diff --git a/lib/Extension/ReferenceFinderRpc/Handler/GotoImplementationHandler.php b/lib/Extension/ReferenceFinderRpc/Handler/GotoImplementationHandler.php
index 8f48bcc6..17ea0a94 100644
--- a/lib/Extension/ReferenceFinderRpc/Handler/GotoImplementationHandler.php
+++ b/lib/Extension/ReferenceFinderRpc/Handler/GotoImplementationHandler.php
@@ -25,7 +25,6 @@ class GotoImplementationHandler extends AbstractHandler
     const PARAM_PATH = 'path';
     const PARAM_LANGUAGE = 'language';
     const PARAM_TARGET = 'target';
-    const PARAM_SELECTED_PATH = 'selected_path';
 
     private ClassImplementationFinder $finder;
 
@@ -51,6 +50,21 @@ class GotoImplementationHandler extends AbstractHandler
             self::PARAM_SOURCE,
             self::PARAM_PATH,
         ]);
+        $resolver->setEnums([
+            self::PARAM_TARGET => OpenFileResponse::VALID_TARGETS,
+        ]);
+        $resolver->setTypes([
+            self::PARAM_OFFSET => 'integer',
+            self::PARAM_LANGUAGE => 'string',
+            self::PARAM_TARGET => 'string',
+        ]);
+        $resolver->setDescriptions([
+            self::PARAM_OFFSET => 'Number of character into the buffer',
+            self::PARAM_SOURCE => 'Content of the current file',
+            self::PARAM_PATH => 'Path of the current file',
+            self::PARAM_LANGUAGE => 'Language of the current file',
+            self::PARAM_TARGET => 'Where should the reference be opened',
+        ]);
     }
 
     public function handle(array $arguments)
diff --git a/lib/Extension/ReferenceFinderRpc/Handler/GotoTypeHandler.php b/lib/Extension/ReferenceFinderRpc/Handler/GotoTypeHandler.php
index 3486c251..aee97115 100644
--- a/lib/Extension/ReferenceFinderRpc/Handler/GotoTypeHandler.php
+++ b/lib/Extension/ReferenceFinderRpc/Handler/GotoTypeHandler.php
@@ -42,6 +42,21 @@ class GotoTypeHandler implements Handler
             self::PARAM_SOURCE,
             self::PARAM_PATH,
         ]);
+        $resolver->setEnums([
+            self::PARAM_TARGET => OpenFileResponse::VALID_TARGETS,
+        ]);
+        $resolver->setTypes([
+            self::PARAM_OFFSET => 'integer',
+            self::PARAM_LANGUAGE => 'string',
+            self::PARAM_TARGET => 'string',
+        ]);
+        $resolver->setDescriptions([
+            self::PARAM_OFFSET => 'Number of character into the buffer',
+            self::PARAM_SOURCE => 'Content of the current file',
+            self::PARAM_PATH => 'Path of the current file',
+            self::PARAM_LANGUAGE => 'Language of the current file',
+            self::PARAM_TARGET => 'Where should the reference be opened',
+        ]);
     }
 
     public function handle(array $arguments)
diff --git a/lib/Indexer/Adapter/Worse/WorseRecordReferenceEnhancer.php b/lib/Indexer/Adapter/Worse/WorseRecordReferenceEnhancer.php
index a4406c5b..07447ad3 100644
--- a/lib/Indexer/Adapter/Worse/WorseRecordReferenceEnhancer.php
+++ b/lib/Indexer/Adapter/Worse/WorseRecordReferenceEnhancer.php
@@ -8,6 +8,7 @@ use Phpactor\Indexer\Model\Record\FileRecord;
 use Phpactor\Indexer\Model\Record\MemberRecord;
 use Phpactor\WorseReflection\Core\Exception\NotFound;
 use Phpactor\WorseReflection\Core\Reflector\SourceCodeReflector;
+use Phpactor\WorseReflection\Core\Type\ClassType;
 use Psr\Log\LoggerInterface;
 use Safe\Exceptions\FilesystemException;
 use function Safe\file_get_contents;
@@ -65,6 +66,10 @@ class WorseRecordReferenceEnhancer implements RecordReferenceEnhancer
             return $reference;
         }
 
+        if ($containerType instanceof ClassType) {
+            $containerType = $containerType->name()->__toString();
+        }
+
         return $reference->withContainerType($containerType);
     }
 }
diff --git a/lib/WorseReflection/Core/DefaultResolverFactory.php b/lib/WorseReflection/Core/DefaultResolverFactory.php
index 773c5c1d..ad34b80f 100644
--- a/lib/WorseReflection/Core/DefaultResolverFactory.php
+++ b/lib/WorseReflection/Core/DefaultResolverFactory.php
@@ -54,6 +54,7 @@ use Phpactor\WorseReflection\Core\Inference\FunctionStub\InArrayStub;
 use Phpactor\WorseReflection\Core\Inference\FunctionStub\IsSomethingStub;
 use Phpactor\WorseReflection\Core\Inference\FunctionStub\IteratorToArrayStub;
 use Phpactor\WorseReflection\Core\Inference\FunctionStub\ResetStub;
+use Phpactor\WorseReflection\Core\Inference\GenericMapResolver;
 use Phpactor\WorseReflection\Core\Inference\NodeToTypeConverter;
 use Phpactor\WorseReflection\Core\Inference\Resolver;
 use Phpactor\WorseReflection\Core\Inference\Resolver\AnonymousFunctionCreationExpressionResolver;
@@ -107,15 +108,19 @@ final class DefaultResolverFactory
 
     private NodeContextFromMemberAccess $nodeContextFromMemberAccess;
 
+    private GenericMapResolver $genericResolver;
+
     public function __construct(
         Reflector $reflector,
         NodeToTypeConverter $nodeTypeConverter,
+        GenericMapResolver $genericResolver,
         NodeContextFromMemberAccess $nodeContextFromMemberAccess
     ) {
         $this->reflector = $reflector;
         $this->nodeTypeConverter = $nodeTypeConverter;
         $this->functionStubRegistry = $this->createStubRegistry();
         $this->nodeContextFromMemberAccess = $nodeContextFromMemberAccess;
+        $this->genericResolver = $genericResolver;
     }
 
     /**
@@ -142,7 +147,7 @@ final class DefaultResolverFactory
             TraitDeclaration::class => new ClassLikeResolver(),
             EnumDeclaration::class => new ClassLikeResolver(),
             FunctionDeclaration::class => new FunctionDeclarationResolver(),
-            ObjectCreationExpression::class => new ObjectCreationExpressionResolver(),
+            ObjectCreationExpression::class => new ObjectCreationExpressionResolver($this->genericResolver),
             SubscriptExpression::class => new SubscriptExpressionResolver(),
             StringLiteral::class => new StringLiteralResolver(),
             NumericLiteral::class => new NumericLiteralResolver(),
diff --git a/lib/WorseReflection/Core/Inference/FunctionArguments.php b/lib/WorseReflection/Core/Inference/FunctionArguments.php
index ebbca764..81ed314f 100644
--- a/lib/WorseReflection/Core/Inference/FunctionArguments.php
+++ b/lib/WorseReflection/Core/Inference/FunctionArguments.php
@@ -32,6 +32,13 @@ class FunctionArguments implements IteratorAggregate, Countable
         $this->frame = $frame;
     }
 
+    public function __toString(): string
+    {
+        return implode(', ', array_map(function (NodeContext $type) {
+            return $type->type()->__toString();
+        }, iterator_to_array($this->getIterator())));
+    }
+
     public static function fromList(NodeContextResolver $resolver, Frame $frame, ?ArgumentExpressionList $list): self
     {
         if ($list === null) {
diff --git a/lib/WorseReflection/Core/Inference/GenericMapResolver.php b/lib/WorseReflection/Core/Inference/GenericMapResolver.php
index 0714a15d..a2a3fa8c 100644
--- a/lib/WorseReflection/Core/Inference/GenericMapResolver.php
+++ b/lib/WorseReflection/Core/Inference/GenericMapResolver.php
@@ -85,7 +85,7 @@ class GenericMapResolver
                 }
 
                 if ($templateMap->has($type->short())) {
-                    $templateMap->replace($type->short(), $arguments->at($parameter->index())->type());
+                    $templateMap->replace($type->short(), $arguments->at($parameter->index())->type()->generalize());
                 }
 
                 return $type;
diff --git a/lib/WorseReflection/Core/Inference/Resolver/ObjectCreationExpressionResolver.php b/lib/WorseReflection/Core/Inference/Resolver/ObjectCreationExpressionResolver.php
index 2c0ee4fb..d120ea3b 100644
--- a/lib/WorseReflection/Core/Inference/Resolver/ObjectCreationExpressionResolver.php
+++ b/lib/WorseReflection/Core/Inference/Resolver/ObjectCreationExpressionResolver.php
@@ -5,13 +5,26 @@ namespace Phpactor\WorseReflection\Core\Inference\Resolver;
 use Microsoft\PhpParser\Node;
 use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
 use Phpactor\WorseReflection\Core\Exception\CouldNotResolveNode;
+use Phpactor\WorseReflection\Core\Exception\NotFound;
 use Phpactor\WorseReflection\Core\Inference\Frame;
+use Phpactor\WorseReflection\Core\Inference\FunctionArguments;
+use Phpactor\WorseReflection\Core\Inference\GenericMapResolver;
 use Phpactor\WorseReflection\Core\Inference\NodeContext;
 use Phpactor\WorseReflection\Core\Inference\Resolver;
 use Phpactor\WorseReflection\Core\Inference\NodeContextResolver;
+use Phpactor\WorseReflection\Core\Type;
+use Phpactor\WorseReflection\Core\Type\ClassType;
+use Phpactor\WorseReflection\Core\Type\GenericClassType;
 
 class ObjectCreationExpressionResolver implements Resolver
 {
+    private GenericMapResolver $resolver;
+
+    public function __construct(GenericMapResolver $resolver)
+    {
+        $this->resolver = $resolver;
+    }
+
     public function resolve(NodeContextResolver $resolver, Frame $frame, Node $node): NodeContext
     {
         assert($node instanceof ObjectCreationExpression);
@@ -19,6 +32,40 @@ class ObjectCreationExpressionResolver implements Resolver
             throw new CouldNotResolveNode(sprintf('Could not create object from "%s"', get_class($node)));
         }
 
-        return $resolver->resolveNode($frame, $node->classTypeDesignator);
+
+        $classContext = $resolver->resolveNode($frame, $node->classTypeDesignator);
+        $classType = $classContext->type();
+
+        if ($classType instanceof ClassType) {
+            return $classContext->withType($this->resolveClassType($resolver, $frame, $node, $classType));
+        }
+
+
+        return $classContext;
+    }
+
+    private function resolveClassType(NodeContextResolver $resolver, Frame $frame, ObjectCreationExpression $node, ClassType $classType): Type
+    {
+        try {
+            $reflection = $resolver->reflector()->reflectClass($classType->name());
+        } catch (NotFound $notFound) {
+            return $classType;
+        }
+        if (!$reflection->methods()->has('__construct')) {
+            return $classType;
+        }
+        $templateMap = $reflection->docblock()->templateMap();
+
+        if (!count($templateMap)) {
+            return $classType;
+        }
+
+        $arguments = FunctionArguments::fromList($resolver, $frame, $node->argumentExpressionList);
+        $templateMap = $this->resolver->mergeParameters(
+            $templateMap,
+            $reflection->methods()->get('__construct')->parameters(),
+            $arguments
+        );
+        return new GenericClassType($resolver->reflector(), $classType->name(), $templateMap->toArguments());
     }
 }
diff --git a/lib/WorseReflection/Core/ServiceLocator.php b/lib/WorseReflection/Core/ServiceLocator.php
index 224cd67f..8c930c6c 100644
--- a/lib/WorseReflection/Core/ServiceLocator.php
+++ b/lib/WorseReflection/Core/ServiceLocator.php
@@ -152,6 +152,7 @@ class ServiceLocator
             (new DefaultResolverFactory(
                 $this->reflector,
                 $this->nameResolver,
+                new GenericMapResolver($this->reflector),
                 new NodeContextFromMemberAccess(
                     new GenericMapResolver($this->reflector),
                     $this->memberContextResolvers
diff --git a/lib/WorseReflection/Core/TemplateMap.php b/lib/WorseReflection/Core/TemplateMap.php
index 97a94fa5..5b02f0ac 100644
--- a/lib/WorseReflection/Core/TemplateMap.php
+++ b/lib/WorseReflection/Core/TemplateMap.php
@@ -108,4 +108,12 @@ final class TemplateMap implements Countable
 
         return new self($newMap);
     }
+
+    /**
+     * @return Type[]
+     */
+    public function toArguments(): array
+    {
+        return array_values($this->map);
+    }
 }


Test patch:
diff --git a/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassOne.ph b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassOne.ph
new file mode 100644
index 00000000..4fc83772
--- /dev/null
+++ b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassOne.ph
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @template T
+ */
+class ClassOne
+{
+    /**
+     * @param T
+     */
+    public string $foobar;
+
+    /**
+     * @param T $foobar
+     */
+    public function __construct(string $foobar)
+    {
+        $this->foobar = $foobar;
+    }
+}
diff --git a/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassTwo.ph b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassTwo.ph
new file mode 100644
index 00000000..1135ad3e
--- /dev/null
+++ b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/ClassTwo.ph
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @template T
+ */
+class ClassTwo extends ClassOne
+{
+    public function barfoo(): string
+    {
+        return $this->foobar;
+    }
+}
diff --git a/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/test.ph b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/test.ph
new file mode 100644
index 00000000..f274fd4a
--- /dev/null
+++ b/lib/Rename/Tests/Cases/member_renamer/property_declaration_public_generic/test.ph
@@ -0,0 +1,15 @@
+<?php
+
+require __DIR__ . '/ClassOne.php';
+require __DIR__ . '/ClassTwo.php';
+
+$two = new ClassTwo('foobar');
+$one = new ClassOne('foobar');
+if (!$two->barfoo() === 'foobar') {
+    echo 'expected "foobar" but didn\'t get it';
+    exit(127);
+}
+if (!$two->foobar === 'foobar') {
+    echo 'expected "foobar" but didn\'t get it';
+    exit(127);
+}
diff --git a/lib/Rename/Tests/Integration/Adapter/ReferenceFinder/MemberRenamerTest.php b/lib/Rename/Tests/Integration/Adapter/ReferenceFinder/MemberRenamerTest.php
index 529db048..3f4808b0 100644
--- a/lib/Rename/Tests/Integration/Adapter/ReferenceFinder/MemberRenamerTest.php
+++ b/lib/Rename/Tests/Integration/Adapter/ReferenceFinder/MemberRenamerTest.php
@@ -159,6 +159,32 @@ class MemberRenamerTest extends RenamerTestCase
             }
         ];
 
+        yield 'property declaration generic' => [
+            'member_renamer/property_declaration_public_generic',
+            function (Reflector $reflector, Renamer $renamer): Generator {
+                $reflection = $reflector->reflectClass('ClassOne');
+                $property = $reflection->properties()->get('foobar');
+
+                return $renamer->rename(
+                    $reflection->sourceCode(),
+                    $property->nameRange()->start(),
+                    'newName'
+                );
+            },
+            function (Reflector $reflector): void {
+                $propertyAccesses = $reflector->navigate(
+                    $this->workspace()->getContents('project/ClassTwo.php')
+                )->propertyAccesses();
+                $first = $propertyAccesses->first();
+                self::assertEquals('newName', $first->name());
+                $propertyAccesses = $reflector->navigate(
+                    $this->workspace()->getContents('project/test.php')
+                )->propertyAccesses();
+                $first = $propertyAccesses->first();
+                self::assertEquals('newName', $first->name());
+            }
+        ];
+
         // these tests verify that the code works using the current PHP runtime
         // so fail if the runtime doesn't support promoted properties
         if (version_compare(PHP_VERSION, '8.0', '>=')) {
diff --git a/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_generic_objects.test b/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_generic_objects.test
new file mode 100644
index 00000000..829464a4
--- /dev/null
+++ b/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_generic_objects.test
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @template T
+ */
+class Foobar {
+/** @param T $param */
+public function __construct($param) {}
+}
+
+$foo = new Foobar(1);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
+$foo = new Foobar(2);
diff --git a/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_objects.test b/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_objects.test
new file mode 100644
index 00000000..b8efcdd6
--- /dev/null
+++ b/lib/WorseReflection/Tests/Benchmarks/fixtures/diagnostics/lots_of_new_objects.test
@@ -0,0 +1,62 @@
+<?php
+
+class Foobar {
+}
+
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
+$foo = new Foobar();
diff --git a/lib/WorseReflection/Tests/Inference/generics/constructor-param-and-extend.test b/lib/WorseReflection/Tests/Inference/generics/constructor-param-and-extend.test
new file mode 100644
index 00000000..c9f9ac97
--- /dev/null
+++ b/lib/WorseReflection/Tests/Inference/generics/constructor-param-and-extend.test
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @template T
+ */
+class Bar {
+  /**
+   * @return T
+   */
+   public function b()
+   {
+   }
+}
+
+/**
+ * @template T
+ * @extends Bar<T>
+ */
+class Foo extends Bar {
+    /**
+     * @var T
+     */
+    private $a;
+
+    /** @param T $a */
+    public function __construct($a) {
+        $this->a = $a;
+    }
+
+    /**
+     * @return T
+     */
+    public function a()
+    {
+        return $this->a;
+    }
+}
+
+$f = new Foo('hello');
+wrAssertType('Foo<string>', $f);
+wrAssertType('string', $f->b());
diff --git a/lib/WorseReflection/Tests/Inference/generics/constructor-params.test b/lib/WorseReflection/Tests/Inference/generics/constructor-params.test
new file mode 100644
index 00000000..f367fce1
--- /dev/null
+++ b/lib/WorseReflection/Tests/Inference/generics/constructor-params.test
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @template T
+ */
+class Foo {
+    /**
+     * @var T
+     */
+    private $a;
+
+    /** @param T $a */
+    public function __construct($a) {
+        $this->a = $a;
+    }
+
+    /**
+     * @return T
+     */
+    public function a()
+    {
+        return $this->a;
+    }
+}
+
+$f = new Foo('hello');
+wrAssertType('string', $f->a());
diff --git a/lib/WorseReflection/Tests/Inference/generics/method_generic.test b/lib/WorseReflection/Tests/Inference/generics/method_generic.test
index d21a3748..84fd825f 100644
--- a/lib/WorseReflection/Tests/Inference/generics/method_generic.test
+++ b/lib/WorseReflection/Tests/Inference/generics/method_generic.test
@@ -16,4 +16,4 @@ class Foo
 
 $f = new Foo();
 $res = $f->bar('hello');
-wrAssertType('"hello"', $res);
+wrAssertType('string', $res);
diff --git a/lib/WorseReflection/Tests/Integration/Core/Inference/NodeContextResolverTest.php b/lib/WorseReflection/Tests/Integration/Core/Inference/NodeContextResolverTest.php
index af7bb281..3fc17487 100644
--- a/lib/WorseReflection/Tests/Integration/Core/Inference/NodeContextResolverTest.php
+++ b/lib/WorseReflection/Tests/Integration/Core/Inference/NodeContextResolverTest.php
@@ -1167,6 +1167,7 @@ class NodeContextResolverTest extends IntegrationTestCase
             (new DefaultResolverFactory(
                 $reflector,
                 $nameResolver,
+                new GenericMapResolver($reflector),
                 new NodeContextFromMemberAccess(
                     new GenericMapResolver($reflector),
                     []