Skip to content
This repository has been archived by the owner on Mar 6, 2022. It is now read-only.

Commit

Permalink
refactor to use ResolveAliasSuggestionCompletor decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
camilledejoye committed Nov 29, 2020
1 parent 3eae721 commit cebd325
Show file tree
Hide file tree
Showing 7 changed files with 419 additions and 249 deletions.
14 changes: 3 additions & 11 deletions lib/Bridge/TolerantParser/ChainTolerantCompletor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

class ChainTolerantCompletor implements Completor
{
use ResolveAliasSuggestionsTrait;

/**
* @var Parser
*/
Expand All @@ -38,7 +36,8 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
$truncatedSource = $this->truncateSource((string) $source, $byteOffset->toInt());

$node = $this->parser->parseSourceFile($truncatedSource)->getDescendantNodeAtPosition(
// the parser requires the byte offset, not the char offset
// use strlen because the parser requires the byte offset, not the char offset
// But we need to recalculate it because we removed trailing spaces when truncating
strlen($truncatedSource)
);

Expand All @@ -55,16 +54,9 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
continue;
}

$importTable = $this->getClassImportTablesForNode($completionNode);
$suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset);

foreach ($suggestions as $suggestion) {
// Trick to avoid any BC break when converting to an array
// https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from
foreach ($this->resolveAliasSuggestions($importTable, $suggestion) as $resolvedSuggestion) {
yield $resolvedSuggestion;
}
}
yield from $suggestions;

$isComplete = $isComplete && $suggestions->getReturn();
}
Expand Down
150 changes: 150 additions & 0 deletions lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

namespace Phpactor\Completion\Bridge\TolerantParser;

use Generator;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\SourceFileNode;
use Microsoft\PhpParser\Parser;
use Microsoft\PhpParser\ResolvedName;
use Phpactor\Completion\Core\Completor;
use Phpactor\Completion\Core\Suggestion;
use Phpactor\Completion\Core\Util\OffsetHelper;
use Phpactor\TextDocument\ByteOffset;
use Phpactor\TextDocument\TextDocument;

final class ResolveAliasSuggestionCompletor implements Completor
{
/**
* @var Completor
*/
private $decorated;

/**
* @var Parser
*/
private $parser;

public function __construct(Completor $decorated, Parser $parser = null)
{
$this->decorated = $decorated;
$this->parser = $parser ?: new Parser();
}

/**
* {@inheritDoc}
*/
public function complete(TextDocument $source, ByteOffset $byteOffset): Generator
{
$importTable = $this->getClassImportTableAtPosition($source, $byteOffset);
$suggestions = $this->decorated->complete($source, $byteOffset);

foreach ($suggestions as $suggestion) {
$resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion);

// Trick to avoid any BC break when converting to an array
// https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from
foreach ($resolvedSuggestions as $resolvedSuggestion) {
yield $resolvedSuggestion;
}
}

return $suggestions->getReturn();
}

private function truncateSource(string $source, int $byteOffset): string
{
// truncate source at byte offset - we don't want the rest of the source
// file contaminating the completion (for example `$foo($<>\n $bar =
// ` will evaluate the Variable node as an expression node with a
// double variable `$\n $bar = `
$truncatedSource = substr($source, 0, $byteOffset);

// determine the last non-whitespace _character_ offset
$characterOffset = OffsetHelper::lastNonWhitespaceCharacterOffset($truncatedSource);

// truncate the source at the character offset
$truncatedSource = mb_substr($source, 0, $characterOffset);

return $truncatedSource;
}

/**
* Add suggestions when a class is already imported with an alias or when a relative name is abailable.
*
* Will update the suggestion to remove the import_name option if already imported.
* Will add a suggestion if the class is imported under an alias.
* Will add a suggestion if part of the namespace is imported (i.e. ORM\Column is a relative name).
*
* @param ResolvedName[] $importTable
*
* @return Suggestion[]
*/
private function resolveAliasSuggestions(array $importTable, Suggestion $suggestion): array
{
if (Suggestion::TYPE_CLASS !== $suggestion->type()) {
return [$suggestion];
}

$suggestionFqcn = $suggestion->nameImport();
$originalName = $suggestion->name();
$originalSnippet = $suggestion->snippet();
$suggestions = [$suggestion->name() => $suggestion];

foreach ($importTable as $alias => $resolvedName) {
$importFqcn = $resolvedName->getFullyQualifiedNameText();

if (0 !== strpos($suggestionFqcn, $importFqcn)) {
continue;
}

$name = $alias.substr($suggestionFqcn, strlen($importFqcn));
$suggestions[$alias] = $suggestion->withoutNameImport()->withName($name);

if ($originalSnippet && $originalName !== $name) {
$snippet = str_replace($originalName, $name, $originalSnippet);
$suggestions[$alias] = $suggestions[$alias]->withSnippet($snippet);
}
}

return array_values($suggestions);
}

/**
* @return ResolvedName[]
*/
private function getClassImportTableAtPosition(TextDocument $source, ByteOffset $byteOffset): array
{
// We only need the closest node to retrieve the import table
// It's not a big deal if it's not the completed node as long as it has
// the same import table
$node = $this->getClosestNodeAtPosition(
$this->parser->parseSourceFile((string) $source),
$byteOffset->toInt(),
);

try {
[$importTable] = $node->getImportTablesForCurrentScope();
} catch (\Exception $e) {
// If the node does not have an import table (SourceFileNode for example)
$importTable = [];
}

return $importTable;
}

private function getClosestNodeAtPosition(SourceFileNode $sourceFileNode, int $position): Node
{
$lastNode = $sourceFileNode;
/** @var Node $node */
foreach ($sourceFileNode->getDescendantNodes() as $node) {
if ($position < $node->getFullStart()) {
return $lastNode;
}

$lastNode = $node;
}

return $lastNode;
}
}
58 changes: 0 additions & 58 deletions lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\SourceFileNode;
use Microsoft\PhpParser\Parser;
use Phpactor\Completion\Bridge\TolerantParser\ResolveAliasSuggestionsTrait;
use Phpactor\Completion\Core\Completor;
use Phpactor\Completion\Core\Completor\NameSearcherCompletor;
use Phpactor\Completion\Core\Suggestion;
use Phpactor\Completion\Core\Util\OffsetHelper;
use Phpactor\ReferenceFinder\NameSearcher;
use Phpactor\ReferenceFinder\Search\NameSearchResult;
use Phpactor\TextDocument\ByteOffset;
use Phpactor\TextDocument\TextDocument;
use Phpactor\WorseReflection\Core\Exception\NotFound;
use Phpactor\WorseReflection\Reflector;

class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Completor
{
use ResolveAliasSuggestionsTrait;

/**
* @var Reflector
*/
Expand Down Expand Up @@ -63,23 +61,26 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
return true;
}

$importTable = $this->getClassImportTablesForNode($node);
$suggestions = $this->completeName($annotation);

foreach ($suggestions as $suggestion) {
if (!$this->isAnAnnotation($suggestion)) {
continue;
}

$resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion);
foreach ($resolvedSuggestions as $resolvedSuggestion) {
yield $resolvedSuggestion->withSnippet($resolvedSuggestion->name().'($1)$0');
}
yield $suggestion;
}

return $suggestions->getReturn();
}

protected function createSuggestionOptions(NameSearchResult $result): array
{
return array_merge(parent::createSuggestionOptions($result), [
'snippet' => (string) $result->name()->head() .'($1)$0',
]);
}

private function truncateSource(string $source, int $byteOffset): string
{
// truncate source at byte offset - we don't want the rest of the source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,43 +109,6 @@ class Foo {}
]
]];


yield 'in a namespace with an import' => [
<<<'EOT'
<?php
namespace App\Annotation;
/**
* @Annotation
*/
class Entity {}
namespace App;
use App\Annotation as APP;
/**
* @Ent<>
*/
class Foo {}
EOT
, [
[
'type' => Suggestion::TYPE_CLASS,
'name' => 'APP\Entity',
'short_description' => 'App\Annotation\Entity',
'snippet' => 'APP\Entity($1)$0',
'name_import' => null,
], [
'type' => Suggestion::TYPE_CLASS,
'name' => 'Entity',
'short_description' => 'App\Annotation\Entity',
'snippet' => 'Entity($1)$0',
'name_import' => 'App\Annotation\Entity',
]
]];

yield 'annotation on a node in the middle of the AST' => [
<<<'EOT'
<?php
Expand Down
Loading

0 comments on commit cebd325

Please sign in to comment.