Repository: phpactor/phpactor (php)

Issue:
LSP rename: missing `RenameFile` operation in `documentChanges`
Currently when I try to rename some class form LSP client I have to rename its file manually. For a simple class with no references the server sends to the client `WorkspaceEdit` with a single `TextDocumentEdit` (as I guess from its format) inside `documentChanges`. It's enough to change the file content by some client's response handler. The original name remains untouched. 

After dumping such example edit in Lua format
```lua
{ -- WorkspaceEdit
  documentChanges = {
  { -- TextDocumentEdit
      edits = {
      {
          newText = "Arctgx\\PhpactorTestEnv",
          range = {
            ['end'] = {
              character = 32,
              line = 2
            },
            start = {
              character = 10,
              line = 2
            }
          }
        },
      {
          newText = "V",
          range = {
            ['end'] = {
              character = 7,
              line = 4
            },
            start = {
              character = 6,
              line = 4
            }
          }
        }
      },
      textDocument = {
        uri = "file:///home/arctgx/dev/arctgx/phpactor-test-env/src/Z.php",
        version = 7
      }
    },
  },
}
```
and adding `RenameFile` and applying such simulated response on the client side
```lua
local out = { -- WorkspaceEdit
  documentChanges = {
    { -- RenameFile
        kind = 'rename',
        oldUri = "file:///home/arctgx/dev/arctgx/phpactor-test-env/src/Z.php",
        newUri = "file:///home/arctgx/dev/arctgx/phpactor-test-env/src/V.php",
      },
  { -- TextDocumentEdit
      edits = {
      {
          newText = "Arctgx\\PhpactorTestEnv",
          range = {
            ['end'] = {
              character = 32,
              line = 2
            },
            start = {
              character = 10,
              line = 2
            }
          }
        },
      {
          newText = "V",
          range = {
            ['end'] = {
              character = 7,
              line = 4
            },
            start = {
              character = 6,
              line = 4
            }
          }
        }
      },
      textDocument = {
        uri = "file:///home/arctgx/dev/arctgx/phpactor-test-env/src/V.php",
        version = 7
      }
    },
  },
}
vim.lsp.util.apply_workspace_edit(out, vim.lsp.get_client_by_id(1).offset_encoding)
```
I get the expected change. It suggests that `RenameFile` should be added to `documentChanges`.

Pull request description:
Complete class renaming
Closes #1366

Patch:
diff --git a/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileNameToUriConverter.php b/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileNameToUriConverter.php
new file mode 100644
index 00000000..e07cce04
--- /dev/null
+++ b/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileNameToUriConverter.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Phpactor\Extension\LanguageServerRename\Adapter\ClassToFile;
+
+use Phpactor\ClassFileConverter\Domain\ClassName;
+use Phpactor\ClassFileConverter\Domain\ClassToFile;
+use Phpactor\Extension\LanguageServerRename\Model\Exception\CouldNotConvertClassToUri;
+use Phpactor\Extension\LanguageServerRename\Model\NameToUriConverter;
+use Phpactor\TextDocument\TextDocumentUri;
+use RuntimeException;
+
+class ClassToFileNameToUriConverter implements NameToUriConverter
+{
+    private ClassToFile $classToFile;
+
+    public function __construct(
+        ClassToFile $classToFile
+    ) {
+        $this->classToFile = $classToFile;
+    }
+
+    public function convert(string $className): TextDocumentUri
+    {
+        try {
+            return TextDocumentUri::fromString($this->classToFile->classToFileCandidates(ClassName::fromString($className))->best());
+        } catch (RuntimeException $error) {
+            throw new CouldNotConvertClassToUri($error->getMessage(), 0, $error);
+        }
+    }
+}
diff --git a/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileUriToNameConverter.php b/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileUriToNameConverter.php
index 8bf04f8d..77d02d04 100644
--- a/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileUriToNameConverter.php
+++ b/lib/Extension/LanguageServerRename/Adapter/ClassToFile/ClassToFileUriToNameConverter.php
@@ -18,7 +18,6 @@ class ClassToFileUriToNameConverter implements UriToNameConverter
         $this->fileToClass = $fileToClass;
     }
 
-    
     public function convert(TextDocumentUri $uri): string
     {
         try {
diff --git a/lib/Extension/LanguageServerRename/Adapter/ReferenceFinder/ClassMover/ClassRenamer.php b/lib/Extension/LanguageServerRename/Adapter/ReferenceFinder/ClassMover/ClassRenamer.php
index bebad375..faf61803 100644
--- a/lib/Extension/LanguageServerRename/Adapter/ReferenceFinder/ClassMover/ClassRenamer.php
+++ b/lib/Extension/LanguageServerRename/Adapter/ReferenceFinder/ClassMover/ClassRenamer.php
@@ -15,16 +15,21 @@ use Phpactor\ClassMover\ClassMover;
 use Phpactor\ClassMover\Domain\Name\QualifiedName;
 use Phpactor\Extension\LanguageServerRename\Adapter\Tolerant\TokenUtil;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEdit;
+use Phpactor\Extension\LanguageServerRename\Model\NameToUriConverter;
+use Phpactor\Extension\LanguageServerRename\Model\RenameResult;
 use Phpactor\Extension\LanguageServerRename\Model\Renamer;
 use Phpactor\ReferenceFinder\ReferenceFinder;
 use Phpactor\TextDocument\ByteOffset;
 use Phpactor\TextDocument\ByteOffsetRange;
 use Phpactor\TextDocument\TextDocument;
 use Phpactor\TextDocument\TextDocumentLocator;
+use Phpactor\TextDocument\TextDocumentUri;
 use RuntimeException;
 
 final class ClassRenamer implements Renamer
 {
+    private NameToUriConverter $nameToUriConverter;
+
     private ReferenceFinder $referenceFinder;
 
     private TextDocumentLocator $locator;
@@ -34,11 +39,13 @@ final class ClassRenamer implements Renamer
     private ClassMover $classMover;
 
     public function __construct(
+        NameToUriConverter $nameToUriConverter,
         ReferenceFinder $referenceFinder,
         TextDocumentLocator $locator,
         Parser $parser,
         ClassMover $classMover
     ) {
+        $this->nameToUriConverter = $nameToUriConverter;
         $this->referenceFinder = $referenceFinder;
         $this->locator = $locator;
         $this->parser = $parser;
@@ -68,13 +75,17 @@ final class ClassRenamer implements Renamer
         return null;
     }
 
-    
     public function rename(TextDocument $textDocument, ByteOffset $offset, string $newName): Generator
     {
         $node = $this->parser->parseSourceFile($textDocument->__toString())->getDescendantNodeAtPosition($offset->toInt());
 
         $originalName = $this->getFullName($node);
         $newName = $this->createNewName($originalName, $newName);
+        $newUri = TextDocumentUri::fromString($this->nameToUriConverter->convert($newName));
+
+        if ($newName === $originalName->getFullyQualifiedNameText()) {
+            return;
+        }
 
         $seen = [];
         foreach ($this->referenceFinder->findReferences($textDocument, $offset) as $reference) {
@@ -97,10 +108,12 @@ final class ClassRenamer implements Renamer
             foreach ($edits as $edit) {
                 yield new LocatedTextEdit(
                     $reference->location()->uri(),
-                    $edit
+                    $edit,
                 );
             }
         }
+
+        return new RenameResult($textDocument->uri(), $newUri);
     }
 
     private function rangeText(TextDocument $textDocument, ByteOffsetRange $range): string
diff --git a/lib/Extension/LanguageServerRename/Handler/RenameHandler.php b/lib/Extension/LanguageServerRename/Handler/RenameHandler.php
index a80c7ea0..2d5092de 100644
--- a/lib/Extension/LanguageServerRename/Handler/RenameHandler.php
+++ b/lib/Extension/LanguageServerRename/Handler/RenameHandler.php
@@ -8,6 +8,7 @@ use Phpactor\Extension\LanguageServerBridge\Converter\RangeConverter;
 use Phpactor\Extension\LanguageServerRename\Model\Exception\CouldNotRename;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEdit;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEditsMap;
+use Phpactor\Extension\LanguageServerRename\Model\RenameResult;
 use Phpactor\Extension\LanguageServerRename\Model\Renamer;
 use Phpactor\Extension\LanguageServerRename\Util\LocatedTextEditConverter;
 use Phpactor\LanguageServerProtocol\PrepareRenameParams;
@@ -47,7 +48,7 @@ class RenameHandler implements Handler, CanRegisterCapabilities
         $this->documentLocator = $documentLocator;
     }
 
-    
+
     public function methods(): array
     {
         return [
@@ -67,19 +68,22 @@ class RenameHandler implements Handler, CanRegisterCapabilities
             $count = 0;
 
             try {
-                foreach ($this->renamer->rename(
+                $rename = $this->renamer->rename(
                     $document,
                     PositionConverter::positionToByteOffset(
                         $params->position,
                         (string)$document
                     ),
                     $params->newName
-                ) as $result) {
+                );
+                foreach ($rename as $result) {
                     if ($count++ === 10) {
                         yield delay(1);
                     }
                     $locatedEdits[] = $result;
                 }
+
+                return $this->resultToWorkspaceEdit($locatedEdits, $rename->getReturn());
             } catch (CouldNotRename $couldNotRename) {
                 $this->clientApi->window()->showMessage()->error(sprintf(
                     $couldNotRename->getMessage()
@@ -87,8 +91,6 @@ class RenameHandler implements Handler, CanRegisterCapabilities
 
                 return new WorkspaceEdit(null, []);
             }
-
-            return $this->resultToWorkspaceEdit($locatedEdits);
         });
     }
 
@@ -121,10 +123,11 @@ class RenameHandler implements Handler, CanRegisterCapabilities
     /**
      * @param LocatedTextEdit[] $locatedEdits
      */
-    private function resultToWorkspaceEdit(array $locatedEdits): WorkspaceEdit
+    private function resultToWorkspaceEdit(array $locatedEdits, ?RenameResult $renameResult): WorkspaceEdit
     {
         return $this->converter->toWorkspaceEdit(
-            LocatedTextEditsMap::fromLocatedEdits($locatedEdits)
+            LocatedTextEditsMap::fromLocatedEdits($locatedEdits),
+            $renameResult
         );
     }
 }
diff --git a/lib/Extension/LanguageServerRename/LanguageServerRenameWorseExtension.php b/lib/Extension/LanguageServerRename/LanguageServerRenameWorseExtension.php
index 749a05f3..73dc4e36 100644
--- a/lib/Extension/LanguageServerRename/LanguageServerRenameWorseExtension.php
+++ b/lib/Extension/LanguageServerRename/LanguageServerRenameWorseExtension.php
@@ -9,6 +9,7 @@ use Phpactor\Container\Extension;
 use Phpactor\Extension\ClassToFile\ClassToFileExtension;
 use Phpactor\Extension\LanguageServerReferenceFinder\Adapter\Indexer\WorkspaceUpdateReferenceFinder;
 use Phpactor\Extension\LanguageServerRename\Adapter\ClassMover\FileRenamer as PhpactorFileRenamer;
+use Phpactor\Extension\LanguageServerRename\Adapter\ClassToFile\ClassToFileNameToUriConverter;
 use Phpactor\Extension\LanguageServerRename\Adapter\ClassToFile\ClassToFileUriToNameConverter;
 use Phpactor\Extension\LanguageServerRename\Adapter\ReferenceFinder\ClassMover\ClassRenamer;
 use Phpactor\Extension\LanguageServerRename\Adapter\ReferenceFinder\MemberRenamer;
@@ -31,7 +32,6 @@ class LanguageServerRenameWorseExtension implements Extension
     public const TAG_RENAMER = 'language_server_rename.renamer';
     public const PARAM_FILE_RENAME_LISTENER = 'language_server_rename.file_rename_listener';
 
-    
     public function load(ContainerBuilder $container): void
     {
         $container->register(VariableRenamer::class, function (Container $container) {
@@ -59,6 +59,7 @@ class LanguageServerRenameWorseExtension implements Extension
 
         $container->register(ClassRenamer::class, function (Container $container) {
             return new ClassRenamer(
+                new ClassToFileNameToUriConverter($container->get(ClassToFileExtension::SERVICE_CONVERTER)),
                 $container->get(DefinitionAndReferenceFinder::class),
                 $container->get(TextDocumentLocator::class),
                 $container->get('worse_reflection.tolerant_parser'),
@@ -94,7 +95,7 @@ class LanguageServerRenameWorseExtension implements Extension
         });
     }
 
-    
+
     public function configure(Resolver $schema): void
     {
     }
diff --git a/lib/Extension/LanguageServerRename/Model/Exception/CouldNotConvertClassToUri.php b/lib/Extension/LanguageServerRename/Model/Exception/CouldNotConvertClassToUri.php
new file mode 100644
index 00000000..d058d8be
--- /dev/null
+++ b/lib/Extension/LanguageServerRename/Model/Exception/CouldNotConvertClassToUri.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Phpactor\Extension\LanguageServerRename\Model\Exception;
+
+use RuntimeException;
+
+class CouldNotConvertClassToUri extends RuntimeException
+{
+}
diff --git a/lib/Extension/LanguageServerRename/Model/NameToUriConverter.php b/lib/Extension/LanguageServerRename/Model/NameToUriConverter.php
new file mode 100644
index 00000000..bf3f4599
--- /dev/null
+++ b/lib/Extension/LanguageServerRename/Model/NameToUriConverter.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Phpactor\Extension\LanguageServerRename\Model;
+
+use Phpactor\Extension\LanguageServerRename\Model\Exception\CouldNotConvertUriToClass;
+use Phpactor\TextDocument\TextDocumentUri;
+
+interface NameToUriConverter
+{
+    /**
+     * @throws CouldNotConvertUriToClass
+     */
+    public function convert(string $uri): TextDocumentUri;
+}
diff --git a/lib/Extension/LanguageServerRename/Model/RenameResult.php b/lib/Extension/LanguageServerRename/Model/RenameResult.php
new file mode 100644
index 00000000..1dc6630f
--- /dev/null
+++ b/lib/Extension/LanguageServerRename/Model/RenameResult.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Phpactor\Extension\LanguageServerRename\Model;
+
+use Phpactor\TextDocument\TextDocumentUri;
+
+class RenameResult
+{
+    private TextDocumentUri $oldUri;
+
+    private TextDocumentUri $newUri;
+
+    public function __construct(TextDocumentUri $oldUri, TextDocumentUri $newUri)
+    {
+        $this->oldUri = $oldUri;
+        $this->newUri = $newUri;
+    }
+
+    public function oldUri(): TextDocumentUri
+    {
+        return $this->oldUri;
+    }
+
+    public function newUri(): TextDocumentUri
+    {
+        return $this->newUri;
+    }
+}
diff --git a/lib/Extension/LanguageServerRename/Model/Renamer/ChainRenamer.php b/lib/Extension/LanguageServerRename/Model/Renamer/ChainRenamer.php
index d4586c21..78113c2b 100644
--- a/lib/Extension/LanguageServerRename/Model/Renamer/ChainRenamer.php
+++ b/lib/Extension/LanguageServerRename/Model/Renamer/ChainRenamer.php
@@ -28,13 +28,14 @@ class ChainRenamer implements Renamer
         return null;
     }
 
-    
+
     public function rename(TextDocument $textDocument, ByteOffset $offset, string $newName): Generator
     {
         foreach ($this->renamers as $renamer) {
             if (null !== ($range = $renamer->getRenameRange($textDocument, $offset))) {
-                yield from $renamer->rename($textDocument, $offset, $newName);
-                return;
+                $rename = $renamer->rename($textDocument, $offset, $newName);
+                yield from $rename;
+                return $rename->getReturn();
             }
         }
     }
diff --git a/lib/Extension/LanguageServerRename/Util/LocatedTextEditConverter.php b/lib/Extension/LanguageServerRename/Util/LocatedTextEditConverter.php
index 5d1f7eaa..8698ff50 100644
--- a/lib/Extension/LanguageServerRename/Util/LocatedTextEditConverter.php
+++ b/lib/Extension/LanguageServerRename/Util/LocatedTextEditConverter.php
@@ -4,6 +4,8 @@ namespace Phpactor\Extension\LanguageServerRename\Util;
 
 use Phpactor\Extension\LanguageServerBridge\Converter\TextEditConverter;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEditsMap;
+use Phpactor\Extension\LanguageServerRename\Model\RenameResult;
+use Phpactor\LanguageServerProtocol\RenameFile;
 use Phpactor\LanguageServerProtocol\TextDocumentEdit;
 use Phpactor\LanguageServerProtocol\VersionedTextDocumentIdentifier;
 use Phpactor\LanguageServerProtocol\WorkspaceEdit;
@@ -22,7 +24,7 @@ final class LocatedTextEditConverter
         $this->locator = $locator;
     }
 
-    public function toWorkspaceEdit(LocatedTextEditsMap $map): WorkspaceEdit
+    public function toWorkspaceEdit(LocatedTextEditsMap $map, ?RenameResult $renameResult = null): WorkspaceEdit
     {
         $documentEdits = [];
         foreach ($map->toLocatedTextEdits() as $result) {
@@ -38,6 +40,15 @@ final class LocatedTextEditConverter
                 )
             );
         }
+
+        if (null !== $renameResult) {
+            $documentEdits[] = new RenameFile(
+                'rename',
+                $renameResult->oldUri(),
+                $renameResult->newUri(),
+            );
+        }
+
         return new WorkspaceEdit(null, $documentEdits);
     }
 
diff --git a/lib/WorseReflection/Core/Inference/SymbolContextResolver.php b/lib/WorseReflection/Core/Inference/SymbolContextResolver.php
index d95a49a1..742074ad 100644
--- a/lib/WorseReflection/Core/Inference/SymbolContextResolver.php
+++ b/lib/WorseReflection/Core/Inference/SymbolContextResolver.php
@@ -394,7 +394,7 @@ class SymbolContextResolver
 
     private function resolveParameter(Frame $frame, Parameter $node): SymbolContext
     {
-        /** @var MethodDeclaration $method */
+        /** @var MethodDeclaration|null $method */
         $method = $node->getFirstAncestor(AnonymousFunctionCreationExpression::class, MethodDeclaration::class);
 
         if ($method instanceof MethodDeclaration) {
@@ -413,11 +413,15 @@ class SymbolContextResolver
 
         if ($typeDeclaration instanceof Token) {
             $type = TypeFactory::fromStringWithReflector(
-                $typeDeclaration->getText($node->getFileContents()),
+                (string)$typeDeclaration->getText($node->getFileContents()),
                 $this->reflector,
             );
         }
 
+        if ($node->questionToken) {
+            $type = TypeFactory::nullable($type);
+        }
+
         $value = null;
         if ($node->default) {
             $value = $this->_resolveNode($frame, $node->default)->value();
@@ -724,7 +728,7 @@ class SymbolContextResolver
         $classSymbolContext = $this->_resolveNode($frame, $classNode);
 
         return $this->symbolFactory->context(
-            $node->name->getText($node->getFileContents()),
+            (string)$node->name->getText($node->getFileContents()),
             $node->name->getStartPosition(),
             $node->name->getEndPosition(),
             [
@@ -738,7 +742,7 @@ class SymbolContextResolver
     {
         assert($node instanceof MemberAccessExpression || $node instanceof ScopedPropertyAccessExpression);
 
-        $memberName = $node->memberName->getText($node->getFileContents());
+        $memberName = NodeUtil::nameFromTokenOrNode($node, $node->memberName);
         $memberType = $node->getParent() instanceof CallExpression ? Symbol::METHOD : Symbol::PROPERTY;
 
         if ($node->memberName instanceof Node) {
diff --git a/lib/WorseReflection/Core/Util/NodeUtil.php b/lib/WorseReflection/Core/Util/NodeUtil.php
index 00354d98..1faea760 100644
--- a/lib/WorseReflection/Core/Util/NodeUtil.php
+++ b/lib/WorseReflection/Core/Util/NodeUtil.php
@@ -9,6 +9,21 @@ use Microsoft\PhpParser\Token;
 
 class NodeUtil
 {
+    /**
+     * @param Token|Node|mixed $nodeOrToken
+     */
+    public static function nameFromTokenOrNode(Node $node, $nodeOrToken): string
+    {
+        if ($nodeOrToken instanceof Token) {
+            return (string)$nodeOrToken->getText($node->getFileContents());
+        }
+        if ($nodeOrToken instanceof Node) {
+            return (string)$nodeOrToken->getText();
+        }
+
+        return '';
+    }
+
     /**
      * @param Token|QualifiedName|mixed $name
      */


Test patch:
diff --git a/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ClassToFile/ClassToFileNameToUriConverterTest.php b/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ClassToFile/ClassToFileNameToUriConverterTest.php
new file mode 100644
index 00000000..31768fa5
--- /dev/null
+++ b/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ClassToFile/ClassToFileNameToUriConverterTest.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Phpactor\Extension\LanguageServerRename\Tests\Unit\Adapter\ClassToFile;
+
+use Phpactor\ClassFileConverter\Adapter\Simple\SimpleClassToFile;
+use Phpactor\Extension\LanguageServerRename\Adapter\ClassToFile\ClassToFileNameToUriConverter;
+use Phpactor\Extension\LanguageServerRename\Tests\IntegrationTestCase;
+use Phpactor\TextDocument\TextDocumentUri;
+
+class ClassToFileNameToUriConverterTest extends IntegrationTestCase
+{
+    public function testConvert(): void
+    {
+        $this->workspace()->put('Foo.php', '<?php class Foo {}');
+
+        $converter = new ClassToFileNameToUriConverter(new SimpleClassToFile($this->workspace()->path()));
+
+        $uri = $converter->convert('Foo');
+
+        self::assertInstanceOf(TextDocumentUri::class, $uri);
+        self::assertEquals($this->workspace()->path('Foo.php'), $uri->path());
+    }
+}
diff --git a/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ReferenceFinder/ClassMover/ClassRenamerTest.php b/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ReferenceFinder/ClassMover/ClassRenamerTest.php
index 31640b28..9217487d 100644
--- a/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ReferenceFinder/ClassMover/ClassRenamerTest.php
+++ b/lib/Extension/LanguageServerRename/Tests/Unit/Adapter/ReferenceFinder/ClassMover/ClassRenamerTest.php
@@ -8,19 +8,27 @@ use Phpactor\ClassMover\ClassMover;
 use Phpactor\Extension\LanguageServerRename\Adapter\ReferenceFinder\ClassMover\ClassRenamer;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEdits;
 use Phpactor\Extension\LanguageServerRename\Model\LocatedTextEditsMap;
+use Phpactor\Extension\LanguageServerRename\Model\NameToUriConverter;
 use Phpactor\Extension\LanguageServerRename\Tests\Unit\Adapter\ReferenceFinder\ReferenceRenamerIntegrationTestCase;
 use Phpactor\Extension\LanguageServerRename\Tests\Util\OffsetExtractor;
 use Phpactor\TextDocument\TextDocument;
 use Phpactor\TextDocument\TextDocumentBuilder;
 use Phpactor\TextDocument\TextDocumentLocator\InMemoryDocumentLocator;
+use Phpactor\TextDocument\TextDocumentUri;
 
 class ClassRenamerTest extends ReferenceRenamerIntegrationTestCase
 {
     /**
      * @dataProvider provideRename
      */
-    public function testRename(string $source, string $newName, string $expected): void
-    {
+    public function testRename(
+        string $oldPath,
+        string $source,
+        string $newName,
+        ?string $newUri,
+        int $expectedEditsCount,
+        ?string $expected
+    ): void {
         $extractor = OffsetExtractor::create()
             ->registerOffset('offset', '<>')
             ->registerOffset('r', '<r>')
@@ -30,17 +38,25 @@ class ClassRenamerTest extends ReferenceRenamerIntegrationTestCase
         $references = $extractor->offsets('r');
 
         $source = $extractor->source();
-        
-        $textDocument = TextDocumentBuilder::create($source)->uri('/foo')->build();
-        
-        $renamer = $this->createRenamer($references, $textDocument);
+
+        $textDocument = TextDocumentBuilder::create($source)->uri($oldPath)->build();
+
+        $renamer = $this->createRenamer('/foo/', $references, $textDocument);
         self::assertNotNull($renamer->getRenameRange($textDocument, $offset));
+        $rename = $renamer->rename($textDocument, $offset, $newName);
 
-        $actualResults = iterator_to_array($renamer->rename($textDocument, $offset, $newName), false);
+        $actualResults = iterator_to_array($rename, false);
+
+        $renameResult = $rename->getReturn();
+        self::assertSame($newUri, $renameResult ? $renameResult->newUri()->__toString() : null);
 
         $edits = LocatedTextEditsMap::fromLocatedEdits($actualResults);
         $locateds = $edits->toLocatedTextEdits();
-        self::assertCount(1, $locateds);
+        self::assertCount($expectedEditsCount, $locateds);
+
+        if (0 === $expectedEditsCount) {
+            return;
+        }
         $located = reset($locateds);
         assert($located instanceof LocatedTextEdits);
         self::assertEquals($expected, $located->textEdits()->apply($source));
@@ -49,63 +65,121 @@ class ClassRenamerTest extends ReferenceRenamerIntegrationTestCase
     public function provideRename(): Generator
     {
         yield 'class' => [
+            '/foo/Class1.php',
             '<?php <r>class Cl<>ass1 { }',
             'Class2',
+            'file:///foo/Class2.php',
+            1,
             '<?php class Class2 { }',
         ];
 
+        yield 'class: no change' => [
+            '/foo/Class1.php',
+            '<?php <r>class Cl<>ass1 { }',
+            'Class1',
+            null,
+            0,
+            null,
+        ];
+
         yield 'interface' => [
+            '/foo/Interface1.php',
             '<?php <r>interface Inter<>face1 { }',
             'Interface2',
+            'file:///foo/Interface2.php',
+            1,
             '<?php interface Interface2 { }',
         ];
 
         yield 'interface: updates implements' => [
+            '/foo/Interface1.php',
             '<?php <r>interface Inter<>face1 { } class Class1 implements Interface1 { }',
             'Interface2',
+            'file:///foo/Interface2.php',
+            1,
             '<?php interface Interface2 { } class Class1 implements Interface2 { }',
         ];
 
         yield 'trait' => [
+            '/foo/Trait1.php',
             '<?php <r>trait Tra<>it1 { }',
             'Trait2',
+            'file:///foo/Trait2.php',
+            1,
             '<?php trait Trait2 { }',
         ];
 
         yield 'trait: updates uses' => [
+            '/foo/Trait1.php',
             '<?php <r>trait Tra<>it1 { } class Class1 { use Trait1; }',
             'Trait2',
+            'file:///foo/Trait2.php',
+            1,
             '<?php trait Trait2 { } class Class1 { use Trait2; }',
         ];
 
         yield 'namespaced class' => [
+            '/foo/Foo/Class1.php',
             '<?php namespace Foo; <r>class Cl<>ass1 { }',
             'Class2',
+            'file:///foo/Foo/Class2.php',
+            1,
             '<?php namespace Foo; class Class2 { }',
         ];
 
         yield 'class reference' => [
+            '/foo/Foo/Class1.php',
             '<?php namespace Foo; <r>class Class1 { } <r>Cla<>ss1::foo();',
             'Class2',
+            'file:///foo/Foo/Class2.php',
+            1,
             '<?php namespace Foo; class Class2 { } Class2::foo();',
         ];
 
         yield 'updates namespaced imported class' => [
+            '/foo/Foo/Class1.php',
             '<?php use Foo\Class1; <r>Cla<>ss1::foo();',
             'Class2',
+            'file:///foo/Foo/Class2.php',
+            1,
             '<?php use Foo\Class2; Class2::foo();',
         ];
 
         yield 'updates imported class' => [
+            '/foo/Class1.php',
             '<?php use Class1; <r>Cla<>ss1::foo();',
             'Class2',
+            'file:///foo/Class2.php',
+            1,
             '<?php use Class2; Class2::foo();',
         ];
     }
 
-    private function createRenamer(array $references, TextDocument $textDocument): ClassRenamer
-    {
+    private function createRenamer(
+        string $namespaceRootDir,
+        array $references,
+        TextDocument $textDocument
+    ): ClassRenamer {
+        $nameToUriConverter = new class($namespaceRootDir) implements NameToUriConverter {
+            private string $namespaceRootDir;
+            public function __construct(
+                string $namespaceRootDir
+            ) {
+                $this->namespaceRootDir = $namespaceRootDir;
+            }
+
+            public function convert(string $className): TextDocumentUri
+            {
+                return TextDocumentUri::fromString(sprintf(
+                    '%s%s.php',
+                    $this->namespaceRootDir,
+                    str_replace('\\', '/', $className),
+                ));
+            }
+        };
+
         return new ClassRenamer(
+            $nameToUriConverter,
             $this->offsetsToReferenceFinder($textDocument, $references),
             InMemoryDocumentLocator::fromTextDocuments([$textDocument]),
             new Parser(),
diff --git a/lib/WorseReflection/Tests/SelfTest/SelfTest.php b/lib/WorseReflection/Tests/SelfTest/SelfTest.php
index ccf18515..74b70ea8 100644
--- a/lib/WorseReflection/Tests/SelfTest/SelfTest.php
+++ b/lib/WorseReflection/Tests/SelfTest/SelfTest.php
@@ -27,6 +27,7 @@ class SelfTest extends IntegrationTestCase
     public function provideSelf(): Generator
     {
         foreach ([
+            'flow',
             'generics',
         ] as $topic) {
             foreach ((array)glob(__DIR__ . '/' . $topic . '/*.test') as $fname) {
diff --git a/lib/WorseReflection/Tests/SelfTest/flow/assert.test b/lib/WorseReflection/Tests/SelfTest/flow/assert.test
new file mode 100644
index 00000000..983e0e41
--- /dev/null
+++ b/lib/WorseReflection/Tests/SelfTest/flow/assert.test
@@ -0,0 +1,9 @@
+<?php
+
+namespace Foo;
+
+function foo($foo) {
+    assert($foo instanceof Bar);
+    wrAssertType('Foo\Bar', $foo);
+}
+
diff --git a/lib/WorseReflection/Tests/SelfTest/flow/function_params.test b/lib/WorseReflection/Tests/SelfTest/flow/function_params.test
new file mode 100644
index 00000000..e33c393c
--- /dev/null
+++ b/lib/WorseReflection/Tests/SelfTest/flow/function_params.test
@@ -0,0 +1,7 @@
+<?php
+
+function foo(?Foo $foo = null) {
+    wrAssertType('?Foo', $foo);
+}
+
+
diff --git a/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type1.test b/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type1.test
new file mode 100644
index 00000000..f4407b64
--- /dev/null
+++ b/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type1.test
@@ -0,0 +1,11 @@
+<?php
+
+function foo(?string $foo) {
+    if (!$foo) {
+        return;
+    }
+
+    // @todo the null type should be stripped
+    wrAssertType('?string', $foo);
+}
+
diff --git a/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type2.test b/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type2.test
new file mode 100644
index 00000000..0a2fcbfa
--- /dev/null
+++ b/lib/WorseReflection/Tests/SelfTest/flow/if_remove_null_type2.test
@@ -0,0 +1,11 @@
+<?php
+
+function foo(?Foo $foo = null) {
+    if (null === $foo) {
+        return;
+    }
+
+    // @todo the null type should be stripped
+    wrAssertType('?Foo', $foo);
+}
+