diff --git a/src/Psalm/CodeLocation/DocblockTypeLocation.php b/src/Psalm/CodeLocation/DocblockTypeLocation.php index 9cb2c4809..85812c55c 100644 --- a/src/Psalm/CodeLocation/DocblockTypeLocation.php +++ b/src/Psalm/CodeLocation/DocblockTypeLocation.php @@ -5,12 +5,6 @@ use PhpParser; class DocblockTypeLocation extends \Psalm\CodeLocation { - /** @var int */ - public $raw_file_start; - - /** @var int */ - public $raw_file_end; - /** @var int */ public $raw_line_number; diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 9362a8b2c..7b7d36dc8 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1353,6 +1353,47 @@ class Codebase continue; } + $extra_edits = []; + + $insertion_text = Type::getStringFromFQCLN( + $storage->name, + $aliases && $aliases->namespace ? $aliases->namespace : null, + $aliases ? $aliases->uses_flipped : [], + null + ); + + if ($aliases + && $aliases->namespace + && $insertion_text === '\\' . $storage->name + && $aliases->namespace_first_stmt_start + ) { + $file_contents = $this->getFileContents($file_path); + + $class_name = \preg_replace('/^.*\\\/', '', $storage->name); + + if ($aliases->uses_end) { + $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); + $extra_edits[] = new \LanguageServerProtocol\TextEdit( + new Range( + $position, + $position + ), + \PHP_EOL . 'use ' . $storage->name . ';' + ); + } else { + $position = self::getPositionFromOffset($aliases->namespace_first_stmt_start, $file_contents); + $extra_edits[] = new \LanguageServerProtocol\TextEdit( + new Range( + $position, + $position + ), + 'use ' . $storage->name . ';' . \PHP_EOL . \PHP_EOL + ); + } + + $insertion_text = $class_name; + } + $completion_items[] = new \LanguageServerProtocol\CompletionItem( $storage->name, \LanguageServerProtocol\CompletionItemKind::CLASS_, @@ -1360,12 +1401,9 @@ class Codebase null, null, $storage->name, - Type::getStringFromFQCLN( - $storage->name, - $aliases && $aliases->namespace ? $aliases->namespace : null, - $aliases ? $aliases->uses_flipped : [], - null - ) + $insertion_text, + null, + $extra_edits ); } @@ -1375,9 +1413,12 @@ class Codebase private static function getPositionFromOffset(int $offset, string $file_contents) : Position { $file_contents = substr($file_contents, 0, $offset); + + $before_newline_count = strrpos($file_contents, "\n", $offset - strlen($file_contents)); + return new Position( substr_count($file_contents, "\n"), - $offset - (int)strrpos($file_contents, "\n", strlen($file_contents)) + $offset - (int)$before_newline_count - 1 ); } diff --git a/src/Psalm/Internal/Visitor/ReflectorVisitor.php b/src/Psalm/Internal/Visitor/ReflectorVisitor.php index 0defe3db2..1a82e82a1 100644 --- a/src/Psalm/Internal/Visitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/Visitor/ReflectorVisitor.php @@ -240,7 +240,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $this->aliases->uses_start = (int) $node->getAttribute('startFilePos'); } - $this->aliases->uses_end = (int) $node->getAttribute('endFilePos'); + $this->aliases->uses_end = (int) $node->getAttribute('endFilePos') + 1; } elseif ($node instanceof PhpParser\Node\Stmt\GroupUse) { $use_prefix = implode('\\', $node->prefix->parts); @@ -270,7 +270,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $this->aliases->uses_start = (int) $node->getAttribute('startFilePos'); } - $this->aliases->uses_end = (int) $node->getAttribute('endFilePos'); + $this->aliases->uses_end = (int) $node->getAttribute('endFilePos') + 1; } elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) { if ($this->skip_if_descendants) { return; diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 687faa045..e15fc4849 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -688,7 +688,7 @@ class CompletionTest extends \Psalm\Tests\TestCase /** * @return void */ - public function testCompletionOnNewExceptionWithNamespace() + public function testCompletionOnNewExceptionWithNamespaceNoUse() { $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; @@ -724,7 +724,15 @@ class CompletionTest extends \Psalm\Tests\TestCase $this->assertCount(1, $completion_items); $this->assertSame('Exception', $completion_items[0]->label); - $this->assertSame('\Exception', $completion_items[0]->insertText); + $this->assertSame('Exception', $completion_items[0]->insertText); + + $this->assertNotNull($completion_items[0]->additionalTextEdits); + $this->assertCount(1, $completion_items[0]->additionalTextEdits); + $this->assertSame('use Exception;' . \PHP_EOL . \PHP_EOL, $completion_items[0]->additionalTextEdits[0]->newText); + $this->assertSame(3, $completion_items[0]->additionalTextEdits[0]->range->start->line); + $this->assertSame(16, $completion_items[0]->additionalTextEdits[0]->range->start->character); + $this->assertSame(3, $completion_items[0]->additionalTextEdits[0]->range->end->line); + $this->assertSame(16, $completion_items[0]->additionalTextEdits[0]->range->end->character); } /** @@ -769,6 +777,14 @@ class CompletionTest extends \Psalm\Tests\TestCase $completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php'); $this->assertCount(5, $completion_items); + + $this->assertNotNull($completion_items[0]->additionalTextEdits); + $this->assertCount(1, $completion_items[0]->additionalTextEdits); + $this->assertSame(\PHP_EOL . 'use ArrayObject;', $completion_items[0]->additionalTextEdits[0]->newText); + $this->assertSame(3, $completion_items[0]->additionalTextEdits[0]->range->start->line); + $this->assertSame(44, $completion_items[0]->additionalTextEdits[0]->range->start->character); + $this->assertSame(3, $completion_items[0]->additionalTextEdits[0]->range->end->line); + $this->assertSame(44, $completion_items[0]->additionalTextEdits[0]->range->end->character); } /**