diff --git a/fixtures/InvalidFile.php b/fixtures/InvalidFile.php new file mode 100644 index 0000000..a7f2351 --- /dev/null +++ b/fixtures/InvalidFile.php @@ -0,0 +1,6 @@ +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 + ] + ))); + } +} diff --git a/src/LanguageClient.php b/src/LanguageClient.php new file mode 100644 index 0000000..4ee5073 --- /dev/null +++ b/src/LanguageClient.php @@ -0,0 +1,24 @@ +protocolWriter = $writer; + $this->textDocument = new TextDocument($writer); + } +} diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 39eaeed..8766353 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -2,13 +2,20 @@ namespace LanguageServer; +use LanguageServer\Server\TextDocument; use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message}; use LanguageServer\Protocol\InitializeResult; use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody}; class LanguageServer extends \AdvancedJsonRpc\Dispatcher { + /** + * Handles textDocument/* method calls + * + * @var Server\TextDocument + */ public $textDocument; + public $telemetry; public $window; public $workspace; @@ -17,6 +24,7 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher private $protocolReader; private $protocolWriter; + private $client; public function __construct(ProtocolReader $reader, ProtocolWriter $writer) { @@ -47,7 +55,8 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher } }); $this->protocolWriter = $writer; - $this->textDocument = new TextDocumentManager(); + $this->client = new LanguageClient($writer); + $this->textDocument = new Server\TextDocument($this->client); } /** diff --git a/src/Protocol/PublishDiagnosticsNotification.php b/src/Protocol/PublishDiagnosticsNotification.php new file mode 100644 index 0000000..b84fd33 --- /dev/null +++ b/src/Protocol/PublishDiagnosticsNotification.php @@ -0,0 +1,26 @@ +method = 'textDocument/publishDiagnostics'; + $this->params = $params; + } +} diff --git a/src/Protocol/PublishDiagnosticsParams.php b/src/Protocol/PublishDiagnosticsParams.php new file mode 100644 index 0000000..1081187 --- /dev/null +++ b/src/Protocol/PublishDiagnosticsParams.php @@ -0,0 +1,22 @@ +uri = $uri; + } } diff --git a/src/TextDocumentManager.php b/src/Server/TextDocument.php similarity index 61% rename from src/TextDocumentManager.php rename to src/Server/TextDocument.php index f0825ff..54be2a1 100644 --- a/src/TextDocumentManager.php +++ b/src/Server/TextDocument.php @@ -1,15 +1,24 @@ client = $client; $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]); $this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]); } @@ -74,20 +91,39 @@ class TextDocumentManager $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) { $stmts = $this->parser->parse($content); - // TODO report errors as diagnostics - // foreach ($parser->getErrors() as $error) { - // error_log($error->getMessage()); - // } + $diagnostics = []; + foreach ($this->parser->getErrors() as $error) { + $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 if ($stmts) { $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ColumnCalculator($content)); $traverser->traverse($stmts); + $this->asts[$uri] = $stmts; } - $this->asts[$uri] = $stmts; } } diff --git a/src/SymbolFinder.php b/src/SymbolFinder.php index 0d0b90e..3a48948 100644 --- a/src/SymbolFinder.php +++ b/src/SymbolFinder.php @@ -10,6 +10,7 @@ class SymbolFinder extends NodeVisitorAbstract { const NODE_SYMBOL_KIND_MAP = [ Node\Stmt\Class_::class => SymbolKind::CLASS_, + Node\Stmt\Trait_::class => SymbolKind::CLASS_, Node\Stmt\Interface_::class => SymbolKind::INTERFACE, Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE, Node\Stmt\Function_::class => SymbolKind::FUNCTION, diff --git a/tests/Server/TextDocumentTest.php b/tests/Server/TextDocumentTest.php new file mode 100644 index 0000000..d80f45e --- /dev/null +++ b/tests/Server/TextDocumentTest.php @@ -0,0 +1,200 @@ +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)); + } +}