1
0
Fork 0

Publish errors as diagnostics, improve tests

pull/7/head v2.1.0
Felix Becker 2016-09-02 21:13:30 +02:00
parent 57604e61f1
commit db28e22378
11 changed files with 408 additions and 10 deletions

6
fixtures/InvalidFile.php Normal file
View File

@ -0,0 +1,6 @@
<?php
interface class
{
}

23
fixtures/Symbols.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace TestNamespace;
class TestClass
{
public $testProperty;
public function testMethod($testParameter)
{
$testVariable = 123;
}
}
trait TestTrait
{
}
interface TestInterface
{
}

View File

@ -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
]
)));
}
}

24
src/LanguageClient.php Normal file
View File

@ -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);
}
}

View File

@ -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);
} }
/** /**

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
} }

View File

@ -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;
} }
} }

View File

@ -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,

View File

@ -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));
}
}