parent
57604e61f1
commit
db28e22378
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
interface class
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TestNamespace;
|
||||||
|
|
||||||
|
class TestClass
|
||||||
|
{
|
||||||
|
public $testProperty;
|
||||||
|
|
||||||
|
public function testMethod($testParameter)
|
||||||
|
{
|
||||||
|
$testVariable = 123;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait TestTrait
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Client;
|
||||||
|
|
||||||
|
use AdvancedJsonRpc\Notification as NotificationBody;
|
||||||
|
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||||
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
use LanguageServer\ProtocolWriter;
|
||||||
|
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, Message};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides method handlers for all textDocument/* methods
|
||||||
|
*/
|
||||||
|
class TextDocument
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ProtocolWriter
|
||||||
|
*/
|
||||||
|
private $protocolWriter;
|
||||||
|
|
||||||
|
public function __construct(ProtocolWriter $protocolWriter)
|
||||||
|
{
|
||||||
|
$this->protocolWriter = $protocolWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||||
|
*
|
||||||
|
* @param string $uri
|
||||||
|
* @param Diagnostic[] $diagnostics
|
||||||
|
*/
|
||||||
|
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||||
|
{
|
||||||
|
$this->protocolWriter->write(new Message(new NotificationBody(
|
||||||
|
'textDocument/publishDiagnostics',
|
||||||
|
(object)[
|
||||||
|
'uri' => $uri,
|
||||||
|
'diagnostics' => $diagnostics
|
||||||
|
]
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use LanguageServer\Client\TextDocument;
|
||||||
|
|
||||||
|
class LanguageClient
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handles textDocument/* methods
|
||||||
|
*
|
||||||
|
* @var Client\TextDocument
|
||||||
|
*/
|
||||||
|
public $textDocument;
|
||||||
|
|
||||||
|
private $protocolWriter;
|
||||||
|
|
||||||
|
public function __construct(ProtocolWriter $writer)
|
||||||
|
{
|
||||||
|
$this->protocolWriter = $writer;
|
||||||
|
$this->textDocument = new TextDocument($writer);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,20 @@
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use LanguageServer\Server\TextDocument;
|
||||||
use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message};
|
use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message};
|
||||||
use LanguageServer\Protocol\InitializeResult;
|
use LanguageServer\Protocol\InitializeResult;
|
||||||
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody};
|
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody};
|
||||||
|
|
||||||
class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Handles textDocument/* method calls
|
||||||
|
*
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
public $textDocument;
|
public $textDocument;
|
||||||
|
|
||||||
public $telemetry;
|
public $telemetry;
|
||||||
public $window;
|
public $window;
|
||||||
public $workspace;
|
public $workspace;
|
||||||
|
@ -17,6 +24,7 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
|
|
||||||
private $protocolReader;
|
private $protocolReader;
|
||||||
private $protocolWriter;
|
private $protocolWriter;
|
||||||
|
private $client;
|
||||||
|
|
||||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||||
{
|
{
|
||||||
|
@ -47,7 +55,8 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$this->protocolWriter = $writer;
|
$this->protocolWriter = $writer;
|
||||||
$this->textDocument = new TextDocumentManager();
|
$this->client = new LanguageClient($writer);
|
||||||
|
$this->textDocument = new Server\TextDocument($this->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use AdvancedJsonRpc\Notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||||
|
*/
|
||||||
|
class PublishDiagnosticsNotification extends Notification
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PublishDiagnosticsParams
|
||||||
|
*/
|
||||||
|
public $params;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $uri
|
||||||
|
* @param Diagnostic[] $diagnostics
|
||||||
|
*/
|
||||||
|
public function __construct(string $uri, array $diagnostics)
|
||||||
|
{
|
||||||
|
$this->method = 'textDocument/publishDiagnostics';
|
||||||
|
$this->params = $params;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol\TextDocument;
|
||||||
|
|
||||||
|
use LanguageServer\Protocol\Params;
|
||||||
|
|
||||||
|
class PublishDiagnosticsParams extends Params
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The URI for which diagnostic information is reported.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of diagnostic information items.
|
||||||
|
*
|
||||||
|
* @var LanguageServer\Protocol\Diagnostic[]
|
||||||
|
*/
|
||||||
|
public $diagnostics;
|
||||||
|
}
|
|
@ -10,4 +10,12 @@ class TextDocumentIdentifier
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public $uri;
|
public $uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $uri The text document's URI.
|
||||||
|
*/
|
||||||
|
public function __construct(string $uri = null)
|
||||||
|
{
|
||||||
|
$this->uri = $uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier};
|
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder};
|
||||||
|
use LanguageServer\Protocol\{
|
||||||
|
TextDocumentItem,
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
VersionedTextDocumentIdentifier,
|
||||||
|
Diagnostic,
|
||||||
|
DiagnosticSeverity,
|
||||||
|
Range,
|
||||||
|
Position
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all textDocument/* methods
|
* Provides method handlers for all textDocument/* methods
|
||||||
*/
|
*/
|
||||||
class TextDocumentManager
|
class TextDocument
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var PhpParser\Parser
|
* @var PhpParser\Parser
|
||||||
|
@ -23,8 +32,16 @@ class TextDocumentManager
|
||||||
*/
|
*/
|
||||||
private $asts;
|
private $asts;
|
||||||
|
|
||||||
public function __construct()
|
/**
|
||||||
|
* The lanugage client object to call methods on the client
|
||||||
|
*
|
||||||
|
* @var LanguageServer\LanguageClient
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(LanguageClient $client)
|
||||||
{
|
{
|
||||||
|
$this->client = $client;
|
||||||
$lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]);
|
$lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]);
|
||||||
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]);
|
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]);
|
||||||
}
|
}
|
||||||
|
@ -74,20 +91,39 @@ class TextDocumentManager
|
||||||
$this->updateAst($textDocument->uri, $contentChanges[0]->text);
|
$this->updateAst($textDocument->uri, $contentChanges[0]->text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-parses a source file, updates the AST and reports parsing errors that may occured as diagnostics
|
||||||
|
*
|
||||||
|
* @param string $uri The URI of the source file
|
||||||
|
* @param string $content The new content of the source file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
private function updateAst(string $uri, string $content)
|
private function updateAst(string $uri, string $content)
|
||||||
{
|
{
|
||||||
$stmts = $this->parser->parse($content);
|
$stmts = $this->parser->parse($content);
|
||||||
// TODO report errors as diagnostics
|
$diagnostics = [];
|
||||||
// foreach ($parser->getErrors() as $error) {
|
foreach ($this->parser->getErrors() as $error) {
|
||||||
// error_log($error->getMessage());
|
$diagnostic = new Diagnostic();
|
||||||
// }
|
$diagnostic->range = new Range(
|
||||||
|
new Position($error->getStartLine() - 1, $error->hasColumnInfo() ? $error->getStartColumn($content) - 1 : 0),
|
||||||
|
new Position($error->getEndLine() - 1, $error->hasColumnInfo() ? $error->getEndColumn($content) - 1 : 0)
|
||||||
|
);
|
||||||
|
$diagnostic->severity = DiagnosticSeverity::ERROR;
|
||||||
|
$diagnostic->source = 'php';
|
||||||
|
// Do not include "on line ..." in the error message
|
||||||
|
$diagnostic->message = $error->getRawMessage();
|
||||||
|
$diagnostics[] = $diagnostic;
|
||||||
|
}
|
||||||
|
if (count($diagnostics) > 0) {
|
||||||
|
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
||||||
|
}
|
||||||
// $stmts can be null in case of a fatal parsing error
|
// $stmts can be null in case of a fatal parsing error
|
||||||
if ($stmts) {
|
if ($stmts) {
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
$traverser->addVisitor(new NameResolver);
|
$traverser->addVisitor(new NameResolver);
|
||||||
$traverser->addVisitor(new ColumnCalculator($content));
|
$traverser->addVisitor(new ColumnCalculator($content));
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
|
$this->asts[$uri] = $stmts;
|
||||||
}
|
}
|
||||||
$this->asts[$uri] = $stmts;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,6 +10,7 @@ class SymbolFinder extends NodeVisitorAbstract
|
||||||
{
|
{
|
||||||
const NODE_SYMBOL_KIND_MAP = [
|
const NODE_SYMBOL_KIND_MAP = [
|
||||||
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
||||||
|
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
|
||||||
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
||||||
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
||||||
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, Client, LanguageClient};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolKind, DiagnosticSeverity};
|
||||||
|
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
||||||
|
|
||||||
|
class TextDocumentTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDocumentSymbol()
|
||||||
|
{
|
||||||
|
$textDocument = new Server\TextDocument(new LanguageClient(new MockProtocolStream()));
|
||||||
|
// Trigger parsing of source
|
||||||
|
$textDocumentItem = new TextDocumentItem();
|
||||||
|
$textDocumentItem->uri = 'whatever';
|
||||||
|
$textDocumentItem->languageId = 'php';
|
||||||
|
$textDocumentItem->version = 1;
|
||||||
|
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/Symbols.php');
|
||||||
|
$textDocument->didOpen($textDocumentItem);
|
||||||
|
// Request symbols
|
||||||
|
$result = $textDocument->documentSymbol(new TextDocumentIdentifier('whatever'));
|
||||||
|
$this->assertEquals([
|
||||||
|
[
|
||||||
|
'name' => 'TestNamespace',
|
||||||
|
'kind' => SymbolKind::NAMESPACE,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 23
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestClass',
|
||||||
|
'kind' => SymbolKind::CLASS_,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 4,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 12,
|
||||||
|
'character' => 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'testProperty',
|
||||||
|
'kind' => SymbolKind::PROPERTY,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 11
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 23
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'testMethod',
|
||||||
|
'kind' => SymbolKind::METHOD,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 8,
|
||||||
|
'character' => 4
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 11,
|
||||||
|
'character' => 4
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'testVariable',
|
||||||
|
'kind' => SymbolKind::VARIABLE,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 8
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 20
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestTrait',
|
||||||
|
'kind' => SymbolKind::CLASS_,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 14,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 17,
|
||||||
|
'character' => 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestInterface',
|
||||||
|
'kind' => SymbolKind::INTERFACE,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'whatever',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 19,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 22,
|
||||||
|
'character' => 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => null
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseErrorsArePublishedAsDiagnostics()
|
||||||
|
{
|
||||||
|
$args = null;
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$client->textDocument = new class($args) extends Client\TextDocument {
|
||||||
|
private $args;
|
||||||
|
public function __construct(&$args)
|
||||||
|
{
|
||||||
|
parent::__construct(new MockProtocolStream());
|
||||||
|
$this->args = &$args;
|
||||||
|
}
|
||||||
|
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||||
|
{
|
||||||
|
$this->args = func_get_args();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$textDocument = new Server\TextDocument($client);
|
||||||
|
// Trigger parsing of source
|
||||||
|
$textDocumentItem = new TextDocumentItem();
|
||||||
|
$textDocumentItem->uri = 'whatever';
|
||||||
|
$textDocumentItem->languageId = 'php';
|
||||||
|
$textDocumentItem->version = 1;
|
||||||
|
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/InvalidFile.php');
|
||||||
|
$textDocument->didOpen($textDocumentItem);
|
||||||
|
$this->assertEquals([
|
||||||
|
'whatever',
|
||||||
|
[[
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 10
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 14
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'severity' => DiagnosticSeverity::ERROR,
|
||||||
|
'code' => null,
|
||||||
|
'source' => 'php',
|
||||||
|
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
|
||||||
|
]]
|
||||||
|
], json_decode(json_encode($args), true));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue